From e1e907dff445eb5adeff95e1087b17ae7f906b7e Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 17:58:04 -0500 Subject: [PATCH 01/13] feat(evals): add near-field template experiment Add an analytic folded fan near-field experiment for issue 61. The prototype derives and documents the mapped point-target and bilinear self-interaction forms, frames the reusable correction hypothesis as a functional expansion in the smooth metric field M(z), and adds a script wrapper for running the baseline check. Tests cover fan-map invariants, log-kernel scale behavior, point-target convergence, and the diagonal metric singular split.\n\nValidation: uv run --extra dev python -m pytest tests/evals/test_nearfield_templates.py tests/test_script_wrappers.py; uv run --extra dev python scripts/run_nearfield_template_experiment.py --order 6; make dev Entire-Checkpoint: 3d4ca5797c60 --- ...2-boxcode-nearfield-template-experiment.md | 71 ++++ docs/index.md | 1 + docs/nearfield-template-experiments.md | 163 ++++++++ scripts/run_nearfield_template_experiment.py | 63 +++ src/cutkit/evals/__init__.py | 22 ++ src/cutkit/evals/nearfield_templates.py | 360 ++++++++++++++++++ tests/evals/test_nearfield_templates.py | 105 +++++ 7 files changed, 785 insertions(+) create mode 100644 docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md create mode 100644 docs/nearfield-template-experiments.md create mode 100755 scripts/run_nearfield_template_experiment.py create mode 100644 src/cutkit/evals/nearfield_templates.py create mode 100644 tests/evals/test_nearfield_templates.py diff --git a/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md b/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md new file mode 100644 index 0000000..5c85bee --- /dev/null +++ b/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md @@ -0,0 +1,71 @@ +# Box-Code Near-Field Template Experiment + +## Objective + +Evaluate whether folded-decomposition charts can support reusable near-field +template corrections for box-code and Volumential-style workflows. + +## Scope + +- Derive the mapped-kernel form for a 2D fan folded piece. +- Include both point-target and target-piece bilinear near-field forms. +- Identify singular factors that can be separated from smooth geometry payloads. +- Frame the reuse hypothesis as a functional expansion in smooth metric fields + `M(z)`, not just as raw runtime geometry data. +- Add a small analytic experiment driver with deterministic tests. +- Document feasibility limits and the next CUTKIT/Volumential boundary. + +## Non-Goals + +- No production Volumential integration in this branch. +- No CAD dependency for the first prototype. +- No claim that arbitrary cut shapes have finite exact correction tables. + +## Acceptance Criteria + +- The derivation states the mapped self-interaction form and its split. +- The derivation records the point-target form, bilinear form, interaction + taxonomy, and measurement criteria from issue 61. +- The prototype records a self or adjacent template experiment with a direct + high-order reference value. +- Tests check the experiment against an analytic or independently computed + invariant. +- Documentation explains whether the idea is feasible and what data must remain + runtime geometry payload. + +## Completed Checklist + +- [x] Read issue 61 and repository orientation docs. +- [x] Work through the 2D fan-map singularity derivation. +- [x] Add an experiment module or script for the near-field template prototype. +- [x] Add tests for the prototype invariants. +- [x] Update docs and doc index if new documentation is added. +- [x] Run targeted tests and `make dev`. +- [x] Move this plan to completed with outcomes. + +## Outcomes + +- The near-field template idea is feasible as a CUTKIT experiment: singular and + nearly singular folded-piece interactions can be expressed on fixed template + domains. +- The reusable singular part is a template-space log singular model. The metric + field `M(z) = DT(z)^T DT(z)` and higher-order terms are smooth geometry data. +- The practical reuse hypothesis is that a functional expansion in `M(z)` and + smooth-remainder data covers the folded-decomposition cases with few modes. +- The first prototype covers analytic straight fan maps, point-target + integrals, bilinear self interactions, a log-kernel scale-law check, and a + diagonal metric-remainder sample. + +## Validation + +- `uv run --extra dev python -m pytest tests/evals/test_nearfield_templates.py tests/test_script_wrappers.py` +- `uv run --extra dev python scripts/run_nearfield_template_experiment.py --order 6` +- `make dev` + +## Remaining Risks + +- Template corrections may be reusable only after parameterized compression, not + as shape-independent lookup tables. +- Self-interaction singular quadrature may need specialized rules beyond the + first direct high-order reference prototype. +- Curved analytic pieces and adjacent-piece cases still need follow-up coverage. diff --git a/docs/index.md b/docs/index.md index ff80f0e..b6fe266 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ Repository-local documentation is the system of record for CUTKIT. - `docs/cad-box-batch-interface.md`: consumer-facing CAD object-or-arrays clip/integrate interface - `docs/volumential-handoff.md`: CUTKIT far/near primitive handoff contract for volumential workflows - `docs/meshmode-cut-overlay.md`: meshmode cut-overlay contract and validation semantics +- `docs/nearfield-template-experiments.md`: folded fan near-field template derivation and prototype - `docs/poisson-benchmarks.md`: Poisson-oriented benchmark usage and profiles - `docs/poisson-galerkin-solver.md`: immersed Poisson Galerkin solve + validation workflow - `docs/formdsl-parity-benchmarks.md`: shared IGA + DG-SEM formdsl parity benchmark usage diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md new file mode 100644 index 0000000..070db97 --- /dev/null +++ b/docs/nearfield-template-experiments.md @@ -0,0 +1,163 @@ +# Near-Field Template Experiments + +This note records the first mathematical check for issue 61: whether folded +decomposition charts can support a reusable near-field template story for later +box-code or Volumential work. + +## Fan Map + +For one straight-edge folded 2D piece, use the fan chart + +```text +T(r, t) = (1-r) V + r C(t), +C(t) = (1-t) P0 + t P1, +0 <= r,t <= 1. +``` + +Its Jacobian is + +```text +J(r, t) = r det(C(t)-V, C'(t)) = r det(P0-V, P1-V). +``` + +The factor `r` is the Duffy-style apex degeneracy already used by +`cutkit.quadrature.folded2d`. It is not a new singularity for physical +integration; it cancels area at the apex. + +## Mapped Kernel + +For the 2D Laplace kernel + +```text +G(x, y) = -log(|x-y|)/(2*pi), +``` + +the point-target near-field integral is + +```text +u(x) = integral_[0,1]^2 G(x, T_e(r,t)) rho(T_e(r,t)) J_e(r,t) dr dt. +``` + +This is the cheapest diagnostic path because it isolates source-piece mapping, +source density, and target location before introducing target basis functions. +For target pieces with a second map `T_f(eta)`, the bilinear template integral is + +```text +A_fe = integral_[0,1]^2 integral_[0,1]^2 + psi_f(eta) G(T_f(eta), T_e(xi)) phi_e(xi) + J_f(eta) J_e(xi) dxi deta. +``` + +For the self-piece case, this specializes to + +```text +I = integral_[0,1]^2 integral_[0,1]^2 + G(T(r,t), T(r',t')) J(r,t) J(r',t') dr dt dr' dt'. +``` + +The experiment should classify interactions as: + +- self piece, `e = f`; +- edge-adjacent pieces sharing a boundary segment; +- vertex or seed-adjacent pieces; +- near but disjoint pieces; +- well-separated pieces, where ordinary tensor-product quadrature should already work. + +## Singular Split + +Near a non-apex diagonal point `z=(r,t)`, write `delta = z' - z`. Then + +```text +T(z + delta) - T(z) = DT(z) delta + O(|delta|^2), +|T(z + delta) - T(z)| = sqrt(delta^T M(z) delta) (1 + O(|delta|)), +M(z) = DT(z)^T DT(z). +``` + +Therefore + +```text +G(T(z), T(z+delta)) += -log(sqrt(delta^T M(z) delta))/(2*pi) + smooth remainder. +``` + +The reusable part would be template-space singular model integrals or correction +operators. The runtime payload remains map coefficients, Jacobian data, source +coefficients, target metadata, and smooth-remainder moment/interpolation data. + +## Feasibility Result + +The answer is qualified yes. Singular or nearly singular folded-piece +interactions can be moved to fixed template domains, but not to a finite exact +table independent of geometry. The singular coordinate form is reusable; the +metric and higher-order geometry terms enter through smooth geometry-dependent +payloads. + +The practical hypothesis is stronger than carrying `M(z)` pointwise at runtime: +because the folded maps have smooth, low-parameter structure away from collapsed +seed faces, the metric field `M(z)` should vary smoothly over the template and +across the geometry families produced by folded decomposition. A functional +expansion in the metric data, for example moments, interpolation coefficients, +or a low-rank basis in `M` and the smooth remainder, may cover enough practical +cut-piece cases to make reusable near-field corrections worthwhile. + +The open numerical question is therefore whether the smooth dependence on `M`, +seed location, and curve coefficients is compact enough to approximate by such a +functional expansion for useful geometry families. + +## Measurements + +For each geometry and interaction case, record: + +- reference value; +- ordinary pulled-forward tensor-product quadrature error; +- template singular-correction error; +- smooth-remainder approximation error; +- dependence on quadrature order; +- dependence on seed location and curve coefficients; +- size, rank, and smoothness of geometry-parameterized correction data; +- how many metric-field expansion modes are needed for the observed folded + decomposition cases. + +The first geometry family is CAD-independent: straight fan pieces, then +quadratic Bezier arcs and mildly curved cubics, with seed locations chosen to +produce positive, negative, and folded orientations. OpenCascade should only be +used later as a stress-test backend. + +## Prototype + +`cutkit.evals.nearfield_templates` implements: + +- `FanTemplateMap2D` for the analytic fan map. +- `point_target_laplace_potential(...)` for point-target source integrals with + polynomial template densities. +- `self_interaction_laplace(...)` for a direct high-order mapped self integral. +- `diagonal_remainder_sample(...)` for the metric singular split. +- `run_nearfield_template_experiment(...)` for the baseline feasibility check. + +The script wrapper is: + +```bash +uv run python scripts/run_nearfield_template_experiment.py --order 12 +``` + +The experiment checks the exact scale law for the 2D log kernel. If the fan is +scaled by `lambda`, then + +```text +I_lambda = lambda^4 (I - log(lambda) A^2/(2*pi)), +``` + +where `A` is the signed area of the unscaled fan piece. It also records diagonal +remainder samples that shrink as the source/target template coordinates coalesce +and a point-target low-order-vs-reference error. + +## Next Decision + +The idea is feasible as a CUTKIT experiment, with these limits: + +- Far-field source clouds can remain ordinary signed quadrature sources. +- Near-field correction reuse should target template singular bases plus + functional expansions in the smooth metric field `M(z)` and higher geometry + terms. +- Volumential should still own tree/list composition; CUTKIT should export the + local geometry/operator payloads needed by those lists. diff --git a/scripts/run_nearfield_template_experiment.py b/scripts/run_nearfield_template_experiment.py new file mode 100755 index 0000000..1e4442f --- /dev/null +++ b/scripts/run_nearfield_template_experiment.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Run the folded fan near-field template experiment.""" + +from __future__ import annotations + +import argparse + +from cutkit.evals import run_nearfield_template_experiment + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--order", + type=int, + default=12, + help="Gauss-Legendre order for the target template coordinates.", + ) + parser.add_argument( + "--scale-factor", + type=float, + default=1.75, + help="Positive geometry scale factor for the log-kernel scale-law check.", + ) + return parser.parse_args() + + +def main() -> int: + args = _parse_args() + result = run_nearfield_template_experiment( + order=args.order, + scale_factor=args.scale_factor, + ) + + print("field | value") + print("--- | ---") + print(f"order | {result.order}") + print(f"source_order | {result.source_order}") + print(f"near_point_target | {result.near_point_target}") + print(f"point_target_reference | {result.point_target_reference:.16e}") + print(f"point_target_low_order | {result.point_target_low_order:.16e}") + print(f"point_target_abs_error | {result.point_target_abs_error:.16e}") + print(f"signed_area | {result.signed_area:.16e}") + print(f"self_interaction | {result.self_interaction:.16e}") + print(f"scale_factor | {result.scale_factor:.16e}") + print(f"scaled_self_interaction | {result.scaled_self_interaction:.16e}") + print( + f"expected_scaled_self_interaction | {result.expected_scaled_self_interaction:.16e}" + ) + print(f"scaled_abs_error | {result.scaled_abs_error:.16e}") + print("") + print("delta | physical_distance | model_distance | remainder") + print("--- | --- | --- | ---") + for sample in result.diagonal_remainders: + print( + f"{sample.delta:.1e} | {sample.physical_distance:.16e} | " + f"{sample.model_distance:.16e} | {sample.remainder:.16e}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/cutkit/evals/__init__.py b/src/cutkit/evals/__init__.py index 110c262..245c6d2 100644 --- a/src/cutkit/evals/__init__.py +++ b/src/cutkit/evals/__init__.py @@ -74,6 +74,18 @@ run_poisson_galerkin_benchmark, solve_trimmed_poisson_galerkin, ) +from cutkit.evals.nearfield_templates import ( + DiagonalRemainderSample, + FanTemplateMap2D, + NearfieldTemplateExperiment, + diagonal_remainder_sample, + expected_scaled_laplace_self_interaction, + laplace_log_kernel, + metric_model_distance, + point_target_laplace_potential, + run_nearfield_template_experiment, + self_interaction_laplace, +) __all__ = [ "case_to_fixture_payload", @@ -138,6 +150,16 @@ "build_poisson_galerkin_geometry_snapshot", "run_poisson_galerkin_benchmark", "solve_trimmed_poisson_galerkin", + "DiagonalRemainderSample", + "FanTemplateMap2D", + "NearfieldTemplateExperiment", + "diagonal_remainder_sample", + "expected_scaled_laplace_self_interaction", + "laplace_log_kernel", + "metric_model_distance", + "point_target_laplace_potential", + "run_nearfield_template_experiment", + "self_interaction_laplace", "FormDslParityBenchmark", "FormDslParityRow", "FormDslMultipatchStressBenchmark", diff --git a/src/cutkit/evals/nearfield_templates.py b/src/cutkit/evals/nearfield_templates.py new file mode 100644 index 0000000..d28e5d7 --- /dev/null +++ b/src/cutkit/evals/nearfield_templates.py @@ -0,0 +1,360 @@ +"""Near-field template experiments for folded 2D fan pieces.""" + +from __future__ import annotations + +from dataclasses import dataclass +from math import hypot, log, pi, sqrt +from typing import Callable + +from cutkit.quadrature import gauss_legendre_01 + +Point2D = tuple[float, float] +TemplateDensity2D = Callable[[float, float], float] + + +def unit_template_density(_r: float, _t: float) -> float: + """Return the constant template density used by default experiments.""" + + return 1.0 + + +def _sub(lhs: Point2D, rhs: Point2D) -> Point2D: + return (lhs[0] - rhs[0], lhs[1] - rhs[1]) + + +def _dot(lhs: Point2D, rhs: Point2D) -> float: + return lhs[0] * rhs[0] + lhs[1] * rhs[1] + + +def _cross(lhs: Point2D, rhs: Point2D) -> float: + return lhs[0] * rhs[1] - lhs[1] * rhs[0] + + +def _scale_point(point: Point2D, scale: float) -> Point2D: + return (scale * point[0], scale * point[1]) + + +@dataclass(frozen=True) +class FanTemplateMap2D: + """Straight-edge folded fan chart ``T(r, t) = (1-r)V + r C(t)``.""" + + vertex: Point2D + edge_start: Point2D + edge_end: Point2D + + def point(self, r: float, t: float) -> Point2D: + """Map ``(r, t)`` in ``[0, 1]^2`` to physical space.""" + + one_minus_t = 1.0 - t + curve_x = one_minus_t * self.edge_start[0] + t * self.edge_end[0] + curve_y = one_minus_t * self.edge_start[1] + t * self.edge_end[1] + return ( + (1.0 - r) * self.vertex[0] + r * curve_x, + (1.0 - r) * self.vertex[1] + r * curve_y, + ) + + @property + def boundary_det(self) -> float: + """Return ``det(C(t)-V, C'(t))``, constant for a straight edge.""" + + return _cross( + _sub(self.edge_start, self.vertex), + _sub(self.edge_end, self.edge_start), + ) + + @property + def signed_area(self) -> float: + """Return the oriented physical area of the fan piece.""" + + return 0.5 * self.boundary_det + + def signed_jacobian(self, r: float) -> float: + """Return the signed Jacobian of the ``(r, t)`` fan chart.""" + + return r * self.boundary_det + + def local_metric( + self, + r: float, + t: float, + ) -> tuple[tuple[float, float], tuple[float, float]]: + """Return ``DT(r,t)^T DT(r,t)`` for the fan chart.""" + + radial = _sub(self.point(1.0, t), self.vertex) + tangent_base = _sub(self.edge_end, self.edge_start) + tangent = (r * tangent_base[0], r * tangent_base[1]) + return ( + (_dot(radial, radial), _dot(radial, tangent)), + (_dot(tangent, radial), _dot(tangent, tangent)), + ) + + def scaled(self, factor: float) -> FanTemplateMap2D: + """Return the same template scaled about the origin.""" + + return FanTemplateMap2D( + vertex=_scale_point(self.vertex, factor), + edge_start=_scale_point(self.edge_start, factor), + edge_end=_scale_point(self.edge_end, factor), + ) + + +@dataclass(frozen=True) +class DiagonalRemainderSample: + """One mapped-kernel singular split sample near the diagonal.""" + + delta: float + physical_distance: float + model_distance: float + laplace_kernel: float + metric_model_kernel: float + remainder: float + + +@dataclass(frozen=True) +class NearfieldTemplateExperiment: + """Summary of one fan-chart self-interaction experiment.""" + + fan: FanTemplateMap2D + order: int + source_order: int + near_point_target: Point2D + point_target_reference: float + point_target_low_order: float + point_target_abs_error: float + self_interaction: float + signed_area: float + scale_factor: float + scaled_self_interaction: float + expected_scaled_self_interaction: float + scaled_abs_error: float + diagonal_remainders: tuple[DiagonalRemainderSample, ...] + + +def laplace_log_kernel(target: Point2D, source: Point2D) -> float: + """Return the 2D Laplace fundamental solution ``-log(|x-y|)/(2*pi)``.""" + + distance = hypot(target[0] - source[0], target[1] - source[1]) + if distance <= 0.0: + raise ValueError("Laplace log kernel is singular at coincident points") + return -log(distance) / (2.0 * pi) + + +def metric_model_distance( + fan: FanTemplateMap2D, + *, + r: float, + t: float, + delta_r: float, + delta_t: float, +) -> float: + """Return the local metric distance for a template-space displacement.""" + + metric = fan.local_metric(r, t) + distance_squared = ( + metric[0][0] * delta_r * delta_r + + 2.0 * metric[0][1] * delta_r * delta_t + + metric[1][1] * delta_t * delta_t + ) + if distance_squared <= 0.0: + raise ValueError("metric model distance must be positive") + return sqrt(distance_squared) + + +def self_interaction_laplace( + fan: FanTemplateMap2D, + *, + order: int, + source_order: int | None = None, + source_density: TemplateDensity2D | None = None, + target_density: TemplateDensity2D | None = None, +) -> float: + """Approximate the mapped self interaction on one fan chart.""" + + if order < 1: + raise ValueError("order must be positive") + if source_order is None: + source_order = order + 1 + if source_order < 1: + raise ValueError("source_order must be positive") + if source_density is None: + source_density = unit_template_density + if target_density is None: + target_density = unit_template_density + + target_nodes, target_weights = gauss_legendre_01(order) + source_nodes, source_weights = gauss_legendre_01(source_order) + total = 0.0 + + for target_r, target_wr in zip(target_nodes, target_weights, strict=True): + target_j = fan.signed_jacobian(target_r) + for target_t, target_wt in zip(target_nodes, target_weights, strict=True): + target = fan.point(target_r, target_t) + target_weight = ( + target_density(target_r, target_t) * target_j * target_wr * target_wt + ) + for source_r, source_wr in zip(source_nodes, source_weights, strict=True): + source_j = fan.signed_jacobian(source_r) + for source_t, source_wt in zip( + source_nodes, source_weights, strict=True + ): + source = fan.point(source_r, source_t) + source_weight = ( + source_density(source_r, source_t) + * source_j + * source_wr + * source_wt + ) + total += ( + laplace_log_kernel(target, source) + * target_weight + * source_weight + ) + + return total + + +def point_target_laplace_potential( + fan: FanTemplateMap2D, + target: Point2D, + *, + order: int, + source_density: TemplateDensity2D | None = None, +) -> float: + """Approximate a folded-piece source integral at one point target.""" + + if order < 1: + raise ValueError("order must be positive") + if source_density is None: + source_density = unit_template_density + + nodes, weights = gauss_legendre_01(order) + total = 0.0 + for r, wr in zip(nodes, weights, strict=True): + jacobian = fan.signed_jacobian(r) + for t, wt in zip(nodes, weights, strict=True): + source = fan.point(r, t) + total += ( + laplace_log_kernel(target, source) + * source_density(r, t) + * jacobian + * wr + * wt + ) + return total + + +def expected_scaled_laplace_self_interaction( + base_interaction: float, + signed_area: float, + scale_factor: float, +) -> float: + """Return the exact 2D log-kernel scale law for a scaled fan piece.""" + + if scale_factor <= 0.0: + raise ValueError("scale_factor must be positive") + return scale_factor**4 * ( + base_interaction - log(scale_factor) * signed_area * signed_area / (2.0 * pi) + ) + + +def diagonal_remainder_sample( + fan: FanTemplateMap2D, + *, + r: float, + t: float, + delta: float, + direction: tuple[float, float] = (1.0, 0.5), +) -> DiagonalRemainderSample: + """Compare the full mapped kernel with the local metric singular model.""" + + if delta <= 0.0: + raise ValueError("delta must be positive") + delta_r = delta * direction[0] + delta_t = delta * direction[1] + source = fan.point(r, t) + target = fan.point(r + delta_r, t + delta_t) + physical_distance = hypot(target[0] - source[0], target[1] - source[1]) + model_distance = metric_model_distance( + fan, + r=r, + t=t, + delta_r=delta_r, + delta_t=delta_t, + ) + laplace_kernel = -log(physical_distance) / (2.0 * pi) + metric_model_kernel = -log(model_distance) / (2.0 * pi) + return DiagonalRemainderSample( + delta=delta, + physical_distance=physical_distance, + model_distance=model_distance, + laplace_kernel=laplace_kernel, + metric_model_kernel=metric_model_kernel, + remainder=laplace_kernel - metric_model_kernel, + ) + + +def run_nearfield_template_experiment( + *, + order: int = 12, + scale_factor: float = 1.75, +) -> NearfieldTemplateExperiment: + """Run the baseline analytic fan near-field template experiment.""" + + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + source_order = order + 1 + self_value = self_interaction_laplace( + fan, + order=order, + source_order=source_order, + ) + near_point_target = (0.47, 0.42) + + def point_density(r: float, t: float) -> float: + return 1.0 + r - 0.5 * t + + point_reference = point_target_laplace_potential( + fan, + near_point_target, + order=max(order + 8, 16), + source_density=point_density, + ) + point_low_order = point_target_laplace_potential( + fan, + near_point_target, + order=max(order // 2, 2), + source_density=point_density, + ) + scaled_value = self_interaction_laplace( + fan.scaled(scale_factor), + order=order, + source_order=source_order, + ) + expected_scaled = expected_scaled_laplace_self_interaction( + self_value, + fan.signed_area, + scale_factor, + ) + remainders = tuple( + diagonal_remainder_sample(fan, r=0.43, t=0.37, delta=delta) + for delta in (1.0e-1, 1.0e-2, 1.0e-3) + ) + return NearfieldTemplateExperiment( + fan=fan, + order=order, + source_order=source_order, + near_point_target=near_point_target, + point_target_reference=point_reference, + point_target_low_order=point_low_order, + point_target_abs_error=abs(point_low_order - point_reference), + self_interaction=self_value, + signed_area=fan.signed_area, + scale_factor=scale_factor, + scaled_self_interaction=scaled_value, + expected_scaled_self_interaction=expected_scaled, + scaled_abs_error=abs(scaled_value - expected_scaled), + diagonal_remainders=remainders, + ) diff --git a/tests/evals/test_nearfield_templates.py b/tests/evals/test_nearfield_templates.py new file mode 100644 index 0000000..23cb351 --- /dev/null +++ b/tests/evals/test_nearfield_templates.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import pytest + +from cutkit.evals import ( + FanTemplateMap2D, + diagonal_remainder_sample, + expected_scaled_laplace_self_interaction, + point_target_laplace_potential, + run_nearfield_template_experiment, + self_interaction_laplace, +) + + +def test_fan_map_area_and_metric_are_consistent() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.25, 0.5), + ) + + assert fan.signed_area == pytest.approx(0.25) + metric = fan.local_metric(0.5, 0.25) + assert metric[0][0] > 0.0 + assert metric[1][1] > 0.0 + assert metric[0][1] == pytest.approx(metric[1][0]) + + +def test_self_interaction_obeys_log_kernel_scale_law() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + scale_factor = 2.25 + value = self_interaction_laplace(fan, order=5, source_order=6) + scaled = self_interaction_laplace( + fan.scaled(scale_factor), + order=5, + source_order=6, + ) + expected = expected_scaled_laplace_self_interaction( + value, + fan.signed_area, + scale_factor, + ) + + assert scaled == pytest.approx(expected, abs=1.0e-13) + + +def test_metric_singular_remainder_shrinks_toward_diagonal() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + coarse = diagonal_remainder_sample(fan, r=0.43, t=0.37, delta=1.0e-2) + fine = diagonal_remainder_sample(fan, r=0.43, t=0.37, delta=1.0e-4) + + assert abs(fine.remainder) < abs(coarse.remainder) + assert fine.physical_distance == pytest.approx(fine.model_distance, rel=1.0e-3) + + +def test_point_target_path_converges_for_near_disjoint_target() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + target = (0.8, 0.7) + + def density(r: float, t: float) -> float: + return 1.0 + r * t + + coarse = point_target_laplace_potential( + fan, + target, + order=4, + source_density=density, + ) + fine = point_target_laplace_potential( + fan, + target, + order=10, + source_density=density, + ) + reference = point_target_laplace_potential( + fan, + target, + order=18, + source_density=density, + ) + + assert abs(fine - reference) < abs(coarse - reference) + + +def test_baseline_experiment_records_feasibility_checks() -> None: + result = run_nearfield_template_experiment(order=5, scale_factor=1.5) + + assert result.scaled_abs_error < 1.0e-13 + assert result.point_target_abs_error > 0.0 + assert result.diagonal_remainders[-1].delta == pytest.approx(1.0e-3) + assert abs(result.diagonal_remainders[-1].remainder) < abs( + result.diagonal_remainders[0].remainder + ) From e987ed6079ae5482d1a9d7e2f7ee3e4fb09efdf8 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 18:13:17 -0500 Subject: [PATCH 02/13] docs: derive metric expansion tables Document how smooth metric fields in folded chart singular integrals can be expanded around a reference metric to recover fixed template tables times per-chart runtime coefficients. The derivation ties the precomputed-table technique to the fan-map metric M(r,t), Jacobian factors, and smooth-remainder payloads.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 7ffd824cac89 --- docs/nearfield-template-experiments.md | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 070db97..cdfbef7 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -84,6 +84,110 @@ The reusable part would be template-space singular model integrals or correction operators. The runtime payload remains map coefficients, Jacobian data, source coefficients, target metadata, and smooth-remainder moment/interpolation data. +## Metric-Field Expansion Tables + +The table-building objective is to separate fixed singular template integrals +from per-chart smooth geometry coefficients. For a self or adjacent chart +interaction, use local source/target coordinates near the singular set and write +`z' = z + delta`. The singular distance model is + +```text +|T(z') - T(z)|^2 = delta^T M(z) delta + higher-order smooth terms, +M(z) = DT(z)^T DT(z). +``` + +Choose a positive-definite reference metric `M0` for one metric bin, chart +family, or local average, and write + +```text +M(z) = M0 + Delta M(z). +``` + +Then the logarithmic singular factor can be expanded as + +```text +log(delta^T M(z) delta) += log(delta^T M0 delta) + + log(1 + (delta^T Delta M(z) delta)/(delta^T M0 delta)). +``` + +If the metric family is binned or normalized so that + +```text +abs((delta^T Delta M(z) delta)/(delta^T M0 delta)) < 1, +``` + +then + +```text +log(delta^T M(z) delta) += log(delta^T M0 delta) + + sum_{k>=1} (-1)^(k+1)/k + ((delta^T Delta M(z) delta)/(delta^T M0 delta))^k. +``` + +Thus the kernel singular part has the schematic expansion + +```text +G_sing(z, z + delta) +~= sum_alpha c_alpha(z) S_alpha(delta; M0), +``` + +where `S_alpha` are fixed singular template functions for the selected reference +metric and `c_alpha(z)` are smooth functions of the entries of `Delta M(z)`. +The expansion can be truncated either by polynomial order in `Delta M`, by +interpolation in metric-entry space, or by a learned/empirical low-rank basis. + +For a bilinear self interaction with template basis functions `psi_i` and +`phi_j`, the singular contribution becomes + +```text +A_ij^sing ~= sum_alpha integral integral + psi_i(z) phi_j(z') c_alpha(z) S_alpha(z'-z; M0) + J(z) J(z') dz' dz. +``` + +Expand the smooth per-chart factor in a template basis `p_beta`: + +```text +c_alpha(z) J(z) J(z') ~= sum_beta q_{alpha beta} p_beta(z, z'). +``` + +Then + +```text +A_ij^sing ~= sum_{alpha,beta} q_{alpha beta} T_{ij alpha beta}, +T_{ij alpha beta} = integral integral + psi_i(z) phi_j(z') p_beta(z, z') S_alpha(z'-z; M0) dz' dz. +``` + +The tensors `T_{ij alpha beta}` are precomputed on fixed template domains. At +runtime, each folded chart only supplies the coefficients `q_{alpha beta}` from +its smooth metric/Jacobian fields and any smooth-remainder representation. + +For the fan map + +```text +T(r,t) = (1-r)V + r C(t), +``` + +the metric entries are explicit: + +```text +T_r = C(t) - V, +T_t = r C'(t), + +M(r,t) = [ |C(t)-V|^2 r (C(t)-V).C'(t) ] + [ r (C(t)-V).C'(t) r^2 |C'(t)|^2 ]. +``` + +For straight, Bezier, or mildly curved polynomial `C(t)`, these entries are +smooth low-parameter functions of `(r,t)`, the seed `V`, and curve coefficients. +This is the mathematical reason precomputed template tables can apply to each +chart piece: the singular table is fixed after choosing `M0`, while observed +folded-decomposition geometry should enter through a small smooth expansion of +`M`, `J`, and the nonsingular remainder. + ## Feasibility Result The answer is qualified yes. Singular or nearly singular folded-piece From 2653068afff0585f099c1d0204322e47e42f40e8 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 18:36:41 -0500 Subject: [PATCH 03/13] docs: format near-field derivation math Convert the near-field template derivation from text code fences to display math so GitHub renders the formulas properly. The content is unchanged aside from LaTeX notation cleanup.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: c15094b32a03 --- docs/nearfield-template-experiments.md | 178 +++++++++++++------------ 1 file changed, 96 insertions(+), 82 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index cdfbef7..2fb7a49 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -8,17 +8,17 @@ box-code or Volumential work. For one straight-edge folded 2D piece, use the fan chart -```text -T(r, t) = (1-r) V + r C(t), -C(t) = (1-t) P0 + t P1, -0 <= r,t <= 1. -``` +$$ +T(r,t) = (1-r)V + r C(t), +\qquad C(t) = (1-t)P_0 + tP_1, +\qquad 0 \le r,t \le 1. +$$ Its Jacobian is -```text -J(r, t) = r det(C(t)-V, C'(t)) = r det(P0-V, P1-V). -``` +$$ +J(r,t) = r \det(C(t)-V, C'(t)) = r \det(P_0-V, P_1-V). +$$ The factor `r` is the Duffy-style apex degeneracy already used by `cutkit.quadrature.folded2d`. It is not a new singularity for physical @@ -28,32 +28,32 @@ integration; it cancels area at the apex. For the 2D Laplace kernel -```text -G(x, y) = -log(|x-y|)/(2*pi), -``` +$$ +G(x,y) = -\frac{1}{2\pi}\log |x-y|, +$$ the point-target near-field integral is -```text -u(x) = integral_[0,1]^2 G(x, T_e(r,t)) rho(T_e(r,t)) J_e(r,t) dr dt. -``` +$$ +u(x) = \int_{[0,1]^2} G(x,T_e(r,t))\rho(T_e(r,t))J_e(r,t)\,dr\,dt. +$$ This is the cheapest diagnostic path because it isolates source-piece mapping, source density, and target location before introducing target basis functions. For target pieces with a second map `T_f(eta)`, the bilinear template integral is -```text -A_fe = integral_[0,1]^2 integral_[0,1]^2 - psi_f(eta) G(T_f(eta), T_e(xi)) phi_e(xi) - J_f(eta) J_e(xi) dxi deta. -``` +$$ +A_{fe} = \int_{[0,1]^2}\int_{[0,1]^2} +\psi_f(\eta)G(T_f(\eta),T_e(\xi))\phi_e(\xi) +J_f(\eta)J_e(\xi)\,d\xi\,d\eta. +$$ For the self-piece case, this specializes to -```text -I = integral_[0,1]^2 integral_[0,1]^2 - G(T(r,t), T(r',t')) J(r,t) J(r',t') dr dt dr' dt'. -``` +$$ +I = \int_{[0,1]^2}\int_{[0,1]^2} +G(T(r,t),T(r',t'))J(r,t)J(r',t')\,dr\,dt\,dr'\,dt'. +$$ The experiment should classify interactions as: @@ -67,18 +67,22 @@ The experiment should classify interactions as: Near a non-apex diagonal point `z=(r,t)`, write `delta = z' - z`. Then -```text -T(z + delta) - T(z) = DT(z) delta + O(|delta|^2), -|T(z + delta) - T(z)| = sqrt(delta^T M(z) delta) (1 + O(|delta|)), -M(z) = DT(z)^T DT(z). -``` +$$ +T(z+\delta)-T(z) = DT(z)\delta + O(|\delta|^2), +$$ + +$$ +|T(z+\delta)-T(z)| = \sqrt{\delta^T M(z)\delta}\,(1+O(|\delta|)), +\qquad M(z) = DT(z)^TDT(z). +$$ Therefore -```text -G(T(z), T(z+delta)) -= -log(sqrt(delta^T M(z) delta))/(2*pi) + smooth remainder. -``` +$$ +G(T(z),T(z+\delta)) += -\frac{1}{2\pi}\log\sqrt{\delta^T M(z)\delta} ++ \text{smooth remainder}. +$$ The reusable part would be template-space singular model integrals or correction operators. The runtime payload remains map coefficients, Jacobian data, source @@ -91,47 +95,48 @@ from per-chart smooth geometry coefficients. For a self or adjacent chart interaction, use local source/target coordinates near the singular set and write `z' = z + delta`. The singular distance model is -```text -|T(z') - T(z)|^2 = delta^T M(z) delta + higher-order smooth terms, -M(z) = DT(z)^T DT(z). -``` +$$ +|T(z')-T(z)|^2 = \delta^T M(z)\delta + \text{higher-order smooth terms}, +\qquad M(z)=DT(z)^TDT(z). +$$ Choose a positive-definite reference metric `M0` for one metric bin, chart family, or local average, and write -```text -M(z) = M0 + Delta M(z). -``` +$$ +M(z) = M_0 + \Delta M(z). +$$ Then the logarithmic singular factor can be expanded as -```text -log(delta^T M(z) delta) -= log(delta^T M0 delta) - + log(1 + (delta^T Delta M(z) delta)/(delta^T M0 delta)). -``` +$$ +\log(\delta^T M(z)\delta) += \log(\delta^T M_0\delta) ++ \log\left(1+ +\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right). +$$ If the metric family is binned or normalized so that -```text -abs((delta^T Delta M(z) delta)/(delta^T M0 delta)) < 1, -``` +$$ +\left|\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right| < 1, +$$ then -```text -log(delta^T M(z) delta) -= log(delta^T M0 delta) - + sum_{k>=1} (-1)^(k+1)/k - ((delta^T Delta M(z) delta)/(delta^T M0 delta))^k. -``` +$$ +\log(\delta^T M(z)\delta) += \log(\delta^T M_0\delta) ++ \sum_{k\ge 1}\frac{(-1)^{k+1}}{k} +\left(\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right)^k. +$$ Thus the kernel singular part has the schematic expansion -```text -G_sing(z, z + delta) -~= sum_alpha c_alpha(z) S_alpha(delta; M0), -``` +$$ +G_{\mathrm{sing}}(z,z+\delta) +\approx \sum_\alpha c_\alpha(z)S_\alpha(\delta;M_0), +$$ where `S_alpha` are fixed singular template functions for the selected reference metric and `c_alpha(z)` are smooth functions of the entries of `Delta M(z)`. @@ -141,25 +146,29 @@ interpolation in metric-entry space, or by a learned/empirical low-rank basis. For a bilinear self interaction with template basis functions `psi_i` and `phi_j`, the singular contribution becomes -```text -A_ij^sing ~= sum_alpha integral integral - psi_i(z) phi_j(z') c_alpha(z) S_alpha(z'-z; M0) - J(z) J(z') dz' dz. -``` +$$ +A_{ij}^{\mathrm{sing}} \approx \sum_\alpha \int\!\int +\psi_i(z)\phi_j(z')c_\alpha(z)S_\alpha(z'-z;M_0) +J(z)J(z')\,dz'\,dz. +$$ Expand the smooth per-chart factor in a template basis `p_beta`: -```text -c_alpha(z) J(z) J(z') ~= sum_beta q_{alpha beta} p_beta(z, z'). -``` +$$ +c_\alpha(z)J(z)J(z') \approx \sum_\beta q_{\alpha\beta}p_\beta(z,z'). +$$ Then -```text -A_ij^sing ~= sum_{alpha,beta} q_{alpha beta} T_{ij alpha beta}, -T_{ij alpha beta} = integral integral - psi_i(z) phi_j(z') p_beta(z, z') S_alpha(z'-z; M0) dz' dz. -``` +$$ +A_{ij}^{\mathrm{sing}} \approx +\sum_{\alpha,\beta}q_{\alpha\beta}T_{ij\alpha\beta}, +$$ + +$$ +T_{ij\alpha\beta} = \int\!\int +\psi_i(z)\phi_j(z')p_\beta(z,z')S_\alpha(z'-z;M_0)\,dz'\,dz. +$$ The tensors `T_{ij alpha beta}` are precomputed on fixed template domains. At runtime, each folded chart only supplies the coefficients `q_{alpha beta}` from @@ -167,19 +176,24 @@ its smooth metric/Jacobian fields and any smooth-remainder representation. For the fan map -```text -T(r,t) = (1-r)V + r C(t), -``` +$$ +T(r,t) = (1-r)V + rC(t), +$$ the metric entries are explicit: -```text -T_r = C(t) - V, -T_t = r C'(t), +$$ +T_r = C(t)-V, +\qquad T_t = rC'(t), +$$ -M(r,t) = [ |C(t)-V|^2 r (C(t)-V).C'(t) ] - [ r (C(t)-V).C'(t) r^2 |C'(t)|^2 ]. -``` +$$ +M(r,t) = +\begin{bmatrix} +|C(t)-V|^2 & r(C(t)-V)\cdot C'(t) \\ +r(C(t)-V)\cdot C'(t) & r^2|C'(t)|^2 +\end{bmatrix}. +$$ For straight, Bezier, or mildly curved polynomial `C(t)`, these entries are smooth low-parameter functions of `(r,t)`, the seed `V`, and curve coefficients. @@ -247,9 +261,9 @@ uv run python scripts/run_nearfield_template_experiment.py --order 12 The experiment checks the exact scale law for the 2D log kernel. If the fan is scaled by `lambda`, then -```text -I_lambda = lambda^4 (I - log(lambda) A^2/(2*pi)), -``` +$$ +I_\lambda = \lambda^4\left(I - \frac{\log(\lambda)A^2}{2\pi}\right), +$$ where `A` is the signed area of the unscaled fan piece. It also records diagonal remainder samples that shrink as the source/target template coordinates coalesce From 5ffa3027a6eaaf33d5cf92467a7b62814ecbbe97 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 18:55:23 -0500 Subject: [PATCH 04/13] fix(evals): handle near-field quadrature edge cases Address Codex review feedback on the near-field template prototype. Reject direct self-interaction quadrature choices with overlapping source/target nodes, report a clear error when point-target quadrature hits a coincident source node, and generalize the log-kernel scale law to density-weighted source and target masses. Also weaken the local metric split documentation so it describes a leading singular model plus bounded/higher-order corrections rather than an unqualified smooth remainder.\n\nValidation: make dev Entire-Checkpoint: 95096c66f419 --- docs/nearfield-template-experiments.md | 24 +++++++--- src/cutkit/evals/__init__.py | 2 + src/cutkit/evals/nearfield_templates.py | 54 ++++++++++++++++++++- tests/evals/test_nearfield_templates.py | 64 +++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 9 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 2fb7a49..f1f1a27 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -81,12 +81,18 @@ Therefore $$ G(T(z),T(z+\delta)) = -\frac{1}{2\pi}\log\sqrt{\delta^T M(z)\delta} -+ \text{smooth remainder}. ++ \text{lower-order correction}. $$ The reusable part would be template-space singular model integrals or correction operators. The runtime payload remains map coefficients, Jacobian data, source -coefficients, target metadata, and smooth-remainder moment/interpolation data. +coefficients, target metadata, and correction moment/interpolation data. For the +straight fan map, `T(r,t)` is bilinear in template variables, so subtracting only +the local metric model leaves an `O(|delta|)` correction whose first derivative +at the diagonal can depend on approach direction. A smooth-remainder expansion +should therefore either include higher-order distance terms in the singular model +or treat this first prototype as a leading singular split plus a bounded local +correction. ## Metric-Field Expansion Tables @@ -96,7 +102,7 @@ interaction, use local source/target coordinates near the singular set and write `z' = z + delta`. The singular distance model is $$ -|T(z')-T(z)|^2 = \delta^T M(z)\delta + \text{higher-order smooth terms}, +|T(z')-T(z)|^2 = \delta^T M(z)\delta + \text{higher-order chart terms}, \qquad M(z)=DT(z)^TDT(z). $$ @@ -142,6 +148,9 @@ where `S_alpha` are fixed singular template functions for the selected reference metric and `c_alpha(z)` are smooth functions of the entries of `Delta M(z)`. The expansion can be truncated either by polynomial order in `Delta M`, by interpolation in metric-entry space, or by a learned/empirical low-rank basis. +For full smooth-remainder tables near a chart diagonal, the singular model should +also include enough higher-order chart-distance terms to remove direction- +dependent local corrections. For a bilinear self interaction with template basis functions `psi_i` and `phi_j`, the singular contribution becomes @@ -172,7 +181,8 @@ $$ The tensors `T_{ij alpha beta}` are precomputed on fixed template domains. At runtime, each folded chart only supplies the coefficients `q_{alpha beta}` from -its smooth metric/Jacobian fields and any smooth-remainder representation. +its smooth metric/Jacobian fields and any higher-order correction or remainder +representation. For the fan map @@ -200,7 +210,7 @@ smooth low-parameter functions of `(r,t)`, the seed `V`, and curve coefficients. This is the mathematical reason precomputed template tables can apply to each chart piece: the singular table is fixed after choosing `M0`, while observed folded-decomposition geometry should enter through a small smooth expansion of -`M`, `J`, and the nonsingular remainder. +`M`, `J`, and the higher-order correction data. ## Feasibility Result @@ -215,7 +225,7 @@ because the folded maps have smooth, low-parameter structure away from collapsed seed faces, the metric field `M(z)` should vary smoothly over the template and across the geometry families produced by folded decomposition. A functional expansion in the metric data, for example moments, interpolation coefficients, -or a low-rank basis in `M` and the smooth remainder, may cover enough practical +or a low-rank basis in `M` and the correction terms, may cover enough practical cut-piece cases to make reusable near-field corrections worthwhile. The open numerical question is therefore whether the smooth dependence on `M`, @@ -229,7 +239,7 @@ For each geometry and interaction case, record: - reference value; - ordinary pulled-forward tensor-product quadrature error; - template singular-correction error; -- smooth-remainder approximation error; +- higher-order correction/remainder approximation error; - dependence on quadrature order; - dependence on seed location and curve coefficients; - size, rank, and smoothness of geometry-parameterized correction data; diff --git a/src/cutkit/evals/__init__.py b/src/cutkit/evals/__init__.py index 245c6d2..a1f1557 100644 --- a/src/cutkit/evals/__init__.py +++ b/src/cutkit/evals/__init__.py @@ -85,6 +85,7 @@ point_target_laplace_potential, run_nearfield_template_experiment, self_interaction_laplace, + template_density_mass, ) __all__ = [ @@ -160,6 +161,7 @@ "point_target_laplace_potential", "run_nearfield_template_experiment", "self_interaction_laplace", + "template_density_mass", "FormDslParityBenchmark", "FormDslParityRow", "FormDslMultipatchStressBenchmark", diff --git a/src/cutkit/evals/nearfield_templates.py b/src/cutkit/evals/nearfield_templates.py index d28e5d7..b4ec56a 100644 --- a/src/cutkit/evals/nearfield_templates.py +++ b/src/cutkit/evals/nearfield_templates.py @@ -10,6 +10,7 @@ Point2D = tuple[float, float] TemplateDensity2D = Callable[[float, float], float] +_COINCIDENT_TOL = 1.0e-14 def unit_template_density(_r: float, _t: float) -> float: @@ -34,6 +35,19 @@ def _scale_point(point: Point2D, scale: float) -> Point2D: return (scale * point[0], scale * point[1]) +def _rules_share_nodes( + lhs: tuple[float, ...], + rhs: tuple[float, ...], + *, + atol: float = _COINCIDENT_TOL, +) -> bool: + for left in lhs: + for right in rhs: + if abs(left - right) <= atol: + return True + return False + + @dataclass(frozen=True) class FanTemplateMap2D: """Straight-edge folded fan chart ``T(r, t) = (1-r)V + r C(t)``.""" @@ -183,6 +197,11 @@ def self_interaction_laplace( target_nodes, target_weights = gauss_legendre_01(order) source_nodes, source_weights = gauss_legendre_01(source_order) + if _rules_share_nodes(target_nodes, source_nodes): + raise ValueError( + "source_order quadrature nodes must not overlap target order nodes " + "for direct self-interaction reference quadrature" + ) total = 0.0 for target_r, target_wr in zip(target_nodes, target_weights, strict=True): @@ -213,6 +232,28 @@ def self_interaction_laplace( return total +def template_density_mass( + fan: FanTemplateMap2D, + *, + order: int, + density: TemplateDensity2D | None = None, +) -> float: + """Return ``integral density(r,t) * J(r,t) dr dt`` on one fan chart.""" + + if order < 1: + raise ValueError("order must be positive") + if density is None: + density = unit_template_density + + nodes, weights = gauss_legendre_01(order) + total = 0.0 + for r, wr in zip(nodes, weights, strict=True): + jacobian = fan.signed_jacobian(r) + for t, wt in zip(nodes, weights, strict=True): + total += density(r, t) * jacobian * wr * wt + return total + + def point_target_laplace_potential( fan: FanTemplateMap2D, target: Point2D, @@ -233,6 +274,11 @@ def point_target_laplace_potential( jacobian = fan.signed_jacobian(r) for t, wt in zip(nodes, weights, strict=True): source = fan.point(r, t) + if hypot(target[0] - source[0], target[1] - source[1]) <= _COINCIDENT_TOL: + raise ValueError( + "point target coincides with a source quadrature node; " + "use a singular point-target reference rule" + ) total += ( laplace_log_kernel(target, source) * source_density(r, t) @@ -245,15 +291,19 @@ def point_target_laplace_potential( def expected_scaled_laplace_self_interaction( base_interaction: float, - signed_area: float, + source_mass: float, scale_factor: float, + *, + target_mass: float | None = None, ) -> float: """Return the exact 2D log-kernel scale law for a scaled fan piece.""" if scale_factor <= 0.0: raise ValueError("scale_factor must be positive") + if target_mass is None: + target_mass = source_mass return scale_factor**4 * ( - base_interaction - log(scale_factor) * signed_area * signed_area / (2.0 * pi) + base_interaction - log(scale_factor) * target_mass * source_mass / (2.0 * pi) ) diff --git a/tests/evals/test_nearfield_templates.py b/tests/evals/test_nearfield_templates.py index 23cb351..126a9e2 100644 --- a/tests/evals/test_nearfield_templates.py +++ b/tests/evals/test_nearfield_templates.py @@ -9,7 +9,9 @@ point_target_laplace_potential, run_nearfield_template_experiment, self_interaction_laplace, + template_density_mass, ) +from cutkit.quadrature import gauss_legendre_01 def test_fan_map_area_and_metric_are_consistent() -> None: @@ -48,6 +50,55 @@ def test_self_interaction_obeys_log_kernel_scale_law() -> None: assert scaled == pytest.approx(expected, abs=1.0e-13) +def test_self_interaction_rejects_overlapping_template_nodes() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + + with pytest.raises(ValueError, match="must not overlap"): + self_interaction_laplace(fan, order=3, source_order=5) + + +def test_scaled_self_interaction_uses_density_weighted_masses() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + + def source_density(r: float, t: float) -> float: + return 1.0 + r + + def target_density(r: float, t: float) -> float: + return 1.0 - 0.25 * t + + scale_factor = 1.4 + value = self_interaction_laplace( + fan, + order=4, + source_order=5, + source_density=source_density, + target_density=target_density, + ) + scaled = self_interaction_laplace( + fan.scaled(scale_factor), + order=4, + source_order=5, + source_density=source_density, + target_density=target_density, + ) + expected = expected_scaled_laplace_self_interaction( + value, + template_density_mass(fan, order=4, density=source_density), + scale_factor, + target_mass=template_density_mass(fan, order=4, density=target_density), + ) + + assert scaled == pytest.approx(expected, abs=1.0e-13) + + def test_metric_singular_remainder_shrinks_toward_diagonal() -> None: fan = FanTemplateMap2D( vertex=(0.0, 0.0), @@ -94,6 +145,19 @@ def density(r: float, t: float) -> float: assert abs(fine - reference) < abs(coarse - reference) +def test_point_target_path_rejects_coincident_source_node() -> None: + fan = FanTemplateMap2D( + vertex=(0.0, 0.0), + edge_start=(1.0, 0.0), + edge_end=(0.35, 0.9), + ) + nodes, _weights = gauss_legendre_01(3) + target = fan.point(nodes[1], nodes[1]) + + with pytest.raises(ValueError, match="coincides with a source quadrature node"): + point_target_laplace_potential(fan, target, order=3) + + def test_baseline_experiment_records_feasibility_checks() -> None: result = run_nearfield_template_experiment(order=5, scale_factor=1.5) From 38560bbfc3d14dc707119593268798cfbe1baf21 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 18:58:32 -0500 Subject: [PATCH 05/13] docs: make Bezier fan chart the primary derivation Update the near-field template derivation to start from a CAD-realistic Bezier trim fan chart instead of a straight-edge specialization. Straight fan pieces are now described as smoke-test fixtures, while the metric-field expansion is framed in terms of Bezier control points and seed locations.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: c9e0b70886a7 --- docs/nearfield-template-experiments.md | 69 +++++++++++++++----------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index f1f1a27..a182b3f 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -4,25 +4,36 @@ This note records the first mathematical check for issue 61: whether folded decomposition charts can support a reusable near-field template story for later box-code or Volumential work. -## Fan Map +## Bezier Fan Chart -For one straight-edge folded 2D piece, use the fan chart +For the mathematical model, start with the chart type expected from CAD-backed +folded decomposition: a fan from a seed point `V` to a smooth Bezier trim curve +`C(t)`. A degree-`p` Bezier edge is $$ -T(r,t) = (1-r)V + r C(t), -\qquad C(t) = (1-t)P_0 + tP_1, -\qquad 0 \le r,t \le 1. +C(t) = \sum_{a=0}^p B_a^p(t)P_a, +\qquad B_a^p(t)=\binom{p}{a}(1-t)^{p-a}t^a, +\qquad 0\le t\le 1. +$$ + +The folded chart is + +$$ +T(r,t) = (1-r)V + rC(t), +\qquad 0\le r,t\le 1. $$ Its Jacobian is $$ -J(r,t) = r \det(C(t)-V, C'(t)) = r \det(P_0-V, P_1-V). +J(r,t) = r\det(C(t)-V,C'(t)). $$ The factor `r` is the Duffy-style apex degeneracy already used by `cutkit.quadrature.folded2d`. It is not a new singularity for physical -integration; it cancels area at the apex. +integration; it cancels area at the apex. The straight-edge case is only the +degree-1 specialization and should be treated as a smoke-test fixture, not as +the primary derivation. ## Mapped Kernel @@ -86,13 +97,13 @@ $$ The reusable part would be template-space singular model integrals or correction operators. The runtime payload remains map coefficients, Jacobian data, source -coefficients, target metadata, and correction moment/interpolation data. For the -straight fan map, `T(r,t)` is bilinear in template variables, so subtracting only -the local metric model leaves an `O(|delta|)` correction whose first derivative -at the diagonal can depend on approach direction. A smooth-remainder expansion -should therefore either include higher-order distance terms in the singular model -or treat this first prototype as a leading singular split plus a bounded local -correction. +coefficients, target metadata, and correction moment/interpolation data. For a +Bezier fan map, `T(r,t)` is polynomial in `(r,t)` and contains mixed higher-order +terms from `rC(t)`. Subtracting only the local metric model leaves an +`O(|delta|)` correction whose first derivative at the diagonal can depend on +approach direction. A smooth-remainder expansion should therefore either include +higher-order distance terms in the singular model or treat this first prototype +as a leading singular split plus a bounded local correction. ## Metric-Field Expansion Tables @@ -184,7 +195,7 @@ runtime, each folded chart only supplies the coefficients `q_{alpha beta}` from its smooth metric/Jacobian fields and any higher-order correction or remainder representation. -For the fan map +For the Bezier fan map $$ T(r,t) = (1-r)V + rC(t), @@ -205,12 +216,12 @@ r(C(t)-V)\cdot C'(t) & r^2|C'(t)|^2 \end{bmatrix}. $$ -For straight, Bezier, or mildly curved polynomial `C(t)`, these entries are -smooth low-parameter functions of `(r,t)`, the seed `V`, and curve coefficients. -This is the mathematical reason precomputed template tables can apply to each -chart piece: the singular table is fixed after choosing `M0`, while observed -folded-decomposition geometry should enter through a small smooth expansion of -`M`, `J`, and the higher-order correction data. +For Bezier or piecewise-Bezier CAD trims, these entries are smooth +low-parameter functions of `(r,t)`, the seed `V`, and the Bezier control points +`P_a`. This is the mathematical reason precomputed template tables can apply to +each chart piece: the singular table is fixed after choosing `M0`, while +observed folded-decomposition geometry should enter through a small smooth +expansion of `M`, `J`, and the higher-order correction data. ## Feasibility Result @@ -246,16 +257,18 @@ For each geometry and interaction case, record: - how many metric-field expansion modes are needed for the observed folded decomposition cases. -The first geometry family is CAD-independent: straight fan pieces, then -quadratic Bezier arcs and mildly curved cubics, with seed locations chosen to -produce positive, negative, and folded orientations. OpenCascade should only be -used later as a stress-test backend. +The first geometry family should be CAD-independent but CAD-realistic: quadratic +and cubic Bezier fan charts with seed locations chosen to produce positive, +negative, and folded orientations. Straight fan pieces are useful only as +low-level smoke tests. OpenCascade should be used later as a source of +stress-test Bezier/NURBS-derived fixtures, not as a dependency of the numerical +question. ## Prototype `cutkit.evals.nearfield_templates` implements: -- `FanTemplateMap2D` for the analytic fan map. +- `FanTemplateMap2D` for the current straight-edge smoke-test fan map. - `point_target_laplace_potential(...)` for point-target source integrals with polynomial template densities. - `self_interaction_laplace(...)` for a direct high-order mapped self integral. @@ -268,8 +281,8 @@ The script wrapper is: uv run python scripts/run_nearfield_template_experiment.py --order 12 ``` -The experiment checks the exact scale law for the 2D log kernel. If the fan is -scaled by `lambda`, then +The current smoke-test experiment checks the exact scale law for the 2D log +kernel. If the fan is scaled by `lambda`, then $$ I_\lambda = \lambda^4\left(I - \frac{\log(\lambda)A^2}{2\pi}\right), From db19ee4e0dfe575ed2aa22c00170fcae7c90e671 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 19:25:40 -0500 Subject: [PATCH 06/13] fix(evals): make near-field prototype point-target only Remove the folded-target and bilinear self-interaction framing from the near-field template experiment. The docs now describe source folded pieces evaluated at physical target nodes in containing and neighboring boxes, and the experiment API now reports point-target scale-law quantities only.\n\nValidation: make dev; uv run --extra dev python -m pytest tests/evals/test_nearfield_templates.py tests/test_script_wrappers.py; uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 6cb8d3064a3b --- ...2-boxcode-nearfield-template-experiment.md | 12 +- docs/nearfield-template-experiments.md | 140 ++++++++++-------- scripts/run_nearfield_template_experiment.py | 11 +- src/cutkit/evals/__init__.py | 6 +- src/cutkit/evals/nearfield_templates.py | 132 ++++------------- tests/evals/test_nearfield_templates.py | 52 +++---- 6 files changed, 143 insertions(+), 210 deletions(-) diff --git a/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md b/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md index 5c85bee..4b07f66 100644 --- a/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md +++ b/docs/exec-plans/completed/20260612-boxcode-nearfield-template-experiment.md @@ -8,7 +8,7 @@ template corrections for box-code and Volumential-style workflows. ## Scope - Derive the mapped-kernel form for a 2D fan folded piece. -- Include both point-target and target-piece bilinear near-field forms. +- Focus the near-field form on point targets in physical space. - Identify singular factors that can be separated from smooth geometry payloads. - Frame the reuse hypothesis as a functional expansion in smooth metric fields `M(z)`, not just as raw runtime geometry data. @@ -24,9 +24,9 @@ template corrections for box-code and Volumential-style workflows. ## Acceptance Criteria - The derivation states the mapped self-interaction form and its split. -- The derivation records the point-target form, bilinear form, interaction - taxonomy, and measurement criteria from issue 61. -- The prototype records a self or adjacent template experiment with a direct +- The derivation records the point-target form, target/source geometry taxonomy, + and measurement criteria from issue 61. +- The prototype records a point-target template experiment with a direct high-order reference value. - Tests check the experiment against an analytic or independently computed invariant. @@ -53,8 +53,8 @@ template corrections for box-code and Volumential-style workflows. - The practical reuse hypothesis is that a functional expansion in `M(z)` and smooth-remainder data covers the folded-decomposition cases with few modes. - The first prototype covers analytic straight fan maps, point-target - integrals, bilinear self interactions, a log-kernel scale-law check, and a - diagonal metric-remainder sample. + integrals, a log-kernel scale-law check, and a diagonal metric-remainder + sample. ## Validation diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index a182b3f..5ef90e3 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -49,72 +49,87 @@ $$ u(x) = \int_{[0,1]^2} G(x,T_e(r,t))\rho(T_e(r,t))J_e(r,t)\,dr\,dt. $$ -This is the cheapest diagnostic path because it isolates source-piece mapping, -source density, and target location before introducing target basis functions. -For target pieces with a second map `T_f(eta)`, the bilinear template integral is +This is the primary target model for the box-code experiment. The source is a +folded piece, but targets are physical-space discretization nodes in the source +box and neighboring near-field boxes. There is no target folded piece in the +first near-field experiment. + +For reusable tables, express the target relative to the same physical box or +chart payload. If `x0` is a chart/box reference point and `H` is a box-scale map, +write $$ -A_{fe} = \int_{[0,1]^2}\int_{[0,1]^2} -\psi_f(\eta)G(T_f(\eta),T_e(\xi))\phi_e(\xi) -J_f(\eta)J_e(\xi)\,d\xi\,d\eta. +x = x_0 + H\tau, $$ -For the self-piece case, this specializes to +where `tau` is the template-space location of a target node in the containing or +neighboring box. The source-to-point integral is then a fixed-domain integral in +the source chart coordinates and the target offset parameter `tau`: $$ -I = \int_{[0,1]^2}\int_{[0,1]^2} -G(T(r,t),T(r',t'))J(r,t)J(r',t')\,dr\,dt\,dr'\,dt'. +u(\tau) = \int_{[0,1]^2}G(x_0+H\tau,T_e(\xi))\rho(T_e(\xi))J_e(\xi)\,d\xi. $$ -The experiment should classify interactions as: +The experiment should classify target/source geometry as: -- self piece, `e = f`; -- edge-adjacent pieces sharing a boundary segment; -- vertex or seed-adjacent pieces; -- near but disjoint pieces; -- well-separated pieces, where ordinary tensor-product quadrature should already work. +- target node on or very near the source folded piece; +- target node in the source piece's containing box; +- target node in an edge-neighboring box; +- target node in a vertex-neighboring box; +- well-separated target node, where ordinary tensor-product quadrature should already work. -## Singular Split +## Point-Target Singular Split -Near a non-apex diagonal point `z=(r,t)`, write `delta = z' - z`. Then +For a target point near the source chart, let `z_x` be a closest or projected +template coordinate with `T(z_x)` near `x`, and write `xi = z_x + delta`. Then $$ -T(z+\delta)-T(z) = DT(z)\delta + O(|\delta|^2), +T(z_x+\delta)-T(z_x) = DT(z_x)\delta + O(|\delta|^2), $$ $$ -|T(z+\delta)-T(z)| = \sqrt{\delta^T M(z)\delta}\,(1+O(|\delta|)), -\qquad M(z) = DT(z)^TDT(z). +|T(z_x+\delta)-T(z_x)| = +\sqrt{\delta^T M(z_x)\delta}\,(1+O(|\delta|)), +\qquad M(z_x) = DT(z_x)^TDT(z_x). $$ -Therefore +For an on-surface or asymptotically close target, this gives the leading model $$ -G(T(z),T(z+\delta)) -= -\frac{1}{2\pi}\log\sqrt{\delta^T M(z)\delta} +G(x,T(z_x+\delta)) += -\frac{1}{2\pi}\log\sqrt{\delta^T M(z_x)\delta} + \text{lower-order correction}. $$ +For an off-surface target, include the normal/offset residual `d = x-T(z_x)`: + +$$ +|x-T(z_x+\delta)|^2 += |d-DT(z_x)\delta|^2 + \text{higher-order chart terms}. +$$ + The reusable part would be template-space singular model integrals or correction operators. The runtime payload remains map coefficients, Jacobian data, source -coefficients, target metadata, and correction moment/interpolation data. For a -Bezier fan map, `T(r,t)` is polynomial in `(r,t)` and contains mixed higher-order -terms from `rC(t)`. Subtracting only the local metric model leaves an -`O(|delta|)` correction whose first derivative at the diagonal can depend on -approach direction. A smooth-remainder expansion should therefore either include -higher-order distance terms in the singular model or treat this first prototype -as a leading singular split plus a bounded local correction. +coefficients, target-node offsets, and correction moment/interpolation data. For +a Bezier fan map, `T(r,t)` is polynomial in `(r,t)` and contains mixed +higher-order terms from `rC(t)`. Subtracting only the local metric model leaves +an `O(|delta|)` correction whose first derivative at the singular point can +depend on approach direction. A smooth-remainder expansion should therefore +either include higher-order distance terms in the singular model or treat this +first prototype as a leading singular split plus a bounded local correction. ## Metric-Field Expansion Tables The table-building objective is to separate fixed singular template integrals -from per-chart smooth geometry coefficients. For a self or adjacent chart -interaction, use local source/target coordinates near the singular set and write -`z' = z + delta`. The singular distance model is +from per-chart smooth geometry and per-target offset coefficients. For a target +node represented by offset coordinates `tau`, use local source coordinates near +the closest chart point and write `xi = z_x + delta`. The on-surface singular +distance model is $$ -|T(z')-T(z)|^2 = \delta^T M(z)\delta + \text{higher-order chart terms}, -\qquad M(z)=DT(z)^TDT(z). +|T(z_x+\delta)-T(z_x)|^2 += \delta^T M(z_x)\delta + \text{higher-order chart terms}, +\qquad M(z_x)=DT(z_x)^TDT(z_x). $$ Choose a positive-definite reference metric `M0` for one metric bin, chart @@ -151,49 +166,50 @@ $$ Thus the kernel singular part has the schematic expansion $$ -G_{\mathrm{sing}}(z,z+\delta) -\approx \sum_\alpha c_\alpha(z)S_\alpha(\delta;M_0), +G_{\mathrm{sing}}(x,T(z_x+\delta)) +\approx \sum_\alpha c_\alpha(z_x,\tau)S_\alpha(\delta;M_0), $$ where `S_alpha` are fixed singular template functions for the selected reference -metric and `c_alpha(z)` are smooth functions of the entries of `Delta M(z)`. -The expansion can be truncated either by polynomial order in `Delta M`, by -interpolation in metric-entry space, or by a learned/empirical low-rank basis. +metric and `c_alpha(z_x,tau)` are smooth functions of the entries of +`Delta M(z_x)` and the target offset data. The expansion can be truncated either +by polynomial order in `Delta M`, by interpolation in metric/offset space, or by +a learned/empirical low-rank basis. For full smooth-remainder tables near a chart diagonal, the singular model should also include enough higher-order chart-distance terms to remove direction- dependent local corrections. -For a bilinear self interaction with template basis functions `psi_i` and -`phi_j`, the singular contribution becomes +For one target node and one source density expansion mode `rho_j`, the singular +contribution becomes $$ -A_{ij}^{\mathrm{sing}} \approx \sum_\alpha \int\!\int -\psi_i(z)\phi_j(z')c_\alpha(z)S_\alpha(z'-z;M_0) -J(z)J(z')\,dz'\,dz. +u_j^{\mathrm{sing}}(\tau) \approx \sum_\alpha \int_{[0,1]^2} +\rho_j(\xi)c_\alpha(z_x,\tau)S_\alpha(\xi-z_x;M_0)J(\xi)\,d\xi. $$ -Expand the smooth per-chart factor in a template basis `p_beta`: +Expand the smooth per-chart/per-target factor in a template basis `p_beta`: $$ -c_\alpha(z)J(z)J(z') \approx \sum_\beta q_{\alpha\beta}p_\beta(z,z'). +c_\alpha(z_x,\tau)J(\xi) \approx \sum_\beta q_{\alpha\beta}(\tau)p_\beta(\xi). $$ Then $$ -A_{ij}^{\mathrm{sing}} \approx -\sum_{\alpha,\beta}q_{\alpha\beta}T_{ij\alpha\beta}, +u_j^{\mathrm{sing}}(\tau) \approx +\sum_{\alpha,\beta}q_{\alpha\beta}(\tau)T_{j\alpha\beta}, $$ $$ -T_{ij\alpha\beta} = \int\!\int -\psi_i(z)\phi_j(z')p_\beta(z,z')S_\alpha(z'-z;M_0)\,dz'\,dz. +T_{j\alpha\beta} = \int_{[0,1]^2} +\rho_j(\xi)p_\beta(\xi)S_\alpha(\xi-z_x;M_0)\,d\xi. $$ -The tensors `T_{ij alpha beta}` are precomputed on fixed template domains. At -runtime, each folded chart only supplies the coefficients `q_{alpha beta}` from -its smooth metric/Jacobian fields and any higher-order correction or remainder -representation. +The tensors `T_{j alpha beta}` are precomputed on fixed source template domains, +or tabulated over a small target-offset grid. At runtime, each folded chart and +target node only supply the coefficients `q_{alpha beta}(tau)` from smooth +metric/Jacobian fields, target offset data, and any higher-order correction or +remainder representation. For the Bezier fan map @@ -271,7 +287,6 @@ question. - `FanTemplateMap2D` for the current straight-edge smoke-test fan map. - `point_target_laplace_potential(...)` for point-target source integrals with polynomial template densities. -- `self_interaction_laplace(...)` for a direct high-order mapped self integral. - `diagonal_remainder_sample(...)` for the metric singular split. - `run_nearfield_template_experiment(...)` for the baseline feasibility check. @@ -282,15 +297,18 @@ uv run python scripts/run_nearfield_template_experiment.py --order 12 ``` The current smoke-test experiment checks the exact scale law for the 2D log -kernel. If the fan is scaled by `lambda`, then +kernel. If both the source fan and physical target point are scaled by `lambda`, +then $$ -I_\lambda = \lambda^4\left(I - \frac{\log(\lambda)A^2}{2\pi}\right), +u_\lambda(\lambda x) += \lambda^2\left(u(x) - \frac{\log(\lambda)m_\rho}{2\pi}\right), $$ -where `A` is the signed area of the unscaled fan piece. It also records diagonal -remainder samples that shrink as the source/target template coordinates coalesce -and a point-target low-order-vs-reference error. +where `m_rho` is the density-weighted signed source mass on the unscaled fan +piece. It also records diagonal remainder samples that shrink as the source and +target template coordinates coalesce and a point-target low-order-vs-reference +error. ## Next Decision diff --git a/scripts/run_nearfield_template_experiment.py b/scripts/run_nearfield_template_experiment.py index 1e4442f..4e5991f 100755 --- a/scripts/run_nearfield_template_experiment.py +++ b/scripts/run_nearfield_template_experiment.py @@ -35,17 +35,20 @@ def main() -> int: print("field | value") print("--- | ---") print(f"order | {result.order}") - print(f"source_order | {result.source_order}") print(f"near_point_target | {result.near_point_target}") print(f"point_target_reference | {result.point_target_reference:.16e}") print(f"point_target_low_order | {result.point_target_low_order:.16e}") print(f"point_target_abs_error | {result.point_target_abs_error:.16e}") print(f"signed_area | {result.signed_area:.16e}") - print(f"self_interaction | {result.self_interaction:.16e}") + print(f"density_mass | {result.density_mass:.16e}") print(f"scale_factor | {result.scale_factor:.16e}") - print(f"scaled_self_interaction | {result.scaled_self_interaction:.16e}") + print(f"scaled_near_point_target | {result.scaled_near_point_target}") print( - f"expected_scaled_self_interaction | {result.expected_scaled_self_interaction:.16e}" + f"scaled_point_target_potential | {result.scaled_point_target_potential:.16e}" + ) + print( + "expected_scaled_point_target_potential | " + f"{result.expected_scaled_point_target_potential:.16e}" ) print(f"scaled_abs_error | {result.scaled_abs_error:.16e}") print("") diff --git a/src/cutkit/evals/__init__.py b/src/cutkit/evals/__init__.py index a1f1557..1b9956b 100644 --- a/src/cutkit/evals/__init__.py +++ b/src/cutkit/evals/__init__.py @@ -79,12 +79,11 @@ FanTemplateMap2D, NearfieldTemplateExperiment, diagonal_remainder_sample, - expected_scaled_laplace_self_interaction, + expected_scaled_laplace_point_potential, laplace_log_kernel, metric_model_distance, point_target_laplace_potential, run_nearfield_template_experiment, - self_interaction_laplace, template_density_mass, ) @@ -155,12 +154,11 @@ "FanTemplateMap2D", "NearfieldTemplateExperiment", "diagonal_remainder_sample", - "expected_scaled_laplace_self_interaction", + "expected_scaled_laplace_point_potential", "laplace_log_kernel", "metric_model_distance", "point_target_laplace_potential", "run_nearfield_template_experiment", - "self_interaction_laplace", "template_density_mass", "FormDslParityBenchmark", "FormDslParityRow", diff --git a/src/cutkit/evals/nearfield_templates.py b/src/cutkit/evals/nearfield_templates.py index b4ec56a..0fdd49f 100644 --- a/src/cutkit/evals/nearfield_templates.py +++ b/src/cutkit/evals/nearfield_templates.py @@ -35,19 +35,6 @@ def _scale_point(point: Point2D, scale: float) -> Point2D: return (scale * point[0], scale * point[1]) -def _rules_share_nodes( - lhs: tuple[float, ...], - rhs: tuple[float, ...], - *, - atol: float = _COINCIDENT_TOL, -) -> bool: - for left in lhs: - for right in rhs: - if abs(left - right) <= atol: - return True - return False - - @dataclass(frozen=True) class FanTemplateMap2D: """Straight-edge folded fan chart ``T(r, t) = (1-r)V + r C(t)``.""" @@ -126,21 +113,21 @@ class DiagonalRemainderSample: @dataclass(frozen=True) class NearfieldTemplateExperiment: - """Summary of one fan-chart self-interaction experiment.""" + """Summary of one fan-chart point-target experiment.""" fan: FanTemplateMap2D order: int - source_order: int near_point_target: Point2D point_target_reference: float point_target_low_order: float point_target_abs_error: float - self_interaction: float + scaled_near_point_target: Point2D + scaled_point_target_potential: float + expected_scaled_point_target_potential: float + scaled_abs_error: float signed_area: float + density_mass: float scale_factor: float - scaled_self_interaction: float - expected_scaled_self_interaction: float - scaled_abs_error: float diagonal_remainders: tuple[DiagonalRemainderSample, ...] @@ -174,64 +161,6 @@ def metric_model_distance( return sqrt(distance_squared) -def self_interaction_laplace( - fan: FanTemplateMap2D, - *, - order: int, - source_order: int | None = None, - source_density: TemplateDensity2D | None = None, - target_density: TemplateDensity2D | None = None, -) -> float: - """Approximate the mapped self interaction on one fan chart.""" - - if order < 1: - raise ValueError("order must be positive") - if source_order is None: - source_order = order + 1 - if source_order < 1: - raise ValueError("source_order must be positive") - if source_density is None: - source_density = unit_template_density - if target_density is None: - target_density = unit_template_density - - target_nodes, target_weights = gauss_legendre_01(order) - source_nodes, source_weights = gauss_legendre_01(source_order) - if _rules_share_nodes(target_nodes, source_nodes): - raise ValueError( - "source_order quadrature nodes must not overlap target order nodes " - "for direct self-interaction reference quadrature" - ) - total = 0.0 - - for target_r, target_wr in zip(target_nodes, target_weights, strict=True): - target_j = fan.signed_jacobian(target_r) - for target_t, target_wt in zip(target_nodes, target_weights, strict=True): - target = fan.point(target_r, target_t) - target_weight = ( - target_density(target_r, target_t) * target_j * target_wr * target_wt - ) - for source_r, source_wr in zip(source_nodes, source_weights, strict=True): - source_j = fan.signed_jacobian(source_r) - for source_t, source_wt in zip( - source_nodes, source_weights, strict=True - ): - source = fan.point(source_r, source_t) - source_weight = ( - source_density(source_r, source_t) - * source_j - * source_wr - * source_wt - ) - total += ( - laplace_log_kernel(target, source) - * target_weight - * source_weight - ) - - return total - - def template_density_mass( fan: FanTemplateMap2D, *, @@ -289,21 +218,17 @@ def point_target_laplace_potential( return total -def expected_scaled_laplace_self_interaction( - base_interaction: float, +def expected_scaled_laplace_point_potential( + base_potential: float, source_mass: float, scale_factor: float, - *, - target_mass: float | None = None, ) -> float: - """Return the exact 2D log-kernel scale law for a scaled fan piece.""" + """Return the exact 2D log-kernel scale law for a scaled source and target.""" if scale_factor <= 0.0: raise ValueError("scale_factor must be positive") - if target_mass is None: - target_mass = source_mass - return scale_factor**4 * ( - base_interaction - log(scale_factor) * target_mass * source_mass / (2.0 * pi) + return scale_factor**2 * ( + base_potential - log(scale_factor) * source_mass / (2.0 * pi) ) @@ -355,12 +280,6 @@ def run_nearfield_template_experiment( edge_start=(1.0, 0.0), edge_end=(0.35, 0.9), ) - source_order = order + 1 - self_value = self_interaction_laplace( - fan, - order=order, - source_order=source_order, - ) near_point_target = (0.47, 0.42) def point_density(r: float, t: float) -> float: @@ -378,14 +297,21 @@ def point_density(r: float, t: float) -> float: order=max(order // 2, 2), source_density=point_density, ) - scaled_value = self_interaction_laplace( + scaled_target = _scale_point(near_point_target, scale_factor) + scaled_value = point_target_laplace_potential( fan.scaled(scale_factor), - order=order, - source_order=source_order, + scaled_target, + order=max(order + 8, 16), + source_density=point_density, + ) + density_mass = template_density_mass( + fan, + order=max(order + 8, 16), + density=point_density, ) - expected_scaled = expected_scaled_laplace_self_interaction( - self_value, - fan.signed_area, + expected_scaled = expected_scaled_laplace_point_potential( + point_reference, + density_mass, scale_factor, ) remainders = tuple( @@ -395,16 +321,16 @@ def point_density(r: float, t: float) -> float: return NearfieldTemplateExperiment( fan=fan, order=order, - source_order=source_order, near_point_target=near_point_target, point_target_reference=point_reference, point_target_low_order=point_low_order, point_target_abs_error=abs(point_low_order - point_reference), - self_interaction=self_value, + scaled_near_point_target=scaled_target, + scaled_point_target_potential=scaled_value, + expected_scaled_point_target_potential=expected_scaled, + scaled_abs_error=abs(scaled_value - expected_scaled), signed_area=fan.signed_area, + density_mass=density_mass, scale_factor=scale_factor, - scaled_self_interaction=scaled_value, - expected_scaled_self_interaction=expected_scaled, - scaled_abs_error=abs(scaled_value - expected_scaled), diagonal_remainders=remainders, ) diff --git a/tests/evals/test_nearfield_templates.py b/tests/evals/test_nearfield_templates.py index 126a9e2..d3c60e1 100644 --- a/tests/evals/test_nearfield_templates.py +++ b/tests/evals/test_nearfield_templates.py @@ -5,10 +5,9 @@ from cutkit.evals import ( FanTemplateMap2D, diagonal_remainder_sample, - expected_scaled_laplace_self_interaction, + expected_scaled_laplace_point_potential, point_target_laplace_potential, run_nearfield_template_experiment, - self_interaction_laplace, template_density_mass, ) from cutkit.quadrature import gauss_legendre_01 @@ -28,40 +27,30 @@ def test_fan_map_area_and_metric_are_consistent() -> None: assert metric[0][1] == pytest.approx(metric[1][0]) -def test_self_interaction_obeys_log_kernel_scale_law() -> None: +def test_point_target_potential_obeys_log_kernel_scale_law() -> None: fan = FanTemplateMap2D( vertex=(0.0, 0.0), edge_start=(1.0, 0.0), edge_end=(0.35, 0.9), ) scale_factor = 2.25 - value = self_interaction_laplace(fan, order=5, source_order=6) - scaled = self_interaction_laplace( + target = (0.8, 0.7) + value = point_target_laplace_potential(fan, target, order=6) + scaled = point_target_laplace_potential( fan.scaled(scale_factor), - order=5, - source_order=6, + (scale_factor * target[0], scale_factor * target[1]), + order=6, ) - expected = expected_scaled_laplace_self_interaction( + expected = expected_scaled_laplace_point_potential( value, - fan.signed_area, + template_density_mass(fan, order=6), scale_factor, ) assert scaled == pytest.approx(expected, abs=1.0e-13) -def test_self_interaction_rejects_overlapping_template_nodes() -> None: - fan = FanTemplateMap2D( - vertex=(0.0, 0.0), - edge_start=(1.0, 0.0), - edge_end=(0.35, 0.9), - ) - - with pytest.raises(ValueError, match="must not overlap"): - self_interaction_laplace(fan, order=3, source_order=5) - - -def test_scaled_self_interaction_uses_density_weighted_masses() -> None: +def test_scaled_point_target_potential_uses_density_weighted_mass() -> None: fan = FanTemplateMap2D( vertex=(0.0, 0.0), edge_start=(1.0, 0.0), @@ -71,29 +60,24 @@ def test_scaled_self_interaction_uses_density_weighted_masses() -> None: def source_density(r: float, t: float) -> float: return 1.0 + r - def target_density(r: float, t: float) -> float: - return 1.0 - 0.25 * t - scale_factor = 1.4 - value = self_interaction_laplace( + target = (0.8, 0.7) + value = point_target_laplace_potential( fan, + target, order=4, - source_order=5, source_density=source_density, - target_density=target_density, ) - scaled = self_interaction_laplace( + scaled = point_target_laplace_potential( fan.scaled(scale_factor), + (scale_factor * target[0], scale_factor * target[1]), order=4, - source_order=5, source_density=source_density, - target_density=target_density, ) - expected = expected_scaled_laplace_self_interaction( + expected = expected_scaled_laplace_point_potential( value, template_density_mass(fan, order=4, density=source_density), scale_factor, - target_mass=template_density_mass(fan, order=4, density=target_density), ) assert scaled == pytest.approx(expected, abs=1.0e-13) @@ -163,6 +147,10 @@ def test_baseline_experiment_records_feasibility_checks() -> None: assert result.scaled_abs_error < 1.0e-13 assert result.point_target_abs_error > 0.0 + assert result.scaled_point_target_potential == pytest.approx( + result.expected_scaled_point_target_potential, + abs=1.0e-13, + ) assert result.diagonal_remainders[-1].delta == pytest.approx(1.0e-3) assert abs(result.diagonal_remainders[-1].remainder) < abs( result.diagonal_remainders[0].remainder From f4363a270fbc1a8d380dba6168579b4a233bac4c Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 19:56:46 -0500 Subject: [PATCH 07/13] docs: use high-order point-target singular split Revise the point-target singular split to use a high-order Taylor-jet distance model instead of a leading metric-only model. Also adjust display equations to use aligned environments and scalable matrix delimiters so GitHub Markdown renders the formulas correctly.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: d6bbf8d080ee --- docs/nearfield-template-experiments.md | 176 ++++++++++++++++--------- 1 file changed, 116 insertions(+), 60 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 5ef90e3..3439f9b 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -11,16 +11,20 @@ folded decomposition: a fan from a seed point `V` to a smooth Bezier trim curve `C(t)`. A degree-`p` Bezier edge is $$ -C(t) = \sum_{a=0}^p B_a^p(t)P_a, -\qquad B_a^p(t)=\binom{p}{a}(1-t)^{p-a}t^a, +\begin{aligned} +C(t) &= \sum_{a=0}^p B_a^p(t)P_a, \\ +B_a^p(t) &= \binom{p}{a}(1-t)^{p-a}t^a, \qquad 0\le t\le 1. +\end{aligned} $$ The folded chart is $$ -T(r,t) = (1-r)V + rC(t), +\begin{aligned} +T(r,t) &= (1-r)V + rC(t), \qquad 0\le r,t\le 1. +\end{aligned} $$ Its Jacobian is @@ -80,72 +84,106 @@ The experiment should classify target/source geometry as: ## Point-Target Singular Split -For a target point near the source chart, let `z_x` be a closest or projected -template coordinate with `T(z_x)` near `x`, and write `xi = z_x + delta`. Then +For a target point near the source chart, choose a template coordinate `z_x` +whose mapped source point `T(z_x)` is closest to `x`, or otherwise represents the +local source point responsible for the near-singular behavior. Write nearby +source coordinates as $$ -T(z_x+\delta)-T(z_x) = DT(z_x)\delta + O(|\delta|^2), +\begin{aligned} +\xi &= z_x + \delta, \\ +d &= x - T(z_x). +\end{aligned} $$ +Here `delta` is the local source displacement away from the nearest source point, +and `d` is the physical target offset from that point. Instead of keeping only +the first metric term, use a high-order Taylor jet of the chart around `z_x`: + $$ -|T(z_x+\delta)-T(z_x)| = -\sqrt{\delta^T M(z_x)\delta}\,(1+O(|\delta|)), -\qquad M(z_x) = DT(z_x)^TDT(z_x). +\begin{aligned} +T(z_x+\delta) +&= T(z_x) + \sum_{1\le |\alpha|\le K} \\ +&\quad \frac{1}{\alpha!}\partial^\alpha T(z_x)\delta^\alpha \\ +&\quad + R_{K+1}(\delta). +\end{aligned} $$ -For an on-surface or asymptotically close target, this gives the leading model +Define the order-`K` displacement polynomial $$ -G(x,T(z_x+\delta)) -= -\frac{1}{2\pi}\log\sqrt{\delta^T M(z_x)\delta} -+ \text{lower-order correction}. +\begin{aligned} +P_K(\delta;z_x,d) +&= d - \sum_{1\le |\alpha|\le K} \\ +&\quad \frac{1}{\alpha!}\partial^\alpha T(z_x)\delta^\alpha. +\end{aligned} $$ -For an off-surface target, include the normal/offset residual `d = x-T(z_x)`: +The singular model for the point-target kernel is then $$ -|x-T(z_x+\delta)|^2 -= |d-DT(z_x)\delta|^2 + \text{higher-order chart terms}. +\begin{aligned} +G_K^{\mathrm{sing}}(x,z_x,\delta) +&= -\frac{1}{2\pi}\log |P_K(\delta;z_x,d)|. +\end{aligned} $$ -The reusable part would be template-space singular model integrals or correction -operators. The runtime payload remains map coefficients, Jacobian data, source -coefficients, target-node offsets, and correction moment/interpolation data. For -a Bezier fan map, `T(r,t)` is polynomial in `(r,t)` and contains mixed -higher-order terms from `rC(t)`. Subtracting only the local metric model leaves -an `O(|delta|)` correction whose first derivative at the singular point can -depend on approach direction. A smooth-remainder expansion should therefore -either include higher-order distance terms in the singular model or treat this -first prototype as a leading singular split plus a bounded local correction. +For an on-surface target, `d=0`. For an off-surface target, `d` carries the +normal and tangential target offset. Increasing `K` moves curvature and mixed +terms from the remainder into the singular model. If `C(t)` is a degree-`p` +Bezier curve, the fan map `T(r,t)=(1-r)V+rC(t)` is a polynomial of total degree +`p+1` in `(r,t)`, so the Taylor jet terminates once `K >= p+1`. For +rational/NURBS-derived charts, `K` is a truncation order chosen by the requested +accuracy. + +The reusable part is a family of template-space singular model integrals or +correction operators parameterized by the finite jet data +$\{\partial^\alpha T(z_x)\}$ and the target offset `d`. The runtime payload remains +map coefficients, Jacobian data, source coefficients, target-node offsets, and +high-order correction or interpolation data. ## Metric-Field Expansion Tables The table-building objective is to separate fixed singular template integrals from per-chart smooth geometry and per-target offset coefficients. For a target node represented by offset coordinates `tau`, use local source coordinates near -the closest chart point and write `xi = z_x + delta`. The on-surface singular +the closest chart point and write `xi = z_x + delta`. The high-order singular distance model is $$ -|T(z_x+\delta)-T(z_x)|^2 -= \delta^T M(z_x)\delta + \text{higher-order chart terms}, -\qquad M(z_x)=DT(z_x)^TDT(z_x). +|x-T(z_x+\delta)|^2 \approx |P_K(\delta;z_x,d)|^2. +$$ + +For `K=1` and an on-surface target, this reduces to the metric model + +$$ +\begin{aligned} +|P_1(\delta;z_x,0)|^2 &= \delta^T M(z_x)\delta, \\ +M(z_x) &= DT(z_x)^TDT(z_x). +\end{aligned} $$ -Choose a positive-definite reference metric `M0` for one metric bin, chart -family, or local average, and write +The higher-order table strategy uses the full finite jet, not just `M(z_x)`. The +metric field remains the first term and a useful organizing parameter, but the +practical expansion variables are the coefficients of `P_K` plus the target +offset `d`. + +Choose a reference jet, beginning with a positive-definite reference metric `M0` +and reference higher-order coefficients for one metric/curvature bin, chart +family, or local average. For the first metric term, write $$ M(z) = M_0 + \Delta M(z). $$ -Then the logarithmic singular factor can be expanded as +Then the metric contribution to the logarithmic singular factor can be expanded as $$ +\begin{aligned} \log(\delta^T M(z)\delta) -= \log(\delta^T M_0\delta) -+ \log\left(1+ -\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right). +&= \log(\delta^T M_0\delta) \\ +&\quad + \log\left(1+\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right). +\end{aligned} $$ If the metric family is binned or normalized so that @@ -157,34 +195,41 @@ $$ then $$ +\begin{aligned} \log(\delta^T M(z)\delta) -= \log(\delta^T M_0\delta) -+ \sum_{k\ge 1}\frac{(-1)^{k+1}}{k} -\left(\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right)^k. +&= \log(\delta^T M_0\delta) \\ +&\quad + \sum_{k\ge 1}\frac{(-1)^{k+1}}{k}\left(\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right)^k. +\end{aligned} $$ -Thus the kernel singular part has the schematic expansion +The same expansion idea applies to the full high-order polynomial distance by +expanding the coefficients of `P_K` around the reference jet. Thus the kernel +singular part has the schematic expansion $$ -G_{\mathrm{sing}}(x,T(z_x+\delta)) -\approx \sum_\alpha c_\alpha(z_x,\tau)S_\alpha(\delta;M_0), +\begin{aligned} +G_K^{\mathrm{sing}}(x,z_x,\delta) +&\approx \sum_\alpha c_\alpha(z_x,\tau)S_\alpha(\delta;\mathcal J_0). +\end{aligned} $$ -where `S_alpha` are fixed singular template functions for the selected reference -metric and `c_alpha(z_x,tau)` are smooth functions of the entries of -`Delta M(z_x)` and the target offset data. The expansion can be truncated either -by polynomial order in `Delta M`, by interpolation in metric/offset space, or by -a learned/empirical low-rank basis. -For full smooth-remainder tables near a chart diagonal, the singular model should -also include enough higher-order chart-distance terms to remove direction- -dependent local corrections. +where $\mathcal J_0$ denotes the selected reference jet. The fixed functions +`S_alpha` depend only on template displacement and the reference jet, while +`c_alpha(z_x,tau)` are smooth functions of metric entries, higher-order chart +derivatives, and target offset data. The expansion can be truncated by polynomial +order in the jet perturbation, by interpolation in jet/offset space, or by a +learned/empirical low-rank basis. For one target node and one source density expansion mode `rho_j`, the singular contribution becomes $$ -u_j^{\mathrm{sing}}(\tau) \approx \sum_\alpha \int_{[0,1]^2} -\rho_j(\xi)c_\alpha(z_x,\tau)S_\alpha(\xi-z_x;M_0)J(\xi)\,d\xi. +\begin{aligned} +u_j^{\mathrm{sing}}(\tau) +&\approx \sum_\alpha \int_{[0,1]^2} \\ +&\quad \rho_j(\xi)c_\alpha(z_x,\tau) \\ +&\quad \times S_\alpha(\xi-z_x;\mathcal J_0)J(\xi)\,d\xi. +\end{aligned} $$ Expand the smooth per-chart/per-target factor in a template basis `p_beta`: @@ -196,13 +241,18 @@ $$ Then $$ -u_j^{\mathrm{sing}}(\tau) \approx -\sum_{\alpha,\beta}q_{\alpha\beta}(\tau)T_{j\alpha\beta}, +\begin{aligned} +u_j^{\mathrm{sing}}(\tau) +&\approx \sum_{\alpha,\beta}q_{\alpha\beta}(\tau)T_{j\alpha\beta}. +\end{aligned} $$ $$ -T_{j\alpha\beta} = \int_{[0,1]^2} -\rho_j(\xi)p_\beta(\xi)S_\alpha(\xi-z_x;M_0)\,d\xi. +\begin{aligned} +T_{j\alpha\beta} +&= \int_{[0,1]^2} \\ +&\quad \rho_j(\xi)p_\beta(\xi)S_\alpha(\xi-z_x;\mathcal J_0)\,d\xi. +\end{aligned} $$ The tensors `T_{j alpha beta}` are precomputed on fixed source template domains, @@ -220,16 +270,20 @@ $$ the metric entries are explicit: $$ -T_r = C(t)-V, -\qquad T_t = rC'(t), +\begin{aligned} +T_r &= C(t)-V, \\ +T_t &= rC'(t). +\end{aligned} $$ $$ M(r,t) = -\begin{bmatrix} +\left[ +\begin{matrix} |C(t)-V|^2 & r(C(t)-V)\cdot C'(t) \\ r(C(t)-V)\cdot C'(t) & r^2|C'(t)|^2 -\end{bmatrix}. +\end{matrix} +\right]. $$ For Bezier or piecewise-Bezier CAD trims, these entries are smooth @@ -301,8 +355,10 @@ kernel. If both the source fan and physical target point are scaled by `lambda`, then $$ +\begin{aligned} u_\lambda(\lambda x) -= \lambda^2\left(u(x) - \frac{\log(\lambda)m_\rho}{2\pi}\right), +&= \lambda^2\left(u(x) - \frac{\log(\lambda)m_\rho}{2\pi}\right). +\end{aligned} $$ where `m_rho` is the density-weighted signed source mass on the unscaled fan From 5f3f7d69e586547b210dc0fe413c4602af258846 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 21:04:00 -0500 Subject: [PATCH 08/13] docs: motivate point-target displacement polynomial Clarify that P_K is the high-order local model of the physical displacement x - T(xi), obtained by substituting the chart Taylor jet into the point-target kernel distance. This makes the critical table object explicit before defining it.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: b61a31d972cf --- docs/nearfield-template-experiments.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 3439f9b..74a40b1 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -97,8 +97,17 @@ d &= x - T(z_x). $$ Here `delta` is the local source displacement away from the nearest source point, -and `d` is the physical target offset from that point. Instead of keeping only -the first metric term, use a high-order Taylor jet of the chart around `z_x`: +and `d` is the physical target offset from that point. The kernel depends on the +physical displacement from the source point to the target: + +$$ +x - T(\xi) = x - T(z_x+\delta). +$$ + +The purpose of the singular split is to approximate this displacement accurately +near `delta=0`, while keeping the approximation expressed on the fixed source +template. Instead of keeping only the first metric term, replace the chart by its +order-`K` Taylor jet around `z_x`: $$ \begin{aligned} @@ -109,7 +118,9 @@ T(z_x+\delta) \end{aligned} $$ -Define the order-`K` displacement polynomial +Substituting this jet into `x - T(z_x+delta)` gives a polynomial approximation to +the target-to-source displacement. This is the critical object to tabulate +against: $$ \begin{aligned} @@ -119,6 +130,10 @@ P_K(\delta;z_x,d) \end{aligned} $$ +Thus `P_K` is not an extra geometric map; it is the high-order local model of +the physical vector `x - T(xi)`. Its coefficients are exactly the chart jet at +`z_x` plus the target offset `d`. + The singular model for the point-target kernel is then $$ From 2908753ba6e076a92fa7fe53924730095ad63942 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 21:58:56 -0500 Subject: [PATCH 09/13] docs: clarify full-jet expansion tables Rewrite the metric-field expansion tables section around the full high-order displacement polynomial P_K for both on-surface and off-surface point targets. The metric-only model is now explicitly a K=1, d=0 sanity case rather than the intended table construction.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 26fe5c4309e9 --- docs/nearfield-template-experiments.md | 91 +++++++++++++++----------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 74a40b1..9ee21ec 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -160,16 +160,28 @@ high-order correction or interpolation data. ## Metric-Field Expansion Tables The table-building objective is to separate fixed singular template integrals -from per-chart smooth geometry and per-target offset coefficients. For a target -node represented by offset coordinates `tau`, use local source coordinates near -the closest chart point and write `xi = z_x + delta`. The high-order singular -distance model is +from per-chart smooth geometry and per-target offset coefficients. The table is +not built from the metric term alone. It is built from the high-order +target-to-source displacement polynomial `P_K`. + +For a target node represented by offset coordinates `tau`, write source points +near the closest chart point as `xi = z_x + delta`. The singular distance model is $$ |x-T(z_x+\delta)|^2 \approx |P_K(\delta;z_x,d)|^2. $$ -For `K=1` and an on-surface target, this reduces to the metric model +The coefficients of `P_K` are the runtime geometry payload: + +$$ +\mathcal J_K(z_x,d) += \left(d,\{\partial^\alpha T(z_x):1\le |\alpha|\le K\}\right). +$$ + +This includes both on-surface and off-surface targets. On-surface targets have +`d=0`; off-surface targets have nonzero `d`, which may include both normal and +tangential offset components. The metric field is only the first quadratic part +of this larger jet. In the special case `K=1` and `d=0`, the model reduces to $$ \begin{aligned} @@ -178,48 +190,50 @@ M(z_x) &= DT(z_x)^TDT(z_x). \end{aligned} $$ -The higher-order table strategy uses the full finite jet, not just `M(z_x)`. The -metric field remains the first term and a useful organizing parameter, but the -practical expansion variables are the coefficients of `P_K` plus the target -offset `d`. - -Choose a reference jet, beginning with a positive-definite reference metric `M0` -and reference higher-order coefficients for one metric/curvature bin, chart -family, or local average. For the first metric term, write +That special case is useful for sanity checks, but it is not the intended table +construction. The practical table construction chooses a reference jet +$\mathcal J_0$ for a geometry/target-offset bin and expands the actual jet around +it: $$ -M(z) = M_0 + \Delta M(z). +\mathcal J_K(z_x,d) = \mathcal J_0 + \Delta\mathcal J(z_x,d). $$ -Then the metric contribution to the logarithmic singular factor can be expanded as +Equivalently, write the displacement polynomial as a reference model plus a +smooth perturbation: $$ \begin{aligned} -\log(\delta^T M(z)\delta) -&= \log(\delta^T M_0\delta) \\ -&\quad + \log\left(1+\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right). +P_K(\delta;z_x,d) +&= P_K^0(\delta) + \Delta P_K(\delta;z_x,d). \end{aligned} $$ -If the metric family is binned or normalized so that +Then the log kernel can be expanded around the reference displacement: $$ -\left|\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right| < 1, +\begin{aligned} +\log |P_K(\delta;z_x,d)| +&= \log |P_K^0(\delta)| \\ +&\quad + \frac{1}{2}\log\left(1+R_K(\delta;z_x,d)\right), +\end{aligned} $$ -then +where $$ \begin{aligned} -\log(\delta^T M(z)\delta) -&= \log(\delta^T M_0\delta) \\ -&\quad + \sum_{k\ge 1}\frac{(-1)^{k+1}}{k}\left(\frac{\delta^T\Delta M(z)\delta}{\delta^T M_0\delta}\right)^k. +R_K(\delta;z_x,d) +&= \frac{N_K(\delta;z_x,d)}{|P_K^0(\delta)|^2}, \\ +N_K(\delta;z_x,d) +&= 2P_K^0(\delta)\cdot\Delta P_K(\delta;z_x,d) \\ +&\quad + |\Delta P_K(\delta;z_x,d)|^2. \end{aligned} $$ -The same expansion idea applies to the full high-order polynomial distance by -expanding the coefficients of `P_K` around the reference jet. Thus the kernel -singular part has the schematic expansion +When each bin is chosen so the perturbation ratio is controlled, this expression +can be expanded in powers, interpolation modes, or a low-rank basis in the jet +perturbation coefficients. The resulting singular model has the schematic form $$ \begin{aligned} @@ -228,12 +242,11 @@ G_K^{\mathrm{sing}}(x,z_x,\delta) \end{aligned} $$ -where $\mathcal J_0$ denotes the selected reference jet. The fixed functions -`S_alpha` depend only on template displacement and the reference jet, while -`c_alpha(z_x,tau)` are smooth functions of metric entries, higher-order chart -derivatives, and target offset data. The expansion can be truncated by polynomial -order in the jet perturbation, by interpolation in jet/offset space, or by a -learned/empirical low-rank basis. +Here `S_alpha` are fixed template functions for the selected reference jet, and +`c_alpha(z_x,tau)` are smooth functions of the target offset `d`, the metric +entries, and all higher-order chart derivatives included in `P_K`. For Bezier +fan charts, these jet coefficients are smooth functions of the seed point and +Bezier control points. For one target node and one source density expansion mode `rho_j`, the singular contribution becomes @@ -247,13 +260,17 @@ u_j^{\mathrm{sing}}(\tau) \end{aligned} $$ -Expand the smooth per-chart/per-target factor in a template basis `p_beta`: +If the smooth per-chart/per-target factor is also expanded in a template basis +`p_beta`, $$ -c_\alpha(z_x,\tau)J(\xi) \approx \sum_\beta q_{\alpha\beta}(\tau)p_\beta(\xi). +\begin{aligned} +c_\alpha(z_x,\tau)J(\xi) +&\approx \sum_\beta q_{\alpha\beta}(\tau)p_\beta(\xi), +\end{aligned} $$ -Then +then the runtime evaluation uses precomputed source-template tables: $$ \begin{aligned} @@ -273,7 +290,7 @@ $$ The tensors `T_{j alpha beta}` are precomputed on fixed source template domains, or tabulated over a small target-offset grid. At runtime, each folded chart and target node only supply the coefficients `q_{alpha beta}(tau)` from smooth -metric/Jacobian fields, target offset data, and any higher-order correction or +jet/Jacobian fields, target offset data, and any higher-order correction or remainder representation. For the Bezier fan map From 4f5040c0ba05c14e92a6a2ed42cc433cb01726b6 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 22:11:48 -0500 Subject: [PATCH 10/13] docs: update near-field feasibility assessment Revise the feasibility assessment to reflect the full point-target displacement-jet analysis. The reusable object is now framed as reference-jet tables plus compact interpolation or low-rank coefficients, with metric-only organization called out as a possible subcase rather than the main hypothesis.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 9b045011c52a --- docs/nearfield-template-experiments.md | 65 +++++++++++++++----------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 9ee21ec..6a978ad 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -320,30 +320,39 @@ $$ For Bezier or piecewise-Bezier CAD trims, these entries are smooth low-parameter functions of `(r,t)`, the seed `V`, and the Bezier control points -`P_a`. This is the mathematical reason precomputed template tables can apply to -each chart piece: the singular table is fixed after choosing `M0`, while -observed folded-decomposition geometry should enter through a small smooth -expansion of `M`, `J`, and the higher-order correction data. +`P_a`. They are the first terms of the larger displacement-jet payload. The +mathematical reason precomputed template tables may apply is not that `M` alone +is universal, but that the full finite jet $\mathcal J_K$, the Jacobian, and the +target offset vary smoothly across the folded-decomposition chart family. ## Feasibility Result -The answer is qualified yes. Singular or nearly singular folded-piece -interactions can be moved to fixed template domains, but not to a finite exact -table independent of geometry. The singular coordinate form is reusable; the -metric and higher-order geometry terms enter through smooth geometry-dependent -payloads. +The answer is qualified yes. Singular or nearly singular source-folded-piece to +physical-point interactions can be moved to fixed source template domains. The +reusable object is not a finite exact table independent of geometry. It is a +family of reference-jet tables plus interpolation, expansion, or low-rank +coefficients for the runtime displacement jet. -The practical hypothesis is stronger than carrying `M(z)` pointwise at runtime: -because the folded maps have smooth, low-parameter structure away from collapsed -seed faces, the metric field `M(z)` should vary smoothly over the template and -across the geometry families produced by folded decomposition. A functional -expansion in the metric data, for example moments, interpolation coefficients, -or a low-rank basis in `M` and the correction terms, may cover enough practical -cut-piece cases to make reusable near-field corrections worthwhile. +The practical hypothesis is now about the compactness of the full point-target +payload, not only the metric field. For each near target, the relevant runtime +data is -The open numerical question is therefore whether the smooth dependence on `M`, -seed location, and curve coefficients is compact enough to approximate by such a -functional expansion for useful geometry families. +$$ +\mathcal J_K(z_x,d),\quad J(\xi),\quad \rho(\xi),\quad \tau, +$$ + +where $\mathcal J_K$ contains the target offset and all chart derivatives used by +`P_K`. Because Bezier fan maps have smooth, low-parameter structure away from +collapsed seed faces, these jet coefficients should vary smoothly across the +folded-decomposition cases produced by CAD-like trims. A functional expansion in +the jet data, target-offset data, and Jacobian/density factors may therefore +cover enough practical cases to make precomputed near-field tables worthwhile. + +The open numerical question is whether the observed set of jets and target +offsets is compact or low-rank enough after binning by reference jet +$\mathcal J_0$. +If many bins or many expansion modes are required, the technique may not be +worthwhile even though the template formulation is mathematically valid. ## Measurements @@ -354,10 +363,14 @@ For each geometry and interaction case, record: - template singular-correction error; - higher-order correction/remainder approximation error; - dependence on quadrature order; -- dependence on seed location and curve coefficients; -- size, rank, and smoothness of geometry-parameterized correction data; -- how many metric-field expansion modes are needed for the observed folded - decomposition cases. +- dependence on target offset, including on-surface, near-surface, containing-box, + and neighbor-box target nodes; +- dependence on seed location and Bezier curve coefficients; +- size, rank, and smoothness of the reference-jet correction data; +- how many reference-jet bins and jet-expansion modes are needed for the observed + folded-decomposition cases; +- whether metric-only organization is sufficient for any subfamily, or whether + higher-order jet coefficients dominate the correction size. The first geometry family should be CAD-independent but CAD-realistic: quadratic and cubic Bezier fan charts with seed locations chosen to produce positive, @@ -403,8 +416,8 @@ error. The idea is feasible as a CUTKIT experiment, with these limits: - Far-field source clouds can remain ordinary signed quadrature sources. -- Near-field correction reuse should target template singular bases plus - functional expansions in the smooth metric field `M(z)` and higher geometry - terms. +- Near-field correction reuse should target point-target template singular bases + plus functional expansions in the smooth high-order displacement jet, target + offset, Jacobian, and density data. - Volumential should still own tree/list composition; CUTKIT should export the local geometry/operator payloads needed by those lists. From c1264d0f68ab8c28d36c5d1eae1843766cb0e51d Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 22:19:41 -0500 Subject: [PATCH 11/13] docs: add Gaussian-window near-field split Document the Gaussian-window kernel split proposed for controlling point-target near-field tables. The singular table is now restricted to targets inside or very close to each fan piece, while the smooth remainder is handled by ordinary folded-decomposition quadrature.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 3c186d8f69f9 --- docs/nearfield-template-experiments.md | 87 +++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 6a978ad..93adc26 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -293,6 +293,63 @@ target node only supply the coefficients `q_{alpha beta}(tau)` from smooth jet/Jacobian fields, target offset data, and any higher-order correction or remainder representation. +## Gaussian Window Split + +A more controlled near-field experiment is to split the kernel before building +tables. Choose a length scale `sigma`, usually tied to the local box size or fan +diameter, and a smooth radial window `w_sigma(r)` that is near one at `r=0` and +rapidly decays to numerical zero for `r` larger than a few `sigma`. For the 2D +Laplace kernel, write + +$$ +\begin{aligned} +G(x,y) &= G_{\mathrm{sing},\sigma}(x,y) + G_{\mathrm{smooth},\sigma}(x,y), \\ +G_{\mathrm{sing},\sigma}(x,y) &= w_\sigma(|x-y|)G(x,y), \\ +G_{\mathrm{smooth},\sigma}(x,y) &= \left(1-w_\sigma(|x-y|)\right)G(x,y). +\end{aligned} +$$ + +The smooth part can be handled by ordinary folded-decomposition quadrature on +the source fan: + +$$ +\begin{aligned} +u_{\mathrm{smooth},\sigma}(x) +&= \int_{[0,1]^2}G_{\mathrm{smooth},\sigma}(x,T(\xi)) \\ +&\quad \times \rho(T(\xi))J(\xi)\,d\xi. +\end{aligned} +$$ + +The singular-windowed part is numerically local: + +$$ +\begin{aligned} +u_{\mathrm{sing},\sigma}(x) +&= \int_{[0,1]^2}G_{\mathrm{sing},\sigma}(x,T(\xi)) \\ +&\quad \times \rho(T(\xi))J(\xi)\,d\xi. +\end{aligned} +$$ + +Because `G_{sing,sigma}` is compactly supported to numerical tolerance, this +term matters only when the physical target point lies inside the fan piece or +within the chosen window radius of it. For target nodes in neighboring boxes that +are outside this support, the singular table contribution is skipped and the +ordinary folded quadrature of the smooth part is sufficient. + +This changes the table problem substantially. The expensive singular table only +needs to cover a restricted local target set: + +- target points on or inside the source fan; +- target points within a few `sigma` of the fan boundary; +- target offsets whose support intersects the fan under the high-order local + displacement model `P_K`. + +The smooth remainder no longer needs singular quadrature or reference-jet tables; +it is evaluated directly with the same signed folded quadrature machinery used +for far-field source clouds. The open design choices are the window family, the +scale `sigma`, and the criterion used to skip the singular table for targets +outside the window support. + For the Bezier fan map $$ @@ -327,11 +384,13 @@ target offset vary smoothly across the folded-decomposition chart family. ## Feasibility Result -The answer is qualified yes. Singular or nearly singular source-folded-piece to -physical-point interactions can be moved to fixed source template domains. The -reusable object is not a finite exact table independent of geometry. It is a -family of reference-jet tables plus interpolation, expansion, or low-rank -coefficients for the runtime displacement jet. +The answer is more favorable with a Gaussian-window split. Singular or nearly +singular source-folded-piece to physical-point interactions can be moved to fixed +source template domains, and the tabled part can be restricted to targets inside +or very close to the fan piece. The reusable object is still not a finite exact +table independent of geometry. It is a local family of reference-jet tables plus +interpolation, expansion, or low-rank coefficients for the runtime displacement +jet. The practical hypothesis is now about the compactness of the full point-target payload, not only the metric field. For each near target, the relevant runtime @@ -347,12 +406,16 @@ collapsed seed faces, these jet coefficients should vary smoothly across the folded-decomposition cases produced by CAD-like trims. A functional expansion in the jet data, target-offset data, and Jacobian/density factors may therefore cover enough practical cases to make precomputed near-field tables worthwhile. +The Gaussian-window split improves the odds because the table does not need to +represent weakly near or well-separated target interactions; those move to the +smooth folded-quadrature path. The open numerical question is whether the observed set of jets and target offsets is compact or low-rank enough after binning by reference jet $\mathcal J_0$. -If many bins or many expansion modes are required, the technique may not be -worthwhile even though the template formulation is mathematically valid. +If many bins or many expansion modes are required even after windowing, the +technique may not be worthwhile even though the template formulation is +mathematically valid. ## Measurements @@ -361,7 +424,9 @@ For each geometry and interaction case, record: - reference value; - ordinary pulled-forward tensor-product quadrature error; - template singular-correction error; +- Gaussian-window smooth-part quadrature error; - higher-order correction/remainder approximation error; +- sensitivity to the window scale `sigma` and support cutoff; - dependence on quadrature order; - dependence on target offset, including on-surface, near-surface, containing-box, and neighbor-box target nodes; @@ -371,6 +436,8 @@ For each geometry and interaction case, record: folded-decomposition cases; - whether metric-only organization is sufficient for any subfamily, or whether higher-order jet coefficients dominate the correction size. +- how often source-box and neighbor-box target nodes actually require the + singular table after the window-support test. The first geometry family should be CAD-independent but CAD-realistic: quadratic and cubic Bezier fan charts with seed locations chosen to produce positive, @@ -416,8 +483,8 @@ error. The idea is feasible as a CUTKIT experiment, with these limits: - Far-field source clouds can remain ordinary signed quadrature sources. -- Near-field correction reuse should target point-target template singular bases - plus functional expansions in the smooth high-order displacement jet, target - offset, Jacobian, and density data. +- Near-field correction reuse should target a Gaussian-windowed point-target + singular table for targets inside or very close to each fan piece. The smooth + remainder should use ordinary folded-decomposition quadrature. - Volumential should still own tree/list composition; CUTKIT should export the local geometry/operator payloads needed by those lists. From 92d2324e7d054e1adf01aa3d385342f4a27debb1 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 22:22:34 -0500 Subject: [PATCH 12/13] docs: add target-centered Duffy near-field path Document the simplification for interior point targets: choose the folded-decomposition anchor at the target and refold the source panel so the logarithmic singularity sits at the Duffy apex. This reduces the table problem to near-boundary/outside targets after Gaussian windowing.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: bd206c686530 --- docs/nearfield-template-experiments.md | 45 +++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 93adc26..397745e 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -344,6 +344,35 @@ needs to cover a restricted local target set: - target offsets whose support intersects the fan under the high-order local displacement model `P_K`. +There is an additional simplification for targets inside the source cut region. +Folded decomposition does not require a fixed seed; for a point target `x` inside +the cut region, choose the decomposition anchor to be `x` itself and refold the +local source panel around that target. Each resulting fan has the form + +$$ +\begin{aligned} +T_x(r,t) &= (1-r)x + rC(t). +\end{aligned} +$$ + +The point singularity is then exactly at the Duffy apex `r=0`, and the Jacobian +contributes the usual factor `r`. For the logarithmic kernel, the local integrand +has the form + +$$ +\begin{aligned} +G(x,T_x(r,t))J_x(r,t) +&\sim -\frac{1}{2\pi}\log(r|C(t)-x|)\,r\,\det(C(t)-x,C'(t)), +\end{aligned} +$$ + +which is integrable in the Duffy coordinate. This means inside-target cases may +not need a general reference-jet table at all: they can use target-centered +folded/Duffy quadrature as the singular treatment. The remaining table problem is +then concentrated on targets just outside the fan, near boundaries, or otherwise +too close for the smooth quadrature but not eligible for target-centered +refolding. + The smooth remainder no longer needs singular quadrature or reference-jet tables; it is evaluated directly with the same signed folded quadrature machinery used for far-field source clouds. The open design choices are the window family, the @@ -387,10 +416,13 @@ target offset vary smoothly across the folded-decomposition chart family. The answer is more favorable with a Gaussian-window split. Singular or nearly singular source-folded-piece to physical-point interactions can be moved to fixed source template domains, and the tabled part can be restricted to targets inside -or very close to the fan piece. The reusable object is still not a finite exact +or very close to the fan piece. For targets inside the cut region, a +target-centered Duffy refolding may handle the singularity directly, reducing the +need for tabulation even further. The reusable object is still not a finite exact table independent of geometry. It is a local family of reference-jet tables plus interpolation, expansion, or low-rank coefficients for the runtime displacement -jet. +jet, focused on the near-boundary/outside cases that remain after the Duffy and +window-support tests. The practical hypothesis is now about the compactness of the full point-target payload, not only the metric field. For each near target, the relevant runtime @@ -408,7 +440,9 @@ the jet data, target-offset data, and Jacobian/density factors may therefore cover enough practical cases to make precomputed near-field tables worthwhile. The Gaussian-window split improves the odds because the table does not need to represent weakly near or well-separated target interactions; those move to the -smooth folded-quadrature path. +smooth folded-quadrature path. Target-centered refolding improves the odds again +for interior targets because the singularity is placed at the Duffy apex instead +of being represented by a generic local jet table. The open numerical question is whether the observed set of jets and target offsets is compact or low-rank enough after binning by reference jet @@ -438,6 +472,8 @@ For each geometry and interaction case, record: higher-order jet coefficients dominate the correction size. - how often source-box and neighbor-box target nodes actually require the singular table after the window-support test. +- among supported targets, how many can use target-centered Duffy refolding + instead of a reference-jet table. The first geometry family should be CAD-independent but CAD-realistic: quadratic and cubic Bezier fan charts with seed locations chosen to produce positive, @@ -485,6 +521,7 @@ The idea is feasible as a CUTKIT experiment, with these limits: - Far-field source clouds can remain ordinary signed quadrature sources. - Near-field correction reuse should target a Gaussian-windowed point-target singular table for targets inside or very close to each fan piece. The smooth - remainder should use ordinary folded-decomposition quadrature. + remainder should use ordinary folded-decomposition quadrature. Interior targets + should first try target-centered Duffy refolding before falling back to tables. - Volumential should still own tree/list composition; CUTKIT should export the local geometry/operator payloads needed by those lists. From d62167a8e4d3542039fea7bcd6c480acdbc83692 Mon Sep 17 00:00:00 2001 From: xywei Date: Fri, 12 Jun 2026 22:26:11 -0500 Subject: [PATCH 13/13] docs: clarify windowed singular asymptotics Update the Gaussian-window split to reflect the DMK-style approach: evaluate the compact local singular part with analytic or semi-analytic asymptotic expansion, while using ordinary folded quadrature for the smooth remainder. The local table target is now asymptotic moments/coefficient maps rather than a generic near-field table.\n\nValidation: uv run python scripts/check_docs_freshness.py Entire-Checkpoint: 4d7a58573bba --- docs/nearfield-template-experiments.md | 97 ++++++++++++++------------ 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/docs/nearfield-template-experiments.md b/docs/nearfield-template-experiments.md index 397745e..4799d6e 100644 --- a/docs/nearfield-template-experiments.md +++ b/docs/nearfield-template-experiments.md @@ -293,13 +293,13 @@ target node only supply the coefficients `q_{alpha beta}(tau)` from smooth jet/Jacobian fields, target offset data, and any higher-order correction or remainder representation. -## Gaussian Window Split +## Gaussian Window And Asymptotic Local Part -A more controlled near-field experiment is to split the kernel before building -tables. Choose a length scale `sigma`, usually tied to the local box size or fan -diameter, and a smooth radial window `w_sigma(r)` that is near one at `r=0` and -rapidly decays to numerical zero for `r` larger than a few `sigma`. For the 2D -Laplace kernel, write +A more controlled near-field experiment is to split the kernel before deciding +what must be tabled. Choose a length scale `sigma`, usually tied to the local box +size or fan diameter, and a smooth radial window `w_sigma(r)` that is near one at +`r=0` and rapidly decays to numerical zero for `r` larger than a few `sigma`. +For the 2D Laplace kernel, write $$ \begin{aligned} @@ -333,17 +333,25 @@ $$ Because `G_{sing,sigma}` is compactly supported to numerical tolerance, this term matters only when the physical target point lies inside the fan piece or within the chosen window radius of it. For target nodes in neighboring boxes that -are outside this support, the singular table contribution is skipped and the +are outside this support, the singular local contribution is skipped and the ordinary folded quadrature of the smooth part is sufficient. -This changes the table problem substantially. The expensive singular table only -needs to cover a restricted local target set: +This changes the hard problem substantially. The windowed singular part only +needs a special local treatment on a restricted target set: - target points on or inside the source fan; - target points within a few `sigma` of the fan boundary; - target offsets whose support intersects the fan under the high-order local displacement model `P_K`. +Following the DMK-style strategy, this local treatment should not be ordinary +folded quadrature of the singular kernel. It should use an analytic or +semi-analytic asymptotic expansion of the windowed singular integral. In local +coordinates, the expansion is built from `P_K`, the Gaussian-window scale, and +smooth expansions of the density and Jacobian. Precomputation, if used, should +target reusable asymptotic moments/coefficient maps for this local expansion, +not a generic table for the whole near-field integral. + There is an additional simplification for targets inside the source cut region. Folded decomposition does not require a fixed seed; for a point target `x` inside the cut region, choose the decomposition anchor to be `x` itself and refold the @@ -368,16 +376,16 @@ $$ which is integrable in the Duffy coordinate. This means inside-target cases may not need a general reference-jet table at all: they can use target-centered -folded/Duffy quadrature as the singular treatment. The remaining table problem is -then concentrated on targets just outside the fan, near boundaries, or otherwise -too close for the smooth quadrature but not eligible for target-centered -refolding. +folded/Duffy coordinates together with the local asymptotic/windowed singular +treatment. The remaining difficult cases are targets just outside the fan, near +boundaries, or otherwise too close for the smooth quadrature but not eligible for +target-centered refolding. -The smooth remainder no longer needs singular quadrature or reference-jet tables; -it is evaluated directly with the same signed folded quadrature machinery used -for far-field source clouds. The open design choices are the window family, the -scale `sigma`, and the criterion used to skip the singular table for targets -outside the window support. +The smooth remainder no longer needs singular quadrature or local asymptotics; it +is evaluated directly with the same signed folded quadrature machinery used for +far-field source clouds. The open design choices are the window family, the scale +`sigma`, the asymptotic expansion order, and the criterion used to skip the local +singular treatment for targets outside the window support. For the Bezier fan map @@ -415,14 +423,13 @@ target offset vary smoothly across the folded-decomposition chart family. The answer is more favorable with a Gaussian-window split. Singular or nearly singular source-folded-piece to physical-point interactions can be moved to fixed -source template domains, and the tabled part can be restricted to targets inside -or very close to the fan piece. For targets inside the cut region, a -target-centered Duffy refolding may handle the singularity directly, reducing the -need for tabulation even further. The reusable object is still not a finite exact -table independent of geometry. It is a local family of reference-jet tables plus -interpolation, expansion, or low-rank coefficients for the runtime displacement -jet, focused on the near-boundary/outside cases that remain after the Duffy and -window-support tests. +source template domains, and the local singular treatment can be restricted to +targets inside or very close to the fan piece. For targets inside the cut region, +target-centered Duffy refolding can place the singularity at the apex. The +reusable object is therefore not a broad near-field table. It is more likely a +small set of asymptotic expansion formulas, moments, or coefficient maps for the +windowed local singular integral, plus direct folded quadrature for the smooth +remainder. The practical hypothesis is now about the compactness of the full point-target payload, not only the metric field. For each near target, the relevant runtime @@ -438,18 +445,19 @@ collapsed seed faces, these jet coefficients should vary smoothly across the folded-decomposition cases produced by CAD-like trims. A functional expansion in the jet data, target-offset data, and Jacobian/density factors may therefore cover enough practical cases to make precomputed near-field tables worthwhile. -The Gaussian-window split improves the odds because the table does not need to -represent weakly near or well-separated target interactions; those move to the -smooth folded-quadrature path. Target-centered refolding improves the odds again -for interior targets because the singularity is placed at the Duffy apex instead -of being represented by a generic local jet table. +The Gaussian-window split improves the odds because the local asymptotic +treatment does not need to represent weakly near or well-separated target +interactions; those move to the smooth folded-quadrature path. Target-centered +refolding improves the odds again for interior targets because the singularity is +placed at the Duffy apex instead of being represented by a generic local jet +table. The open numerical question is whether the observed set of jets and target offsets is compact or low-rank enough after binning by reference jet $\mathcal J_0$. -If many bins or many expansion modes are required even after windowing, the -technique may not be worthwhile even though the template formulation is -mathematically valid. +If many asymptotic terms, bins, or local coefficient modes are required even +after windowing and target-centered refolding, the technique may not be +worthwhile even though the template formulation is mathematically valid. ## Measurements @@ -459,21 +467,23 @@ For each geometry and interaction case, record: - ordinary pulled-forward tensor-product quadrature error; - template singular-correction error; - Gaussian-window smooth-part quadrature error; -- higher-order correction/remainder approximation error; +- Gaussian-window singular asymptotic expansion error; +- higher-order correction/remainder approximation error after the asymptotic + local part is removed; - sensitivity to the window scale `sigma` and support cutoff; - dependence on quadrature order; - dependence on target offset, including on-surface, near-surface, containing-box, and neighbor-box target nodes; - dependence on seed location and Bezier curve coefficients; -- size, rank, and smoothness of the reference-jet correction data; -- how many reference-jet bins and jet-expansion modes are needed for the observed - folded-decomposition cases; +- size, rank, and smoothness of the local asymptotic coefficient data; +- how many window/asymptotic orders, bins, and jet modes are needed for the + observed folded-decomposition cases; - whether metric-only organization is sufficient for any subfamily, or whether higher-order jet coefficients dominate the correction size. - how often source-box and neighbor-box target nodes actually require the - singular table after the window-support test. + singular local treatment after the window-support test. - among supported targets, how many can use target-centered Duffy refolding - instead of a reference-jet table. + instead of a more general local expansion. The first geometry family should be CAD-independent but CAD-realistic: quadratic and cubic Bezier fan charts with seed locations chosen to produce positive, @@ -520,8 +530,9 @@ The idea is feasible as a CUTKIT experiment, with these limits: - Far-field source clouds can remain ordinary signed quadrature sources. - Near-field correction reuse should target a Gaussian-windowed point-target - singular table for targets inside or very close to each fan piece. The smooth - remainder should use ordinary folded-decomposition quadrature. Interior targets - should first try target-centered Duffy refolding before falling back to tables. + local asymptotic expansion for targets inside or very close to each fan piece. + The smooth remainder should use ordinary folded-decomposition quadrature. + Interior targets should first try target-centered Duffy refolding before falling + back to the more general local expansion. - Volumential should still own tree/list composition; CUTKIT should export the local geometry/operator payloads needed by those lists.