fdtd-coremem/crates/applications/stacked_cores/scripts/stacked_cores_52xx_plotters.py

156 lines
6.6 KiB
Python

from math import sqrt
import plotly.express as px
from pandas import DataFrame
import scipy.optimize as opt
unit_to_m = lambda u: -17000 + 34000 * u
sweep_1d = lambda points=101: [unit_to_m(x/(points-1)) for x in range(points)]
def plot(name: str, x_name: str, y_series: list):
""" plot y(x), where y values are specified by `y_series` and x is inferred """
df = DataFrame(data={ x_name: sweep_1d(len(y_series)), "y": y_series })
fig = px.line(df, x=x_name, y="y", title=name)
fig.show()
def eval_series(meas: 'ParameterizedMeas', points: list, extract_tx, y_idx: int = -1) -> list:
"""
extract a list of y-value floats from `meas`.
each x value is a tuple of desired M values at which to sample the curve.
e.g. points = [ (None, 1000.0, 2000.0, None) ] samples at M1=1000.0, M2=2000.0,
treating M0 and M3 as dependent values.
`y_idx` specifies which M value should be treated as the dependent value to be computed.
e.g. `y_idx=0` to compute M0.
`extract_tx` is a function mapping one run (list[list[float]] of M values)
to a measured point of the transfer function. e.g. [15000, -15000, 14000] for a 3-core OR gate.
"""
return [sample_all(meas, p, extract_tx)[y_idx] for p in points]
def sample_all(meas: 'ParameterizedMeas', at: tuple, extract_tx) -> tuple:
"""
computes the interpolated M values at the provided `at` coordinate;
effectively fillin in whichever items in `at` are left at `None`
"""
runs = [extract_tx(r) for r in meas.runs()]
distances = [(distance_to(m, at), m) for m in runs]
# interpolated = weighted_sum_of_neighbors_by_inv_distance(distances)
interpolated = interpolate_minl1(at, runs, distances)
print(at, interpolated)
return interpolated
def extract_52xx_tx(meas_rows: list) -> tuple:
"""
extracts a flat list of input/output M mappings from a 52xx run
"""
return (meas_rows[0].m[0], meas_rows[0].m[1], meas_rows[1].m[2], meas_rows[2].m[3])
def interpolate_minl1(at: tuple, runs: list, distances: list) -> tuple:
# let R = `runs`, A = `at`, D = `distances`, x be the weight of each run
# such that the result is R0 x0 + R1 x1 + ...
#
# solve for x
# subject to R0 x0 + R1 x1 + ... = A for the elements of A != None
# minimize D0 x0 + D1 x1 + ...
#
# relevant scipy docs:
# - <https://docs.scipy.org/doc/scipy/tutorial/optimize.html#trust-region-constrained-algorithm-method-trust-constr>
# - <https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.optimize.minimize.html>
fixed_coords = [(i, a) for (i, a) in enumerate(at) if a is not None]
num_fixed_coords = len(fixed_coords)
num_runs = len(runs)
eq_constraints = [[0]*num_runs for _ in range(num_fixed_coords)]
for run_idx, run in enumerate(runs):
for constraint_idx, (coord_idx, _a) in enumerate(fixed_coords):
eq_constraints[constraint_idx][run_idx] = run[coord_idx]
eq_rhs = [a for (i, a) in fixed_coords]
eq_constraint = opt.LinearConstraint(eq_constraints, eq_rhs, eq_rhs)
# constrain the weights to be positive
bounds = opt.Bounds([0]*num_runs, [float("Inf")]*num_runs)
def score(weights: list) -> float:
# function to minimize: D0 x0 + D1 x1 + ...
return sum(w*d[0] for w, d in zip(weights, distances))
# compute the weight of each run
init = [0]*num_runs
res = opt.minimize(score, init, method='trust-constr', constraints=[eq_constraint], bounds=bounds)
run_weights = res.x
# sum the weighted runs
return element_sum([weighted(run, weight) for run, weight in zip(runs, run_weights)])
def interpolate(meas: 'ParameterizedMeas', a0: float, a1: float) -> tuple:
"""
this interpolates a point among four neighboring points in 2d.
the implementation only supports 2d, but the technique is extendable to N dim.
"""
rows = [r.m for r in meas.all_rows()]
distances = [(distance_to(m, (a0, a1)), m) for m in rows]
# a0_below_dist, a0_below_val = min(d for d in distances if d[1][0] <= a0)
# a0_above_dist, a0_above_val = min(d for d in distances if d[1][0] >= a0)
# a1_below_dist, a1_below_val = min(d for d in distances if d[1][1] <= a1)
# a1_above_dist, a1_above_val = min(d for d in distances if d[1][1] >= a1)
a0_below = min((d for d in distances if d[1][0] <= a0), default=None)
a0_above = min((d for d in distances if d[1][0] >= a0), default=None)
a1_below = min((d for d in distances if d[1][1] <= a1), default=None)
a1_above = min((d for d in distances if d[1][1] >= a1), default=None)
neighbors = [a for a in [a0_below, a0_above, a1_below, a1_above] if a is not None]
return weighted_sum_of_neighbors_by_inv_distance(neighbors)
def weighted_sum_of_neighbors_by_inv_distance(neighbors: list) -> tuple:
"""
each neighbor is (distance, value).
return a weighted sum of these neighbors, where lower-distance neighbors are more strongly weighted.
"""
D = sum(a[0] for a in neighbors)
weight_n = lambda n: 1/max(n[0], 1e-3) # non-normalized weight for neighbor
W = sum(weight_n(n) for n in neighbors)
weighted_n = lambda n: weighted(n[1], weight_n(n)/W) # normalized weighted contribution for neighbor
return element_sum([weighted_n(n) for n in neighbors])
def weighted_sum_of_neighbors(neighbors: list) -> tuple:
"""
each neighbor is (distance, value).
return a weighted sum of these neighbors, where lower-distance neighbors are more strongly weighted.
"""
D = sum(a[0] for a in neighbors)
weight_n = lambda n: D - n[0] # non-normalized weight for neighbor
W = sum(weight_n(n) for n in neighbors)
weighted_n = lambda n: weighted(n[1], weight_n(n)/W) # normalized weighted contribution for neighbor
return element_sum([weighted_n(n) for n in neighbors])
def distance_to(p0: tuple, p1: tuple) -> float:
"""
return the L2-norm distance from p0 to p1.
any coordinates set to `None` are ignored.
e.g. `distance_to((1, 2, 3), (None, 4, 5))` is the same as `distance_to((2, 3), (4, 5))`
"""
return sqrt(sum((x0-x1)*(x0-x1) for (x0, x1) in zip(p0, p1) if x0 is not None and x1 is not None))
def element_sum(lists: list) -> list:
"""
given a list[list[float]] where each inner length is of identical length,
returns a list[float] by summing along each axis.
e.g. element_sum([[1, 2], [3, 4], [5, 6]]) gives `[1+2+5, 2+4+6]`
"""
elems = lists[0]
for l in lists[1:]:
for i, e in enumerate(l):
elems[i] += e
return elems
def weighted(l: list, scale: float) -> list:
"""
given list[float], returns a new list[float] where each element is multipled by `scale`
"""
return [e*scale for e in l]