Added load distribution plotting.

Still need to add capacity plots.
This commit is contained in:
Michael Whittaker 2021-01-28 22:46:57 -08:00
parent bcfc4d4098
commit cf520371d9
2 changed files with 91 additions and 37 deletions

View file

@ -1,4 +1,5 @@
from typing import Any, Callable, List, NamedTuple, Optional, Tuple from typing import Any, Callable, List, NamedTuple, Optional, Tuple
import math
import unittest import unittest
@ -26,13 +27,20 @@ class Segment:
else: else:
return False return False
def compatible(self, other: 'Segment') -> float: def __hash__(self) -> int:
return self.l.x == other.l.x and self.r.x == other.r.x return hash((self.l, self.r))
def __call__(self, x: float) -> float: def __call__(self, x: float) -> float:
assert self.l.x <= x <= self.r.x assert self.l.x <= x <= self.r.x
return self.slope() * (x - self.l.x) + self.l.y return self.slope() * (x - self.l.x) + self.l.y
def approximately_equal(self, other: 'Segment') -> float:
return (math.isclose(self.l.y, other.l.y, rel_tol=1e-5) and
math.isclose(self.r.y, other.r.y, rel_tol=1e-5))
def compatible(self, other: 'Segment') -> float:
return self.l.x == other.l.x and self.r.x == other.r.x
def slope(self) -> float: def slope(self) -> float:
return (self.r.y - self.l.y) / (self.r.x - self.l.x) return (self.r.y - self.l.y) / (self.r.x - self.l.x)
@ -72,36 +80,16 @@ def max_of_segments(segments: List[Segment]) -> List[Tuple[float, float]]:
assert len({segment.l.x for segment in segments}) == 1 assert len({segment.l.x for segment in segments}) == 1
assert len({segment.r.x for segment in segments}) == 1 assert len({segment.r.x for segment in segments}) == 1
# First, we remove any segments that are subsumed by other segments. # We compute the x-coordinate of every intersection point. We sort the
non_dominated: List[Segment] = [] # x-coordinates and for every x, we compute the highest line at that point.
for segment in segments: xs: List[float] = [0.0, 1.0]
if any(other.above_eq(segment) for other in non_dominated): for (i, s1) in enumerate(segments):
# If this segment is dominated by another, we exclude it. for (j, s2) in enumerate(segments[i + 1:], i + 1):
pass p = s1.intersection(s2)
else: if p is not None:
# Otherwise, we add this segment and remove any that it dominates. xs.append(p.x)
non_dominated = [other xs.sort()
for other in non_dominated return [(x, max(segments, key=lambda s: s(x))(x)) for x in xs]
if not segment.above_eq(other)]
non_dominated.append(segment)
# Next, we start at the leftmost segment and continually jump over to the
# segment with the first intersection.
segment = max(non_dominated, key=lambda segment: segment.l.y)
path: List[Point] = [segment.l]
while True:
intersections: List[Tuple[Point, Segment]] = []
for other in non_dominated:
p = segment.intersection(other)
if p is not None and p.x > path[-1].x:
intersections.append((p, other))
if len(intersections) == 0:
path.append(segment.r)
return [(p.x, p.y) for p in path]
intersection, segment = min(intersections, key=lambda t: t[0].x)
path.append(intersection)
class TestGeometry(unittest.TestCase): class TestGeometry(unittest.TestCase):
@ -231,6 +219,9 @@ class TestGeometry(unittest.TestCase):
s4 = Segment(Point(0, 0.25), Point(1, 0.25)) s4 = Segment(Point(0, 0.25), Point(1, 0.25))
s5 = Segment(Point(0, 0.75), Point(1, 0.75)) s5 = Segment(Point(0, 0.75), Point(1, 0.75))
def is_subset(xs: List[Any], ys: List[Any]) -> bool:
return all(x in ys for x in xs)
for s in [s1, s2, s3, s4, s5]: for s in [s1, s2, s3, s4, s5]:
self.assertEqual(max_of_segments([s]), [s.l, s.r]) self.assertEqual(max_of_segments([s]), [s.l, s.r])
@ -255,8 +246,8 @@ class TestGeometry(unittest.TestCase):
([s1, s2, s5], [(0, 1), (0.25, 0.75), (0.75, 0.75), (1, 1)]), ([s1, s2, s5], [(0, 1), (0.25, 0.75), (0.75, 0.75), (1, 1)]),
] ]
for segments, path in expected: for segments, path in expected:
self.assertEqual(max_of_segments(segments), path, segments) self.assertTrue(is_subset(path, max_of_segments(segments)))
self.assertEqual(max_of_segments(segments[::-1]), path, segments) self.assertTrue(is_subset(path, max_of_segments(segments[::-1])))
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -1,8 +1,12 @@
from . import distribution from . import distribution
from . import geometry
from .distribution import Distribution from .distribution import Distribution
from .expr import Node from .expr import Node
from typing import Dict, Generic, List, Optional, Set, TypeVar from .geometry import Point, Segment
from typing import Dict, Generic, List, Optional, Set, Tuple, TypeVar
import collections import collections
import itertools
import math
import matplotlib import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
@ -69,12 +73,12 @@ class Strategy(Generic[T]):
for (fr, weight) in d.items()) for (fr, weight) in d.items())
def node_load(self, def node_load(self,
x: T, node: Node[T],
read_fraction: Optional[Distribution] = None, read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None) \ write_fraction: Optional[Distribution] = None) \
-> float: -> float:
d = distribution.canonicalize_rw(read_fraction, write_fraction) d = distribution.canonicalize_rw(read_fraction, write_fraction)
return sum(weight * self._node_load(x, fr) return sum(weight * self._node_load(node.x, fr)
for (fr, weight) in d.items()) for (fr, weight) in d.items())
def capacity(self, def capacity(self,
@ -164,6 +168,65 @@ class Strategy(Generic[T]):
read_fraction=read_fraction, read_fraction=read_fraction,
write_fraction=write_fraction) write_fraction=write_fraction)
def plot_load_distribution(self,
filename: str,
nodes: Optional[List[Node[T]]] = None) \
-> None:
fig, ax = plt.subplots()
self.plot_load_distribution_on(ax, nodes)
ax.set_xlabel('Read Fraction')
ax.set_ylabel('Load')
fig.tight_layout()
fig.savefig(filename)
def _group(self, segments: List[Tuple[Segment, T]]) -> Dict[Segment, List[T]]:
groups: Dict[Segment, List[T]] = collections.defaultdict(list)
for segment, x in segments:
match_found = False
for other, xs in groups.items():
if segment.approximately_equal(other):
xs.append(x)
match_found = True
break
if not match_found:
groups[segment].append(x)
return groups
def plot_load_distribution_on(self,
ax: plt.Axes,
nodes: Optional[List[Node[T]]] = None) \
-> None:
nodes = nodes or list(self.nodes)
# We want to plot every node's load distribution. Multiple nodes might
# have the same load distribution, so we group the nodes by their
# distribution. The grouping is a little annoying because two floats
# might not be exactly equal but pretty close.
groups = self._group([
(Segment(Point(0, self.node_load(node, read_fraction=0)),
Point(1, self.node_load(node, read_fraction=1))), node.x)
for node in nodes
])
# Compute and plot the max of all segments. We increase the line
# slightly so it doesn't overlap with the other lines.
path = geometry.max_of_segments(list(groups.keys()))
ax.plot([p[0] for p in path],
[p[1] for p in path],
label='load',
linewidth=4)
for segment, xs in groups.items():
ax.plot([segment.l.x, segment.r.x],
[segment.l.y, segment.r.y],
'--',
label=','.join(str(x) for x in xs),
linewidth=2,
alpha=0.75)
def _node_load(self, x: T, fr: float) -> float: def _node_load(self, x: T, fr: float) -> float:
""" """
_node_load returns the load on x given a fixed read fraction fr. _node_load returns the load on x given a fixed read fraction fr.