Source code for neuralfieldmanifold.geometry.torus

import numpy as np
from scipy import optimize

from .results import FitConfig, GeometryFit, validate_cloud


[docs] def fit_torus(points, *, config=None): """Fit a thick elliptical torus to a three-dimensional point cloud. Parameters ---------- points : array_like, shape (n_points, 3) Finite coordinates with at least four points and nonzero extent along each coordinate. config : FitConfig or None Compactness, hole, and angular homogeneity settings. Returns ------- GeometryFit Center, orientation, major radii, tube radius, fit measures, and optimizer status. Distances are in input coordinate units. Notes ----- Uses PCA initialization and bounded robust least squares. Score is the fraction of points contained within the fitted tube. """ config = FitConfig() if config is None else config if not isinstance(config, FitConfig): raise TypeError("config must be a FitConfig") lam = config.regularization hole_ratio = 0.75 if config.hole_ratio is None else config.hole_ratio n_modes = config.angular_modes points = validate_cloud(points) pts = np.asarray(points, dtype=np.float64) N = len(pts) center0 = pts.mean(axis=0) centered = pts - center0 (_, S, Vt) = np.linalg.svd(centered, full_matrices=False) proj = centered @ Vt.T pc_ext = np.array( [ np.percentile(proj[:, i], 97.5) - np.percentile(proj[:, i], 2.5) for i in range(3) ] ) pc_range = np.ptp(proj, axis=0) r_tgt = pc_ext[2] / 2.0 R1_tgt = max(pc_ext[0] / 2.0 - r_tgt, r_tgt * 0.5) R2_tgt = max(pc_ext[1] / 2.0 - r_tgt, r_tgt * 0.5) (u0, v0, n0) = (Vt[0], Vt[1], Vt[2]) def _frame(dn): R1 = np.linalg.norm(dn) + 1e-12 n_ax = dn / R1 u = u0 - np.dot(u0, n_ax) * n_ax u_ax = u / (np.linalg.norm(u) + 1e-12) v_ax = np.cross(n_ax, u_ax) v_ax = v_ax / (np.linalg.norm(v_ax) + 1e-12) return (R1, n_ax, u_ax, v_ax) def _nearest_phi(cu, cv, R1, R2, iters=4): phi = np.arctan2(cv * R1, cu * R2) for _ in range(iters): (cp, sp) = (np.cos(phi), np.sin(phi)) (dx, dy) = (cu - R1 * cp, cv - R2 * sp) g = dx * R1 * sp - dy * R2 * cp h = dx * R1 * cp + dy * R2 * sp + (R1 * sp) ** 2 + (R2 * cp) ** 2 phi -= np.clip(g / (h + 1e-12), -0.5, 0.5) return phi def _dist(c, n_ax, u_ax, v_ax, R1, R2, pts): dp = pts - c h = dp @ n_ax perp = dp - h[:, None] * n_ax (cu, cv) = (perp @ u_ax, perp @ v_ax) phi = _nearest_phi(cu, cv, R1, R2) near = ( (R1 * np.cos(phi))[:, None] * u_ax + (R2 * np.sin(phi))[:, None] * v_ax + c ) return (np.linalg.norm(pts - near, axis=1), phi) def residuals(params): c = params[:3] dn = params[3:6] (R1, n_ax, u_ax, v_ax) = _frame(dn) R2 = R1 * np.exp(params[6]) r = np.exp(params[7]) (d, phi) = _dist(c, n_ax, u_ax, v_ax, R1, R2, pts) coverage = np.maximum(0.0, d - r) w = lam * np.sqrt(N) Rmin = min(R1, R2) hole_overshoot = max(0.0, r - hole_ratio * Rmin) / (Rmin + 1e-12) compact = np.array( [ w * max(0.0, 2 * (R1 + r) - pc_ext[0]) / (pc_ext[0] + 1e-12), w * max(0.0, 2 * (R2 + r) - pc_ext[1]) / (pc_ext[1] + 1e-12), w * max(0.0, 2 * r - pc_ext[2]) / (pc_ext[2] + 1e-12), 3.0 * w * hole_overshoot, ] ) lam_h = 0.3 raw = d - r homog = [] for k in range(1, n_modes + 1): homog.append(lam_h * np.sqrt(N) * np.mean(raw * np.cos(k * phi))) homog.append(lam_h * np.sqrt(N) * np.mean(raw * np.sin(k * phi))) return np.concatenate([coverage, compact, np.array(homog)]) ratio0 = np.clip(R2_tgt / (R1_tgt + 1e-12), 0.1, 1.0) x0 = np.concatenate( [center0, n0 * R1_tgt, [np.log(ratio0)], [np.log(max(r_tgt, 0.001))]] ) lb = np.full_like(x0, -np.inf) ub = np.full_like(x0, np.inf) max_R1 = pc_range[0] / 2.0 (lb[3:6], ub[3:6]) = (-max_R1, max_R1) lb[6] = np.log(0.05) ub[6] = np.log(np.clip(pc_range[1] / (pc_range[0] + 1e-12), 0.05, 1.0)) lb[7] = np.log(0.001) ub[7] = np.log(pc_range[2] / 2.0 + 1e-06) x0 = np.clip(x0, lb + 1e-08, ub - 1e-08) result = optimize.least_squares( residuals, x0, bounds=(lb, ub), loss="huber", f_scale=1.0, max_nfev=6000 ) c = result.x[:3] dn = result.x[3:6] (R1, n_ax, u_ax, v_ax) = _frame(dn) R2 = R1 * np.exp(result.x[6]) r = np.exp(result.x[7]) if abs(np.dot(u_ax, u0)) < abs(np.dot(v_ax, u0)): (R1, R2) = (R2, R1) (u_ax, v_ax) = (v_ax, u_ax) (d, _) = _dist(c, n_ax, u_ax, v_ax, R1, R2, pts) signed = d - r outside_dist = np.maximum(0.0, signed) inside_mask = signed <= 0 dp = pts - c perp = dp - (dp @ n_ax)[:, None] * n_ax ell = (perp @ u_ax / (R1 + 1e-12)) ** 2 + (perp @ v_ax / (R2 + 1e-12)) ** 2 in_hole = (ell < 1.0) & ~inside_mask ss_res = np.sum(outside_dist**2) ss_tot = np.sum(np.sum((pts - pts.mean(axis=0)) ** 2, axis=1)) r_squared = 1.0 - ss_res / (ss_tot + 1e-12) output = dict( center=c, direction=n_ax, u_axis=u_ax, v_axis=v_ax, R1=float(R1), R2=float(R2), minor_radius=float(r), hole_quality=float(1.0 - r / (min(R1, R2) + 1e-12)), r_squared=float(r_squared), mse=float(np.mean(outside_dist**2)), mean_error=float(np.mean(outside_dist)), frac_inside=float(inside_mask.sum() / N), frac_in_hole=float(in_hole.sum() / N), pc_extents=pc_ext, pc_ranges=pc_range, ) return GeometryFit( kind="torus", center=c, normal=n_ax, u_axis=u_ax, v_axis=v_ax, major_radii=(output["R1"], output["R2"]), tube_radius=output["minor_radius"], inner_radii=None, score=output["frac_inside"], error=output["mean_error"], mse=output["mse"], r_squared=output["r_squared"], hole_quality=output["hole_quality"], fraction_in_hole=output["frac_in_hole"], success=bool(result.success), evaluations=int(result.nfev), optimizer_message=str(result.message), )