quoracle/quorums/viz.py
2021-02-04 11:41:44 -08:00

229 lines
8.8 KiB
Python

from . import distribution
from . import geometry
from .distribution import Distribution
from .expr import Node
from .geometry import Point, Segment
from .quorum_system import Strategy
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, TypeVar
import collections
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
T = TypeVar('T')
def plot_node_load(filename: str,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None):
fig, ax = plt.subplots()
plot_node_load_on(ax, strategy, nodes, read_fraction, write_fraction)
ax.set_xlabel('Node')
ax.set_ylabel('Load')
fig.tight_layout()
fig.savefig(filename)
def plot_node_load_on(ax: plt.Axes,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None):
_plot_node_load_on(ax,
strategy,
nodes or list(strategy.nodes()),
scale=1,
scale_by_node_capacity=True,
read_fraction=read_fraction,
write_fraction=write_fraction)
def plot_node_utilization(filename: str,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None):
fig, ax = plt.subplots()
plot_node_utilization_on(ax, strategy, nodes, read_fraction, write_fraction)
ax.set_xlabel('Node')
ax.set_ylabel('Utilization')
fig.tight_layout()
fig.savefig(filename)
def plot_node_utilization_on(ax: plt.Axes,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None):
_plot_node_load_on(ax,
strategy,
nodes or list(strategy.nodes()),
scale=strategy.capacity(read_fraction, write_fraction),
scale_by_node_capacity=True,
read_fraction=read_fraction,
write_fraction=write_fraction)
def plot_node_throughput(filename: str,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None):
fig, ax = plt.subplots()
plot_node_throughput_on(ax, strategy, nodes, read_fraction, write_fraction)
ax.set_xlabel('Node')
ax.set_ylabel('Throughput')
fig.tight_layout()
fig.savefig(filename)
def plot_node_throughput_on(ax: plt.Axes,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None,
draw_node_capacities: bool = True):
nodes = nodes or list(strategy.nodes())
d = distribution.canonicalize_rw(read_fraction, write_fraction)
fr = sum(weight * fr for (fr, weight) in d.items())
fw = 1 - fr
# TODO(mwhittaker): Explain.
node_limits = [
fr * strategy.x_read_probability[node.x] / node_load +
fw * strategy.x_write_probability[node.x] / node_load
for node in nodes
for node_load in [strategy.node_load(
node,
read_fraction=read_fraction,
write_fraction=write_fraction
)]
]
_plot_node_load_on(ax,
strategy,
nodes,
scale=strategy.capacity(read_fraction, write_fraction),
scale_by_node_capacity=False,
read_fraction=read_fraction,
write_fraction=write_fraction,
node_limits=node_limits if draw_node_capacities else None)
def _plot_node_load_on(ax: plt.Axes,
sigma: Strategy[T],
nodes: List[Node[T]],
scale: float,
scale_by_node_capacity: bool,
read_fraction: Optional[Distribution] = None,
write_fraction: Optional[Distribution] = None,
node_limits: List[float] = None):
d = distribution.canonicalize_rw(read_fraction, write_fraction)
x_list = [node.x for node in nodes]
x_index = {x: i for (i, x) in enumerate(x_list)}
x_ticks = list(range(len(x_list)))
def one_hot(quorum: FrozenSet[T]) -> np.array:
bar_heights = np.zeros(len(x_list))
for x in quorum:
bar_heights[x_index[x]] = 1
return bar_heights
width = 0.8
def plot_quorums(sigma: Dict[FrozenSet[T], float],
fraction: float,
bottoms: np.array,
capacities: np.array,
cmap: matplotlib.colors.Colormap):
for (i, (quorum, weight)) in enumerate(sigma.items()):
bar_heights = scale * fraction * weight * one_hot(quorum)
if scale_by_node_capacity:
bar_heights /= capacities
ax.bar(x_ticks,
bar_heights,
bottom=bottoms,
color=cmap(0.75 - i * 0.5 / len(sigma)),
edgecolor='white', width=width)
for j, (bar_height, bottom) in enumerate(zip(bar_heights, bottoms)):
text = ''.join(str(x) for x in sorted(list(quorum))) # type: ignore
if bar_height != 0:
ax.text(x_ticks[j], bottom + bar_height / 2, text,
ha='center', va='center')
bottoms += bar_heights
# Plot the quorums.
fr = sum(weight * fr for (fr, weight) in d.items())
fw = 1 - fr
read_capacities = np.array([node.read_capacity for node in nodes])
write_capacities = np.array([node.write_capacity for node in nodes])
bottoms = np.zeros(len(x_list))
plot_quorums(sigma.sigma_r, fr, bottoms, read_capacities,
matplotlib.cm.get_cmap('Reds'))
plot_quorums(sigma.sigma_w, fw, bottoms,
write_capacities, matplotlib.cm.get_cmap('Blues'))
# Plot the limits, if there are any.
if node_limits is not None:
for (i, limit) in enumerate(node_limits):
ax.plot([i - width/2, i + width/2], [limit, limit], color='black')
ax.set_xticks(x_ticks)
ax.set_xticklabels(str(x) for x in x_list)
def plot_load_distribution(filename: str,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None):
fig, ax = plt.subplots()
plot_load_distribution_on(ax, strategy, nodes)
ax.set_xlabel('Read Fraction')
ax.set_ylabel('Load')
fig.tight_layout()
fig.savefig(filename)
def _group(segments: Dict[T, Segment]) -> Dict[Segment, List[T]]:
groups: Dict[Segment, List[T]] = collections.defaultdict(list)
for x, segment in segments.items():
matches = (s for s in groups if segment.approximately_equal(s))
groups[next(matches, segment)].append(x)
return groups
def plot_load_distribution_on(ax: plt.Axes,
strategy: Strategy[T],
nodes: Optional[List[Node[T]]] = None):
nodes = nodes or list(strategy.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 = _group({
node.x: Segment(
Point(0, strategy.node_load(node, read_fraction=0)),
Point(1, strategy.node_load(node, read_fraction=1))
)
for node in nodes
})
# Compute and plot the max of all segments. We plot the load first so that
# it lies underneath the node loads.
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)
# We plot the node loads second so that they appear above the load.
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)