math-solver / solver /coordinate_solver.py
Cuong2004
Deploy API from GitHub Actions
1993d5c
Raw History Blame Contribute Delete
11.3 kB
"""Coordinate Solver: Numerical & symbolic coordinate solver for geometric equation systems."""
from __future__ import annotations
import logging
import numpy as np
import scipy.optimize
import sympy as sp
from typing import Any, Dict, List, Optional, Tuple
from .constraint_compiler import CompiledSystem
logger = logging.getLogger(__name__)
class CoordinateSolver:
"""Solves compiled equation systems for exact or optimized 2D/3D numerical coordinates."""
def solve(self, system: CompiledSystem, is_3d: bool = False) -> Optional[Dict[str, List[float]]]:
all_vars = []
for v in system.point_vars.values():
all_vars.extend(v)
n_eqs = len(system.equations)
n_vars = len(all_vars)
logger.info(f"[CoordinateSolver] Solving {n_eqs} equations for {n_vars} unknowns (is_3d={is_3d}).")
# Strategy 1: SymPy symbolic
coords = self._try_symbolic(system.equations, all_vars, system.point_vars)
if coords:
return coords
# Strategy 2: Numerical nsolve (for square systems)
if n_eqs == n_vars:
coords = self._try_nsolve(system.equations, all_vars, system.point_vars, n_vars, system.pt_list, is_3d)
if coords:
return coords
# Strategy 3: Scipy least-squares optimization
coords = self._try_lsq(system.equations, all_vars, system.point_vars, n_vars, system.pt_list, is_3d)
if coords:
return coords
# Strategy 4: Global differential evolution optimization
coords = self._try_global(system.equations, all_vars, system.point_vars, n_vars, is_3d)
if coords:
return coords
logger.error("[CoordinateSolver] All solving strategies exhausted.")
return None
def _try_symbolic(
self,
equations: List[sp.Expr],
all_vars: List[sp.Symbol],
point_vars: Dict[str, Tuple[sp.Symbol, sp.Symbol, sp.Symbol]],
) -> Optional[Dict[str, List[float]]]:
if len(all_vars) > 10 or len(equations) != len(all_vars):
return None
try:
solution = sp.solve(equations, all_vars, dict=True)
if solution:
best_res = solution[0]
for candidate in solution:
z_vals = [float(candidate.get(vz, 0.0)) for _, (_, _, vz) in point_vars.items()]
if all(z >= -1e-6 for z in z_vals):
best_res = candidate
break
elif any(z > 1e-6 for z in z_vals):
best_res = candidate
logger.info("[CoordinateSolver] Strategy 1 (SymPy symbolic): SUCCESS.")
return {
pid: [
float(best_res.get(vx, 0.0)),
float(best_res.get(vy, 0.0)),
float(best_res.get(vz, 0.0)) if is_3d else 0.0,
]
for pid, (vx, vy, vz) in point_vars.items()
}
except Exception as e:
logger.debug("[CoordinateSolver] Strategy 1 threw exception: %s", e)
return None
def _try_nsolve(
self,
equations: List[sp.Expr],
all_vars: List[sp.Symbol],
point_vars: Dict[str, Tuple[sp.Symbol, sp.Symbol, sp.Symbol]],
n_vars: int,
pt_list: list,
is_3d: bool,
) -> Optional[Dict[str, List[float]]]:
MAX_NSOLVE_ATTEMPTS = 15
for attempt in range(MAX_NSOLVE_ATTEMPTS):
try:
x0 = []
n_pts = len(pt_list)
for i, p in enumerate(pt_list):
px = float(p.x) if p.x is not None else None
py = float(p.y) if p.y is not None else None
pz = float(p.z) if p.z is not None else None
if is_3d:
if px is not None and py is not None and pz is not None:
x0.extend([px, py, pz])
elif i == 0:
x0.extend([px if px is not None else 0.0, py if py is not None else 0.0, pz if pz is not None else 0.0])
elif i == n_pts - 1 and n_pts >= 4:
x0.extend([px if px is not None else 2.0, py if py is not None else 2.0, pz if pz is not None else 4.0])
else:
angle = 2 * np.pi * i / max(n_pts - 1, 1)
x0.extend([
px if px is not None else 3.0 * np.cos(angle),
py if py is not None else 3.0 * np.sin(angle),
pz if pz is not None else 0.0,
])
else:
angle = 2 * np.pi * i / max(n_pts, 1)
r = 4.0 + (attempt * 0.5)
x0.extend([
px if px is not None else r * np.cos(angle),
py if py is not None else r * np.sin(angle),
0.0,
])
if attempt > 0:
perturbation = np.random.uniform(-1.0, 1.0, size=len(x0))
x0 = [float(v + p) for v, p in zip(x0, perturbation)]
sol = sp.nsolve(equations, all_vars, x0, verify=False, maxsteps=100)
if sol is not None:
res_map = {var: float(sol[i]) for i, var in enumerate(all_vars)}
logger.info(f"[CoordinateSolver] Strategy 2 (nsolve): SUCCESS on attempt {attempt+1}.")
return {
pid: [
float(res_map.get(vx, 0.0)),
float(res_map.get(vy, 0.0)),
float(res_map.get(vz, 0.0)) if is_3d else 0.0,
]
for pid, (vx, vy, vz) in point_vars.items()
}
except Exception:
pass
return None
def _try_lsq(
self,
equations: List[sp.Expr],
all_vars: List[sp.Symbol],
point_vars: Dict[str, Tuple[sp.Symbol, sp.Symbol, sp.Symbol]],
n_vars: int,
pt_list: list,
is_3d: bool,
) -> Optional[Dict[str, List[float]]]:
try:
f_lambdified = sp.lambdify([all_vars], equations, modules=['numpy', 'scipy'])
def residual(x):
try:
res = f_lambdified(x)
return np.array(res, dtype=float).flatten()
except Exception:
return np.full(len(equations), 1e6)
n_pts = len(pt_list)
x0_base = []
for i, p in enumerate(pt_list):
px = float(p.x) if p.x is not None else None
py = float(p.y) if p.y is not None else None
pz = float(p.z) if p.z is not None else None
if is_3d:
if px is not None and py is not None and pz is not None:
x0_base.extend([px, py, pz])
elif i == 0:
x0_base.extend([px if px is not None else 0.0, py if py is not None else 0.0, pz if pz is not None else 0.0])
elif i == n_pts - 1 and n_pts >= 4:
x0_base.extend([px if px is not None else 2.0, py if py is not None else 2.0, pz if pz is not None else 4.0])
else:
angle = 2 * np.pi * i / max(n_pts - 1, 1)
x0_base.extend([
px if px is not None else 3.0 * np.cos(angle),
py if py is not None else 3.0 * np.sin(angle),
pz if pz is not None else 0.0,
])
else:
angle = 2 * np.pi * i / max(n_pts, 1)
x0_base.extend([
px if px is not None else 4.0 * np.cos(angle),
py if py is not None else 4.0 * np.sin(angle),
0.0,
])
best_res = None
best_cost = float('inf')
for attempt in range(8):
if attempt == 0:
x0 = np.array(x0_base, dtype=float)
else:
x0 = np.array(x0_base, dtype=float) + np.random.normal(0, 1.5, len(x0_base))
res = scipy.optimize.least_squares(
residual,
x0,
method='lm' if len(equations) >= n_vars else 'trf',
max_nfev=3000,
ftol=1e-8,
xtol=1e-8,
)
cost = np.sum(res.fun**2)
if cost < best_cost:
best_cost = cost
best_res = res
if best_cost < 1e-4:
break
if best_res is not None and best_cost < 1e-2:
logger.info(f"[CoordinateSolver] Strategy 3 (least_squares): SUCCESS (cost={best_cost:.2e}).")
sol = best_res.x
res_map = {var: float(sol[i]) for i, var in enumerate(all_vars)}
return {
pid: [
float(res_map.get(vx, 0.0)),
float(res_map.get(vy, 0.0)),
float(res_map.get(vz, 0.0)) if is_3d else 0.0,
]
for pid, (vx, vy, vz) in point_vars.items()
}
except Exception as e:
logger.debug("[CoordinateSolver] Strategy 3 failed: %s", e)
return None
def _try_global(
self,
equations: List[sp.Expr],
all_vars: List[sp.Symbol],
point_vars: Dict[str, Tuple[sp.Symbol, sp.Symbol, sp.Symbol]],
n_vars: int,
is_3d: bool,
) -> Optional[Dict[str, List[float]]]:
try:
f_lambdified = sp.lambdify([all_vars], equations, modules=['numpy', 'scipy'])
def loss(x):
try:
res = f_lambdified(x)
arr = np.array(res, dtype=float).flatten()
return float(np.sum(arr**2))
except Exception:
return 1e9
bounds = [(-15.0, 15.0)] * n_vars
res = scipy.optimize.differential_evolution(loss, bounds, maxiter=500, popsize=15, tol=1e-5)
if res.fun < 1e-2:
logger.info(f"[CoordinateSolver] Strategy 4 (diff evolution): SUCCESS (loss={res.fun:.2e}).")
sol = res.x
res_map = {var: float(sol[i]) for i, var in enumerate(all_vars)}
return {
pid: [
float(res_map.get(vx, 0.0)),
float(res_map.get(vy, 0.0)),
float(res_map.get(vz, 0.0)) if is_3d else 0.0,
]
for pid, (vx, vy, vz) in point_vars.items()
}
except Exception as e:
logger.debug("[CoordinateSolver] Strategy 4 failed: %s", e)
return None