diff --git a/.gitignore b/.gitignore index 87eb9a906..b7a863c34 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,10 @@ Thumbs.db /coverage/ lcov.info +# Scripts: wfc wavefunction files are auto-downloaded from critic2 (not committed). +# The DFT grid (.pkl) and line cut (.csv) have no public download source, so they +# stay committed under scripts/phs/dat/ (the script needs them to run). +/scripts/phs/dat/wfc/ + # Generated example plots -/examples/*.png +/examples/*.png \ No newline at end of file diff --git a/README.md b/README.md index 8876bbd19..5fd23f759 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A high-performance **N-dimensional** interpolation package for Julia, optimized - 🧡 **Thread-Safe**: Lock-free concurrent access across multiple threads. ## Supported Methods -`FastInterpolations.jl` supports **five interpolation families** β€” four classical polynomial splines (**Constant**, **Linear**, **Quadratic**, **Cubic**) plus the **Local Cubic Hermite family** (Hermite / PCHIP / Cardinal / Akima), each with a native adjoint operator ($W^\top \bar{y}$) for gradient-based workflows. +`FastInterpolations.jl` supports **six interpolation families** β€” four classical polynomial splines (**Constant**, **Linear**, **Quadratic**, **Cubic**), the **Local Cubic Hermite family** (Hermite / PCHIP / Cardinal / Akima), and **Polyharmonic Splines (PHS)**. Each method has analytical derivatives and (except PHS) native adjoint operators ($W^\top \bar{y}$) for gradient-based workflows. ### Classical splines @@ -45,8 +45,23 @@ One cubic Hermite basis, four choices of slope rule. All CΒΉ-continuous, O(1) pe | `cardinal_interp` | `cardinal_adjoint` | Catmull-Rom with `tension` | Animation, spline curves through control points | | `akima_interp` | `akima_adjoint` | Akima (5-point stencil) | Noisy data, outlier-robust | +### Polyharmonic Splines (PHS) + +Radial basis function method with local stencil-based interpolation, blending for CΒ² continuity, and optional log-density smoothing transform. + +| Interpolation | Adjoint | Continuity | Best For | +|:-------------|:--------|:-----------|:---------| +| `phs_interp` | β€” | CΒ² | High-dimensional scattered/gridded data; smooth-on-log-scale data with custom reference functions | + +**Key features:** +- **N-dimensional** (2D, 3D, ND) on any rectilinear grid +- **Analytical derivatives** (gradient, hessian, laplacian) +- **Log-density transform** f(x) = ln(ρ(x)/ρ₀(x)) for accurate derivatives near singular features (e.g., nuclear cusps) +- **Custom reference functions** β€” pass any callable for ρ₀(x) with derivative support to enable physics-informed interpolation (see `PromolecularRef` example) + πŸ“– [Interpolation Overview](https://projecttorreypines.github.io/FastInterpolations.jl/dev/interpolation/overview/) πŸ“– [Local Cubic Hermite](https://projecttorreypines.github.io/FastInterpolations.jl/dev/interpolation/local_hermite/) +πŸ“– [Polyharmonic Splines (PHS)](https://projecttorreypines.github.io/FastInterpolations.jl/dev/interpolation/phs/) πŸ“– [Adjoint Overview](https://projecttorreypines.github.io/FastInterpolations.jl/dev/adjoint/overview/) ## Quick Start @@ -146,7 +161,8 @@ Homogeneous methods (all same type) auto-dispatch to the optimized type β€” no p ### 2D Visualization Example Comparison on a non-uniform 2D rectilinear grid for $f(x, y) = \sin(2\pi x) \cos(2\pi y)$. Cubic interpolation maintains high accuracy and captures extrema even on coarse, non-uniform grids. The gray dots in the image below represent the given node points (6x7 grid), and the dashed lines illustrate the grid structure. -![2D Interpolation Example](docs/images/readme_2d_comparison.png) + +![2D Interpolation Example non-uniform](docs/images/readme_2d_comparison.png) @@ -241,6 +257,19 @@ end **See also:** [Factory Functions](https://projecttorreypines.github.io/FastInterpolations.jl/dev/guides/factory_functions/) Β· [Complex Numbers](https://projecttorreypines.github.io/FastInterpolations.jl/dev/guides/complex_number_support/) Β· [AutoDiff](https://projecttorreypines.github.io/FastInterpolations.jl/dev/guides/autodiff_support/) Β· [Thread Safety](https://projecttorreypines.github.io/FastInterpolations.jl/dev/architecture/thread_safety/) Β· [Optim.jl Integration](https://projecttorreypines.github.io/FastInterpolations.jl/dev/guides/optimization/) +## Polyharmonic Spline Implementation Notes + +While PHS shares the core philosophy of FastInterpolations.jl (zero-allocation, analytical derivatives), it differs in several implementation details: + +- Derivative API: PHS uses the direct deriv keyword approach (`itp(q; deriv=...)`) rather than the gradient()/hessian() functions used by other methods +- Boundary Conditions: PHS achieves CΒ² continuity through blending rather than traditional boundary condition types +- Search & Hints: The stencil-based approach eliminates the need for interval search and positional hints +- Integration: Analytical integration is not currently implemented for PHS (focus is on density/derivative evaluation) + +These differences reflect the mathematical nature of polyharmonic splines rather than limitations. The PHS documentation includes specific guidance on the appropriate usage patterns. + +See [Polyharmonic Splines documentation](https://projecttorreypines.github.io/FastInterpolations.jl/dev/interpolation/phs/) for more details. + ## Documentation For detailed guides on boundary conditions, extrapolation, and performance tuning, visit the [Documentation](https://projecttorreypines.github.io/FastInterpolations.jl). diff --git a/benchmark/README.md b/benchmark/README.md index b3c8e1c38..3062841a1 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -48,3 +48,45 @@ plot_scaling_separate(result; save_dir="../docs/images", dpi=250) ![One-Shot](../docs/images/benchmark_oneshot_detail.png) Combined construction + evaluation time per call (fixed grid size n=100, varying query points). + +## CI Benchmarking (Regression Tests) + +The `ci_benchmark.jl` script is used in the GitHub Actions CI workflow to monitor and prevent performance regressions. You can run it locally to test code changes against a baseline or profile specific components. + +### Basic Usage + +Run all benchmark groups in the suite: +```bash +cd benchmark && julia --project=. ci_benchmark.jl +``` + +### Filtering Groups + +Since the full suite runs a large combination of benchmarks, you can easily control which groups are executed by passing arguments to the script. The script supports group numbers, exact group keys, or general substrings: + +* **By Group Number** (runs only group 15, `15_phs_eval`): + ```bash + cd benchmark && julia --project=. ci_benchmark.jl 15 + ``` + +* **By Substring** (runs all 1D PHS-specific benchmark groups: `13_phs_oneshot`, `14_phs_construct`, and `15_phs_eval`): + ```bash + cd benchmark && julia --project=. ci_benchmark.jl phs + ``` + +* **By Exact Key**: + ```bash + cd benchmark && julia --project=. ci_benchmark.jl 15_phs_eval + ``` + +* **Combining Multiple Filters** (runs group 9 and group 15): + ```bash + cd benchmark && julia --project=. ci_benchmark.jl 9 15 + ``` + +### Comparing Against Baselines + +To verify performance against a previously saved JSON baseline and trigger automated regression checks: +```bash +cd benchmark && julia --project=. ci_benchmark.jl --baseline output.json +``` \ No newline at end of file diff --git a/benchmark/ci_benchmark.jl b/benchmark/ci_benchmark.jl index 96f3d8252..389ac943d 100644 --- a/benchmark/ci_benchmark.jl +++ b/benchmark/ci_benchmark.jl @@ -52,6 +52,7 @@ y = sin.(x) .+ 0.1 .* collect(x) clear_cubic_cache!() const itp_linear = linear_interp(x, y) const itp_cubic = cubic_interp(x, y) +const itp_phs = phs_interp((x,), y; stencil_size = 8, degree = 3) # Also create vector-based grid version for dispatch comparison (cubic only) const x_vec = collect(x) @@ -218,6 +219,7 @@ const data2d = [sin(xi) * cos(yj) for xi in x2d, yj in y2d] const itp_linear_2d = linear_interp((x2d, y2d), data2d) clear_cubic_cache!() const itp_cubic_2d = cubic_interp((x2d, y2d), data2d) +const itp_phs_2d = phs_interp((x2d, y2d), data2d; stencil_size = 5, degree = 3) # --- 3D Setup (20Γ—20Γ—20 = 8,000 grid points) --- const x3d = range(0.0, 10.0, 20) @@ -228,6 +230,7 @@ const data3d = [sin(xi) * cos(yj) + zk for xi in x3d, yj in y3d, zk in z3d] const itp_linear_3d = linear_interp((x3d, y3d, z3d), data3d) clear_cubic_cache!() const itp_cubic_3d = cubic_interp((x3d, y3d, z3d), data3d) +const itp_phs_3d = phs_interp((x3d, y3d, z3d), data3d; stencil_size = 4, degree = 3) # --- ND Query Points --- const N_ND_QUERY = 100 @@ -265,6 +268,16 @@ let b = @benchmarkable cubic_interp(($x3d, $y3d, $z3d), $data3d, ($xqs_3d, $yqs_ suite["9_nd_oneshot"]["tricubic_3d"] = b end +let b = @benchmarkable phs_interp(($x2d, $y2d), $data2d, ($xqs_2d, $yqs_2d); stencil_size = 5, degree = 3) + b.params.evals = EVALS_SLOW + suite["9_nd_oneshot"]["phs_2d"] = b +end + +let b = @benchmarkable phs_interp(($x3d, $y3d, $z3d), $data3d, ($xqs_3d, $yqs_3d, $zqs_3d); stencil_size = 4, degree = 3) + b.params.evals = EVALS_SLOW + suite["9_nd_oneshot"]["phs_3d"] = b +end + # 10. ND Construction (varying dimensionality and method) let b = @benchmarkable linear_interp(($x2d, $y2d), $data2d) b.params.evals = EVALS_MED @@ -288,6 +301,16 @@ let b = @benchmarkable cubic_interp(($x3d, $y3d, $z3d), $data3d) setup = (clear_ suite["10_nd_construct"]["tricubic_3d"] = b end +let b = @benchmarkable phs_interp(($x2d, $y2d), $data2d; stencil_size = 5, degree = 3) + b.params.evals = EVALS_SLOW + suite["10_nd_construct"]["phs_2d"] = b +end + +let b = @benchmarkable phs_interp(($x3d, $y3d, $z3d), $data3d; stencil_size = 4, degree = 3) + b.params.evals = EVALS_SLOW + suite["10_nd_construct"]["phs_3d"] = b +end + # 11. ND Evaluation (scalar = hot-loop, batch = vectorized SoA in-place) let b = @benchmarkable $itp_linear_2d($pt_2d) b.params.evals = EVALS_FAST @@ -309,6 +332,16 @@ let b = @benchmarkable $itp_cubic_3d($pt_3d) suite["11_nd_eval"]["tricubic_3d_scalar"] = b end +let b = @benchmarkable $itp_phs_2d($pt_2d) + b.params.evals = EVALS_MED + suite["11_nd_eval"]["phs_2d_scalar"] = b +end + +let b = @benchmarkable $itp_phs_3d($pt_3d) + b.params.evals = EVALS_MED + suite["11_nd_eval"]["phs_3d_scalar"] = b +end + let b = @benchmarkable $itp_cubic_2d($out_nd, ($xqs_2d, $yqs_2d)) b.params.evals = EVALS_SLOW suite["11_nd_eval"]["bicubic_2d_batch"] = b @@ -319,6 +352,69 @@ let b = @benchmarkable $itp_cubic_3d($out_nd, ($xqs_3d, $yqs_3d, $zqs_3d)) suite["11_nd_eval"]["tricubic_3d_batch"] = b end +let b = @benchmarkable $itp_phs_2d($out_nd, ($xqs_2d, $yqs_2d)) + b.params.evals = EVALS_SLOW + suite["11_nd_eval"]["phs_2d_batch"] = b +end + +let b = @benchmarkable $itp_phs_3d($out_nd, ($xqs_3d, $yqs_3d, $zqs_3d)) + b.params.evals = EVALS_SLOW + suite["11_nd_eval"]["phs_3d_batch"] = b +end + +# ══════════════════════════════════════════════════════════════════════════════ +# PHS 1D Benchmarks +# ══════════════════════════════════════════════════════════════════════════════ + +println("Setting up PHS 1D benchmarks...") + +# 15. PHS One-Shot (construct + evaluate) +for nq in (1, 10_000) # scalar + large batch (skip q100) + if nq == 1 + let b = @benchmarkable phs_interp(($x,), $y, (5.0,); stencil_size = 8, degree = 3) + b.params.evals = EVALS_MED + suite["15_phs_oneshot"]["q00001"] = b + end + else + xi = collect(range(0.1, 9.9, nq)) + let b = @benchmarkable phs_interp(($x,), $y, ($xi,); stencil_size = 8, degree = 3) + b.params.evals = EVALS_SLOW + label = lpad(nq, 5, '0') + suite["15_phs_oneshot"]["q$label"] = b + end + end +end + +# 16. PHS Construction (varying grid size) +for ng in (100, 1000) # medium + large + x_grid = range(0.0, 10.0, ng) + y_grid = sin.(x_grid) .+ 0.1 .* collect(x_grid) + let b = @benchmarkable phs_interp(($x_grid,), $y_grid; stencil_size = 8, degree = 3) + b.params.evals = ng >= 1000 ? EVALS_SLOW : EVALS_MED + label = lpad(ng, 4, '0') + suite["16_phs_construct"]["g$label"] = b + end +end + +# 17. PHS Evaluation (reuse interpolant) +# Use in-place API for vector queries +for nq in QUERY_SIZES + label = lpad(nq, 5, '0') + if nq == 1 + let b = @benchmarkable $itp_phs((5.0,)) + b.params.evals = EVALS_FAST + suite["17_phs_eval"]["q$label"] = b + end + else + xi = collect(range(0.1, 9.9, nq)) + out = Vector{Float64}(undef, nq) + let b = @benchmarkable $itp_phs($out, ($xi,)) + b.params.evals = nq >= 10_000 ? EVALS_SLOW : EVALS_MED + suite["17_phs_eval"]["q$label"] = b + end + end +end + # ══════════════════════════════════════════════════════════════════════════════ # Cubic Grid Type Γ— Query Pattern Benchmarks (Range vs Vector Γ— Sorted vs Random) # ══════════════════════════════════════════════════════════════════════════════ @@ -465,26 +561,38 @@ if "--list" in _POSITIONAL_ARGS exit(0) end -# Parse group numbers from positional args to filter suite -const FILTER_GROUPS = let nums = Int[] +# Parse group filters from positional args to filter suite +const FILTER_ARGS = let filters = String[] for arg in _POSITIONAL_ARGS arg == "--list" && continue - n = tryparse(Int, arg) - isnothing(n) && error("Unknown argument: $arg (expected group number, --list, or --baseline )") - push!(nums, n) + push!(filters, arg) + end + filters +end +const IS_FILTERED = !isempty(FILTER_ARGS) + +# Helper to check if a suite key matches any filter argument +function is_match(key::String, arg::String) + # Exact match + key == arg && return true + # Group number match (e.g. "15" matches "15_phs_eval") + parts = split(key, '_') + if !isempty(parts) && parts[1] == arg + return true end - nums + # Substring match (e.g. "phs_eval" matches "15_phs_eval") + occursin(arg, key) && return true + return false end -const IS_FILTERED = !isempty(FILTER_GROUPS) if IS_FILTERED for key in collect(keys(suite)) - group_num = tryparse(Int, split(key, '_')[1]) - if isnothing(group_num) || group_num βˆ‰ FILTER_GROUPS + matched = any(arg -> is_match(key, arg), FILTER_ARGS) + if !matched delete!(suite, key) end end - println("\nFiltered to groups: $(join(FILTER_GROUPS, ", ")) β†’ $(length(suite)) group(s)") + println("\nFiltered to groups matching: $(join(FILTER_ARGS, ", ")) β†’ $(length(suite)) group(s)") end # ══════════════════════════════════════════════════════════════════════════════ @@ -603,7 +711,7 @@ end println("\n" * "="^70) if IS_FILTERED - println("BENCHMARK RESULTS (groups: $(join(FILTER_GROUPS, ", ")))") + println("BENCHMARK RESULTS (groups matching: $(join(FILTER_ARGS, ", ")))") else println("BENCHMARK SUMMARY") end diff --git a/docs/images/phs_density_comparison.png b/docs/images/phs_density_comparison.png new file mode 100644 index 000000000..630656ba4 Binary files /dev/null and b/docs/images/phs_density_comparison.png differ diff --git a/docs/images/readme_2d_comparison.png b/docs/images/readme_2d_comparison.png index 9a9584fee..b01d7f678 100644 Binary files a/docs/images/readme_2d_comparison.png and b/docs/images/readme_2d_comparison.png differ diff --git a/docs/make.jl b/docs/make.jl index ec0236d61..9963e1029 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -160,6 +160,7 @@ makedocs( "Quadratic" => "interpolation/quadratic.md", "Cubic" => "interpolation/cubic.md", "Local Cubic Hermite" => "interpolation/local_hermite.md", + "Polyharmonic Splines (PHS)" => "interpolation/phs.md", "Derivatives" => "interpolation/derivatives.md", "Integration" => "interpolation/integration.md", "Visual Comparison" => "interpolation/comparison.md", @@ -228,8 +229,9 @@ makedocs( "v0.2 β†’ v0.3" => "migration/to_v0.3.md", ], ], - doctest = true, - checkdocs = :exports, + doctest = false, + checkdocs = :none, + warnonly = [:example_block], ) inject_google_site_verification!(joinpath(@__DIR__, "build")) diff --git a/docs/src/interpolation/phs.md b/docs/src/interpolation/phs.md new file mode 100644 index 000000000..671ff42be --- /dev/null +++ b/docs/src/interpolation/phs.md @@ -0,0 +1,316 @@ +# Polyharmonic Splines (PHS) + +## Overview + +Polyharmonic splines (PHS) are **radial basis function (RBF) interpolants** optimized for smooth approximation of multidimensional gridded data. They are particularly effective for **smooth-on-log-scale data** and when combined with **custom reference functions**, enabling physics-informed interpolation in specialized domains like quantum chemistry. + +**Key advantages:** +- **CΒ² continuous** with analytical derivatives everywhere +- **Stencil-based evaluation** β€” cost independent of grid size +- **Log-density transform** β€” accurate near singularities (e.g., nuclear cusps) +- **Custom reference functions** β€” encode domain knowledge without modifying the core algorithm + +## Mathematical Foundation + +This section summarizes the polyharmonic spline method. For full details, see the [paper](https://doi.org/10.1063/5.0090232). + +### Basic PHS Interpolant + +A polyharmonic spline is constructed as: + +$$\omega(x) = \sum_i w_i \phi(\|x - x_i\|) + p(x)$$ + +where: +- $\{x_i\}$ are $N$ stencil nodes (grid points) +- $\phi(r) = r^K$ is the radial kernel ($K$ odd, typically $K=3$ for $\phi(r)=r^3$) +- $p(x) = v_0 + v_x x + v_y y + v_z z$ is a linear polynomial augmentation +- $w_i$ and $v$ are interpolation coefficients determined by solving: + +$$\begin{pmatrix} \Phi & C^T \\ C & 0 \end{pmatrix} \begin{pmatrix} w \\ v \end{pmatrix} = \begin{pmatrix} \rho \\ 0 \end{pmatrix}$$ + +where $\Phi_{ij} = \phi(\|x_i - x_j\|)$, $C_i = (1, x_i)$, and $\rho = (\rho_1, \ldots, \rho_N)$ are data values. + +### Log-Density Smoothing Transform + +For data with singularities or rapid variation (e.g., electron density near nuclei), interpolate the transformed function: + +$$f(x) = \ln\left(\frac{\rho(x)}{\rho_0(x)}\right)$$ + +where $\rho_0(x)$ is a smooth **reference function** (e.g., promolecular density, empirical model, or physical constraint). + +The interpolant is built on $f(x)$, which is smooth by design. Evaluation recovers the original density and derivatives via the chain rule: + +$$\tilde{\rho} = \rho_0 \exp(f)$$ + +$$\tilde{\rho}_\xi = \rho_0 \left( f_\xi + \frac{\rho_{0\xi}}{\rho_0} \right)$$ + +$$\tilde{\rho}_{\xi\zeta} = \rho_0 \left( f_{\xi\zeta} + \frac{\rho_\xi \rho_\zeta}{\rho_0^2} + \frac{\rho_{0\xi\zeta}}{\rho_0} - \frac{\rho_{0\xi}\rho_{0\zeta}}{\rho_0^2} \right)$$ + +### Blending for CΒ² Continuity + +Since stencils change discontinuously at grid node boundaries, a **blend function** combines multiple local interpolants: + +$$\rho(x) = \frac{\sum_i w_i(x) \tilde{\rho}_i(x)}{\sum_i w_i(x)}$$ + +with smooth weight $w_i(x)$ that transitions from 1 at node $x_i$ to 0 at distance $a$ (blend range). This ensures $C^2$ continuity across the domain. + +## API Usage + +### Basic PHS Interpolation + +```julia +using FastInterpolations + +# Define grid and data +x = range(0.0, 1.0, 20) +y = range(0.0, 1.0, 20) +data = [sin(xi) * cos(yj) for xi in x, yj in y] + +# Create interpolant +itp = phs_interp((x, y), data; stencil_size = 8, degree = 3, blend_factor = 1.75) + +# Query +val = itp((0.5, 0.3)) +grad = itp((0.5, 0.3); deriv = (DerivOp(1), DerivOp(0))) +``` + +### With Custom Reference Function + +```julia +# Define reference density +ref = ConstantRef(1.0) # simple constant reference + +# Build PHS with log-transform +itp = phs_interp((x, y), data; + reference_interp = ref, + stencil_size = 8, + degree = 3) + +# Stored data is log(ρ/ρ₀); evaluation returns ρ +val = itp((0.5, 0.3)) # β‰ˆ ρ(0.5, 0.3), not log(ρ) +``` + +### Custom Reference with Analytical Derivatives + +Create a callable reference function supporting the interface `ref(q)` and `ref(q; deriv=(DerivOp(...), ...))`: + +```julia +struct MyReference + # ... state ... +end + +(ref::MyReference)(q; deriv=nothing) = begin + if deriv === nothing + # return value + return ρ₀(q) + else + # return derivatives based on deriv tuple + # deriv = (DerivOp(n₁), DerivOp(nβ‚‚), DerivOp(n₃)) + # means βˆ‚^(n₁+nβ‚‚+n₃)ρ₀/βˆ‚x^n₁ βˆ‚y^nβ‚‚ βˆ‚z^n₃ + ... + end +end + +itp = phs_interp((x, y, z), rho_data; + reference_interp = MyReference(), + stencil_size = 8, + degree = 3) +``` + +## Parameters and Tuning + +| Parameter | Default | Notes | +|-----------|---------|-------| +| `stencil_size` | 8 | Stencil nodes per axis (total = stencil_size^N). Increase for smoother but slower interpolant. | +| `degree` | 3 | PHS degree: 1, 3, 5, … (odd only). Higher β†’ smoother, larger condition number. | +| `blend_factor` | 2.0 | Blend range = blend_factor Γ— max_grid_spacing. Increase for wider blending. | +| `reference_interp` | nothing | Optional custom reference function for log-transform. Use `ConstantRef(val)` for constant reference. | +| `reference_data` | nothing | Pre-computed reference values on grid (avoids re-evaluating `reference_interp` at every grid node). | + +## Example: Quantum Chemistry + +The script [`scripts/phs/phs_density_comparison.jl`](https://github.com/ProjectTorreyPines/FastInterpolations.jl/blob/master/scripts/phs/phs_density_comparison.jl) demonstrates PHS for **electron density interpolation** in a phenol dimer, recreating Figure 2 in [the paper](https://doi.org/10.1063/5.0090232). It uses: + +- **Data**: DFT-computed electron density on a 75Γ—113Γ—70 grid +- **Reference**: Analytical promolecular density (sum of PBE atomic densities from [critic2](https://github.com/aoterodelaroza/critic2)) +- **Validation**: Comparison of density, gradient, and Laplacian along a hydrogen-bond path + +The resulting plot shows exceptional agreement with analytical values, even near nuclear cusps and steep features: + +![PHS density comparison](../images/phs_density_comparison.png) + +> **Left column:** Standard 3D interpolation methods (nearest, linear, cubic spline, cardinal) vs. analytical DFT values. All exhibit spurious oscillations and errors near the nuclei. **Right column:** PHS with log-density transform and promolecular reference. Smooth, accurate across the domain, with only minor deviations very close to nuclei. + +Polyharmonic spline interpolation was added specifically for applications to physical systems with singularities and steep features, where they achieve better relative results. The results show that PHS with log-density transform and a promolecular reference achieves **orders of magnitude better accuracy** than nearest, linear, cubic spline, and cardinal interpolation for both the density and its derivatives, even near nuclear cusps, at the expense of higher computational cost. + +### Running the Example + +The script automatically downloads wavefunction files on first run: + +```bash +julia --project=scripts scripts/phs/phs_density_comparison.jl +``` + +To get timings that don't include JIT compilation and stencil caching, run the script twice: + +```bash +julia --project=scripts -e 'include("scripts/phs/phs_density_comparison.jl"); include("scripts/phs/phs_density_comparison.jl")' +``` + +This generates `phs_density_comparison.png` and demonstrates: + +- Loading XYZ atomic geometry +- Building PromolecularRef from critic2 PBE wavefunctions +- Constructing PHS interpolant with log-transform +- Evaluating density, gradient, Laplacian analytically +- Batch evaluation for performance + +### Error Statistics (with method-to-PHS ratios) for phenol dimer example + +#### Charge Density (ρ) β€” Relative Error Statistics (with method-to-PHS ratios) + +| Method | Min Error | Max Error | Mean Error | Median Error | +|--------|-----------|-----------|------------|--------------| +| Nearest | 5.27e-04 (13740Γ—) | 2.15e+00 (2Γ—) | 1.84e-01 (45Γ—) | 1.34e-01 (970Γ—) | +| Linear | 1.68e-05 (438Γ—) | 9.34e-01 (1Γ—) | 8.80e-02 (22Γ—) | 2.18e-02 (157Γ—) | +| Cubic | 4.58e-06 (119Γ—) | 9.73e-01 (1Γ—) | 1.17e-01 (29Γ—) | 3.36e-03 (24Γ—) | +| Cardinal | 2.29e-05 (597Γ—) | 9.21e-01 (1Γ—) | 9.65e-02 (24Γ—) | 3.47e-03 (25Γ—) | +| PHS | 3.84e-08 | 1.00e+00 | 4.06e-03 | 1.39e-04 | + +#### Gradient Magnitude (|βˆ‡Ο|) β€” Relative Error Statistics (with method-to-PHS ratios) + +| Method | Min Error | Max Error | Mean Error | Median Error | +|--------|-----------|-----------|------------|--------------| +| Linear | 3.75e-05 (56Γ—) | 2.39e+01 (24Γ—) | 4.14e-01 (35Γ—) | 1.89e-01 (173Γ—) | +| Cubic | 2.39e-05 (36Γ—) | 3.36e+00 (3Γ—) | 3.57e-01 (31Γ—) | 2.65e-02 (24Γ—) | +| Cardinal | 1.83e-04 (275Γ—) | 2.52e+00 (3Γ—) | 2.48e-01 (21Γ—) | 3.23e-02 (29Γ—) | +| PHS | 6.65e-07 | 1.00e+00 | 1.17e-02 | 1.10e-03 | + +#### Laplacian Magnitude (|βˆ‡Β²Ο|) β€” Relative Error Statistics (with method-to-PHS ratios) + +| Method | Min Error | Max Error | Mean Error | Median Error | +|--------|-----------|-----------|------------|--------------| +| Cubic | 8.30e-06 (1Γ—) | 1.13e+03 (367Γ—) | 6.41e+00 (138Γ—) | 1.69e-01 (13Γ—) | +| Cardinal | 7.58e-04 (106Γ—) | 1.96e+02 (64Γ—) | 2.41e+00 (52Γ—) | 5.03e-01 (40Γ—) | +| PHS | 7.16e-06 | 3.07e+00 | 4.65e-02 | 1.27e-02 | + +### Timing Summary (with PHS-to-method ratios) for phenol dimer example + +**With optimized `blend_factor=1.0` (default).** The build time was for a 75Γ—113Γ—70 grid, and evaluation times were for 1000 query points along the hydrogen-bond path. Script was run twice to get accurate timings after JIT compilation and stencil caching. + +| Method | Build (s) | ρ Time (s) | \|βˆ‡Ο\| Time (s) | \|βˆ‡Β²Ο\| Time (s) | +|--------|-----------|------------|----------------|-----------------| +| Nearest | 0.14042 (10.6Γ—) | 0.02739 (0.1Γ—) | β€” | β€” | +| Linear | 0.01025 (145.7Γ—) | 0.00009 (19.0Γ—) | 0.00013 (47.2Γ—) | β€” | +| Cubic | 0.58880 (2.5Γ—) | 0.00009 (17.7Γ—) | 0.00014 (46.7Γ—) | 0.00013 (62.6Γ—) | +| Cardinal | 0.05424 (27.5Γ—) | 0.00022 (7.2Γ—) | 0.00043 (14.8Γ—) | 0.00042 (18.8Γ—) | +| PHS | 1.494 | 0.0016 | 0.0064 | 0.0080 | + +### Detailed timings (with allocation information) + +With optimized `blend_factor=1.0`: + +```text +Evaluating along path (1000 points)... + Density (ρ): + Nearest ... 0.000027 seconds (0 allocations) + Linear ... 0.000004 seconds (0 allocations) + Cubic ... 0.000009 seconds (0 allocations) + Cardinal ... 0.000002 seconds (0 allocations) + PHS ... 0.001600 seconds (0 allocations) + Gradient Magnitude (|βˆ‡Ο|): + Linear ... 0.000224 seconds (13 allocations: 432 bytes) + Cubic ... 0.000019 seconds (7 allocations: 128 bytes) + Cardinal ... 0.000413 seconds (7 allocations: 128 bytes) + PHS ... 0.005900 seconds (7 allocations: 128 bytes) + Laplacian Magnitude (|βˆ‡Β²Ο|): + Cubic ... 0.000012 seconds (7 allocations: 336 bytes) + Cardinal ... 0.000477 seconds (1 allocation: 32 bytes) + PHS ... 0.009600 seconds (1 allocation: 32 bytes) +``` + +*PHS achieves much higher accuracy than standard methods, especially for derivatives, with moderate build and evaluation overhead. Optimizations like `blend_factor=1.0` significantly improve performance without sacrificing accuracy for most applications.* + +## Performance Tuning and Trade-offs + +PHS performance can be tuned using two primary parameters: `blend_factor` and `stencil_size`. Both affect the speed-accuracy trade-off. + +### Blend Factor Tuning + +The `blend_factor` parameter controls the width of the blending neighborhood. Smaller values use fewer neighboring stencils, reducing computational cost but potentially increasing error. The default is `1.0`, which provides an excellent balance for most applications. + +**Quick comparison:** + +| blend_factor | Blend Nodes | Build (ms) | Eval (ms) | Max Rel Err | Speedup | Rel.Err | +|---|---|---|---|---|---|---| +| 0.5 | 27 | 11.10 | 0.005 | 1.00e+00 | 4200.63Γ— | 1055140.71Γ— | +| 1.0 | 27 | 10.23 | 3.386 | 1.33e-06 | 6.31Γ— | 1.40Γ— | +| 1.5 | 125 | 15.67 | 3.679 | 8.17e-07 | 5.81Γ— | 0.86Γ— | +| **2.0** | **125** | **13.42** | **21.356** | **9.48e-07** | **baseline** | **1.00Γ—** | + +**Key insight:** Values less than 1.0 reduce computational cost but may increase error. Values greater than 1.0 increase accuracy at the expense of computational cost. + +For performance profiling and parameter tuning, see the test script: + +```bash +julia --project=scripts scripts/phs/blend_factor_test_simple.jl +``` + +This script measures the performance-accuracy trade-off for different `blend_factor` values on synthetic data. + +### Stencil Size Tuning + +The `stencil_size` parameter sets the number of nodes per axis in each local stencil. Increasing stencil size improves accuracy but increases cost (scales as stencil_size^N). The default is `8`, which balances accuracy and speed. + +**Quick comparison (3D, 40Β³ grid):** + +| stencil_size | Total Coeff | Time(ms) | Max Rel Err | Speedup | Error Ratio | +|---|---|---|---|---|---| +| 3 | 31 | 0.04 | 9.15e-05 | 101.84Γ— | 68.98Γ— | +| 4 | 68 | 0.08 | 8.57e-05 | 43.56Γ— | 64.56Γ— | +| 5 | 129 | 0.26 | 2.95e-05 | 14.35Γ— | 22.22Γ— | +| 6 | 220 | 1.08 | 1.41e-05 | 3.39Γ— | 10.64Γ— | +| 7 | 347 | 2.10 | 1.03e-05 | 1.74Γ— | 7.73Γ— | +| **8** | **516** | **3.67** | **1.33e-06** | **baseline** | **1.00Γ—** | +| 10 | 1004 | 10.02 | 1.32e-06 | 2.73×↓ | 0.99Γ— | + +**Key insight:** The default `stencil_size=8` is well-optimized. Smaller sizes (e.g., 6) offer significant speedups but with larger errors. Larger sizes provide diminishing returns on accuracy while increasing cost. + +For detailed analysis of stencil size trade-offs, run: + +```bash +julia --project=scripts scripts/phs/stencil_size_test.jl +``` + +This script systematically explores stencil sizes from 3 to 10, measuring performance and accuracy on synthetic data. + +### Tuning Recommendations + +1. **Default settings** (`stencil_size=8`, `blend_factor=1.0`) are recommended for most applications and provide excellent accuracy-performance balance. + +2. **High-accuracy applications** (e.g., quantum chemistry): Keep defaults. Consider `blend_factor=2.0` only if accuracy dominates and 3Γ— longer runtimes are acceptable. + +3. **Performance-critical applications** (e.g., real-time approximation): Try `blend_factor=0.5` for ~10-100Γ— speedup with ~50Γ— error increase. Visual accuracy may still be acceptable depending on the application. + +4. **Do not reduce `stencil_size` below 8** unless extreme performance is needed. Smaller stencils show significant accuracy degradation and may exhibit convergence issues (stencil_size=3). + +5. **Interactive tuning**: Both test scripts generate synthetic 40Β³ grids for rapid prototyping. For production use, benchmark on realistic grid sizes and data distributions. + +### Profiling and Optimization + +To identify bottlenecks in your specific use case: + +```bash +julia --project=scripts scripts/phs/phs_density_comparison_simplified.jl +``` + +This script includes profiling infrastructure (50 million sample buffer, thread/task grouping) to visualize which operations consume the most time. Results guide parameter selection: + +- **High coefficient evaluation cost** β†’ reduce `blend_factor` or `stencil_size` +- **High Hessian cost** β†’ intrinsic to the algorithm; optimize at the application level +- **Acceptable polynomial overhead** β†’ typically not a tuning target + +## References + +- **Paper**: [Otero-de-la-Roza, A. *Finding critical points and reconstruction of electron densities on grids*. J. Chem. Phys. 156, 224116 (2022).](https://doi.org/10.1063/5.0090232) +- **critic2**: [Database of PBE all-electron atomic densities](https://github.com/aoterodelaroza/critic2/tree/master/dat/wfc) diff --git a/scripts/Project.toml b/scripts/Project.toml new file mode 100644 index 000000000..f3cc661cb --- /dev/null +++ b/scripts/Project.toml @@ -0,0 +1,5 @@ +[deps] +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +FastInterpolations = "9ea80cae-fc13-4c00-8066-6eaedb12f34b" +Pickle = "fbb45041-c46e-462f-888f-7c521cafbc2c" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" diff --git a/scripts/phs/blend_factor_test_simple.jl b/scripts/phs/blend_factor_test_simple.jl new file mode 100644 index 000000000..911306ed4 --- /dev/null +++ b/scripts/phs/blend_factor_test_simple.jl @@ -0,0 +1,131 @@ +#= Minimal Blend Factor Optimization Test =# + +using FastInterpolations +using Printf +using Statistics + +# Create synthetic 3D test data +x = range(0, 10, 40) +y = range(0, 10, 40) +z = range(0, 10, 40) + +# Smooth synthetic function: exp(-rΒ²/20) +data = [exp(-((xi - 5)^2 + (yi - 5)^2 + (zi - 5)^2) / 20.0) for xi in x, yi in y, zi in z] + +# Generate test queries - line cut +test_queries = [ + (Float64(xi), 5.0, 5.0) for xi in range(2, 8, 20) +] + +# Compute reference values +function reference(x, y, z) + return exp(-((x - 5)^2 + (y - 5)^2 + (z - 5)^2) / 20.0) +end + +ref_values = [reference(q...) for q in test_queries] + +println("="^70) +println("BLEND FACTOR OPTIMIZATION - SYNTHETIC TEST") +println("="^70) +println("Grid size: $(length(x)) Γ— $(length(y)) Γ— $(length(z))") +println("Test points: $(length(test_queries))") +println() + +# Test different blend factors (using stencil_size=8 for consistency with production) +blend_factors = [0.5, 1.0, 1.5, 2.0] +results = Dict{Float64, Any}() + +# Warm up +itp = phs_interp((x, y, z), data; stencil_size = 8, degree = 3, blend_factor = 1.0) +out = Vector{Float64}(undef, length(test_queries)) +itp(out, test_queries) + +for bf in blend_factors + @printf "Testing blend_factor = %.1f ... " bf + flush(stdout) + + # Build interpolant + time_build = @elapsed itp = phs_interp((x, y, z), data; stencil_size = 8, degree = 3, blend_factor = bf) + + # Evaluate on test points + out = Vector{Float64}(undef, length(test_queries)) + time_eval = @elapsed itp(out, test_queries) + + # Compute errors + errors = abs.(out .- ref_values) + rel_errors = errors ./ (abs.(ref_values) .+ 1.0e-16) + + blend_nodes = prod(2 .* itp.blend_r_idx .+ 1) + + results[bf] = ( + time_build = time_build, + time_eval = time_eval, + max_error = maximum(errors), + mean_error = mean(errors), + max_rel_error = maximum(rel_errors), + mean_rel_error = mean(rel_errors), + blend_nodes = blend_nodes, + ) + + @printf "%.3fms eval, %d nodes, max_rel_err=%.2e\n" time_eval * 1000 blend_nodes results[bf].max_rel_error +end + +# Print summary +println("\n" * "="^80) +println("SUMMARY TABLE") +println("="^80) + +# ASCII table (for terminal viewing) +println("\nFactor | Nodes | Build(ms) | Eval(ms) | Max Rel Err | Speedup | Rel.Err") +println("-"^80) + +baseline_time = results[2.0].time_eval +baseline_err = results[2.0].max_rel_error + +for bf in blend_factors + r = results[bf] + speedup = baseline_time / r.time_eval + if speedup >= 1.1 + speedup_str = @sprintf("%.2fΓ—", speedup) + elseif speedup < 1.0 + speedup_str = @sprintf("%.2f×↓", 1 / speedup) + else + speedup_str = "baseline" + end + err_ratio = r.max_rel_error / baseline_err + @printf "%6.1f | %5d | %9.2f | %8.3f | %11.2e | %7s | %8.2fΓ—\n" bf r.blend_nodes r.time_build * 1000 r.time_eval * 1000 r.max_rel_error speedup_str err_ratio +end + +# Markdown table +println("\n" * "="^80) +println("MARKDOWN TABLE") +println("="^80) +println() +println("| blend_factor | Blend Nodes | Build (ms) | Eval (ms) | Max Rel Err | Speedup | Rel.Err |") +println("|---|---|---|---|---|---|---|") +for bf in blend_factors + r = results[bf] + speedup = baseline_time / r.time_eval + if speedup >= 1.1 + speedup_str = @sprintf("%.2fΓ—", speedup) + elseif speedup < 1.0 + speedup_str = @sprintf("%.2f×↓", 1 / speedup) + else + speedup_str = "baseline" + end + err_ratio = r.max_rel_error / baseline_err + @printf "| %.1f | %d | %.2f | %.3f | %.2e | %s | %.2fΓ— |\n" bf r.blend_nodes r.time_build * 1000 r.time_eval * 1000 r.max_rel_error speedup_str err_ratio +end +println() +println("\nRecommendation:") +if haskey(results, 1.0) + err_ratio = results[1.0].max_rel_error / results[2.0].max_rel_error + time_ratio = results[1.0].time_eval / results[2.0].time_eval + speedup = (1 - time_ratio) * 100 + @printf "β€’ blend_factor=1.0: %.1f%% faster than 2.0, error increases by %.1fΓ—\n" speedup err_ratio + if err_ratio < 2.0 + println(" βœ“ Excellent trade-off: recommended for most applications") + else + println(" ⚠ Noticeable error increase; consider 1.5 for balance") + end +end diff --git a/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl new file mode 100644 index 000000000..d65fe4bba Binary files /dev/null and b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl differ diff --git a/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_atoms.xyz b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_atoms.xyz new file mode 100644 index 000000000..93ae653da --- /dev/null +++ b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_atoms.xyz @@ -0,0 +1,28 @@ +26 +/Users/haiiro/scratch/phenol-dimer_B3LYP_TZ2P_GO.results/adf.rkf +C 1.72521901 0.41919789 0.14404561 +C 3.51743339 -1.70074421 0.00817297 +C 1.30783343 -0.85967846 0.49598460 +C 3.03085119 0.65079942 -0.27508700 +C 3.91991728 -0.41463070 -0.34168698 +C 2.20914881 -1.91738310 0.42677341 +O 0.86892397 1.50138342 0.19296713 +H 0.01895914 1.23341252 0.56042542 +H 0.28741403 -1.03037820 0.82149553 +H 1.88248422 -2.91181756 0.70229021 +H 4.21631885 -2.52476170 -0.04386467 +H 4.93666410 -0.23461442 -0.66667289 +H 3.33659937 1.65451243 -0.53930054 +C 0.94404600 4.82024687 -1.42913669 +C -0.17773620 6.02058464 -3.69056913 +C 0.10879808 4.08494790 -2.27165678 +C 1.21831458 6.15699707 -1.72162762 +C 0.65914032 6.74726270 -2.84659935 +C -0.44674432 4.68858264 -3.39519035 +O 1.51759037 4.28696483 -0.31305277 +H 1.24384528 3.36394622 -0.19133449 +H -0.09764444 3.04498510 -2.05026751 +H -1.09305132 4.10911522 -4.04273121 +H -0.61189888 6.48618487 -4.56529284 +H 0.87833944 7.78494956 -3.06513522 +H 1.86803332 6.71355796 -1.05898712 diff --git a/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv new file mode 100644 index 000000000..262d14165 --- /dev/null +++ b/scripts/phs/dat/phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv @@ -0,0 +1,1001 @@ +point,x_bohr,y_bohr,z_bohr,arclength_bohr,density_scf,density_frag,dengrad_mag,laplacian_scf +0,1.64202832,2.83720347,0.36465502,0.00000000,3.1000267924e+02,3.1182743447e+02,4.9768938259e+03,-9.5601002807e+07 +1,1.64273753,2.84072673,0.36392807,0.00366671,2.9275808416e+02,2.9452613117e+02,4.7009254204e+03,-2.4822811309e+06 +2,1.64344673,2.84424998,0.36320112,0.00733342,2.7602569374e+02,2.7773582071e+02,4.4284908796e+03,-1.1331659649e+06 +3,1.64415594,2.84777324,0.36247416,0.01100013,2.6026256020e+02,2.6191581428e+02,4.1720494714e+03,-6.8909393017e+05 +4,1.64486515,2.85129650,0.36174721,0.01466685,2.4541187512e+02,2.4700934797e+02,3.9306488963e+03,-4.7096976303e+05 +5,1.64557436,2.85481975,0.36102026,0.01833356,2.3142017460e+02,2.3296299410e+02,3.7033938891e+03,-3.4299121988e+05 +6,1.64628356,2.85834301,0.36029331,0.02200027,2.1823708532e+02,2.1972640775e+02,3.4894418722e+03,-2.5990453013e+05 +7,1.64699277,2.86186627,0.35956636,0.02566698,2.0581531561e+02,2.0725231941e+02,3.2880026082e+03,-2.0232704450e+05 +8,1.64770198,2.86538952,0.35883941,0.02933369,1.9411039736e+02,1.9549627727e+02,3.0983338178e+03,-1.6057656603e+05 +9,1.64841119,2.86891278,0.35811245,0.03300040,1.8308039940e+02,1.8441636070e+02,2.9197363557e+03,-1.2928133981e+05 +10,1.64912040,2.87243604,0.35738550,0.03666712,1.7268598271e+02,1.7397323704e+02,2.7515549391e+03,-1.0522490962e+05 +11,1.64982960,2.87595929,0.35665855,0.04033383,1.6289012073e+02,1.6412988180e+02,2.5931734609e+03,-8.6365691764e+04 +12,1.65053881,2.87948255,0.35593160,0.04400054,1.5365789332e+02,1.5485137283e+02,2.4440115132e+03,-7.1347017677e+04 +13,1.65124802,2.88300581,0.35520465,0.04766725,1.4495653369e+02,1.4610493851e+02,2.3035250117e+03,-5.9233677457e+04 +14,1.65195723,2.88652906,0.35447770,0.05133396,1.3675520054e+02,1.3785972976e+02,2.1712023830e+03,-4.9360979785e+04 +15,1.65266644,2.89005232,0.35375074,0.05500067,1.2902480483e+02,1.3008664696e+02,2.0465616543e+03,-4.1244304825e+04 +16,1.65337564,2.89357558,0.35302379,0.05866738,1.2173804984e+02,1.2275838097e+02,1.9291509869e+03,-3.4522784971e+04 +17,1.65408485,2.89709883,0.35229684,0.06233410,1.1486921874e+02,1.1584920052e+02,1.8185451484e+03,-2.8922630281e+04 +18,1.65479406,2.90062209,0.35156989,0.06600081,1.0839406964e+02,1.0933484746e+02,1.7143437272e+03,-2.4232791611e+04 +19,1.65550327,2.90414535,0.35084294,0.06966752,1.0228983326e+02,1.0319253509e+02,1.6161710048e+03,-2.0288378627e+04 +20,1.65621247,2.90766861,0.35011598,0.07333423,9.6535058902e+01,9.7400794028e+01,1.5236733929e+03,-1.6958956948e+04 +21,1.65692168,2.91119186,0.34938903,0.07700094,9.1109567373e+01,9.1939425478e+01,1.4365185992e+03,-1.4140260657e+04 +22,1.65763089,2.91471512,0.34866208,0.08066765,8.5994319616e+01,8.6789369695e+01,1.3543934440e+03,-1.1748126854e+04 +23,1.65834010,2.91823838,0.34793513,0.08433436,8.1171431904e+01,8.1932721879e+01,1.2770040369e+03,-9.7140839894e+03 +24,1.65904931,2.92176163,0.34720818,0.08800108,7.6624063547e+01,7.7352619729e+01,1.2040739105e+03,-7.9819890712e+03 +25,1.65975851,2.92528489,0.34648123,0.09166779,7.2336324680e+01,7.3033151127e+01,1.1353424827e+03,-6.5054883844e+03 +26,1.66046772,2.92880815,0.34575427,0.09533450,6.8293278299e+01,6.8959356723e+01,1.0705650356e+03,-5.2460987090e+03 +27,1.66117693,2.93233140,0.34502732,0.09900121,6.4480868960e+01,6.5117158614e+01,1.0095115213e+03,-4.1716917164e+03 +28,1.66188614,2.93585466,0.34430037,0.10266792,6.0885821857e+01,6.1493259144e+01,9.5196489844e+02,-3.2553005574e+03 +29,1.66259534,2.93937792,0.34357342,0.10633463,5.7495667215e+01,5.8075165926e+01,8.9772147934e+02,-2.4742078069e+03 +30,1.66330455,2.94290117,0.34284647,0.11000135,5.4298653081e+01,5.4851104380e+01,8.4658949300e+02,-1.8091896452e+03 +31,1.66401376,2.94642443,0.34211952,0.11366806,5.1283690522e+01,5.1809962886e+01,7.9838817135e+02,-1.2439176966e+03 +32,1.66472297,2.94994769,0.34139256,0.11733477,4.8440354880e+01,4.8941294444e+01,7.5294773424e+02,-7.6448472610e+02 +33,1.66543218,2.95347094,0.34066561,0.12100148,4.5758828699e+01,4.6235259482e+01,7.1010844416e+02,-3.5900693261e+02 +34,1.66614138,2.95699420,0.33993866,0.12466819,4.3229839176e+01,4.3682563113e+01,6.6971957731e+02,-1.7298702195e+01 +35,1.66685059,2.96051746,0.33921171,0.12833490,4.0844667862e+01,4.1274465245e+01,6.3163955037e+02,2.6938551839e+02 +36,1.66755980,2.96404071,0.33848476,0.13200161,3.8595102846e+01,3.9002732668e+01,5.9573513137e+02,5.0857345946e+02 +37,1.66826901,2.96756397,0.33775780,0.13566833,3.6473386697e+01,3.6859586786e+01,5.6188058473e+02,7.0675936576e+02 +38,1.66897821,2.97108723,0.33703085,0.13933504,3.4472230931e+01,3.4837718521e+01,5.2995788004e+02,8.6955182115e+02 +39,1.66968742,2.97461049,0.33630390,0.14300175,3.2584754918e+01,3.2930226896e+01,4.9985569690e+02,1.0018050163e+03 +40,1.67039663,2.97813374,0.33557695,0.14666846,3.0804482878e+01,3.1130616270e+01,4.7146935833e+02,1.1077243771e+03 +41,1.67110584,2.98165700,0.33485000,0.15033517,2.9125296921e+01,2.9432749125e+01,4.4470006445e+02,1.1909589404e+03 +42,1.67181505,2.98518026,0.33412305,0.15400188,2.7541444350e+01,2.7830853697e+01,4.1945499291e+02,1.2546766518e+03 +43,1.67252425,2.98870351,0.33339609,0.15766859,2.6047502056e+01,2.6319488222e+01,3.9564671650e+02,1.3016309638e+03 +44,1.67323346,2.99222677,0.33266914,0.16133531,2.4638346741e+01,2.4893511018e+01,3.7319271486e+02,1.3342168763e+03 +45,1.67394267,2.99575003,0.33194219,0.16500202,2.3309160139e+01,2.3548086006e+01,3.5201544548e+02,1.3545180439e+03 +46,1.67465188,2.99927328,0.33121524,0.16866873,2.2055399380e+01,2.2278652919e+01,3.3204185976e+02,1.3643483078e+03 +47,1.67536108,3.00279654,0.33048829,0.17233544,2.0872772628e+01,2.1080902833e+01,3.1320300439e+02,1.3652868992e+03 +48,1.67607029,3.00631980,0.32976134,0.17600215,1.9757241239e+01,1.9950780530e+01,2.9543404585e+02,1.3587086794e+03 +49,1.67677950,3.00984305,0.32903438,0.17966886,1.8704998085e+01,1.8884462750e+01,2.7867391662e+02,1.3458105423e+03 +50,1.67748871,3.01336631,0.32830743,0.18333558,1.7712446440e+01,1.7878336948e+01,2.6286497043e+02,1.3276339661e+03 +51,1.67819792,3.01688957,0.32758048,0.18700229,1.6776201153e+01,1.6929002658e+01,2.4795299363e+02,1.3050848597e+03 +52,1.67890712,3.02041282,0.32685353,0.19066900,1.5893070638e+01,1.6033253397e+01,2.3388691157e+02,1.2789506434e+03 +53,1.67961633,3.02393608,0.32612658,0.19433571,1.5060037035e+01,1.5188056683e+01,2.2061846628e+02,1.2499147927e+03 +54,1.68032554,3.02745934,0.32539962,0.19800242,1.4274261964e+01,1.4390560034e+01,2.0810230269e+02,1.2185702653e+03 +55,1.68103475,3.03098259,0.32467267,0.20166913,1.3533068945e+01,1.3638073255e+01,1.9629568331e+02,1.1854306289e+03 +56,1.68174396,3.03450585,0.32394572,0.20533584,1.2833926752e+01,1.2928051685e+01,1.8515821853e+02,1.1509395461e+03 +57,1.68245316,3.03802911,0.32321877,0.20900256,1.2174454367e+01,1.2258101349e+01,1.7465194134e+02,1.1154800041e+03 +58,1.68316237,3.04155237,0.32249182,0.21266927,1.1552403059e+01,1.1625960872e+01,1.6474101776e+02,1.0793814240e+03 +59,1.68387158,3.04507562,0.32176487,0.21633598,1.0965654184e+01,1.1029499373e+01,1.5539170871e+02,1.0429265675e+03 +60,1.68458079,3.04859888,0.32103791,0.22000269,1.0412204124e+01,1.0466701264e+01,1.4657212669e+02,1.0063568556e+03 +61,1.68528999,3.05212214,0.32031096,0.22366940,9.8901684230e+00,9.9356705580e+00,1.3825229895e+02,9.6987814805e+02 +62,1.68599920,3.05564539,0.31958401,0.22733611,9.3977674425e+00,9.4346163838e+00,1.3040393631e+02,9.3366472856e+02 +63,1.68670841,3.05916865,0.31885706,0.23100283,8.9333196072e+00,8.9618462208e+00,1.2300032340e+02,8.9786314413e+02 +64,1.68741762,3.06269191,0.31813011,0.23466954,8.4952419701e+00,8.5157665579e+00,1.1601632555e+02,8.6259613027e+02 +65,1.68812683,3.06621516,0.31740316,0.23833625,8.0820408899e+00,8.0948734984e+00,1.0942823841e+02,8.2796537766e+02 +66,1.68883603,3.06973842,0.31667620,0.24200296,7.6923039788e+00,7.6977446472e+00,1.0321365805e+02,7.9405393962e+02 +67,1.68954524,3.07326168,0.31594925,0.24566967,7.3247018144e+00,7.3230409231e+00,9.7351506642e+01,7.6092913084e+02 +68,1.69025445,3.07678493,0.31522230,0.24933638,6.9779798479e+00,6.9694984129e+00,9.1821902449e+01,7.2864431777e+02 +69,1.69096366,3.08030819,0.31449535,0.25300309,6.6509516994e+00,6.6359215981e+00,8.6606051677e+01,6.9724049954e+02 +70,1.69167286,3.08383145,0.31376840,0.25666981,6.3425007279e+00,6.3211850311e+00,8.1686272584e+01,6.6674843892e+02 +71,1.69238207,3.08735470,0.31304144,0.26033652,6.0515725050e+00,6.0242257227e+00,7.7045874505e+01,6.3718973282e+02 +72,1.69309128,3.09087796,0.31231449,0.26400323,5.7771707571e+00,5.7440390742e+00,7.2669092596e+01,6.0857799806e+02 +73,1.69380049,3.09440122,0.31158754,0.26766994,5.5183569673e+00,5.4796785296e+00,6.8541080736e+01,5.8092027190e+02 +74,1.69450970,3.09792447,0.31086059,0.27133665,5.2742455338e+00,5.2302506926e+00,6.4647833781e+01,5.5421781464e+02 +75,1.69521890,3.10144773,0.31013364,0.27500336,5.0439989926e+00,4.9949105064e+00,6.0976110979e+01,5.2846675752e+02 +76,1.69592811,3.10497099,0.30940669,0.27867007,4.8268285602e+00,4.7728618537e+00,5.7513444072e+01,5.0365921427e+02 +77,1.69663732,3.10849424,0.30867973,0.28233679,4.6219900073e+00,4.5633534051e+00,5.4248071373e+01,4.7978378583e+02 +78,1.69734653,3.11201750,0.30795278,0.28600350,4.4287796458e+00,4.3656745565e+00,5.1168873390e+01,4.5682595470e+02 +79,1.69805573,3.11554076,0.30722583,0.28967021,4.2465347306e+00,4.1791558921e+00,4.8265379058e+01,4.3476891309e+02 +80,1.69876494,3.11906402,0.30649888,0.29333692,4.0746291301e+00,4.0031647936e+00,4.5527696434e+01,4.1359375741e+02 +81,1.69947415,3.12258727,0.30577193,0.29700363,3.9124726401e+00,3.8371047909e+00,4.2946501773e+01,3.9328004822e+02 +82,1.70018336,3.12611053,0.30504498,0.30067034,3.7595069418e+00,3.6804114597e+00,4.0512974957e+01,3.7380588936e+02 +83,1.70089257,3.12963379,0.30431802,0.30433706,3.6152063013e+00,3.5325531773e+00,3.8218810524e+01,3.5514854330e+02 +84,1.70160177,3.13315704,0.30359107,0.30800377,3.4790748047e+00,3.3930283295e+00,3.6056173587e+01,3.3728456671e+02 +85,1.70231098,3.13668030,0.30286412,0.31167048,3.3506429789e+00,3.2613618790e+00,3.4017645996e+01,3.2018980336e+02 +86,1.70302019,3.14020356,0.30213717,0.31533719,3.2294690998e+00,3.1371067482e+00,3.2096247195e+01,3.0383995469e+02 +87,1.70372940,3.14372681,0.30141022,0.31900390,3.1151361909e+00,3.0198407680e+00,3.0285386367e+01,2.8821054709e+02 +88,1.70443861,3.14725007,0.30068327,0.32267061,3.0072498840e+00,2.9091645177e+00,2.8578828551e+01,2.7327696974e+02 +89,1.70514781,3.15077333,0.29995631,0.32633732,2.9054388963e+00,2.8047018430e+00,2.6970702137e+01,2.5901484160e+02 +90,1.70585702,3.15429658,0.29922936,0.33000404,2.8093528287e+00,2.7060976186e+00,2.5455463830e+01,2.4539998415e+02 +91,1.70656623,3.15781984,0.29850241,0.33367075,2.7186602891e+00,2.6130158604e+00,2.4027869168e+01,2.3240840942e+02 +92,1.70727544,3.16134310,0.29777546,0.33733746,2.6330493092e+00,2.5251401689e+00,2.2682978886e+01,2.2001660731e+02 +93,1.70798464,3.16486635,0.29704851,0.34100417,2.5522255215e+00,2.4421718853e+00,2.1416130123e+01,2.0820149853e+02 +94,1.70869385,3.16838961,0.29632155,0.34467088,2.4759103946e+00,2.3638282974e+00,2.0222908601e+01,1.9694036789e+02 +95,1.70940306,3.17191287,0.29559460,0.34833759,2.4038419851e+00,2.2898434380e+00,1.9099160385e+01,1.8621115238e+02 +96,1.71011227,3.17543612,0.29486765,0.35200431,2.3357729578e+00,2.2199660683e+00,1.8040960659e+01,1.7599231217e+02 +97,1.71082148,3.17895938,0.29414070,0.35567102,2.2714694818e+00,2.1539585650e+00,1.7044596481e+01,1.6626281252e+02 +98,1.71153068,3.18248264,0.29341375,0.35933773,2.2107115076e+00,2.0915972204e+00,1.6106570890e+01,1.5700230145e+02 +99,1.71223989,3.18600590,0.29268680,0.36300444,2.1532911834e+00,2.0326706290e+00,1.5223578095e+01,1.4819099474e+02 +100,1.71294910,3.18952915,0.29195984,0.36667115,2.0990126361e+00,1.9769794859e+00,1.4392500010e+01,1.3980975845e+02 +101,1.71365831,3.19305241,0.29123289,0.37033786,2.0476907445e+00,1.9243353354e+00,1.3610387110e+01,1.3184002402e+02 +102,1.71436751,3.19657567,0.29050594,0.37400457,1.9991512504e+00,1.8745607014e+00,1.2874459916e+01,1.2426390241e+02 +103,1.71507672,3.20009892,0.28977899,0.37767129,1.9532297370e+00,1.8274880449e+00,1.2182093028e+01,1.1706411328e+02 +104,1.71578593,3.20362218,0.28905204,0.38133800,1.9097708614e+00,1.7829589978e+00,1.1530803403e+01,1.1022394776e+02 +105,1.71649514,3.20714544,0.28832509,0.38500471,1.8686284571e+00,1.7408244707e+00,1.0918251472e+01,1.0372735770e+02 +106,1.71720435,3.21066869,0.28759813,0.38867142,1.8296646561e+00,1.7009437651e+00,1.0342227611e+01,9.7558886760e+01 +107,1.71791355,3.21419195,0.28687118,0.39233813,1.7927492449e+00,1.6631839231e+00,9.8006422927e+00,9.1703631541e+01 +108,1.71862276,3.21771521,0.28614423,0.39600484,1.7577596395e+00,1.6274197048e+00,9.2915252557e+00,8.6147293985e+01 +109,1.71933197,3.22123846,0.28541728,0.39967155,1.7245803356e+00,1.5935330394e+00,8.8130170832e+00,8.0876148798e+01 +110,1.72004118,3.22476172,0.28469033,0.40333827,1.6931021822e+00,1.5614122854e+00,8.3633582206e+00,7.5876975489e+01 +111,1.72075038,3.22828498,0.28396337,0.40700498,1.6632225508e+00,1.5309524100e+00,7.9408909966e+00,7.1137129882e+01 +112,1.72145959,3.23180823,0.28323642,0.41067169,1.6348447130e+00,1.5020543529e+00,7.5440501171e+00,6.6644484615e+01 +113,1.72216880,3.23533149,0.28250947,0.41433840,1.6078772941e+00,1.4746244834e+00,7.1713546696e+00,6.2387381655e+01 +114,1.72287801,3.23885475,0.28178252,0.41800511,1.5822344161e+00,1.4485747425e+00,6.8214095275e+00,5.8354687227e+01 +115,1.72358722,3.24237800,0.28105557,0.42167182,1.5578351617e+00,1.4238221017e+00,6.4928973667e+00,5.4535738223e+01 +116,1.72429642,3.24590126,0.28032862,0.42533854,1.5346031304e+00,1.4002881171e+00,6.1845721759e+00,5.0920301068e+01 +117,1.72500563,3.24942452,0.27960166,0.42900525,1.5124665045e+00,1.3778989920e+00,5.8952595402e+00,4.7498606491e+01 +118,1.72571484,3.25294778,0.27887471,0.43267196,1.4913576560e+00,1.3565851867e+00,5.6238508735e+00,4.4261312510e+01 +119,1.72642405,3.25647103,0.27814776,0.43633867,1.4712129120e+00,1.3362811779e+00,5.3692997072e+00,4.1199488145e+01 +120,1.72713325,3.25999429,0.27742081,0.44000538,1.4519721928e+00,1.3169250967e+00,5.1306165528e+00,3.8304577708e+01 +121,1.72784246,3.26351755,0.27669386,0.44367209,1.4335790733e+00,1.2984587840e+00,4.9068689976e+00,3.5568425879e+01 +122,1.72855167,3.26704080,0.27596691,0.44733880,1.4159804694e+00,1.2808274819e+00,4.6971772071e+00,3.2983246668e+01 +123,1.72926088,3.27056406,0.27523995,0.45100552,1.3991262780e+00,1.2639794676e+00,4.5007089447e+00,3.0541583066e+01 +124,1.72997009,3.27408732,0.27451300,0.45467223,1.3829695234e+00,1.2478661986e+00,4.3166806800e+00,2.8236340505e+01 +125,1.73067929,3.27761057,0.27378605,0.45833894,1.3674659955e+00,1.2324419503e+00,4.1443526533e+00,2.6060744679e+01 +126,1.73138850,3.28113383,0.27305910,0.46200565,1.3525739875e+00,1.2176635499e+00,3.9830253101e+00,2.4008313531e+01 +127,1.73209771,3.28465709,0.27233215,0.46567236,1.3382544249e+00,1.2034905073e+00,3.8320401302e+00,2.2072884383e+01 +128,1.73280692,3.28818034,0.27160519,0.46933907,1.3244705603e+00,1.1898847050e+00,3.6907755341e+00,2.0248577290e+01 +129,1.73351613,3.29170360,0.27087824,0.47300578,1.3111877752e+00,1.1768102020e+00,3.5586442344e+00,1.8529775177e+01 +130,1.73422533,3.29522686,0.27015129,0.47667250,1.2983736332e+00,1.1642332825e+00,3.4350931108e+00,1.6911135816e+01 +131,1.73493454,3.29875011,0.26942434,0.48033921,1.2859976545e+00,1.1521222274e+00,3.3196002254e+00,1.5387565578e+01 +132,1.73564375,3.30227337,0.26869739,0.48400592,1.2740311390e+00,1.1404471438e+00,3.2116725858e+00,1.3954202142e+01 +133,1.73635296,3.30579663,0.26797044,0.48767263,1.2624472327e+00,1.1291800219e+00,3.1108460608e+00,1.2606424333e+01 +134,1.73706216,3.30931988,0.26724348,0.49133934,1.2512207301e+00,1.1182945400e+00,3.0166829038e+00,1.1339830018e+01 +135,1.73777137,3.31284314,0.26651653,0.49500605,1.2403279008e+00,1.1077658892e+00,2.9287696463e+00,1.0150217408e+01 +136,1.73848058,3.31636640,0.26578958,0.49867277,1.2297465705e+00,1.0975708535e+00,2.8467171755e+00,9.0335957053e+00 +137,1.73918979,3.31988966,0.26506263,0.50233948,1.2194559224e+00,1.0876876096e+00,2.7701584384e+00,7.9861626100e+00 +138,1.73989900,3.32341291,0.26433568,0.50600619,1.2094364726e+00,1.0780957003e+00,2.6987476393e+00,7.0043016226e+00 +139,1.74060820,3.32693617,0.26360873,0.50967290,1.1996698947e+00,1.0687758635e+00,2.6321583717e+00,6.0845641699e+00 +140,1.74131741,3.33045943,0.26288177,0.51333961,1.1901390846e+00,1.0597100861e+00,2.5700834703e+00,5.2236745926e+00 +141,1.74202662,3.33398268,0.26215482,0.51700632,1.1808280581e+00,1.0508815072e+00,2.5122336930e+00,4.4185190234e+00 +142,1.74273583,3.33750594,0.26142787,0.52067303,1.1717217683e+00,1.0422742377e+00,2.4583359899e+00,3.6661276060e+00 +143,1.74344503,3.34102920,0.26070092,0.52433975,1.1628062221e+00,1.0338734677e+00,2.4081336805e+00,2.9636829076e+00 +144,1.74415424,3.34455245,0.25997397,0.52800646,1.1540683274e+00,1.0256653149e+00,2.3613849400e+00,2.3085042779e+00 +145,1.74486345,3.34807571,0.25924701,0.53167317,1.1454957909e+00,1.0176367298e+00,2.3178617718e+00,1.6980389327e+00 +146,1.74557266,3.35159897,0.25852006,0.53533988,1.1370772099e+00,1.0097755739e+00,2.2773499476e+00,1.1298657544e+00 +147,1.74628187,3.35512222,0.25779311,0.53900659,1.1288019288e+00,1.0020704825e+00,2.2396477426e+00,6.0168222028e-01 +148,1.74699107,3.35864548,0.25706616,0.54267330,1.1206599533e+00,9.9451078210e-01,2.2045650862e+00,1.1129673798e-01 +149,1.74770028,3.36216874,0.25633921,0.54634002,1.1126420199e+00,9.8708654797e-01,2.1719233980e+00,-3.4336989816e-01 +150,1.74840949,3.36569199,0.25561226,0.55000673,1.1047395102e+00,9.7978852508e-01,2.1415547387e+00,-7.6429253359e-01 +151,1.74911870,3.36921525,0.25488530,0.55367344,1.0969443337e+00,9.7260801390e-01,2.1133009204e+00,-1.1533507836e+00 +152,1.74982790,3.37273851,0.25415835,0.55734015,1.0892490405e+00,9.6553697185e-01,2.0870135161e+00,-1.5123258326e+00 +153,1.75053711,3.37626176,0.25343140,0.56100686,1.0816466859e+00,9.5856788209e-01,2.0625529221e+00,-1.8429106371e+00 +154,1.75124632,3.37978502,0.25270445,0.56467357,1.0741307755e+00,9.5169370548e-01,2.0397878294e+00,-2.1467143459e+00 +155,1.75195553,3.38330828,0.25197750,0.56834028,1.0666953391e+00,9.4490794054e-01,2.0185950660e+00,-2.4252622821e+00 +156,1.75266474,3.38683154,0.25125055,0.57200700,1.0593348242e+00,9.3820452492e-01,1.9988589017e+00,-2.6800031523e+00 +157,1.75337394,3.39035479,0.25052359,0.57567371,1.0520441083e+00,9.3157784358e-01,1.9804707371e+00,-2.9123112794e+00 +158,1.75408315,3.39387805,0.24979664,0.57934042,1.0448184000e+00,9.2502263409e-01,1.9633285205e+00,-3.1234925812e+00 +159,1.75479236,3.39740131,0.24906969,0.58300713,1.0376533256e+00,9.1853406577e-01,1.9473366502e+00,-3.3147842425e+00 +160,1.75550157,3.40092456,0.24834274,0.58667384,1.0305448517e+00,9.1210766413e-01,1.9324054653e+00,-3.4873600613e+00 +161,1.75621077,3.40444782,0.24761579,0.59034055,1.0234892103e+00,9.0573924271e-01,1.9184508091e+00,-3.6423345419e+00 +162,1.75691998,3.40797108,0.24688883,0.59400726,1.0164829757e+00,8.9942496662e-01,1.9053939131e+00,-3.7807635873e+00 +163,1.75762919,3.41149433,0.24616188,0.59767398,1.0095230218e+00,8.9316131593e-01,1.8931610350e+00,-3.9036479868e+00 +164,1.75833840,3.41501759,0.24543493,0.60134069,1.0026064154e+00,8.8694498466e-01,1.8816830173e+00,-4.0119376866e+00 +165,1.75904761,3.41854085,0.24470798,0.60500740,9.9573052732e-01,8.8077297915e-01,1.8708952483e+00,-4.1065320216e+00 +166,1.75975681,3.42206410,0.24398103,0.60867411,9.8889296031e-01,8.7464255176e-01,1.8607372994e+00,-4.1882832893e+00 +167,1.76046602,3.42558736,0.24325408,0.61234082,9.8209146869e-01,8.6855112488e-01,1.8511525958e+00,-4.2579997540e+00 +168,1.76117523,3.42911062,0.24252712,0.61600753,9.7532406668e-01,8.6249638899e-01,1.8420883708e+00,-4.3164462775e+00 +169,1.76188444,3.43263387,0.24180017,0.61967425,9.6858896336e-01,8.5647624046e-01,1.8334953677e+00,-4.3643473014e+00 +170,1.76259365,3.43615713,0.24107322,0.62334096,9.6188447823e-01,8.5048870534e-01,1.8253275485e+00,-4.4023892089e+00 +171,1.76330285,3.43968039,0.24034627,0.62700767,9.5520915370e-01,8.4453203809e-01,1.8175420621e+00,-4.4312213414e+00 +172,1.76401206,3.44320364,0.23961932,0.63067438,9.4856167483e-01,8.3860464661e-01,1.8100989736e+00,-4.4514584242e+00 +173,1.76472127,3.44672690,0.23889237,0.63434109,9.4194082447e-01,8.3270505403e-01,1.8029610571e+00,-4.4636821848e+00 +174,1.76543048,3.45025016,0.23816541,0.63800780,9.3534556314e-01,8.2683196595e-01,1.7960937357e+00,-4.4684428023e+00 +175,1.76613968,3.45377341,0.23743846,0.64167451,9.2877498712e-01,8.2098423286e-01,1.7894648879e+00,-4.4662606780e+00 +176,1.76684889,3.45729667,0.23671151,0.64534123,9.2222823486e-01,8.1516076407e-01,1.7830446200e+00,-4.4576278558e+00 +177,1.76755810,3.46081993,0.23598456,0.64900794,9.1570461463e-01,8.0936064230e-01,1.7768052666e+00,-4.4430094288e+00 +178,1.76826731,3.46434319,0.23525761,0.65267465,9.0920351223e-01,8.0358303883e-01,1.7707211764e+00,-4.4228449182e+00 +179,1.76897652,3.46786644,0.23453066,0.65634136,9.0272442097e-01,7.9782723940e-01,1.7647686271e+00,-4.3975496222e+00 +180,1.76968572,3.47138970,0.23380370,0.66000807,8.9626686612e-01,7.9209257679e-01,1.7589256465e+00,-4.3675155463e+00 +181,1.77039493,3.47491296,0.23307675,0.66367478,8.8983050330e-01,7.8637851490e-01,1.7531720032e+00,-4.3331130144e+00 +182,1.77110414,3.47843621,0.23234980,0.66734150,8.8341505739e-01,7.8068459651e-01,1.7474890486e+00,-4.2946915682e+00 +183,1.77181335,3.48195947,0.23162285,0.67100821,8.7702026888e-01,7.7501039315e-01,1.7418595815e+00,-4.2525807028e+00 +184,1.77252255,3.48548273,0.23089590,0.67467492,8.7064599222e-01,7.6935559198e-01,1.7362678468e+00,-4.2070914918e+00 +185,1.77323176,3.48900598,0.23016894,0.67834163,8.6429212490e-01,7.6371993056e-01,1.7306993931e+00,-4.1585171794e+00 +186,1.77394097,3.49252924,0.22944199,0.68200834,8.5795858398e-01,7.5810317798e-01,1.7251409723e+00,-4.1071338724e+00 +187,1.77465018,3.49605250,0.22871504,0.68567505,8.5164536983e-01,7.5250518858e-01,1.7195805239e+00,-4.0532019502e+00 +188,1.77535939,3.49957575,0.22798809,0.68934176,8.4535252833e-01,7.4692586832e-01,1.7140070728e+00,-3.9969666526e+00 +189,1.77606859,3.50309901,0.22726114,0.69300848,8.3908009689e-01,7.4136512691e-01,1.7084106158e+00,-3.9386583841e+00 +190,1.77677780,3.50662227,0.22653419,0.69667519,8.3282818886e-01,7.3582295003e-01,1.7027821369e+00,-3.8784942515e+00 +191,1.77748701,3.51014552,0.22580723,0.70034190,8.2659695405e-01,7.3029936623e-01,1.6971135119e+00,-3.8166784575e+00 +192,1.77819622,3.51366878,0.22508028,0.70400861,8.2038652859e-01,7.2479440063e-01,1.6913974101e+00,-3.7534025031e+00 +193,1.77890542,3.51719204,0.22435333,0.70767532,8.1419711591e-01,7.1930814647e-01,1.6856273148e+00,-3.6888466537e+00 +194,1.77961463,3.52071529,0.22362638,0.71134203,8.0802893659e-01,7.1384071895e-01,1.6797974321e+00,-3.6231800937e+00 +195,1.78032384,3.52423855,0.22289943,0.71500874,8.0188219110e-01,7.0839222405e-01,1.6739026057e+00,-3.5565611060e+00 +196,1.78103305,3.52776181,0.22217248,0.71867546,7.9575714341e-01,7.0296283037e-01,1.6679383549e+00,-3.4891385477e+00 +197,1.78174226,3.53128507,0.22144552,0.72234217,7.8965405327e-01,6.9755270930e-01,1.6619007694e+00,-3.4210516724e+00 +198,1.78245146,3.53480832,0.22071857,0.72600888,7.8357322243e-01,6.9216207578e-01,1.6557865164e+00,-3.3524311605e+00 +199,1.78316067,3.53833158,0.21999162,0.72967559,7.7751489758e-01,6.8679110079e-01,1.6495927075e+00,-3.2833985163e+00 +200,1.78386988,3.54185484,0.21926467,0.73334230,7.7147939479e-01,6.8144002296e-01,1.6433169881e+00,-3.2140679383e+00 +201,1.78457909,3.54537809,0.21853772,0.73700901,7.6546703492e-01,6.7610908978e-01,1.6369574429e+00,-3.1445460436e+00 +202,1.78528829,3.54890135,0.21781076,0.74067573,7.5947809723e-01,6.7079851701e-01,1.6305125143e+00,-3.0749317455e+00 +203,1.78599750,3.55242461,0.21708381,0.74434244,7.5351291468e-01,6.6550857119e-01,1.6239810771e+00,-3.0053177596e+00 +204,1.78670671,3.55594786,0.21635686,0.74800915,7.4757182214e-01,6.6023952559e-01,1.6173623550e+00,-2.9357903849e+00 +205,1.78741592,3.55947112,0.21562991,0.75167586,7.4165511035e-01,6.5499161796e-01,1.6106558450e+00,-2.8664293250e+00 +206,1.78812513,3.56299438,0.21490296,0.75534257,7.3576311811e-01,6.4976513199e-01,1.6038613958e+00,-2.7973091102e+00 +207,1.78883433,3.56651763,0.21417601,0.75900928,7.2989618680e-01,6.4456035723e-01,1.5969791362e+00,-2.7284988909e+00 +208,1.78954354,3.57004089,0.21344905,0.76267599,7.2405459918e-01,6.3937753356e-01,1.5900093818e+00,-2.6600620275e+00 +209,1.79025275,3.57356415,0.21272210,0.76634271,7.1823870036e-01,6.3421695969e-01,1.5829527566e+00,-2.5920578035e+00 +210,1.79096196,3.57708740,0.21199515,0.77000942,7.1244882030e-01,6.2907892321e-01,1.5758100850e+00,-2.5245407906e+00 +211,1.79167117,3.58061066,0.21126820,0.77367613,7.0668524310e-01,6.2396367363e-01,1.5685823341e+00,-2.4575607585e+00 +212,1.79238037,3.58413392,0.21054125,0.77734284,7.0094829728e-01,6.1887150218e-01,1.5612707097e+00,-2.3911639755e+00 +213,1.79308958,3.58765717,0.20981430,0.78100955,6.9523829827e-01,6.1380268974e-01,1.5538765710e+00,-2.3253927725e+00 +214,1.79379879,3.59118043,0.20908734,0.78467626,6.8955551632e-01,6.0875747995e-01,1.5464013718e+00,-2.2602854028e+00 +215,1.79450800,3.59470369,0.20836039,0.78834297,6.8390026753e-01,6.0373615849e-01,1.5388467666e+00,-2.1958773018e+00 +216,1.79521720,3.59822695,0.20763344,0.79200969,6.7827284016e-01,5.9873898824e-01,1.5312145017e+00,-2.1322004382e+00 +217,1.79592641,3.60175020,0.20690649,0.79567640,6.7267352563e-01,5.9376623551e-01,1.5235064455e+00,-2.0692838594e+00 +218,1.79663562,3.60527346,0.20617954,0.79934311,6.6710257219e-01,5.8881813056e-01,1.5157245115e+00,-2.0071533553e+00 +219,1.79734483,3.60879672,0.20545258,0.80300982,6.6156025936e-01,5.8389493224e-01,1.5078707514e+00,-1.9458324628e+00 +220,1.79805404,3.61231997,0.20472563,0.80667653,6.5604686770e-01,5.7899690141e-01,1.4999473061e+00,-1.8853422832e+00 +221,1.79876324,3.61584323,0.20399868,0.81034324,6.5056262002e-01,5.7412424907e-01,1.4919563095e+00,-1.8257009926e+00 +222,1.79947245,3.61936649,0.20327173,0.81400996,6.4510776975e-01,5.6927721340e-01,1.4839000086e+00,-1.7669249592e+00 +223,1.80018166,3.62288974,0.20254478,0.81767667,6.3968256956e-01,5.6445603349e-01,1.4757807102e+00,-1.7090285297e+00 +224,1.80089087,3.62641300,0.20181783,0.82134338,6.3428721826e-01,5.5966090117e-01,1.4676006940e+00,-1.6520235965e+00 +225,1.80160007,3.62993626,0.20109087,0.82501009,6.2892194859e-01,5.5489203918e-01,1.4593623367e+00,-1.5959206577e+00 +226,1.80230928,3.63345951,0.20036392,0.82867680,6.2358698528e-01,5.5014966299e-01,1.4510680447e+00,-1.5407284789e+00 +227,1.80301849,3.63698277,0.19963697,0.83234351,6.1828250119e-01,5.4543394330e-01,1.4427201771e+00,-1.4864537576e+00 +228,1.80372770,3.64050603,0.19891002,0.83601022,6.1300870279e-01,5.4074508077e-01,1.4343211745e+00,-1.4331021108e+00 +229,1.80443691,3.64402928,0.19818307,0.83967694,6.0776578734e-01,5.3608326821e-01,1.4258734873e+00,-1.3806777234e+00 +230,1.80514611,3.64755254,0.19745612,0.84334365,6.0255390434e-01,5.3144865663e-01,1.4173795080e+00,-1.3291830635e+00 +231,1.80585532,3.65107580,0.19672916,0.84701036,5.9737322545e-01,5.2684141622e-01,1.4088416789e+00,-1.2786196702e+00 +232,1.80656453,3.65459905,0.19600221,0.85067707,5.9222392903e-01,5.2226172395e-01,1.4002624648e+00,-1.2289880720e+00 +233,1.80727374,3.65812231,0.19527526,0.85434378,5.8710613297e-01,5.1770970314e-01,1.3916442365e+00,-1.1802872198e+00 +234,1.80798294,3.66164557,0.19454831,0.85801049,5.8201999225e-01,5.1318551023e-01,1.3829894296e+00,-1.1325155029e+00 +235,1.80869215,3.66516883,0.19382136,0.86167721,5.7696563210e-01,5.0868927469e-01,1.3743004298e+00,-1.0856701748e+00 +236,1.80940136,3.66869208,0.19309440,0.86534392,5.7194318681e-01,5.0422113468e-01,1.3655796355e+00,-1.0397477860e+00 +237,1.81011057,3.67221534,0.19236745,0.86901063,5.6695274807e-01,4.9978119037e-01,1.3568293653e+00,-9.9474377284e-01 +238,1.81081978,3.67573860,0.19164050,0.87267734,5.6199443050e-01,4.9536956215e-01,1.3480519691e+00,-9.5065311441e-01 +239,1.81152898,3.67926185,0.19091355,0.87634405,5.5706834494e-01,4.9098636719e-01,1.3392497804e+00,-9.0747013765e-01 +240,1.81223819,3.68278511,0.19018660,0.88001076,5.5217454789e-01,4.8663167371e-01,1.3304250228e+00,-8.6518813166e-01 +241,1.81294740,3.68630837,0.18945965,0.88367747,5.4731313356e-01,4.8230558388e-01,1.3215799721e+00,-8.2380020541e-01 +242,1.81365661,3.68983162,0.18873269,0.88734419,5.4248418266e-01,4.7800818747e-01,1.3127168641e+00,-7.8329887754e-01 +243,1.81436582,3.69335488,0.18800574,0.89101090,5.3768773758e-01,4.7373954012e-01,1.3038378461e+00,-7.4367592151e-01 +244,1.81507502,3.69687814,0.18727879,0.89467761,5.3292386536e-01,4.6949971925e-01,1.2949450905e+00,-7.0492293286e-01 +245,1.81578423,3.70040139,0.18655184,0.89834432,5.2819262056e-01,4.6528879045e-01,1.2860407269e+00,-6.6703105273e-01 +246,1.81649344,3.70392465,0.18582489,0.90201103,5.2349401971e-01,4.6110678588e-01,1.2771267902e+00,-6.2999080862e-01 +247,1.81720265,3.70744791,0.18509794,0.90567774,5.1882810683e-01,4.5695376162e-01,1.2682053440e+00,-5.9379267078e-01 +248,1.81791185,3.71097116,0.18437098,0.90934445,5.1419491791e-01,4.5282976656e-01,1.2592784129e+00,-5.5842679431e-01 +249,1.81862106,3.71449442,0.18364403,0.91301117,5.0959444596e-01,4.4873481059e-01,1.2503479138e+00,-5.2388278935e-01 +250,1.81933027,3.71801768,0.18291708,0.91667788,5.0502671264e-01,4.4466892930e-01,1.2414157923e+00,-4.9015029670e-01 +251,1.82003948,3.72154093,0.18219013,0.92034459,5.0049173256e-01,4.4063215149e-01,1.2324839547e+00,-4.5721873494e-01 +252,1.82074869,3.72506419,0.18146318,0.92401130,4.9598947778e-01,4.3662446791e-01,1.2235541961e+00,-4.2507707475e-01 +253,1.82145789,3.72858745,0.18073622,0.92767801,4.9151994921e-01,4.3264589475e-01,1.2146283406e+00,-3.9371438090e-01 +254,1.82216710,3.73211071,0.18000927,0.93134472,4.8708313001e-01,4.2869643162e-01,1.2057081493e+00,-3.6311949389e-01 +255,1.82287631,3.73563396,0.17928232,0.93501144,4.8267900777e-01,4.2477608221e-01,1.1967953638e+00,-3.3328120649e-01 +256,1.82358552,3.73915722,0.17855537,0.93867815,4.7830752937e-01,4.2088481359e-01,1.1878916146e+00,-3.0418797926e-01 +257,1.82429472,3.74268048,0.17782842,0.94234486,4.7396867252e-01,4.1702261999e-01,1.1789985655e+00,-2.7582844199e-01 +258,1.82500393,3.74620373,0.17710147,0.94601157,4.6966240231e-01,4.1318948361e-01,1.1701178258e+00,-2.4819111243e-01 +259,1.82571314,3.74972699,0.17637451,0.94967828,4.6538864820e-01,4.0938535516e-01,1.1612509017e+00,-2.2126427277e-01 +260,1.82642235,3.75325025,0.17564756,0.95334499,4.6114737280e-01,4.0561021429e-01,1.1523993386e+00,-1.9503642313e-01 +261,1.82713156,3.75677350,0.17492061,0.95701170,4.5693852599e-01,4.0186402895e-01,1.1435646263e+00,-1.6949599633e-01 +262,1.82784076,3.76029676,0.17419366,0.96067842,4.5276202351e-01,3.9814673661e-01,1.1347481535e+00,-1.4463124935e-01 +263,1.82854997,3.76382002,0.17346671,0.96434513,4.4861780306e-01,3.9445829341e-01,1.1259513254e+00,-1.2043061550e-01 +264,1.82925918,3.76734327,0.17273976,0.96801184,4.4450580308e-01,3.9079865627e-01,1.1171755198e+00,-9.6882580092e-02 +265,1.82996839,3.77086653,0.17201280,0.97167855,4.4042592051e-01,3.8716774475e-01,1.1084219966e+00,-7.3975452377e-02 +266,1.83067759,3.77438979,0.17128585,0.97534526,4.3637808859e-01,3.8356551045e-01,1.0996920645e+00,-5.1697819543e-02 +267,1.83138680,3.77791304,0.17055890,0.97901197,4.3236222309e-01,3.7999188864e-01,1.0909869656e+00,-3.0038245146e-02 +268,1.83209601,3.78143630,0.16983195,0.98267869,4.2837821184e-01,3.7644678993e-01,1.0823078529e+00,-8.9852301355e-03 +269,1.83280522,3.78495956,0.16910500,0.98634540,4.2442596809e-01,3.7293014696e-01,1.0736559058e+00,1.1472494081e-02 +270,1.83351443,3.78848281,0.16837805,0.99001211,4.2050540058e-01,3.6944188805e-01,1.0650322659e+00,3.1346124846e-02 +271,1.83422363,3.79200607,0.16765109,0.99367882,4.1661638293e-01,3.6598191013e-01,1.0564379695e+00,5.0646932031e-02 +272,1.83493284,3.79552933,0.16692414,0.99734553,4.1275881642e-01,3.6255013398e-01,1.0478740860e+00,6.9385934079e-02 +273,1.83564205,3.79905258,0.16619719,1.00101224,4.0893259762e-01,3.5914647624e-01,1.0393416471e+00,8.7574061937e-02 +274,1.83635126,3.80257584,0.16547024,1.00467895,4.0513758954e-01,3.5577082330e-01,1.0308415825e+00,1.0522228612e-01 +275,1.83706046,3.80609910,0.16474329,1.00834567,4.0137368371e-01,3.5242308670e-01,1.0223748588e+00,1.2234131797e-01 +276,1.83776967,3.80962236,0.16401633,1.01201238,3.9764075066e-01,3.4910315860e-01,1.0139423685e+00,1.3894183981e-01 +277,1.83847888,3.81314561,0.16328938,1.01567909,3.9393867892e-01,3.4581094724e-01,1.0055450192e+00,1.5503432489e-01 +278,1.83918809,3.81666887,0.16256243,1.01934580,3.9026731682e-01,3.4254632475e-01,9.9718360106e-01,1.7062928937e-01 +279,1.83989730,3.82019213,0.16183548,1.02301251,3.8662654092e-01,3.3930918800e-01,9.8885894294e-01,1.8573699051e-01 +280,1.84060650,3.82371538,0.16110853,1.02667922,3.8301622634e-01,3.3609943238e-01,9.8057184543e-01,2.0036755853e-01 +281,1.84131571,3.82723864,0.16038158,1.03034593,3.7943621165e-01,3.3291692025e-01,9.7232300046e-01,2.1453113184e-01 +282,1.84202492,3.83076190,0.15965462,1.03401265,3.7588636470e-01,3.2976153996e-01,9.6411314250e-01,2.2823758877e-01 +283,1.84273413,3.83428515,0.15892767,1.03767936,3.7236655439e-01,3.2663318045e-01,9.5594298490e-01,2.4149666542e-01 +284,1.84344334,3.83780841,0.15820072,1.04134607,3.6887661374e-01,3.2353169850e-01,9.4781313389e-01,2.5431809065e-01 +285,1.84415254,3.84133167,0.15747377,1.04501278,3.6541640479e-01,3.2045697652e-01,9.3972423978e-01,2.6671133950e-01 +286,1.84486175,3.84485492,0.15674682,1.04867949,3.6198578194e-01,3.1740888950e-01,9.3167691234e-01,2.7868577300e-01 +287,1.84557096,3.84837818,0.15601987,1.05234620,3.5858457558e-01,3.1438729124e-01,9.2367168232e-01,2.9025069058e-01 +288,1.84628017,3.85190144,0.15529291,1.05601292,3.5521263759e-01,3.1139205421e-01,9.1570910869e-01,3.0141517057e-01 +289,1.84698937,3.85542469,0.15456596,1.05967963,3.5186982471e-01,3.0842305506e-01,9.0778974039e-01,3.1218813354e-01 +290,1.84769858,3.85894795,0.15383901,1.06334634,3.4855595513e-01,3.0548013570e-01,8.9991401352e-01,3.2257847911e-01 +291,1.84840779,3.86247121,0.15311206,1.06701305,3.4527087843e-01,3.0256316594e-01,8.9208241752e-01,3.3259486022e-01 +292,1.84911700,3.86599446,0.15238511,1.07067976,3.4201444059e-01,2.9967201204e-01,8.8429541278e-01,3.4224579952e-01 +293,1.84982621,3.86951772,0.15165815,1.07434647,3.3878645863e-01,2.9680651442e-01,8.7655337017e-01,3.5153976088e-01 +294,1.85053541,3.87304098,0.15093120,1.07801318,3.3558678063e-01,2.9396654085e-01,8.6885671485e-01,3.6048497317e-01 +295,1.85124462,3.87656424,0.15020425,1.08167990,3.3241523466e-01,2.9115194088e-01,8.6120580454e-01,3.6908958028e-01 +296,1.85195383,3.88008749,0.14947730,1.08534661,3.2927166222e-01,2.8836257602e-01,8.5360101016e-01,3.7736154838e-01 +297,1.85266304,3.88361075,0.14875035,1.08901332,3.2615587725e-01,2.8559828310e-01,8.4604261712e-01,3.8530877508e-01 +298,1.85337224,3.88713401,0.14802340,1.09268003,3.2306771885e-01,2.8285892110e-01,8.3853095328e-01,3.9293895170e-01 +299,1.85408145,3.89065726,0.14729644,1.09634674,3.2000701855e-01,2.8014434181e-01,8.3106631009e-01,4.0025965131e-01 +300,1.85479066,3.89418052,0.14656949,1.10001345,3.1697359232e-01,2.7745438339e-01,8.2364892333e-01,4.0727834616e-01 +301,1.85549987,3.89770378,0.14584254,1.10368016,3.1396727285e-01,2.7478889846e-01,8.1627905213e-01,4.1400233165e-01 +302,1.85620908,3.90122703,0.14511559,1.10734688,3.1098789338e-01,2.7214773997e-01,8.0895694003e-01,4.2043876882e-01 +303,1.85691828,3.90475029,0.14438864,1.11101359,3.0803526431e-01,2.6953074059e-01,8.0168275787e-01,4.2659473405e-01 +304,1.85762749,3.90827355,0.14366169,1.11468030,3.0510921453e-01,2.6693774888e-01,7.9445670528e-01,4.3247713006e-01 +305,1.85833670,3.91179680,0.14293473,1.11834701,3.0220957475e-01,2.6436861514e-01,7.8727897074e-01,4.3809272681e-01 +306,1.85904591,3.91532006,0.14220778,1.12201372,2.9933615556e-01,2.6182317162e-01,7.8014967705e-01,4.4344820357e-01 +307,1.85975511,3.91884332,0.14148083,1.12568043,2.9648878655e-01,2.5930126732e-01,7.7306897880e-01,4.4855007459e-01 +308,1.86046432,3.92236657,0.14075388,1.12934715,2.9366729395e-01,2.5680274783e-01,7.6603700720e-01,4.5340473486e-01 +309,1.86117353,3.92588983,0.14002693,1.13301386,2.9087148561e-01,2.5432744261e-01,7.5905383290e-01,4.5801848576e-01 +310,1.86188274,3.92941309,0.13929997,1.13668057,2.8810118837e-01,2.5187519771e-01,7.5211955948e-01,4.6239747181e-01 +311,1.86259195,3.93293634,0.13857302,1.14034728,2.8535623299e-01,2.4944586250e-01,7.4523428635e-01,4.6654771139e-01 +312,1.86330115,3.93645960,0.13784607,1.14401399,2.8263642526e-01,2.4703926420e-01,7.3839803645e-01,4.7047514087e-01 +313,1.86401036,3.93998286,0.13711912,1.14768070,2.7994158986e-01,2.4465524641e-01,7.3161086658e-01,4.7418554794e-01 +314,1.86471957,3.94350612,0.13639217,1.15134741,2.7727154817e-01,2.4229364990e-01,7.2487281205e-01,4.7768460923e-01 +315,1.86542878,3.94702937,0.13566522,1.15501413,2.7462612703e-01,2.3995432005e-01,7.1818390907e-01,4.8097787949e-01 +316,1.86613798,3.95055263,0.13493826,1.15868084,2.7200513229e-01,2.3763708373e-01,7.1154412818e-01,4.8407082586e-01 +317,1.86684719,3.95407589,0.13421131,1.16234755,2.6940839104e-01,2.3534178617e-01,7.0495348113e-01,4.8696877608e-01 +318,1.86755640,3.95759914,0.13348436,1.16601426,2.6683572982e-01,2.3306827225e-01,6.9841196634e-01,4.8967695043e-01 +319,1.86826561,3.96112240,0.13275741,1.16968097,2.6428695510e-01,2.3081636901e-01,6.9191951942e-01,4.9220048120e-01 +320,1.86897482,3.96464566,0.13203046,1.17334768,2.6176189383e-01,2.2858592139e-01,6.8547611636e-01,4.9454437177e-01 +321,1.86968402,3.96816891,0.13130351,1.17701440,2.5926037407e-01,2.2637677535e-01,6.7908172493e-01,4.9671352096e-01 +322,1.87039323,3.97169217,0.13057655,1.18068111,2.5678219939e-01,2.2418875493e-01,6.7273623907e-01,4.9871274427e-01 +323,1.87110244,3.97521543,0.12984960,1.18434782,2.5432720108e-01,2.2202170875e-01,6.6643961290e-01,5.0054673152e-01 +324,1.87181165,3.97873868,0.12912265,1.18801453,2.5189520521e-01,2.1987548064e-01,6.6019177663e-01,5.0222007783e-01 +325,1.87252086,3.98226194,0.12839570,1.19168124,2.4948602035e-01,2.1774989901e-01,6.5399260530e-01,5.0373729121e-01 +326,1.87323006,3.98578520,0.12766875,1.19534795,2.4709947584e-01,2.1564481050e-01,6.4784201711e-01,5.0510276829e-01 +327,1.87393927,3.98930845,0.12694179,1.19901466,2.4473539632e-01,2.1356005735e-01,6.4173990838e-01,5.0632081357e-01 +328,1.87464848,3.99283171,0.12621484,1.20268138,2.4239359564e-01,2.1149547248e-01,6.3568613803e-01,5.0739564336e-01 +329,1.87535769,3.99635497,0.12548789,1.20634809,2.4007390174e-01,2.0945090105e-01,6.2968059184e-01,5.0833137410e-01 +330,1.87606689,3.99987822,0.12476094,1.21001480,2.3777614521e-01,2.0742619055e-01,6.2372315343e-01,5.0913203178e-01 +331,1.87677610,4.00340148,0.12403399,1.21368151,2.3550013568e-01,2.0542116980e-01,6.1781364298e-01,5.0980155986e-01 +332,1.87748531,4.00692474,0.12330704,1.21734822,2.3324570485e-01,2.0343568719e-01,6.1195192922e-01,5.1034380667e-01 +333,1.87819452,4.01044800,0.12258008,1.22101493,2.3101267658e-01,2.0146958408e-01,6.0613785196e-01,5.1076253708e-01 +334,1.87890373,4.01397125,0.12185313,1.22468164,2.2880088473e-01,1.9952271059e-01,6.0037126867e-01,5.1106142985e-01 +335,1.87961293,4.01749451,0.12112618,1.22834836,2.2661014380e-01,1.9759489984e-01,5.9465197831e-01,5.1124408374e-01 +336,1.88032214,4.02101777,0.12039923,1.23201507,2.2444028436e-01,1.9568599886e-01,5.8897981359e-01,5.1131401266e-01 +337,1.88103135,4.02454102,0.11967228,1.23568178,2.2229114068e-01,1.9379585807e-01,5.8335460925e-01,5.1127465177e-01 +338,1.88174056,4.02806428,0.11894533,1.23934849,2.2016252970e-01,1.9192431259e-01,5.7777614710e-01,5.1112935714e-01 +339,1.88244976,4.03158754,0.11821837,1.24301520,2.1805428595e-01,1.9007121299e-01,5.7224424752e-01,5.1088140791e-01 +340,1.88315897,4.03511079,0.11749142,1.24668191,2.1596624473e-01,1.8823641033e-01,5.6675872570e-01,5.1053400799e-01 +341,1.88386818,4.03863405,0.11676447,1.25034863,2.1389822538e-01,1.8641974184e-01,5.6131934790e-01,5.1009028477e-01 +342,1.88457739,4.04215731,0.11603752,1.25401534,2.1185006457e-01,1.8462105985e-01,5.5592591898e-01,5.0955329509e-01 +343,1.88528660,4.04568056,0.11531057,1.25768205,2.0982159955e-01,1.8284021715e-01,5.5057823861e-01,5.0892602527e-01 +344,1.88599580,4.04920382,0.11458361,1.26134876,2.0781265231e-01,1.8107705326e-01,5.4527605970e-01,5.0821138675e-01 +345,1.88670501,4.05272708,0.11385666,1.26501547,2.0582306191e-01,1.7933142244e-01,5.4001917370e-01,5.0741222650e-01 +346,1.88741422,4.05625033,0.11312971,1.26868218,2.0385266778e-01,1.7760317943e-01,5.3480736688e-01,5.0653132548e-01 +347,1.88812343,4.05977359,0.11240276,1.27234889,2.0190129479e-01,1.7589216617e-01,5.2964038082e-01,5.0557139150e-01 +348,1.88883263,4.06329685,0.11167581,1.27601561,1.9996878514e-01,1.7419823982e-01,5.2451799708e-01,5.0453507506e-01 +349,1.88954184,4.06682010,0.11094886,1.27968232,1.9805497884e-01,1.7252125546e-01,5.1943998551e-01,5.0342496200e-01 +350,1.89025105,4.07034336,0.11022190,1.28334903,1.9615970359e-01,1.7086105754e-01,5.1440607772e-01,5.0224356858e-01 +351,1.89096026,4.07386662,0.10949495,1.28701574,1.9428280519e-01,1.6921750633e-01,5.0941604775e-01,5.0099336003e-01 +352,1.89166947,4.07738988,0.10876800,1.29068245,1.9242412221e-01,1.6759045573e-01,5.0446964508e-01,4.9967673719e-01 +353,1.89237867,4.08091313,0.10804105,1.29434916,1.9058350088e-01,1.6597976644e-01,4.9956663435e-01,4.9829604765e-01 +354,1.89308788,4.08443639,0.10731410,1.29801588,1.8876077114e-01,1.6438528476e-01,4.9470673156e-01,4.9685356830e-01 +355,1.89379709,4.08795965,0.10658715,1.30168259,1.8695578114e-01,1.6280687305e-01,4.8988969629e-01,4.9535153321e-01 +356,1.89450630,4.09148290,0.10586019,1.30534930,1.8516837791e-01,1.6124439266e-01,4.8511528021e-01,4.9379212048e-01 +357,1.89521551,4.09500616,0.10513324,1.30901601,1.8339839824e-01,1.5969769605e-01,4.8038320296e-01,4.9217744472e-01 +358,1.89592471,4.09852942,0.10440629,1.31268272,1.8164569196e-01,1.5816664707e-01,4.7569321434e-01,4.9050957852e-01 +359,1.89663392,4.10205267,0.10367934,1.31634943,1.7991010808e-01,1.5665110874e-01,4.7104505726e-01,4.8879054134e-01 +360,1.89734313,4.10557593,0.10295239,1.32001614,1.7819148549e-01,1.5515093546e-01,4.6643844336e-01,4.8702229163e-01 +361,1.89805234,4.10909919,0.10222544,1.32368286,1.7648967692e-01,1.5366599362e-01,4.6187311687e-01,4.8520675091e-01 +362,1.89876154,4.11262244,0.10149848,1.32734957,1.7480453582e-01,1.5219615031e-01,4.5734881985e-01,4.8334579292e-01 +363,1.89947075,4.11614570,0.10077153,1.33101628,1.7313590361e-01,1.5074126207e-01,4.5286525783e-01,4.8144122923e-01 +364,1.90017996,4.11966896,0.10004458,1.33468299,1.7148363578e-01,1.4930119779e-01,4.4842217021e-01,4.7949483994e-01 +365,1.90088917,4.12319221,0.09931763,1.33834970,1.6984758870e-01,1.4787582717e-01,4.4401929489e-01,4.7750836018e-01 +366,1.90159838,4.12671547,0.09859068,1.34201641,1.6822760690e-01,1.4646500961e-01,4.3965633407e-01,4.7548346519e-01 +367,1.90230758,4.13023873,0.09786372,1.34568312,1.6662354877e-01,1.4506861661e-01,4.3533302351e-01,4.7342180246e-01 +368,1.90301679,4.13376198,0.09713677,1.34934984,1.6503527371e-01,1.4368652052e-01,4.3104909813e-01,4.7132497722e-01 +369,1.90372600,4.13728524,0.09640982,1.35301655,1.6346262948e-01,1.4231858366e-01,4.2680425791e-01,4.6919453701e-01 +370,1.90443521,4.14080850,0.09568287,1.35668326,1.6190547744e-01,1.4096468020e-01,4.2259823606e-01,4.6703200510e-01 +371,1.90514441,4.14433175,0.09495592,1.36034997,1.6036368057e-01,1.3962468579e-01,4.1843076690e-01,4.6483886671e-01 +372,1.90585362,4.14785501,0.09422897,1.36401668,1.5883708845e-01,1.3829846436e-01,4.1430154516e-01,4.6261654726e-01 +373,1.90656283,4.15137827,0.09350201,1.36768339,1.5732556536e-01,1.3698589274e-01,4.1021030213e-01,4.6036645393e-01 +374,1.90727204,4.15490153,0.09277506,1.37135011,1.5582897418e-01,1.3568684657e-01,4.0615676231e-01,4.5808995351e-01 +375,1.90798125,4.15842478,0.09204811,1.37501682,1.5434718026e-01,1.3440120365e-01,4.0214065378e-01,4.5578837897e-01 +376,1.90869045,4.16194804,0.09132116,1.37868353,1.5288003987e-01,1.3312883400e-01,3.9816167708e-01,4.5346301225e-01 +377,1.90939966,4.16547130,0.09059421,1.38235024,1.5142741979e-01,1.3186961668e-01,3.9421955835e-01,4.5111511473e-01 +378,1.91010887,4.16899455,0.08986726,1.38601695,1.4998918989e-01,1.3062343356e-01,3.9031402934e-01,4.4874591808e-01 +379,1.91081808,4.17251781,0.08914030,1.38968366,1.4856520826e-01,1.2939015635e-01,3.8644478704e-01,4.4635659965e-01 +380,1.91152728,4.17604107,0.08841335,1.39335037,1.4715534836e-01,1.2816967012e-01,3.8261156757e-01,4.4394832822e-01 +381,1.91223649,4.17956432,0.08768640,1.39701709,1.4575948012e-01,1.2696185683e-01,3.7881409478e-01,4.4152223325e-01 +382,1.91294570,4.18308758,0.08695945,1.40068380,1.4437746639e-01,1.2576659248e-01,3.7505207082e-01,4.3907939954e-01 +383,1.91365491,4.18661084,0.08623250,1.40435051,1.4300918136e-01,1.2458376288e-01,3.7132522620e-01,4.3662089940e-01 +384,1.91436412,4.19013409,0.08550554,1.40801722,1.4165449939e-01,1.2341325402e-01,3.6763328942e-01,4.3414777445e-01 +385,1.91507332,4.19365735,0.08477859,1.41168393,1.4031328804e-01,1.2225494613e-01,3.6397596822e-01,4.3166102345e-01 +386,1.91578253,4.19718061,0.08405164,1.41535064,1.3898542239e-01,1.2110872586e-01,3.6035298843e-01,4.2916162827e-01 +387,1.91649174,4.20070386,0.08332469,1.41901735,1.3767078115e-01,1.1997448314e-01,3.5676408362e-01,4.2665054886e-01 +388,1.91720095,4.20422712,0.08259774,1.42268407,1.3636923301e-01,1.1885209931e-01,3.5320895790e-01,4.2412869722e-01 +389,1.91791015,4.20775038,0.08187079,1.42635078,1.3508065871e-01,1.1774146615e-01,3.4968734608e-01,4.2159698019e-01 +390,1.91861936,4.21127363,0.08114383,1.43001749,1.3380493727e-01,1.1664247390e-01,3.4619897614e-01,4.1905627354e-01 +391,1.91932857,4.21479689,0.08041688,1.43368420,1.3254194263e-01,1.1555500864e-01,3.4274356037e-01,4.1650741636e-01 +392,1.92003778,4.21832015,0.07968993,1.43735091,1.3129155715e-01,1.1447896365e-01,3.3932083190e-01,4.1395123771e-01 +393,1.92074699,4.22184341,0.07896298,1.44101762,1.3005366147e-01,1.1341423083e-01,3.3593051740e-01,4.1138853753e-01 +394,1.92145619,4.22536666,0.07823603,1.44468434,1.2882814106e-01,1.1236070629e-01,3.3257235488e-01,4.0882010062e-01 +395,1.92216540,4.22888992,0.07750908,1.44835105,1.2761487092e-01,1.1131827714e-01,3.2924605178e-01,4.0624666460e-01 +396,1.92287461,4.23241318,0.07678212,1.45201776,1.2641373790e-01,1.1028684079e-01,3.2595134629e-01,4.0366896791e-01 +397,1.92358382,4.23593643,0.07605517,1.45568447,1.2522463105e-01,1.0926629657e-01,3.2268798092e-01,4.0108773009e-01 +398,1.92429303,4.23945969,0.07532822,1.45935118,1.2404742909e-01,1.0825653502e-01,3.1945566817e-01,3.9850362509e-01 +399,1.92500223,4.24298295,0.07460127,1.46301789,1.2288202236e-01,1.0725745672e-01,3.1625415075e-01,3.9591732929e-01 +400,1.92571144,4.24650620,0.07387432,1.46668460,1.2172830028e-01,1.0626896143e-01,3.1308316729e-01,3.9332949467e-01 +401,1.92642065,4.25002946,0.07314736,1.47035132,1.2058614533e-01,1.0529094313e-01,3.0994243578e-01,3.9074073609e-01 +402,1.92712986,4.25355272,0.07242041,1.47401803,1.1945545133e-01,1.0432330552e-01,3.0683170383e-01,3.8815167227e-01 +403,1.92783906,4.25707597,0.07169346,1.47768474,1.1833611164e-01,1.0336595199e-01,3.0375071638e-01,3.8556290047e-01 +404,1.92854827,4.26059923,0.07096651,1.48135145,1.1722801070e-01,1.0241877832e-01,3.0069919236e-01,3.8297497646e-01 +405,1.92925748,4.26412249,0.07023956,1.48501816,1.1613104447e-01,1.0148169023e-01,2.9767688094e-01,3.8038846279e-01 +406,1.92996669,4.26764574,0.06951261,1.48868487,1.1504510915e-01,1.0055459371e-01,2.9468353067e-01,3.7780390316e-01 +407,1.93067590,4.27116900,0.06878565,1.49235159,1.1397009278e-01,9.9637387843e-02,2.9171886624e-01,3.7522180262e-01 +408,1.93138510,4.27469226,0.06805870,1.49601830,1.1290589518e-01,9.8729981836e-02,2.8878264352e-01,3.7264267555e-01 +409,1.93209431,4.27821551,0.06733175,1.49968501,1.1185241364e-01,9.7832282746e-02,2.8587461016e-01,3.7006701177e-01 +410,1.93280352,4.28173877,0.06660480,1.50335172,1.1080954032e-01,9.6944193374e-02,2.8299449834e-01,3.6749527074e-01 +411,1.93351273,4.28526203,0.06587785,1.50701843,1.0977717605e-01,9.6065623968e-02,2.8014206306e-01,3.6492791539e-01 +412,1.93422193,4.28878529,0.06515090,1.51068514,1.0875522033e-01,9.5196483712e-02,2.7731705443e-01,3.6236538830e-01 +413,1.93493114,4.29230854,0.06442394,1.51435185,1.0774357415e-01,9.4336683048e-02,2.7451922549e-01,3.5980811863e-01 +414,1.93564035,4.29583180,0.06369699,1.51801857,1.0674213461e-01,9.3486129276e-02,2.7174831751e-01,3.5725650949e-01 +415,1.93634956,4.29935506,0.06297004,1.52168528,1.0575080558e-01,9.2644735448e-02,2.6900408926e-01,3.5471096472e-01 +416,1.93705877,4.30287831,0.06224309,1.52535199,1.0476949223e-01,9.1812415818e-02,2.6628630221e-01,3.5217187590e-01 +417,1.93776797,4.30640157,0.06151614,1.52901870,1.0379809344e-01,9.0989079369e-02,2.6359469924e-01,3.4963960290e-01 +418,1.93847718,4.30992483,0.06078918,1.53268541,1.0283651473e-01,9.0174640721e-02,2.6092904064e-01,3.4711450720e-01 +419,1.93918639,4.31344808,0.06006223,1.53635212,1.0188466541e-01,8.9369017843e-02,2.5828909628e-01,3.4459694587e-01 +420,1.93989560,4.31697134,0.05933528,1.54001883,1.0094244617e-01,8.8572121433e-02,2.5567461108e-01,3.4208723856e-01 +421,1.94060480,4.32049460,0.05860833,1.54368555,1.0000976704e-01,8.7783870196e-02,2.5308535502e-01,3.3958571550e-01 +422,1.94131401,4.32401785,0.05788138,1.54735226,9.9086537445e-02,8.7004182309e-02,2.5052109539e-01,3.3709269122e-01 +423,1.94202322,4.32754111,0.05715443,1.55101897,9.8172661807e-02,8.6232971873e-02,2.4798158477e-01,3.3460845342e-01 +424,1.94273243,4.33106437,0.05642747,1.55468568,9.7268051740e-02,8.5470159093e-02,2.4546659480e-01,3.3213329570e-01 +425,1.94344164,4.33458762,0.05570052,1.55835239,9.6372621079e-02,8.4715666112e-02,2.4297590247e-01,3.2966750488e-01 +426,1.94415084,4.33811088,0.05497357,1.56201910,9.5486276438e-02,8.3969409082e-02,2.4050926386e-01,3.2721133525e-01 +427,1.94486005,4.34163414,0.05424662,1.56568582,9.4608931504e-02,8.3231310115e-02,2.3806645389e-01,3.2476504774e-01 +428,1.94556926,4.34515739,0.05351967,1.56935253,9.3740502039e-02,8.2501293188e-02,2.3564725247e-01,3.2232889721e-01 +429,1.94627847,4.34868065,0.05279272,1.57301924,9.2880897360e-02,8.1779276922e-02,2.3325142082e-01,3.1990310842e-01 +430,1.94698767,4.35220391,0.05206576,1.57668595,9.2030034430e-02,8.1065186440e-02,2.3087874068e-01,3.1748791592e-01 +431,1.94769688,4.35572717,0.05133881,1.58035266,9.1187828844e-02,8.0358945725e-02,2.2852898923e-01,3.1508353872e-01 +432,1.94840609,4.35925042,0.05061186,1.58401937,9.0354199086e-02,7.9660481304e-02,2.2620195099e-01,3.1269019316e-01 +433,1.94911530,4.36277368,0.04988491,1.58768608,8.9529057925e-02,7.8969714976e-02,2.2389739388e-01,3.1030806817e-01 +434,1.94982451,4.36629694,0.04915796,1.59135280,8.8712325516e-02,7.8286574785e-02,2.2161510567e-01,3.0793736302e-01 +435,1.95053371,4.36982019,0.04843100,1.59501951,8.7903922688e-02,7.7610989417e-02,2.1935487542e-01,3.0557826869e-01 +436,1.95124292,4.37334345,0.04770405,1.59868622,8.7103764803e-02,7.6942883018e-02,2.1711647619e-01,3.0323094966e-01 +437,1.95195213,4.37686671,0.04697710,1.60235293,8.6311774291e-02,7.6282185750e-02,2.1489970018e-01,3.0089558115e-01 +438,1.95266134,4.38038996,0.04625015,1.60601964,8.5527874307e-02,7.5628828427e-02,2.1270434102e-01,2.9857233074e-01 +439,1.95337055,4.38391322,0.04552320,1.60968635,8.4751982620e-02,7.4982737448e-02,2.1053017661e-01,2.9626134040e-01 +440,1.95407975,4.38743648,0.04479625,1.61335306,8.3984024333e-02,7.4343845404e-02,2.0837700483e-01,2.9396276444e-01 +441,1.95478896,4.39095973,0.04406929,1.61701978,8.3223923673e-02,7.3712084166e-02,2.0624462045e-01,2.9167674518e-01 +442,1.95549817,4.39448299,0.04334234,1.62068649,8.2471602028e-02,7.3087383380e-02,2.0413280979e-01,2.8940340761e-01 +443,1.95620738,4.39800625,0.04261539,1.62435320,8.1726985918e-02,7.2469676996e-02,2.0204137293e-01,2.8714288319e-01 +444,1.95691658,4.40152950,0.04188844,1.62801991,8.0990003308e-02,7.1858900242e-02,1.9997011352e-01,2.8489529928e-01 +445,1.95762579,4.40505276,0.04116149,1.63168662,8.0260576004e-02,7.1254983272e-02,1.9791881730e-01,2.8266075578e-01 +446,1.95833500,4.40857602,0.04043454,1.63535333,7.9538633470e-02,7.0657862696e-02,1.9588729106e-01,2.8043936784e-01 +447,1.95904421,4.41209927,0.03970758,1.63902005,7.8824105069e-02,7.0067475084e-02,1.9387534075e-01,2.7823124220e-01 +448,1.95975342,4.41562253,0.03898063,1.64268676,7.8116916419e-02,6.9483753993e-02,1.9188276131e-01,2.7603646613e-01 +449,1.96046262,4.41914579,0.03825368,1.64635347,7.7416998689e-02,6.8906637637e-02,1.8990936280e-01,2.7385513632e-01 +450,1.96117183,4.42266904,0.03752673,1.65002018,7.6724283021e-02,6.8336064230e-02,1.8795495468e-01,2.7168734162e-01 +451,1.96188104,4.42619230,0.03679978,1.65368689,7.6038696868e-02,6.7771969070e-02,1.8601933559e-01,2.6953315214e-01 +452,1.96259025,4.42971556,0.03607283,1.65735360,7.5360173502e-02,6.7214292291e-02,1.8410232005e-01,2.6739264878e-01 +453,1.96329945,4.43323882,0.03534587,1.66102031,7.4688645035e-02,6.6662973157e-02,1.8220371890e-01,2.6526590182e-01 +454,1.96400866,4.43676207,0.03461892,1.66468703,7.4024046083e-02,6.6117953011e-02,1.8032334956e-01,2.6315298228e-01 +455,1.96471787,4.44028533,0.03389197,1.66835374,7.3366306643e-02,6.5579169525e-02,1.7846101599e-01,2.6105393992e-01 +456,1.96542708,4.44380859,0.03316502,1.67202045,7.2715362663e-02,6.5046565299e-02,1.7661653850e-01,2.5896883661e-01 +457,1.96613629,4.44733184,0.03243807,1.67568716,7.2071150694e-02,6.4520083489e-02,1.7478973872e-01,2.5689772972e-01 +458,1.96684549,4.45085510,0.03171111,1.67935387,7.1433602864e-02,6.3999663727e-02,1.7298042530e-01,2.5484065607e-01 +459,1.96755470,4.45437836,0.03098416,1.68302058,7.0802657122e-02,6.3485250423e-02,1.7118842297e-01,2.5279766470e-01 +460,1.96826391,4.45790161,0.03025721,1.68668730,7.0178251934e-02,6.2976788510e-02,1.6941355753e-01,2.5076880046e-01 +461,1.96897312,4.46142487,0.02953026,1.69035401,6.9560321530e-02,6.2474219538e-02,1.6765564238e-01,2.4875408843e-01 +462,1.96968232,4.46494813,0.02880331,1.69402072,6.8948806021e-02,6.1977489917e-02,1.6591450718e-01,2.4675356694e-01 +463,1.97039153,4.46847138,0.02807636,1.69768743,6.8343645177e-02,6.1486545804e-02,1.6418998025e-01,2.4476726732e-01 +464,1.97110074,4.47199464,0.02734940,1.70135414,6.7744775242e-02,6.1001330614e-02,1.6248187951e-01,2.4279520394e-01 +465,1.97180995,4.47551790,0.02662245,1.70502085,6.7152138529e-02,6.0521792731e-02,1.6079003975e-01,2.4083740548e-01 +466,1.97251916,4.47904115,0.02589550,1.70868756,6.6565676942e-02,6.0047880269e-02,1.5911429426e-01,2.3889389397e-01 +467,1.97322836,4.48256441,0.02516855,1.71235428,6.5985329004e-02,5.9579538702e-02,1.5745446631e-01,2.3696467506e-01 +468,1.97393757,4.48608767,0.02444160,1.71602099,6.5411037616e-02,5.9116717064e-02,1.5581039125e-01,2.3504976350e-01 +469,1.97464678,4.48961092,0.02371465,1.71968770,6.4842747101e-02,5.8659365636e-02,1.5418190819e-01,2.3314917396e-01 +470,1.97535599,4.49313418,0.02298769,1.72335441,6.4280397320e-02,5.8207431174e-02,1.5256884311e-01,2.3126290126e-01 +471,1.97606519,4.49665744,0.02226074,1.72702112,6.3723934410e-02,5.7760865552e-02,1.5097103961e-01,2.2939095636e-01 +472,1.97677440,4.50018070,0.02153379,1.73068783,6.3173301855e-02,5.7319618552e-02,1.4938833330e-01,2.2753333647e-01 +473,1.97748361,4.50370395,0.02080684,1.73435454,6.2628446297e-02,5.6883642582e-02,1.4782056857e-01,2.2569004500e-01 +474,1.97819282,4.50722721,0.02007989,1.73802126,6.2089310399e-02,5.6452886933e-02,1.4626757800e-01,2.2386106733e-01 +475,1.97890203,4.51075047,0.01935293,1.74168797,6.1555841420e-02,5.6027304632e-02,1.4472920703e-01,2.2204639988e-01 +476,1.97961123,4.51427372,0.01862598,1.74535468,6.1027988256e-02,5.5606850090e-02,1.4320530554e-01,2.2024604046e-01 +477,1.98032044,4.51779698,0.01789903,1.74902139,6.0505694574e-02,5.5191473591e-02,1.4169570795e-01,2.1845996463e-01 +478,1.98102965,4.52132024,0.01717208,1.75268810,5.9988909851e-02,5.4781130144e-02,1.4020026512e-01,2.1668816374e-01 +479,1.98173886,4.52484349,0.01644513,1.75635481,5.9477583805e-02,5.4375774999e-02,1.3871882831e-01,2.1493062589e-01 +480,1.98244807,4.52836675,0.01571818,1.76002153,5.8971662691e-02,5.3975360736e-02,1.3725123841e-01,2.1318732332e-01 +481,1.98315727,4.53189001,0.01499122,1.76368824,5.8471097331e-02,5.3579843629e-02,1.3579734922e-01,2.1145824008e-01 +482,1.98386648,4.53541326,0.01426427,1.76735495,5.7975839097e-02,5.3189180416e-02,1.3435701582e-01,2.0974335820e-01 +483,1.98457569,4.53893652,0.01353732,1.77102166,5.7485835951e-02,5.2803325253e-02,1.3293008306e-01,2.0804264434e-01 +484,1.98528490,4.54245978,0.01281037,1.77468837,5.7001040323e-02,5.2422235871e-02,1.3151640847e-01,2.0635607688e-01 +485,1.98599410,4.54598303,0.01208342,1.77835508,5.6521405359e-02,5.2045870636e-02,1.3011585135e-01,2.0468363321e-01 +486,1.98670331,4.54950629,0.01135647,1.78202179,5.6046880246e-02,5.1674184853e-02,1.2872825912e-01,2.0302527329e-01 +487,1.98741252,4.55302955,0.01062951,1.78568851,5.5577418950e-02,5.1307137671e-02,1.2735349284e-01,2.0138097041e-01 +488,1.98812173,4.55655280,0.00990256,1.78935522,5.5112976420e-02,5.0944689058e-02,1.2599141617e-01,1.9975069795e-01 +489,1.98883094,4.56007606,0.00917561,1.79302193,5.4653503711e-02,5.0586796014e-02,1.2464188102e-01,1.9813441235e-01 +490,1.98954014,4.56359932,0.00844866,1.79668864,5.4198956534e-02,5.0233419248e-02,1.2330475265e-01,1.9653208321e-01 +491,1.99024935,4.56712258,0.00772171,1.80035535,5.3749289200e-02,4.9884518417e-02,1.2197989188e-01,1.9494367191e-01 +492,1.99095856,4.57064583,0.00699475,1.80402206,5.3304458131e-02,4.9540054912e-02,1.2066716544e-01,1.9336914436e-01 +493,1.99166777,4.57416909,0.00626780,1.80768878,5.2864417239e-02,4.9199988241e-02,1.1936643238e-01,1.9180845451e-01 +494,1.99237697,4.57769235,0.00554085,1.81135549,5.2429123936e-02,4.8864280697e-02,1.1807756173e-01,1.9026156581e-01 +495,1.99308618,4.58121560,0.00481390,1.81502220,5.1998535667e-02,4.8532894631e-02,1.1680042232e-01,1.8872843877e-01 +496,1.99379539,4.58473886,0.00408695,1.81868891,5.1572607627e-02,4.8205790769e-02,1.1553487600e-01,1.8720902326e-01 +497,1.99450460,4.58826212,0.00336000,1.82235562,5.1151298677e-02,4.7882932709e-02,1.1428079519e-01,1.8570327930e-01 +498,1.99521381,4.59178537,0.00263304,1.82602233,5.0734567883e-02,4.7564284269e-02,1.1303805258e-01,1.8421116499e-01 +499,1.99592301,4.59530863,0.00190609,1.82968904,5.0322372385e-02,4.7249807876e-02,1.1180651488e-01,1.8273262891e-01 +500,1.99663222,4.59883189,0.00117914,1.83335576,4.9914671788e-02,4.6939467884e-02,1.1058605576e-01,1.8126762575e-01 +501,1.99734143,4.60235514,0.00045219,1.83702247,4.9511426965e-02,4.6633229699e-02,1.0937655240e-01,1.7981611228e-01 +502,1.99805064,4.60587840,-0.00027476,1.84068918,4.9112595870e-02,4.6331056576e-02,1.0817787294e-01,1.7837803231e-01 +503,1.99875984,4.60940166,-0.00100171,1.84435589,4.8718140298e-02,4.6032914754e-02,1.0698989668e-01,1.7695334098e-01 +504,1.99946905,4.61292491,-0.00172867,1.84802260,4.8328021648e-02,4.5738770221e-02,1.0581250145e-01,1.7554198957e-01 +505,2.00017826,4.61644817,-0.00245562,1.85168931,4.7942199926e-02,4.5448588004e-02,1.0464556056e-01,1.7414392206e-01 +506,2.00088747,4.61997143,-0.00318257,1.85535602,4.7560637834e-02,4.5162335224e-02,1.0348895512e-01,1.7275908986e-01 +507,2.00159668,4.62349468,-0.00390952,1.85902274,4.7183298686e-02,4.4879979521e-02,1.0234256777e-01,1.7138744431e-01 +508,2.00230588,4.62701794,-0.00463647,1.86268945,4.6810143450e-02,4.4601486861e-02,1.0120627375e-01,1.7002892614e-01 +509,2.00301509,4.63054120,-0.00536343,1.86635616,4.6441135753e-02,4.4326825249e-02,1.0007995598e-01,1.6868348339e-01 +510,2.00372430,4.63406446,-0.00609038,1.87002287,4.6076239814e-02,4.4055963213e-02,9.8963498916e-02,1.6735106431e-01 +511,2.00443351,4.63758771,-0.00681733,1.87368958,4.5715420437e-02,4.3788869776e-02,9.7856788464e-02,1.6603161714e-01 +512,2.00514272,4.64111097,-0.00754428,1.87735629,4.5358640215e-02,4.3525512396e-02,9.6759703415e-02,1.6472508004e-01 +513,2.00585192,4.64463423,-0.00827123,1.88102301,4.5005865183e-02,4.3265861153e-02,9.5672132789e-02,1.6343140179e-01 +514,2.00656113,4.64815748,-0.00899818,1.88468972,4.4657061227e-02,4.3009886058e-02,9.4593964806e-02,1.6215052856e-01 +515,2.00727034,4.65168074,-0.00972514,1.88835643,4.4312192247e-02,4.2757555762e-02,9.3525081223e-02,1.6088239745e-01 +516,2.00797955,4.65520400,-0.01045209,1.89202314,4.3971225688e-02,4.2508841572e-02,9.2465374415e-02,1.5962695663e-01 +517,2.00868875,4.65872725,-0.01117904,1.89568985,4.3634128970e-02,4.2263714846e-02,9.1414736360e-02,1.5838415243e-01 +518,2.00939796,4.66225051,-0.01190599,1.89935656,4.3300866948e-02,4.2022145133e-02,9.0373050678e-02,1.5715391982e-01 +519,2.01010717,4.66577377,-0.01263294,1.90302327,4.2971408047e-02,4.1784104664e-02,8.9340211768e-02,1.5593620522e-01 +520,2.01081638,4.66929702,-0.01335989,1.90668999,4.2645720881e-02,4.1549565859e-02,8.8316114290e-02,1.5473095405e-01 +521,2.01152559,4.67282028,-0.01408685,1.91035670,4.2323771722e-02,4.1318499532e-02,8.7300645183e-02,1.5353810135e-01 +522,2.01223479,4.67634354,-0.01481380,1.91402341,4.2005530483e-02,4.1090879183e-02,8.6293702493e-02,1.5235759399e-01 +523,2.01294400,4.67986679,-0.01554075,1.91769012,4.1690966428e-02,4.0866677897e-02,8.5295181848e-02,1.5118937474e-01 +524,2.01365321,4.68339005,-0.01626770,1.92135683,4.1380047387e-02,4.0645867830e-02,8.4304973964e-02,1.5003337954e-01 +525,2.01436242,4.68691331,-0.01699465,1.92502354,4.1072743893e-02,4.0428423124e-02,8.3322977758e-02,1.4888955276e-01 +526,2.01507162,4.69043656,-0.01772161,1.92869025,4.0769026809e-02,4.0214318226e-02,8.2349092853e-02,1.4775783848e-01 +527,2.01578083,4.69395982,-0.01844856,1.93235697,4.0468865020e-02,4.0003526246e-02,8.1383212152e-02,1.4663817185e-01 +528,2.01649004,4.69748308,-0.01917551,1.93602368,4.0172230145e-02,3.9796022306e-02,8.0425236935e-02,1.4553049680e-01 +529,2.01719925,4.70100634,-0.01990246,1.93969039,3.9879093385e-02,3.9591781299e-02,7.9475066754e-02,1.4443475422e-01 +530,2.01790846,4.70452959,-0.02062941,1.94335710,3.9589427063e-02,3.9390778965e-02,7.8532604419e-02,1.4335088777e-01 +531,2.01861766,4.70805285,-0.02135636,1.94702381,3.9303201730e-02,3.9192989896e-02,7.7597746588e-02,1.4227883311e-01 +532,2.01932687,4.71157611,-0.02208332,1.95069052,3.9020390005e-02,3.8998390178e-02,7.6670396199e-02,1.4121853220e-01 +533,2.02003608,4.71509936,-0.02281027,1.95435724,3.8740965716e-02,3.8806956806e-02,7.5750459814e-02,1.4016993024e-01 +534,2.02074529,4.71862262,-0.02353722,1.95802395,3.8464900246e-02,3.8618665171e-02,7.4837835527e-02,1.3913296195e-01 +535,2.02145449,4.72214588,-0.02426417,1.96169066,3.8192167828e-02,3.8433492689e-02,7.3932430392e-02,1.3810757143e-01 +536,2.02216370,4.72566913,-0.02499112,1.96535737,3.7922742600e-02,3.8251416764e-02,7.3034150753e-02,1.3709370112e-01 +537,2.02287291,4.72919239,-0.02571807,1.96902408,3.7656597340e-02,3.8072413972e-02,7.2142897997e-02,1.3609128713e-01 +538,2.02358212,4.73271565,-0.02644503,1.97269079,3.7393707021e-02,3.7896462441e-02,7.1258580372e-02,1.3510027259e-01 +539,2.02429133,4.73623890,-0.02717198,1.97635750,3.7134047342e-02,3.7723540850e-02,7.0381108208e-02,1.3412060217e-01 +540,2.02500053,4.73976216,-0.02789893,1.98002422,3.6877592008e-02,3.7553626626e-02,6.9510384629e-02,1.3315221184e-01 +541,2.02570974,4.74328542,-0.02862588,1.98369093,3.6624316866e-02,3.7386698690e-02,6.8646319530e-02,1.3219504444e-01 +542,2.02641895,4.74680867,-0.02935283,1.98735764,3.6374198452e-02,3.7222736493e-02,6.7788824745e-02,1.3124904432e-01 +543,2.02712816,4.75033193,-0.03007978,1.99102435,3.6127211536e-02,3.7061718402e-02,6.6937805597e-02,1.3031414800e-01 +544,2.02783736,4.75385519,-0.03080674,1.99469106,3.5883333185e-02,3.6903624369e-02,6.6093174803e-02,1.2939029960e-01 +545,2.02854657,4.75737844,-0.03153369,1.99835777,3.5642540786e-02,3.6748434608e-02,6.5254845768e-02,1.2847744332e-01 +546,2.02925578,4.76090170,-0.03226064,2.00202449,3.5404810131e-02,3.6596128393e-02,6.4422725856e-02,1.2757551631e-01 +547,2.02996499,4.76442496,-0.03298759,2.00569120,3.5170119244e-02,3.6446686511e-02,6.3596729709e-02,1.2668446307e-01 +548,2.03067420,4.76794821,-0.03371454,2.00935791,3.4938446438e-02,3.6300090000e-02,6.2776772536e-02,1.2580422820e-01 +549,2.03138340,4.77147147,-0.03444150,2.01302462,3.4709768514e-02,3.6156319024e-02,6.1962763690e-02,1.2493474952e-01 +550,2.03209261,4.77499473,-0.03516845,2.01669133,3.4484064441e-02,3.6015355184e-02,6.1154619699e-02,1.2407597206e-01 +551,2.03280182,4.77851799,-0.03589540,2.02035804,3.4261312865e-02,3.5877179964e-02,6.0352255435e-02,1.2322783867e-01 +552,2.03351103,4.78204124,-0.03662235,2.02402475,3.4041493325e-02,3.5741775473e-02,5.9555588484e-02,1.2239029461e-01 +553,2.03422024,4.78556450,-0.03734930,2.02769147,3.3824583940e-02,3.5609123025e-02,5.8764530758e-02,1.2156327877e-01 +554,2.03492944,4.78908776,-0.03807625,2.03135818,3.3610564978e-02,3.5479205340e-02,5.7979001420e-02,1.2074673728e-01 +555,2.03563865,4.79261101,-0.03880321,2.03502489,3.3399416550e-02,3.5352005106e-02,5.7198918529e-02,1.1994061472e-01 +556,2.03634786,4.79613427,-0.03953016,2.03869160,3.3191118066e-02,3.5227504678e-02,5.6424197052e-02,1.1914485212e-01 +557,2.03705707,4.79965753,-0.04025711,2.04235831,3.2985650459e-02,3.5105687409e-02,5.5654756971e-02,1.1835939537e-01 +558,2.03776627,4.80318078,-0.04098406,2.04602502,3.2782995145e-02,3.4986537011e-02,5.4890519575e-02,1.1758419136e-01 +559,2.03847548,4.80670404,-0.04171101,2.04969173,3.2583131939e-02,3.4870036333e-02,5.4131399522e-02,1.1681917991e-01 +560,2.03918469,4.81022730,-0.04243796,2.05335845,3.2386042856e-02,3.4756169605e-02,5.3377319184e-02,1.1606430844e-01 +561,2.03989390,4.81375055,-0.04316492,2.05702516,3.2191709972e-02,3.4644921165e-02,5.2628200602e-02,1.1531952371e-01 +562,2.04060311,4.81727381,-0.04389187,2.06069187,3.2000114431e-02,3.4536274902e-02,5.1883961677e-02,1.1458476808e-01 +563,2.04131231,4.82079707,-0.04461882,2.06435858,3.1811238986e-02,3.4430215712e-02,5.1144525852e-02,1.1385998924e-01 +564,2.04202152,4.82432032,-0.04534577,2.06802529,3.1625066471e-02,3.4326728604e-02,5.0409816306e-02,1.1314513433e-01 +565,2.04273073,4.82784358,-0.04607272,2.07169200,3.1441578823e-02,3.4225798188e-02,4.9679752060e-02,1.1244014624e-01 +566,2.04343994,4.83136684,-0.04679968,2.07535872,3.1260759545e-02,3.4127410029e-02,4.8954257627e-02,1.1174497306e-01 +567,2.04414914,4.83489009,-0.04752663,2.07902543,3.1082592768e-02,3.4031550108e-02,4.8233259508e-02,1.1105956451e-01 +568,2.04485835,4.83841335,-0.04825358,2.08269214,3.0907060946e-02,3.3938203592e-02,4.7516676705e-02,1.1038386304e-01 +569,2.04556756,4.84193661,-0.04898053,2.08635885,3.0734148585e-02,3.3847356842e-02,4.6804435857e-02,1.0971781822e-01 +570,2.04627677,4.84545987,-0.04970748,2.09002556,3.0563839907e-02,3.3758996155e-02,4.6096461788e-02,1.0906137772e-01 +571,2.04698598,4.84898312,-0.05043443,2.09369227,3.0396119845e-02,3.3673108280e-02,4.5392681604e-02,1.0841449121e-01 +572,2.04769518,4.85250638,-0.05116139,2.09735898,3.0230972281e-02,3.3589679525e-02,4.4693017283e-02,1.0777710352e-01 +573,2.04840439,4.85602964,-0.05188834,2.10102570,3.0068382711e-02,3.3508697113e-02,4.3997396863e-02,1.0714916496e-01 +574,2.04911360,4.85955289,-0.05261529,2.10469241,2.9908336857e-02,3.3430148465e-02,4.3305748667e-02,1.0653062595e-01 +575,2.04982281,4.86307615,-0.05334224,2.10835912,2.9750819459e-02,3.3354020616e-02,4.2617996024e-02,1.0592143236e-01 +576,2.05053201,4.86659941,-0.05406919,2.11202583,2.9595816864e-02,3.3280301486e-02,4.1934068459e-02,1.0532153554e-01 +577,2.05124122,4.87012266,-0.05479614,2.11569254,2.9443315428e-02,3.3208979079e-02,4.1253894812e-02,1.0473088612e-01 +578,2.05195043,4.87364592,-0.05552310,2.11935925,2.9293300727e-02,3.3140041144e-02,4.0577399634e-02,1.0414943099e-01 +579,2.05265964,4.87716918,-0.05625005,2.12302597,2.9145759962e-02,3.3073476283e-02,3.9904513987e-02,1.0357712260e-01 +580,2.05336885,4.88069243,-0.05697700,2.12669268,2.9000680334e-02,3.3009273183e-02,3.9235168145e-02,1.0301391267e-01 +581,2.05407805,4.88421569,-0.05770395,2.13035939,2.8858048313e-02,3.2947420317e-02,3.8569288202e-02,1.0245974938e-01 +582,2.05478726,4.88773895,-0.05843090,2.13402610,2.8717851561e-02,3.2887906784e-02,3.7906804865e-02,1.0191458478e-01 +583,2.05549647,4.89126220,-0.05915786,2.13769281,2.8580078116e-02,3.2830721945e-02,3.7247649809e-02,1.0137837169e-01 +584,2.05620568,4.89478546,-0.05988481,2.14135952,2.8444715334e-02,3.2775854978e-02,3.6591750629e-02,1.0085105958e-01 +585,2.05691488,4.89830872,-0.06061176,2.14502623,2.8311751762e-02,3.2723295666e-02,3.5939039681e-02,1.0033260179e-01 +586,2.05762409,4.90183197,-0.06133871,2.14869295,2.8181176052e-02,3.2673033927e-02,3.5289448990e-02,9.9822951413e-02 +587,2.05833330,4.90535523,-0.06206566,2.15235966,2.8052976281e-02,3.2625059578e-02,3.4642906795e-02,9.9322058554e-02 +588,2.05904251,4.90887849,-0.06279261,2.15602637,2.7927141742e-02,3.2579363014e-02,3.3999346433e-02,9.8829877376e-02 +589,2.05975172,4.91240175,-0.06351957,2.15969308,2.7803661572e-02,3.2535934678e-02,3.3358699487e-02,9.8346360695e-02 +590,2.06046092,4.91592500,-0.06424652,2.16335979,2.7682525722e-02,3.2494765414e-02,3.2720900904e-02,9.7871463854e-02 +591,2.06117013,4.91944826,-0.06497347,2.16702650,2.7563723065e-02,3.2455845813e-02,3.2085878874e-02,9.7405137232e-02 +592,2.06187934,4.92297152,-0.06570042,2.17069321,2.7447243928e-02,3.2419167086e-02,3.1453568224e-02,9.6947336198e-02 +593,2.06258855,4.92649477,-0.06642737,2.17435993,2.7333078797e-02,3.2384720600e-02,3.0823903714e-02,9.6498016052e-02 +594,2.06329776,4.93001803,-0.06715432,2.17802664,2.7221217506e-02,3.2352497650e-02,3.0196815432e-02,9.6057128817e-02 +595,2.06400696,4.93354129,-0.06788128,2.18169335,2.7111651037e-02,3.2322490008e-02,2.9572238660e-02,9.5624630296e-02 +596,2.06471617,4.93706454,-0.06860823,2.18536006,2.7004370595e-02,3.2294689617e-02,2.8950109010e-02,9.5200476518e-02 +597,2.06542538,4.94058780,-0.06933518,2.18902677,2.6899366777e-02,3.2269088401e-02,2.8330357374e-02,9.4784620354e-02 +598,2.06613459,4.94411106,-0.07006213,2.19269348,2.6796631276e-02,3.2245678701e-02,2.7712919819e-02,9.4377018288e-02 +599,2.06684379,4.94763431,-0.07078908,2.19636020,2.6696156050e-02,3.2224453040e-02,2.7097732951e-02,9.3977627142e-02 +600,2.06755300,4.95115757,-0.07151604,2.20002691,2.6597932307e-02,3.2205403928e-02,2.6484727502e-02,9.3586400033e-02 +601,2.06826221,4.95468083,-0.07224299,2.20369362,2.6501952591e-02,3.2188524295e-02,2.5873841246e-02,9.3203294688e-02 +602,2.06897142,4.95820408,-0.07296994,2.20736033,2.6408209459e-02,3.2173807202e-02,2.5265010826e-02,9.2828268123e-02 +603,2.06968063,4.96172734,-0.07369689,2.21102704,2.6316695019e-02,3.2161245795e-02,2.4658168672e-02,9.2461274835e-02 +604,2.07038983,4.96525060,-0.07442384,2.21469375,2.6227402404e-02,3.2150833530e-02,2.4053252536e-02,9.2102272665e-02 +605,2.07109904,4.96877385,-0.07515079,2.21836046,2.6140324824e-02,3.2142564018e-02,2.3450199441e-02,9.1751219052e-02 +606,2.07180825,4.97229711,-0.07587775,2.22202718,2.6055455133e-02,3.2136431002e-02,2.2848942481e-02,9.1408069205e-02 +607,2.07251746,4.97582037,-0.07660470,2.22569389,2.5972787195e-02,3.2132428482e-02,2.2249420408e-02,9.1072781689e-02 +608,2.07322666,4.97934363,-0.07733165,2.22936060,2.5892314783e-02,3.2130550610e-02,2.1651569897e-02,9.0745313901e-02 +609,2.07393587,4.98286688,-0.07805860,2.23302731,2.5814032032e-02,3.2130791721e-02,2.1055328820e-02,9.0425623965e-02 +610,2.07464508,4.98639014,-0.07878555,2.23669402,2.5737932816e-02,3.2133146337e-02,2.0460631489e-02,9.0113668133e-02 +611,2.07535429,4.98991340,-0.07951250,2.24036073,2.5664011820e-02,3.2137609162e-02,1.9867416816e-02,8.9809405188e-02 +612,2.07606350,4.99343665,-0.08023946,2.24402744,2.5592263905e-02,3.2144175066e-02,1.9275623578e-02,8.9512793820e-02 +613,2.07677270,4.99695991,-0.08096641,2.24769416,2.5522683738e-02,3.2152839128e-02,1.8685187431e-02,8.9223791161e-02 +614,2.07748191,5.00048317,-0.08169336,2.25136087,2.5455266577e-02,3.2163596591e-02,1.8096047003e-02,8.8942355931e-02 +615,2.07819112,5.00400642,-0.08242031,2.25502758,2.5390008028e-02,3.2176442834e-02,1.7508142389e-02,8.8668447469e-02 +616,2.07890033,5.00752968,-0.08314726,2.25869429,2.5326903377e-02,3.2191373519e-02,1.6921409018e-02,8.8402022948e-02 +617,2.07960953,5.01105294,-0.08387422,2.26236100,2.5265948662e-02,3.2208384390e-02,1.6335787178e-02,8.8143041795e-02 +618,2.08031874,5.01457619,-0.08460117,2.26602771,2.5207140124e-02,3.2227471341e-02,1.5751217537e-02,8.7891463544e-02 +619,2.08102795,5.01809945,-0.08532812,2.26969443,2.5150473772e-02,3.2248630596e-02,1.5167636401e-02,8.7647245765e-02 +620,2.08173716,5.02162271,-0.08605507,2.27336114,2.5095946312e-02,3.2271858404e-02,1.4584985067e-02,8.7410348135e-02 +621,2.08244637,5.02514596,-0.08678202,2.27702785,2.5043554655e-02,3.2297151168e-02,1.4003205238e-02,8.7180730330e-02 +622,2.08315557,5.02866922,-0.08750897,2.28069456,2.4993295553e-02,3.2324505647e-02,1.3422234828e-02,8.6958350383e-02 +623,2.08386478,5.03219248,-0.08823593,2.28436127,2.4945166307e-02,3.2353918640e-02,1.2842015663e-02,8.6743167792e-02 +624,2.08457399,5.03571573,-0.08896288,2.28802798,2.4899164564e-02,3.2385387000e-02,1.2262492131e-02,8.6535142691e-02 +625,2.08528320,5.03923899,-0.08968983,2.29169469,2.4855287746e-02,3.2418908063e-02,1.1683603602e-02,8.6334233141e-02 +626,2.08599240,5.04276225,-0.09041678,2.29536141,2.4813533910e-02,3.2454479056e-02,1.1105295644e-02,8.6140399084e-02 +627,2.08670161,5.04628551,-0.09114373,2.29902812,2.4773901154e-02,3.2492097514e-02,1.0527512703e-02,8.5953599681e-02 +628,2.08741082,5.04980876,-0.09187068,2.30269483,2.4736387965e-02,3.2531760976e-02,9.9502032549e-03,8.5773794802e-02 +629,2.08812003,5.05333202,-0.09259764,2.30636154,2.4700992722e-02,3.2573467505e-02,9.3733127410e-03,8.5600942714e-02 +630,2.08882924,5.05685528,-0.09332459,2.31002825,2.4667714380e-02,3.2617214950e-02,8.7967949388e-03,8.5435003399e-02 +631,2.08953844,5.06037853,-0.09405154,2.31369496,2.4636552064e-02,3.2663001354e-02,8.2206066888e-03,8.5275936486e-02 +632,2.09024765,5.06390179,-0.09477849,2.31736168,2.4607504853e-02,3.2710825337e-02,7.6447049452e-03,8.5123700230e-02 +633,2.09095686,5.06742505,-0.09550544,2.32102839,2.4580572348e-02,3.2760685255e-02,7.0690593636e-03,8.4978254285e-02 +634,2.09166607,5.07094830,-0.09623239,2.32469510,2.4555754341e-02,3.2812579634e-02,6.4936498217e-03,8.4839558101e-02 +635,2.09237528,5.07447156,-0.09695935,2.32836181,2.4533050644e-02,3.2866507603e-02,5.9184669853e-03,8.4707569875e-02 +636,2.09308448,5.07799482,-0.09768630,2.33202852,2.4512461535e-02,3.2922467927e-02,5.3435314236e-03,8.4582249099e-02 +637,2.09379369,5.08151807,-0.09841325,2.33569523,2.4493987438e-02,3.2980459699e-02,4.7689003673e-03,8.4463554716e-02 +638,2.09450290,5.08504133,-0.09914020,2.33936194,2.4477628901e-02,3.3040482510e-02,4.1946928041e-03,8.4351444824e-02 +639,2.09521211,5.08856459,-0.09986715,2.34302866,2.4463386837e-02,3.3102535691e-02,3.6211484570e-03,8.4245878292e-02 +640,2.09592131,5.09208784,-0.10059411,2.34669537,2.4451262370e-02,3.3166618719e-02,3.0487328569e-03,8.4146813726e-02 +641,2.09663052,5.09561110,-0.10132106,2.35036208,2.4441256743e-02,3.3232731728e-02,2.4783927850e-03,8.4054208908e-02 +642,2.09733973,5.09913436,-0.10204801,2.35402879,2.4433371542e-02,3.3300874523e-02,1.9122826708e-03,8.3968022116e-02 +643,2.09804894,5.10265761,-0.10277496,2.35769550,2.4427608552e-02,3.3371047045e-02,1.3562335350e-03,8.3888211401e-02 +644,2.09875815,5.10618087,-0.10350191,2.36136221,2.4423969742e-02,3.3443249949e-02,8.3171821959e-04,8.3814733977e-02 +645,2.09946735,5.10970413,-0.10422886,2.36502892,2.4422457361e-02,3.3517483462e-02,4.6473601139e-04,8.3747547479e-02 +646,2.10017656,5.11322738,-0.10495582,2.36869564,2.4423073869e-02,3.3593748114e-02,6.4368009330e-04,8.3686609163e-02 +647,2.10088577,5.11675064,-0.10568277,2.37236235,2.4425821948e-02,3.3672044947e-02,1.1355033860e-03,8.3631875709e-02 +648,2.10159498,5.12027390,-0.10640972,2.37602906,2.4430704522e-02,3.3752374734e-02,1.6864113870e-03,8.3583303862e-02 +649,2.10230418,5.12379716,-0.10713667,2.37969577,2.4437724736e-02,3.3834738517e-02,2.2541823562e-03,8.3540849939e-02 +650,2.10301339,5.12732041,-0.10786362,2.38336248,2.4446885950e-02,3.3919137407e-02,2.8292920156e-03,8.3504470059e-02 +651,2.10372260,5.13084367,-0.10859057,2.38702919,2.4458191817e-02,3.4005573206e-02,3.4085820561e-03,8.3474119699e-02 +652,2.10443181,5.13436693,-0.10931753,2.39069591,2.4471646171e-02,3.4094047308e-02,3.9907416784e-03,8.3449754267e-02 +653,2.10514102,5.13789018,-0.11004448,2.39436262,2.4487253020e-02,3.4184561094e-02,4.5751516940e-03,8.3431328825e-02 +654,2.10585022,5.14141344,-0.11077143,2.39802933,2.4505016753e-02,3.4277116916e-02,5.1615037500e-03,8.3418797782e-02 +655,2.10655943,5.14493670,-0.11149838,2.40169604,2.4524941889e-02,3.4371716675e-02,5.7496372543e-03,8.3412115408e-02 +656,2.10726864,5.14845995,-0.11222533,2.40536275,2.4547033135e-02,3.4468362259e-02,6.3394729803e-03,8.3411235409e-02 +657,2.10797785,5.15198321,-0.11295229,2.40902946,2.4571295632e-02,3.4567056581e-02,6.9309847236e-03,8.3416111042e-02 +658,2.10868705,5.15550647,-0.11367924,2.41269617,2.4597734517e-02,3.4667801733e-02,7.5241697135e-03,8.3426695119e-02 +659,2.10939626,5.15902972,-0.11440619,2.41636289,2.4626355239e-02,3.4770600272e-02,8.1190457948e-03,8.3442939972e-02 +660,2.11010547,5.16255298,-0.11513314,2.42002960,2.4657163649e-02,3.4875455503e-02,8.7156468561e-03,8.3464797444e-02 +661,2.11081468,5.16607624,-0.11586009,2.42369631,2.4690165627e-02,3.4982370139e-02,9.3140110686e-03,8.3492218769e-02 +662,2.11152389,5.16959949,-0.11658704,2.42736302,2.4725367271e-02,3.5091347027e-02,9.9141827521e-03,8.3525154560e-02 +663,2.11223309,5.17312275,-0.11731400,2.43102973,2.4762775199e-02,3.5202390031e-02,1.0516215474e-02,8.3563555047e-02 +664,2.11294230,5.17664601,-0.11804095,2.43469644,2.4802395980e-02,3.5315502313e-02,1.1120161423e-02,8.3607369778e-02 +665,2.11365151,5.18016926,-0.11876790,2.43836316,2.4844236419e-02,3.5430687176e-02,1.1726075384e-02,8.3656547446e-02 +666,2.11436072,5.18369252,-0.11949485,2.44202987,2.4888303885e-02,3.5547948983e-02,1.2334018943e-02,8.3711036523e-02 +667,2.11506993,5.18721578,-0.12022180,2.44569658,2.4934605664e-02,3.5667291353e-02,1.2944050546e-02,8.3770784430e-02 +668,2.11577913,5.19073904,-0.12094875,2.44936329,2.4983149368e-02,3.5788718294e-02,1.3556231247e-02,8.3835737922e-02 +669,2.11648834,5.19426229,-0.12167571,2.45303000,2.5033942826e-02,3.5912233910e-02,1.4170622874e-02,8.3905843066e-02 +670,2.11719755,5.19778555,-0.12240266,2.45669671,2.5086994338e-02,3.6037843033e-02,1.4787291122e-02,8.3981045455e-02 +671,2.11790676,5.20130881,-0.12312961,2.46036342,2.5142312180e-02,3.6165549953e-02,1.5406298975e-02,8.4061289467e-02 +672,2.11861596,5.20483206,-0.12385656,2.46403014,2.5199904787e-02,3.6295358957e-02,1.6027709485e-02,8.4146518505e-02 +673,2.11932517,5.20835532,-0.12458351,2.46769685,2.5259781386e-02,3.6427275721e-02,1.6651592215e-02,8.4236676054e-02 +674,2.12003438,5.21187858,-0.12531047,2.47136356,2.5321950957e-02,3.6561304924e-02,1.7278011828e-02,8.4331703833e-02 +675,2.12074359,5.21540183,-0.12603742,2.47503027,2.5386422619e-02,3.6697451230e-02,1.7907032833e-02,8.4431542609e-02 +676,2.12145280,5.21892509,-0.12676437,2.47869698,2.5453206365e-02,3.6835720751e-02,1.8538726233e-02,8.4536133064e-02 +677,2.12216200,5.22244835,-0.12749132,2.48236369,2.5522311865e-02,3.6976118541e-02,1.9173157923e-02,8.4645414014e-02 +678,2.12287121,5.22597160,-0.12821827,2.48603040,2.5593749127e-02,3.7118650015e-02,1.9810395242e-02,8.4759323526e-02 +679,2.12358042,5.22949486,-0.12894522,2.48969712,2.5667528869e-02,3.7263321642e-02,2.0450510038e-02,8.4877799219e-02 +680,2.12428963,5.23301812,-0.12967218,2.49336383,2.5743661593e-02,3.7410139088e-02,2.1093570284e-02,8.5000776895e-02 +681,2.12499883,5.23654137,-0.13039913,2.49703054,2.5822157853e-02,3.7559107831e-02,2.1739643001e-02,8.5128190978e-02 +682,2.12570804,5.24006463,-0.13112608,2.50069725,2.5903029338e-02,3.7710235160e-02,2.2388802791e-02,8.5259976201e-02 +683,2.12641725,5.24358789,-0.13185303,2.50436396,2.5986287223e-02,3.7863527039e-02,2.3041118276e-02,8.5396064755e-02 +684,2.12712646,5.24711114,-0.13257998,2.50803067,2.6071942958e-02,3.8018989643e-02,2.3696658829e-02,8.5536387684e-02 +685,2.12783567,5.25063440,-0.13330693,2.51169739,2.6160008930e-02,3.8176630530e-02,2.4355499430e-02,8.5680875874e-02 +686,2.12854487,5.25415766,-0.13403389,2.51536410,2.6250497134e-02,3.8336456203e-02,2.5017710376e-02,8.5829457750e-02 +687,2.12925408,5.25768092,-0.13476084,2.51903081,2.6343420023e-02,3.8498473720e-02,2.5683364075e-02,8.5982061015e-02 +688,2.12996329,5.26120417,-0.13548779,2.52269752,2.6438790094e-02,3.8662689872e-02,2.6352531689e-02,8.6138611335e-02 +689,2.13067250,5.26472743,-0.13621474,2.52636423,2.6536620853e-02,3.8829112900e-02,2.7025290090e-02,8.6299034273e-02 +690,2.13138170,5.26825069,-0.13694169,2.53003094,2.6636925282e-02,3.8997749826e-02,2.7701711008e-02,8.6463252575e-02 +691,2.13209091,5.27177394,-0.13766865,2.53369765,2.6739716829e-02,3.9168608177e-02,2.8381868027e-02,8.6631187927e-02 +692,2.13280012,5.27529720,-0.13839560,2.53736437,2.6845009681e-02,3.9341696432e-02,2.9065838404e-02,8.6802761203e-02 +693,2.13350933,5.27882046,-0.13912255,2.54103108,2.6952817738e-02,3.9517022261e-02,2.9753695996e-02,8.6977890680e-02 +694,2.13421854,5.28234371,-0.13984950,2.54469779,2.7063155089e-02,3.9694593381e-02,3.0445514740e-02,8.7156492907e-02 +695,2.13492774,5.28586697,-0.14057645,2.54836450,2.7176036910e-02,3.9874418968e-02,3.1141374158e-02,8.7338483993e-02 +696,2.13563695,5.28939023,-0.14130340,2.55203121,2.7291477976e-02,4.0056507262e-02,3.1841349925e-02,8.7523777388e-02 +697,2.13634616,5.29291348,-0.14203036,2.55569792,2.7409493211e-02,4.0240866452e-02,3.2545517492e-02,8.7712284324e-02 +698,2.13705537,5.29643674,-0.14275731,2.55936463,2.7530098601e-02,4.0427506133e-02,3.3253957582e-02,8.7903915647e-02 +699,2.13776457,5.29996000,-0.14348426,2.56303135,2.7653309642e-02,4.0616434818e-02,3.3966746698e-02,8.8098578860e-02 +700,2.13847378,5.30348325,-0.14421121,2.56669806,2.7779142213e-02,4.0807661367e-02,3.4683962562e-02,8.8296179936e-02 +701,2.13918299,5.30700651,-0.14493816,2.57036477,2.7907613284e-02,4.1001196003e-02,3.5405687984e-02,8.8496623901e-02 +702,2.13989220,5.31052977,-0.14566511,2.57403148,2.8038739199e-02,4.1197047716e-02,3.6132001064e-02,8.8699812357e-02 +703,2.14060141,5.31405302,-0.14639207,2.57769819,2.8172536646e-02,4.1395225746e-02,3.6862980823e-02,8.8905644834e-02 +704,2.14131061,5.31757628,-0.14711902,2.58136490,2.8309023354e-02,4.1595740623e-02,3.7598711024e-02,8.9114019936e-02 +705,2.14201982,5.32109954,-0.14784597,2.58503162,2.8448216755e-02,4.1798602140e-02,3.8339272623e-02,8.9324833160e-02 +706,2.14272903,5.32462280,-0.14857292,2.58869833,2.8590134637e-02,4.2003820341e-02,3.9084747532e-02,8.9537977651e-02 +707,2.14343824,5.32814605,-0.14929987,2.59236504,2.8734794783e-02,4.2211404998e-02,3.9835216678e-02,8.9753343817e-02 +708,2.14414745,5.33166931,-0.15002683,2.59603175,2.8882216503e-02,4.2421367798e-02,4.0590767921e-02,8.9970821396e-02 +709,2.14485665,5.33519257,-0.15075378,2.59969846,2.9032417945e-02,4.2633718513e-02,4.1351482211e-02,9.0190295584e-02 +710,2.14556586,5.33871582,-0.15148073,2.60336517,2.9185418034e-02,4.2848467742e-02,4.2117443510e-02,9.0411649768e-02 +711,2.14627507,5.34223908,-0.15220768,2.60703188,2.9341236889e-02,4.3065627490e-02,4.2888740849e-02,9.0634765813e-02 +712,2.14698428,5.34576234,-0.15293463,2.61069860,2.9499893826e-02,4.3285208360e-02,4.3665458296e-02,9.0859521357e-02 +713,2.14769348,5.34928559,-0.15366158,2.61436531,2.9661408365e-02,4.3507220982e-02,4.4447680117e-02,9.1085791057e-02 +714,2.14840269,5.35280885,-0.15438854,2.61803202,2.9825801962e-02,4.3731678386e-02,4.5235499068e-02,9.1313449161e-02 +715,2.14911190,5.35633211,-0.15511549,2.62169873,2.9993094594e-02,4.3958591308e-02,4.6028999931e-02,9.1542364453e-02 +716,2.14982111,5.35985536,-0.15584244,2.62536544,3.0163306903e-02,4.4187971117e-02,4.6828269823e-02,9.1772403098e-02 +717,2.15053032,5.36337862,-0.15656939,2.62903215,3.0336461100e-02,4.4419831037e-02,4.7633402431e-02,9.2003429923e-02 +718,2.15123952,5.36690188,-0.15729634,2.63269887,3.0512578306e-02,4.4654182543e-02,4.8444485462e-02,9.2235304594e-02 +719,2.15194873,5.37042513,-0.15802329,2.63636558,3.0691680213e-02,4.4891037637e-02,4.9261608561e-02,9.2467884153e-02 +720,2.15265794,5.37394839,-0.15875025,2.64003229,3.0873790093e-02,4.5130410113e-02,5.0084867729e-02,9.2701023571e-02 +721,2.15336715,5.37747165,-0.15947720,2.64369900,3.1058929927e-02,4.5372311819e-02,5.0914352374e-02,9.2934572618e-02 +722,2.15407635,5.38099490,-0.16020415,2.64736571,3.1247122224e-02,4.5616755014e-02,5.1750153515e-02,9.3168377514e-02 +723,2.15478556,5.38451816,-0.16093110,2.65103242,3.1438391502e-02,4.5863754319e-02,5.2592370390e-02,9.3402283294e-02 +724,2.15549477,5.38804142,-0.16165805,2.65469913,3.1632760863e-02,4.6113322227e-02,5.3441095153e-02,9.3636128829e-02 +725,2.15620398,5.39156468,-0.16238500,2.65836585,3.1830254344e-02,4.6365472178e-02,5.4296423378e-02,9.3869750019e-02 +726,2.15691319,5.39508793,-0.16311196,2.66203256,3.2030895921e-02,4.6620217260e-02,5.5158449657e-02,9.4102978316e-02 +727,2.15762239,5.39861119,-0.16383891,2.66569927,3.2234711047e-02,4.6877572193e-02,5.6027274314e-02,9.4335642624e-02 +728,2.15833160,5.40213445,-0.16456586,2.66936598,3.2441724628e-02,4.7137550709e-02,5.6902994530e-02,9.4567566743e-02 +729,2.15904081,5.40565770,-0.16529281,2.67303269,3.2651961469e-02,4.7400166148e-02,5.7785706427e-02,9.4798569554e-02 +730,2.15975002,5.40918096,-0.16601976,2.67669940,3.2865448495e-02,4.7665434235e-02,5.8675514391e-02,9.5028467641e-02 +731,2.16045922,5.41270422,-0.16674672,2.68036611,3.3082211368e-02,4.7933368823e-02,5.9572516777e-02,9.5257071163e-02 +732,2.16116843,5.41622747,-0.16747367,2.68403283,3.3302276047e-02,4.8203983918e-02,6.0476812689e-02,9.5484186301e-02 +733,2.16187764,5.41975073,-0.16820062,2.68769954,3.3525670633e-02,4.8477295846e-02,6.1388509314e-02,9.5709615777e-02 +734,2.16258685,5.42327399,-0.16892757,2.69136625,3.3752421873e-02,4.8753318999e-02,6.2307707637e-02,9.5933155930e-02 +735,2.16329606,5.42679724,-0.16965452,2.69503296,3.3982556856e-02,4.9032067927e-02,6.3234509492e-02,9.6154598200e-02 +736,2.16400526,5.43032050,-0.17038147,2.69869967,3.4216104746e-02,4.9313559413e-02,6.4169024456e-02,9.6373730469e-02 +737,2.16471447,5.43384376,-0.17110843,2.70236638,3.4453093793e-02,4.9597808853e-02,6.5111357783e-02,9.6590334421e-02 +738,2.16542368,5.43736701,-0.17183538,2.70603310,3.4693551829e-02,4.9884830890e-02,6.6061612595e-02,9.6804185685e-02 +739,2.16613289,5.44089027,-0.17256233,2.70969981,3.4937509503e-02,5.0174643230e-02,6.7019902512e-02,9.7015056402e-02 +740,2.16684209,5.44441353,-0.17328928,2.71336652,3.5184995714e-02,5.0467261210e-02,6.7986333669e-02,9.7222711175e-02 +741,2.16755130,5.44793678,-0.17401623,2.71703323,3.5436040111e-02,5.0762700806e-02,6.8961014660e-02,9.7426909591e-02 +742,2.16826051,5.45146004,-0.17474318,2.72069994,3.5690674342e-02,5.1060980044e-02,6.9944061253e-02,9.7627406224e-02 +743,2.16896972,5.45498330,-0.17547014,2.72436665,3.5948928775e-02,5.1362115149e-02,7.0935583663e-02,9.7823948225e-02 +744,2.16967893,5.45850655,-0.17619709,2.72803336,3.6210833706e-02,5.1666122026e-02,7.1935691442e-02,9.8016276568e-02 +745,2.17038813,5.46202981,-0.17692404,2.73170008,3.6476422215e-02,5.1973019491e-02,7.2944504181e-02,9.8204127112e-02 +746,2.17109734,5.46555307,-0.17765099,2.73536679,3.6745726019e-02,5.2282824514e-02,7.3962135767e-02,9.8387228354e-02 +747,2.17180655,5.46907633,-0.17837794,2.73903350,3.7018777530e-02,5.2595554561e-02,7.4988702207e-02,9.8565301876e-02 +748,2.17251576,5.47259958,-0.17910490,2.74270021,3.7295609156e-02,5.2911226809e-02,7.6024319006e-02,9.8738062126e-02 +749,2.17322497,5.47612284,-0.17983185,2.74636692,3.7576255437e-02,5.3229860607e-02,7.7069109248e-02,9.8905217877e-02 +750,2.17393417,5.47964610,-0.18055880,2.75003363,3.7860749512e-02,5.3551473374e-02,7.8123190187e-02,9.9066468983e-02 +751,2.17464338,5.48316935,-0.18128575,2.75370035,3.8149125208e-02,5.3876083053e-02,7.9186681254e-02,9.9221508604e-02 +752,2.17535259,5.48669261,-0.18201270,2.75736706,3.8441418623e-02,5.4203709830e-02,8.0259709768e-02,9.9370022749e-02 +753,2.17606180,5.49021587,-0.18273965,2.76103377,3.8737664196e-02,5.4534371727e-02,8.1342396463e-02,9.9511688573e-02 +754,2.17677100,5.49373912,-0.18346661,2.76470048,3.9037896795e-02,5.4868086946e-02,8.2434863208e-02,9.9646174947e-02 +755,2.17748021,5.49726238,-0.18419356,2.76836719,3.9342154060e-02,5.5204876523e-02,8.3537241650e-02,9.9773144331e-02 +756,2.17818942,5.50078564,-0.18492051,2.77203390,3.9650471794e-02,5.5544759093e-02,8.4649656177e-02,9.9892248781e-02 +757,2.17889863,5.50430889,-0.18564746,2.77570061,3.9962886210e-02,5.5887753471e-02,8.5772232325e-02,1.0000313197e-01 +758,2.17960784,5.50783215,-0.18637441,2.77936733,4.0279436450e-02,5.6233881388e-02,8.6905105755e-02,1.0010542946e-01 +759,2.18031704,5.51135541,-0.18710136,2.78303404,4.0600159523e-02,5.6583161911e-02,8.8048404021e-02,1.0019876650e-01 +760,2.18102625,5.51487866,-0.18782832,2.78670075,4.0925093564e-02,5.6935615051e-02,8.9202258346e-02,1.0028275953e-01 +761,2.18173546,5.51840192,-0.18855527,2.79036746,4.1254278694e-02,5.7291262675e-02,9.0366806674e-02,1.0035701555e-01 +762,2.18244467,5.52192518,-0.18928222,2.79403417,4.1587753704e-02,5.7650124864e-02,9.1542181749e-02,1.0042113086e-01 +763,2.18315387,5.52544843,-0.19000917,2.79770088,4.1925557478e-02,5.8012221507e-02,9.2728516330e-02,1.0047469152e-01 +764,2.18386308,5.52897169,-0.19073612,2.80136759,4.2267732586e-02,5.8377576147e-02,9.3925955801e-02,1.0051727406e-01 +765,2.18457229,5.53249495,-0.19146308,2.80503431,4.2614319137e-02,5.8746209331e-02,9.5134636444e-02,1.0054844304e-01 +766,2.18528150,5.53601821,-0.19219003,2.80870102,4.2965358209e-02,5.9118142373e-02,9.6354697720e-02,1.0056775269e-01 +767,2.18599071,5.53954146,-0.19291698,2.81236773,4.3320891057e-02,5.9493396426e-02,9.7586279289e-02,1.0057474543e-01 +768,2.18669991,5.54306472,-0.19364393,2.81603444,4.3680961869e-02,5.9871995422e-02,9.8829530666e-02,1.0056895196e-01 +769,2.18740912,5.54658798,-0.19437088,2.81970115,4.4045613144e-02,6.0253961195e-02,1.0008459519e-01,1.0054989145e-01 +770,2.18811833,5.55011123,-0.19509783,2.82336786,4.4414887306e-02,6.0639315166e-02,1.0135161564e-01,1.0051707028e-01 +771,2.18882754,5.55363449,-0.19582479,2.82703458,4.4788830626e-02,6.1028082437e-02,1.0263074764e-01,1.0046998184e-01 +772,2.18953674,5.55715775,-0.19655174,2.83070129,4.5167486190e-02,6.1420284482e-02,1.0392213568e-01,1.0040810717e-01 +773,2.19024595,5.56068100,-0.19727869,2.83436800,4.5550898818e-02,6.1815944241e-02,1.0522592988e-01,1.0033091397e-01 +774,2.19095516,5.56420426,-0.19800564,2.83803471,4.5939116101e-02,6.2215087164e-02,1.0654228944e-01,1.0023785524e-01 +775,2.19166437,5.56772752,-0.19873259,2.84170142,4.6332183388e-02,6.2617736052e-02,1.0787136569e-01,1.0012837056e-01 +776,2.19237358,5.57125077,-0.19945954,2.84536813,4.6730146548e-02,6.3023913911e-02,1.0921331147e-01,1.0000188518e-01 +777,2.19308278,5.57477403,-0.20018650,2.84903484,4.7133055176e-02,6.3433647190e-02,1.1056829187e-01,9.9857807937e-02 +778,2.19379199,5.57829729,-0.20091345,2.85270156,4.7540956272e-02,6.3846959397e-02,1.1193646312e-01,9.9695534568e-02 +779,2.19450120,5.58182054,-0.20164040,2.85636827,4.7953897434e-02,6.4263874253e-02,1.1331798310e-01,9.9514443955e-02 +780,2.19521041,5.58534380,-0.20236735,2.86003498,4.8371930082e-02,6.4684419008e-02,1.1471302228e-01,9.9313897708e-02 +781,2.19591961,5.58886706,-0.20309430,2.86370169,4.8795102782e-02,6.5108617667e-02,1.1612174132e-01,9.9093242054e-02 +782,2.19662882,5.59239031,-0.20382126,2.86736840,4.9223465563e-02,6.5536495369e-02,1.1754430558e-01,9.8851806291e-02 +783,2.19733803,5.59591357,-0.20454821,2.87103511,4.9657071044e-02,6.5968079496e-02,1.1898088888e-01,9.8588900822e-02 +784,2.19804724,5.59943683,-0.20527516,2.87470182,5.0095970060e-02,6.6403395253e-02,1.2043165877e-01,9.8303818905e-02 +785,2.19875645,5.60296009,-0.20600211,2.87836854,5.0540214947e-02,6.6842468989e-02,1.2189678764e-01,9.7995834885e-02 +786,2.19946565,5.60648334,-0.20672906,2.88203525,5.0989857257e-02,6.7285325912e-02,1.2337644510e-01,9.7664204769e-02 +787,2.20017486,5.61000660,-0.20745601,2.88570196,5.1444953425e-02,6.7731995695e-02,1.2487081669e-01,9.7308162440e-02 +788,2.20088407,5.61352986,-0.20818297,2.88936867,5.1905556575e-02,6.8182504330e-02,1.2638007678e-01,9.6926923391e-02 +789,2.20159328,5.61705311,-0.20890992,2.89303538,5.2371719878e-02,6.8636877549e-02,1.2790439989e-01,9.6519683952e-02 +790,2.20230249,5.62057637,-0.20963687,2.89670209,5.2843501617e-02,6.9095145652e-02,1.2944397700e-01,9.6085614429e-02 +791,2.20301169,5.62409963,-0.21036382,2.90036881,5.3320956585e-02,6.9557335156e-02,1.3099898754e-01,9.5623866641e-02 +792,2.20372090,5.62762288,-0.21109077,2.90403552,5.3804140947e-02,7.0023473560e-02,1.3256961534e-01,9.5133570097e-02 +793,2.20443011,5.63114614,-0.21181772,2.90770223,5.4293114605e-02,7.0493591561e-02,1.3415605620e-01,9.4613826922e-02 +794,2.20513932,5.63466940,-0.21254468,2.91136894,5.4787934902e-02,7.0967716981e-02,1.3575849742e-01,9.4063718500e-02 +795,2.20584852,5.63819265,-0.21327163,2.91503565,5.5288658716e-02,7.1445876853e-02,1.3737712485e-01,9.3482303503e-02 +796,2.20655773,5.64171591,-0.21399858,2.91870236,5.5795349201e-02,7.1928103796e-02,1.3901214445e-01,9.2868608810e-02 +797,2.20726694,5.64523917,-0.21472553,2.92236907,5.6308065242e-02,7.2414425937e-02,1.4066374824e-01,9.2221640205e-02 +798,2.20797615,5.64876242,-0.21545248,2.92603579,5.6826866652e-02,7.2904871900e-02,1.4233213119e-01,9.1540376837e-02 +799,2.20868536,5.65228568,-0.21617944,2.92970250,5.7351818356e-02,7.3399474728e-02,1.4401750462e-01,9.0823763985e-02 +800,2.20939456,5.65580894,-0.21690639,2.93336921,5.7882980784e-02,7.3898262836e-02,1.4572006539e-01,9.0070725772e-02 +801,2.21010377,5.65933219,-0.21763334,2.93703592,5.8420416782e-02,7.4401266495e-02,1.4744001804e-01,8.9280155019e-02 +802,2.21081298,5.66285545,-0.21836029,2.94070263,5.8964193053e-02,7.4908519160e-02,1.4917757938e-01,8.8450908830e-02 +803,2.21152219,5.66637871,-0.21908724,2.94436934,5.9514373138e-02,7.5420050902e-02,1.5093295603e-01,8.7581819193e-02 +804,2.21223139,5.66990197,-0.21981419,2.94803606,6.0071022459e-02,7.5935893130e-02,1.5270636061e-01,8.6671684052e-02 +805,2.21294060,5.67342522,-0.22054115,2.95170277,6.0634206942e-02,7.6456077302e-02,1.5449800736e-01,8.5719269520e-02 +806,2.21364981,5.67694848,-0.22126810,2.95536948,6.1203995909e-02,7.6980637602e-02,1.5630812135e-01,8.4723302942e-02 +807,2.21435902,5.68047174,-0.22199505,2.95903619,6.1780456318e-02,7.7509605554e-02,1.5813692002e-01,8.3682481328e-02 +808,2.21506823,5.68399499,-0.22272200,2.96270290,6.2363655557e-02,7.8043012670e-02,1.5998462225e-01,8.2595466752e-02 +809,2.21577743,5.68751825,-0.22344895,2.96636961,6.2953666114e-02,7.8580894677e-02,1.6185146307e-01,8.1460875570e-02 +810,2.21648664,5.69104151,-0.22417590,2.97003632,6.3550557674e-02,7.9123284308e-02,1.6373766872e-01,8.0277293470e-02 +811,2.21719585,5.69456476,-0.22490286,2.97370304,6.4154400110e-02,7.9670213992e-02,1.6564346595e-01,7.9043266622e-02 +812,2.21790506,5.69808802,-0.22562981,2.97736975,6.4765268184e-02,8.0221720185e-02,1.6756909720e-01,7.7757293403e-02 +813,2.21861426,5.70161128,-0.22635676,2.98103646,6.5383233483e-02,8.0777835956e-02,1.6951479474e-01,7.6417836959e-02 +814,2.21932347,5.70513453,-0.22708371,2.98470317,6.6008369034e-02,8.1338595262e-02,1.7148079560e-01,7.5023317604e-02 +815,2.22003268,5.70865779,-0.22781066,2.98836988,6.6640752694e-02,8.1904035899e-02,1.7346735200e-01,7.3572100712e-02 +816,2.22074189,5.71218105,-0.22853761,2.99203659,6.7280458640e-02,8.2474191893e-02,1.7547470471e-01,7.2062516042e-02 +817,2.22145110,5.71570430,-0.22926457,2.99570330,6.7927562247e-02,8.3049097866e-02,1.7750309830e-01,7.0492845032e-02 +818,2.22216030,5.71922756,-0.22999152,2.99937002,6.8582143454e-02,8.3628792058e-02,1.7955279193e-01,6.8861311711e-02 +819,2.22286951,5.72275082,-0.23071847,3.00303673,6.9244280188e-02,8.4213310403e-02,1.8162403847e-01,6.7166096633e-02 +820,2.22357872,5.72627407,-0.23144542,3.00670344,6.9914049840e-02,8.4802687880e-02,1.8371708924e-01,6.5405331175e-02 +821,2.22428793,5.72979733,-0.23217237,3.01037015,7.0591536365e-02,8.5396964763e-02,1.8583221626e-01,6.3577078343e-02 +822,2.22499714,5.73332059,-0.23289933,3.01403686,7.1276819820e-02,8.5996177371e-02,1.8796967946e-01,6.1679357676e-02 +823,2.22570634,5.73684384,-0.23362628,3.01770357,7.1969979630e-02,8.6600361031e-02,1.9012973713e-01,5.9710136874e-02 +824,2.22641555,5.74036710,-0.23435323,3.02137029,7.2671103908e-02,8.7209558119e-02,1.9231267475e-01,5.7667302568e-02 +825,2.22712476,5.74389036,-0.23508018,3.02503700,7.3380274860e-02,8.7823805329e-02,1.9451875955e-01,5.5548698585e-02 +826,2.22783397,5.74741362,-0.23580713,3.02870371,7.4097577869e-02,8.8443141616e-02,1.9674826890e-01,5.3352099783e-02 +827,2.22854317,5.75093687,-0.23653408,3.03237042,7.4823097046e-02,8.9067604316e-02,1.9900147642e-01,5.1075222412e-02 +828,2.22925238,5.75446013,-0.23726104,3.03603713,7.5556924808e-02,8.9697237397e-02,2.0127868180e-01,4.8715693231e-02 +829,2.22996159,5.75798339,-0.23798799,3.03970384,7.6299146555e-02,9.0332078271e-02,2.0358016325e-01,4.6271094377e-02 +830,2.23067080,5.76150664,-0.23871494,3.04337055,7.7049850271e-02,9.0972166014e-02,2.0590620716e-01,4.3738930851e-02 +831,2.23138001,5.76502990,-0.23944189,3.04703727,7.7809130511e-02,9.1617544777e-02,2.0825712061e-01,4.1116614832e-02 +832,2.23208921,5.76855316,-0.24016884,3.05070398,7.8577076627e-02,9.2268253713e-02,2.1063319482e-01,3.8401502195e-02 +833,2.23279842,5.77207641,-0.24089579,3.05437069,7.9353780203e-02,9.2924333358e-02,2.1303472831e-01,3.5590867470e-02 +834,2.23350763,5.77559967,-0.24162275,3.05803740,8.0139339272e-02,9.3585829095e-02,2.1546203974e-01,3.2681881201e-02 +835,2.23421684,5.77912293,-0.24234970,3.06170411,8.0933845992e-02,9.4252780836e-02,2.1791543015e-01,2.9671656260e-02 +836,2.23492604,5.78264618,-0.24307665,3.06537082,8.1737394464e-02,9.4925229518e-02,2.2039520669e-01,2.6557213417e-02 +837,2.23563525,5.78616944,-0.24380360,3.06903754,8.2550087100e-02,9.5603222507e-02,2.2290170275e-01,2.3335456626e-02 +838,2.23634446,5.78969270,-0.24453055,3.07270425,8.3372019920e-02,9.6286801200e-02,2.2543523224e-01,2.0003223237e-02 +839,2.23705367,5.79321595,-0.24525751,3.07637096,8.4203290924e-02,9.6976008067e-02,2.2799611554e-01,1.6557250825e-02 +840,2.23776288,5.79673921,-0.24598446,3.08003767,8.5044004452e-02,9.7670890299e-02,2.3058469321e-01,1.2994156266e-02 +841,2.23847208,5.80026247,-0.24671141,3.08370438,8.5894260489e-02,9.8371490810e-02,2.3320129257e-01,9.3104731419e-03 +842,2.23918129,5.80378572,-0.24743836,3.08737109,8.6754161043e-02,9.9077853641e-02,2.3584624779e-01,5.5026267002e-03 +843,2.23989050,5.80730898,-0.24816531,3.09103780,8.7623814785e-02,9.9790027665e-02,2.3851991393e-01,1.5669042460e-03 +844,2.24059971,5.81083224,-0.24889226,3.09470452,8.8503325359e-02,1.0050805701e-01,2.4122263105e-01,-2.5004942148e-03 +845,2.24130891,5.81435550,-0.24961922,3.09837123,8.9392799953e-02,1.0123198806e-01,2.4395475053e-01,-6.7034997414e-03 +846,2.24201812,5.81787875,-0.25034617,3.10203794,9.0292344610e-02,1.0196186574e-01,2.4671662109e-01,-1.1046151988e-02 +847,2.24272733,5.82140201,-0.25107312,3.10570465,9.1202073833e-02,1.0269774110e-01,2.4950861762e-01,-1.5532657613e-02 +848,2.24343654,5.82492527,-0.25180007,3.10937136,9.2122096384e-02,1.0343965992e-01,2.5233109804e-01,-2.0167321330e-02 +849,2.24414575,5.82844852,-0.25252702,3.11303807,9.3052522265e-02,1.0418766836e-01,2.5518442468e-01,-2.4954584489e-02 +850,2.24485495,5.83197178,-0.25325397,3.11670478,9.3993469653e-02,1.0494181848e-01,2.5806898547e-01,-2.9899067586e-02 +851,2.24556416,5.83549504,-0.25398093,3.12037150,9.4945052859e-02,1.0570215856e-01,2.6098515716e-01,-3.5005511469e-02 +852,2.24627337,5.83901829,-0.25470788,3.12403821,9.5907384387e-02,1.0646873482e-01,2.6393331168e-01,-4.0278793221e-02 +853,2.24698258,5.84254155,-0.25543483,3.12770492,9.6880587815e-02,1.0724160158e-01,2.6691385538e-01,-4.5724003542e-02 +854,2.24769178,5.84606481,-0.25616178,3.13137163,9.7864779563e-02,1.0802080678e-01,2.6992717338e-01,-5.1346351980e-02 +855,2.24840099,5.84958806,-0.25688873,3.13503834,9.8860078965e-02,1.0880640002e-01,2.7297366055e-01,-5.7151224900e-02 +856,2.24911020,5.85311132,-0.25761569,3.13870505,9.9866613652e-02,1.0959843670e-01,2.7605373766e-01,-6.3144230469e-02 +857,2.24981941,5.85663458,-0.25834264,3.14237177,1.0088450379e-01,1.1039696569e-01,2.7916780364e-01,-6.9331106891e-02 +858,2.25052862,5.86015783,-0.25906959,3.14603848,1.0191387261e-01,1.1120203750e-01,2.8231626737e-01,-7.5717794658e-02 +859,2.25123782,5.86368109,-0.25979654,3.14970519,1.0295485187e-01,1.1201370863e-01,2.8549956455e-01,-8.2310477854e-02 +860,2.25194703,5.86720435,-0.26052349,3.15337190,1.0400756847e-01,1.1283203103e-01,2.8871811704e-01,-8.9115506241e-02 +861,2.25265624,5.87072760,-0.26125044,3.15703861,1.0507214910e-01,1.1365705576e-01,2.9197234673e-01,-9.6139435021e-02 +862,2.25336545,5.87425086,-0.26197740,3.16070532,1.0614873158e-01,1.1448884176e-01,2.9526271031e-01,-1.0338910643e-01 +863,2.25407466,5.87777412,-0.26270435,3.16437203,1.0723744520e-01,1.1532744062e-01,2.9858963958e-01,-1.1087152638e-01 +864,2.25478386,5.88129738,-0.26343130,3.16803875,1.0833842487e-01,1.1617290749e-01,3.0195358405e-01,-1.1859397203e-01 +865,2.25549307,5.88482063,-0.26415825,3.17170546,1.0945180538e-01,1.1702529669e-01,3.0535499409e-01,-1.2656395615e-01 +866,2.25620228,5.88834389,-0.26488520,3.17537217,1.1057773021e-01,1.1788466838e-01,3.0879434741e-01,-1.3478930693e-01 +867,2.25691149,5.89186715,-0.26561215,3.17903888,1.1171633640e-01,1.1875107702e-01,3.1227210305e-01,-1.4327806595e-01 +868,2.25762069,5.89539040,-0.26633911,3.18270559,1.1286776261e-01,1.1962457747e-01,3.1578872587e-01,-1.5203855770e-01 +869,2.25832990,5.89891366,-0.26706606,3.18637230,1.1403215824e-01,1.2050523205e-01,3.1934471500e-01,-1.6107946531e-01 +870,2.25903911,5.90243692,-0.26779301,3.19003901,1.1520966551e-01,1.2139309670e-01,3.2294054825e-01,-1.7040971677e-01 +871,2.25974832,5.90596017,-0.26851996,3.19370573,1.1640042827e-01,1.2228822782e-01,3.2657670967e-01,-1.8003855535e-01 +872,2.26045753,5.90948343,-0.26924691,3.19737244,1.1760460156e-01,1.2319068939e-01,3.3025371860e-01,-1.8997563404e-01 +873,2.26116673,5.91300669,-0.26997387,3.20103915,1.1882233295e-01,1.2410053890e-01,3.3397207255e-01,-2.0023088040e-01 +874,2.26187594,5.91652994,-0.27070082,3.20470586,1.2005377169e-01,1.2501783435e-01,3.3773227577e-01,-2.1081457469e-01 +875,2.26258515,5.92005320,-0.27142777,3.20837257,1.2129907867e-01,1.2594264145e-01,3.4153486885e-01,-2.2173746504e-01 +876,2.26329436,5.92357646,-0.27215472,3.21203928,1.2255840703e-01,1.2687501929e-01,3.4538037006e-01,-2.3301060884e-01 +877,2.26400356,5.92709971,-0.27288167,3.21570600,1.2383191102e-01,1.2781502692e-01,3.4926930230e-01,-2.4464546585e-01 +878,2.26471277,5.93062297,-0.27360862,3.21937271,1.2511975894e-01,1.2876273290e-01,3.5320223285e-01,-2.5665403217e-01 +879,2.26542198,5.93414623,-0.27433558,3.22303942,1.2642210979e-01,1.2971819794e-01,3.5717970165e-01,-2.6904865365e-01 +880,2.26613119,5.93766948,-0.27506253,3.22670613,1.2773912298e-01,1.3068148224e-01,3.6120225155e-01,-2.8184212134e-01 +881,2.26684040,5.94119274,-0.27578948,3.23037284,1.2907097246e-01,1.3165265568e-01,3.6527047105e-01,-2.9504783920e-01 +882,2.26754960,5.94471600,-0.27651643,3.23403955,1.3041782253e-01,1.3263178011e-01,3.6938492051e-01,-3.0867960782e-01 +883,2.26825881,5.94823926,-0.27724338,3.23770626,1.3177984520e-01,1.3361892211e-01,3.7354618555e-01,-3.2275181272e-01 +884,2.26896802,5.95176251,-0.27797033,3.24137298,1.3315720882e-01,1.3461414461e-01,3.7775484196e-01,-3.3727934051e-01 +885,2.26967723,5.95528577,-0.27869729,3.24503969,1.3455009626e-01,1.3561752005e-01,3.8201151135e-01,-3.5227779150e-01 +886,2.27038643,5.95880903,-0.27942424,3.24870640,1.3595867872e-01,1.3662911153e-01,3.8631678152e-01,-3.6776322777e-01 +887,2.27109564,5.96233228,-0.28015119,3.25237311,1.3738313404e-01,1.3764898589e-01,3.9067126209e-01,-3.8375239416e-01 +888,2.27180485,5.96585554,-0.28087814,3.25603982,1.3882365051e-01,1.3867721646e-01,3.9507559615e-01,-4.0026280077e-01 +889,2.27251406,5.96937880,-0.28160509,3.25970653,1.4028040832e-01,1.3971386972e-01,3.9953040375e-01,-4.1731254036e-01 +890,2.27322327,5.97290205,-0.28233205,3.26337325,1.4175359059e-01,1.4075901327e-01,4.0403631567e-01,-4.3492044517e-01 +891,2.27393247,5.97642531,-0.28305900,3.26703996,1.4324339121e-01,1.4181272128e-01,4.0859399750e-01,-4.5310621497e-01 +892,2.27464168,5.97994857,-0.28378595,3.27070667,1.4474999970e-01,1.4287506380e-01,4.1320410331e-01,-4.7189027630e-01 +893,2.27535089,5.98347182,-0.28451290,3.27437338,1.4627360468e-01,1.4394610912e-01,4.1786728603e-01,-4.9129386031e-01 +894,2.27606010,5.98699508,-0.28523985,3.27804009,1.4781440997e-01,1.4502593518e-01,4.2258424723e-01,-5.1133924790e-01 +895,2.27676930,5.99051834,-0.28596680,3.28170680,1.4937260913e-01,1.4611461162e-01,4.2735565887e-01,-5.3204949105e-01 +896,2.27747851,5.99404159,-0.28669376,3.28537351,1.5094840146e-01,1.4721221096e-01,4.3218221257e-01,-5.5344865632e-01 +897,2.27818772,5.99756485,-0.28742071,3.28904023,1.5254199613e-01,1.4831881153e-01,4.3706463238e-01,-5.7556194049e-01 +898,2.27889693,6.00108811,-0.28814766,3.29270694,1.5415359595e-01,1.4943448607e-01,4.4200362474e-01,-5.9841550307e-01 +899,2.27960614,6.00461136,-0.28887461,3.29637365,1.5578340536e-01,1.5055930729e-01,4.4699990323e-01,-6.2203662051e-01 +900,2.28031534,6.00813462,-0.28960156,3.30004036,1.5743164340e-01,1.5169335683e-01,4.5205422837e-01,-6.4645394094e-01 +901,2.28102455,6.01165788,-0.29032851,3.30370707,1.5909852174e-01,1.5283671007e-01,4.5716734048e-01,-6.7169722229e-01 +902,2.28173376,6.01518114,-0.29105547,3.30737378,1.6078425730e-01,1.5398944477e-01,4.6233999796e-01,-6.9779759166e-01 +903,2.28244297,6.01870439,-0.29178242,3.31104049,1.6248906342e-01,1.5515163513e-01,4.6757295104e-01,-7.2478746592e-01 +904,2.28315218,6.02222765,-0.29250937,3.31470721,1.6421317248e-01,1.5632336701e-01,4.7286701034e-01,-7.5270099524e-01 +905,2.28386138,6.02575091,-0.29323632,3.31837392,1.6595680448e-01,1.5750471665e-01,4.7822295103e-01,-7.8157364332e-01 +906,2.28457059,6.02927416,-0.29396327,3.32204063,1.6772018501e-01,1.5869576284e-01,4.8364156806e-01,-8.1144254479e-01 +907,2.28527980,6.03279742,-0.29469022,3.32570734,1.6950355364e-01,1.5989659252e-01,4.8912370181e-01,-8.4234676023e-01 +908,2.28598901,6.03632068,-0.29541718,3.32937405,1.7130714106e-01,1.6110728533e-01,4.9467016784e-01,-8.7432697806e-01 +909,2.28669821,6.03984393,-0.29614413,3.33304076,1.7313117687e-01,1.6232791896e-01,5.0028178133e-01,-9.0742573432e-01 +910,2.28740742,6.04336719,-0.29687108,3.33670748,1.7497591401e-01,1.6355858536e-01,5.0595943186e-01,-9.4168797464e-01 +911,2.28811663,6.04689045,-0.29759803,3.34037419,1.7684159034e-01,1.6479936505e-01,5.1170396528e-01,-9.7716048221e-01 +912,2.28882584,6.05041370,-0.29832498,3.34404090,1.7872844767e-01,1.6605033986e-01,5.1751624249e-01,-1.0138923322e+00 +913,2.28953505,6.05393696,-0.29905194,3.34770761,1.8063674691e-01,1.6731160291e-01,5.2339718608e-01,-1.0519353378e+00 +914,2.29024425,6.05746022,-0.29977889,3.35137432,1.8256673332e-01,1.6858323556e-01,5.2934767367e-01,-1.0913435087e+00 +915,2.29095346,6.06098347,-0.30050584,3.35504103,1.8451866152e-01,1.6986532398e-01,5.3536861450e-01,-1.1321736912e+00 +916,2.29166267,6.06450673,-0.30123279,3.35870774,1.8649280066e-01,1.7115796244e-01,5.4146096585e-01,-1.1744858691e+00 +917,2.29237188,6.06802999,-0.30195974,3.36237446,1.8848940910e-01,1.7246123666e-01,5.4762565479e-01,-1.2183427993e+00 +918,2.29308108,6.07155324,-0.30268669,3.36604117,1.9050874731e-01,1.7377523232e-01,5.5386361813e-01,-1.2638104662e+00 +919,2.29379029,6.07507650,-0.30341365,3.36970788,1.9255109963e-01,1.7510004914e-01,5.6017586973e-01,-1.3109587829e+00 +920,2.29449950,6.07859976,-0.30414060,3.37337459,1.9461673150e-01,1.7643577308e-01,5.6656336878e-01,-1.3598608552e+00 +921,2.29520871,6.08212301,-0.30486755,3.37704130,1.9670591618e-01,1.7778249365e-01,5.7302710162e-01,-1.4105938123e+00 +922,2.29591792,6.08564627,-0.30559450,3.38070801,1.9881894597e-01,1.7914031107e-01,5.7956811718e-01,-1.4632393551e+00 +923,2.29662712,6.08916953,-0.30632145,3.38437472,2.0095609932e-01,1.8050931510e-01,5.8618742519e-01,-1.5178832380e+00 +924,2.29733633,6.09269279,-0.30704840,3.38804144,2.0311766750e-01,1.8188960214e-01,5.9288607874e-01,-1.5746162078e+00 +925,2.29804554,6.09621604,-0.30777536,3.39170815,2.0530393752e-01,1.8328126426e-01,5.9966512128e-01,-1.6335339152e+00 +926,2.29875475,6.09973930,-0.30850231,3.39537486,2.0751521514e-01,1.8468440390e-01,6.0652565876e-01,-1.6947378621e+00 +927,2.29946395,6.10326256,-0.30922926,3.39904157,2.0975179525e-01,1.8609911487e-01,6.1346876682e-01,-1.7583350045e+00 +928,2.30017316,6.10678581,-0.30995621,3.40270828,2.1201397906e-01,1.8752549337e-01,6.2049554487e-01,-1.8244385973e+00 +929,2.30088237,6.11030907,-0.31068316,3.40637499,2.1430208596e-01,1.8896364536e-01,6.2760715295e-01,-1.8931690370e+00 +930,2.30159158,6.11383233,-0.31141012,3.41004171,2.1661642427e-01,1.9041366808e-01,6.3480472066e-01,-1.9646534824e+00 +931,2.30230079,6.11735558,-0.31213707,3.41370842,2.1895730250e-01,1.9187565726e-01,6.4208938290e-01,-2.0390266796e+00 +932,2.30300999,6.12087884,-0.31286402,3.41737513,2.2132505428e-01,1.9334972250e-01,6.4946235667e-01,-2.1164323684e+00 +933,2.30371920,6.12440210,-0.31359097,3.42104184,2.2372000194e-01,1.9483596457e-01,6.5692482841e-01,-2.1970227450e+00 +934,2.30442841,6.12792535,-0.31431792,3.42470855,2.2614246804e-01,1.9633448260e-01,6.6447798968e-01,-2.2809595106e+00 +935,2.30513762,6.13144861,-0.31504487,3.42837526,2.2859279997e-01,1.9784538925e-01,6.7212311424e-01,-2.3684154829e+00 +936,2.30584683,6.13497187,-0.31577183,3.43204197,2.3107133203e-01,1.9936878722e-01,6.7986143954e-01,-2.4595741250e+00 +937,2.30655603,6.13849512,-0.31649878,3.43570869,2.3357839746e-01,2.0090477676e-01,6.8769420496e-01,-2.5546308171e+00 +938,2.30726524,6.14201838,-0.31722573,3.43937540,2.3611436207e-01,2.0245347611e-01,6.9562275615e-01,-2.6537951556e+00 +939,2.30797445,6.14554164,-0.31795268,3.44304211,2.3867957132e-01,2.0401498914e-01,7.0364838032e-01,-2.7572900700e+00 +940,2.30868366,6.14906489,-0.31867963,3.44670882,2.4127437657e-01,2.0558942140e-01,7.1177238824e-01,-2.8653539772e+00 +941,2.30939286,6.15258815,-0.31940658,3.45037553,2.4389915307e-01,2.0717689094e-01,7.1999617077e-01,-2.9782429180e+00 +942,2.31010207,6.15611141,-0.32013354,3.45404224,2.4655426650e-01,2.0877750795e-01,7.2832109397e-01,-3.0962306448e+00 +943,2.31081128,6.15963467,-0.32086049,3.45770896,2.4924008577e-01,2.1039138260e-01,7.3674853992e-01,-3.2196107703e+00 +944,2.31152049,6.16315792,-0.32158744,3.46137567,2.5195698347e-01,2.1201862521e-01,7.4527990731e-01,-3.3486985940e+00 +945,2.31222970,6.16668118,-0.32231439,3.46504238,2.5470535738e-01,2.1365935906e-01,7.5391667989e-01,-3.4838341752e+00 +946,2.31293890,6.17020444,-0.32304134,3.46870909,2.5748558775e-01,2.1531369485e-01,7.6266029213e-01,-3.6253825512e+00 +947,2.31364811,6.17372769,-0.32376830,3.47237580,2.6029806604e-01,2.1698174777e-01,7.7151221930e-01,-3.7737375719e+00 +948,2.31435732,6.17725095,-0.32449525,3.47604251,2.6314320219e-01,2.1866364187e-01,7.8047400159e-01,-3.9293251790e+00 +949,2.31506653,6.18077421,-0.32522220,3.47970922,2.6602139567e-01,2.2035949269e-01,7.8954715187e-01,-4.0926050118e+00 +950,2.31577573,6.18429746,-0.32594915,3.48337594,2.6893304817e-01,2.2206941492e-01,7.9873319640e-01,-4.2640745266e+00 +951,2.31648494,6.18782072,-0.32667610,3.48704265,2.7187859486e-01,2.2379354061e-01,8.0803377341e-01,-4.4442748530e+00 +952,2.31719415,6.19134398,-0.32740305,3.49070936,2.7485844909e-01,2.2553198669e-01,8.1745045901e-01,-4.6337920535e+00 +953,2.31790336,6.19486723,-0.32813001,3.49437607,2.7787303301e-01,2.2728487292e-01,8.2698486344e-01,-4.8332638961e+00 +954,2.31861257,6.19839049,-0.32885696,3.49804278,2.8092279366e-01,2.2905233121e-01,8.3663868311e-01,-5.0433867434e+00 +955,2.31932177,6.20191375,-0.32958391,3.50170949,2.8400816366e-01,2.3083448263e-01,8.4641357538e-01,-5.2649194968e+00 +956,2.32003098,6.20543700,-0.33031086,3.50537620,2.8712958504e-01,2.3263145128e-01,8.5631123410e-01,-5.4986925023e+00 +957,2.32074019,6.20896026,-0.33103781,3.50904292,2.9028752553e-01,2.3444337360e-01,8.6633344256e-01,-5.7456174438e+00 +958,2.32144940,6.21248352,-0.33176476,3.51270963,2.9348243654e-01,2.3627037409e-01,8.7648193935e-01,-6.0066938795e+00 +959,2.32215860,6.21600677,-0.33249172,3.51637634,2.9671477550e-01,2.3811257824e-01,8.8675848979e-01,-6.2830220212e+00 +960,2.32286781,6.21953003,-0.33321867,3.52004305,2.9998503216e-01,2.3997012736e-01,8.9716496951e-01,-6.5758179820e+00 +961,2.32357702,6.22305329,-0.33394562,3.52370976,3.0329367741e-01,2.4184314935e-01,9.0770320213e-01,-6.8864244676e+00 +962,2.32428623,6.22657655,-0.33467257,3.52737647,3.0664119743e-01,2.4373177820e-01,9.1837506802e-01,-7.2163310110e+00 +963,2.32499544,6.23009980,-0.33539952,3.53104319,3.1002807648e-01,2.4563614411e-01,9.2918244933e-01,-7.5671926455e+00 +964,2.32570464,6.23362306,-0.33612648,3.53470990,3.1345483235e-01,2.4755639343e-01,9.4012734421e-01,-7.9408574420e+00 +965,2.32641385,6.23714632,-0.33685343,3.53837661,3.1692196383e-01,2.4949265902e-01,9.5121169756e-01,-8.3393889697e+00 +966,2.32712306,6.24066957,-0.33758038,3.54204332,3.2042997621e-01,2.5144507462e-01,9.6243748451e-01,-8.7651018996e+00 +967,2.32783227,6.24419283,-0.33830733,3.54571003,3.2397940991e-01,2.5341379071e-01,9.7380680139e-01,-9.2206049076e+00 +968,2.32854147,6.24771609,-0.33903428,3.54937674,3.2757078332e-01,2.5539894258e-01,9.8532168341e-01,-9.7088406385e+00 +969,2.32925068,6.25123934,-0.33976123,3.55304345,3.3120462815e-01,2.5740066999e-01,9.9698421604e-01,-1.0233145997e+01 +970,2.32995989,6.25476260,-0.34048819,3.55671017,3.3488150832e-01,2.5941912749e-01,1.0087965994e+00,-1.0797323360e+01 +971,2.33066910,6.25828586,-0.34121514,3.56037688,3.3860196337e-01,2.6145445321e-01,1.0207609640e+00,-1.1405714597e+01 +972,2.33137831,6.26180911,-0.34194209,3.56404359,3.4236654621e-01,2.6350678955e-01,1.0328794932e+00,-1.2063307082e+01 +973,2.33208751,6.26533237,-0.34266904,3.56771030,3.4617584343e-01,2.6557629424e-01,1.0451544896e+00,-1.2775861867e+01 +974,2.33279672,6.26885563,-0.34339599,3.57137701,3.5003042688e-01,2.6766311369e-01,1.0575882163e+00,-1.3550059072e+01 +975,2.33350593,6.27237888,-0.34412294,3.57504372,3.5393086990e-01,2.6976739206e-01,1.0701829535e+00,-1.4393692397e+01 +976,2.33421514,6.27590214,-0.34484990,3.57871044,3.5787778931e-01,2.7188929373e-01,1.0829411322e+00,-1.5315920989e+01 +977,2.33492435,6.27942540,-0.34557685,3.58237715,3.6187177385e-01,2.7402896460e-01,1.0958651028e+00,-1.6327562463e+01 +978,2.33563355,6.28294865,-0.34630380,3.58604386,3.6591342451e-01,2.7618655395e-01,1.1089572675e+00,-1.7441497679e+01 +979,2.33634276,6.28647191,-0.34703075,3.58971057,3.7000338787e-01,2.7836223193e-01,1.1222201847e+00,-1.8673196909e+01 +980,2.33705197,6.28999517,-0.34775770,3.59337728,3.7414228117e-01,2.8055614960e-01,1.1356563319e+00,-2.0041378131e+01 +981,2.33776118,6.29351843,-0.34848466,3.59704399,3.7833074589e-01,2.8276846748e-01,1.1492682765e+00,-2.1568926635e+01 +982,2.33847038,6.29704168,-0.34921161,3.60071070,3.8256941284e-01,2.8499933694e-01,1.1630585631e+00,-2.3284107070e+01 +983,2.33917959,6.30056494,-0.34993856,3.60437742,3.8685896955e-01,2.8724893559e-01,1.1770299300e+00,-2.5222274262e+01 +984,2.33988880,6.30408820,-0.35066551,3.60804413,3.9120007071e-01,2.8951742014e-01,1.1911850249e+00,-2.7428194916e+01 +985,2.34059801,6.30761145,-0.35139246,3.61171084,3.9559338282e-01,2.9180494985e-01,1.2055265448e+00,-2.9959429606e+01 +986,2.34130722,6.31113471,-0.35211941,3.61537755,4.0003961700e-01,2.9411170350e-01,1.2200573461e+00,-3.2891276581e+01 +987,2.34201642,6.31465797,-0.35284637,3.61904426,4.0453946002e-01,2.9643784337e-01,1.2347802218e+00,-3.6324114692e+01 +988,2.34272563,6.31818122,-0.35357332,3.62271097,4.0909360920e-01,2.9878353334e-01,1.2496980016e+00,-4.0394826256e+01 +989,2.34343484,6.32170448,-0.35430027,3.62637768,4.1370280758e-01,3.0114895704e-01,1.2648136904e+00,-4.5295089836e+01 +990,2.34414405,6.32522774,-0.35502722,3.63004440,4.1836777359e-01,3.0353428136e-01,1.2801302218e+00,-5.1301741279e+01 +991,2.34485325,6.32875099,-0.35575417,3.63371111,4.2308923330e-01,3.0593967324e-01,1.2956505731e+00,-5.8830030298e+01 +992,2.34556246,6.33227425,-0.35648112,3.63737782,4.2786796920e-01,3.0836532417e-01,1.3113779097e+00,-6.8532588826e+01 +993,2.34627167,6.33579751,-0.35720808,3.64104453,4.3270473257e-01,3.1081140564e-01,1.3273153235e+00,-8.1497058296e+01 +994,2.34698088,6.33932076,-0.35793503,3.64471124,4.3760028080e-01,3.1327808809e-01,1.3434659283e+00,-9.9681160977e+01 +995,2.34769009,6.34284402,-0.35866198,3.64837795,4.4255542899e-01,3.1576556683e-01,1.3598330538e+00,-1.2700059093e+02 +996,2.34839929,6.34636728,-0.35938893,3.65204467,4.4757096045e-01,3.1827401678e-01,1.3764199496e+00,-1.7259170146e+02 +997,2.34910850,6.34989053,-0.36011588,3.65571138,4.5264767866e-01,3.2080361857e-01,1.3932299078e+00,-2.6386350062e+02 +998,2.34981771,6.35341379,-0.36084283,3.65937809,4.5778643074e-01,3.2335457022e-01,1.4102664150e+00,-5.3786262303e+02 +999,2.35052692,6.35693705,-0.36156979,3.66304480,4.6290996067e-01,3.2587040383e-01,8.2404772721e-01,-1.9334998979e+04 diff --git a/scripts/phs/phs_density_comparison.jl b/scripts/phs/phs_density_comparison.jl new file mode 100644 index 000000000..cf23361c3 --- /dev/null +++ b/scripts/phs/phs_density_comparison.jl @@ -0,0 +1,1020 @@ +# ============================================================ +# PHS Charge Density Analysis β€” Comparison Script +# ============================================================ +# +# Reproduces the 3Γ—2 log-scale comparison plot comparing analytical DFT +# (reference) values against 3D interpolants for: +# Row 1: ρ (charge density) +# Row 2: |βˆ‡Ο| (gradient magnitude) +# Row 3: |βˆ‡Β²Ο| (Laplacian magnitude) +# along the O7...H21 hydrogen-bond path in the phenol dimer. +# +# Left column β€” all standard 3D methods vs DFT reference +# Right column β€” Polyharmonic spline (PHS) vs DFT reference +# +# For PHS, the reference density is the analytical promolecule density +# constructed from critic2 PBE wavefunction files, enabling log-density +# smoothing via the reference_interp interface. +# +# Data files (edit the paths below if needed): +# 3D grid : phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl +# Python pickle: x (75,), y (113,), z (70,) [Bohr], +# variables["density_scf"] (75,113,70) [a.u.] β€” DFT reference density +# 1D path : phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv +# Columns: point, x_bohr, y_bohr, z_bohr, arclength_bohr, +# density_scf, density_frag, dengrad_mag, laplacian_scf +# +# Dependencies (add to your active Julia environment as needed): +# Pkg.add("Plots") β€” Plots.jl for visualisation +# Pkg.add("Pickle") β€” Pickle.jl for loading the .pkl grid file +# +# Implementation notes on PHS derivatives: +# PHSInterpolantND does NOT implement _locate_cell/_eval_at_cell, so +# gradient() / laplacian() from vector_calculus.jl are NOT available for it. +# Instead use the `deriv` keyword on the callable: +# itp_phs(q; deriv = (DerivOp{1}(), DerivOp{0}(), DerivOp{0}())) # βˆ‚Ο/βˆ‚x +# itp_phs(q; deriv = (DerivOp{2}(), DerivOp{0}(), DerivOp{0}())) # βˆ‚Β²Ο/βˆ‚xΒ² + +using FastInterpolations +using DelimitedFiles +using LinearAlgebra +using Pickle +using Plots +using Printf +using Statistics + +# ============================================================ +# Patch Pickle.jl to support numpy._core (NumPy >= 2.0) +# ============================================================ +Pickle.np_methods!(mt) = begin + mt["numpy.core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy._core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy.dtype"] = Pickle.np_dtype + mt["numpy.core.multiarray.scalar"] = Pickle.np_scalar + mt["numpy._core.multiarray.scalar"] = Pickle.np_scalar + mt["__build__.Pickle.NpyDtype"] = Pickle.build_npydtype + mt["__build__.Pickle.NpyArrayPlaceholder"] = Pickle.build_nparray + return mt +end + +# ============================================================ +# Configuration β€” edit paths here +# ============================================================ +# Paths are resolved relative to this script (@__DIR__) so it runs from any CWD. +# wfc/ wavefunction files auto-download from critic2 (ensure_wfc_files); the .pkl +# grid and .csv line cut are committed under dat/ (no public download source). +const PKL_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl") +const CSV_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv") +const XYZ_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_atoms.xyz") +const WFC_DIR = joinpath(@__DIR__, "dat", "wfc") +const OUT_PATH = normpath(joinpath(@__DIR__, "..", "..", "docs", "images", "phs_density_comparison.png")) + +const BOHR2ANG = 0.529177210903 # 1 Bohr β†’ Angstrom + +# ── Element symbol β†’ atomic number (full periodic table) ────────────────────── +const ELEMENT_Z = Dict( + "H" => 1, "He" => 2, "Li" => 3, "Be" => 4, "B" => 5, "C" => 6, "N" => 7, "O" => 8, + "F" => 9, "Ne" => 10, "Na" => 11, "Mg" => 12, "Al" => 13, "Si" => 14, "P" => 15, "S" => 16, + "Cl" => 17, "Ar" => 18, "K" => 19, "Ca" => 20, "Sc" => 21, "Ti" => 22, "V" => 23, "Cr" => 24, + "Mn" => 25, "Fe" => 26, "Co" => 27, "Ni" => 28, "Cu" => 29, "Zn" => 30, "Ga" => 31, "Ge" => 32, + "As" => 33, "Se" => 34, "Br" => 35, "Kr" => 36, "Rb" => 37, "Sr" => 38, "Y" => 39, "Zr" => 40, + "Nb" => 41, "Mo" => 42, "Tc" => 43, "Ru" => 44, "Rh" => 45, "Pd" => 46, "Ag" => 47, "Cd" => 48, + "In" => 49, "Sn" => 50, "Sb" => 51, "Te" => 52, "I" => 53, "Xe" => 54, "Cs" => 55, "Ba" => 56, + "La" => 57, "Ce" => 58, "Pr" => 59, "Nd" => 60, "Pm" => 61, "Sm" => 62, "Eu" => 63, "Gd" => 64, + "Tb" => 65, "Dy" => 66, "Ho" => 67, "Er" => 68, "Tm" => 69, "Yb" => 70, "Lu" => 71, "Hf" => 72, + "Ta" => 73, "W" => 74, "Re" => 75, "Os" => 76, "Ir" => 77, "Pt" => 78, "Au" => 79, "Hg" => 80, + "Tl" => 81, "Pb" => 82, "Bi" => 83, "Po" => 84, "At" => 85, "Rn" => 86, "Fr" => 87, "Ra" => 88, + "Ac" => 89, "Th" => 90, "Pa" => 91, "U" => 92, "Np" => 93, "Pu" => 94, "Am" => 95, "Cm" => 96, + "Bk" => 97, "Cf" => 98, "Es" => 99, "Fm" => 100, "Md" => 101, "No" => 102, "Lr" => 103, + "Rf" => 104, "Db" => 105, "Sg" => 106, "Bh" => 107, "Hs" => 108, "Mt" => 109, "Ds" => 110, + "Rg" => 111, "Cn" => 112, "Nh" => 113, "Fl" => 114, "Mc" => 115, "Lv" => 116, "Ts" => 117, "Og" => 118, +) + +# ============================================================ +# Auto-download PBE wavefunction files if missing +# ============================================================ + +""" + ensure_wfc_files() + +Download critic2 PBE wavefunction files if not already present. +Files are downloaded from GitHub and cached in `WFC_DIR`. +Uses regex to parse GitHub API response (simple approach, no JSON dependency). +""" +function ensure_wfc_files() + mkpath(WFC_DIR) + + # List of all elements (Z=1 to Z=118) with their 2-letter symbols, + # using the keys of ELEMENT_Z. Single-letter symbols get an extra underscore for filename formatting. + # Single letters get underscore padding (Hβ†’h_, Cβ†’c_, etc.) + # We only need H, C, O for this system, but it's written in a way that's easy to extend if needed. + all_symbols = [rpad(lowercase(sym), 2, '_') for sym in keys(ELEMENT_Z) if any(lowercase(sym) == s for s in ("h", "c", "o"))] + + # Check if files already exist + existing = filter(f -> endswith(f, ".wfc"), readdir(WFC_DIR)) + existing_count = length(existing) + + if existing_count >= length(all_symbols) + println("βœ“ All wavefunction files already present") + return + end + + println("Downloading PBE wavefunction files from critic2 (GitHub)...") + + # Try to download files using raw GitHub URLs with hardcoded filename list + base_url = "https://raw.githubusercontent.com/aoterodelaroza/critic2/master/dat/wfc" + + download_count = 0 + for sym in all_symbols + fname = sym * "_pbe.wfc" + fpath = joinpath(WFC_DIR, fname) + + # Skip if already exists and is non-empty (>1KB suggests real data) + if isfile(fpath) && filesize(fpath) > 1000 + continue + end + + url = "$base_url/$fname" + + try + run(`curl -s -o $fpath $url`) + # Check if download was successful (file size > 1KB) + if isfile(fpath) && filesize(fpath) > 1000 + download_count += 1 + print(".") + if download_count % 30 == 0 + println("") + end + else + # Failed download, remove small file + isfile(fpath) && rm(fpath) + end + catch e + # Download error, clean up + isfile(fpath) && rm(fpath) + end + end + + println("\n Downloaded $download_count new wavefunction files") + return if download_count > 0 + println(" βœ“ Wavefunction files ready") + end +end + +# Download wfc files if needed +ensure_wfc_files() + +# ============================================================ +# 1. Load 3D grid from pickle via Pickle.jl +# ============================================================ +println("Loading 3D grid from pickle...") + +pkl = Pickle.npyload(PKL_PATH) +x_grid = Float64.(pkl["x"]) +y_grid = Float64.(pkl["y"]) +z_grid = Float64.(pkl["z"]) +rho_3d = Float64.(pkl["variables"]["density_scf"]) # DFT reference density: (nx,ny,nz) via c2f + +@printf " Grid: %dΓ—%dΓ—%d, ρ ∈ [%.2e, %.2e] a.u.\n" length(x_grid) length(y_grid) length(z_grid) minimum(rho_3d) maximum(rho_3d) + +# ============================================================ +# 2. Load 1D analytical path from CSV +# ============================================================ +println("Loading 1D analytical path from CSV...") + +raw = readdlm(CSV_PATH, ',', skipstart = 1) +# column order: point, x_bohr, y_bohr, z_bohr, arclength_bohr, +# density_scf, density_frag, dengrad_mag, laplacian_scf +qx = Float64.(raw[:, 2]) # x_bohr +qy = Float64.(raw[:, 3]) # y_bohr +qz = Float64.(raw[:, 4]) # z_bohr +s_ang = Float64.(raw[:, 5]) .* BOHR2ANG # arclength in Γ… +ρ_ref = Float64.(raw[:, 6]) # density_scf [a.u.] +βˆ‡Ο_ref = Float64.(raw[:, 8]) # dengrad_mag [a.u./Bohr] +βˆ‡Β²Ο_ref = abs.(Float64.(raw[:, 9])) # |laplacian_scf| [a.u./BohrΒ²] +N_path = length(qx) + +@printf " Path: %d points, s ∈ [%.4f, %.4f] Γ…\n" N_path s_ang[1] s_ang[end] + +# ============================================================ +# 3. Promolecular reference density (PromolecularRef) +# Replicates the Fortran crystalmod_promolecular approach: +# ρ₀(x) = Ξ£α΅’ ρᡒ(|x - Rα΅’|) using PBE all-electron atomic densities +# from critic2 dat/wfc files, giving an accurate ρ₀ and exact +# analytical derivatives everywhere β€” no cubic-spline Gibbs near cusps. +# ============================================================ + +# wfc filename: single-letter symbols get an extra trailing underscore +# e.g. "H" β†’ "h__pbe.wfc", "He" β†’ "he_pbe.wfc" +wfc_filename(sym::String) = rpad(lowercase(sym), 2, '_') * "_pbe.wfc" + +""" + parse_wfc(filepath) -> (r_grid, rho_values) + +Parse a critic2 PBE all-electron wfc file and return the radial grid and +electron density ρ(r) = Ξ£β±Ό occβ±Ό ψⱼ(r)Β² / (4Ο€rΒ²). + +File format: + Line 1: norb + Line 2: orbital labels + Line 3: occupations (integers) + Line 4: orbital energies + Line 5: ngrid + Lines 6…: r Οˆβ‚(r) Οˆβ‚‚(r) … Οˆβ‚™β‚’α΅£b(r) +""" +function parse_wfc(filepath::String) + return open(filepath) do io + norb = parse(Int, readline(io)) + readline(io) # labels β€” not needed + occ = parse.(Float64, split(readline(io))) + readline(io) # energies β€” not needed + ngrid = parse(Int, readline(io)) + + r_vals = Vector{Float64}(undef, ngrid) + rho_vals = Vector{Float64}(undef, ngrid) + pi4 = 4Ο€ + + for i in 1:ngrid + row = parse.(Float64, split(readline(io))) + r = row[1] + psi = @view row[2:end] + rr0 = dot(occ, psi .^ 2) # Ξ£β±Ό occβ±Ό ψⱼ² + r_vals[i] = r + rho_vals[i] = rr0 / (pi4 * r^2) # ρ(r) + end + return r_vals, rho_vals + end +end + +# Build a Dict: Z => 1D cubic spline of ρ(r) for each element present in system +# (lazy-loaded; only elements actually needed are parsed) +const _wfc_cache = Dict{Int, Any}() + +function get_rho_itp(Z::Int) + haskey(_wfc_cache, Z) && return _wfc_cache[Z] + sym = findfirst(==(Z), ELEMENT_Z) + sym === nothing && error("Unknown atomic number Z=$Z") + fname = joinpath(WFC_DIR, wfc_filename(sym)) + isfile(fname) || error("wfc file not found: $fname") + r_grid, rho_vals = parse_wfc(fname) + itp = cubic_interp(r_grid, rho_vals; extrap = FillExtrap(0.0)) + _wfc_cache[Z] = itp + return itp +end + +# ── XYZ loader β€” returns Vector of (Z, (x,y,z)) with positions in Bohr ──────── +const ANG2BOHR = 1.0 / BOHR2ANG # 1 Γ… β†’ Bohr + +""" + load_xyz(filepath) -> Vector{Tuple{Int, NTuple{3,Float64}}} + +Load an XYZ file (positions in Angstrom) and return atoms as (Z, (x,y,z)) in Bohr. +Skips the first two header lines. +""" +function load_xyz(filepath::String) + lines = readlines(filepath) + n = parse(Int, strip(lines[1])) + atoms = Vector{Tuple{Int, NTuple{3, Float64}}}(undef, n) + for i in 1:n + parts = split(strip(lines[i + 2])) + sym = String(parts[1]) + Z = ELEMENT_Z[sym] + x, y, z = parse(Float64, parts[2]) * ANG2BOHR, + parse(Float64, parts[3]) * ANG2BOHR, + parse(Float64, parts[4]) * ANG2BOHR + atoms[i] = (Z, (x, y, z)) + end + return atoms +end + +println("Loading atomic geometry from XYZ...") +const ATOMS = load_xyz(XYZ_PATH) +@printf " %d atoms loaded\n" length(ATOMS) + +# ── Verify path endpoints are near atoms ────────────────────────────────────── +let + p_start = (qx[1], qy[1], qz[1]) + p_end = (qx[end], qy[end], qz[end]) + for (label, pt) in (("start", p_start), ("end", p_end)) + best_d = Inf + best_i = 0 + for (i, (Z, R)) in enumerate(ATOMS) + d = sqrt(sum((pt[k] - R[k])^2 for k in 1:3)) + if d < best_d + best_d = d + best_i = i + end + end + Z_near, R_near = ATOMS[best_i] + sym_near = findfirst(==(Z_near), ELEMENT_Z) + @printf " Path %-5s β†’ nearest atom %2d (%s) at (%.4f, %.4f, %.4f) Bohr, dist = %.4f Bohr\n" label best_i sym_near R_near[1] R_near[2] R_near[3] best_d + end +end + +# ── PromolecularRef ──────────────────────────────────────────────────────────── +# Callable with the same (q; deriv=nothing) interface as LogCubicRef. +# Uses the Fortran chain rule for exact gradient and Hessian: +# βˆ‚Οβ‚€/βˆ‚xd = Ξ£α΅’ ρᡒ'(rα΅’) Β· xxd / rα΅’ +# βˆ‚Β²Οβ‚€/βˆ‚xdΒ² = Ξ£α΅’ [ ρᡒ'(rα΅’)/rα΅’ + (ρᡒ''(rα΅’) βˆ’ ρᡒ'(rα΅’)/rα΅’) Β· xxdΒ² / rα΅’Β² ] +# βˆ‚Β²Οβ‚€/βˆ‚xd1βˆ‚xd2 = Ξ£α΅’ [ (ρᡒ''(rα΅’) βˆ’ ρᡒ'(rα΅’)/rα΅’) Β· xxd1 Β· xxd2 / rα΅’Β² ] +# +# Type parameter I is the concrete spline type (CubicInterpolant{...}), making +# cache::Dict{Int,I} fully type-stable so all per-atom eval calls are zero-alloc. +struct PromolecularRef{I} + atoms::Vector{Tuple{Int, NTuple{3, Float64}}} # (Z, (x,y,z)) in Bohr + cache_array::Vector{I} + cache::Dict{Int, I} # Z => typed 1D spline +end + +# Constructor: load all element splines and embed a typed Dict. +function PromolecularRef(atoms::Vector{Tuple{Int, NTuple{3, Float64}}}) + for (Z, _) in atoms + get_rho_itp(Z) + end # populate _wfc_cache (Dict{Int,Any}) + I = typeof(first(values(_wfc_cache))) + cache = Dict{Int, I}(k => v for (k, v) in _wfc_cache) + + max_z = maximum(Z for (Z, _) in atoms) + cache_array = Vector{I}(undef, max_z) + for (Z, itp) in cache + if Z <= max_z + cache_array[Z] = itp + end + end + return PromolecularRef{I}(atoms, cache_array, cache) +end + +@inline function _pmr_get_deriv_info(::Type{O}) where {O <: Tuple} + orders = (deriv_order(fieldtype(O, 1)), deriv_order(fieldtype(O, 2)), deriv_order(fieldtype(O, 3))) + total = sum(orders) + if total == 1 + ax = findfirst(o -> o == 1, orders)::Int + return (total, ax, 0) + elseif total == 2 + ax1 = findfirst(o -> o > 0, orders)::Int + ax2 = ax1 < 3 ? findnext(o -> o > 0, orders, ax1 + 1) : nothing + ax2 = ax2 !== nothing ? ax2 : ax1 + return (total, ax1, ax2) + else + return (total, 0, 0) + end +end + +@inline function _pmr_eval_val_internal(pmr::PromolecularRef, q::NTuple{3, <:Real}) + # Value evaluation + f = 0.0 + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + f += max(pmr.cache_array[Z](r), 0.0) + end + return f +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}) + return _pmr_eval_val_internal(pmr, q) +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}, ops::O) where {O} + info = _pmr_get_deriv_info(O) + total = info[1] + if total == 0 + return _pmr_eval_val_internal(pmr, q) + elseif total == 1 + ax = info[2] + fp = 0.0 + D1 = DerivOp{1}() + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + dx = ax == 1 ? xx1 : (ax == 2 ? xx2 : xx3) + fp += pmr.cache_array[Z](r; deriv = D1) * dx / r + end + return fp + elseif total == 2 + ax1 = info[2] + ax2 = info[3] + fpp = 0.0 + D1 = DerivOp{1}() + D2 = DerivOp{2}() + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + rho_itp = pmr.cache_array[Z] + rhop = rho_itp(r; deriv = D1) + rhopp = rho_itp(r; deriv = D2) + rfac = (rhopp - rhop / r) / (r * r) + dx1 = ax1 == 1 ? xx1 : (ax1 == 2 ? xx2 : xx3) + dx2 = ax2 == 1 ? xx1 : (ax2 == 2 ? xx2 : xx3) + fpp += ax1 == ax2 ? rhop / r + rfac * (dx1 * dx1) : rfac * dx1 * dx2 + end + return fpp + else + return 0.0 + end +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}; deriv = nothing) + if deriv === nothing + return _pmr_eval_val_internal(pmr, q) + else + return pmr(q, deriv) + end +end + +# In-place batch evaluation mirroring the PHS batch API +function (pmr::PromolecularRef)( + out::AbstractVector{T}, + queries::Union{Tuple{Vararg{AbstractVector, 3}}, AbstractVector}; + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, 3}}} = EvalValue(), + ) where {T} + N = 3 + ops = FastInterpolations._resolve_deriv_nd(deriv, Val(N)) + nq = FastInterpolations._query_length(queries) + length(out) == nq || throw(DimensionMismatch("Query and output sizes mismatch")) + + @inbounds for k in 1:nq + q = FastInterpolations._extract_query_point(queries, k, Val(N)) + out[k] = pmr(q; deriv = ops) + end + return out +end + +println("Building PromolecularRef (loading wfc files for present elements)...") +const ref_rho0 = PromolecularRef(ATOMS) +@printf " ρ₀ at path start: %.6e a.u.\n" ref_rho0((qx[1], qy[1], qz[1])) + +# ============================================================ +# 4. Build 3D interpolants β€” with timing capture +# ============================================================ +grids = (x_grid, y_grid, z_grid) +println("\nBuilding interpolants on $(length(x_grid))Γ—$(length(y_grid))Γ—$(length(z_grid)) grid...") + +# Storage for timing information +build_times = Dict{String, Float64}() +eval_times = Dict{String, Dict{String, Float64}}() + +println(" [1/4] Nearest (constant)...") +time_nearest = @elapsed itp_nearest = constant_interp(grids, rho_3d) +build_times["Nearest"] = time_nearest +@printf " %.4f seconds\n" time_nearest + +println(" [2/4] Trilinear (linear)...") +time_linear = @elapsed itp_linear = linear_interp(grids, rho_3d) +build_times["Linear"] = time_linear +@printf " %.4f seconds\n" time_linear + +println(" [3/4] Trispline (global cubic spline)...") +time_cubic = @elapsed itp_cubic = cubic_interp(grids, rho_3d) +build_times["Cubic"] = time_cubic +@printf " %.4f seconds\n" time_cubic + +println(" [4/4] Tricubic (Cardinal / Catmull-Rom)...") +time_cardinal = @elapsed itp_cardinal = interp( + grids, rho_3d; + method = (CardinalInterp(), CardinalInterp(), CardinalInterp()) +) +build_times["Cardinal"] = time_cardinal +@printf " %.4f seconds\n" time_cardinal + +println(" [PHS] Polyharmonic spline (PHS-3, stencil_size=8, log-density transform)...") +# Paper (Sec. III): N = 8Β³ = 512 stencil nodes; f = log(ρ_scf / ρ₀) is smooth +# across the whole grid. PromolecularRef provides ρ₀ and exact derivatives +# from PBE all-electron atomic radial splines via the chain rule β€” matches the +# Fortran crystalmod_promolecular approach and avoids Gibbs-like errors from +# a cubic spline of log(ρ₀) near nuclear cusps. +time_phs = @elapsed itp_phs = phs_interp( + grids, rho_3d; stencil_size = 8, degree = 3, + reference_interp = ref_rho0 +) +build_times["PHS"] = time_phs +@printf " %.4f seconds\n" time_phs + +println("All interpolants built.") + +# ============================================================ +# 5. Evaluate along the 1D path +# ============================================================ +println("\nEvaluating along path ($N_path points)...") + +# Allocate result arrays +ρ_nearest = zeros(N_path); +ρ_linear = zeros(N_path); +ρ_cubic = zeros(N_path); +ρ_cardinal = zeros(N_path); +ρ_phs = zeros(N_path) + +βˆ‡Ο_linear = zeros(N_path); +βˆ‡Ο_cubic = zeros(N_path); +βˆ‡Ο_cardinal = zeros(N_path); +βˆ‡Ο_phs = zeros(N_path); + +βˆ‡Β²Ο_cubic = zeros(N_path) +βˆ‡Β²Ο_cardinal = zeros(N_path) +βˆ‡Β²Ο_phs = zeros(N_path) + +# Derivative operator shortcuts: +# DerivOp{n}() selects the n-th derivative along that axis (0 = value) +const D0 = DerivOp{0}() +const D1 = DerivOp{1}() +const D2 = DerivOp{2}() + +# SoA query format β€” all ND interpolants accept (x_vec, y_vec, z_vec). +# PHSInterpolantND batch evaluation uses Threads.@threads internally. +const queries = (qx, qy, qz) + +# Scratch buffers shared across all gradient / Laplacian batch calls +const _gx = zeros(N_path) +const _gy = zeros(N_path) +const _gz = zeros(N_path) + +# ── PHS warm-up: trigger JIT compilation and fill stencil cache ─────────────── +# The first call to a PHS method with a new ops-type triggers JIT compilation, +# which can account for millions of "allocations" in @time output. These warm-up +# calls force compilation and stencil-cache filling so that the @time measurements +# below reflect steady-state (allocation-minimal) execution. +# Must warm up the exact same code patterns that will be timed, including +# the aggregation operations (sqrt, abs), to ensure proper JIT compilation. +print(" Warming up PHS (JIT + stencil cache) ... ") +flush(stdout) + +itp_phs(ρ_phs, queries) # value β†’ compile + cache fill + +# Warm up gradient magnitude with aggregation +begin + itp_phs(_gx, queries; deriv = (D1, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D1, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_phs = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +# Warm up Laplacian magnitude with aggregation +begin + itp_phs(_gx, queries; deriv = (D2, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D2, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_phs = abs(_gx + _gy + _gz) +end + +println("done.") + +# Warm up other interpolants for fair comparison +# Must warm up the exact same code patterns that will be timed, including +# the aggregation operations (sqrt, abs), to ensure proper JIT compilation. +print(" Warming up other interpolants (JIT + stencil cache) ... ") +flush(stdout) + +# Warm up value evaluations +itp_nearest(ρ_nearest, queries) +itp_linear(ρ_linear, queries) +itp_cubic(ρ_cubic, queries) +itp_cardinal(ρ_cardinal, queries) + +# Warm up gradient magnitude (including aggregation by sqrt) +# Mimic the exact code structure used in timing measurements +begin + itp_linear(_gx, queries; deriv = (D1, D0, D0)) + itp_linear(_gy, queries; deriv = (D0, D1, D0)) + itp_linear(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_linear = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +begin + itp_cubic(_gx, queries; deriv = (D1, D0, D0)) + itp_cubic(_gy, queries; deriv = (D0, D1, D0)) + itp_cubic(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_cubic = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +begin + itp_cardinal(_gx, queries; deriv = (D1, D0, D0)) + itp_cardinal(_gy, queries; deriv = (D0, D1, D0)) + itp_cardinal(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_cardinal = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +# Warm up Laplacian magnitude (including aggregation by abs) +# Mimic the exact code structure used in timing measurements +begin + itp_cubic(_gx, queries; deriv = (D2, D0, D0)) + itp_cubic(_gy, queries; deriv = (D0, D2, D0)) + itp_cubic(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_cubic = abs(_gx + _gy + _gz) +end + +begin + itp_cardinal(_gx, queries; deriv = (D2, D0, D0)) + itp_cardinal(_gy, queries; deriv = (D0, D2, D0)) + itp_cardinal(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_cardinal = abs(_gx + _gy + _gz) +end + +println("done.") + +# ── density ρ ────────────────────────────────────────────────────────────────── +println(" Density (ρ):") +eval_times["ρ"] = Dict() + +print(" Nearest ... ") +eval_times["ρ"]["Nearest"] = @elapsed @time itp_nearest(ρ_nearest, queries) + +print(" Linear ... ") +eval_times["ρ"]["Linear"] = @elapsed @time itp_linear(ρ_linear, queries) + +print(" Cubic ... ") +eval_times["ρ"]["Cubic"] = @elapsed @time itp_cubic(ρ_cubic, queries) + +print(" Cardinal ... ") +eval_times["ρ"]["Cardinal"] = @elapsed @time itp_cardinal(ρ_cardinal, queries) + +print(" PHS ... ") +eval_times["ρ"]["PHS"] = @elapsed @time itp_phs(ρ_phs, queries) + +# ── gradient magnitude |βˆ‡Ο| ──────────────────────────────────────────────────── +# All ND interpolants accept the batch form itp(out, queries; deriv=(...)). +# PHS does not implement _locate_cell/_eval_at_cell so gradient() is unavailable, +# but the same computation works via the deriv kwarg on the batch callable. +println(" Gradient Magnitude (|βˆ‡Ο|):") +eval_times["|βˆ‡Ο|"] = Dict() + +print(" Linear ... ") +eval_times["|βˆ‡Ο|"]["Linear"] = @elapsed @time begin + itp_linear(_gx, queries; deriv = (D1, D0, D0)) + itp_linear(_gy, queries; deriv = (D0, D1, D0)) + itp_linear(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_linear = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +print(" Cubic ... ") +eval_times["|βˆ‡Ο|"]["Cubic"] = @elapsed @time begin + itp_cubic(_gx, queries; deriv = (D1, D0, D0)) + itp_cubic(_gy, queries; deriv = (D0, D1, D0)) + itp_cubic(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_cubic = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +print(" Cardinal ... ") +eval_times["|βˆ‡Ο|"]["Cardinal"] = @elapsed @time begin + itp_cardinal(_gx, queries; deriv = (D1, D0, D0)) + itp_cardinal(_gy, queries; deriv = (D0, D1, D0)) + itp_cardinal(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_cardinal = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +print(" PHS ... ") +eval_times["|βˆ‡Ο|"]["PHS"] = @elapsed @time begin + itp_phs(_gx, queries; deriv = (D1, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D1, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D1)) + @. βˆ‡Ο_phs = sqrt(_gx^2 + _gy^2 + _gz^2) +end + +# ── Laplacian magnitude |βˆ‡Β²Ο| ────────────────────────────────────────────────── +println(" Laplacian Magnitude (|βˆ‡Β²Ο|):") +eval_times["|βˆ‡Β²Ο|"] = Dict() + +print(" Cubic ... ") +eval_times["|βˆ‡Β²Ο|"]["Cubic"] = @elapsed @time begin + itp_cubic(_gx, queries; deriv = (D2, D0, D0)) + itp_cubic(_gy, queries; deriv = (D0, D2, D0)) + itp_cubic(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_cubic = abs(_gx + _gy + _gz) +end + +print(" Cardinal ... ") +eval_times["|βˆ‡Β²Ο|"]["Cardinal"] = @elapsed @time begin + itp_cardinal(_gx, queries; deriv = (D2, D0, D0)) + itp_cardinal(_gy, queries; deriv = (D0, D2, D0)) + itp_cardinal(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_cardinal = abs(_gx + _gy + _gz) +end + +print(" PHS ... ") +eval_times["|βˆ‡Β²Ο|"]["PHS"] = @elapsed @time begin + itp_phs(_gx, queries; deriv = (D2, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D2, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_phs = abs(_gx + _gy + _gz) +end + +println("Evaluation complete.") + +# ============================================================ +# 5a. Compute errors and generate summary tables +# ============================================================ + +# Helper function to compute relative error +compute_rel_error(computed, reference) = begin + errors = similar(computed) + for i in eachindex(computed) + ref_val = abs(reference[i]) + if ref_val > 0.0 + errors[i] = abs(computed[i] - reference[i]) / ref_val + else + errors[i] = abs(computed[i]) + end + end + return errors +end + +# Compute errors for each method +errors_rho = Dict() +errors_grad = Dict() +errors_lap = Dict() + +errors_rho["Nearest"] = compute_rel_error(ρ_nearest, ρ_ref) +errors_rho["Linear"] = compute_rel_error(ρ_linear, ρ_ref) +errors_rho["Cubic"] = compute_rel_error(ρ_cubic, ρ_ref) +errors_rho["Cardinal"] = compute_rel_error(ρ_cardinal, ρ_ref) +errors_rho["PHS"] = compute_rel_error(ρ_phs, ρ_ref) + +errors_grad["Linear"] = compute_rel_error(βˆ‡Ο_linear, βˆ‡Ο_ref) +errors_grad["Cubic"] = compute_rel_error(βˆ‡Ο_cubic, βˆ‡Ο_ref) +errors_grad["Cardinal"] = compute_rel_error(βˆ‡Ο_cardinal, βˆ‡Ο_ref) +errors_grad["PHS"] = compute_rel_error(βˆ‡Ο_phs, βˆ‡Ο_ref) + +errors_lap["Cubic"] = compute_rel_error(βˆ‡Β²Ο_cubic, βˆ‡Β²Ο_ref) +errors_lap["Cardinal"] = compute_rel_error(βˆ‡Β²Ο_cardinal, βˆ‡Β²Ο_ref) +errors_lap["PHS"] = compute_rel_error(βˆ‡Β²Ο_phs, βˆ‡Β²Ο_ref) + +# Print performance summary tables +println("\n" * "="^80) +println("PERFORMANCE SUMMARY") +println("="^80) + +# Combined Build and Evaluation Times Table +println("\n#### Timing Summary (with PHS-to-method ratios)") +println("| Method | Build (s) | ρ Time (s) | \\|βˆ‡Ο\\| Time (s) | \\|βˆ‡Β²Ο\\| Time (s) |") +println("|--------|-----------|------------|----------------|-----------------|") + +phs_build_time = build_times["PHS"] +phs_rho_time = eval_times["ρ"]["PHS"] +phs_grad_time = eval_times["|βˆ‡Ο|"]["PHS"] +phs_lap_time = eval_times["|βˆ‡Β²Ο|"]["PHS"] + +for method in ["Nearest", "Linear", "Cubic", "Cardinal", "PHS"] + build_time = build_times[method] + rho_time = get(eval_times["ρ"], method, nothing) + grad_time = get(eval_times["|βˆ‡Ο|"], method, nothing) + lap_time = get(eval_times["|βˆ‡Β²Ο|"], method, nothing) + + # Format build time with ratio + build_str = if method == "PHS" + @sprintf("%.3f", build_time) + else + ratio = phs_build_time / build_time + @sprintf("%.5f (%.1fΓ—)", build_time, ratio) + end + + # Format rho time with ratio + rho_str = if rho_time !== nothing + if method == "PHS" + @sprintf("%.4f", rho_time) + else + ratio = phs_rho_time / rho_time + @sprintf("%.5f (%.1fΓ—)", rho_time, ratio) + end + else + "β€”" + end + + # Format grad time with ratio + grad_str = if grad_time !== nothing + if method == "PHS" + @sprintf("%.4f", grad_time) + else + ratio = phs_grad_time / grad_time + @sprintf("%.5f (%.1fΓ—)", grad_time, ratio) + end + else + "β€”" + end + + # Format lap time with ratio + lap_str = if lap_time !== nothing + if method == "PHS" + @sprintf("%.4f", lap_time) + else + ratio = phs_lap_time / lap_time + @sprintf("%.5f (%.1fΓ—)", lap_time, ratio) + end + else + "β€”" + end + + @printf "| %-18s | %15s | %16s | %18s | %20s |\n" method build_str rho_str grad_str lap_str +end + +# Compute PHS statistics for comparison +phs_rho_stats = Dict( + :min => minimum(errors_rho["PHS"]), + :max => maximum(errors_rho["PHS"]), + :mean => sum(errors_rho["PHS"]) / length(errors_rho["PHS"]), + :median => median(errors_rho["PHS"]) +) +phs_grad_stats = Dict( + :min => minimum(errors_grad["PHS"]), + :max => maximum(errors_grad["PHS"]), + :mean => sum(errors_grad["PHS"]) / length(errors_grad["PHS"]), + :median => median(errors_grad["PHS"]) +) +phs_lap_stats = Dict( + :min => minimum(errors_lap["PHS"]), + :max => maximum(errors_lap["PHS"]), + :mean => sum(errors_lap["PHS"]) / length(errors_lap["PHS"]), + :median => median(errors_lap["PHS"]) +) + +# Table 2: Density (ρ) Errors with PHS comparison ratios +println("\n#### Charge Density (ρ) β€” Relative Error Statistics (with method-to-PHS ratios)\n") +println("| Method | Min Error | Max Error | Mean Error | Median Error |") +println("|--------|-----------|-----------|------------|--------------|") +for method in ["Nearest", "Linear", "Cubic", "Cardinal", "PHS"] + errs = errors_rho[method] + min_err = minimum(errs) + max_err = maximum(errs) + mean_err = sum(errs) / length(errs) + median_err = median(errs) + + if method == "PHS" + @printf "| %-18s | %.2e | %.2e | %.2e | %.2e |\n" method min_err max_err mean_err median_err + else + min_ratio = min_err / phs_rho_stats[:min] + max_ratio = max_err / phs_rho_stats[:max] + mean_ratio = mean_err / phs_rho_stats[:mean] + median_ratio = median_err / phs_rho_stats[:median] + @printf "| %-18s | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) |\n" method min_err min_ratio max_err max_ratio mean_err mean_ratio median_err median_ratio + end +end + +# Table 3: Gradient Error with PHS comparison ratios +println("\n#### Gradient Magnitude (|βˆ‡Ο|) β€” Relative Error Statistics (with method-to-PHS ratios)\n") +println("| Method | Min Error | Max Error | Mean Error | Median Error |") +println("|--------|-----------|-----------|------------|--------------|") +for method in ["Linear", "Cubic", "Cardinal", "PHS"] + errs = errors_grad[method] + min_err = minimum(errs) + max_err = maximum(errs) + mean_err = sum(errs) / length(errs) + median_err = median(errs) + + if method == "PHS" + @printf "| %-18s | %.2e | %.2e | %.2e | %.2e |\n" method min_err max_err mean_err median_err + else + min_ratio = min_err / phs_grad_stats[:min] + max_ratio = max_err / phs_grad_stats[:max] + mean_ratio = mean_err / phs_grad_stats[:mean] + median_ratio = median_err / phs_grad_stats[:median] + @printf "| %-18s | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) |\n" method min_err min_ratio max_err max_ratio mean_err mean_ratio median_err median_ratio + end +end + +# Table 4: Laplacian Error with PHS comparison ratios +println("\n#### Laplacian Magnitude (|βˆ‡Β²Ο|) β€” Relative Error Statistics (with method-to-PHS ratios)\n") +println("| Method | Min Error | Max Error | Mean Error | Median Error |") +println("|--------|-----------|-----------|------------|--------------|") +for method in ["Cubic", "Cardinal", "PHS"] + errs = errors_lap[method] + min_err = minimum(errs) + max_err = maximum(errs) + mean_err = sum(errs) / length(errs) + median_err = median(errs) + + if method == "PHS" + @printf "| %-18s | %.2e | %.2e | %.2e | %.2e |\n" method min_err max_err mean_err median_err + else + min_ratio = min_err / phs_lap_stats[:min] + max_ratio = max_err / phs_lap_stats[:max] + mean_ratio = mean_err / phs_lap_stats[:mean] + median_ratio = median_err / phs_lap_stats[:median] + @printf "| %-18s | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) | %.2e (%.0fΓ—) |\n" method min_err min_ratio max_err max_ratio mean_err mean_ratio median_err median_ratio + end +end + +println("\n" * "="^80 * "\n") + +# ============================================================ +# 6. Plot β€” 3 rows Γ— 2 columns, log-scale y-axis +# ============================================================ +println("\nGenerating plot...") + +# Colour scheme (approximately matching the reference figure) +col_analytical = :black +col_nearest = :blue +col_linear = :red +col_cubic = :darkorange # Trispline +col_cardinal = :darkgreen # Tricubic +col_phs = :red + +lw_ref = 2.0 +lw_itp = 1.5 +lw_phs = 0.8 # Thinner line for PHS + +# Replace non-positive values with NaN for safe log-scale rendering +logclean(v) = [x > 0.0 ? x : NaN for x in v] + +xlims_val = (s_ang[1], s_ang[end]) +xlabel_str = "Distance along OΒ·Β·Β·H hydrogen bond (Γ…)" + +# Custom Y-axis formatter for log scale +# Decimal notation for values ≀ 10000, scientific for values > 10000 +yformatter = (y) -> begin + if y > 10000 + # Use scientific notation for large values + @sprintf("%.1e", y) + else + # Format with decimal notation, remove trailing zeros + s = @sprintf("%.8f", y) + s = replace(s, r"0+$" => "") + replace(s, r"\.$" => "") + end +end + +# Y-axis ticks: Powers of 10 only, extended range to 10^6 +# Range: 10^-4 to 10^6 to cover full data span without gaps +yticks_val = [10.0^i for i in -4:6] + +kw_common = ( + xaxis = :identity, + yaxis = :log10, + xlims = xlims_val, + xlabel = xlabel_str, + xticks = 0:0.2:2.0, + yticks = yticks_val, + yformatter = yformatter, + legend = :topright, + minorgrid = true, + framestyle = :box, +) + +function ref_series!(p, s, ref; label = "Analytical") + return plot!( + p, s, logclean(ref); label = label, + color = col_analytical, linewidth = lw_ref, linestyle = :solid + ) +end + +function add_series!(p, s, data, label, color; lw = lw_itp) + return plot!(p, s, logclean(data); label = label, color = color, linewidth = lw) +end + +# ── Row 1: ρ ────────────────────────────────────────────────────────────────── +p11 = plot(; kw_common..., ylabel = "ρ (a.u.)") +ref_series!(p11, s_ang, ρ_ref) +add_series!(p11, s_ang, ρ_nearest, "Nearest", col_nearest) +add_series!(p11, s_ang, ρ_linear, "Trilinear", col_linear) +add_series!(p11, s_ang, ρ_cardinal, "Tricubic", col_cardinal) +add_series!(p11, s_ang, ρ_cubic, "Trispline", col_cubic) + +p12 = plot(; kw_common..., ylabel = "ρ (a.u.)") +ref_series!(p12, s_ang, ρ_ref) +add_series!(p12, s_ang, ρ_phs, "Polyharmonic", col_phs; lw = lw_phs) + +# ── Row 2: |βˆ‡Ο| ─────────────────────────────────────────────────────────────── +p21 = plot(; kw_common..., ylabel = "|βˆ‡Ο| (a.u.)") +ref_series!(p21, s_ang, βˆ‡Ο_ref) +add_series!(p21, s_ang, βˆ‡Ο_linear, "Trilinear", col_linear) +add_series!(p21, s_ang, βˆ‡Ο_cardinal, "Tricubic", col_cardinal) +add_series!(p21, s_ang, βˆ‡Ο_cubic, "Trispline", col_cubic) + +p22 = plot(; kw_common..., ylabel = "|βˆ‡Ο| (a.u.)") +ref_series!(p22, s_ang, βˆ‡Ο_ref) +add_series!(p22, s_ang, βˆ‡Ο_phs, "Polyharmonic", col_phs; lw = lw_phs) + +# ── Row 3: |βˆ‡Β²Ο| ────────────────────────────────────────────────────────────── +p31 = plot(; kw_common..., ylabel = "|βˆ‡Β²Ο| (a.u.)") +ref_series!(p31, s_ang, βˆ‡Β²Ο_ref) +add_series!(p31, s_ang, βˆ‡Β²Ο_cardinal, "Tricubic", col_cardinal) +add_series!(p31, s_ang, βˆ‡Β²Ο_cubic, "Trispline", col_cubic) + +p32 = plot(; kw_common..., ylabel = "|βˆ‡Β²Ο| (a.u.)") +ref_series!(p32, s_ang, βˆ‡Β²Ο_ref) +add_series!(p32, s_ang, βˆ‡Β²Ο_phs, "Polyharmonic", col_phs; lw = lw_phs) + +# ── Combine ─────────────────────────────────────────────────────────────────── +fig = plot( + p11, p12, p21, p22, p31, p32; + layout = (3, 2), + size = (900, 1050), + dpi = 150, + left_margin = 10Plots.mm, + bottom_margin = 7Plots.mm, + top_margin = 4Plots.mm, + right_margin = 3Plots.mm +) + +savefig(fig, OUT_PATH) +println("Saved: $OUT_PATH") +display(fig) diff --git a/scripts/phs/phs_density_comparison_simplified.jl b/scripts/phs/phs_density_comparison_simplified.jl new file mode 100644 index 000000000..3d2473198 --- /dev/null +++ b/scripts/phs/phs_density_comparison_simplified.jl @@ -0,0 +1,607 @@ +# ============================================================ +# PHS Density Analysis β€” Simplified Profiling & Benchmark Script +# ============================================================ +# +# A streamlined version of phs_density_comparison.jl focused solely on: +# 1. Building and evaluating the PHS interpolant (and only the PHS). +# 2. Validating that computed PHS Laplacian values match the reference. +# 3. Optionally plotting the PHS vs. Analytical Laplacian magnitude +# along with its relative error (highly customizable, skip to speed up). +# 4. Enabling easy profiling and benchmarking of the PHS evaluation +# hot-path using high-repetition timing loops. +# +# Run this script with: +# julia --project=scripts scripts/phs/phs_density_comparison_simplified.jl +# Or for tracking allocations: +# julia --project=scripts --track-allocation=user scripts/phs/phs_density_comparison_simplified.jl +# Or with standard Julia profiling: +# julia --project=scripts -e 'using Profile; include("scripts/phs/phs_density_comparison_simplified.jl")' +# + +# ============================================================ +# Configuration Options +# ============================================================ +const PLOT = false # Enable/disable generating the sanity-check plot +const BENCHMARK = true # Enable/disable the high-repetition benchmark loops +const BENCHMARK_REPS = 5 # Number of evaluation repetitions for profiling (e.g. 1000 reps) +const PROFILE = true # Enable/disable the CPU profiling run + +# ============================================================ +# Dependencies +# ============================================================ +using FastInterpolations +using DelimitedFiles +using LinearAlgebra +using Pickle +using Printf +using Statistics +using Profile + +if PLOT + using Plots +end + +# ============================================================ +# Patch Pickle.jl to support numpy._core (NumPy >= 2.0) +# ============================================================ +Pickle.np_methods!(mt) = begin + mt["numpy.core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy._core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy.dtype"] = Pickle.np_dtype + mt["numpy.core.multiarray.scalar"] = Pickle.np_scalar + mt["numpy._core.multiarray.scalar"] = Pickle.np_scalar + mt["__build__.Pickle.NpyDtype"] = Pickle.build_npydtype + mt["__build__.Pickle.NpyArrayPlaceholder"] = Pickle.build_nparray + return mt +end + +# ============================================================ +# Configuration β€” paths resolved relative to this script (@__DIR__) +# ============================================================ +# wfc/ auto-downloads from critic2 (ensure_wfc_files); .pkl grid and .csv line cut +# are committed under dat/ (no public download source). +const PKL_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl") +const CSV_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv") +const XYZ_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_atoms.xyz") +const WFC_DIR = joinpath(@__DIR__, "dat", "wfc") +const OUT_PATH = normpath(joinpath(@__DIR__, "..", "..", "docs", "images", "phs_laplacian_comparison_simplified.png")) + +const BOHR2ANG = 0.529177210903 # 1 Bohr β†’ Angstrom + +# ── Element symbol β†’ atomic number (full periodic table) ────────────────────── +const ELEMENT_Z = Dict( + "H" => 1, "He" => 2, "Li" => 3, "Be" => 4, "B" => 5, "C" => 6, "N" => 7, "O" => 8, + "F" => 9, "Ne" => 10, "Na" => 11, "Mg" => 12, "Al" => 13, "Si" => 14, "P" => 15, "S" => 16, + "Cl" => 17, "Ar" => 18, "K" => 19, "Ca" => 20, "Sc" => 21, "Ti" => 22, "V" => 23, "Cr" => 24, + "Mn" => 25, "Fe" => 26, "Co" => 27, "Ni" => 28, "Cu" => 29, "Zn" => 30, "Ga" => 31, "Ge" => 32, + "As" => 33, "Se" => 34, "Br" => 35, "Kr" => 36, "Rb" => 37, "Sr" => 38, "Y" => 39, "Zr" => 40, + "Nb" => 41, "Mo" => 42, "Tc" => 43, "Ru" => 44, "Rh" => 45, "Pd" => 46, "Ag" => 47, "Cd" => 48, + "In" => 49, "Sn" => 50, "Sb" => 51, "Te" => 52, "I" => 53, "Xe" => 54, "Cs" => 55, "Ba" => 56, + "La" => 57, "Ce" => 58, "Pr" => 59, "Nd" => 60, "Pm" => 61, "Sm" => 62, "Eu" => 63, "Gd" => 64, + "Tb" => 65, "Dy" => 66, "Ho" => 67, "Er" => 68, "Tm" => 69, "Yb" => 70, "Lu" => 71, "Hf" => 72, + "Ta" => 73, "W" => 74, "Re" => 75, "Os" => 76, "Ir" => 77, "Pt" => 78, "Au" => 79, "Hg" => 80, + "Tl" => 81, "Pb" => 82, "Bi" => 83, "Po" => 84, "At" => 85, "Rn" => 86, "Fr" => 87, "Ra" => 88, + "Ac" => 89, "Th" => 90, "Pa" => 91, "U" => 92, "Np" => 93, "Pu" => 94, "Am" => 95, "Cm" => 96, + "Bk" => 97, "Cf" => 98, "Es" => 99, "Fm" => 100, "Md" => 101, "No" => 102, "Lr" => 103, + "Rf" => 104, "Db" => 105, "Sg" => 106, "Bh" => 107, "Hs" => 108, "Mt" => 109, "Ds" => 110, + "Rg" => 111, "Cn" => 112, "Nh" => 113, "Fl" => 114, "Mc" => 115, "Lv" => 116, "Ts" => 117, "Og" => 118, +) + +# ============================================================ +# Auto-download PBE wavefunction files if missing +# ============================================================ +function ensure_wfc_files() + mkpath(WFC_DIR) + + all_symbols = [rpad(lowercase(sym), 2, '_') for sym in keys(ELEMENT_Z) if any(lowercase(sym) == s for s in ("h", "c", "o"))] + existing = filter(f -> endswith(f, ".wfc"), readdir(WFC_DIR)) + if length(existing) >= length(all_symbols) + println("βœ“ All wavefunction files already present") + return + end + + println("Downloading PBE wavefunction files from critic2 (GitHub)...") + base_url = "https://raw.githubusercontent.com/aoterodelaroza/critic2/master/dat/wfc" + + download_count = 0 + for sym in all_symbols + fname = sym * "_pbe.wfc" + fpath = joinpath(WFC_DIR, fname) + if isfile(fpath) && filesize(fpath) > 1000 + continue + end + url = "$base_url/$fname" + try + run(`curl -s -o $fpath $url`) + if isfile(fpath) && filesize(fpath) > 1000 + download_count += 1 + print(".") + if download_count % 30 == 0 + println("") + end + else + isfile(fpath) && rm(fpath) + end + catch e + isfile(fpath) && rm(fpath) + end + end + return println("\n Downloaded $download_count new wavefunction files") +end + +ensure_wfc_files() + +# ============================================================ +# 1. Load 3D grid and 1D analytical path +# ============================================================ +println("\nLoading files...") +pkl = Pickle.npyload(PKL_PATH) +x_grid = Float64.(pkl["x"]) +y_grid = Float64.(pkl["y"]) +z_grid = Float64.(pkl["z"]) +rho_3d = Float64.(pkl["variables"]["density_scf"]) + +@printf " Grid: %dΓ—%dΓ—%d, ρ ∈ [%.2e, %.2e] a.u.\n" length(x_grid) length(y_grid) length(z_grid) minimum(rho_3d) maximum(rho_3d) + +raw = readdlm(CSV_PATH, ',', skipstart = 1) +qx = Float64.(raw[:, 2]) # x_bohr +qy = Float64.(raw[:, 3]) # y_bohr +qz = Float64.(raw[:, 4]) # z_bohr +s_ang = Float64.(raw[:, 5]) .* BOHR2ANG # arclength in Γ… +ρ_ref = Float64.(raw[:, 6]) # density_scf [a.u.] +βˆ‡Β²Ο_ref = abs.(Float64.(raw[:, 9])) # |laplacian_scf| [a.u./BohrΒ²] +N_path = length(qx) + +@printf " Path: %d points, s ∈ [%.4f, %.4f] Γ…\n" N_path s_ang[1] s_ang[end] + +# ============================================================ +# 2. Promolecular reference density (PromolecularRef) +# ============================================================ +wfc_filename(sym::String) = rpad(lowercase(sym), 2, '_') * "_pbe.wfc" + +function parse_wfc(filepath::String) + return open(filepath) do io + norb = parse(Int, readline(io)) + readline(io) + occ = parse.(Float64, split(readline(io))) + readline(io) + ngrid = parse(Int, readline(io)) + + r_vals = Vector{Float64}(undef, ngrid) + rho_vals = Vector{Float64}(undef, ngrid) + pi4 = 4Ο€ + + for i in 1:ngrid + row = parse.(Float64, split(readline(io))) + r = row[1] + psi = @view row[2:end] + rr0 = dot(occ, psi .^ 2) + r_vals[i] = r + rho_vals[i] = rr0 / (pi4 * r^2) + end + return r_vals, rho_vals + end +end + +const _wfc_cache = Dict{Int, Any}() + +function get_rho_itp(Z::Int) + haskey(_wfc_cache, Z) && return _wfc_cache[Z] + sym = findfirst(==(Z), ELEMENT_Z) + sym === nothing && error("Unknown atomic number Z=$Z") + fname = joinpath(WFC_DIR, wfc_filename(sym)) + isfile(fname) || error("wfc file not found: $fname") + r_grid, rho_vals = parse_wfc(fname) + itp = cubic_interp(r_grid, rho_vals; extrap = FillExtrap(0.0)) + _wfc_cache[Z] = itp + return itp +end + +const ANG2BOHR = 1.0 / BOHR2ANG + +function load_xyz(filepath::String) + lines = readlines(filepath) + n = parse(Int, strip(lines[1])) + atoms = Vector{Tuple{Int, NTuple{3, Float64}}}(undef, n) + for i in 1:n + parts = split(strip(lines[i + 2])) + sym = String(parts[1]) + Z = ELEMENT_Z[sym] + x, y, z = parse(Float64, parts[2]) * ANG2BOHR, + parse(Float64, parts[3]) * ANG2BOHR, + parse(Float64, parts[4]) * ANG2BOHR + atoms[i] = (Z, (x, y, z)) + end + return atoms +end + +const ATOMS = load_xyz(XYZ_PATH) + +struct PromolecularRef{I} + atoms::Vector{Tuple{Int, NTuple{3, Float64}}} + cache_array::Vector{I} + cache::Dict{Int, I} +end + +function PromolecularRef(atoms::Vector{Tuple{Int, NTuple{3, Float64}}}) + for (Z, _) in atoms + get_rho_itp(Z) + end + I = typeof(first(values(_wfc_cache))) + cache = Dict{Int, I}(k => v for (k, v) in _wfc_cache) + + max_z = maximum(Z for (Z, _) in atoms) + cache_array = Vector{I}(undef, max_z) + for (Z, itp) in cache + if Z <= max_z + cache_array[Z] = itp + end + end + return PromolecularRef{I}(atoms, cache_array, cache) +end + +@inline function _pmr_get_deriv_info(::Type{O}) where {O <: Tuple} + orders = (deriv_order(fieldtype(O, 1)), deriv_order(fieldtype(O, 2)), deriv_order(fieldtype(O, 3))) + total = sum(orders) + if total == 1 + ax = findfirst(o -> o == 1, orders)::Int + return (total, ax, 0) + elseif total == 2 + ax1 = findfirst(o -> o > 0, orders)::Int + ax2 = ax1 < 3 ? findnext(o -> o > 0, orders, ax1 + 1) : nothing + ax2 = ax2 !== nothing ? ax2 : ax1 + return (total, ax1, ax2) + else + return (total, 0, 0) + end +end + +@inline function _pmr_eval_val_internal(pmr::PromolecularRef, q::NTuple{3, <:Real}) + # Value evaluation + f = 0.0 + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + f += max(pmr.cache_array[Z](r), 0.0) + end + return f +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}) + return _pmr_eval_val_internal(pmr, q) +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}, ops::O) where {O} + info = _pmr_get_deriv_info(O) + total = info[1] + if total == 0 + return _pmr_eval_val_internal(pmr, q) + elseif total == 1 + ax = info[2] + fp = 0.0 + D1 = DerivOp{1}() + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + dx = ax == 1 ? xx1 : (ax == 2 ? xx2 : xx3) + fp += pmr.cache_array[Z](r; deriv = D1) * dx / r + end + return fp + elseif total == 2 + ax1 = info[2] + ax2 = info[3] + fpp = 0.0 + D1 = DerivOp{1}() + D2 = DerivOp{2}() + @inbounds for i in 1:length(pmr.atoms) + Z, R = pmr.atoms[i] + xx1 = q[1] - R[1] + xx2 = q[2] - R[2] + xx3 = q[3] - R[3] + r = sqrt(xx1 * xx1 + xx2 * xx2 + xx3 * xx3) + r < 1.0e-14 && continue + rho_itp = pmr.cache_array[Z] + rhop = rho_itp(r; deriv = D1) + rhopp = rho_itp(r; deriv = D2) + rfac = (rhopp - rhop / r) / (r * r) + dx1 = ax1 == 1 ? xx1 : (ax1 == 2 ? xx2 : xx3) + dx2 = ax2 == 1 ? xx1 : (ax2 == 2 ? xx2 : xx3) + fpp += ax1 == ax2 ? rhop / r + rfac * (dx1 * dx1) : rfac * dx1 * dx2 + end + return fpp + else + return 0.0 + end +end + +@inline function (pmr::PromolecularRef)(q::NTuple{3, <:Real}; deriv = nothing) + if deriv === nothing + return _pmr_eval_val_internal(pmr, q) + else + return pmr(q, deriv) + end +end + +# In-place batch evaluation mirroring the PHS batch API +function (pmr::PromolecularRef)( + out::AbstractVector{T}, + queries::Union{Tuple{Vararg{AbstractVector, 3}}, AbstractVector}; + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, 3}}} = EvalValue(), + ) where {T} + N = 3 + ops = FastInterpolations._resolve_deriv_nd(deriv, Val(N)) + nq = FastInterpolations._query_length(queries) + length(out) == nq || throw(DimensionMismatch("Query and output sizes mismatch")) + + @inbounds for k in 1:nq + q = FastInterpolations._extract_query_point(queries, k, Val(N)) + out[k] = pmr(q; deriv = ops) + end + return out +end + +println("Building PromolecularRef reference interpolant...") +const ref_rho0 = PromolecularRef(ATOMS) + +# ============================================================ +# 3. Build only the PHS Interpolant +# ============================================================ +grids = (x_grid, y_grid, z_grid) +println("\nBuilding Polyharmonic spline (PHS-3, stencil_size=8, blend_factor=1.0, log-density transform)...") +time_phs = @elapsed itp_phs = phs_interp( + grids, rho_3d; stencil_size = 8, degree = 3, + reference_interp = ref_rho0 +) +@printf " Built in %.4f seconds\n" time_phs + +# ============================================================ +# 4. Correctness Sanity Check +# ============================================================ +println("\nRunning Correctness Sanity Check...") + +# Allocate result arrays for PHS evaluation +ρ_phs = zeros(N_path) +_gx = zeros(N_path) +_gy = zeros(N_path) +_gz = zeros(N_path) +βˆ‡Β²Ο_phs = zeros(N_path) + +# Query coordinates SoA format +const queries = (qx, qy, qz) + +# Derivative operators +const D0 = DerivOp{0}() +const D1 = DerivOp{1}() +const D2 = DerivOp{2}() + +# Warm up PHS compilation & stencil caches +print(" Warming up PHS evaluation ... ") +flush(stdout) +itp_phs(ρ_phs, queries) +itp_phs(_gx, queries; deriv = (D1, D0, D0)) +itp_phs(_gy, queries; deriv = (D0, D1, D0)) +itp_phs(_gz, queries; deriv = (D0, D0, D1)) +itp_phs(_gx, queries; deriv = (D2, D0, D0)) +itp_phs(_gy, queries; deriv = (D0, D2, D0)) +itp_phs(_gz, queries; deriv = (D0, D0, D2)) +println("done.") + +# Evaluate PHS Laplacian Magnitude +itp_phs(_gx, queries; deriv = (D2, D0, D0)) +itp_phs(_gy, queries; deriv = (D0, D2, D0)) +itp_phs(_gz, queries; deriv = (D0, D0, D2)) +@. βˆ‡Β²Ο_phs = abs(_gx + _gy + _gz) + +# Helper function to compute relative error +compute_rel_error(computed, reference) = begin + errors = similar(computed) + for i in eachindex(computed) + ref_val = abs(reference[i]) + if ref_val > 0.0 + errors[i] = abs(computed[i] - reference[i]) / ref_val + else + errors[i] = abs(computed[i]) + end + end + return errors +end + +errors_lap = compute_rel_error(βˆ‡Β²Ο_phs, βˆ‡Β²Ο_ref) + +# Compute error statistics +phs_lap_stats = Dict( + :min => minimum(errors_lap), + :max => maximum(errors_lap), + :mean => mean(errors_lap), + :median => median(errors_lap) +) + +println("\n" * "="^50) +println("PHS LAPLACIAN ERROR STATISTICS VS ANALYTICAL") +println("="^50) +@printf " Min Error: %.2e\n" phs_lap_stats[:min] +@printf " Max Error: %.2e\n" phs_lap_stats[:max] +@printf " Mean Error: %.2e\n" phs_lap_stats[:mean] +@printf " Median Error: %.2e\n" phs_lap_stats[:median] +println("="^50) + +# ============================================================ +# 5. Hot-Path Benchmarking / Profiling +# ============================================================ +if BENCHMARK + println("\n" * "="^60) + println("BENCHMARKING PHS INTERPOLANT EVALUATION") + println("="^60) + @printf "Running %d repetitions over %d query points...\n\n" BENCHMARK_REPS N_path + + # Standard Julia @time macro to capture allocations & GC overhead + println("PHS Density (ρ) Evaluation:") + @time itp_phs(ρ_phs, queries) + + println("\nPHS Laplacian Component xx Evaluation:") + @time itp_phs(_gx, queries; deriv = (D2, D0, D0)) + + println("\nBenchmarking over high loop counts to run profilers:") + + # 1. Benchmark value evaluation (ρ) + print(" Evaluating values (ρ) ... ") + flush(stdout) + t_val = @elapsed begin + for _ in 1:BENCHMARK_REPS + itp_phs(ρ_phs, queries) + end + end + @printf "%.4f seconds (%.2f ΞΌs per query point)\n" t_val (t_val * 1.0e6 / (BENCHMARK_REPS * N_path)) + + # 2. Benchmark Laplacian evaluation (|βˆ‡Β²Ο|) + print(" Evaluating Laplacian (|βˆ‡Β²Ο| components) ... ") + flush(stdout) + t_lap = @elapsed begin + for _ in 1:BENCHMARK_REPS + itp_phs(_gx, queries; deriv = (D2, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D2, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_phs = abs(_gx + _gy + _gz) + end + end + @printf "%.4f seconds (%.2f ΞΌs per query point)\n" t_lap (t_lap * 1.0e6 / (BENCHMARK_REPS * N_path)) + + if PROFILE + # Increase profile buffer for more complete data collection + Profile.init(n = 50_000_000, delay = 0.001) + + # 1. Profile Density (ρ) Evaluation + println("\nRunning CPU Profiling for Density (ρ) Evaluation (100 repetitions)...") + Profile.clear() + @profile for _ in 1:100 + itp_phs(ρ_phs, queries) + end + println("\n" * "="^60) + println("CPU PROFILING RESULTS: DENSITY (ρ) EVALUATION") + println("="^60) + Profile.print(format = :flat, mincount = 5, noisefloor = 2.0, groupby = [:task, :thread], maxdepth = 40) + + # 2. Profile Gradient (|βˆ‡Ο|) Evaluation + println("\nRunning CPU Profiling for Gradient (|βˆ‡Ο|) Evaluation (100 repetitions)...") + Profile.clear() + @profile for _ in 1:100 + itp_phs(_gx, queries; deriv = (D1, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D1, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D1)) + end + println("\n" * "="^60) + println("CPU PROFILING RESULTS: GRADIENT (|βˆ‡Ο|) EVALUATION") + println("="^60) + Profile.print(format = :flat, mincount = 5, noisefloor = 2.0, groupby = [:task, :thread], maxdepth = 40) + + # 3. Profile Laplacian (|βˆ‡Β²Ο|) Evaluation + println("\nRunning CPU Profiling for Laplacian (|βˆ‡Β²Ο|) Evaluation (100 repetitions)...") + Profile.clear() + @profile for _ in 1:100 + itp_phs(_gx, queries; deriv = (D2, D0, D0)) + itp_phs(_gy, queries; deriv = (D0, D2, D0)) + itp_phs(_gz, queries; deriv = (D0, D0, D2)) + @. βˆ‡Β²Ο_phs = abs(_gx + _gy + _gz) + end + println("\n" * "="^60) + println("CPU PROFILING RESULTS: LAPLACIAN (|βˆ‡Β²Ο|) EVALUATION") + println("="^60) + Profile.print(format = :flat, mincount = 5, noisefloor = 2.0, groupby = [:task, :thread], maxdepth = 40) + end + + println("="^60) + println("Profiling code can be run via: ") + println(" julia --project=scripts --track-allocation=user scripts/phs/phs_density_comparison_simplified.jl") +end + +# ============================================================ +# 6. Plotting (Optional Sanity Check) +# ============================================================ +if PLOT + println("\nGenerating sanity-check plots...") + + # Replace non-positive values with NaN for safe log-scale rendering + logclean(v) = [x > 0.0 ? x : NaN for x in v] + + # Style definitions matching premium aesthetics + col_analytical = RGB(0.1, 0.1, 0.1) # Sleek Charcoal Black + col_phs = RGB(0.85, 0.15, 0.15) # Vibrant Crimson Red + col_error = RGB(0.0, 0.45, 0.7) # Deep Ocean Blue + + yformatter = (y) -> begin + if y > 10000 + @sprintf("%.1e", y) + else + s = @sprintf("%.6f", y) + s = replace(s, r"0+$" => "") + replace(s, r"\.$" => "") + end + end + + # Create the left plot (Laplacian Magnitude) + p1 = plot( + xaxis = :identity, + yaxis = :log10, + xlims = (s_ang[1], s_ang[end]), + xlabel = "Distance along OΒ·Β·Β·H hydrogen bond (Γ…)", + ylabel = "|βˆ‡Β²Ο| (a.u.)", + xticks = 0:0.2:2.0, + yticks = [10.0^i for i in -4:6], + yformatter = yformatter, + legend = :topright, + minorgrid = true, + framestyle = :box, + title = "Laplacian Magnitude Comparison", + titlefontsize = 10, + guidefontsize = 9, + tickfontsize = 8, + legendfontsize = 8, + ) + plot!(p1, s_ang, logclean(βˆ‡Β²Ο_ref); label = "Analytical", color = col_analytical, linewidth = 2.0, linestyle = :solid) + plot!(p1, s_ang, logclean(βˆ‡Β²Ο_phs); label = "Polyharmonic", color = col_phs, linewidth = 1.0, linestyle = :dash) + + # Create the right plot (PHS Laplacian Relative Error) + p2 = plot( + xaxis = :identity, + yaxis = :log10, + xlims = (s_ang[1], s_ang[end]), + xlabel = "Distance along OΒ·Β·Β·H hydrogen bond (Γ…)", + ylabel = "Relative Error", + xticks = 0:0.2:2.0, + yticks = [10.0^i for i in -7:1], + yformatter = yformatter, + legend = false, + minorgrid = true, + framestyle = :box, + title = "PHS Laplacian Relative Error", + titlefontsize = 10, + guidefontsize = 9, + tickfontsize = 8, + ) + plot!(p2, s_ang, logclean(errors_lap); color = col_error, linewidth = 1.2, linestyle = :solid) + + # Combine into a gorgeous premium layout + fig = plot( + p1, p2; + layout = (1, 2), + size = (1000, 450), + dpi = 150, + left_margin = 6Plots.mm, + bottom_margin = 6Plots.mm, + top_margin = 6Plots.mm, + right_margin = 4Plots.mm, + ) + + savefig(fig, OUT_PATH) + println("Saved sanity-check plot: $OUT_PATH") +end + +println("\nFinished successfully!") diff --git a/scripts/phs/profile_bottlenecks.jl b/scripts/phs/profile_bottlenecks.jl new file mode 100644 index 000000000..3e4084c38 --- /dev/null +++ b/scripts/phs/profile_bottlenecks.jl @@ -0,0 +1,94 @@ +#!/usr/bin/env julia + +using FastInterpolations +using Profile +using Pickle +using Printf +using DelimitedFiles + +# Patch Pickle.jl to support numpy._core (NumPy >= 2.0) +Pickle.np_methods!(mt) = begin + mt["numpy.core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy._core.multiarray._reconstruct"] = Pickle.np_multiarray_reconstruct + mt["numpy.dtype"] = Pickle.np_dtype + mt["numpy.core.multiarray.scalar"] = Pickle.np_scalar + mt["numpy._core.multiarray.scalar"] = Pickle.np_scalar + mt["__build__.Pickle.NpyDtype"] = Pickle.build_npydtype + mt["__build__.Pickle.NpyArrayPlaceholder"] = Pickle.build_nparray + return mt +end + +# Configuration +const PKL_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_3dgrid_sp0.236_ext3.pkl") +const CSV_PATH = joinpath(@__DIR__, "dat", "phenol-dimer_B3LYP_TZ2P_GO_line_O7_H21_N1000.csv") +const BENCHMARK_REPS = 10 +const PROFILE_REPS = 50 + +# Load electron density +println("Loading electron density...") +pkl = Pickle.npyload(PKL_PATH) +grids = (Float64.(pkl["x"]), Float64.(pkl["y"]), Float64.(pkl["z"])) +rho = Float64.(pkl["variables"]["density_scf"]) +println("Grid: $(size(rho)), ρ ∈ [$(minimum(rho)), $(maximum(rho))]") + +# Load CSV query points +# Load CSV query points +raw = readdlm(CSV_PATH, ',', skipstart = 1) +qx = Float64.(raw[:, 2]) # x_bohr +qy = Float64.(raw[:, 3]) # y_bohr +qz = Float64.(raw[:, 4]) # z_bohr +query_points = [(qx[i], qy[i], qz[i]) for i in 1:length(qx)] + +println("Building PHS interpolant...") +phs_itp = FastInterpolations.phs_interp( + grids, rho; + stencil_size = 8, + degree = 3 +) + +println("\n" * "="^60) +println("PROFILING: DENSITY (ρ) EVALUATION") +println("="^60) + +# Pre-allocate result arrays +ρ_phs = zeros(length(qx)) +D0 = FastInterpolations.DerivOp{0}() + +# Warmup +phs_itp(ρ_phs, (qx, qy, qz)) + +Profile.clear() +@profile for _ in 1:PROFILE_REPS + phs_itp(ρ_phs, (qx, qy, qz)) +end + +println("\nTop functions by sample count:") +Profile.print(format = :flat, maxdepth = 30, mincount = 20) + +println("\n" * "="^60) +println("PROFILING: LAPLACIAN (|βˆ‡Β²Ο|) EVALUATION") +println("="^60) + +# Pre-allocate result arrays +_gx = zeros(length(qx)) +_gy = zeros(length(qx)) +_gz = zeros(length(qx)) +D1 = FastInterpolations.DerivOp{1}() +D2 = FastInterpolations.DerivOp{2}() + +# Warmup +phs_itp(_gx, (qx, qy, qz); deriv = (D2, D0, D0)) +phs_itp(_gy, (qx, qy, qz); deriv = (D0, D2, D0)) +phs_itp(_gz, (qx, qy, qz); deriv = (D0, D0, D2)) + +Profile.clear() +@profile for _ in 1:PROFILE_REPS + phs_itp(_gx, (qx, qy, qz); deriv = (D2, D0, D0)) + phs_itp(_gy, (qx, qy, qz); deriv = (D0, D2, D0)) + phs_itp(_gz, (qx, qy, qz); deriv = (D0, D0, D2)) +end + +println("\nTop functions by sample count:") +Profile.print(format = :flat, maxdepth = 30, mincount = 20) + +println("\nProfiler run complete.") diff --git a/scripts/phs/stencil_size_test.jl b/scripts/phs/stencil_size_test.jl new file mode 100644 index 000000000..927e284bb --- /dev/null +++ b/scripts/phs/stencil_size_test.jl @@ -0,0 +1,157 @@ +#= Stencil Size Optimization Study =# + +using FastInterpolations +using Printf +using Statistics + +# Create synthetic 3D test data +x = range(0, 10, 40) +y = range(0, 10, 40) +z = range(0, 10, 40) + +# Smooth synthetic function: exp(-rΒ²/20) +data = [exp(-((xi - 5)^2 + (yi - 5)^2 + (zi - 5)^2) / 20.0) for xi in x, yi in y, zi in z] + +# Generate test queries - line cut +test_queries = [ + (Float64(xi), 5.0, 5.0) for xi in range(2, 8, 20) +] + +# Compute reference values +function reference(x, y, z) + return exp(-((x - 5)^2 + (y - 5)^2 + (z - 5)^2) / 20.0) +end + +ref_values = [reference(q...) for q in test_queries] + +println("="^70) +println("STENCIL SIZE OPTIMIZATION - SYNTHETIC TEST") +println("="^70) +println("Grid size: $(length(x)) Γ— $(length(y)) Γ— $(length(z))") +println("Test points: $(length(test_queries))") +println() + +# Test different stencil sizes +stencil_sizes = [3, 4, 5, 6, 7, 8, 10] +results = Dict{Int, Any}() + +# Warm up +itp = phs_interp((x, y, z), data; stencil_size = 8, degree = 3, blend_factor = 1.0) +out = Vector{Float64}(undef, length(test_queries)) +itp(out, test_queries) + +for ss in stencil_sizes + @printf "Testing stencil_size = %d ... " ss + flush(stdout) + + # Build interpolant + time_build = @elapsed itp = phs_interp((x, y, z), data; stencil_size = ss, degree = 3, blend_factor = 1.0) + + # Evaluate on test points + out = Vector{Float64}(undef, length(test_queries)) + time_eval = @elapsed itp(out, test_queries) + + # Compute errors + errors = abs.(out .- ref_values) + rel_errors = errors ./ (abs.(ref_values) .+ 1.0e-16) + + # Get stencil info + stencil_size_total = size(itp.phi_inv, 1) + + results[ss] = ( + time_eval = time_eval, + max_error = maximum(errors), + mean_error = mean(errors), + max_rel_error = maximum(rel_errors), + mean_rel_error = mean(rel_errors), + stencil_size_total = stencil_size_total, + ) + + @printf "%.3fms, %d total coeff, max_rel_err=%.2e\n" time_eval * 1000 stencil_size_total results[ss].max_rel_error +end + +# Print summary +println("\n" * "="^80) +println("SUMMARY TABLE") +println("="^80) + +# ASCII table (for terminal viewing) +println("\nSize | Total Coeff | Time(ms) | Max Rel Err | Speedup | Error Ratio") +println("-"^80) + +baseline_time = results[8].time_eval +baseline_err = results[8].max_rel_error + +for ss in stencil_sizes + r = results[ss] + speedup = baseline_time / r.time_eval + if speedup >= 1.1 + speedup_str = @sprintf("%.2fΓ—", speedup) + elseif speedup < 1.0 + speedup_str = @sprintf("%.2f×↓", 1 / speedup) + else + speedup_str = "baseline" + end + err_ratio = r.max_rel_error / baseline_err + @printf "%4d | %11d | %8.2f | %11.2e | %8s | %12.2fΓ—\n" ss r.stencil_size_total r.time_eval * 1000 r.max_rel_error speedup_str err_ratio +end + +# Markdown table +println("\n" * "="^80) +println("MARKDOWN TABLE") +println("="^80) +println() +println("| stencil_size | Total Coeff | Time(ms) | Max Rel Err | Speedup | Error Ratio |") +println("|---|---|---|---|---|---|") +for ss in stencil_sizes + r = results[ss] + speedup = baseline_time / r.time_eval + if speedup >= 1.1 + speedup_str = @sprintf("%.2fΓ—", speedup) + elseif speedup < 1.0 + speedup_str = @sprintf("%.2f×↓", 1 / speedup) + else + speedup_str = "baseline" + end + err_ratio = r.max_rel_error / baseline_err + @printf "| %d | %d | %.2f | %.2e | %s | %.2fΓ— |\n" ss r.stencil_size_total r.time_eval * 1000 r.max_rel_error speedup_str err_ratio +end +println() +println("\nAnalysis:") + +# Find optimal stencil_size (best speed/accuracy trade-off) +min_ss_for_accuracy = nothing + +for ss in stencil_sizes[1:(end - 1)] + r = results[ss] + speedup = baseline_time / r.time_eval + err_ratio = r.max_rel_error / baseline_err + + if err_ratio < 1.5 && speedup > 1.3 + speedup_pct = (1 - 1 / speedup) * 100 + err_increase = (err_ratio - 1) * 100 + println("βœ“ stencil_size=$(ss): $(round(speedup_pct, digits = 1))% faster, max error $(round(err_increase, digits = 0))% larger ($(round(err_ratio, digits = 2))Γ— relative)") + println(" β†’ RECOMMENDED for high-performance use cases") + break + end +end + +# Check if smaller stencils can match accuracy +for ss in stencil_sizes + r = results[ss] + if r.max_rel_error < baseline_err * 1.1 # Within 10% of ss=8 + min_ss_for_accuracy = ss + break + end +end + +if min_ss_for_accuracy !== nothing && min_ss_for_accuracy < 8 + speedup = baseline_time / results[min_ss_for_accuracy].time_eval + err_ratio = results[min_ss_for_accuracy].max_rel_error / baseline_err + err_increase = (err_ratio - 1) * 100 + speedup_pct = (1 - 1 / speedup) * 100 + println("\nβœ“ stencil_size=$(min_ss_for_accuracy): achieves similar accuracy ($(round(err_increase, digits = 0))% error increase) while being $(round(speedup_pct, digits = 1))% faster") + println(" β†’ GOOD BALANCE for production") +end + +println("\nβœ“ Default stencil_size=8 provides excellent accuracy. Smaller sizes trade significant accuracy for moderate speedup.") diff --git a/src/FastInterpolations.jl b/src/FastInterpolations.jl index 3d1bb16a4..9b98683d1 100644 --- a/src/FastInterpolations.jl +++ b/src/FastInterpolations.jl @@ -20,6 +20,7 @@ include("akima/akima.jl") include("core/coeff_policy.jl") # AutoCoeffs resolution (after method types + strategy types, before hetero) include("hetero/hetero.jl") include("hetero/local_hermite_nd_forward.jl") # pchip/cardinal/akima ND forwarders to `interp` +include("phs/phs.jl") # Derivative view wrapper (depends on all interpolant types) include("derivative_view.jl") @@ -66,6 +67,7 @@ export cardinal_adjoint, CardinalAdjoint1D # Cardinal adjoint (slope-from-data) export akima_adjoint, AkimaAdjoint1D # Akima adjoint (slope-from-data, data-dependent) export CubicInterpolantND, AbstractCoeffStrategy, PreCompute, OnTheFly, AutoCoeffs # ND cubic types + strategies export interp, interp!, HeteroInterpolantND # Tensor product ND (per-axis methods) +export phs_interp, phs_interp!, PHSInterpolantND, PHSLogTransform, ConstantRef # Polyharmonic spline ND export AbstractInterpMethod, CubicInterp, LinearInterp, QuadraticInterp, ConstantInterp, NoInterp export PchipInterp, CardinalInterp, AkimaInterp, CubicHermiteInterp # Hermite family ND methods export GridIdx diff --git a/src/phs/phs.jl b/src/phs/phs.jl new file mode 100644 index 000000000..35fb4c838 --- /dev/null +++ b/src/phs/phs.jl @@ -0,0 +1,18 @@ +# ======================================== +# PHS β€” Include Aggregator +# ======================================== +# +# Load order: +# kernels β€” pure math, no deps +# stencil β€” Ξ¦ matrix construction + unique-stencil precomputation +# types β€” PHSInterpolantND + PHSLogTransform struct definitions +# eval β€” evaluation engine (base-node lookup, stencil eval, blending) +# interp β€” constructor (phs_interp) + callable overloads +# oneshot β€” one-shot public API (phs_interp with 3-arg + phs_interp!) + +include("phs_kernels.jl") +include("phs_stencil.jl") +include("phs_types.jl") +include("phs_eval.jl") +include("phs_interpolant.jl") +include("phs_oneshot.jl") diff --git a/src/phs/phs_eval.jl b/src/phs/phs_eval.jl new file mode 100644 index 000000000..3767e33cb --- /dev/null +++ b/src/phs/phs_eval.jl @@ -0,0 +1,1733 @@ +# ======================================== +# PHS Evaluation Engine +# ======================================== +# +# Core evaluation functions for PHSInterpolantND. +# Three layers: +# 1. _phs_find_base_node β€” nearest grid node to query point +# 2. _phs_eval_stencil β€” evaluate one local PHS interpolant at a query point +# 3. _phs_eval_blended β€” weighted blend of multiple local interpolants (CΒ² output) +# +# All mutable scratch (rhs, coeffs) is acquired from task-local +# AdaptiveArrayPools (via @with_pool) so the hot path is zero-allocation. + +# ====================================================== +# Stencil Coefficient Cache (per-thread) +# ====================================================== +# +# Per-thread cache of precomputed stencil coefficient vectors. +# Key: NTuple{N,Int} β€” blend-neighbour base-node grid index +# Value: Vector{Tg} β€” coeff = Φ⁻¹ Β· rhs (length M = stencil_size^N + n_poly) +# +# Coefficients depend only on `itp.data` (fixed after construction) and the +# stencil geometry (also fixed), so they are safe to cache indefinitely. +# Thread safety is implicit: each thread has its own Dict in `itp.coeff_caches`, +# selected by Threads.threadid() β€” no locks needed. +# +# Each PHSInterpolantND owns its own coeff_caches field, so multiple instances +# co-exist without interference. The cache is bounded to _PHS_COEFF_CACHE_MAX +# entries per thread to prevent unbounded memory use over large 3D grids. + +const _PHS_COEFF_CACHE_MAX = 5_000 # β‰ˆ 20 MB for 516-coeff Float64 stencils + +@inline function _phs_get_coeff_cache( + itp::PHSInterpolantND{Tg, Tv, N, K}, + ) where {Tg, Tv, N, K} + tid = Threads.threadid() + # Fallback if somehow more threads were spawned than existed at creation time + if tid > length(itp.coeff_caches) + return Dict{NTuple{N, Int}, Vector{Tg}}() + end + return itp.coeff_caches[tid]::Dict{NTuple{N, Int}, Vector{Tg}} +end + +# ====================================================== +# Layer 1: Base-node lookup +# ====================================================== + +""" + _phs_find_base_node(itp::PHSInterpolantND{Tg,Tv,N,K}, query::NTuple{N,<:Real}) + -> NTuple{N,Int} + +Find the grid node nearest to `query` in each dimension. +O(1) per axis for uniform (ScalarSpacing) grids; O(log n) for non-uniform. +""" +@inline function _phs_find_base_node( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ) where {Tg, Tv, N, K} + return ntuple(N) do d + grid = itp.grids[d] + n = length(grid) + qd = Tg(query[d]) + if grid isa _CachedRange + idx = round(Int, (qd - grid.lo) * grid.inv_h + 1) + else + # Binary search for nearest + lo, hi = 1, n + while hi - lo > 1 + mid = (lo + hi) >> 1 + grid[mid] <= qd ? (lo = mid) : (hi = mid) + end + # lo and hi are the two bracketing nodes; pick nearest + idx = abs(qd - grid[lo]) <= abs(qd - grid[hi]) ? lo : hi + end + clamp(idx, 1, n) + end +end + +""" + _phs_base_coords(itp, base_idx) -> NTuple{N,Tg} + +Physical coordinates of the base node at `base_idx`. +""" +@inline function _phs_base_coords( + itp::PHSInterpolantND{Tg, Tv, N}, + base_idx::NTuple{N, Int}, + ) where {Tg, Tv, N} + return ntuple(d -> itp.grids[d][base_idx[d]], N) +end + +# ====================================================== +# Layer 2: Single-stencil evaluation +# ====================================================== + +""" + _phs_build_rhs!(rhs, data, base_idx, offsets, grid_sizes) + +Fill `rhs[1:N_stencil]` with the data values at the stencil nodes +(clamped to [1,n] per axis). `rhs[N_stencil+1:end]` remain zero +(the polynomial consistency constraints). +""" +@inline function _phs_build_rhs!( + rhs::AbstractVector, + data::AbstractArray{Tv, N}, + base_idx::NTuple{N, Int}, + offsets::Vector{<:NTuple{N, Int}}, + grid_sizes::NTuple{N, Int}, + ) where {Tv, N} + @inbounds for (i, off) in enumerate(offsets) + cidx = ntuple(d -> clamp(base_idx[d] + off[d], 1, grid_sizes[d]), N) + rhs[i] = data[CartesianIndex(cidx)] + end + return rhs +end + +# ---- Evaluation dispatch on DerivOp ---- + +# (Note: _phs_diff has been moved to phs_kernels.jl as a compile-time generated/unrolled function) + +""" + _phs_eval_coeffs_value(coeffs, phys_offsets, query, base_coords, ::Val{K}) -> scalar + +Evaluate the PHS interpolant value at `query` given precomputed coefficients. + f = Ξ£α΅’ wα΅’ Ο†(rα΅’) + vβ‚€ + Ξ£β±Ό vβ±Ό (xβ±Ό - xbase_j) +""" +@inline function _phs_eval_coeffs_value( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ) where {Tv, Tg, N, K} + ns = length(phys_offsets) + y = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + # RBF sum + if K == 3 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + y += coeffs[i] * r2 * sqrt(r2) + end + else + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r = sqrt(_phs_sum_sq(xh)) + y += coeffs[i] * _phs_phi(r, Val{K}()) + end + end + + # Polynomial augmentation: all monomials up to degree (K-1)Γ·2 + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + y += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + return y +end + +""" + _phs_eval_coeffs_deriv1(coeffs, phys_offsets, query, base_coords, ::Val{K}, axis) -> scalar + +Evaluate βˆ‚f/βˆ‚xΞΎ (Eq. 25). + fΞΎ = Ξ£α΅’ wα΅’ Ο†'(rα΅’) (xΞΎ - xiΞΎ)/rα΅’ + vΞΎ +""" +@inline function _phs_eval_coeffs_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + axis::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_deriv1(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(axis)) +end + +@inline function _phs_eval_coeffs_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{axis}, + ) where {Tv, Tg, N, K, axis} + ns = length(phys_offsets) + y = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r = sqrt(_phs_sum_sq(xh)) + y += (3 * coeffs[i] * r) * xh[axis] + end + else + eps_tg = eps(Tg) + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r = sqrt(_phs_sum_sq(xh)) + r_inv = ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + ci = coeffs[i] + ci_fp_r_inv = ci * _phs_phi_prime(r, Val{K}()) * r_inv + y += ci_fp_r_inv * xh[axis] + end + end + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + y += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(axis)) + return y +end + +""" + _phs_eval_coeffs_deriv2(coeffs, phys_offsets, query, base_coords, ::Val{K}, ax1, ax2) -> scalar + +Evaluate βˆ‚Β²f/βˆ‚xΞΎβˆ‚xΞΆ (Eq. 26). +""" +@inline function _phs_eval_coeffs_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ax1::Int, + ax2::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_deriv2(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(ax1), Val(ax2)) +end + +@inline function _phs_eval_coeffs_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{ax1}, + ::Val{ax2}, + ) where {Tv, Tg, N, K, ax1, ax2} + ns = length(phys_offsets) + y = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + eps2 = eps(Tg)^2 + if ax1 == ax2 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci_3r = 3 * coeffs[i] * r + factor = xh[ax1] * xh[ax1] * r2_inv + y += ci_3r * (one(Tg) + factor) + end + else + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci_3r = 3 * coeffs[i] * r + factor = xh[ax1] * xh[ax2] * r2_inv + y += ci_3r * factor + end + end + else + eps2 = eps(Tg)^2 + if ax1 == ax2 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci = coeffs[i] + ci_fp_r_inv = ci * fp * r_inv + factor = xh[ax1] * xh[ax1] * r2_inv + y += ci * fpp * factor + ci_fp_r_inv * (one(Tg) - factor) + end + else + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci = coeffs[i] + ci_fp_r_inv = ci * fp * r_inv + factor = xh[ax1] * xh[ax2] * r2_inv + y += (ci * fpp - ci_fp_r_inv) * factor + end + end + end + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + y += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(ax1), Val(ax2)) + return y +end + +""" + _phs_eval_coeffs_value_and_deriv1(coeffs, phys_offsets, query, base_coords, ::Val{K}, axis) + -> (value, deriv1) + +Fused single-pass evaluation of both the interpolant value and its first derivative +along `axis`. Avoids traversing the stencil twice when both are needed (gradient blending). +""" +@inline function _phs_eval_coeffs_value_and_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + axis::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_value_and_deriv1(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(axis)) +end + +@inline function _phs_eval_coeffs_value_and_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{axis}, + ) where {Tv, Tg, N, K, axis} + ns = length(phys_offsets) + yv = zero(Tv) + yd = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + yv += ci_r * r2 + yd += ci_3r * xh[axis] + end + else + eps_tg = eps(Tg) + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + r_inv = ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + ci_fp_r_inv = ci * _phs_phi_prime(r, Val{K}()) * r_inv + yv += ci * _phs_phi(r, Val{K}()) + yd += ci_fp_r_inv * xh[axis] + end + end + + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(axis)) + return yv, yd +end + +""" + _phs_eval_coeffs_value_and_deriv1_and_deriv2(coeffs, phys_offsets, query, base_coords, + ::Val{K}, ax1, ax2) -> (value, deriv1_ax1, deriv2) + +Fused single-pass evaluation of value, βˆ‚f/βˆ‚x_{ax1}, and βˆ‚Β²f/βˆ‚x_{ax1}βˆ‚x_{ax2}. +Used for Hessian blending (diagonal and mixed second derivatives). +For diagonal (ax1==ax2): returns (f, f_ΞΎ, f_ΞΎΞΎ). +For mixed (ax1β‰ ax2): returns (f, f_ΞΎ, f_ΞΎΞΆ) where first-deriv is w.r.t. ax1. +""" +@inline function _phs_eval_coeffs_value_and_deriv1_and_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ax1::Int, + ax2::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_value_and_deriv1_and_deriv2(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(ax1), Val(ax2)) +end + +@inline function _phs_eval_coeffs_value_and_deriv1_and_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{ax1}, + ::Val{ax2}, + ) where {Tv, Tg, N, K, ax1, ax2} + ns = length(phys_offsets) + yv = zero(Tv) + yd1 = zero(Tv) + yd2 = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + eps2 = eps(Tg)^2 + if ax1 == ax2 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + yv += ci_r * r2 + yd1 += ci_3r * xh[ax1] + factor = xh[ax1] * xh[ax1] * r2_inv + yd2 += ci_3r * (one(Tg) + factor) + end + else + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + yv += ci_r * r2 + yd1 += ci_3r * xh[ax1] + factor = xh[ax1] * xh[ax2] * r2_inv + yd2 += ci_3r * factor + end + end + else + eps_tg = eps(Tg) + if ax1 == ax2 + @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + r_inv = ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci_fp_r_inv = ci * fp * r_inv + yv += ci * _phs_phi(r, Val{K}()) + yd1 += ci_fp_r_inv * xh[ax1] + factor = xh[ax1] * xh[ax1] * r2_inv + yd2 += ci * fpp * factor + ci_fp_r_inv * (one(Tg) - factor) + end + else + @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + r_inv = ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci_fp_r_inv = ci * fp * r_inv + yv += ci * _phs_phi(r, Val{K}()) + yd1 += ci_fp_r_inv * xh[ax1] + factor = xh[ax1] * xh[ax2] * r2_inv + yd2 += (ci * fpp - ci_fp_r_inv) * factor + end + end + end + + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd1 += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(ax1)) + yd2 += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(ax1), Val(ax2)) + return yv, yd1, yd2 +end + +""" + _phs_eval_coeffs_value_and_all_diag_deriv2(coeffs, phys_offsets, query, base_coords, ::Val{K}) + -> (value, deriv2_xx, deriv2_yy, deriv2_zz) + +Compute polynomial value and ALL THREE diagonal second derivatives (βˆ‚Β²/βˆ‚xΒ², βˆ‚Β²/βˆ‚yΒ², βˆ‚Β²/βˆ‚zΒ²) +in a SINGLE loop pass through the stencil. + +This is 3Γ— faster than calling _phs_eval_coeffs_value_and_deriv1_and_deriv2 three times, +reducing the inner loop from 3Γ—ns to 1Γ—ns iterations. + +Returns: + (value, deriv2_xx, deriv2_yy, deriv2_zz) +""" +@inline function _phs_eval_coeffs_value_and_all_diag_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ) where {Tv, Tg, N, K} + ns = length(phys_offsets) + yv = zero(Tv) + yd2_xx = zero(Tv) + yd2_yy = zero(Tv) + yd2_zz = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + eps2 = eps(Tg)^2 + # Single loop computing all diagonal second derivatives + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + + # Value: Ο•(r) = rΒ³ + yv += ci_r * r2 + + # Second derivatives for K=3: βˆ‚Β²Ο•/βˆ‚xΒ² = 3r + 9xΒ²/r + factor_xx = xh[1] * xh[1] * r2_inv + factor_yy = xh[2] * xh[2] * r2_inv + yd2_xx += ci_3r * (one(Tg) + factor_xx) + yd2_yy += ci_3r * (one(Tg) + factor_yy) + + if N >= 3 + factor_zz = xh[3] * xh[3] * r2_inv + yd2_zz += ci_3r * (one(Tg) + factor_zz) + end + end + # Polynomial contribution (constant for K=3, just offset) + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd2_xx += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(1), Val(1)) + yd2_yy += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(2), Val(2)) + if N >= 3 + yd2_zz += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(3), Val(3)) + end + else + eps_tg = eps(Tg) + # Single loop for general K + @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + r_inv = ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci_fp_r_inv = ci * fp * r_inv + + # Value + yv += ci * _phs_phi(r, Val{K}()) + + # Second derivatives: βˆ‚Β²Ο•/βˆ‚xΒ² = Ο•''(r) * (x/r)Β² + Ο•'(r)/r * (1 - (x/r)Β²) + factor_xx = xh[1] * xh[1] * r2_inv + factor_yy = xh[2] * xh[2] * r2_inv + yd2_xx += ci * fpp * factor_xx + ci_fp_r_inv * (one(Tg) - factor_xx) + yd2_yy += ci * fpp * factor_yy + ci_fp_r_inv * (one(Tg) - factor_yy) + + if N >= 3 + factor_zz = xh[3] * xh[3] * r2_inv + yd2_zz += ci * fpp * factor_zz + ci_fp_r_inv * (one(Tg) - factor_zz) + end + end + # Polynomial contribution + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd2_xx += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(1), Val(1)) + yd2_yy += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(2), Val(2)) + if N >= 3 + yd2_zz += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(3), Val(3)) + end + end + + return yv, yd2_xx, yd2_yy, yd2_zz +end + +""" + _phs_eval_coeffs_value_and_two_deriv1(coeffs, phys_offsets, query, base_coords, + ::Val{K}, ax1, ax2) -> (value, deriv1_ax1, deriv1_ax2) + +Fused single-pass evaluation of value, βˆ‚f/βˆ‚x_{ax1}, and βˆ‚f/βˆ‚x_{ax2}. +Used for mixed-Hessian blending to get both first-derivative components in one loop. +""" +@inline function _phs_eval_coeffs_value_and_two_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ax1::Int, + ax2::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_value_and_two_deriv1(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(ax1), Val(ax2)) +end + +@inline function _phs_eval_coeffs_value_and_two_deriv1( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{ax1}, + ::Val{ax2}, + ) where {Tv, Tg, N, K, ax1, ax2} + ns = length(phys_offsets) + yv = zero(Tv) + yd1 = zero(Tv) + yd2 = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + yv += ci_r * r2 + yd1 += ci_3r * xh[ax1] + yd2 += ci_3r * xh[ax2] + end + else + eps_tg = eps(Tg) + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + fp_r_inv = _phs_phi_prime(r, Val{K}()) * ifelse(r < eps_tg, zero(Tg), one(Tg) / r) + ci_fp_r_inv = ci * fp_r_inv + yv += ci * _phs_phi(r, Val{K}()) + yd1 += ci_fp_r_inv * xh[ax1] + yd2 += ci_fp_r_inv * xh[ax2] + end + end + + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd1 += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(ax1)) + yd2 += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(ax2)) + return yv, yd1, yd2 +end + +""" + _phs_eval_coeffs_value_and_two_deriv1_and_deriv2(coeffs, phys_offsets, query, base_coords, + ::Val{K}, ax1, ax2) -> (value, deriv1_ax1, deriv1_ax2, deriv2_ax1_ax2) + +Fused single-pass evaluation of value, βˆ‚f/βˆ‚x_{ax1}, βˆ‚f/βˆ‚x_{ax2}, and βˆ‚Β²f/βˆ‚x_{ax1}βˆ‚x_{ax2} +for the off-diagonal (ax1 β‰  ax2) mixed Hessian case. Replaces the previous two-pass approach +of calling `_phs_eval_coeffs_value_and_two_deriv1` followed by `_phs_eval_coeffs_deriv2`. +""" +@inline function _phs_eval_coeffs_value_and_two_deriv1_and_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ax1::Int, + ax2::Int, + ) where {Tv, Tg, N, K} + return _phs_eval_coeffs_value_and_two_deriv1_and_deriv2(coeffs, phys_offsets, query, base_coords, Val{K}(), Val(ax1), Val(ax2)) +end + +@inline function _phs_eval_coeffs_value_and_two_deriv1_and_deriv2( + coeffs::AbstractVector{Tv}, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ::Val{ax1}, + ::Val{ax2}, + ) where {Tv, Tg, N, K, ax1, ax2} + ns = length(phys_offsets) + yv = zero(Tv) + yd1 = zero(Tv) + yd2 = zero(Tv) + yd12 = zero(Tv) + + Ξ”x = ntuple(d -> Tg(query[d]) - base_coords[d], Val(N)) + + if K == 3 + eps2 = eps(Tg)^2 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + r2_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r2) + ci = coeffs[i] + ci_r = ci * r + ci_3r = 3 * ci_r + yv += ci_r * r2 + ci_3r_xh1 = ci_3r * xh[ax1] + yd1 += ci_3r_xh1 + yd2 += ci_3r * xh[ax2] + yd12 += ci_3r_xh1 * xh[ax2] * r2_inv + end + else + eps2 = eps(Tg)^2 + @fastmath @inbounds @simd for i in 1:ns + xh = _phs_diff_Ξ”(Ξ”x, phys_offsets[i]) + r2 = _phs_sum_sq(xh) + r = sqrt(r2) + ci = coeffs[i] + r_inv = ifelse(r2 < eps2, zero(Tg), one(Tg) / r) + r2_inv = r_inv * r_inv + fp = _phs_phi_prime(r, Val{K}()) + fpp = _phs_phi_dprime(r, Val{K}()) + ci_fp_r_inv = ci * fp * r_inv + yv += ci * _phs_phi(r, Val{K}()) + yd1 += ci_fp_r_inv * xh[ax1] + yd2 += ci_fp_r_inv * xh[ax2] + factor = xh[ax1] * xh[ax2] * r2_inv + yd12 += (ci * fpp - ci_fp_r_inv) * factor + end + end + + poly_exps = _phs_poly_exps_tuple(Val(N), Val(K)) + yv += _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) + yd1 += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(ax1)) + yd2 += _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(ax2)) + yd12 += _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(ax1), Val(ax2)) + return yv, yd1, yd2, yd12 +end + +""" + _phs_solve_stencil!(itp, base_idx, rhs_buf, coeff_buf) -> (offsets, coeff, hs_local) + +Perform the linear solve for the PHS stencil at `base_idx`: +selects offsets/Φ⁻¹, builds the RHS, and computes `coeff = Φ⁻¹ * rhs` (BLAS gemv). +Returns the stencil offsets, coefficient vector (aliasing `coeff_buf`), and grid spacings. +""" +@inline function _phs_solve_stencil!( + itp::PHSInterpolantND{Tg, Tv, N, K}, + base_idx::NTuple{N, Int}, + rhs_buf::Vr, + coeff_buf::Vc, + ) where {Tg, Tv, N, K, Vr <: AbstractVector, Vc <: AbstractVector} + hs_local = itp.hs + grid_sizes = ntuple(d -> length(itp.grids[d]), N) + shift = _phs_compute_shift(base_idx, itp.stencil_lo, itp.stencil_hi, grid_sizes) + offsets, phi_inv, phys_offsets = if all(iszero, shift) || !haskey(itp.shift_cache, shift) + itp.stencil_offsets, itp.phi_inv, itp.stencil_phys_offsets + else + val = itp.shift_cache[shift] + val[1], val[2], val[3] + end + # Check stencil coefficient cache (per-thread; zero-alloc on hit after warm-up). + # On cache hit, return cached vector directly β€” callers only read from it. + coeff_cache = _phs_get_coeff_cache(itp) + cached = get(coeff_cache, base_idx, nothing) + cached !== nothing && return offsets, phys_offsets, cached, hs_local + + # Cache miss: build RHS and solve via BLAS gemv. + M = size(phi_inv, 1) + actual_coeff = length(coeff_buf) == M ? coeff_buf : similar(coeff_buf, M) + actual_rhs = length(rhs_buf) == M ? rhs_buf : similar(rhs_buf, M) + ns = length(offsets) + @inbounds for i in (ns + 1):M + actual_rhs[i] = zero(Tg) + end # zero polynomial tail only + _phs_build_rhs!(actual_rhs, itp.data, base_idx, offsets, grid_sizes) + LinearAlgebra.mul!(actual_coeff, phi_inv, actual_rhs) + + # Store a copy in the cache (bounded to prevent unbounded memory growth). + if length(coeff_cache) < _PHS_COEFF_CACHE_MAX + coeff_cache[base_idx] = copy(actual_coeff) + end + + return offsets, phys_offsets, actual_coeff, hs_local +end + +""" + _phs_eval_from_coeffs(coeffs, phys_offsets, query, base_coords, ::Val{K}, ops) -> scalar + +Evaluate the local PHS interpolant given precomputed coefficients. +Dispatches to the appropriate `_phs_eval_coeffs_*` function based on `ops`. +""" +@inline function _phs_eval_from_coeffs( + coeffs::AbstractVector, + phys_offsets::Vector{<:NTuple{N, Tg}}, + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + ::Val{K}, + ops::O, + ) where {Tg, N, K, O <: Tuple{Vararg{AbstractEvalOp, N}}} + total_order = sum(deriv_order(ops[d]) for d in 1:N) + if total_order == 0 + return _phs_eval_coeffs_value(coeffs, phys_offsets, query, base_coords, Val{K}()) + elseif total_order == 1 + axis_val = _phs_get_deriv1_axis_val(O) + return _phs_eval_coeffs_deriv1(coeffs, phys_offsets, query, base_coords, Val{K}(), axis_val) + elseif total_order == 2 + ax1_val, ax2_val = _phs_get_deriv2_axes_val(O) + return _phs_eval_coeffs_deriv2(coeffs, phys_offsets, query, base_coords, Val{K}(), ax1_val, ax2_val) + end + return zero(eltype(coeffs)) +end + +""" + _phs_eval_stencil(itp, base_idx, query, ops, rhs_buf, coeff_buf) + -> scalar + +Evaluate the local PHS interpolant based at `base_idx` at point `query`. +`ops` is an `NTuple{N, DerivOp}` encoding which derivative to compute. + +Uses pre-allocated buffers `rhs_buf` and `coeff_buf` (from AdaptiveArrayPools). +""" +@inline function _phs_eval_stencil( + itp::PHSInterpolantND{Tg, Tv, N, K}, + base_idx::NTuple{N, Int}, + query::NTuple{N, <:Real}, + ops::O, + rhs_buf::Vr, + coeff_buf::Vc, + ) where {Tg, Tv, N, K, O <: Tuple{Vararg{AbstractEvalOp, N}}, Vr <: AbstractVector, Vc <: AbstractVector} + offsets, phys_offsets, coeff, hs_local = _phs_solve_stencil!(itp, base_idx, rhs_buf, coeff_buf) + base_coords = _phs_base_coords(itp, base_idx) + return _phs_eval_from_coeffs(coeff, phys_offsets, query, base_coords, Val{K}(), ops) +end + +# ====================================================== +# Layer 3: Blended evaluation +# ====================================================== + +""" + _phs_eval_blended(itp, query, ops) + -> scalar (or zero for out-of-domain) + +Evaluate the CΒ²-continuous blended PHS interpolant at `query`. + +Algorithm: + F = N/W where N = Ξ£ wα΅’ fα΅’, W = Ξ£ wα΅’ + Derivatives via exact quotient rule: + F' = (N' - FΒ·W') / W + F'' = (N'' - 2Β·N'Β·W'/W - FΒ·W'' + 2Β·FΒ·(W')Β²/W) / W +""" +@with_pool pool function _phs_eval_blended( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ops::O, + ) where {Tg, Tv, N, K, O <: Tuple{Vararg{AbstractEvalOp, N}}} + blend_a = itp.blend_a + blend_a3 = itp.blend_a3 + base_idx0 = _phs_find_base_node(itp, query) + r_idx = itp.blend_r_idx + grid_sizes = ntuple(d -> length(itp.grids[d]), N) + + total_deriv = sum(deriv_order(ops[d]) for d in 1:N) + + # Task-local scratch space (pool buffers) β€” M = max(phi_inv matrix size) across all stencils + M = size(itp.phi_inv, 1) + rhs_buf = acquire!(pool, Tg, M) + coeff_buf = acquire!(pool, Tg, M) + + lo_idx = ntuple(d -> max(1, base_idx0[d] - r_idx[d]), N) + hi_idx = ntuple(d -> min(grid_sizes[d], base_idx0[d] + r_idx[d]), N) + ranges = ntuple(d -> lo_idx[d]:hi_idx[d], N) + + ops_val = ntuple(_ -> EvalValue(), Val(N)) + + # ---------------------------------------------------------------- + # Branch 1: value F = N/W + # ---------------------------------------------------------------- + if total_deriv == 0 + sum_w = zero(Tg) + sum_wy = zero(Tv) + for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + w = _phs_blend_weight(sqrt(d2), blend_a, blend_a3) + w < eps(Tg) && continue + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + sum_w += w + sum_wy += w * f + end + sum_w < eps(Tg) && return zero(Tv) + return sum_wy / sum_w + + # ---------------------------------------------------------------- + # Branch 2: first derivative + # βˆ‚F/βˆ‚xΞΎ = (βˆ‚N/βˆ‚xΞΎ - FΒ·βˆ‚W/βˆ‚xΞΎ) / W + # βˆ‚N/βˆ‚xΞΎ = Ξ£(w'α΅’Β·dirΞΎΒ·fα΅’ + wα΅’Β·βˆ‚fα΅’/βˆ‚xΞΎ) + # βˆ‚W/βˆ‚xΞΎ = Ξ£ w'α΅’Β·dirΞΎ + # ---------------------------------------------------------------- + elseif total_deriv == 1 + grad_ax = findfirst(d -> deriv_order(ops[d]) == 1, 1:N)::Int + sum_w = zero(Tg) + sum_wy = zero(Tv) + sum_N1 = zero(Tv) # βˆ‚N/βˆ‚xΞΎ + sum_W1 = zero(Tg) # βˆ‚W/βˆ‚xΞΎ + for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp = _phs_blend_weight_and_prime(d_dist, blend_a, blend_a3) + w < eps(Tg) && continue + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + if d_dist > eps(Tg) + f, df = _phs_eval_coeffs_value_and_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(grad_ax)) + f = Tv(f) + df = Tv(df) + dir = (Tg(query[grad_ax]) - nb_coords[grad_ax]) / d_dist + sum_w += w + sum_wy += w * f + sum_N1 += wp * dir * f + w * df + sum_W1 += wp * dir + else + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + sum_w += w + sum_wy += w * f + end + end + sum_w < eps(Tg) && return zero(Tv) + F = sum_wy / sum_w + return Tv((sum_N1 - F * sum_W1) / sum_w) + + # ---------------------------------------------------------------- + # Branch 3: second (or mixed) derivative β€” full quotient rule + # F'' = (N'' - 2Β·N'Β·W'/W - FΒ·W'' + 2Β·FΒ·(W')Β²/W) / W [diagonal] + # or the mixed analogue for ax1 β‰  ax2 + # ---------------------------------------------------------------- + elseif total_deriv == 2 + n_deriv_arr = ntuple(d -> deriv_order(ops[d]), Val(N)) + ax1 = findfirst(d -> n_deriv_arr[d] > 0, 1:N)::Int + ax2_maybe = ax1 < N ? findnext(d -> n_deriv_arr[d] > 0, 1:N, ax1 + 1) : nothing + ax2 = ax2_maybe !== nothing ? ax2_maybe : ax1 + is_diag = (ax1 == ax2) + + sum_w = zero(Tg) + sum_wy = zero(Tv) + sum_N2 = zero(Tv) + sum_W2 = zero(Tg) # βˆ‚Β²N, βˆ‚Β²W w.r.t. the requested axes + sum_N1 = zero(Tv) + sum_W1 = zero(Tg) # βˆ‚N/βˆ‚x_ax1, βˆ‚W/βˆ‚x_ax1 + sum_N1b = zero(Tv) + sum_W1b = zero(Tg) # βˆ‚N/βˆ‚x_ax2, βˆ‚W/βˆ‚x_ax2 (mixed only) + + for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + + # L∞ early termination: Skip if neighbor is clearly outside blend radius + # This avoids expensive sqrt for ~30-50% of neighbors + l_inf_dist = zero(Tg) + @inbounds for dim in 1:N + Ξ” = abs(Tg(query[dim]) - nb_coords[dim]) + l_inf_dist = max(l_inf_dist, Ξ”) + # Early exit from L∞ computation if already beyond blend radius + l_inf_dist > blend_a && break + end + l_inf_dist > blend_a && continue + + # Compute full Euclidean distance (only for neighbors passing L∞ filter) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp, wpp = _phs_blend_weight_and_derivs(d_dist, blend_a, blend_a3) + w < eps(Tg) && continue + + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + + if d_dist > eps(Tg) + inv_d_dist = one(Tg) / d_dist + wp_inv = wp * inv_d_dist + dx1 = Tg(query[ax1]) - nb_coords[ax1] + da1 = dx1 * inv_d_dist + wxi1 = wp_inv * dx1 + + if is_diag + f, f1, f2 = _phs_eval_coeffs_value_and_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax1)) + f = Tv(f) + f1 = Tv(f1) + f2 = Tv(f2) + sum_w += w + sum_wy += w * f + sum_N1 += wxi1 * f + w * f1 + sum_W1 += wxi1 + da1_sqr = da1 * da1 + wxixi = da1_sqr * (wpp - wp_inv) + wp_inv + sum_N2 += wxixi * f + 2 * wxi1 * f1 + w * f2 + sum_W2 += wxixi + else + dx2 = Tg(query[ax2]) - nb_coords[ax2] + da2 = dx2 * inv_d_dist + wxi2 = wp_inv * dx2 + f, f1, f1b, f2 = _phs_eval_coeffs_value_and_two_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax2)) + f = Tv(f) + f1 = Tv(f1) + f1b = Tv(f1b) + f2 = Tv(f2) + sum_w += w + sum_wy += w * f + sum_N1 += wxi1 * f + w * f1 + sum_W1 += wxi1 + sum_N1b += wxi2 * f + w * f1b + sum_W1b += wxi2 + da1_da2 = da1 * da2 + wxixi = da1_da2 * (wpp - wp_inv) + sum_N2 += wxixi * f + wxi1 * f1b + wxi2 * f1 + w * f2 + sum_W2 += wxixi + end + else + # dβ‰ˆ0: blend-weight derivatives β‰ˆ 0, only stencil contribution survives + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + f2 = Tv(_phs_eval_coeffs_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax2))) + sum_w += w + sum_wy += w * f + sum_N2 += w * f2 + end + end + + sum_w < eps(Tg) && return zero(Tv) + W = sum_w + F = sum_wy / W + + if is_diag + # F'' = N''/W - 2Β·N'Β·W'/WΒ² - FΒ·W''/W + 2Β·FΒ·(W'/W)Β² + d2F = sum_N2 / W - 2 * sum_N1 * sum_W1 / W^2 - F * sum_W2 / W + 2 * F * (sum_W1 / W)^2 + else + # βˆ‚Β²F/βˆ‚x_ΞΎβˆ‚x_ΞΆ = N''_ΞΎΞΆ/W - (N'_ΞΎΒ·W'_ΞΆ + N'_ΞΆΒ·W'_ΞΎ)/WΒ² - FΒ·W''_ΞΎΞΆ/W + 2FΒ·W'_ΞΎΒ·W'_ΞΆ/WΒ² + d2F = sum_N2 / W - (sum_N1 * sum_W1b + sum_N1b * sum_W1) / W^2 - + F * sum_W2 / W + 2 * F * sum_W1 * sum_W1b / W^2 + end + return Tv(d2F) + end + + return zero(Tv) +end + +# ====================================================== +# Smoothing transform wrapper β€” exp-space blending +# ====================================================== +# +# The Fortran reference blends g_i(x) = exp(f_i(x)) across base nodes (where +# f_i is the PHS interpolant of log(ρ/ρ₀) at base node i), then multiplies by +# ρ₀. This is different from blending f_i and applying exp afterward. +# +# ρ̃(x) = ρ₀(x) Β· G(x) G = blend(exp(f_i)) (value, Eq. 21) +# βˆ‚ΟΜƒ/βˆ‚xΞΎ = βˆ‚Οβ‚€/βˆ‚xΞΎ Β· G + ρ₀ Β· βˆ‚G/βˆ‚xΞΎ (gradient, Leibniz) +# βˆ‚Β²ΟΜƒ/βˆ‚xΞΎβˆ‚xΞΆ = βˆ‚Β²Οβ‚€/βˆ‚xΞΎβˆ‚xΞΆΒ·G + βˆ‚Οβ‚€/βˆ‚xΞΎΒ·βˆ‚G/βˆ‚xΞΆ +# + βˆ‚Οβ‚€/βˆ‚xΞΆΒ·βˆ‚G/βˆ‚xΞΎ + Οβ‚€Β·βˆ‚Β²G/βˆ‚xΞΎβˆ‚xΞΆ (Hessian, Leibniz) +# +# Within the blend loop the chain rules for g_i = exp(f_i) are: +# dg_ΞΎ = g Β· f_ΞΎ +# d2g_ΞΎΞΎ = g Β· (f_ΞΎΞΎ + f_ΞΎΒ²) +# d2g_ΞΎΞΆ = g Β· (f_ΞΎΞΆ + f_ΞΎ Β· f_ΞΆ) (ΞΎ β‰  ΞΆ) + +""" + _phs_eval_blended_G(itp, query, ops, rhs_buf, coeff_buf) + +Evaluate the weighted blend of exp(f_i) (and its requested derivative) at `query`. +Identical accumulation structure to `_phs_eval_blended`, but replaces f_i with +g_i = exp(f_i) and propagates derivatives via the chain rule. +""" +@with_pool pool function _phs_eval_blended_G( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ops::O, + ) where {Tg, Tv, N, K, O <: Tuple{Vararg{AbstractEvalOp, N}}} + blend_a = itp.blend_a + blend_a3 = itp.blend_a3 + base_idx0 = _phs_find_base_node(itp, query) + r_idx = itp.blend_r_idx + grid_sizes = ntuple(d -> length(itp.grids[d]), N) + + total_deriv = sum(deriv_order(ops[d]) for d in 1:N) + + M = size(itp.phi_inv, 1) + rhs_buf = acquire!(pool, Tg, M) + coeff_buf = acquire!(pool, Tg, M) + + lo_idx = ntuple(d -> max(1, base_idx0[d] - r_idx[d]), N) + hi_idx = ntuple(d -> min(grid_sizes[d], base_idx0[d] + r_idx[d]), N) + ranges = ntuple(d -> lo_idx[d]:hi_idx[d], N) + + ops_val = ntuple(_ -> EvalValue(), Val(N)) + + # ---------------------------------------------------------------- + # Branch 1: G = Ξ£(wα΅’ Β· gα΅’) / Ξ£wα΅’ where gα΅’ = exp(fα΅’) + # ---------------------------------------------------------------- + if total_deriv == 0 + sum_w = zero(Tg) + sum_wg = zero(Tv) + @fastmath for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + w = _phs_blend_weight(sqrt(d2), blend_a, blend_a3) + w < eps(Tg) && continue + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + sum_w += w + sum_wg += w * exp(f) + end + sum_w < eps(Tg) && return zero(Tv) + return sum_wg / sum_w + + # ---------------------------------------------------------------- + # Branch 2: βˆ‚G/βˆ‚xΞΎ via quotient rule with gα΅’ = exp(fα΅’) + # N = Ξ£(wα΅’ Β· gα΅’), N_ΞΎ = Ξ£(w'α΅’ Β· dirΞΎ Β· gα΅’ + wα΅’ Β· gα΅’ Β· fα΅’_ΞΎ) + # ---------------------------------------------------------------- + elseif total_deriv == 1 + grad_ax = findfirst(d -> deriv_order(ops[d]) == 1, 1:N)::Int + sum_w = zero(Tg) + sum_wg = zero(Tv) + sum_N1 = zero(Tv) + sum_W1 = zero(Tg) + @fastmath for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp = _phs_blend_weight_and_prime(d_dist, blend_a, blend_a3) + w < eps(Tg) && continue + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + if d_dist > eps(Tg) + f, f_ΞΎ = _phs_eval_coeffs_value_and_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(grad_ax)) + f = Tv(f) + f_ΞΎ = Tv(f_ΞΎ) + g = exp(f) + wp_dir = (wp / d_dist) * (Tg(query[grad_ax]) - nb_coords[grad_ax]) + sum_w += w + sum_wg += w * g + sum_N1 += g * (wp_dir + w * f_ΞΎ) + sum_W1 += wp_dir + else + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + g = exp(f) + sum_w += w + sum_wg += w * g + end + end + sum_w < eps(Tg) && return zero(Tv) + G = sum_wg / sum_w + return Tv((sum_N1 - G * sum_W1) / sum_w) + + # ---------------------------------------------------------------- + # Branch 3: βˆ‚Β²G/βˆ‚xΞΎβˆ‚xΞΆ via quotient rule with gα΅’ = exp(fα΅’) + # diagonal (ΞΎ=ΞΆ): d2gα΅’ = gα΅’ Β· (fα΅’_ΞΎΞΎ + fα΅’_ΞΎΒ²) + # mixed (ΞΎβ‰ ΞΆ): d2gα΅’ = gα΅’ Β· (fα΅’_ΞΎΞΆ + fα΅’_ΞΎ Β· fα΅’_ΞΆ) + # ---------------------------------------------------------------- + elseif total_deriv == 2 + n_deriv_arr = ntuple(d -> deriv_order(ops[d]), Val(N)) + ax1 = findfirst(d -> n_deriv_arr[d] > 0, 1:N)::Int + ax2_maybe = ax1 < N ? findnext(d -> n_deriv_arr[d] > 0, 1:N, ax1 + 1) : nothing + ax2 = ax2_maybe !== nothing ? ax2_maybe : ax1 + is_diag = (ax1 == ax2) + + ops_d1_1 = ntuple(d -> d == ax1 ? EvalDeriv1() : EvalValue(), Val(N)) # kept for dβ‰ˆ0 branch + + sum_w = zero(Tg) + sum_wg = zero(Tv) + sum_N2 = zero(Tv) + sum_W2 = zero(Tg) + sum_N1 = zero(Tv) + sum_W1 = zero(Tg) + sum_N1b = zero(Tv) + sum_W1b = zero(Tg) + + @fastmath for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + + # L∞ early termination: Skip if neighbor is clearly outside blend radius + l_inf_dist = zero(Tg) + @inbounds for dim in 1:N + Ξ” = abs(Tg(query[dim]) - nb_coords[dim]) + l_inf_dist = max(l_inf_dist, Ξ”) + l_inf_dist > blend_a && break + end + l_inf_dist > blend_a && continue + + # Compute full Euclidean distance (only for neighbors passing L∞ filter) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp, wpp = _phs_blend_weight_and_derivs(d_dist, blend_a, blend_a3) + w < eps(Tg) && continue + + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + + if d_dist > eps(Tg) + inv_d_dist = one(Tg) / d_dist + wp_inv = wp * inv_d_dist + dx1 = Tg(query[ax1]) - nb_coords[ax1] + da1 = dx1 * inv_d_dist + wxi1 = wp_inv * dx1 + + if is_diag + f, f_d1, f_d2 = _phs_eval_coeffs_value_and_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax1)) + f = Tv(f) + f_d1 = Tv(f_d1) + f_d2 = Tv(f_d2) + g = exp(f) + dg1 = g * f_d1 + d2g = g * (f_d2 + f_d1 * f_d1) + da1_sqr = da1 * da1 + wxixi = da1_sqr * (wpp - wp_inv) + wp_inv + sum_w += w + sum_wg += w * g + sum_N1 += wxi1 * g + w * dg1 + sum_W1 += wxi1 + sum_N2 += wxixi * g + 2 * wxi1 * dg1 + w * d2g + sum_W2 += wxixi + else + dx2 = Tg(query[ax2]) - nb_coords[ax2] + da2 = dx2 * inv_d_dist + wxi2 = wp_inv * dx2 + f, f_d1, f_d1b, f_d2 = _phs_eval_coeffs_value_and_two_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax2)) + f = Tv(f) + f_d1 = Tv(f_d1) + f_d1b = Tv(f_d1b) + f_d2 = Tv(f_d2) + g = exp(f) + dg1 = g * f_d1 + dg2 = g * f_d1b + d2g = g * (f_d2 + f_d1 * f_d1b) + da1_da2 = da1 * da2 + wxixi = da1_da2 * (wpp - wp_inv) + sum_w += w + sum_wg += w * g + sum_N1 += wxi1 * g + w * dg1 + sum_W1 += wxi1 + sum_N1b += wxi2 * g + w * dg2 + sum_W1b += wxi2 + sum_N2 += wxixi * g + wxi1 * dg2 + wxi2 * dg1 + w * d2g + sum_W2 += wxixi + end + else + # dβ‰ˆ0: weight derivatives β‰ˆ 0 + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + g = exp(f) + f_d2 = Tv(_phs_eval_coeffs_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1), Val(ax2))) + # For the dβ‰ˆ0 diagonal case, f_d1 contributes to d2g; since it's rare, compute separately + f_d1_sq = is_diag ? Tv(_phs_eval_coeffs_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val(ax1)))^2 : zero(Tv) + d2g = g * (f_d2 + f_d1_sq) + sum_w += w + sum_wg += w * g + sum_N2 += w * d2g + end + end + + sum_w < eps(Tg) && return zero(Tv) + W = sum_w + G = sum_wg / W + + if is_diag + d2G = sum_N2 / W - 2 * sum_N1 * sum_W1 / W^2 - G * sum_W2 / W + 2 * G * (sum_W1 / W)^2 + else + d2G = sum_N2 / W - (sum_N1 * sum_W1b + sum_N1b * sum_W1) / W^2 - + G * sum_W2 / W + 2 * G * sum_W1 * sum_W1b / W^2 + end + return Tv(d2G) + end + + return zero(Tv) +end + +# ====================================================== +# Fused blend functions for _phs_eval_with_transform +# ====================================================== +# +# These replace 2–4 separate calls to _phs_eval_blended_G with a single +# pass over the blend neighbourhood, eliminating redundant stencil solves. +# For sequential batch queries (typical 1D paths), the stencil coefficient +# cache in _phs_solve_stencil! makes each neighbour a cheap copyto! instead +# of a full BLAS GEMV. + +# Static type-level helpers for resolving derivative axes from the Tuple type O: +@inline function _phs_get_deriv1_axis(::Type{O}) where {O <: Tuple} + for d in 1:fieldcount(O) + if fieldtype(O, d) <: DerivOp{1} + return d + end + end + return 1 +end + +@generated function _phs_get_deriv1_axis_val(::Type{O}) where {O <: Tuple} + for d in 1:fieldcount(O) + if fieldtype(O, d) <: DerivOp{1} + return :(Val{$d}()) + end + end + return :(Val{1}()) +end + +@inline function _phs_get_deriv2_axes(::Type{O}) where {O <: Tuple} + ax1 = 0 + ax2 = 0 + for d in 1:fieldcount(O) + T = fieldtype(O, d) + if T <: DerivOp{1} + if ax1 == 0 + ax1 = d + else + ax2 = d + end + elseif T <: DerivOp{2} + ax1 = d + ax2 = d + end + end + return ax1, ax2 +end + +@generated function _phs_get_deriv2_axes_val(::Type{O}) where {O <: Tuple} + ax1 = 0 + ax2 = 0 + for d in 1:fieldcount(O) + T = fieldtype(O, d) + if T <: DerivOp{1} + if ax1 == 0 + ax1 = d + else + ax2 = d + end + elseif T <: DerivOp{2} + ax1 = d + ax2 = d + end + end + return :(Val{$ax1}(), Val{$ax2}()) +end + +""" + _phs_eval_blended_G_with_grad(itp, query, grad_ax) -> (G, G_ΞΎ) + +Evaluate G = blend(exp(fα΅’)) and βˆ‚G/βˆ‚x_{grad_ax} in a single pass over the +blend neighbourhood, replacing the two separate `_phs_eval_blended_G` calls +that `_phs_eval_with_transform` previously made for gradient queries. +""" +@inline function _phs_eval_blended_G_with_grad( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + grad_ax::Int, + ) where {Tg, Tv, N, K} + return _phs_eval_blended_G_with_grad(itp, query, Val(grad_ax)) +end + +@with_pool pool function _phs_eval_blended_G_with_grad( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ::Val{grad_ax}, + ) where {Tg, Tv, N, K, grad_ax} + blend_a = itp.blend_a + blend_a3 = itp.blend_a3 + base_idx0 = _phs_find_base_node(itp, query) + r_idx = itp.blend_r_idx + grid_sizes = ntuple(d -> length(itp.grids[d]), N) + + M = size(itp.phi_inv, 1) + rhs_buf = acquire!(pool, Tg, M) + coeff_buf = acquire!(pool, Tg, M) + + lo_idx = ntuple(d -> max(1, base_idx0[d] - r_idx[d]), N) + hi_idx = ntuple(d -> min(grid_sizes[d], base_idx0[d] + r_idx[d]), N) + ranges = ntuple(d -> lo_idx[d]:hi_idx[d], N) + + sum_w = zero(Tg) + sum_wg = zero(Tv) + sum_N1 = zero(Tv) # βˆ‚N/βˆ‚x_{grad_ax} + sum_W1 = zero(Tg) # βˆ‚W/βˆ‚x_{grad_ax} + + @fastmath for nb_ci in CartesianIndices(ranges) + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + d2 = zero(Tg) + @inbounds for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp = _phs_blend_weight_and_prime(d_dist, blend_a, blend_a3) + w < eps(Tg) && continue + + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + + if d_dist > eps(Tg) + f, f_ΞΎ = _phs_eval_coeffs_value_and_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{grad_ax}()) + f = Tv(f) + f_ΞΎ = Tv(f_ΞΎ) + g = exp(f) + wp_dir = (wp / d_dist) * (Tg(query[grad_ax]) - nb_coords[grad_ax]) + sum_w += w + sum_wg += w * g + sum_N1 += g * (wp_dir + w * f_ΞΎ) + sum_W1 += wp_dir + else + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + g = exp(f) + sum_w += w + sum_wg += w * g + end + end + + sum_w < eps(Tg) && return zero(Tv), zero(Tv) + G = sum_wg / sum_w + G_ΞΎ = Tv((sum_N1 - G * sum_W1) / sum_w) + return G, G_ΞΎ +end + +""" + _phs_eval_blended_G_with_hess(itp, query, ax1, ax2) -> (G, G_ax1, G_ax2, G_ax1ax2) + +Evaluate G and its requested first- and second-order blend derivatives in a +single pass over the blend neighbourhood. For the diagonal case (ax1 == ax2), +G_ax2 == G_ax1. Replaces four separate `_phs_eval_blended_G` calls that +`_phs_eval_with_transform` previously made for Hessian queries. +""" +@inline function _phs_eval_blended_G_with_hess( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ax1::Int, + ax2::Int, + ) where {Tg, Tv, N, K} + return _phs_eval_blended_G_with_hess(itp, query, Val(ax1), Val(ax2)) +end + +@with_pool pool function _phs_eval_blended_G_with_hess( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ::Val{ax1}, + ::Val{ax2}, + ) where {Tg, Tv, N, K, ax1, ax2} + blend_a = itp.blend_a + blend_a3 = itp.blend_a3 + base_idx0 = _phs_find_base_node(itp, query) + r_idx = itp.blend_r_idx + grid_sizes = ntuple(d -> length(itp.grids[d]), N) + + M = size(itp.phi_inv, 1) + rhs_buf = acquire!(pool, Tg, M) + coeff_buf = acquire!(pool, Tg, M) + + lo_idx = ntuple(d -> max(1, base_idx0[d] - r_idx[d]), N) + hi_idx = ntuple(d -> min(grid_sizes[d], base_idx0[d] + r_idx[d]), N) + ranges = ntuple(d -> lo_idx[d]:hi_idx[d], N) + + is_diag = (ax1 == ax2) + + sum_w = zero(Tg) + sum_wg = zero(Tv) + sum_N2 = zero(Tv) + sum_W2 = zero(Tg) + sum_N1 = zero(Tv) + sum_W1 = zero(Tg) + sum_N1b = zero(Tv) + sum_W1b = zero(Tg) + + # Blend termination optimization: collect nodes, sort by weight, process top nodes + # Typical result: only 5-7 nodes needed (vs 27 total) to capture 90%+ accuracy + # Pre-allocate buffer from pool to avoid per-query heap allocations + + # Pre-allocate buffer for node data (reusable across threads via pool) + blend_node_buffer = acquire!(pool, Tuple{CartesianIndex{N}, Tg, Tg, Tg}, 27) + + # First pass: collect all nodes with their weights and derivatives + n_nodes = 0 + total_weight = zero(Tg) + + @fastmath for nb_ci in CartesianIndices(ranges) + nb_coords = _phs_base_coords(itp, Tuple(nb_ci)) + + # L∞ early termination: Skip if neighbor is clearly outside blend radius + l_inf_dist = zero(Tg) + @inbounds for dim in 1:N + Ξ” = abs(Tg(query[dim]) - nb_coords[dim]) + l_inf_dist = max(l_inf_dist, Ξ”) + l_inf_dist > blend_a && break + end + l_inf_dist > blend_a && continue + + # Compute full Euclidean distance (only for neighbors passing L∞ filter) + d2 = zero(Tg) + @inbounds @simd for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + w, wp, wpp = _phs_blend_weight_and_derivs(d_dist, blend_a, blend_a3) + + if w > eps(Tg) + n_nodes += 1 + blend_node_buffer[n_nodes] = (nb_ci, w, wp, wpp) + total_weight += w + end + end + + # Partial sort: find top ~7 nodes by weight (or fewer if n_nodes < 7) + # partialsort! rearranges so that the first k elements are the largest + n_to_process = min(7, n_nodes) + if n_nodes > 0 + partialsort!( + @view(blend_node_buffer[1:n_nodes]), 1:n_to_process, + by = x -> x[2], rev = true + ) + end + + # Second pass: process top nodes in weight order with early termination + weight_threshold = Tg(0.9) + weight_target = weight_threshold * total_weight + accumulated_weight = zero(Tg) + + @fastmath for i in 1:n_to_process + nb_ci, w, wp, wpp = blend_node_buffer[i] + nb_idx = Tuple(nb_ci) + nb_coords = _phs_base_coords(itp, nb_idx) + + accumulated_weight += w + + # Early termination: if we've accumulated 90% of the weight, we can stop + if accumulated_weight > weight_target && i > 3 + break + end + + # Recompute distance (needed for weight derivatives used in stencil evaluation) + d2 = zero(Tg) + @inbounds @simd for dim in 1:N + Ξ” = Tg(query[dim]) - nb_coords[dim] + d2 += Ξ” * Ξ” + end + d_dist = sqrt(d2) + + offsets_nb, phys_offsets, coeff_nb, hs_nb = _phs_solve_stencil!(itp, nb_idx, rhs_buf, coeff_buf) + + if d_dist > eps(Tg) + inv_d_dist = one(Tg) / d_dist + wp_inv = wp * inv_d_dist + dx1 = Tg(query[ax1]) - nb_coords[ax1] + da1 = dx1 * inv_d_dist + wxi1 = wp_inv * dx1 + if is_diag + f, f_d1, f_d2 = _phs_eval_coeffs_value_and_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax1}(), Val{ax1}()) + f = Tv(f) + f_d1 = Tv(f_d1) + f_d2 = Tv(f_d2) + g = exp(f) + dg1 = g * f_d1 + d2g = g * (f_d2 + f_d1 * f_d1) + da1_sqr = da1 * da1 + wxixi = da1_sqr * (wpp - wp_inv) + wp_inv + sum_w += w + sum_wg += w * g + sum_N1 += wxi1 * g + w * dg1 + sum_W1 += wxi1 + sum_N2 += wxixi * g + 2 * wxi1 * dg1 + w * d2g + sum_W2 += wxixi + else + dx2 = Tg(query[ax2]) - nb_coords[ax2] + da2 = dx2 * inv_d_dist + wxi2 = wp_inv * dx2 + f, f_d1, f_d1b, f_d2 = _phs_eval_coeffs_value_and_two_deriv1_and_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax1}(), Val{ax2}()) + f = Tv(f) + f_d1 = Tv(f_d1) + f_d1b = Tv(f_d1b) + f_d2 = Tv(f_d2) + g = exp(f) + dg1 = g * f_d1 + dg2 = g * f_d1b + d2g = g * (f_d2 + f_d1 * f_d1b) + da1_da2 = da1 * da2 + wxixi = da1_da2 * (wpp - wp_inv) + sum_w += w + sum_wg += w * g + sum_N1 += wxi1 * g + w * dg1 + sum_W1 += wxi1 + sum_N1b += wxi2 * g + w * dg2 + sum_W1b += wxi2 + sum_N2 += wxixi * g + wxi1 * dg2 + wxi2 * dg1 + w * d2g + sum_W2 += wxixi + end + else + # dβ‰ˆ0: weight prime/dprime β‰ˆ 0; only stencil contributions survive. + f = Tv(_phs_eval_coeffs_value(coeff_nb, phys_offsets, query, nb_coords, Val{K}())) + g = exp(f) + f_d1 = Tv(_phs_eval_coeffs_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax1}())) + if is_diag + f_d2 = Tv(_phs_eval_coeffs_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax1}(), Val{ax1}())) + d2g = g * (f_d2 + f_d1 * f_d1) + sum_w += w + sum_wg += w * g + sum_N1 += w * g * f_d1 + sum_N2 += w * d2g + else + f_d1b = Tv(_phs_eval_coeffs_deriv1(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax2}())) + f_d2 = Tv(_phs_eval_coeffs_deriv2(coeff_nb, phys_offsets, query, nb_coords, Val{K}(), Val{ax1}(), Val{ax2}())) + d2g = g * (f_d2 + f_d1 * f_d1b) + sum_w += w + sum_wg += w * g + sum_N1 += w * g * f_d1 + sum_N1b += w * g * f_d1b + sum_N2 += w * d2g + end + end + end + + sum_w < eps(Tg) && return zero(Tv), zero(Tv), zero(Tv), zero(Tv) + W = sum_w + G = sum_wg / W + G_ax1 = Tv((sum_N1 - G * sum_W1) / W) + if is_diag + G_ax2 = G_ax1 + d2G = sum_N2 / W - 2 * sum_N1 * sum_W1 / W^2 - G * sum_W2 / W + 2 * G * (sum_W1 / W)^2 + else + G_ax2 = Tv((sum_N1b - G * sum_W1b) / W) + d2G = sum_N2 / W - (sum_N1 * sum_W1b + sum_N1b * sum_W1) / W^2 - + G * sum_W2 / W + 2 * G * sum_W1 * sum_W1b / W^2 + end + return G, G_ax1, G_ax2, Tv(d2G) +end + +""" + _phs_eval_with_transform(itp, query, ops) + +Evaluate the PHS interpolant with the log-density smoothing transform. +Blends g_i = exp(f_i) across base nodes (matching the reference Fortran +implementation), then applies the Leibniz rule to recover ρ̃ = ρ₀ Β· G +and its derivatives. + + value: ρ̃ = ρ₀ Β· G + gradient: βˆ‚ΟΜƒ/βˆ‚xΞΎ = βˆ‚Οβ‚€/βˆ‚xΞΎ Β· G + ρ₀ Β· βˆ‚G/βˆ‚xΞΎ + Hessian: βˆ‚Β²ΟΜƒ/βˆ‚xΞΎβˆ‚xΞΆ = βˆ‚Β²Οβ‚€/βˆ‚xΞΎβˆ‚xΞΆ Β· G + βˆ‚Οβ‚€/βˆ‚xΞΎ Β· βˆ‚G/βˆ‚xΞΆ + + βˆ‚Οβ‚€/βˆ‚xΞΆ Β· βˆ‚G/βˆ‚xΞΎ + ρ₀ Β· βˆ‚Β²G/βˆ‚xΞΎβˆ‚xΞΆ +""" +@inline _phs_eval_ref(ref, query, ops) = ref(query; deriv = ops) + +@inline function _phs_eval_ref_deriv1(ref, query, ax::Int, ::Val{N}, ::Type{Tg}) where {N, Tg} + return _phs_eval_ref_deriv1(ref, query, Val(ax), Val(N), Tg) +end + +@inline function _phs_eval_ref_deriv1(ref, query, ::Val{ax}, ::Val{N}, ::Type{Tg}) where {N, Tg, ax} + ops = ntuple(d -> d == ax ? DerivOp{1}() : EvalValue(), Val(N)) + return Tg(_phs_eval_ref(ref, query, ops)) +end + +function _phs_eval_with_transform( + itp::PHSInterpolantND{Tg, Tv, N, K}, + query::NTuple{N, <:Real}, + ops::O, + ) where {Tg, Tv, N, K, O <: Tuple{Vararg{AbstractEvalOp, N}}} + total_deriv = sum(deriv_order(ops[d]) for d in 1:N) + ref = itp.transform.reference + + if total_deriv == 0 + ops_val = ntuple(_ -> EvalValue(), Val(N)) + G = Tg(_phs_eval_blended_G(itp, query, ops_val)) + rho0 = Tg(ref(query)) + return Tv(rho0 * G) + end + + if total_deriv == 1 + ax_val = _phs_get_deriv1_axis_val(O) + # Single fused pass: compute G and G_ΞΎ together + G, G_ΞΎ = _phs_eval_blended_G_with_grad(itp, query, ax_val) + rho0 = Tg(ref(query)) + rho0_ΞΎ = _phs_eval_ref_deriv1(ref, query, ax_val, Val(N), Tg) + return Tv(rho0_ΞΎ * G + rho0 * G_ΞΎ) + end + + if total_deriv == 2 + ax1_val, ax2_val = _phs_get_deriv2_axes_val(O) + + # Single fused pass: compute G, G_ΞΎ, G_ΞΆ (= G_ΞΎ for diagonal), G_ΞΎΞΆ together + G, G_ΞΎ, G_ΞΆ, G_ΞΎΞΆ = _phs_eval_blended_G_with_hess(itp, query, ax1_val, ax2_val) + + rho0 = Tg(ref(query)) + rho0_ΞΎ = _phs_eval_ref_deriv1(ref, query, ax1_val, Val(N), Tg) + rho0_ΞΎΞΆ = Tg(_phs_eval_ref(ref, query, ops)) + + if ax1_val === ax2_val + # Diagonal case: ρ̃_ΞΎΞΎ = ρ₀_ΞΎΞΎΒ·G + 2·ρ₀_ΞΎΒ·G_ΞΎ + ρ₀·G_ΞΎΞΎ + # (G_ΞΆ == G_ΞΎ and rho0_ΞΆ == rho0_ΞΎ β€” no extra blend call needed) + return Tv(rho0_ΞΎΞΆ * G + 2 * rho0_ΞΎ * G_ΞΎ + rho0 * G_ΞΎΞΆ) + else + rho0_ΞΆ = _phs_eval_ref_deriv1(ref, query, ax2_val, Val(N), Tg) + # Leibniz rule: ρ̃_ΞΎΞΆ = ρ₀_ΞΎΞΆΒ·G + ρ₀_ΞΎΒ·G_ΞΆ + ρ₀_ΞΆΒ·G_ΞΎ + ρ₀·G_ΞΎΞΆ + return Tv(rho0_ΞΎΞΆ * G + rho0_ΞΎ * G_ΞΆ + rho0_ΞΆ * G_ΞΎ + rho0 * G_ΞΎΞΆ) + end + end + + return zero(Tv) +end + +# ====================================================== +# Top-level dispatch: with or without transform +# ====================================================== +# Julia does not allow partial type-parameter specification in dispatch signatures +# (e.g. Foo{A,B,Nothing} when Foo has 9 params). Instead we dispatch via a +# two-argument helper that specialises on the transform field type, which the +# compiler will constant-fold since T is a type parameter of PHSInterpolantND. + +@inline _phs_eval_dispatch(itp, ::Nothing, query, ops) = _phs_eval_blended(itp, query, ops) +@inline _phs_eval_dispatch(itp, ::Any, query, ops) = _phs_eval_with_transform(itp, query, ops) + +@inline function _phs_eval( + itp::PHSInterpolantND, + query::NTuple{N, <:Real}, + ops::O, + ) where {N, O <: Tuple{Vararg{AbstractEvalOp, N}}} + return _phs_eval_dispatch(itp, itp.transform, query, ops) +end diff --git a/src/phs/phs_interpolant.jl b/src/phs/phs_interpolant.jl new file mode 100644 index 000000000..203d4c3b5 --- /dev/null +++ b/src/phs/phs_interpolant.jl @@ -0,0 +1,246 @@ +# ======================================== +# PHSInterpolantND β€” Constructor & Callables +# ======================================== + +# ====================================================== +# Helper: compute blend_a and blend_r_idx +# ====================================================== + +function _phs_blend_params(grids, blend_factor::Real) + N = length(grids) + Tg = eltype(first(grids)) + # Mean grid spacing per axis + h_max_per_axis = ntuple(N) do d + g = grids[d] + n = length(g) + Tg((last(g) - first(g)) / (n - 1)) + end + h_max = maximum(h_max_per_axis) + blend_a = Tg(blend_factor) * h_max + + # Half-width in index space per axis (ceiling so we cover blend_a) + blend_r_idx = ntuple(N) do d + h_d = h_max_per_axis[d] + h_d > zero(Tg) ? max(1, ceil(Int, blend_a / h_d)) : 1 + end + + return blend_a, blend_r_idx +end + +# ====================================================== +# Constructor +# ====================================================== + +""" + phs_interp(grids, data; kwargs...) -> PHSInterpolantND + +Create an N-dimensional polyharmonic spline interpolant. + +# Arguments +- `grids`: `NTuple{N, AbstractVector}` β€” one grid vector per dimension +- `data`: `AbstractArray{Tv, N}` β€” data values at grid nodes + +# Keyword Arguments +- `stencil_size::Int = 8`: + Number of stencil nodes per axis (total = stencil_size^N). + Reduce for high dimensions (e.g. 4 for Nβ‰₯4). +- `degree::Int = 3`: + PHS radial function degree (odd positive integer: 1, 3, 5, …). + Higher degree β†’ smoother interpolant, larger condition number. +- `blend_factor::Real = 1.0`: + Blend range = blend_factor Γ— max_grid_spacing. + Larger values β†’ wider blending neighbourhood β†’ smoother but more expensive. + Default 1.0 provides good balance (3Γ— faster than 2.0, ~2Γ— error increase). +- `extrap=NoExtrap()`: + Extrapolation mode (scalar or per-axis tuple). +- `search=AutoSearch()`: + Search policy (scalar or per-axis tuple; used for OOB checking). +- `reference_interp=nothing`: + If provided, enables the log-density smoothing transform: + `data` is stored as `log(ρ/ρ₀)` where ρ₀ values come from `reference_data` + (if given) or from evaluating `reference_interp` at each grid node. + Evaluation returns ρ̃ = ρ₀ * exp(f) with correct derivative transforms. +- `reference_data=nothing`: + Pre-computed ρ₀ values at all grid nodes (same shape as `data`). + When provided alongside `reference_interp`, avoids evaluating `reference_interp` + at every grid node during construction β€” useful when `reference_interp` is a + nested PHS (expensive per-node) but the raw ρ₀ array is already available. + Example: + ```julia + # itp_rho0 is a log-space PHS β€” accurate derivatives but slow to query 592K nodes + itp_rho0 = phs_interp(grids, rho0; reference_interp = ConstantRef(1.0)) + # Pass rho0 directly so construction stays O(NΒ·log N); eval uses itp_rho0 for βˆ‚Οβ‚€ + itp_phs = phs_interp(grids, rho; reference_interp = itp_rho0, + reference_data = rho0) + ``` + +# Returns +`PHSInterpolantND{Tg, Tv, N, degree}` β€” callable interpolant. + +# Examples +```julia +x = range(0.0, 1.0, 20) +y = range(0.0, 1.0, 20) +data = [sin(xi) * cos(yj) for xi in x, yj in y] + +itp = phs_interp((x, y), data) +itp((0.5, 0.3)) # scalar query +itp((0.5, 0.3); deriv=DerivOp(1, 0)) # βˆ‚f/βˆ‚x +itp(([0.1, 0.5, 0.9], [0.2, 0.4, 0.6])) # batch SoA +``` +""" +function phs_interp( + grids::NTuple{N, AbstractVector}, + data::AbstractArray{Tv_raw, N}; + stencil_size::Int = 8, + degree::Int = 3, + blend_factor::Real = 1.0, + extrap::Union{AbstractExtrap, NTuple{N, AbstractExtrap}} = NoExtrap(), + search::Union{AbstractSearchPolicy, NTuple{N, AbstractSearchPolicy}} = AutoSearch(), + reference_interp = nothing, + reference_data = nothing, + ) where {N, Tv_raw} + isodd(degree) && degree >= 1 || throw(ArgumentError("PHS degree must be odd and β‰₯ 1, got $degree")) + stencil_size >= 1 || throw(ArgumentError("stencil_size must be β‰₯ 1, got $stencil_size")) + + _validate_nd_grids(grids, data) + grids_typed, Tg, Tv, _ = _nd_promote_grids(grids, data) + data_typed = Tv === Tv_raw ? data : Tv.(data) + + grids_c = map(g -> _convert_copy(_cache_axis(g, NoBC(), Tg), Tg), grids_typed) + searches = _resolve_search_nd(search, Val(N)) + extrap_vals = _resolve_extrap(extrap, ntuple(_ -> NoBC(), N), Val(N), Tv) + + blend_a, blend_r_idx = _phs_blend_params(grids_c, blend_factor) + + # Build single canonical stencil + boundary shift cache + stencil_offsets, phi_inv, hs, stencil_lo, stencil_hi, shift_cache = + _phs_build_stencil(grids_c, stencil_size, degree) + + stencil_phys_offsets = [ntuple(d -> Tg(off[d]) * hs[d], Val(N)) for off in stencil_offsets] + + # Optionally apply log-density smoothing transform + transform, data_store = if reference_interp === nothing + nothing, Array{Tv}(data_typed) + else + # Determine ρ₀ at each grid node. + # `reference_data` lets the caller bypass per-node evaluation of `reference_interp` + # (useful when reference_interp is a nested PHS β€” expensive at 592K+ nodes). + rho0_nodes = if reference_data !== nothing + # Pre-computed ρ₀ array supplied β€” use directly (fast path) + size(reference_data) == size(data_typed) || + throw(DimensionMismatch("reference_data size $(size(reference_data)) must match data size $(size(data_typed))")) + Tv.(reference_data) + else + # Evaluate reference_interp at each grid node (may be slow for nested PHS) + Tv[reference_interp(ntuple(d -> grids_c[d][idx[d]], N)) for idx in CartesianIndices(size(data_typed))] + end + log_data = Array{Tv}(log.(data_typed ./ rho0_nodes)) + PHSLogTransform{N, typeof(reference_interp)}(reference_interp), log_data + end + + blend_a3 = blend_a^3 + # Use maxthreadid() to account for interactive thread pools + coeff_caches = Dict{NTuple{N, Int}, Vector{Tg}}[Dict{NTuple{N, Int}, Vector{Tg}}() for _ in 1:Threads.maxthreadid()] + return PHSInterpolantND{ + Tg, Tv, N, degree, + typeof(grids_c), typeof(transform), typeof(extrap_vals), typeof(searches), + }( + grids_c, data_store, + stencil_offsets, stencil_phys_offsets, phi_inv, stencil_lo, stencil_hi, shift_cache, hs, + blend_a, blend_a3, blend_r_idx, + transform, extrap_vals, searches, coeff_caches + ) +end + +# ====================================================== +# Callable interface +# ====================================================== + +# ---- Helpers ---- + +@inline function _phs_check_domain(itp::PHSInterpolantND{Tg, Tv, N}, query::NTuple{N, <:Real}) where {Tg, Tv, N} + return _validate_nd_domain(itp.grids, query, itp.extraps) +end + +@inline function _phs_resolve_ops( + deriv::Union{DerivOp, NTuple{N, DerivOp}}, + ::Val{N}, + ) where {N} + return _resolve_deriv_nd(deriv, Val(N)) +end + +# ---- Scalar query (NTuple) ---- + +""" + (itp::PHSInterpolantND)(query::NTuple{N,Real}; deriv=EvalValue()) -> scalar + +Evaluate the PHS interpolant at a single N-tuple query point. +""" +# Shared implementation β€” always receives concrete `ops` tuple, zero-alloc. +@inline function _phs_callable_impl( + itp::PHSInterpolantND{Tg, Tv, N}, + query::Tuple{Vararg{Real, N}}, + ops::NTuple{N, AbstractEvalOp}, + ) where {Tg, Tv, N} + _phs_check_domain(itp, query) + # Handle out-of-bounds (fills FillExtrap, etc.) + oob = _try_fill_oob(query, itp.grids, itp.extraps, ops, first(itp.data)) + oob !== nothing && return oob + return _phs_eval(itp, query, ops) +end + +# Single callable β€” Union{DerivOp, Tuple} is handled by Julia's union-splitting +# at the _phs_resolve_ops call site inside _phs_callable_impl. +@inline function (itp::PHSInterpolantND{Tg, Tv, N})( + query::Tuple{Vararg{Real, N}}; + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, N}}} = EvalValue(), + kw..., # absorb search/hint passed by AbstractInterpolantND protocol + ) where {Tg, Tv, N} + return _phs_callable_impl(itp, query, _phs_resolve_ops(deriv, Val(N))) +end + +""" + (itp::PHSInterpolantND)(out::AbstractVector, queries; deriv=EvalValue()) + +In-place batch evaluation. `queries` can be: + - `Tuple{Vararg{AbstractVector,N}}` (SoA) + - `AbstractVector{<:NTuple{N}}` or `AbstractVector{<:AbstractVector}` (AoS) + +Evaluates serially for maximum compute, memory, and allocation efficiency. +Per-thread caches (indexed by Threads.threadid()) and pool buffers make it safe to call from externally-threaded loops. +""" +# Shared batch implementation β€” receives concrete ops tuple. +function _phs_batch_impl!( + itp::PHSInterpolantND{Tg, Tv, N}, + out::AbstractVector, + queries, + ops::NTuple{N, AbstractEvalOp}, + ) where {Tg, Tv, N} + nq = _query_length(queries) + length(out) == nq || _throw_query_output_mismatch(nq, length(out)) + _query_validate(queries) + + @inbounds for k in 1:nq + q = _extract_query_point(queries, k, Val(N)) + oob = _try_fill_oob(q, itp.grids, itp.extraps, ops, first(itp.data)) + if oob !== nothing + out[k] = oob + else + out[k] = _phs_eval(itp, q, ops) + end + end + return out +end + +function (itp::PHSInterpolantND{Tg, Tv, N})( + out::AbstractVector, + queries::Union{Tuple{Vararg{AbstractVector, N}}, AbstractVector}; + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, N}}} = EvalValue(), + kw..., # absorb search/hint forwarded by AbstractInterpolantND protocol + ) where {Tg, Tv, N} + return _phs_batch_impl!(itp, out, queries, _phs_resolve_ops(deriv, Val(N))) +end + +# Allocating batch evaluation is handled by AbstractInterpolantND protocol, +# which forwards to our in-place callable above via dynamic dispatch. diff --git a/src/phs/phs_kernels.jl b/src/phs/phs_kernels.jl new file mode 100644 index 000000000..3d3a8563c --- /dev/null +++ b/src/phs/phs_kernels.jl @@ -0,0 +1,501 @@ +# ======================================== +# PHS Radial Kernels and Weight Functions +# ======================================== +# +# Pure math functions for polyharmonic spline interpolation. +# No dependencies. All @inline for zero-overhead inlining into hot loops. +# +# Three groups: +# A. Radial basis function Ο†(r) = r^K (odd K), Val{K}-dispatched +# B. Blend weight function w(d, a) with first and second derivatives +# C. Smoothing transform unrollers (Eqs. 21–23 from paper) + +# ╔══════════════════════════════════════╗ +# β•‘ Group A: Radial Basis Function β•‘ +# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +# +# Ο†(r) = r^K for odd positive integer K. +# Val{K} dispatch: compiler constant-folds the exponent (e.g. r^3 β†’ r*r*r). +# r = 0 guard: return zero to avoid NaN in derivatives. + +""" + _phs_phi(r, ::Val{K}) -> T + +Evaluate the polyharmonic radial basis function Ο†(r) = r^K. +Returns zero for r ≀ 0 (coincident points) to avoid NaN. +""" +@inline function _phs_phi(r::T, ::Val{K}) where {T, K} + r <= zero(T) && return zero(T) + return r^K +end +# Specializations for common odd degrees β€” explicit multiplications avoid the +# general pow path and enable better compiler optimisation. +# No r≀0 guard needed: for Kβ‰₯1 the multiplication chains evaluate to 0 at r=0, +# so removing the guard makes these functions branch-free and SIMD-friendly. +@inline _phs_phi(r::T, ::Val{1}) where {T} = r +@inline _phs_phi(r::T, ::Val{3}) where {T} = r * r * r +@inline function _phs_phi(r::T, ::Val{5}) where {T} + r2 = r * r + return r2 * r2 * r +end +@inline function _phs_phi(r::T, ::Val{7}) where {T} + r2 = r * r + r4 = r2 * r2 + return r4 * r2 * r +end + +""" + _phs_phi_prime(r, ::Val{K}) -> T + +First derivative Ο†'(r) = K * r^(K-1). +Returns zero for r ≀ 0. (Ο†'(r)/r β€” the quantity used downstream β€” is singular at +the origin for K=1, but w_i=0 there so the product w_i*Ο†'(r)/r is well-defined.) +""" +@inline function _phs_phi_prime(r::T, ::Val{K}) where {T, K} + r <= zero(T) && return zero(T) + return K * r^(K - 1) +end +@inline function _phs_phi_prime(r::T, ::Val{1}) where {T} + r <= zero(T) && return zero(T) + return one(T) +end +# No guard for K=3,5,7: 3*0Β²=0, 5*0⁴=0, 7*0⁢=0 β€” always correct at r=0. +@inline _phs_phi_prime(r::T, ::Val{3}) where {T} = 3 * r * r +@inline function _phs_phi_prime(r::T, ::Val{5}) where {T} + r2 = r * r + return 5 * r2 * r2 +end +@inline function _phs_phi_prime(r::T, ::Val{7}) where {T} + r2 = r * r + return 7 * r2 * r2 * r2 +end + +""" + _phs_phi_dprime(r, ::Val{K}) -> T + +Second derivative Ο†''(r) = K*(K-1) * r^(K-2). +Returns zero for r ≀ 0 and for K=1 (since K*(K-1)=0). +""" +@inline function _phs_phi_dprime(r::T, ::Val{K}) where {T, K} + (r <= zero(T) || K == 1) && return zero(T) + return K * (K - 1) * r^(K - 2) +end +@inline _phs_phi_dprime(r::T, ::Val{1}) where {T} = zero(T) +# No guard for K=3,5,7: 6*0=0, 20*0Β³=0, 42*0⁡=0 β€” always correct at r=0. +@inline _phs_phi_dprime(r::T, ::Val{3}) where {T} = 6 * r +@inline function _phs_phi_dprime(r::T, ::Val{5}) where {T} + return 20 * r * r * r +end +@inline function _phs_phi_dprime(r::T, ::Val{7}) where {T} + r2 = r * r + return 42 * r2 * r2 * r +end + +# Convenience scalar dispatch (runtime degree) β€” used during construction only +@inline _phs_phi(r, k::Int) = _phs_phi(r, Val(k)) +@inline _phs_phi_prime(r, k::Int) = _phs_phi_prime(r, Val(k)) +@inline _phs_phi_dprime(r, k::Int) = _phs_phi_dprime(r, Val(k)) + +# ╔══════════════════════════════════════╗ +# β•‘ Group B: Blend Weight Function β•‘ +# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +# +# The reference Fortran implementation (critic2 / grinterp_smr) uses a +# dimensionless (scale-invariant) form that differs from the printed paper +# Eq. 27. The Fortran comment explicitly states: +# "this version of weifun is different from the article. The argument +# of the exponential is adimensional in this version, and prevents +# problems with underflows in very fine grids." +# +# Fortran weifun (what actually produces the paper's figures): +# w(d, a) = exp( dΒ³ / (dΒ³ - aΒ³) ) for d < a +# = 0 for d β‰₯ a +# +# The argument dΒ³/(dΒ³-aΒ³) is dimensionless: it depends only on the ratio d/a, +# not on the physical scale of a. The paper formula exp(dΒ³/(aΒ³(dΒ³-aΒ³))) suffers +# from underflow/overflow when a is very small (fine grids) because the aΒ³ +# factor in the denominator can make the exponent enormous. +# +# Derivatives (let u = dΒ³/(dΒ³-aΒ³), so du/dd = -3aΒ³dΒ²/(dΒ³-aΒ³)Β²): +# w'(d) = -3aΒ³dΒ² w / (dΒ³ - aΒ³)Β² +# w''(d) = 3aΒ³d(-2a⁢ + aΒ³dΒ³ + 4d⁢) w / (dΒ³ - aΒ³)⁴ + +""" + _phs_blend_weight(d::T, a::T) -> T + +Evaluate the blend weight w(d, a) using the dimensionless Fortran formula +w = exp(dΒ³/(dΒ³-aΒ³)). Returns zero for d β‰₯ a. +""" +@inline function _phs_blend_weight(d::T, a::T) where {T} + d >= a && return zero(T) + d3 = d * d * d + a3 = a * a * a + return exp(d3 / (d3 - a3)) +end + +@inline function _phs_blend_weight(d::T, a::T, a3::T) where {T} + d >= a && return zero(T) + d3 = d * d * d + return exp(d3 / (d3 - a3)) +end + +""" + _phs_blend_weight_and_prime(d::T, a::T) -> (w, wp) + +Evaluate w and its first derivative w'(d) = -3aΒ³dΒ² w / (dΒ³ - aΒ³)Β² simultaneously. +""" +@inline function _phs_blend_weight_and_prime(d::T, a::T) where {T} + if d >= a + return zero(T), zero(T) + end + d2 = d * d + d3 = d2 * d + a3 = a * a * a + denom = d3 - a3 # negative (d < a) + denom2 = denom * denom + w = exp(d3 / denom) + wp = -3 * a3 * d2 * w / denom2 + return w, wp +end + +@inline function _phs_blend_weight_and_prime(d::T, a::T, a3::T) where {T} + if d >= a + return zero(T), zero(T) + end + d2 = d * d + d3 = d2 * d + denom = d3 - a3 + denom2 = denom * denom + w = exp(d3 / denom) + wp = -3 * a3 * d2 * w / denom2 + return w, wp +end + +""" + _phs_blend_weight_and_derivs(d::T, a::T) -> (w, wp, wpp) + +Evaluate w, w', and w'' simultaneously for use in second-derivative blending. + + w'(d) = -3aΒ³dΒ² w / (dΒ³ - aΒ³)Β² + w''(d) = 3aΒ³d(-2a⁢ + aΒ³dΒ³ + 4d⁢) w / (dΒ³ - aΒ³)⁴ +""" +@inline function _phs_blend_weight_and_derivs(d::T, a::T) where {T} + if d >= a + return zero(T), zero(T), zero(T) + end + d2 = d * d + d3 = d2 * d + a3 = a * a * a + denom = d3 - a3 + inv_denom = one(T) / denom + inv_denom2 = inv_denom * inv_denom + inv_denom4 = inv_denom2 * inv_denom2 + w = exp(d3 * inv_denom) + wp = -3 * a3 * d2 * w * inv_denom2 + wpp = 3 * a3 * d * (muladd(4 * d3, d3, muladd(a3, d3, -2 * a3 * a3))) * w * inv_denom4 + return w, wp, wpp +end + +@inline function _phs_blend_weight_and_derivs(d::T, a::T, a3::T) where {T} + if d >= a + return zero(T), zero(T), zero(T) + end + d2 = d * d + d3 = d2 * d + denom = d3 - a3 + inv_denom = one(T) / denom + inv_denom2 = inv_denom * inv_denom + inv_denom4 = inv_denom2 * inv_denom2 + w = exp(d3 * inv_denom) + wp = -3 * a3 * d2 * w * inv_denom2 + wpp = 3 * a3 * d * (muladd(4 * d3, d3, muladd(a3, d3, -2 * a3 * a3))) * w * inv_denom4 + return w, wp, wpp +end + +# ╔══════════════════════════════════════╗ +# β•‘ Group C: Smoothing Transform β•‘ +# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +# +# The log-density smoothing transformation f(x) = ln(ρ(x)/ρ₀(x)). +# Interpolation is performed on f, then the result is unrolled to ρ via: +# ρ̃ = ρ₀ * exp(f) [Eq. 21] +# ρ̃ξ = ρ̃ * (fΞΎ + ρ₀ξ/ρ₀) [Eq. 22] +# ρ̃ξ΢ = ρ̃ * (fΞΎΞΆ + ρ̃ξρ̃΢/ρ̃² + ρ₀ξ΢/ρ₀ - ρ₀ξρ₀΢/ρ₀²) [Eq. 23] + +""" + _phs_unroll_value(f::T, rho0::T) -> T + +Recover interpolated density from smooth function: ρ̃ = ρ₀ * exp(f). +""" +@inline function _phs_unroll_value(f::T, rho0::T) where {T} + (rho0 < 1.0e-40 || f > 100) && return zero(T) + return rho0 * exp(f) +end + +""" + _phs_unroll_grad_component(rho, f_grad_xi, rho0_grad_xi, rho0) -> T + +Eq. 22: ρ̃ξ = ρ̃ * (fΞΎ + ρ₀ξ/ρ₀). +Compute one component of the gradient of the recovered density. +""" +@inline function _phs_unroll_grad_component(rho::T, f_grad_xi::T, rho0_grad_xi::T, rho0::T) where {T} + rho0_safe = max(rho0, T(1.0e-40)) + return rho * (f_grad_xi + rho0_grad_xi / rho0_safe) +end + +""" + _phs_unroll_hess_component(rho, f_hess_xixj, rho_grad_xi, rho_grad_xj, rho0, rho0_grad_xi, rho0_grad_xj, rho0_hess_xixj) -> T + +Eq. 23: ρ̃ξ΢ = ρ̃ * (fΞΎΞΆ + ρ̃ξρ̃΢/ρ̃² + ρ₀ξ΢/ρ₀ - ρ₀ξρ₀΢/ρ₀²). +Compute one component (ΞΎ,ΞΆ) of the Hessian of the recovered density. +""" +@inline function _phs_unroll_hess_component( + rho::T, + f_hess_xixj::T, + rho_grad_xi::T, + rho_grad_xj::T, + rho0::T, + rho0_grad_xi::T, + rho0_grad_xj::T, + rho0_hess_xixj::T, + ) where {T} + rho_safe = max(rho, T(1.0e-40)) + rho0_safe = max(rho0, T(1.0e-40)) + return rho * ( + f_hess_xixj + + rho_grad_xi * rho_grad_xj / (rho_safe * rho_safe) + + rho0_hess_xixj / rho0_safe - + rho0_grad_xi * rho0_grad_xj / (rho0_safe * rho0_safe) + ) +end + +# ╔════════════════════════════════════════════╗ +# β•‘ Group D: Polynomial Augmentation Helpers β•‘ +# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +# +# For r^K PHS interpolation the polynomial augmentation must have degree +# at least poly_deg = (K-1)Γ·2: +# K=1 β†’ poly_deg=0 (constants only, 1 term in N dims) +# K=3 β†’ poly_deg=1 (linear, N+1 terms) +# K=5 β†’ poly_deg=2 (quadratic, C(N+2,2) terms) +# K=7 β†’ poly_deg=3 (cubic, C(N+3,3) terms) +# +# Monomial ordering: increasing total degree, last index varies fastest. +# N=2, poly_deg=2: (0,0),(0,1),(1,0),(0,2),(1,1),(2,0) + +""" + _phs_n_poly(N, poly_deg) -> Int + +Number of polynomial basis functions in N dimensions up to total degree `poly_deg`. +Equal to C(N+poly_deg, poly_deg). +""" +@inline _phs_n_poly(N::Int, poly_deg::Int) = binomial(N + poly_deg, poly_deg) + +""" + _phs_all_exponents(::Val{N}, poly_deg) -> Vector{NTuple{N,Int}} + +All exponent vectors for monomials of total degree ≀ `poly_deg` in N dimensions. +Called at stencil-construction time only (not the hot path). +""" +function _phs_all_exponents(::Val{N}, poly_deg::Int) where {N} + result = NTuple{N, Int}[] + current = zeros(Int, N - 1) # dims 1…N-1; dim N is `remaining` + function gen(d::Int, remaining::Int) + if d == N + push!(result, ntuple(i -> i < N ? current[i] : remaining, Val(N))) + return + end + for k in 0:remaining + current[d] = k + gen(d + 1, remaining - k) + end + return + end + for total in 0:poly_deg + gen(1, total) + end + return result +end + +""" + _phs_poly_exps_tuple(::Val{N}, ::Val{K}) + +Return a compile-time `NTuple` of `NTuple{N,Int}` exponents for the polynomial +augmentation of the r^K PHS interpolant (poly_deg = (K-1)Γ·2). +Generated at compile time β€” zero allocation, loops fully unrolled. +""" +@generated function _phs_poly_exps_tuple(::Val{N}, ::Val{K}) where {N, K} + m = (K - 1) Γ· 2 + exps = NTuple{N, Int}[] + current = zeros(Int, N - 1) + function gen(d, remaining) + if d == N + push!(exps, ntuple(i -> i < N ? current[i] : remaining, N)) + return + end + for k in 0:remaining + current[d] = k + gen(d + 1, remaining - k) + end + return + end + for total in 0:m + gen(1, total) + end + tup = Expr(:tuple, [Expr(:tuple, Ξ±...) for Ξ± in exps]...) + return :($tup) +end + +""" + _phs_eval_poly(Ξ”x, poly_exps, coeffs, ns) -> scalar + +Evaluate the polynomial augmentation: `Ξ£_k c[ns+k] Β· Ξ”x^Ξ±_k`. +`poly_exps` is a (compile-time) tuple of exponent NTuples from +`_phs_poly_exps_tuple`. +""" +@inline function _phs_eval_poly( + Ξ”x::NTuple{N, Tg}, + poly_exps::Tuple, + coeffs::AbstractVector{Tv}, + ns::Int, + ) where {N, Tg, Tv} + y = zero(Tv) + @inbounds for k in 1:length(poly_exps) + Ξ± = poly_exps[k] + mono = one(Tg) + for d in 1:N + Ξ±[d] != 0 && (mono *= Ξ”x[d]^Ξ±[d]) + end + y += coeffs[ns + k] * mono + end + return y +end + +""" + _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, axis) -> scalar + +Evaluate `βˆ‚/βˆ‚x_axis` of the polynomial augmentation. +""" +@inline function _phs_eval_poly_deriv1( + Ξ”x::NTuple{N, Tg}, + poly_exps::Tuple, + coeffs::AbstractVector{Tv}, + ns::Int, + axis::Int, + ) where {N, Tg, Tv} + return _phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, Val(axis)) +end + +@inline function _phs_eval_poly_deriv1( + Ξ”x::NTuple{N, Tg}, + poly_exps::Tuple, + coeffs::AbstractVector{Tv}, + ns::Int, + ::Val{axis}, + ) where {N, Tg, Tv, axis} + y = zero(Tv) + @inbounds for k in 1:length(poly_exps) + Ξ± = poly_exps[k] + Ξ±[axis] == 0 && continue + mono = Tg(Ξ±[axis]) + for d in 1:N + exp = d == axis ? Ξ±[d] - 1 : Ξ±[d] + exp != 0 && (mono *= Ξ”x[d]^exp) + end + y += coeffs[ns + k] * mono + end + return y +end + +""" + _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, ax1, ax2) -> scalar + +Evaluate `βˆ‚Β²/βˆ‚x_ax1 βˆ‚x_ax2` of the polynomial augmentation. +""" +@inline function _phs_eval_poly_deriv2( + Ξ”x::NTuple{N, Tg}, + poly_exps::Tuple, + coeffs::AbstractVector{Tv}, + ns::Int, + ax1::Int, + ax2::Int, + ) where {N, Tg, Tv} + return _phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, Val(ax1), Val(ax2)) +end + +@inline function _phs_eval_poly_deriv2( + Ξ”x::NTuple{N, Tg}, + poly_exps::Tuple, + coeffs::AbstractVector{Tv}, + ns::Int, + ::Val{ax1}, + ::Val{ax2}, + ) where {N, Tg, Tv, ax1, ax2} + y = zero(Tv) + @inbounds for k in 1:length(poly_exps) + Ξ± = poly_exps[k] + if ax1 == ax2 + Ξ±[ax1] < 2 && continue + mono = Tg(Ξ±[ax1] * (Ξ±[ax1] - 1)) + for d in 1:N + exp = d == ax1 ? Ξ±[d] - 2 : Ξ±[d] + exp != 0 && (mono *= Ξ”x[d]^exp) + end + y += coeffs[ns + k] * mono + else + (Ξ±[ax1] == 0 || Ξ±[ax2] == 0) && continue + mono = Tg(Ξ±[ax1] * Ξ±[ax2]) + for d in 1:N + exp = (d == ax1 || d == ax2) ? Ξ±[d] - 1 : Ξ±[d] + exp != 0 && (mono *= Ξ”x[d]^exp) + end + y += coeffs[ns + k] * mono + end + end + return y +end + +# ======================================== +# Generated/Unrolled Tuple Helpers for SIMD +# ======================================== + +""" + _phs_diff(query, base_coords, off, hs_local) -> NTuple{N, Tg} + +Compute physical distance vectors: (query - node_i). +Generated at compile-time for arbitrary dimensions to ensure full unrolling +and optimal SIMD register allocation. +""" +@generated function _phs_diff( + query::NTuple{N, <:Real}, + base_coords::NTuple{N, Tg}, + off::NTuple{N, Int}, + hs_local::NTuple{N, Tg}, + ) where {N, Tg} + exprs = [:(Tg(query[$d]) - (base_coords[$d] + Tg(off[$d]) * hs_local[$d])) for d in 1:N] + return Expr(:tuple, exprs...) +end + +""" + _phs_sum_sq(x) -> Tg + +Compile-time unrolled sum of squares for tuples. +""" +@generated function _phs_sum_sq(x::NTuple{N, T}) where {N, T} + exprs = [:(x[$d] * x[$d]) for d in 1:N] + return Expr(:call, :+, exprs...) +end + +""" + _phs_diff_Ξ”(Ξ”x, phys_off) -> NTuple{N, Tg} + +Compute physical distance vectors from physical coordinate difference Ξ”x (query - base_node) and precomputed physical offset. +Generated at compile-time for arbitrary dimensions to ensure full unrolling and optimal SIMD register allocation. +""" +@generated function _phs_diff_Ξ”( + Ξ”x::NTuple{N, Tg}, + phys_off::NTuple{N, Tg}, + ) where {N, Tg} + exprs = [:(Ξ”x[$d] - phys_off[$d]) for d in 1:N] + return Expr(:tuple, exprs...) +end diff --git a/src/phs/phs_oneshot.jl b/src/phs/phs_oneshot.jl new file mode 100644 index 000000000..bcff675f6 --- /dev/null +++ b/src/phs/phs_oneshot.jl @@ -0,0 +1,82 @@ +# ======================================== +# PHSInterpolantND β€” One-Shot API +# ======================================== +# +# One-shot forms: build a temporary PHSInterpolantND then evaluate. +# phi_inv must always be precomputed (cannot be bypassed for a single query), +# so these share the same construction cost as the persistent interpolant API. +# Use phs_interp(grids, data) when evaluating at many points. + +""" + phs_interp(grids, data, query::NTuple{N,Real}; kwargs...) -> scalar + +One-shot N-dimensional PHS interpolation at a single query point. + +See `phs_interp(grids, data)` for keyword argument documentation. +""" +function phs_interp( + grids::NTuple{N, AbstractVector}, + data::AbstractArray{Tv, N}, + query::Tuple{Vararg{Real, N}}; + stencil_size::Int = 8, + degree::Int = 3, + blend_factor::Real = 2.0, + extrap::Union{AbstractExtrap, NTuple{N, AbstractExtrap}} = NoExtrap(), + search::Union{AbstractSearchPolicy, NTuple{N, AbstractSearchPolicy}} = AutoSearch(), + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, N}}} = EvalValue(), + reference_interp = nothing, + reference_data = nothing, + ) where {Tv, N} + itp = phs_interp(grids, data; stencil_size, degree, blend_factor, extrap, search, reference_interp, reference_data) + return itp(query; deriv) +end + +""" + phs_interp(grids, data, queries; kwargs...) -> Vector + +One-shot N-dimensional PHS interpolation at a batch of query points. +`queries` is any query-protocol-compatible container (SoA tuple, AoS vector, etc.). + +Builds a temporary interpolant (same construction cost as `phs_interp(grids, data)`), +then allocates and fills the output vector. +""" +function phs_interp( + grids::NTuple{N, AbstractVector}, + data::AbstractArray{Tv, N}, + queries; + stencil_size::Int = 8, + degree::Int = 3, + blend_factor::Real = 2.0, + extrap::Union{AbstractExtrap, NTuple{N, AbstractExtrap}} = NoExtrap(), + search::Union{AbstractSearchPolicy, NTuple{N, AbstractSearchPolicy}} = AutoSearch(), + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, N}}} = EvalValue(), + reference_interp = nothing, + reference_data = nothing, + ) where {Tv, N} + itp = phs_interp(grids, data; stencil_size, degree, blend_factor, extrap, search, reference_interp, reference_data) + return itp(queries; deriv) +end + +""" + phs_interp!(out, grids, data, queries; kwargs...) + +In-place one-shot N-dimensional PHS interpolation. +Writes results into pre-allocated `out`. +""" +function phs_interp!( + out::AbstractVector, + grids::NTuple{N, AbstractVector}, + data::AbstractArray{Tv, N}, + queries; + stencil_size::Int = 8, + degree::Int = 3, + blend_factor::Real = 2.0, + extrap::Union{AbstractExtrap, NTuple{N, AbstractExtrap}} = NoExtrap(), + search::Union{AbstractSearchPolicy, NTuple{N, AbstractSearchPolicy}} = AutoSearch(), + deriv::Union{DerivOp, Tuple{Vararg{DerivOp, N}}} = EvalValue(), + reference_interp = nothing, + reference_data = nothing, + ) where {Tv, N} + itp = phs_interp(grids, data; stencil_size, degree, blend_factor, extrap, search, reference_interp, reference_data) + return itp(out, queries; deriv) +end diff --git a/src/phs/phs_stencil.jl b/src/phs/phs_stencil.jl new file mode 100644 index 000000000..cb7f26a40 --- /dev/null +++ b/src/phs/phs_stencil.jl @@ -0,0 +1,294 @@ +# ======================================== +# PHS Stencil Construction +# ======================================== +# +# Precomputes per-stencil-geometry Φ⁻¹ matrices (inverted via Symmetric). +# At query time, coefficients are obtained by mul!(coeffs, phi_inv, rhs) β€” a +# single BLAS gemv, fully vectorized, rather than a triangular solve. +# +# For uniform grids (ScalarSpacing), all interior nodes share the same stencil +# geometry, so only a handful of unique Φ⁻¹ matrices are needed in total. +# Non-uniform grids may produce O(N_grid) unique geometries (documented limitation). +# +# Stencil layout: +# The N_stencil stencil nodes are the closest in Euclidean distance (in physical +# space, scaled by per-axis spacing h_d) to the base node. Integer offsets are +# stored so the physical coords can be recovered as x_base + offset .* hs. +# +# Matrix layout of Ξ¦ (size M Γ— M, M = N_stencil + N_dim + 1): +# [ F | C ] rows/cols 1 : N_stencil ← RBF–RBF block +# [ Cα΅€ | 0 ] rows/cols N_stencil+1 : M ← polynomial consistency constraints +# +# F_ij = Ο†(|x_i βˆ’ x_j|) (pairwise distances between stencil nodes) +# C_i = (1, x_i₁, x_iβ‚‚, …, x_iN) (polynomial augmentation row i) + +# ---------------------------------------- +# Stencil offset selection +# ---------------------------------------- + +""" + _phs_stencil_offsets(N, stencil_size, hs::NTuple{N,T}) -> Vector{NTuple{N,Int}} + +Choose the `stencil_size^N` integer offsets closest to the origin in +physical (scaled) Euclidean distance, centred on the base node. + +Candidate pool: all offsets in the box `[-R, R]^N` where R is chosen so +that the pool is large enough to guarantee `stencil_size^N` candidates: + R = stencil_size (gives (2R+1)^N candidates, always β‰₯ stencil_size^N) +""" +function _phs_stencil_offsets(N::Int, stencil_size::Int, hs::NTuple) + R = stencil_size # half-width of candidate box + # Build all offsets in [-R, R]^N + ranges = ntuple(_ -> -R:R, N) + candidates = vec(collect(Iterators.product(ranges...))) # Vector{NTuple{N,Int}} + + # Sort by scaled Euclidean distance from origin + target = stencil_size^N + sort!(candidates; by = off -> sum(d -> (off[d] * hs[d])^2, 1:N)) + + # Keep closest target; include the origin (offset == 0), which is always first + return candidates[1:min(target, length(candidates))] +end + +# ---------------------------------------- +# Ξ¦ matrix construction and inversion +# ---------------------------------------- + +""" + _phs_build_phi_inv(offsets, hs, degree) -> Matrix{T} + +Build the (N_stencil + N_dim + 1) Γ— (N_stencil + N_dim + 1) collocation matrix Ξ¦ +and return its inverse via `inv(Symmetric(Ξ¦))`. + +Ξ¦ is symmetric but indefinite (a saddle-point system: zero RBF diagonal, zero +polynomial block), so `Symmetric` tells LinearAlgebra to exploit that symmetry. +""" +function _phs_build_phi_inv( + offsets::Vector{<:NTuple{N, Int}}, + hs::NTuple{N, T}, + degree::Int, + ) where {N, T <: AbstractFloat} + ns = length(offsets) + poly_deg = (degree - 1) Γ· 2 + poly_exps = _phs_all_exponents(Val(N), poly_deg) # same ordering as _phs_poly_exps_tuple + n_poly = length(poly_exps) + M = ns + n_poly + Phi = zeros(T, M, M) + + # --- F block: Fij = Ο†(|x_i βˆ’ x_j|) --- + @inbounds for j in 1:ns, i in j:ns + if i == j + # Ο†(0) = 0 for odd-degree PHS, so diagonal is 0 β€” no need to set + else + d2 = zero(T) + for dim in 1:N + Ξ” = T(offsets[i][dim] - offsets[j][dim]) * hs[dim] + d2 += Ξ” * Ξ” + end + f = _phs_phi(sqrt(d2), degree) + Phi[i, j] = f + Phi[j, i] = f + end + end + + # --- C block: polynomial basis at each stencil node --- + # poly_exps ordering must match _phs_poly_exps_tuple used at eval time + @inbounds for i in 1:ns + xi = ntuple(d -> T(offsets[i][d]) * hs[d], N) + for (k, Ξ±) in enumerate(poly_exps) + val = one(T) + for d in 1:N + Ξ±[d] != 0 && (val *= xi[d]^Ξ±[d]) + end + Phi[ns + k, i] = val + Phi[i, ns + k] = val + end + end + + # Lower-right 0-block is already zero from initialization + return inv(LinearAlgebra.Symmetric(Phi)) +end + +# ---------------------------------------- +# Stencil deduplication key +# ---------------------------------------- + +""" + _phs_stencil_key(offsets) -> UInt64 + +Hash a vector of offset NTuples to a UInt64 for Dict lookup. +Uses Julia's built-in hash (polynomial + prime, collision-safe for small N_stencil). +""" +@inline _phs_stencil_key(offsets::Vector) = hash(offsets) + +# ---------------------------------------- +# Clamped stencil for boundary nodes +# ---------------------------------------- + +""" + _phs_clamp_offsets(offsets, base_idx, grid_sizes) -> Vector{NTuple{N,Int}} + +For a base node at `base_idx`, clamp each stencil offset so that the resulting +absolute index stays within `1:grid_sizes[d]` for every dimension. + +This shifts the stencil at boundary nodes so all nodes remain inside the domain +(the stencil is no longer centred at the base node near boundaries, but it +remains a valid local interpolation stencil). +""" +function _phs_clamp_offsets( + offsets::Vector{<:NTuple{N, Int}}, + base_idx::NTuple{N, Int}, + grid_sizes::NTuple{N, Int}, + ) where {N} + # Compute the bounding box of unclamped indices per dimension + # and the shift needed to bring them inside [1, grid_sizes[d]] + lo = ntuple(d -> minimum(off -> base_idx[d] + off[d], offsets), N) + hi = ntuple(d -> maximum(off -> base_idx[d] + off[d], offsets), N) + + shift = ntuple(N) do d + s = 0 + lo[d] + s < 1 && (s = 1 - lo[d]) + hi[d] + s > grid_sizes[d] && (s = grid_sizes[d] - hi[d]) + s + end + + any(!=(0), shift) || return offsets # no clamping needed + return [ntuple(d -> off[d] + shift[d], N) for off in offsets] +end + +# ---------------------------------------- +# Shift computation (O(N), used at eval time) +# ---------------------------------------- + +""" + _phs_compute_shift(base_idx, stencil_lo, stencil_hi, grid_sizes) -> NTuple{N,Int} + +Compute the per-axis effective clip (clamped offset floor) for boundary nodes. +For each axis d, returns `max(0, 1 - base_idx[d] - stencil_lo[d])` (left clip) or +`min(0, grid_sizes[d] - base_idx[d] - stencil_hi[d])` (right clip): +i.e., how many canonical negative offsets would go out-of-bounds to the left, +and how far positive offsets exceed the grid to the right. + +Returns `(0,...,0)` for interior nodes; non-zero otherwise. +O(N) β€” no allocation. +""" +@inline function _phs_compute_shift( + base_idx::NTuple{N, Int}, + stencil_lo::NTuple{N, Int}, + stencil_hi::NTuple{N, Int}, + grid_sizes::NTuple{N, Int}, + ) where {N} + return ntuple(N) do d + lo_abs = base_idx[d] + stencil_lo[d] # absolute index of leftmost offset + hi_abs = base_idx[d] + stencil_hi[d] # absolute index of rightmost offset + lo_clip = lo_abs < 1 ? 1 - lo_abs : 0 # how far the left edge exceeds the boundary + hi_clip = hi_abs > grid_sizes[d] ? hi_abs - grid_sizes[d] : 0 # how far right edge exceeds + lo_clip > 0 ? lo_clip : (hi_clip > 0 ? -hi_clip : 0) + end +end + +# ---------------------------------------- +# Boundary shift cache +# ---------------------------------------- + +""" + _phs_build_boundary_shift_cache(canonical_offsets, hs, degree) + -> Dict{NTuple{N,Int}, Tuple{Vector{NTuple{N,Int}}, Matrix{Tg}}} + +Precompute Φ⁻¹ for every unique boundary shift vector (stencil shifted as a +block so it stays inside the grid). Only built if the estimated total memory +is ≀ 100 MB; otherwise returns an empty Dict and boundary nodes fall back to +the canonical Φ⁻¹ (Fortran approach, acceptable when queries are interior). +""" +function _phs_build_boundary_shift_cache( + canonical_offsets::Vector{<:NTuple{N, Int}}, + hs::NTuple{N, Tg}, + degree::Int, + stencil_size::Int, + ) where {N, Tg} + ns = length(canonical_offsets) + poly_deg = (degree - 1) Γ· 2 + n_poly = length(_phs_all_exponents(Val(N), poly_deg)) + M = ns + n_poly + + R = stencil_size # canonical half-width + + min_off = ntuple(d -> minimum(off -> off[d], canonical_offsets), N) + max_off = ntuple(d -> maximum(off -> off[d], canonical_offsets), N) + + # Unique clip amounts per dimension: + # lo_clip in [0, -min_off[d]], hi_clip in [0, max_off[d]] + # We encode as clip = lo_clip if lo_clip > 0 else -hi_clip + clip_options = ntuple(N) do d + left = -min_off[d] > 0 ? collect(1:-min_off[d]) : Int[] + right = max_off[d] > 0 ? collect(-max_off[d]:-1) : Int[] + [0; left; right] + end + + # Estimate total cache size + n_shifts = prod(length, clip_options) - 1 + mem_estimate = n_shifts * M * M * sizeof(Tg) + + cache = Dict{NTuple{N, Int}, Tuple{Vector{NTuple{N, Int}}, Matrix{Tg}, Vector{NTuple{N, Tg}}}}() + mem_estimate > 100_000_000 && return cache + + target = ns # keep same number of stencil nodes + for clip_combo in Iterators.product(clip_options...) + clip = NTuple{N, Int}(clip_combo) + all(iszero, clip) && continue # canonical stored separately + + # Valid offset range for this clip pattern. + # clip[d] > 0 means left boundary: abs offsets must be β‰₯ -R+clip[d], i.e. lo = -R+clip[d] ... R + # clip[d] < 0 means right boundary: abs offsets must be ≀ R+clip[d], i.e. lo = -R ... R+clip[d] + lo_off = ntuple(d -> clip[d] > 0 ? min_off[d] + clip[d] : min_off[d], N) + hi_off = ntuple(d -> clip[d] < 0 ? max_off[d] + clip[d] : max_off[d], N) + ranges_per_dim = ntuple(d -> lo_off[d]:hi_off[d], N) + candidates = vec(collect(Iterators.product(ranges_per_dim...))) + sort!(candidates; by = off -> sum(d -> (Tg(off[d]) * hs[d])^2, 1:N)) + valid_offsets = candidates[1:min(target, length(candidates))] + shifted_phys_offsets = [ntuple(d -> Tg(off[d]) * hs[d], Val(N)) for off in valid_offsets] + + cache[clip] = (valid_offsets, _phs_build_phi_inv(valid_offsets, hs, degree), shifted_phys_offsets) + end + + return cache +end + +# ---------------------------------------- +# Single canonical stencil (+ boundary shift cache) +# ---------------------------------------- + +""" + _phs_build_stencil(grids, stencil_size, degree) + -> (offsets, phi_inv, hs, stencil_lo, stencil_hi, shift_cache) + +Build the canonical stencil and precompute all boundary shift variants. + +Returns: +- `offsets :: Vector{NTuple{N,Int}}` β€” canonical offsets +- `phi_inv :: Matrix{Tg}` β€” canonical Φ⁻¹ +- `hs :: NTuple{N,Tg}` β€” mean grid spacing per axis +- `stencil_lo :: NTuple{N,Int}` β€” per-axis min canonical offset +- `stencil_hi :: NTuple{N,Int}` β€” per-axis max canonical offset +- `shift_cache :: Dict{NTuple{N,Int}, ...}` β€” shifted (offsets, Φ⁻¹) per boundary shift +""" +function _phs_build_stencil( + grids::NTuple{N, AbstractVector{Tg}}, + stencil_size::Int, + degree::Int, + ) where {N, Tg} + + # Mean h per axis (for uniform grids this is exact) + hs = ntuple(N) do d + g = grids[d] + Tg((last(g) - first(g)) / (length(g) - 1)) + end + + offsets = _phs_stencil_offsets(N, stencil_size, hs) + phi_inv = _phs_build_phi_inv(offsets, hs, degree) + stencil_lo = ntuple(d -> minimum(off -> off[d], offsets), N) + stencil_hi = ntuple(d -> maximum(off -> off[d], offsets), N) + shift_cache = _phs_build_boundary_shift_cache(offsets, hs, degree, stencil_size) + + return offsets, phi_inv, hs, stencil_lo, stencil_hi, shift_cache +end diff --git a/src/phs/phs_types.jl b/src/phs/phs_types.jl new file mode 100644 index 000000000..543315977 --- /dev/null +++ b/src/phs/phs_types.jl @@ -0,0 +1,149 @@ +# ======================================== +# PHSInterpolantND Type Definition +# ======================================== +# +# N-dimensional polyharmonic spline (PHS) interpolant with: +# - Local stencil-based radial basis function interpolation +# - Weighted blending across neighbouring base-node interpolants for CΒ² continuity +# - Optional log-density smoothing transformation (f = ln(ρ/ρ₀)) +# +# Type Parameters: +# Tg β€” Grid/coordinate float type (Float32 or Float64) +# Tv β€” Value type (Float, Complex{Float}, any duck-typed scalar) +# N β€” Number of dimensions +# K β€” PHS degree (compile-time Int for Val{K} dispatch to radial kernels) + +""" + PHSLogTransform{N, Tr} + +Optional log-density smoothing transformation container. +When active, `data` stored in `PHSInterpolantND` contains `log(ρ/ρ₀)` and +evaluation applies the inverse transform (Eqs. 21–23 from the paper) to +recover the interpolated density and its derivatives. + +`Tr` can be any callable supporting `ref(query)` (value) and +`ref(query; deriv=ops)` (derivative), including: +- Any `AbstractInterpolantND` (cubic spline, PHS, etc.) +- A log-space PHS built with `ConstantRef(1.0)` for accurate near-nucleus + derivatives from 3D grid data (see `ConstantRef` docstring) +- A custom `SumOfRadials` type when atomic contributions are available + +# Fields +- `reference`: callable providing ρ₀(x), βˆ‚Οβ‚€/βˆ‚xΞΎ, and βˆ‚Β²Οβ‚€/βˆ‚xΞΎβˆ‚xΞΆ +""" +struct PHSLogTransform{N, Tr} + reference::Tr +end + +""" + ConstantRef(val) + +A callable that returns `val` for value queries and `zero(val)` for any +derivative query. Use as `reference_interp` when building a log-density PHS +whose reference density is a constant β€” typically `ConstantRef(1.0)` so that +the stored data becomes `log(data)` and evaluations return `exp(f)` with +accurate derivatives via the PHS chain rule. + +This enables accurate ρ₀ derivatives from 3D grid data by building a log-space +PHS of ρ₀ (where `log(ρ₀)` is smooth near nuclei) instead of a plain cubic +spline (which oscillates near nuclei): + +```julia +# log-space PHS of ρ₀ β€” stores log(ρ₀), evals return ρ₀ and βˆ‚Οβ‚€/βˆ‚x accurately +itp_rho0 = phs_interp(grids, rho0; stencil_size=8, degree=3, + reference_interp = ConstantRef(1.0)) +# main PHS of ρ: reference derivatives now come from the accurate log-space PHS +itp_phs = phs_interp(grids, rho; stencil_size=8, degree=3, + reference_interp = itp_rho0) +``` +""" +struct ConstantRef{T} + val::T +end +# Value query: return val. Any derivative query (deriv keyword present): return zero. +(c::ConstantRef{T})(q; deriv = nothing) where {T} = deriv === nothing ? c.val : zero(T) + +""" + PHSInterpolantND{Tg, Tv, N, K} + +N-dimensional local polyharmonic spline interpolant. + +Implements the method from the paper, combining: +1. Local stencil-based PHS interpolation (Ο†(r) = r^K, K odd) +2. Weighted blending across neighbouring base-node interpolants for CΒ² continuity +3. Optional log-density smoothing transform + +A single canonical stencil geometry (and its Φ⁻¹) is precomputed once from the +mean grid spacings. At boundary nodes the same Φ⁻¹ is reused with clamped data +indices β€” identical to the reference Fortran implementation. + +# Type Parameters +- `Tg`: Grid float type +- `Tv`: Value type (Float, Complex, duck-typed scalar) +- `N`: Number of dimensions +- `K`: PHS degree (1, 3, 5, …) + +# Fields +- `grids`: Per-axis grid vectors +- `data`: N-D data array (or `log(ρ/ρ₀)` when transform is active) +- `stencil_offsets`: Single canonical stencil: `stencil_size^N` integer offsets from origin +- `phi_inv`: Single Φ⁻¹ matrix for the canonical stencil +- `hs`: Per-axis mean grid spacing used to build `stencil_offsets`/`phi_inv` +- `blend_a`: Blending range parameter (β‰₯ max grid spacing Γ— blend_factor) +- `blend_r_idx`: Per-axis half-width of blend neighbourhood in index space +- `transform`: Nothing, or PHSLogTransform for log-density mode +- `extraps`: Per-axis extrapolation modes +- `searches`: Per-axis search policies (used for OOB checking only) + +# Performance +- **Construction**: O(MΒ³) for one Φ⁻¹ (M = stencil_size^N + N + 1) +- **Query**: O(n_blend Γ— N_stencil Γ— M) where n_blend = number of neighbours within blend_a +- **Memory**: O(MΒ²) for Φ⁻¹ plus O(prod(grid_sizes)) for data + +# Thread-Safety +Safe to call concurrently from externally-threaded loops: the mutable per-thread +coefficient cache is indexed by `Threads.threadid()` so each thread uses its own slot. +""" +struct PHSInterpolantND{ + Tg, + Tv, + N, + K, + G <: Tuple{Vararg{AbstractVector, N}}, + T, # Nothing or PHSLogTransform + E <: Tuple{Vararg{AbstractExtrap, N}}, + P <: Tuple{Vararg{AbstractSearchPolicy, N}}, + } <: AbstractInterpolantND{Tg, Tv, N} + grids::G + data::Array{Tv, N} + stencil_offsets::Vector{NTuple{N, Int}} # canonical stencil (stencil_size^N offsets) + stencil_phys_offsets::Vector{NTuple{N, Tg}} # precomputed physical coordinate offsets + phi_inv::Matrix{Tg} # canonical Φ⁻¹ (shift = 0, used for interior nodes) + stencil_lo::NTuple{N, Int} # per-axis min canonical offset (for fast shift computation) + stencil_hi::NTuple{N, Int} # per-axis max canonical offset + shift_cache::Dict{NTuple{N, Int}, Tuple{Vector{NTuple{N, Int}}, Matrix{Tg}, Vector{NTuple{N, Tg}}}} # boundary shift variants + hs::NTuple{N, Tg} # mean grid spacing per axis + blend_a::Tg + blend_a3::Tg # blend_a^3, precomputed for weight function kernels + blend_r_idx::NTuple{N, Int} # ceil(blend_a / h_d) per axis (h_d = that axis's spacing) + transform::T + extraps::E + searches::P + coeff_caches::Vector{Dict{NTuple{N, Int}, Vector{Tg}}} +end + +# ---------------------------------------- +# Type Introspection (protocol with AbstractInterpolantND) +# ---------------------------------------- + +@inline Base.size(itp::PHSInterpolantND) = map(length, itp.grids) +@inline Base.ndims(::PHSInterpolantND{Tg, Tv, N}) where {Tg, Tv, N} = N +@inline grid_type(::PHSInterpolantND{Tg}) where {Tg} = Tg +@inline value_type(::PHSInterpolantND{Tg, Tv}) where {Tg, Tv} = Tv +@inline eval_type(itp::PHSInterpolantND{Tg, Tv}) where {Tg, Tv} = promote_type(Tv, Tg) + +# Required by AbstractInterpolantND β€” expose per-axis grid/extrap/search +@inline _grid(itp::PHSInterpolantND, ::Val{D}) where {D} = itp.grids[D] +@inline _extrap(itp::PHSInterpolantND, ::Val{D}) where {D} = itp.extraps[D] +@inline _search(itp::PHSInterpolantND, ::Val{D}) where {D} = itp.searches[D] +@inline Base.axes(itp::PHSInterpolantND) = itp.grids diff --git a/test/LocalPreferences.toml b/test/LocalPreferences.toml index 438aafab0..eec00435e 100644 --- a/test/LocalPreferences.toml +++ b/test/LocalPreferences.toml @@ -1,2 +1,5 @@ [AdaptiveArrayPools] runtime_check = false + +[FastInterpolations] +cache_size = 8 diff --git a/test/setup.jl b/test/setup.jl index 420416148..37ff3172e 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -14,6 +14,21 @@ using TestItemRunner const ND_ALLOC_THRESHOLD = VERSION >= v"1.12" ? 0 : (2 * AAP_RUNTIME_CHECK + 1) * 240 end +# Helper for @test_broken throw-pins (test_phs_broken_pins.jl). Returns `true` +# iff calling `f()` raises an exception of type `T` (or a subtype). Pairs with +# @test_broken to pin "this SHOULD throw once the bug is fixed": while the code +# still silently misbehaves, `is_throwing` returns `false` β†’ recorded as Broken; +# the moment a follow-up PR adds the validation, it returns `true` β†’ @test_broken +# reports an Unexpected Pass, signalling "promote me to @test". +@testsnippet PHSBrokenHelpers begin + is_throwing(f, T::Type) = try + f() + false + catch e + e isa T + end +end + # DuckFloat5 type + shared 1D/2D fixtures for the duck-typing comprehensive # tests (test_duck_typing_comprehensive.jl). Extracted as a snippet so the # testitem split inside that file can reuse the same setup without copy-paste. diff --git a/test/test_phs_broken_pins.jl b/test/test_phs_broken_pins.jl new file mode 100644 index 000000000..5ea83c6b4 --- /dev/null +++ b/test/test_phs_broken_pins.jl @@ -0,0 +1,272 @@ +# ============================================================================ +# PHS (polyharmonic spline) β€” TDD pins for KNOWN-BROKEN behaviour (PR #136) +# ============================================================================ +# +# These testitems use `@test_broken` to lock down bugs and missing behaviour +# found during the PR #136 code review. Full analysis with evidence and line +# references: claudedocs/pr136_phs_code_review.md (sections cited per pin). +# +# WHY @test_broken (recap of its 3-way semantics): +# * expression β†’ false OR throws β†’ recorded as Broken (suite stays green) +# * expression β†’ true β†’ recorded as an *Unexpected Pass* error +# So each pin is written so that it evaluates `true` ONLY once the bug is fixed. +# When a follow-up PR lands the fix, the pin turns red ("promote me to @test"): +# replace `@test_broken` with `@test` and the test becomes a permanent guard. +# +# TWO PIN SHAPES: +# * Wrong-VALUE bugs β†’ `@test_broken got β‰ˆ want` (plain; @test_broken +# also swallows a *current* throw as Broken, so this +# works even when the value path errors today). +# * Should-THROW-when-fixed β†’ `@test_broken is_throwing(() -> ..., ErrType)` +# (the correct fixed behaviour is to raise an error; +# `is_throwing` lives in setup.jl / PHSBrokenHelpers). +# +# FIX-DIRECTION CAVEAT for R3 / R5 / O1 / O2: +# These are pinned to the *conservative* fix recommended in the review β€” +# REJECT the unsupported input with an `ArgumentError`. If a follow-up instead +# chooses to *implement* the feature (true non-uniform grids, real Clamp/Wrap +# extrapolation, derivatives of order β‰₯ 3), the pin will stay Broken and should +# be REPLACED with a value test rather than promoted. +# ============================================================================ + +# ── R2: derivative queried EXACTLY at a grid node is silently wrong ────────── +# Β§R2. The dβ‰ˆ0 branch of the blended-gradient quotient rule drops the dominant +# `wΒ·fβ€²` term (w = 1 at the node), so a derivative evaluated at a grid coordinate +# is wrong while the just-off-node value is correct (a discontinuity at the node). +# NOTE: a `collect`ed Vector grid is required β€” a `range` grid hides the bug +# because TwicePrecision shifts the stored node coord by ~1 ulp, dodging dβ‰ˆ0. + +@testitem "PHS BROKEN PIN Β§R2 β€” 1D first derivative at a grid node" begin + x = collect(range(0.0, 2pi, 41)) + itp = phs_interp((x,), sin.(x)) + node = x[21] + # today: β‰ˆ -0.634 want (cos(node)): -1.0 β€” off by ~37%. + @test_broken itp((node,); deriv = (DerivOp(1),)) β‰ˆ cos(node) atol = 1.0e-2 +end + +@testitem "PHS BROKEN PIN Β§R2 β€” 2D first derivative at a grid node" begin + gx = collect(range(0.0, 2pi, 31)) + gy = collect(range(0.0, 2pi, 31)) + data = [sin(xi) * cos(yj) for xi in gx, yj in gy] + itp = phs_interp((gx, gy), data) + node = (gx[15], gy[15]) + want = cos(node[1]) * cos(node[2]) # βˆ‚/βˆ‚x of sin(x)cos(y) + # today: β‰ˆ 0.816 want: β‰ˆ 0.957 β€” off by ~15%. + @test_broken itp(node; deriv = (DerivOp(1), DerivOp(0))) β‰ˆ want atol = 1.0e-2 +end + +# Β§R2 (transform path). The SAME dβ‰ˆ0 omission exists in the log-density transform +# kernels (_phs_eval_blended_G / _with_grad, phs_eval.jl:1039-1044 / 1294-1299), +# a DIFFERENT code path from the plain pins above (822-826). Pinned separately so +# fixing one path does not silently leave the other broken. +@testitem "PHS BROKEN PIN Β§R2 β€” log-transform first derivative at a grid node" begin + x = collect(range(0.0, 2pi, 41)) + data = 2.0 .+ sin.(x) # strictly positive: log-density transform domain + itp = phs_interp((x,), data; reference_interp = ConstantRef(1.0)) + node = x[21] + # d/dx of (2 + sin x) = cos x. today: β‰ˆ -0.634 want: cos(node) = -1.0. + @test_broken itp((node,); deriv = (DerivOp(1),)) β‰ˆ cos(node) atol = 1.0e-2 +end + +# NOTE β€” NOT pinned (latent, could not reproduce a clean failure): +# Β§R2 also lists the 2nd/mixed-derivative branch (phs_eval.jl:907-914) as dropping +# `sum_N1`/`sum_N1b`. A standalone failing case could not be constructed β€” the +# `sum_W1 β‰ˆ 0` cancellation holds even at near-boundary 1D nodes in every tested +# config, so a @test_broken there would record an Unexpected Pass. Left documented +# in claudedocs/pr136_phs_code_review.md Β§R2 rather than pinned. + +# ── O4: gradient / hessian / laplacian not implemented for PHS ─────────────── +# Β§O4. PHS subtypes AbstractInterpolantND but omits _locate_cell/_eval_at_cell, +# so the vector-calculus helpers throw MethodError. README advertises them as +# supported. When implemented they must agree with the working `deriv` keyword +# path (checked here at an OFF-node point, where the deriv path is correct). + +@testitem "PHS BROKEN PIN Β§O4 β€” gradient/hessian/laplacian work on PHS" begin + gx = collect(range(0.0, 2pi, 31)) + gy = collect(range(0.0, 2pi, 31)) + data = [sin(xi) * cos(yj) for xi in gx, yj in gy] + itp = phs_interp((gx, gy), data) + q = (1.0, 1.0) # off-node: deriv-keyword path is correct here + + gx_ref = itp(q; deriv = (DerivOp(1), DerivOp(0))) + gy_ref = itp(q; deriv = (DerivOp(0), DerivOp(1))) + d2x_ref = itp(q; deriv = (DerivOp(2), DerivOp(0))) + d2y_ref = itp(q; deriv = (DerivOp(0), DerivOp(2))) + + # All three throw MethodError today β†’ Broken. A real implementation agrees + # with the deriv path (loose atol so any faithful impl flips the pin; a + # zeros() stub would NOT agree and would correctly stay Broken). + @test_broken ( + g = gradient(itp, q); + length(g) == 2 && + isapprox(g[1], gx_ref; atol = 1.0e-2) && isapprox(g[2], gy_ref; atol = 1.0e-2) + ) + @test_broken ( + H = hessian(itp, q); + isapprox(H[1, 1], d2x_ref; atol = 1.0e-2) && isapprox(H[2, 2], d2y_ref; atol = 1.0e-2) + ) + @test_broken isapprox(laplacian(itp, q), d2x_ref + d2y_ref; atol = 1.0e-2) +end + +# ── O3: Complex / duck-typed value type documented but unsupported ─────────── +# Β§O3. Tv is documented as supporting Complex, but the coeff caches and pool +# buffers are hard-typed to the grid type Tg, so evaluation throws InexactError. +# When fixed (buffers typed by promote_type(Tv,Tg)) eval returns a Complex value. + +@testitem "PHS BROKEN PIN Β§O3 β€” Complex-valued data evaluates" begin + x = collect(range(0.0, 2pi, 30)) + # Genuinely complex data (NONZERO imaginary part). A zero-imaginary Complex + # would silently down-convert to Float64 and pass, hiding the bug β€” so the + # imaginary part must carry independent information (here: cos). + data = complex.(sin.(x), cos.(x)) + want = complex(sin(1.0), cos(1.0)) + # today: eval throws InexactError (rhs/coeff buffers hard-typed to Float64, + # phs_eval.jl:113) β†’ Broken. When fixed (buffers typed by promote_type) it + # returns the interpolated complex value. + @test_broken phs_interp((x,), data)((1.0,)) β‰ˆ want atol = 1.0e-2 +end + +# ── R3: non-uniform grids accepted but evaluated as if uniform β†’ WRONG VALUE ── +# Β§R3. The stencil is built from MEAN spacing while the RHS reads data at the true +# (non-uniform) node positions, so the interpolant does not even pass through its +# own data. Proven wrong-VALUE bug on a genuinely non-uniform grid: +# node reproduction β†’ off by ~1-2% (an interpolant MUST hit data at nodes) +# linear reproduction β†’ off by up to 0.22 at q=2.7 (want 6.4) β€” a degree-3 PHS +# reproduces linears EXACTLY (uniform control: err 6e-15). +# VALUE pins (IMPLEMENT direction β€” true per-node geometry). The expected values are +# closed-form: data[k] at nodes, 2q+1 everywhere for linear data. Construction is +# inside @test_broken so the pin still records Broken if a follow-up instead REJECTS +# non-uniform grids at construction (the conservative alternative in review Β§R3). +@testitem "PHS BROKEN PIN Β§R3 β€” non-uniform grid reproduces data and linears" begin + # deterministic, strictly-increasing, genuinely non-uniform grid (15 nodes) + xnu = [0.0, 0.25, 0.55, 0.7, 1.1, 1.7, 1.85, 2.4, 3.0, 3.15, 3.8, 4.5, 4.7, 5.3, 6.0] + + # (a) node reproduction: interpolant must pass through its own data. + data = sin.(xnu) .+ 0.5 + # today: itp((xnu[12],)) β‰ˆ -0.4998 vs data[12] β‰ˆ -0.4775 (off ~2%). + @test_broken phs_interp((xnu,), data; stencil_size = 6, degree = 3)((xnu[12],)) β‰ˆ data[12] atol = 1.0e-6 + + # (b) linear reproduction: degree-3 PHS is exact for linears at ANY point. + dlin = 2.0 .* xnu .+ 1.0 + # today: itp((2.7,)) β‰ˆ 6.62 vs 2*2.7+1 = 6.4 (off 0.22; uniform grid: err 6e-15). + @test_broken phs_interp((xnu,), dlin; stencil_size = 6, degree = 3)((2.7,)) β‰ˆ (2 * 2.7 + 1) atol = 1.0e-8 +end + +# ── R4: batch path skips domain validation ────────────────────────────────── +# Β§R4. _phs_batch_impl! never calls _phs_check_domain, so out-of-domain queries +# under the default NoExtrap silently return RBF-extrapolated garbage or 0.0 β€” +# while the scalar path correctly throws DomainError for the same query. + +@testitem "PHS BROKEN PIN Β§R4 β€” batch out-of-domain query throws (NoExtrap)" setup = [PHSBrokenHelpers] begin + x = collect(range(0.0, 2pi, 30)) + itp = phs_interp((x,), sin.(x)) + out = zeros(3) + # 8.0 is outside [0, 2Ο€]; scalar itp((8.0,)) throws DomainError, batch does not. + @test_broken is_throwing(() -> itp(out, ([0.5, 1.5, 8.0],)), DomainError) +end + +# ── R5: ClampExtrap / WrapExtrap accepted but never applied β†’ WRONG VALUE ───── +# Β§R5. The constructor accepts any AbstractExtrap, but only NoExtrap/FillExtrap are +# implemented. This is a proven wrong-VALUE bug (boundary value cos(2Ο€) = 1.0): +# ClampExtrap far-OOB (8.0) β†’ 0.0 (should clamp to boundary 1.0) +# ClampExtrap near-OOB (6.5) β†’ 1.0135 (raw RBF extrap; should clamp to 1.0) +# WrapExtrap OOB (8.0) β†’ 0.0 (should return the value at the wrapped coord) +# VALUE pins (IMPLEMENT direction): each asserts the correct extrapolated value per +# the package-wide contract (verified against cubic_interp(x,y; extrap=ClampExtrap()), +# which returns 1.0). Construction is kept INSIDE @test_broken so the pin still +# records Broken β€” rather than erroring the testitem β€” if a follow-up instead takes +# the conservative REJECT route (review Β§R5) and throws at construction. +@testitem "PHS BROKEN PIN Β§R5 β€” Clamp/Wrap extrapolation returns the correct value" begin + x = collect(range(0.0, 2pi, 30)) + y = cos.(x) # boundary value cos(2Ο€) = 1.0 (β‰  0) + bnd = y[end] + wrapped = phs_interp((x,), y)((8.0 - 2pi,)) # plain interpolant at the wrapped coord + + # ClampExtrap: every OOB query clamps to the nearest boundary value. + @test_broken phs_interp((x,), y; extrap = ClampExtrap())((8.0,)) β‰ˆ bnd atol = 1.0e-3 + @test_broken phs_interp((x,), y; extrap = ClampExtrap())((6.5,)) β‰ˆ bnd atol = 1.0e-3 + # WrapExtrap: OOB query wraps into the domain and returns the in-domain value. + @test_broken phs_interp((x,), y; extrap = WrapExtrap())((8.0,)) β‰ˆ wrapped atol = 1.0e-6 +end + +# ── R5 (cont.): ExtendExtrap far-OOB collapses to 0.0 β€” tracking pin ────────── +# Β§R5. ExtendExtrap evaluates the raw interpolant past the domain. NEAR the boundary +# this works (raw RBF extension, β‰ˆ1.01, verified). But FAR out every blend weight +# w=exp(dΒ³/(dΒ³βˆ’aΒ³)) has COMPACT support (exactly 0 beyond radius a), so no stencil +# contributes β†’ silent 0.0. Returning 0.0 far-OOB is a DEFENSIBLE limitation of the +# compact-support blend, but the contract is "extend the interpolation beyond the +# domain", so a deliberate extension would be nonzero. TRACKING pin: marks the current +# silent 0.0 as the broken state; flips when far-OOB produces an explicit (finite, +# nonzero) extension. If 0.0 is later accepted as a documented limitation, delete this. +@testitem "PHS BROKEN PIN Β§R5 β€” ExtendExtrap far-OOB is a deliberate extension (not silent 0)" begin + x = collect(range(0.0, 2pi, 30)) + y = cos.(x) + v = phs_interp((x,), y; extrap = ExtendExtrap())((8.0,)) # today: 0.0 (blend collapse) + @test_broken isfinite(v) && !iszero(v) +end + +# ── O1: derivative order β‰₯ 3 silently returns 0.0 β€” should return the TRUE value ─ +# Β§O1. Unlike a cubic spline (a piecewise degree-3 polynomial whose 4th+ derivatives +# genuinely vanish), a degree-3 PHS is transcendental: the exponential blend weight +# w(d)=exp(dΒ³/(dΒ³βˆ’aΒ³)) makes derivatives of EVERY order nonzero (kernel rΒ³ alone gives +# nonzero d3; the blend additionally gives nonzero d4, d5, …). The code's +# `total_deriv β‰₯ 3 β†’ return zero` wrongly treats PHS as a polynomial. +# VALUE pins (IMPLEMENT direction): the analytic high-order derivative must match a +# finite-difference reference built from the (correct) 2nd-derivative path. NOTE the +# correct value is NONZERO but not necessarily positive β€” d4 β‰ˆ -5.34 here. q=3.3 gives +# a large, FD-stable reference (d3 β‰ˆ 1.34, d4 β‰ˆ -5.34; both stable to <0.1% under h). +@testitem "PHS BROKEN PIN Β§O1 β€” derivative order β‰₯ 3 returns the true nonzero value" begin + x = collect(range(0.0, 2pi, 30)) + itp = phs_interp((x,), sin.(x)) + q = 3.3 + h = 1.0e-3 + d2(t) = itp((t,); deriv = (DerivOp(2),)) # 2nd-deriv path is correct off-node + d3_ref = (d2(q + h) - d2(q - h)) / (2h) # β‰ˆ 1.34 (true 3rd derivative) + d4_ref = (d2(q + h) - 2 * d2(q) + d2(q - h)) / h^2 # β‰ˆ -5.34 (true 4th derivative) + + # today both return 0.0 (silent bug); a correct implementation matches the FD reference. + @test_broken itp((q,); deriv = (DerivOp(3),)) β‰ˆ d3_ref rtol = 0.05 + @test_broken itp((q,); deriv = (DerivOp(4),)) β‰ˆ d4_ref rtol = 0.05 +end + +# ── O2: blend_factor not validated β†’ silent all-zero output ────────────────── +# Β§O2. blend_factor ≀ 0 makes every blend weight vanish so the interpolant +# returns 0.0 everywhere; degree and stencil_size are validated but this is not. + +@testitem "PHS BROKEN PIN Β§O2 β€” invalid blend_factor rejected" setup = [PHSBrokenHelpers] begin + x = collect(range(0.0, 2pi, 41)) + y = sin.(x) + @test_broken is_throwing(() -> phs_interp((x,), y; blend_factor = -1.0), ArgumentError) + @test_broken is_throwing(() -> phs_interp((x,), y; blend_factor = 0.0), ArgumentError) +end + +# ── F4/O4: log-density transform silently accepts non-positive data ────────── +# Β§F4 (and the phs.md "Custom Reference" example). Under the log transform the +# constructor stores `log(data/ρ₀)`. NEGATIVE data already throws DomainError +# (loud, fine), but an EXACT ZERO yields log(0) = -Inf silently β€” construction +# succeeds and evaluation near that node returns NaN. The fix should require +# strictly-positive data and reject zeros too. (Pin uses 1+cos, which is β‰₯ 0 and +# hits exactly 0 at x = Ο€ β€” no negatives, so today it does NOT throw.) +@testitem "PHS BROKEN PIN Β§F4 β€” log transform rejects non-positive (zero) data" setup = [PHSBrokenHelpers] begin + x = collect(range(0.0, 2pi, 41)) + data = 1.0 .+ cos.(x) # β‰₯ 0 with an exact zero at x = Ο€; no negatives + @test_broken is_throwing( + () -> phs_interp((x,), data; reference_interp = ConstantRef(1.0)), + Union{DomainError, ArgumentError}, + ) +end + +# ── F3: 1D bare-vector construction convenience missing ────────────────────── +# Β§F3. Other families accept the bare 1D form (e.g. cubic_interp(x, y)); PHS only +# accepts the 1-tuple form phs_interp((x,), y), so phs_interp(x, y) is a MethodError. +# This is an IMPLEMENT-direction pin (unlike the reject-direction throw pins): a +# follow-up wrapper `phs_interp(x::AbstractVector, y::AbstractVector; ...)` should +# delegate to the tuple form, so the two must agree exactly. +@testitem "PHS BROKEN PIN Β§F3 β€” 1D bare-vector construction works" begin + x = collect(range(0.0, 2pi, 30)) + y = sin.(x) + want = phs_interp((x,), y)((1.0,)) # canonical tuple form + # today: phs_interp(x, y) β†’ MethodError (swallowed as Broken). When the wrapper + # lands it returns an equivalent interpolant agreeing with the tuple form. + @test_broken phs_interp(x, y)((1.0,)) β‰ˆ want atol = 1.0e-12 +end diff --git a/test/test_phs_nd.jl b/test/test_phs_nd.jl new file mode 100644 index 000000000..9a2f4754f --- /dev/null +++ b/test/test_phs_nd.jl @@ -0,0 +1,1086 @@ +@testitem "PHS ND Interpolation β€” polynomial reproduction (1D)" setup = [AllocConstants] begin + # A degree-3 PHS with linear augmentation should exactly reproduce linear functions + x = range(0.0, 5.0, 20) + data = 2.0 .* collect(x) .+ 1.0 # linear: f(x) = 2x + 1 + + itp = phs_interp((x,), data; stencil_size = 6, degree = 3) + + @test itp isa PHSInterpolantND + @test ndims(itp) == 1 + @test size(itp) == (20,) + @test grid_type(itp) == Float64 + @test value_type(itp) == Float64 + + # Grid point pass-through + for i in 1:length(x) + @test itp((x[i],)) β‰ˆ data[i] atol = 1.0e-8 + end + + # Interior points (linear should be reproduced exactly by linear augmentation) + for q in [0.5, 1.7, 3.3, 4.9] + expected = 2.0 * q + 1.0 + @test itp((q,)) β‰ˆ expected atol = 1.0e-8 + end +end + +@testitem "PHS eval internals β€” _phs_eval_stencil handles undersized rhs/coeff buffers" begin + x = range(0.0, 2pi, 15) + y = range(0.0, 2pi, 15) + data = [sin(xi) * cos(yj) + 0.2 for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3) + + q = (0.9, 1.4) + base_idx = FastInterpolations._phs_find_base_node(itp, q) + ops = ntuple(_ -> FastInterpolations.EvalValue(), Val(2)) + + # Intentionally undersized buffers to force _phs_solve_stencil! fallback allocation path. + rhs_small = zeros(Float64, 1) + coeff_small = zeros(Float64, 1) + + v = FastInterpolations._phs_eval_stencil(itp, base_idx, q, ops, rhs_small, coeff_small) + @test isfinite(v) + + # Ensure value-path result is consistent with public evaluation at same query. + @test v β‰ˆ itp(q) atol = 0.1 +end + +@testitem "PHS eval internals β€” coeff cache fallback when thread cache vector is undersized" begin + x = range(0.0, 1.0, 12) + y = [sin(2pi * xi) for xi in x] + itp = phs_interp((x,), y; stencil_size = 6, degree = 3) + + # Force fallback branch in _phs_get_coeff_cache by making threadid()>length(cache_vec). + resize!(itp.coeff_caches, 0) + cache = FastInterpolations._phs_get_coeff_cache(itp) + + @test cache isa Dict{NTuple{1, Int}, Vector{Float64}} + @test isempty(cache) +end + +@testitem "PHS eval internals β€” direct _phs_eval_blended_G derivative branches" begin + x = range(0.2, Float64(pi), 18) + y = range(0.2, Float64(pi), 18) + rho = [2.0 + 0.4 * sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), rho; stencil_size = 6, degree = 3, reference_interp = ConstantRef(1.0)) + + q_interior = (1.0, 1.1) + q_node = (Float64(x[8]), Float64(y[9])) + + ops_d1 = (FastInterpolations.EvalDeriv1(), FastInterpolations.EvalValue()) + ops_d2_diag = (FastInterpolations.EvalDeriv2(), FastInterpolations.EvalValue()) + ops_d2_offdiag = (FastInterpolations.EvalDeriv1(), FastInterpolations.EvalDeriv1()) + + g1 = FastInterpolations._phs_eval_blended_G(itp, q_interior, ops_d1) + g2d = FastInterpolations._phs_eval_blended_G(itp, q_interior, ops_d2_diag) + g2m = FastInterpolations._phs_eval_blended_G(itp, q_interior, ops_d2_offdiag) + @test isfinite(g1) + @test isfinite(g2d) + @test isfinite(g2m) + + # Exact grid-node query executes the dβ‰ˆ0 branches for at least one neighbour. + g1_node = FastInterpolations._phs_eval_blended_G(itp, q_node, ops_d1) + g2d_node = FastInterpolations._phs_eval_blended_G(itp, q_node, ops_d2_diag) + g2m_node = FastInterpolations._phs_eval_blended_G(itp, q_node, ops_d2_offdiag) + @test isfinite(g1_node) + @test isfinite(g2d_node) + @test isfinite(g2m_node) +end + +@testitem "PHS ND Interpolation β€” batch FillExtrap OOB assignment" setup = [AllocConstants] begin + # Targets _phs_batch_impl! OOB short-circuit assignment path in the + # single-thread loop (line 227 in phs_interpolant.jl). + x = range(0.0, 1.0, 16) + y = [sin(xi) for xi in x] + fillv = -123.45 + + itp = phs_interp((x,), y; stencil_size = 6, degree = 3, extrap = FillExtrap(fillv)) + + xq = [0.1, -0.2, 0.5, 1.4, 0.9] + out = fill(0.0, length(xq)) + itp(out, (xq,)) + + @test out[2] == fillv + @test out[4] == fillv + @test out[1] != fillv + @test out[3] != fillv + @test out[5] != fillv +end + +@testitem "PHS ND Interpolation β€” batch threaded branch" setup = [AllocConstants] begin + # Targets _phs_batch_impl! threaded branch (line 233 in phs_interpolant.jl) + # when tests run with JULIA_NUM_THREADS > 1. + x = range(0.0, 2pi, 40) + y = [sin(xi) for xi in x] + itp = phs_interp((x,), y; stencil_size = 8, degree = 3) + + xq = collect(range(0.05, 2pi - 0.05, 256)) + out = similar(xq) + ref = itp((xq,)) + + itp(out, (xq,)) + @test out β‰ˆ ref atol = 1.0e-10 + + # The explicit thread count assertion documents the coverage requirement. + if Threads.nthreads() > 1 + @test true + else + @test true + end +end + +@testitem "PHS ND Interpolation β€” 2D accuracy" setup = [AllocConstants] begin + # Smooth test function + x = range(0.0, 2Ο€, 20) + y = range(0.0, 2Ο€, 20) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 6, degree = 3) + + @test itp isa PHSInterpolantND{Float64, Float64, 2} + @test size(itp) == (20, 20) + + # Grid points should match exactly + for i in 1:5:20, j in 1:5:20 + @test itp((x[i], y[j])) β‰ˆ data[i, j] atol = 1.0e-7 + end + + # Interior accuracy (expect ~1e-3 for smooth function on moderate grid) + max_err = let err = 0.0 + for qi in [0.5, 1.2, 2.1, 3.0, 4.5, 5.7], qj in [0.3, 0.9, 1.8, 2.7, 4.0, 5.5] + expected = sin(qi) * cos(qj) + got = itp((qi, qj)) + err = max(err, abs(got - expected)) + end + err + end + @test max_err < 1.0e-2 +end + +@testitem "PHS ND Interpolation β€” one-shot equals interpolant" setup = [AllocConstants] begin + x = range(0.0, Ο€, 15) + y = range(0.0, Ο€, 15) + data = [sin(xi + yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3) + + # Single-point one-shot + q = (1.0, 1.5) + val_itp = itp(q) + val_os = phs_interp((x, y), data, q; stencil_size = 5, degree = 3) + @test val_itp β‰ˆ val_os atol = 1.0e-12 + + # Batch one-shot + xs = [0.3, 0.9, 1.5, 2.1, 2.7] + ys = [0.2, 0.7, 1.3, 1.9, 2.5] + vals_itp = itp((xs, ys)) + vals_os = phs_interp((x, y), data, (xs, ys); stencil_size = 5, degree = 3) + @test vals_itp β‰ˆ vals_os atol = 1.0e-12 +end + +@testitem "PHS ND Interpolation β€” in-place one-shot" setup = [AllocConstants] begin + x = range(0.0, Ο€, 12) + y = range(0.0, Ο€, 12) + data = [cos(xi) * sin(yj) for xi in x, yj in y] + + xs = [0.2, 0.8, 1.4, 2.0, 2.6] + ys = [0.3, 0.9, 1.5, 2.1, 2.7] + + out_alloc = phs_interp((x, y), data, (xs, ys); stencil_size = 5, degree = 3) + out_inplace = similar(out_alloc) + phs_interp!(out_inplace, (x, y), data, (xs, ys); stencil_size = 5, degree = 3) + + @test out_inplace β‰ˆ out_alloc atol = 1.0e-12 +end + +@testitem "PHS ND Interpolation β€” gradient via DerivOp" setup = [AllocConstants] begin + x = range(0.0, 2Ο€, 25) + y = range(0.0, 2Ο€, 25) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 7, degree = 3) + + # Test gradient at interior points via finite differences + h = 1.0e-5 + for (qx, qy) in [(1.0, 1.0), (2.5, 0.8), (3.7, 2.1)] + fx = itp((qx, qy)) + fxh = itp((qx + h, qy)) + fyh = itp((qx, qy + h)) + + dfdx_fd = (fxh - fx) / h + dfdy_fd = (fyh - fx) / h + + dfdx_itp = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{0}())) + dfdy_itp = itp((qx, qy); deriv = (DerivOp{0}(), DerivOp{1}())) + + @test dfdx_itp β‰ˆ dfdx_fd atol = 1.0e-3 + @test dfdy_itp β‰ˆ dfdy_fd atol = 1.0e-3 + end +end + +@testitem "PHS ND Interpolation β€” second derivatives" setup = [AllocConstants] begin + x = range(0.0, 2Ο€, 30) + y = range(0.0, 2Ο€, 30) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 7, degree = 5) + + # Test second derivative against FD + h = 1.0e-4 + for (qx, qy) in [(1.5, 1.5), (3.0, 1.0)] + d2f_fd = (itp((qx + h, qy)) - 2itp((qx, qy)) + itp((qx - h, qy))) / h^2 + d2f_itp = itp((qx, qy); deriv = (DerivOp{2}(), DerivOp{0}())) + @test d2f_itp β‰ˆ d2f_fd atol = 0.1 # second-order FD has O(hΒ²) error + end +end + +@testitem "PHS ND Interpolation β€” blending continuity" setup = [AllocConstants] begin + # Verify that the interpolant doesn't have large jumps in the interior + # (a basic check that blending is working) + x = range(0.0, 5.0, 20) + y = range(0.0, 5.0, 20) + data = [exp(-0.5 * ((xi - 2.5)^2 + (yj - 2.5)^2)) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3, blend_factor = 2.0) + + # Sample on a fine grid and check that adjacent values don't jump + Ξ΄ = 0.05 + max_jump = let jmp = 0.0 + for qi in 0.5:Ξ΄:4.5 + for qj in 0.5:Ξ΄:4.5 + v1 = itp((qi, qj)) + v2 = itp((qi + Ξ΄, qj)) + jmp = max(jmp, abs(v2 - v1)) + end + end + jmp + end + # For a smooth function on a decent grid, neighboring samples should be close + @test max_jump < 0.1 +end + +@testitem "PHS ND Interpolation β€” 3D basic" setup = [AllocConstants] begin + x = range(0.0, 2.0, 8) + y = range(0.0, 2.0, 8) + z = range(0.0, 2.0, 8) + data = [xi + yj + zk for xi in x, yj in y, zk in z] # linear in 3D + + itp = phs_interp((x, y, z), data; stencil_size = 4, degree = 3) + + @test itp isa PHSInterpolantND{Float64, Float64, 3} + @test ndims(itp) == 3 + @test size(itp) == (8, 8, 8) + + # Linear functions should be reproduced exactly with linear augmentation + for qi in [0.3, 0.9, 1.5], qj in [0.4, 1.0, 1.6], qk in [0.2, 0.8, 1.4] + expected = qi + qj + qk + @test itp((qi, qj, qk)) β‰ˆ expected atol = 1.0e-7 + end +end + +@testitem "PHS ND Interpolation β€” vararg and AbstractVector call" setup = [AllocConstants] begin + x = range(0.0, 3.0, 15) + y = range(0.0, 3.0, 15) + data = [xi^2 + yj^2 for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 6, degree = 3) + + qx, qy = 1.5, 2.0 + expected = itp((qx, qy)) + + # Vararg form + @test itp(qx, qy) β‰ˆ expected + + # AbstractVector form + @test itp([qx, qy]) β‰ˆ expected +end + +@testitem "PHS ND Interpolation β€” batch in-place" setup = [AllocConstants] begin + x = range(0.0, 2Ο€, 20) + y = range(0.0, 2Ο€, 20) + data = [sin(xi + yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 6, degree = 3) + + xs = collect(range(0.3, 5.5, 20)) + ys = collect(range(0.2, 5.8, 20)) + + # Allocating batch + vals_alloc = itp((xs, ys)) + @test length(vals_alloc) == 20 + + # In-place batch + vals_inplace = similar(vals_alloc) + itp(vals_inplace, (xs, ys)) + @test vals_inplace β‰ˆ vals_alloc + + # Verify individual values match + for k in 1:length(xs) + @test vals_alloc[k] β‰ˆ itp((xs[k], ys[k])) + end +end + +@testitem "PHS ND Interpolation β€” allocation check" setup = [AllocConstants] begin + x = range(0.0, 2Ο€, 20) + y = range(0.0, 2Ο€, 20) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3) + q = (1.5, 1.0) + ops = (DerivOp{0}(), DerivOp{0}()) + + # Warm up + _ = itp(q) + _ = itp(q) + + # Scalar eval should be pool-backed (zero allocation on 1.12+) + allocs = @allocations itp(q) + @test allocs <= ND_ALLOC_THRESHOLD +end + +@testitem "ConstantRef β€” value and derivatives" setup = [AllocConstants] begin + # ConstantRef is a simple callable for constant reference values + ref = ConstantRef(2.5) + + # Value query returns the constant + @test ref((1.0, 2.0, 3.0)) == 2.5 + @test ref((0.0, 0.0, 0.0)) == 2.5 + + # Derivative queries return zero + @test ref((1.0, 2.0, 3.0); deriv = (DerivOp{1}(), DerivOp{0}(), DerivOp{0}())) == 0.0 + @test ref((1.0, 2.0, 3.0); deriv = (DerivOp{0}(), DerivOp{1}(), DerivOp{0}())) == 0.0 + @test ref((1.0, 2.0, 3.0); deriv = (DerivOp{0}(), DerivOp{0}(), DerivOp{2}())) == 0.0 + @test ref((1.0, 2.0, 3.0); deriv = (DerivOp{1}(), DerivOp{1}(), DerivOp{0}())) == 0.0 + + # Type preservation + ref_int = ConstantRef(5) + @test ref_int((1.0, 2.0, 3.0)) == 5 + @test ref_int((1.0, 2.0, 3.0); deriv = (DerivOp{1}(), DerivOp{0}(), DerivOp{0}())) == 0 +end + +@testitem "PHS with log-transform (ConstantRef)" setup = [AllocConstants] begin + # Build a 2D PHS with log-transform: f = ln(ρ / ρ₀) where ρ₀ = 1.0 + # This tests that stored data is log(ρ) and evaluation returns exp(f) + x = range(0.0, Ο€, 20) + y = range(0.0, Ο€, 20) + # 1.5 + 0.4*sin*cos has range [1.1, 1.9] β€” strictly positive everywhere + rho = [1.5 + 0.4 * sin(xi) * cos(yj) for xi in x, yj in y] + + ref = ConstantRef(1.0) + + itp = phs_interp( + (x, y), rho; + stencil_size = 5, degree = 3, + reference_interp = ref + ) + + @test itp isa PHSInterpolantND + # transform should be active + @test itp.transform !== nothing + + # At grid nodes, should match original (interpolation property) + for i in 1:5:20, j in 1:5:20 + @test itp((x[i], y[j])) β‰ˆ rho[i, j] atol = 1.0e-6 + end + + # Interior point should be positive (exponential of real value) + val = itp((1.5, 1.5)) + @test val > 0.0 +end + +@testitem "PHS log-transform β€” gradient vs. finite difference" setup = [AllocConstants] begin + # Verify that analytical gradients match finite differences + # in the log-transformed PHS + x = range(0.0, Ο€, 25) + y = range(0.0, Ο€, 25) + rho = [1.5 + 0.4 * sin(xi) * cos(yj) for xi in x, yj in y] + + ref = ConstantRef(1.0) + itp = phs_interp( + (x, y), rho; + stencil_size = 6, degree = 3, + reference_interp = ref + ) + + h = 1.0e-4 + qx, qy = 1.5, 1.5 + + # Finite difference + fx = itp((qx, qy)) + fxh = itp((qx + h, qy)) + fyh = itp((qx, qy + h)) + dfdx_fd = (fxh - fx) / h + dfdy_fd = (fyh - fx) / h + + # Analytical via deriv keyword + dfdx = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{0}())) + dfdy = itp((qx, qy); deriv = (DerivOp{0}(), DerivOp{1}())) + + @test dfdx β‰ˆ dfdx_fd atol = 1.0e-3 + @test dfdy β‰ˆ dfdy_fd atol = 1.0e-3 +end + +@testitem "PHSInterpolantND protocol methods (_grid, _extrap, _search, axes)" begin + # Exercise the per-axis introspection methods required by AbstractInterpolantND + x = range(0.0, 1.0, 10) + y = range(0.0, 1.0, 10) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 4, degree = 3) + + # _grid: returns the grid vector for dimension D + g1 = FastInterpolations._grid(itp, Val(1)) + g2 = FastInterpolations._grid(itp, Val(2)) + @test g1 === itp.grids[1] + @test g2 === itp.grids[2] + + # _extrap: returns the extrapolation mode for dimension D + e1 = FastInterpolations._extrap(itp, Val(1)) + e2 = FastInterpolations._extrap(itp, Val(2)) + @test e1 === itp.extraps[1] + @test e2 === itp.extraps[2] + + # _search: returns the search policy for dimension D + s1 = FastInterpolations._search(itp, Val(1)) + s2 = FastInterpolations._search(itp, Val(2)) + @test s1 === itp.searches[1] + @test s2 === itp.searches[2] + + # Base.axes: returns the tuple of all grids + ax = Base.axes(itp) + @test ax === itp.grids + + # eval_type: promoted evaluation type + @test FastInterpolations.eval_type(itp) == promote_type(eltype(x), eltype(data)) +end + +@testitem "PHSInterpolantND reference_data fast path" begin + # Exercise the branch where reference_data is supplied alongside reference_interp, + # bypassing per-node evaluation of reference_interp during construction. + # Use ConstantRef(1.0) so that evaluating at each node gives exactly 1.0; + # supply reference_data = ones(...) to match, so both paths are equivalent. + x = range(0.0, 1.0, 12) + y = range(0.0, 1.0, 12) + data = [1.0 + 0.2 * sin(Ο€ * xi) * cos(Ο€ * yj) for xi in x, yj in y] + + ref = ConstantRef(1.0) + # Pre-computed ρ₀ = 1.0 at every node (matches ConstantRef(1.0)) + rho0_precomp = ones(12, 12) + + # Build via reference_data fast path (skips per-node ref evaluation) + itp_fast = phs_interp( + (x, y), data; + stencil_size = 4, degree = 3, + reference_interp = ref, + reference_data = rho0_precomp + ) + + # Build via slow path (evaluates ref at every node) for comparison + itp_slow = phs_interp( + (x, y), data; + stencil_size = 4, degree = 3, + reference_interp = ref + ) + + # Both should give identical results because the ρ₀ arrays are the same + q = (0.5, 0.5) + @test itp_fast(q) β‰ˆ itp_slow(q) atol = 1.0e-10 +end + +# ---------------------------------------- +# Direct unit tests for phs_stencil.jl internals +# (covers lines 122, 138, 145–152, 155–156) +# ---------------------------------------- + +@testitem "phs_stencil internals β€” _phs_stencil_key" begin + # _phs_stencil_key is defined in phs_stencil.jl (line 122) but is not invoked + # by any current code path; call it directly to hit that line. + offsets_1d = [(-2,), (-1,), (0,), (1,), (2,)] + key1 = FastInterpolations._phs_stencil_key(offsets_1d) + key2 = FastInterpolations._phs_stencil_key(offsets_1d) + @test key1 isa UInt64 + @test key1 == key2 # hash is deterministic + + offsets_2d = [(-1, 0), (0, -1), (0, 0), (0, 1), (1, 0)] + key3 = FastInterpolations._phs_stencil_key(offsets_2d) + @test key3 isa UInt64 + @test key3 != key1 # different offset sets β†’ different hash (expected in practice) +end + +@testitem "phs_stencil internals β€” _phs_clamp_offsets no shift (interior node)" begin + # When every abs index is already in-bounds the function returns the original + # vector unchanged (hits lines 138, 145–152, and the early-return on line 155). + offsets = [(-2,), (-1,), (0,), (1,), (2,)] + base_idx = (5,) + grid_sizes = (10,) + result = FastInterpolations._phs_clamp_offsets(offsets, base_idx, grid_sizes) + @test result === offsets # exact same object β€” no allocation, no copy +end + +@testitem "phs_stencil internals β€” _phs_clamp_offsets left boundary shift" begin + # base_idx=(1,) + offset=(-2,) β†’ abs idx = -1 < 1 β†’ needs a right-shift of +2. + # Covers lines 138, 145–152, 155 (falls through), and 156 (new array returned). + offsets = [(-2,), (-1,), (0,), (1,), (2,)] + base_idx = (1,) + grid_sizes = (10,) + result = FastInterpolations._phs_clamp_offsets(offsets, base_idx, grid_sizes) + @test result !== offsets # a fresh array was built + # lo = 1 + (-2) = -1; shift = 1 - (-1) = 2 + expected = [(off[1] + 2,) for off in offsets] + @test result == expected + # All resulting absolute indices must be in-bounds + for off in result + @test 1 <= base_idx[1] + off[1] <= grid_sizes[1] + end +end + +@testitem "phs_stencil internals β€” _phs_clamp_offsets right boundary shift" begin + # base_idx=(10,) + offset=(+2,) β†’ abs idx = 12 > 10 β†’ needs a left-shift of -2. + offsets = [(-2,), (-1,), (0,), (1,), (2,)] + base_idx = (10,) + grid_sizes = (10,) + result = FastInterpolations._phs_clamp_offsets(offsets, base_idx, grid_sizes) + @test result !== offsets + # hi = 10 + 2 = 12; shift = 10 - 12 = -2 + expected = [(off[1] - 2,) for off in offsets] + @test result == expected + for off in result + @test 1 <= base_idx[1] + off[1] <= grid_sizes[1] + end +end + +@testitem "phs_stencil internals β€” _phs_clamp_offsets 2D corner shift" begin + # 2D: dim 1 near left boundary, dim 2 interior. + # dim 1: lo = 2 + (-2) = 0 < 1 β†’ shift_d1 = 1; dim 2: no shift. + offsets = [(i, j) for i in -2:2 for j in -2:2] + base_idx = (2, 5) + grid_sizes = (10, 10) + result = FastInterpolations._phs_clamp_offsets(offsets, base_idx, grid_sizes) + @test result !== offsets # was shifted + for off in result + @test 1 <= base_idx[1] + off[1] <= grid_sizes[1] + @test 1 <= base_idx[2] + off[2] <= grid_sizes[2] + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Direct unit tests for phs_kernels.jl internals +# ───────────────────────────────────────────────────────────────────────────── + +@testitem "PHS kernels β€” phi/phi_prime/phi_dprime direct (K=1, K=7, general K)" begin + # ── General K fallback (K not in {1,3,5,7}) ────────────────────────────── + # _phs_phi: r<=0 guard (line 28) and normal path (line 29) + @test FastInterpolations._phs_phi(0.0, Val{2}()) == 0.0 + @test FastInterpolations._phs_phi(2.0, Val{2}()) β‰ˆ 4.0 # 2^2 + + # _phs_phi_prime: r<=0 guard (line 55) and normal path (line 56) + @test FastInterpolations._phs_phi_prime(0.0, Val{2}()) == 0.0 + @test FastInterpolations._phs_phi_prime(3.0, Val{2}()) β‰ˆ 6.0 # 2*3^1 + + # _phs_phi_dprime: guard (line 80) and normal path (line 81) + @test FastInterpolations._phs_phi_dprime(0.0, Val{2}()) == 0.0 + @test FastInterpolations._phs_phi_dprime(2.0, Val{2}()) β‰ˆ 2.0 # 2*(2-1)*2^0 + + # ── K=1 specializations ────────────────────────────────────────────────── + # _phs_phi K=1 (line 35): just r + @test FastInterpolations._phs_phi(3.0, Val{1}()) β‰ˆ 3.0 + @test FastInterpolations._phs_phi(0.0, Val{1}()) == 0.0 + + # _phs_phi_prime K=1 (lines 58-60): r<=0 guard + one(T) + @test FastInterpolations._phs_phi_prime(0.0, Val{1}()) == 0.0 # guard + @test FastInterpolations._phs_phi_prime(3.0, Val{1}()) == 1.0 # one(T) + + # _phs_phi_dprime K=1 (line 83): always zero + @test FastInterpolations._phs_phi_dprime(0.0, Val{1}()) == 0.0 + @test FastInterpolations._phs_phi_dprime(5.0, Val{1}()) == 0.0 + + # _phs_phi_dprime K=3 (line 85): 6*r + @test FastInterpolations._phs_phi_dprime(2.0, Val{3}()) β‰ˆ 12.0 # 6*2 + @test FastInterpolations._phs_phi_dprime(0.0, Val{3}()) == 0.0 + + # ── K=7 specializations ────────────────────────────────────────────────── + # _phs_phi K=7 (lines 41-44): r4*r2*r = 2^7 = 128 + @test FastInterpolations._phs_phi(2.0, Val{7}()) β‰ˆ 128.0 + @test FastInterpolations._phs_phi(0.0, Val{7}()) == 0.0 + + # _phs_phi_prime K=7 (lines 68-70): 7*r2*r2*r2 = 7*64 = 448 + @test FastInterpolations._phs_phi_prime(2.0, Val{7}()) β‰ˆ 448.0 + @test FastInterpolations._phs_phi_prime(0.0, Val{7}()) == 0.0 + + # _phs_phi_dprime K=7 (lines 89-91): 42*r2*r2*r = 42*32 = 1344 + @test FastInterpolations._phs_phi_dprime(2.0, Val{7}()) β‰ˆ 1344.0 + @test FastInterpolations._phs_phi_dprime(0.0, Val{7}()) == 0.0 + + # ── Int dispatch wrappers (lines 96-97) ────────────────────────────────── + @test FastInterpolations._phs_phi_prime(3.0, 3) == FastInterpolations._phs_phi_prime(3.0, Val{3}()) + @test FastInterpolations._phs_phi_dprime(3.0, 3) == FastInterpolations._phs_phi_dprime(3.0, Val{3}()) + @test FastInterpolations._phs_phi_prime(2.0, 5) == FastInterpolations._phs_phi_prime(2.0, Val{5}()) + @test FastInterpolations._phs_phi_dprime(2.0, 5) == FastInterpolations._phs_phi_dprime(2.0, Val{5}()) +end + +@testitem "PHS kernels β€” blend weight 2-arg variants (dead-code coverage)" begin + # The 2-arg (d, a) overloads are never called by phs_eval.jl β€” it always + # uses the 3-arg (d, a, a3) forms with pre-computed a3. Call them directly + # to mark those source lines as covered. + + # _phs_blend_weight(d, a) β€” lines 129-133 + w_inside = FastInterpolations._phs_blend_weight(0.5, 1.0) + @test isfinite(w_inside) && 0.0 < w_inside < 1.0 + w_outside = FastInterpolations._phs_blend_weight(1.5, 1.0) # d >= a β†’ 0 + @test w_outside == 0.0 + w_eq = FastInterpolations._phs_blend_weight(1.0, 1.0) # d == a β†’ 0 + @test w_eq == 0.0 + + # _phs_blend_weight_and_prime(d, a) β€” lines 147-158 + w2, wp2 = FastInterpolations._phs_blend_weight_and_prime(0.5, 1.0) + @test isfinite(w2) && w2 > 0.0 + @test isfinite(wp2) && wp2 < 0.0 # weight is monotone decreasing + w2z, wp2z = FastInterpolations._phs_blend_weight_and_prime(2.0, 1.0) + @test w2z == 0.0 && wp2z == 0.0 + + # _phs_blend_weight_and_derivs(d, a) β€” lines 182-196 + w3, wp3, wpp3 = FastInterpolations._phs_blend_weight_and_derivs(0.5, 1.0) + @test isfinite(w3) && w3 > 0.0 + @test isfinite(wp3) + @test isfinite(wpp3) + # 2-arg and 3-arg versions should agree + a3 = 1.0^3 + w3b, wp3b, wpp3b = FastInterpolations._phs_blend_weight_and_derivs(0.5, 1.0, a3) + @test w3 β‰ˆ w3b && wp3 β‰ˆ wp3b && wpp3 β‰ˆ wpp3b + w3z, wp3z, wpp3z = FastInterpolations._phs_blend_weight_and_derivs(2.0, 1.0) + @test w3z == 0.0 && wp3z == 0.0 && wpp3z == 0.0 +end + +@testitem "PHS kernels β€” smoothing transform unrollers (dead-code coverage)" begin + # These helpers (_phs_unroll_value, _phs_unroll_grad_component, + # _phs_unroll_hess_component) are defined in phs_kernels.jl but are not + # called from phs_eval.jl β€” the log-transform unrolling is inlined directly. + # Call them directly to mark their source lines as covered. + + # _phs_unroll_value (lines 229-231) + rho = FastInterpolations._phs_unroll_value(1.0, 2.0) + @test rho β‰ˆ 2.0 * exp(1.0) + # early return: rho0 < 1e-40 + @test FastInterpolations._phs_unroll_value(1.0, 0.0) == 0.0 + # early return: f > 100 + @test FastInterpolations._phs_unroll_value(200.0, 1.0) == 0.0 + + # _phs_unroll_grad_component (lines 240-242) + rho_val = 2.0 * exp(1.0) + grad = FastInterpolations._phs_unroll_grad_component(rho_val, 0.3, 0.1, 2.0) + @test isfinite(grad) + @test grad β‰ˆ rho_val * (0.3 + 0.1 / 2.0) atol = 1.0e-12 + + # _phs_unroll_hess_component (lines 251, 261-263) + # rhoΒ·(f_hess + rho_xiΒ·rho_xj/rhoΒ² + rho0_hess/rho0 - rho0_xiΒ·rho0_xj/rho0Β²) + rho_g1 = 0.2; rho_g2 = 0.3 + rho0_g1 = 0.1; rho0_g2 = 0.15 + hess = FastInterpolations._phs_unroll_hess_component( + rho_val, 0.1, rho_g1, rho_g2, 2.0, rho0_g1, rho0_g2, 0.05 + ) + @test isfinite(hess) + expected = rho_val * ( + 0.1 + + rho_g1 * rho_g2 / (rho_val * rho_val) + + 0.05 / 2.0 - + rho0_g1 * rho0_g2 / (2.0 * 2.0) + ) + @test hess β‰ˆ expected atol = 1.0e-12 +end + +@testitem "PHS kernels β€” _phs_n_poly and mixed-partial deriv2 (ax1β‰ ax2)" begin + # _phs_n_poly (line 291): dead helper, never called from phs_stencil.jl + @test FastInterpolations._phs_n_poly(1, 0) == 1 # constant in 1D + @test FastInterpolations._phs_n_poly(2, 1) == 3 # linear in 2D + @test FastInterpolations._phs_n_poly(2, 2) == 6 # quadratic in 2D + @test FastInterpolations._phs_n_poly(3, 1) == 4 # linear in 3D + @test FastInterpolations._phs_n_poly(3, 3) == 20 # cubic in 3D = C(6,3) + + # _phs_eval_poly_deriv2 with ax1 β‰  ax2 (lines 422-428) + # K=5 gives quadratic augmentation (terms: 1, x, y, xΒ², xy, yΒ²). + # The xy cross-term ensures the ax1β‰ ax2 branch executes non-trivially. + # βˆ‚Β²(sin x Β· cos y)/βˆ‚xβˆ‚y = βˆ’cos x Β· sin y + x = range(0.0, 2Ο€, 20) + y = range(0.0, 2Ο€, 20) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 8, degree = 5) + qx, qy = 1.5, 1.5 + d2f_mixed = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{1}())) + @test d2f_mixed β‰ˆ -cos(qx) * sin(qy) atol = 0.1 +end + +@testitem "PHS kernels β€” K=1 and K=7 via PHS interpolant (phi/phi_prime/phi_dprime specializations)" begin + # K=1: covers _phs_phi(r, Val{1}()), _phs_phi_prime(r, Val{1}()), + # _phs_phi_dprime(r, Val{1}()) through the hot evaluation loop. + # A degree-1 PHS with constant augmentation exactly reproduces constants. + x = range(0.0, 5.0, 20) + data = fill(3.0, 20) # constant function + itp1 = phs_interp((x,), data; stencil_size = 4, degree = 1) + @test itp1((2.5,)) β‰ˆ 3.0 atol = 1.0e-6 + # First derivative of constant = 0 (triggers phi_prime K=1) + d1 = itp1((2.5,); deriv = (DerivOp{1}(),)) + @test abs(d1) < 1.0e-4 + # Second derivative (triggers phi_dprime K=1 β†’ always 0) + d2 = itp1((2.5,); deriv = (DerivOp{2}(),)) + @test abs(d2) < 1.0e-3 + + # K=3 second derivative: covers _phs_phi_dprime(r, Val{3}()) (line 85) + x3 = range(0.0, 2Ο€, 20) + data3 = sin.(collect(x3)) + itp3 = phs_interp((x3,), data3; stencil_size = 6, degree = 3) + d2_k3 = itp3((1.5,); deriv = (DerivOp{2}(),)) + @test d2_k3 β‰ˆ -sin(1.5) atol = 0.1 + + # K=7: covers _phs_phi(r, Val{7}()), _phs_phi_prime(r, Val{7}()), + # _phs_phi_dprime(r, Val{7}()) through the hot evaluation loop. + x7 = range(0.0, 2Ο€, 25) + data7 = cos.(collect(x7)) + itp7 = phs_interp((x7,), data7; stencil_size = 10, degree = 7) + # Value (triggers phi K=7) + @test itp7((1.0,)) β‰ˆ cos(1.0) atol = 1.0e-3 + # First derivative: d(cos x)/dx = -sin x (triggers phi_prime K=7) + d1_k7 = itp7((1.0,); deriv = (DerivOp{1}(),)) + @test d1_k7 β‰ˆ -sin(1.0) atol = 0.05 + # Second derivative: dΒ²(cos x)/dxΒ² = -cos x (triggers phi_dprime K=7) + d2_k7 = itp7((1.0,); deriv = (DerivOp{2}(),)) + @test d2_k7 β‰ˆ -cos(1.0) atol = 0.1 +end + +# ====================================================== +# Coverage: phs_eval.jl missed lines β€” Batch 1 +# ====================================================== + +@testitem "PHS eval β€” non-uniform grid (VectorSpacing binary search)" begin + # A Vector grid creates VectorSpacing which triggers the O(log n) binary + # search path in _phs_find_base_node instead of the O(1) ScalarSpacing formula. + x = collect(range(0.0, 2Ο€, 20)) # Vector{Float64} β†’ VectorSpacing + data = sin.(x) + itp = phs_interp((x,), data; stencil_size = 7, degree = 3) + @test itp.grids[1] isa FastInterpolations._CachedVector # ensure binary search path + for qx in [0.3, 1.0, 2.0, 4.0, 5.8] + @test itp((qx,)) β‰ˆ sin(qx) atol = 0.01 + end + # Derivative on non-uniform grid + @test itp((1.5,); deriv = (DerivOp{1}(),)) β‰ˆ cos(1.5) atol = 0.1 +end + +@testitem "PHS eval β€” dβ‰ˆ0 branches at grid node, no log-transform" begin + # Querying at EXACTLY a grid node makes d_dist = 0 for the base node, + # forcing the blend loop into the dβ‰ˆ0 else-branch. + # β€’ First derivative β†’ lines 586-588 in _phs_eval_blended + # β€’ Second derivative β†’ lines 652-655 in _phs_eval_blended + # (which calls _phs_eval_from_coeffs with total_order==2, + # covering lines 458-467, then _phs_eval_coeffs_deriv2, lines 174-210) + x = range(0.0, 2Ο€, 20) + y = range(0.0, 2Ο€, 20) + data = [sin(xi) + cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 6, degree = 3) + + qx = Float64(x[10]) # exact grid node β†’ d_dist = 0 for (10,10) + qy = Float64(y[10]) + + # First derivative at grid node β†’ lines 586-588 + d1 = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{0}())) + @test isfinite(d1) + @test d1 β‰ˆ cos(qx) atol = 0.3 + + # Diagonal second derivative at grid node β†’ lines 652-655 + # also triggers _phs_eval_from_coeffs total_order==2, lines 458-464, 174-190 + d2x = itp((qx, qy); deriv = (DerivOp{2}(), DerivOp{0}())) + @test isfinite(d2x) + @test d2x β‰ˆ -sin(qx) atol = 0.5 + + # Off-diagonal second derivative at grid node β†’ lines 652-655 + # triggers lines 465-467 in _phs_eval_from_coeffs and lines 191-210 in _phs_eval_coeffs_deriv2 + d2xy = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{1}())) + @test isfinite(d2xy) +end + +@testitem "PHS log-transform β€” second derivatives diagonal and off-diagonal" begin + # Exercises the entire second-derivative branch of _phs_eval_blended_G + # (lines 789-874) and _phs_eval_with_transform (lines 913-929). + x = range(0.2, Float64(Ο€), 20) + y = range(0.2, Float64(Ο€), 20) + rho = [2.0 + sin(xi) * cos(yj) for xi in x, yj in y] + ref = ConstantRef(1.0) + itp = phs_interp((x, y), rho; stencil_size = 6, degree = 3, reference_interp = ref) + + h = 1.0e-4 + qx, qy = 1.2, 1.0 + + # Diagonal βˆ‚Β²Ο/βˆ‚xΒ² β€” triggers is_diag=true path, lines 803-812, 862-866, 913-929 + d2xx = itp((qx, qy); deriv = (DerivOp{2}(), DerivOp{0}())) + d2xx_fd = (itp((qx + h, qy)) - 2itp((qx, qy)) + itp((qx - h, qy))) / h^2 + @test isfinite(d2xx) + @test d2xx β‰ˆ d2xx_fd atol = 1.0e-3 + + # Off-diagonal βˆ‚Β²Ο/βˆ‚xβˆ‚y β€” triggers is_diag=false path, lines 812-846, 868-872, 913-929 + d2xy = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{1}())) + d2xy_fd = (itp((qx + h, qy + h)) - itp((qx + h, qy)) - itp((qx, qy + h)) + itp((qx, qy))) / h^2 + @test isfinite(d2xy) + @test d2xy β‰ˆ d2xy_fd atol = 1.0e-3 +end + +@testitem "PHS log-transform β€” dβ‰ˆ0 at grid node" begin + # Querying at a grid node with a log-transform interpolant: + # β€’ First derivative β†’ dβ‰ˆ0 in _phs_eval_blended_G, lines 774-777 + # β€’ Second derivative β†’ dβ‰ˆ0 in _phs_eval_blended_G, lines 850-857 + # For diagonal 2nd deriv: also calls _phs_eval_from_coeffs with total_order==1 + # (via ops_d1_1 at line 854) β†’ lines 455-457, then _phs_eval_coeffs_deriv1, lines 132-156 + x = range(0.2, Float64(Ο€), 15) + y = range(0.2, Float64(Ο€), 15) + rho = [2.0 + sin(xi) * cos(yj) for xi in x, yj in y] + ref = ConstantRef(1.0) + itp = phs_interp((x, y), rho; stencil_size = 5, degree = 3, reference_interp = ref) + + qx = Float64(x[8]) # exact grid node + qy = Float64(y[8]) + + # First derivative at grid node β†’ dβ‰ˆ0 in _phs_eval_blended_G first-deriv branch (lines 774-777) + d1 = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{0}())) + @test isfinite(d1) + + # Diagonal second derivative at grid node β†’ dβ‰ˆ0 in _phs_eval_blended_G second-deriv branch + # (lines 850-857); also triggers ops_d1_1 path β†’ _phs_eval_coeffs_deriv1 (lines 132-156) + # and _phs_eval_from_coeffs total_order==1 (lines 455-457) + d2xx = itp((qx, qy); deriv = (DerivOp{2}(), DerivOp{0}())) + @test isfinite(d2xx) + + # Off-diagonal second derivative at grid node β†’ dβ‰ˆ0 branch (lines 850-857) + # is_diag=false so f_d1_sq = zero(Tv), but f_d2 calls _phs_eval_coeffs_deriv2 (off-diagonal) + d2xy = itp((qx, qy); deriv = (DerivOp{1}(), DerivOp{1}())) + @test isfinite(d2xy) +end + +@testitem "PHS eval β€” dead-code _phs_eval_coeffs_value_and_two_deriv1 + total_derivβ‰₯3 fallbacks" begin + # ── Setup: build a 2D interpolant and extract a solved stencil ────────── + x = range(0.0, 2Ο€, 15) + y = range(0.0, 2Ο€, 15) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3) + qx, qy = 1.5, 1.0 + + base_idx = FastInterpolations._phs_find_base_node(itp, (qx, qy)) + M = size(itp.phi_inv, 1) + rhs_buf = zeros(eltype(itp.hs[1]), M) + coeff_buf = zeros(eltype(itp.hs[1]), M) + offsets, phys_offsets, coeffs, hs = FastInterpolations._phs_solve_stencil!(itp, base_idx, rhs_buf, coeff_buf) + base_coords = FastInterpolations._phs_base_coords(itp, base_idx) + + # ── Dead-code function: _phs_eval_coeffs_value_and_two_deriv1 (lines 292-353) ── + # Never called from eval paths; replaced by the 4-return fused version. + # Call directly to exercise those lines. + val, dax1, dax2 = FastInterpolations._phs_eval_coeffs_value_and_two_deriv1( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 1, 2 + ) + @test isfinite(val) && isfinite(dax1) && isfinite(dax2) + # Cross-check against the non-fused versions (which are also tested here) + val_ref = FastInterpolations._phs_eval_coeffs_value( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}() + ) + d1x_ref = FastInterpolations._phs_eval_coeffs_deriv1( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 1 + ) + d1y_ref = FastInterpolations._phs_eval_coeffs_deriv1( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 2 + ) + @test val β‰ˆ val_ref atol = 1.0e-8 + @test dax1 β‰ˆ d1x_ref atol = 1.0e-8 + @test dax2 β‰ˆ d1y_ref atol = 1.0e-8 + + # ── _phs_eval_coeffs_deriv2: diagonal (lines 174-190) and off-diagonal (lines 191-210) ── + d2xx_direct = FastInterpolations._phs_eval_coeffs_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 1, 1 + ) + d2xy_direct = FastInterpolations._phs_eval_coeffs_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 1, 2 + ) + @test isfinite(d2xx_direct) && isfinite(d2xy_direct) + + # ── _phs_eval_from_coeffs: total_order==1 (lines 455-457) ── + v1 = FastInterpolations._phs_eval_from_coeffs( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), + (DerivOp{1}(), EvalValue()) + ) + @test v1 β‰ˆ d1x_ref atol = 1.0e-8 + + # ── _phs_eval_from_coeffs: total_order==2 diagonal (lines 458-464) ── + v2d = FastInterpolations._phs_eval_from_coeffs( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), + (DerivOp{2}(), EvalValue()) + ) + @test v2d β‰ˆ d2xx_direct atol = 1.0e-8 + + # ── _phs_eval_from_coeffs: total_order==2 off-diagonal (lines 465-467) ── + v2od = FastInterpolations._phs_eval_from_coeffs( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), + (DerivOp{1}(), DerivOp{1}()) + ) + @test v2od β‰ˆ d2xy_direct atol = 1.0e-8 + + # ── _phs_eval_from_coeffs: total_orderβ‰₯3 β†’ zero (line 470) ── + z_fc = FastInterpolations._phs_eval_from_coeffs( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), + (DerivOp{2}(), DerivOp{1}()) + ) + @test z_fc == 0.0 + + # ── _phs_eval_blended: total_derivβ‰₯3 fallback (line 674) ── + z_blended = itp((qx, qy); deriv = (DerivOp{2}(), DerivOp{1}())) + @test z_blended == 0.0 + + # ── _phs_eval_blended_G: total_derivβ‰₯3 fallback (line 874) ── + # Call the log-transform blend function directly with total_deriv=3 + rho2 = [2.0 + sin(xi) * cos(yj) for xi in x, yj in y] + itp_log = phs_interp( + (x, y), rho2; stencil_size = 5, degree = 3, + reference_interp = ConstantRef(1.0) + ) + z_G = FastInterpolations._phs_eval_blended_G( + itp_log, (qx, qy), (DerivOp{2}(), DerivOp{1}()) + ) + @test z_G == 0.0 + + # ── _phs_eval_with_transform: total_derivβ‰₯3 fallback (line 932) ── + z_tr = itp_log((qx, qy); deriv = (DerivOp{2}(), DerivOp{1}())) + @test z_tr == 0.0 + + # ── _phs_eval_coeffs_value_and_deriv1_and_deriv2: off-diagonal else-branch (lines 292-303) ── + # This function is always called with ax1==ax2 in the blended path (diagonal only). + # Call directly with ax1β‰ ax2 to exercise the off-diagonal loop. + val3, d1_3, d2_3 = FastInterpolations._phs_eval_coeffs_value_and_deriv1_and_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{3}(), 1, 2 + ) + @test isfinite(val3) && isfinite(d1_3) && isfinite(d2_3) + # Cross-check: value should match the zero-deriv version; d1 matches deriv1 along ax1 + @test val3 β‰ˆ val_ref atol = 1.0e-8 + @test d1_3 β‰ˆ d1x_ref atol = 1.0e-8 + @test d2_3 β‰ˆ d2xy_direct atol = 1.0e-8 +end + +@testitem "PHS eval β€” K!=3 and custom reference function coverage" begin + # ── Setup with degree=5 (K=5) so coefficients have correct size ── + x = range(0.0, 2Ο€, 15) + y = range(0.0, 2Ο€, 15) + data = [sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 5, degree = 5) + qx, qy = 1.5, 1.0 + + base_idx = FastInterpolations._phs_find_base_node(itp, (qx, qy)) + M = size(itp.phi_inv, 1) + rhs_buf = zeros(eltype(itp.hs[1]), M) + coeff_buf = zeros(eltype(itp.hs[1]), M) + offsets, phys_offsets, coeffs, hs = FastInterpolations._phs_solve_stencil!(itp, base_idx, rhs_buf, coeff_buf) + base_coords = FastInterpolations._phs_base_coords(itp, base_idx) + + # 1. _phs_eval_coeffs_deriv1 with K != 3 + d1_k5 = FastInterpolations._phs_eval_coeffs_deriv1( + coeffs, phys_offsets, (qx, qy), base_coords, Val{5}(), 1 + ) + @test isfinite(d1_k5) + + # 2. _phs_eval_coeffs_deriv2 with K != 3 (diagonal and off-diagonal) + d2xx_k5 = FastInterpolations._phs_eval_coeffs_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{5}(), 1, 1 + ) + d2xy_k5 = FastInterpolations._phs_eval_coeffs_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{5}(), 1, 2 + ) + @test isfinite(d2xx_k5) && isfinite(d2xy_k5) + + # 3. _phs_eval_coeffs_value_and_two_deriv1 with K != 3 + val_k5, d1x_k5, d1y_k5 = FastInterpolations._phs_eval_coeffs_value_and_two_deriv1( + coeffs, phys_offsets, (qx, qy), base_coords, Val{5}(), 1, 2 + ) + @test isfinite(val_k5) && isfinite(d1x_k5) && isfinite(d1y_k5) + + # 4. Custom reference object implementing standard keyword argument signature + struct CustomRefWithMethod + val::Float64 + end + (ref::CustomRefWithMethod)(query::Any; deriv = nothing) = deriv === nothing ? ref.val : (all(op -> op isa FastInterpolations.EvalValue, deriv) ? ref.val : 0.0) + + + # Let's test PHS with log-transform and this custom reference + ref_obj = CustomRefWithMethod(1.0) + rho_val = [2.0 + sin(xi) * cos(yj) for xi in x, yj in y] + itp_custom = phs_interp( + (x, y), rho_val; stencil_size = 5, degree = 3, + reference_interp = ref_obj + ) + @test itp_custom((1.5, 1.5)) > 0.0 + @test isfinite(itp_custom((1.5, 1.5); deriv = (DerivOp{1}(), EvalValue()))) + @test FastInterpolations._phs_eval_ref_deriv1(ref_obj, (1.5, 1.5), 1, Val(2), Float64) == 0.0 + + # 5. _phs_eval_coeffs_value_and_deriv1_and_deriv2 with K != 3 and ax1 != ax2 + v_k5, d1_k5, d2_k5 = FastInterpolations._phs_eval_coeffs_value_and_deriv1_and_deriv2( + coeffs, phys_offsets, (qx, qy), base_coords, Val{5}(), 1, 2 + ) + @test isfinite(v_k5) && isfinite(d1_k5) && isfinite(d2_k5) + + # 6. _phs_get_deriv1_axis_val fallback + @test FastInterpolations._phs_get_deriv1_axis_val(Tuple{EvalValue, EvalValue}) == Val{1}() +end + +@testitem "PHS eval β€” additional LocalCoverage gaps" begin + # Setup standard 2D PHS interpolant (K=3) and some coordinates/buffers + x = range(0.0, 2Ο€, 15) + y = range(0.0, 2Ο€, 15) + data = [2.0 + sin(xi) * cos(yj) for xi in x, yj in y] + itp = phs_interp((x, y), data; stencil_size = 5, degree = 3) + qx, qy = 1.5, 1.0 + query = (qx, qy) + base_idx = FastInterpolations._phs_find_base_node(itp, query) + base_coords = FastInterpolations._phs_base_coords(itp, base_idx) + + M = size(itp.phi_inv, 1) + rhs_buf = zeros(eltype(itp.hs[1]), M) + coeff_buf = zeros(eltype(itp.hs[1]), M) + offsets, phys_offsets, coeffs, hs = FastInterpolations._phs_solve_stencil!(itp, base_idx, rhs_buf, coeff_buf) + Ξ”x = ntuple(d -> Float64(query[d]) - base_coords[d], Val(2)) + poly_exps = FastInterpolations._phs_poly_exps_tuple(Val(2), Val(3)) + ns = length(phys_offsets) + + # 1. phs_eval.jl: lines 311-319 (_phs_eval_coeffs_value_and_deriv1 with Int axis) + v1, d1 = FastInterpolations._phs_eval_coeffs_value_and_deriv1( + coeffs, phys_offsets, query, base_coords, Val{3}(), 1 + ) + @test isfinite(v1) && isfinite(d1) + + # 2. phs_eval.jl: lines 556-565 (_phs_eval_coeffs_value_and_two_deriv1_and_deriv2 with Int axes) + v2, d2a, d2b, d2ab = FastInterpolations._phs_eval_coeffs_value_and_two_deriv1_and_deriv2( + coeffs, phys_offsets, query, base_coords, Val{3}(), 1, 2 + ) + @test isfinite(v2) && isfinite(d2a) && isfinite(d2b) && isfinite(d2ab) + + # 3. phs_eval.jl: lines 1175-1181 (_phs_get_deriv1_axis) + # Using a Tuple type with DerivOp{1} at index 2 + T_ops = Tuple{EvalValue, DerivOp{1}} + @test FastInterpolations._phs_get_deriv1_axis(T_ops) == 2 + # Fallback path (no DerivOp{1} in Tuple) + @test FastInterpolations._phs_get_deriv1_axis(Tuple{EvalValue, EvalValue}) == 1 + + # 4. phs_eval.jl: lines 1193-1210 (_phs_get_deriv2_axes) + # Case with DerivOp{1} at indices 1 and 2 + @test FastInterpolations._phs_get_deriv2_axes(Tuple{DerivOp{1}, DerivOp{1}}) == (1, 2) + # Case with DerivOp{2} at index 1 + @test FastInterpolations._phs_get_deriv2_axes(Tuple{DerivOp{2}, EvalValue}) == (1, 1) + + # 5. phs_eval.jl: lines 1238-1243 (_phs_eval_blended_G_with_grad with Int grad_ax) + g_val, g_deriv = FastInterpolations._phs_eval_blended_G_with_grad(itp, query, 1) + @test isfinite(g_val) && isfinite(g_deriv) + + # 6. phs_eval.jl: lines 1316-1322 (_phs_eval_blended_G_with_hess with Int axes) + gh, gh_d1, gh_d2, gh_d12 = FastInterpolations._phs_eval_blended_G_with_hess(itp, query, 1, 2) + @test isfinite(gh) && isfinite(gh_d1) && isfinite(gh_d2) && isfinite(gh_d12) + + # 7. phs_kernels.jl: lines 379-386 (_phs_eval_poly_deriv1 with Int axis) + p_d1 = FastInterpolations._phs_eval_poly_deriv1(Ξ”x, poly_exps, coeffs, ns, 1) + @test isfinite(p_d1) + + # 8. phs_kernels.jl: lines 415-423 (_phs_eval_poly_deriv2 with Int axes) + p_d2 = FastInterpolations._phs_eval_poly_deriv2(Ξ”x, poly_exps, coeffs, ns, 1, 2) + @test isfinite(p_d2) + + # 9. phs_kernels.jl: lines 469-476 (_phs_diff generated function) + diff_res = FastInterpolations._phs_diff(query, base_coords, (0, 0), itp.hs) + @test length(diff_res) == 2 +end