Added load distribution plotting.
Still need to add capacity plots.
This commit is contained in:
parent
bcfc4d4098
commit
cf520371d9
2 changed files with 91 additions and 37 deletions
|
@ -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__':
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue