Source code for ewoksfluo.math.scan_shape
import re
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from scipy.optimize import least_squares
[docs]
def solve_scan_shape(
num_scan_points: List[int], equations: List[str]
) -> Optional[Dict[str, int]]:
"""Compute the scan shape based on a number of scan points and their parametrization.
:params num_scan_points: A list of scan points.
:param equations: A parametrization of the list of scan points.
:returns: If the solver, a dictionary containing the solved integer values for each variable.
"""
if len(equations) != len(num_scan_points):
raise ValueError("Number of equations and number scan points must be the same")
keys = [f"npoints{i}" for i in range(len(equations))]
equations_dict = dict(zip(keys, equations))
values_dict = dict(zip(keys, num_scan_points))
def estimate_initial_values(nunknowns) -> List[int]:
min_value = min(values_dict.values())
return [max(1.0, min_value ** (1 / nunknowns))] * nunknowns
return solve_nnls_posint(equations_dict, values_dict, estimate_initial_values)
[docs]
def solve_nnls_posint(
equations_dict: Dict[str, str],
values_dict: Dict[str, int],
estimate_initial_values: Optional[Callable[[int], List[int]]] = None,
) -> Optional[Dict[str, int]]:
"""Solve a system of non-linear equations with strictly positive integer constraints, supporting over-determined systems.
:param equations_dict: A dictionary where keys are equation names and values are the equations as strings.
:param values_dict: A dictionary where keys match those in equations_dict and values are the known results.
:returns: If the solver, a dictionary containing the solved integer values for each variable.
:raises ValueError: If the keys in equations_dict and values_dict do not match.
"""
if set(equations_dict.keys()) != set(values_dict.keys()):
raise ValueError("Mismatch between equations and values dictionary keys")
if not equations_dict:
return
unknowns = set()
for eq in equations_dict.values():
unknowns.update(re.findall(r"[a-zA-Z_]\w*", eq))
unknowns -= set(values_dict.keys()) # Remove constants
unknowns = sorted(unknowns)
def residuals(vars: List[float]) -> List[float]:
local_vars = dict(zip(unknowns, vars))
residuals = []
for key, equation in equations_dict.items():
# Evaluate each equation with current variable values
residual = (
eval(equation, {}, {**local_vars, **values_dict}) - values_dict[key]
)
residuals.append(residual)
return residuals
nunknowns = len(unknowns)
if estimate_initial_values:
initial_guess = estimate_initial_values(nunknowns)
else:
initial_guess = [1.0] * nunknowns
bounds = (1, [float("inf")] * nunknowns)
result = least_squares(residuals, initial_guess, bounds=bounds)
final_values = list(map(round, result.x))
final_residuals = residuals(final_values)
if any(final_residuals):
return
return dict(zip(unknowns, final_values))