co2-eos 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {co2_eos-0.2.0 → co2_eos-0.3.1}/PKG-INFO +2 -2
- {co2_eos-0.2.0 → co2_eos-0.3.1}/README.md +1 -1
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/__init__.py +1 -1
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/core.py +57 -31
- co2_eos-0.3.1/co2_eos/data/chi_ref_cheb.npz +0 -0
- co2_eos-0.3.1/co2_eos/data/seed_table.npz +0 -0
- co2_eos-0.3.1/co2_eos/helmholtz.py +449 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/inversions.py +87 -27
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/transport.py +85 -19
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos.egg-info/PKG-INFO +2 -2
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos.egg-info/SOURCES.txt +4 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/pyproject.toml +1 -1
- co2_eos-0.3.1/tests/test_density_from_PT_branch.py +113 -0
- co2_eos-0.3.1/tests/test_fastpath_validation.py +92 -0
- co2_eos-0.3.1/tests/test_map_fresh.py +27 -0
- co2_eos-0.2.0/co2_eos/data/seed_table.npz +0 -0
- co2_eos-0.2.0/co2_eos/helmholtz.py +0 -337
- {co2_eos-0.2.0 → co2_eos-0.3.1}/LICENSE +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/data/saturation_table.npz +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/saturation.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos/span_wagner.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos.egg-info/dependency_links.txt +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos.egg-info/requires.txt +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/co2_eos.egg-info/top_level.txt +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/setup.cfg +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_accuracy_vs_coolprop.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_analytic_derivs.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_dome_detection.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_inversion_gradients.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_no_coolprop_runtime.py +0 -0
- {co2_eos-0.2.0 → co2_eos-0.3.1}/tests/test_rho_u_inversion.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: co2-eos
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Differentiable CO₂ thermodynamic properties in JAX
|
|
5
5
|
Author-email: John Patrick Therrien <john@flux-tech.de>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -83,7 +83,7 @@ But CoolProp is a C++ library with Python bindings. You can't `jax.grad` through
|
|
|
83
83
|
CO2-EOS solves this for CO₂ by implementing the same reference EOS (Span-Wagner 1996) directly in JAX:
|
|
84
84
|
|
|
85
85
|
- **Differentiable.** `jax.grad` through any property, any inversion, any combination. No finite differences. The inversions carry hand-written `custom_jvp` rules (implicit function theorem), so forward- and reverse-mode gradients are exact and cheap; validated against central finite differences.
|
|
86
|
-
- **Fast.** JIT-compiled and vectorisable, with hand-coded analytic α-derivatives instead of
|
|
86
|
+
- **Fast.** JIT-compiled and vectorisable, with hand-coded, transcendental-economized analytic α-derivatives: the Span-Wagner exponent structure (¼-integer τ-powers, integer δ-powers, shared exponential envelopes) is evaluated by sqrt/multiply ladders, so a full six-derivative bundle costs ~12 exps + 3 pows per point instead of ~150 array-exponent pows — the same arithmetic, re-associated, exact to round-off. For simulation codes that carry `(ρ, u)` as conserved variables, `properties_from_rho_u` fuses the whole primitive recovery: a dome-safe table seed plus three unrolled analytic-Cv Newton steps recover `T(ρ, u)` to 1e-12 K, and one shared derivative bundle feeds every property including the transport critical enhancement. On an Apple M2 Pro (float64) the full recovery runs **123 µs for a 64-point batch and 0.53 µs/point at large batches — 7.6–19.9× faster than the v0.1 autodiff path** and 2.6–6.3× faster than v0.2. On an OVH Tesla V100S the inversion reaches **63.7×** vs v0.1 at a 2²⁰-point batch (2 ns/point; full recovery 16 ns/point), and the launch-latency floor at small batches dropped ~1.7× (`bench/compare.py`, `bench/PROFILING.md`, validation in `docs/validation-fastpath-2026-07.md`).
|
|
87
87
|
- **Also fast for `(P, T)`.** The `state_from_PT` workflow that mirrors `PropsSI` includes an iterative density solve and flattens to a few tens of microseconds per point at large batches, roughly 2.7× faster than `CoolProp.PropsSI` in a Python loop on the same inputs (10,000-point batch, Apple M2 Pro; see `examples/launch_demo.ipynb`, section 2). On a Colab T4 GPU it runs at 27,800 states/sec on a 10⁶-point batch (36 μs/pt) versus 1,600 states/sec on Colab CPU (615 μs/pt), a 17× speedup. Its ceiling is the density solve's data-dependent `while_loop` (lockstep across GPU warps); the feed-forward `(ρ, u)` hot path above does not have that bottleneck.
|
|
88
88
|
- **Composable.** `jit`, `vmap`, `grad`, `custom_vjp`: the full JAX transformation stack works. Embed property evaluations inside your own JIT-compiled simulation and differentiate end-to-end.
|
|
89
89
|
- **Phase-aware.** Robust inversions near the critical point using Halley's method with step damping and bisection fallback. Two-phase dome detection that avoids convergence to thermodynamically unstable spinodal states.
|
|
@@ -49,7 +49,7 @@ But CoolProp is a C++ library with Python bindings. You can't `jax.grad` through
|
|
|
49
49
|
CO2-EOS solves this for CO₂ by implementing the same reference EOS (Span-Wagner 1996) directly in JAX:
|
|
50
50
|
|
|
51
51
|
- **Differentiable.** `jax.grad` through any property, any inversion, any combination. No finite differences. The inversions carry hand-written `custom_jvp` rules (implicit function theorem), so forward- and reverse-mode gradients are exact and cheap; validated against central finite differences.
|
|
52
|
-
- **Fast.** JIT-compiled and vectorisable, with hand-coded analytic α-derivatives instead of
|
|
52
|
+
- **Fast.** JIT-compiled and vectorisable, with hand-coded, transcendental-economized analytic α-derivatives: the Span-Wagner exponent structure (¼-integer τ-powers, integer δ-powers, shared exponential envelopes) is evaluated by sqrt/multiply ladders, so a full six-derivative bundle costs ~12 exps + 3 pows per point instead of ~150 array-exponent pows — the same arithmetic, re-associated, exact to round-off. For simulation codes that carry `(ρ, u)` as conserved variables, `properties_from_rho_u` fuses the whole primitive recovery: a dome-safe table seed plus three unrolled analytic-Cv Newton steps recover `T(ρ, u)` to 1e-12 K, and one shared derivative bundle feeds every property including the transport critical enhancement. On an Apple M2 Pro (float64) the full recovery runs **123 µs for a 64-point batch and 0.53 µs/point at large batches — 7.6–19.9× faster than the v0.1 autodiff path** and 2.6–6.3× faster than v0.2. On an OVH Tesla V100S the inversion reaches **63.7×** vs v0.1 at a 2²⁰-point batch (2 ns/point; full recovery 16 ns/point), and the launch-latency floor at small batches dropped ~1.7× (`bench/compare.py`, `bench/PROFILING.md`, validation in `docs/validation-fastpath-2026-07.md`).
|
|
53
53
|
- **Also fast for `(P, T)`.** The `state_from_PT` workflow that mirrors `PropsSI` includes an iterative density solve and flattens to a few tens of microseconds per point at large batches, roughly 2.7× faster than `CoolProp.PropsSI` in a Python loop on the same inputs (10,000-point batch, Apple M2 Pro; see `examples/launch_demo.ipynb`, section 2). On a Colab T4 GPU it runs at 27,800 states/sec on a 10⁶-point batch (36 μs/pt) versus 1,600 states/sec on Colab CPU (615 μs/pt), a 17× speedup. Its ceiling is the density solve's data-dependent `while_loop` (lockstep across GPU warps); the feed-forward `(ρ, u)` hot path above does not have that bottleneck.
|
|
54
54
|
- **Composable.** `jit`, `vmap`, `grad`, `custom_vjp`: the full JAX transformation stack works. Embed property evaluations inside your own JIT-compiled simulation and differentiate end-to-end.
|
|
55
55
|
- **Phase-aware.** Robust inversions near the critical point using Halley's method with step damping and bisection fallback. Two-phase dome detection that avoids convergence to thermodynamically unstable spinodal states.
|
|
@@ -52,7 +52,7 @@ import jax.numpy as jnp
|
|
|
52
52
|
# Enable float64 for the entire library (also done by submodules).
|
|
53
53
|
jax.config.update("jax_enable_x64", True)
|
|
54
54
|
|
|
55
|
-
__version__ = "0.
|
|
55
|
+
__version__ = "0.3.1"
|
|
56
56
|
|
|
57
57
|
# ── Phase hint constants ────────────────────────────────────────────────────
|
|
58
58
|
LIQUID = 0
|
|
@@ -14,12 +14,13 @@ Design:
|
|
|
14
14
|
* The Newton inner loop touches only τ-derivatives (`helmholtz.residual_tau_*`)
|
|
15
15
|
with the δ-dependent envelopes precomputed once (they are loop-invariant at
|
|
16
16
|
fixed ρ) — all u and Cv need at fixed density.
|
|
17
|
-
* A precomputed (ρ, u) → T₀ table
|
|
18
|
-
|
|
19
|
-
round-off
|
|
20
|
-
|
|
21
|
-
the
|
|
22
|
-
|
|
17
|
+
* A precomputed (ρ, u) → T₀ table (dome-safe, uniform grid) seeds Newton so
|
|
18
|
+
that a short FIXED, unrolled iteration (no `lax.while_loop`) reaches
|
|
19
|
+
float64 round-off — measured 8.5e-13 K max over the single-phase envelope
|
|
20
|
+
at 3 steps. Fixed iteration keeps the GPU kernel uniform and branchless;
|
|
21
|
+
the table seed is purely a convergence accelerator (the polish sets
|
|
22
|
+
accuracy and the IFT JVP sets gradients), so it carries no accuracy or
|
|
23
|
+
differentiability risk.
|
|
23
24
|
* Gradients use the implicit function theorem via `custom_jvp`, with the
|
|
24
25
|
Jacobian entries (Cv, ∂u/∂ρ) taken analytically — correct jvp and vjp.
|
|
25
26
|
|
|
@@ -39,10 +40,13 @@ from co2_eos.span_wagner import TC, RHOC, R, T_TRIPLE
|
|
|
39
40
|
from co2_eos import helmholtz as hz
|
|
40
41
|
from co2_eos import transport as _tr
|
|
41
42
|
|
|
42
|
-
# Fixed Newton iterations for the (ρ, u) → T inversion.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
43
|
+
# Fixed Newton iterations for the (ρ, u) → T inversion. With the dome-filled
|
|
44
|
+
# 256×512 table seed, 3 analytic-Newton steps reach float64 round-off over the
|
|
45
|
+
# single-phase envelope (measured max 8.5e-13 K on 40k samples incl. the
|
|
46
|
+
# critical neighbourhood; see bench/proto_seed_v2.py and tests). The affine
|
|
47
|
+
# fallback seed is coarse (±tens of K), so it gets a longer fixed iteration.
|
|
48
|
+
_NEWTON_ITERS_TABLE = 3
|
|
49
|
+
_NEWTON_ITERS_AFFINE = 8
|
|
46
50
|
_T_MIN = T_TRIPLE
|
|
47
51
|
_T_MAX = 800.0
|
|
48
52
|
|
|
@@ -62,30 +66,51 @@ _AFFINE_A, _AFFINE_B = 255.6, 1.656e-4
|
|
|
62
66
|
_HAVE_SEED = False
|
|
63
67
|
try:
|
|
64
68
|
_seed = np.load(_SEED_PATH)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
# Stored float32 (seed-only precision; halves the embedded-constant
|
|
70
|
+
# footprint, which XLA:CPU gathers care about). Kept flat so the four
|
|
71
|
+
# bilinear corners come from ONE gather of (base + static offsets) —
|
|
72
|
+
# four separate gather ops cost ~1.3 µs each per call.
|
|
73
|
+
_SEED_T0_FLAT = jnp.asarray(_seed["T0_table"], dtype=jnp.float32).ravel()
|
|
74
|
+
_np_rho = np.asarray(_seed["rho_grid"])
|
|
75
|
+
_np_u = np.asarray(_seed["u_grid"])
|
|
76
|
+
_SEED_NRHO = int(_np_rho.shape[0])
|
|
77
|
+
_SEED_NU = int(_np_u.shape[0])
|
|
78
|
+
# The grids are uniform (linspace) — index by arithmetic, not searchsorted.
|
|
79
|
+
_SEED_R_LO = float(_np_rho[0])
|
|
80
|
+
_SEED_R_STEP = float(_np_rho[1] - _np_rho[0])
|
|
81
|
+
_SEED_U_LO = float(_np_u[0])
|
|
82
|
+
_SEED_U_STEP = float(_np_u[1] - _np_u[0])
|
|
83
|
+
for _g, _step in ((_np_rho, _SEED_R_STEP), (_np_u, _SEED_U_STEP)):
|
|
84
|
+
if not np.allclose(np.diff(_g), _step, rtol=1e-12):
|
|
85
|
+
raise ValueError("seed table grids must be uniform")
|
|
70
86
|
_HAVE_SEED = True
|
|
71
87
|
except FileNotFoundError:
|
|
72
88
|
pass
|
|
73
89
|
|
|
74
90
|
|
|
75
91
|
def _seed_T(rho, u):
|
|
76
|
-
"""Initial T guess for the (ρ, u) inversion via the bilinear table.
|
|
92
|
+
"""Initial T guess for the (ρ, u) inversion via the bilinear table.
|
|
93
|
+
|
|
94
|
+
Uniform-grid direct indexing (the generator writes linspace grids), with
|
|
95
|
+
the query clamped to the table box — outside it the Newton polish still
|
|
96
|
+
owns the accuracy, the seed is only a starting point.
|
|
97
|
+
"""
|
|
77
98
|
if not _HAVE_SEED:
|
|
78
99
|
return jnp.clip(_AFFINE_A + _AFFINE_B * u, _T_MIN, _T_MAX)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
fi = jnp.clip((rho - _SEED_R_LO) / _SEED_R_STEP, 0.0,
|
|
101
|
+
_SEED_NRHO - 1 - 1e-9)
|
|
102
|
+
fj = jnp.clip((u - _SEED_U_LO) / _SEED_U_STEP, 0.0, _SEED_NU - 1 - 1e-9)
|
|
103
|
+
ri = fi.astype(jnp.int32)
|
|
104
|
+
uj = fj.astype(jnp.int32)
|
|
105
|
+
fr = fi - ri
|
|
106
|
+
fu = fj - uj
|
|
107
|
+
base = ri * _SEED_NU + uj
|
|
108
|
+
corners = _SEED_T0_FLAT[base + jnp.array([0, 1, _SEED_NU, _SEED_NU + 1],
|
|
109
|
+
dtype=jnp.int32)]
|
|
110
|
+
c00, c01, c10, c11 = (corners[0].astype(jnp.float64),
|
|
111
|
+
corners[1].astype(jnp.float64),
|
|
112
|
+
corners[2].astype(jnp.float64),
|
|
113
|
+
corners[3].astype(jnp.float64))
|
|
89
114
|
return ((c00 * (1.0 - fr) + c10 * fr) * (1.0 - fu)
|
|
90
115
|
+ (c01 * (1.0 - fr) + c11 * fr) * fu)
|
|
91
116
|
|
|
@@ -148,22 +173,23 @@ def _solve_T(rho, u):
|
|
|
148
173
|
"""Fixed-iteration Newton solve for T given (ρ, u). Scalar.
|
|
149
174
|
|
|
150
175
|
δ-invariant envelopes are precomputed once; each step evaluates only the
|
|
151
|
-
τ-dependent transcendentals.
|
|
176
|
+
τ-dependent transcendentals. The short fixed count is unrolled (no loop
|
|
177
|
+
construct) so XLA fuses the steps and the kernel stays branchless.
|
|
152
178
|
"""
|
|
153
179
|
delta = rho / RHOC
|
|
154
180
|
dstate = hz.residual_tau_prep(delta)
|
|
155
181
|
T = _seed_T(rho, u)
|
|
182
|
+
iters = _NEWTON_ITERS_TABLE if _HAVE_SEED else _NEWTON_ITERS_AFFINE
|
|
156
183
|
|
|
157
|
-
|
|
184
|
+
for _ in range(iters):
|
|
158
185
|
tau = TC / T
|
|
159
186
|
a0_t, a0_tt = hz.ideal_tau_only(tau)
|
|
160
187
|
ar_t, ar_tt = hz.residual_tau_fast(tau, dstate)
|
|
161
188
|
f = R * TC * (a0_t + ar_t) - u
|
|
162
189
|
cv = -R * tau ** 2 * (a0_tt + ar_tt)
|
|
163
190
|
cv = jnp.where(jnp.abs(cv) > 1e-30, cv, 1e-30)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return jax.lax.fori_loop(0, _NEWTON_ITERS, step, T)
|
|
191
|
+
T = jnp.clip(T - f / cv, _T_MIN, _T_MAX)
|
|
192
|
+
return T
|
|
167
193
|
|
|
168
194
|
|
|
169
195
|
# ── Implicit differentiation via custom_jvp ─────────────────────────────────
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Span-Wagner (1996) reduced Helmholtz energy with hand-coded analytic
|
|
2
|
+
derivatives — pure JAX, transcendental-economized.
|
|
3
|
+
|
|
4
|
+
This is the numerical core of the EOS hot path. It computes α and all the
|
|
5
|
+
derivatives a property/inversion pass needs — α_δ, α_τ, α_δδ, α_ττ, α_δτ —
|
|
6
|
+
analytically, and exploits the *structure* of the Span-Wagner exponents so the
|
|
7
|
+
per-point transcendental count collapses:
|
|
8
|
+
|
|
9
|
+
* every τ-exponent in the polynomial/exponential/Gaussian terms is a
|
|
10
|
+
multiple of ¼ → all τ-powers come from ``sqrt(sqrt(τ))`` plus a multiply
|
|
11
|
+
ladder (zero ``pow`` calls);
|
|
12
|
+
* every δ-exponent is a small integer (d ≤ 10, l ≤ 6) → multiply ladders;
|
|
13
|
+
* the 27 ``exp(-δ^l)`` envelopes take only six distinct values (l = 1..6);
|
|
14
|
+
* the five Gaussian exponentials share (η, β, γ) pairwise → four distinct;
|
|
15
|
+
* the non-analytic terms share θ (A, β identical across terms) and terms
|
|
16
|
+
40/41 share Δ; Δ^(7/8) is a sqrt ladder; only Δ^0.925, s^(5/3) and
|
|
17
|
+
s^(2/3) remain as real ``pow`` calls.
|
|
18
|
+
|
|
19
|
+
The structural facts are asserted at import against the coefficient tables in
|
|
20
|
+
``span_wagner`` (the single source of truth) — if the tables ever change shape,
|
|
21
|
+
import fails loudly rather than computing a stale factorization.
|
|
22
|
+
|
|
23
|
+
The result is the same Span-Wagner sum re-associated: agreement with the
|
|
24
|
+
autodiff derivatives of ``span_wagner.alphar`` / ``alpha0`` is round-off level
|
|
25
|
+
(~1e-12; see ``tests/test_analytic_derivs.py``), and every function remains
|
|
26
|
+
JIT-compilable, vmappable and differentiable to higher order.
|
|
27
|
+
|
|
28
|
+
Convention: τ = Tc/T (reduced inverse temperature), δ = ρ/ρc (reduced
|
|
29
|
+
density). Derivatives are plain partials w.r.t. τ and δ (not the δ·∂/∂δ
|
|
30
|
+
reduced form), matching the formulas in ``span_wagner``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import jax
|
|
34
|
+
import jax.numpy as jnp
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
jax.config.update("jax_enable_x64", True)
|
|
38
|
+
|
|
39
|
+
# Pull the coefficient tables straight from span_wagner so there is a single
|
|
40
|
+
# source of truth for the constants.
|
|
41
|
+
from co2_eos.span_wagner import (
|
|
42
|
+
_A0_A1, _A0_A2, _A0_LOGTAU, _A0_PE_N, _A0_PE_T,
|
|
43
|
+
_AR_N, _AR_D, _AR_T, _AR_L,
|
|
44
|
+
_AR_GAUSS_N, _AR_GAUSS_D, _AR_GAUSS_T,
|
|
45
|
+
_AR_GAUSS_ETA, _AR_GAUSS_BETA, _AR_GAUSS_GAMMA, _AR_GAUSS_EPS,
|
|
46
|
+
_NA_N, _NA_A, _NA_B, _NA_BETA, _NA_BIG_A, _NA_BIG_B, _NA_BIG_C, _NA_BIG_D,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# ── Static (Python-scalar) views of the tables — folded into the trace ─────
|
|
50
|
+
_PN = np.asarray(_AR_N)
|
|
51
|
+
_PD = np.asarray(_AR_D).astype(int)
|
|
52
|
+
_PT = np.asarray(_AR_T)
|
|
53
|
+
_PL = np.asarray(_AR_L).astype(int)
|
|
54
|
+
|
|
55
|
+
_GN = np.asarray(_AR_GAUSS_N)
|
|
56
|
+
_GD = np.asarray(_AR_GAUSS_D).astype(int)
|
|
57
|
+
_GT = np.asarray(_AR_GAUSS_T)
|
|
58
|
+
_GETA = np.asarray(_AR_GAUSS_ETA)
|
|
59
|
+
_GBETA = np.asarray(_AR_GAUSS_BETA)
|
|
60
|
+
_GGAMMA = np.asarray(_AR_GAUSS_GAMMA)
|
|
61
|
+
|
|
62
|
+
_NAN = np.asarray(_NA_N)
|
|
63
|
+
_NAA = np.asarray(_NA_A)
|
|
64
|
+
_NAB = np.asarray(_NA_B)
|
|
65
|
+
_NABIGB = np.asarray(_NA_BIG_B)
|
|
66
|
+
_NABIGC = np.asarray(_NA_BIG_C)
|
|
67
|
+
|
|
68
|
+
# Structural facts the economization relies on — asserted, not assumed.
|
|
69
|
+
assert np.all(_PD == np.asarray(_AR_D)) and _PD.max() <= 10, "δ-exponent not a small integer"
|
|
70
|
+
assert np.all(_PL == np.asarray(_AR_L)) and _PL.max() <= 6, "l-exponent not a small integer"
|
|
71
|
+
assert np.all(np.abs(_PT * 4 - np.round(_PT * 4)) < 1e-12), "τ-exponent not a multiple of ¼"
|
|
72
|
+
assert np.all(np.abs(_GT * 4 - np.round(_GT * 4)) < 1e-12)
|
|
73
|
+
assert np.all(np.asarray(_AR_GAUSS_EPS) == 1.0), "Gaussian ε ≠ 1"
|
|
74
|
+
_NA_P_SCALAR = float(1.0 / (2.0 * np.asarray(_NA_BETA)[0]))
|
|
75
|
+
assert np.all(np.asarray(_NA_BETA) == np.asarray(_NA_BETA)[0]), "NA β not shared"
|
|
76
|
+
_NA_BIGA_SCALAR = float(np.asarray(_NA_BIG_A)[0])
|
|
77
|
+
assert np.all(np.asarray(_NA_BIG_A) == _NA_BIGA_SCALAR), "NA A not shared"
|
|
78
|
+
_NA_BIGD_SCALAR = float(np.asarray(_NA_BIG_D)[0])
|
|
79
|
+
assert np.all(np.asarray(_NA_BIG_D) == _NA_BIGD_SCALAR), "NA D not shared"
|
|
80
|
+
assert set(np.round(_NAA * 2).astype(int)) <= {6, 7}, "NA a must be 3 or 3.5"
|
|
81
|
+
assert _NAB[0] == 0.875 and _NAB[2] == 0.875, "Δ^(7/8) sqrt ladder assumption"
|
|
82
|
+
|
|
83
|
+
# Reciprocal of (2β) for the non-analytic θ exponent, precomputed (array view
|
|
84
|
+
# kept for external callers/tests; the kernels use the shared scalar).
|
|
85
|
+
_NA_P = 1.0 / (2.0 * _NA_BETA)
|
|
86
|
+
|
|
87
|
+
# Distinct τ-exponents of the polynomial+exponential terms, and per-exponent
|
|
88
|
+
# static weights for the τ-derivative dot products.
|
|
89
|
+
_TPOLY = sorted(set(_PT.tolist()))
|
|
90
|
+
_TP_IDX = {t: j for j, t in enumerate(_TPOLY)}
|
|
91
|
+
_TALL = sorted(set(_TPOLY) | set(_GT.tolist()))
|
|
92
|
+
_W_T = np.array(_TPOLY) # t_j
|
|
93
|
+
_W_TT = np.array([t * (t - 1.0) for t in _TPOLY])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _tau_ladder(tau):
|
|
97
|
+
"""All distinct τ^t needed by the terms, via 2 sqrts + multiplies.
|
|
98
|
+
|
|
99
|
+
Every exponent is a multiple of ¼, so τ^t = τ^int · (τ^¼)^frac with the
|
|
100
|
+
integer parts built by a multiply chain over increasing exponents.
|
|
101
|
+
"""
|
|
102
|
+
st = jnp.sqrt(tau)
|
|
103
|
+
qt = jnp.sqrt(st)
|
|
104
|
+
p = {0.0: jnp.ones_like(tau), 0.25: qt, 0.5: st, 0.75: st * qt, 1.0: tau}
|
|
105
|
+
for t in _TALL:
|
|
106
|
+
if t in p:
|
|
107
|
+
continue
|
|
108
|
+
if t == int(t):
|
|
109
|
+
t_int = int(t)
|
|
110
|
+
half = float(t_int // 2)
|
|
111
|
+
other = float(t_int - t_int // 2)
|
|
112
|
+
if half in p and other in p:
|
|
113
|
+
p[t] = p[half] * p[other]
|
|
114
|
+
else:
|
|
115
|
+
avail = max(k for k in p if k == int(k) and 0 < k <= t)
|
|
116
|
+
acc, rem = p[avail], t_int - int(avail)
|
|
117
|
+
while rem > 0:
|
|
118
|
+
step = max(k for k in p if k == int(k) and 0 < k <= rem)
|
|
119
|
+
acc = acc * p[step]
|
|
120
|
+
rem -= int(step)
|
|
121
|
+
p[t] = acc
|
|
122
|
+
else:
|
|
123
|
+
base, frac = float(int(t)), t - int(t)
|
|
124
|
+
p[t] = p[base] * p[frac] # base already built (sorted order)
|
|
125
|
+
return p
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _delta_ladder(delta, dmax=10):
|
|
129
|
+
"""δ^0..δ^dmax by successive multiplication."""
|
|
130
|
+
p = {0: jnp.ones_like(delta), 1: delta}
|
|
131
|
+
for k in range(2, dmax + 1):
|
|
132
|
+
p[k] = p[k - 1] * delta
|
|
133
|
+
return p
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _exp_envelopes(dp):
|
|
137
|
+
"""The six distinct exponential envelopes E_l = exp(-δ^l), l = 0..6."""
|
|
138
|
+
E = {0: jnp.ones_like(dp[1])}
|
|
139
|
+
for l in range(1, 7):
|
|
140
|
+
E[l] = jnp.exp(-dp[l])
|
|
141
|
+
return E
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
145
|
+
# Ideal-gas part α⁰ — τ derivatives (δ part is only ln δ, not needed here)
|
|
146
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
+
|
|
148
|
+
def ideal_derivs(tau, delta):
|
|
149
|
+
"""Return (α⁰, α⁰_τ, α⁰_ττ) at scalar (τ, δ).
|
|
150
|
+
|
|
151
|
+
α⁰ = a1 + a2·τ + L·ln τ + ln δ + Σ nk·ln(1 - exp(-θk·τ))
|
|
152
|
+
Only the τ-derivatives and the value are used by the property/inversion
|
|
153
|
+
formulas (the ideal δ-dependence cancels out of every measurable property
|
|
154
|
+
except through the additive ln δ in the value, which entropy/Gibbs need).
|
|
155
|
+
"""
|
|
156
|
+
L = _A0_LOGTAU
|
|
157
|
+
e = jnp.exp(-_A0_PE_T * tau) # exp(-θk τ)
|
|
158
|
+
one_minus_e = 1.0 - e
|
|
159
|
+
|
|
160
|
+
a0 = (_A0_A1 + _A0_A2 * tau + L * jnp.log(tau) + jnp.log(delta)
|
|
161
|
+
+ jnp.sum(_A0_PE_N * jnp.log(one_minus_e)))
|
|
162
|
+
|
|
163
|
+
# d/dτ ln(1-e) = θ·e/(1-e)
|
|
164
|
+
a0_t = (_A0_A2 + L / tau
|
|
165
|
+
+ jnp.sum(_A0_PE_N * _A0_PE_T * e / one_minus_e))
|
|
166
|
+
|
|
167
|
+
# d²/dτ² ln(1-e) = -θ²·e/(1-e)²
|
|
168
|
+
a0_tt = (-L / tau ** 2
|
|
169
|
+
- jnp.sum(_A0_PE_N * _A0_PE_T ** 2 * e / one_minus_e ** 2))
|
|
170
|
+
|
|
171
|
+
return a0, a0_t, a0_tt
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ideal_tau_only(tau):
|
|
175
|
+
"""Return (α⁰_τ, α⁰_ττ) — the only ideal quantities the Newton needs.
|
|
176
|
+
|
|
177
|
+
Independent of δ, so cheaper than ``ideal_derivs`` for the inner loop.
|
|
178
|
+
"""
|
|
179
|
+
L = _A0_LOGTAU
|
|
180
|
+
e = jnp.exp(-_A0_PE_T * tau)
|
|
181
|
+
one_minus_e = 1.0 - e
|
|
182
|
+
a0_t = (_A0_A2 + L / tau
|
|
183
|
+
+ jnp.sum(_A0_PE_N * _A0_PE_T * e / one_minus_e))
|
|
184
|
+
a0_tt = (-L / tau ** 2
|
|
185
|
+
- jnp.sum(_A0_PE_N * _A0_PE_T ** 2 * e / one_minus_e ** 2))
|
|
186
|
+
return a0_t, a0_tt
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
190
|
+
# Non-analytic terms — shared helpers
|
|
191
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
192
|
+
|
|
193
|
+
def _pow78(Db):
|
|
194
|
+
"""Db^(7/8) by three sqrts and three multiplies (Db ≥ 1e-300 guarded)."""
|
|
195
|
+
r8 = jnp.sqrt(jnp.sqrt(jnp.sqrt(Db)))
|
|
196
|
+
q2 = r8 * r8
|
|
197
|
+
q4 = q2 * q2
|
|
198
|
+
return q4 * q2 * r8
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _na_delta_state(delta):
|
|
202
|
+
"""δ-side quantities of the non-analytic terms (loop-invariant at fixed ρ).
|
|
203
|
+
|
|
204
|
+
Returns (s, dme, s_p, Bsa, e_cs) where Bsa[(a, B)] = B·s^a and
|
|
205
|
+
e_cs[C] = exp(-C·s) for the distinct (a, B) / C among the three terms.
|
|
206
|
+
"""
|
|
207
|
+
dme = delta - 1.0
|
|
208
|
+
s = dme * dme
|
|
209
|
+
s_p = s ** _NA_P_SCALAR # s^(5/3)
|
|
210
|
+
s3 = s * s * s
|
|
211
|
+
abs_dm1 = jnp.abs(dme)
|
|
212
|
+
sa = {3.5: s3 * abs_dm1, 3.0: s3} # s^a by multiplies
|
|
213
|
+
Bsa = {}
|
|
214
|
+
for k in range(3):
|
|
215
|
+
key = (float(_NAA[k]), float(_NABIGB[k]))
|
|
216
|
+
if key not in Bsa:
|
|
217
|
+
Bsa[key] = key[1] * sa[key[0]]
|
|
218
|
+
e_cs = {float(C): jnp.exp(-float(C) * s) for C in sorted(set(_NABIGC.tolist()))}
|
|
219
|
+
return s, dme, s_p, Bsa, e_cs
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
223
|
+
# Residual part αʳ — full analytic derivative bundle (one economized pass)
|
|
224
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
def residual_derivs(tau, delta):
|
|
227
|
+
"""Return (αʳ, αʳ_δ, αʳ_τ, αʳ_δδ, αʳ_ττ, αʳ_δτ) at scalar (τ, δ).
|
|
228
|
+
|
|
229
|
+
All six are accumulated from the same per-term values; the transcendental
|
|
230
|
+
budget per point is ~12 exps + ~8 sqrts + 3 pows (see module docstring).
|
|
231
|
+
"""
|
|
232
|
+
inv_d = 1.0 / delta
|
|
233
|
+
inv_d2 = inv_d * inv_d
|
|
234
|
+
inv_t = 1.0 / tau
|
|
235
|
+
inv_t2 = inv_t * inv_t
|
|
236
|
+
|
|
237
|
+
tp = _tau_ladder(tau)
|
|
238
|
+
dp = _delta_ladder(delta)
|
|
239
|
+
E = _exp_envelopes(dp)
|
|
240
|
+
|
|
241
|
+
ar = ar_d = ar_t = ar_dd = ar_tt = ar_dt = 0.0
|
|
242
|
+
|
|
243
|
+
# ── Polynomial + exponential terms (static unroll; constants folded) ──
|
|
244
|
+
for k in range(len(_PN)):
|
|
245
|
+
n, d, t, l = float(_PN[k]), int(_PD[k]), float(_PT[k]), int(_PL[k])
|
|
246
|
+
Tk = n * dp[d] * tp[t] * E[l]
|
|
247
|
+
ld = l * dp[l] if l else 0.0 # l·δ^l (0 when l = 0)
|
|
248
|
+
D1 = (d - ld) * inv_d
|
|
249
|
+
D1p = (-d - (l * (l - 1) * dp[l] if l else 0.0)) * inv_d2
|
|
250
|
+
Tt = t * inv_t
|
|
251
|
+
ar += Tk
|
|
252
|
+
ar_d += Tk * D1
|
|
253
|
+
ar_t += Tk * Tt
|
|
254
|
+
ar_dd += Tk * (D1 * D1 + D1p)
|
|
255
|
+
ar_tt += Tk * (t * (t - 1.0) * inv_t2)
|
|
256
|
+
ar_dt += Tk * D1 * Tt
|
|
257
|
+
|
|
258
|
+
# ── Gaussian bell-shaped terms (four distinct exponentials) ──
|
|
259
|
+
dme = delta - 1.0 # ε = 1 for all five terms
|
|
260
|
+
dme2 = dme * dme
|
|
261
|
+
e_g = {}
|
|
262
|
+
for k in range(5):
|
|
263
|
+
key = (float(_GETA[k]), float(_GBETA[k]), float(_GGAMMA[k]))
|
|
264
|
+
if key not in e_g:
|
|
265
|
+
tmg = tau - key[2]
|
|
266
|
+
e_g[key] = (tmg, jnp.exp(-key[0] * dme2 - key[1] * tmg * tmg))
|
|
267
|
+
for k in range(5):
|
|
268
|
+
n, d, t = float(_GN[k]), int(_GD[k]), float(_GT[k])
|
|
269
|
+
eta, beta = float(_GETA[k]), float(_GBETA[k])
|
|
270
|
+
tmg, eg = e_g[(eta, beta, float(_GGAMMA[k]))]
|
|
271
|
+
G = n * dp[d] * tp[t] * eg
|
|
272
|
+
Gd = d * inv_d - 2.0 * eta * dme
|
|
273
|
+
Gdp = -d * inv_d2 - 2.0 * eta
|
|
274
|
+
Gt = t * inv_t - 2.0 * beta * tmg
|
|
275
|
+
Gtp = -t * inv_t2 - 2.0 * beta
|
|
276
|
+
ar += G
|
|
277
|
+
ar_d += G * Gd
|
|
278
|
+
ar_t += G * Gt
|
|
279
|
+
ar_dd += G * (Gd * Gd + Gdp)
|
|
280
|
+
ar_tt += G * (Gt * Gt + Gtp)
|
|
281
|
+
ar_dt += G * Gd * Gt
|
|
282
|
+
|
|
283
|
+
# ── Non-analytic terms (shared θ; terms 40/41 share Δ) ──
|
|
284
|
+
s, dme, s_p, Bsa, e_cs = _na_delta_state(delta)
|
|
285
|
+
s_pm1 = s ** (_NA_P_SCALAR - 1.0) # s^(2/3)
|
|
286
|
+
s2 = s * s
|
|
287
|
+
sam1 = {3.5: s2 * jnp.abs(dme), 3.0: s2} # s^(a-1) by multiplies
|
|
288
|
+
tm1 = tau - 1.0
|
|
289
|
+
|
|
290
|
+
theta = -tm1 + _NA_BIGA_SCALAR * s_p
|
|
291
|
+
th_d = 2.0 * _NA_BIGA_SCALAR * _NA_P_SCALAR * dme * s_pm1
|
|
292
|
+
th_dd = 2.0 * _NA_BIGA_SCALAR * _NA_P_SCALAR * (2.0 * _NA_P_SCALAR - 1.0) * s_pm1
|
|
293
|
+
De_t = -2.0 * theta
|
|
294
|
+
De_tt = 2.0
|
|
295
|
+
exp_tm1 = jnp.exp(-_NA_BIGD_SCALAR * tm1 * tm1)
|
|
296
|
+
|
|
297
|
+
Delta_cache = {}
|
|
298
|
+
for k in range(3):
|
|
299
|
+
a, b = float(_NAA[k]), float(_NAB[k])
|
|
300
|
+
B, Cc = float(_NABIGB[k]), float(_NABIGC[k])
|
|
301
|
+
dkey = (a, B)
|
|
302
|
+
if dkey not in Delta_cache:
|
|
303
|
+
Delta = theta * theta + Bsa[dkey]
|
|
304
|
+
Db = jnp.maximum(Delta, 1e-300)
|
|
305
|
+
De_d = 2.0 * theta * th_d + 2.0 * B * a * dme * sam1[a]
|
|
306
|
+
De_dd = (2.0 * th_d * th_d + 2.0 * theta * th_dd
|
|
307
|
+
+ 2.0 * B * a * (2.0 * a - 1.0) * sam1[a])
|
|
308
|
+
De_dt = -2.0 * th_d
|
|
309
|
+
Delta_cache[dkey] = (Db, De_d, De_dd, De_dt)
|
|
310
|
+
Db, De_d, De_dd, De_dt = Delta_cache[dkey]
|
|
311
|
+
|
|
312
|
+
F = _pow78(Db) if b == 0.875 else Db ** b
|
|
313
|
+
Fm1 = F / Db
|
|
314
|
+
Fm2 = Fm1 / Db
|
|
315
|
+
F_d = b * Fm1 * De_d
|
|
316
|
+
F_t = b * Fm1 * De_t
|
|
317
|
+
F_dd = b * (b - 1.0) * Fm2 * De_d * De_d + b * Fm1 * De_dd
|
|
318
|
+
F_tt = b * (b - 1.0) * Fm2 * De_t * De_t + b * Fm1 * De_tt
|
|
319
|
+
F_dt = b * (b - 1.0) * Fm2 * De_d * De_t + b * Fm1 * De_dt
|
|
320
|
+
|
|
321
|
+
Psi = e_cs[Cc] * exp_tm1
|
|
322
|
+
Ps_d = -2.0 * Cc * dme * Psi
|
|
323
|
+
Ps_t = -2.0 * _NA_BIGD_SCALAR * tm1 * Psi
|
|
324
|
+
Ps_dd = (4.0 * Cc * Cc * s - 2.0 * Cc) * Psi
|
|
325
|
+
Ps_tt = (4.0 * _NA_BIGD_SCALAR * _NA_BIGD_SCALAR * tm1 * tm1
|
|
326
|
+
- 2.0 * _NA_BIGD_SCALAR) * Psi
|
|
327
|
+
Ps_dt = 4.0 * Cc * _NA_BIGD_SCALAR * dme * tm1 * Psi
|
|
328
|
+
|
|
329
|
+
n = float(_NAN[k])
|
|
330
|
+
W = F * delta * Psi
|
|
331
|
+
W_d = F_d * delta * Psi + F * Psi + F * delta * Ps_d
|
|
332
|
+
W_t = F_t * delta * Psi + F * delta * Ps_t
|
|
333
|
+
W_dd = (F_dd * delta * Psi + 2.0 * F_d * Psi + 2.0 * F_d * delta * Ps_d
|
|
334
|
+
+ 2.0 * F * Ps_d + F * delta * Ps_dd)
|
|
335
|
+
W_tt = F_tt * delta * Psi + 2.0 * F_t * delta * Ps_t + F * delta * Ps_tt
|
|
336
|
+
W_dt = (F_dt * delta * Psi + F_d * delta * Ps_t + F_t * Psi + F * Ps_t
|
|
337
|
+
+ F_t * delta * Ps_d + F * delta * Ps_dt)
|
|
338
|
+
|
|
339
|
+
ar += n * W
|
|
340
|
+
ar_d += n * W_d
|
|
341
|
+
ar_t += n * W_t
|
|
342
|
+
ar_dd += n * W_dd
|
|
343
|
+
ar_tt += n * W_tt
|
|
344
|
+
ar_dt += n * W_dt
|
|
345
|
+
|
|
346
|
+
return ar, ar_d, ar_t, ar_dd, ar_tt, ar_dt
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
350
|
+
# τ-only inner-loop pair: δ-invariant prep + per-iteration fast eval
|
|
351
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
def residual_tau_prep(delta):
|
|
354
|
+
"""Precompute the δ-invariant state for the τ-derivative inner loop.
|
|
355
|
+
|
|
356
|
+
At fixed ρ (the Newton solves T at fixed density) every δ-dependent power
|
|
357
|
+
and exponential is loop-invariant. The polynomial+exponential terms
|
|
358
|
+
collapse to one coefficient per distinct τ-exponent:
|
|
359
|
+
αʳ_poly(τ; δ) = Σ_j Ct[j]·τ^t_j with Ct[j] = Σ_{terms with t_j} n·δ^d·E_l.
|
|
360
|
+
"""
|
|
361
|
+
dp = _delta_ladder(delta)
|
|
362
|
+
E = _exp_envelopes(dp)
|
|
363
|
+
Ct = [0.0] * len(_TPOLY)
|
|
364
|
+
for k in range(len(_PN)):
|
|
365
|
+
Ct[_TP_IDX[float(_PT[k])]] += float(_PN[k]) * dp[int(_PD[k])] * E[int(_PL[k])]
|
|
366
|
+
Ct = jnp.stack([jnp.asarray(c) for c in Ct])
|
|
367
|
+
|
|
368
|
+
dme = delta - 1.0
|
|
369
|
+
dme2 = dme * dme
|
|
370
|
+
Cg = jnp.stack([float(_GN[k]) * dp[int(_GD[k])]
|
|
371
|
+
* jnp.exp(-float(_GETA[k]) * dme2) for k in range(5)])
|
|
372
|
+
|
|
373
|
+
_, _, s_p, Bsa, e_cs = _na_delta_state(delta)
|
|
374
|
+
return Ct, Cg, s_p, Bsa, e_cs, delta
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def residual_tau_fast(tau, dstate):
|
|
378
|
+
"""Return (αʳ_τ, αʳ_ττ) from precomputed δ-state ``dstate``.
|
|
379
|
+
|
|
380
|
+
Identical result to ``residual_tau_derivs(tau, delta)`` but per call only
|
|
381
|
+
the τ-side transcendentals are evaluated: ~6 exps + 8 sqrts + 1 pow.
|
|
382
|
+
"""
|
|
383
|
+
Ct, Cg, s_p, Bsa, e_cs, delta = dstate
|
|
384
|
+
inv_t = 1.0 / tau
|
|
385
|
+
inv_t2 = inv_t * inv_t
|
|
386
|
+
|
|
387
|
+
tp = _tau_ladder(tau)
|
|
388
|
+
taupows = jnp.stack([tp[t] for t in _TPOLY])
|
|
389
|
+
base = Ct * taupows
|
|
390
|
+
p_t = jnp.sum(base * _W_T) * inv_t
|
|
391
|
+
p_tt = jnp.sum(base * _W_TT) * inv_t2
|
|
392
|
+
|
|
393
|
+
g_t = 0.0
|
|
394
|
+
g_tt = 0.0
|
|
395
|
+
e_g = {}
|
|
396
|
+
for k in range(5):
|
|
397
|
+
key = (float(_GBETA[k]), float(_GGAMMA[k]))
|
|
398
|
+
if key not in e_g:
|
|
399
|
+
tmg = tau - key[1]
|
|
400
|
+
e_g[key] = (tmg, jnp.exp(-key[0] * tmg * tmg))
|
|
401
|
+
for k in range(5):
|
|
402
|
+
t, beta = float(_GT[k]), float(_GBETA[k])
|
|
403
|
+
tmg, eg = e_g[(beta, float(_GGAMMA[k]))]
|
|
404
|
+
gk = Cg[k] * tp[t] * eg
|
|
405
|
+
Gt = t * inv_t - 2.0 * beta * tmg
|
|
406
|
+
Gtp = -t * inv_t2 - 2.0 * beta
|
|
407
|
+
g_t += gk * Gt
|
|
408
|
+
g_tt += gk * (Gt * Gt + Gtp)
|
|
409
|
+
|
|
410
|
+
tm1 = tau - 1.0
|
|
411
|
+
theta = -tm1 + _NA_BIGA_SCALAR * s_p
|
|
412
|
+
De_t = -2.0 * theta
|
|
413
|
+
De_tt = 2.0
|
|
414
|
+
exp_tm1 = jnp.exp(-_NA_BIGD_SCALAR * tm1 * tm1)
|
|
415
|
+
|
|
416
|
+
na_t = 0.0
|
|
417
|
+
na_tt = 0.0
|
|
418
|
+
Db_cache = {}
|
|
419
|
+
for k in range(3):
|
|
420
|
+
a, b = float(_NAA[k]), float(_NAB[k])
|
|
421
|
+
B, Cc = float(_NABIGB[k]), float(_NABIGC[k])
|
|
422
|
+
dkey = (a, B)
|
|
423
|
+
if dkey not in Db_cache:
|
|
424
|
+
Db_cache[dkey] = jnp.maximum(theta * theta + Bsa[dkey], 1e-300)
|
|
425
|
+
Db = Db_cache[dkey]
|
|
426
|
+
F = _pow78(Db) if b == 0.875 else Db ** b
|
|
427
|
+
Fm1 = F / Db
|
|
428
|
+
Fm2 = Fm1 / Db
|
|
429
|
+
F_t = b * Fm1 * De_t
|
|
430
|
+
F_tt = b * (b - 1.0) * Fm2 * De_t * De_t + b * Fm1 * De_tt
|
|
431
|
+
Psi = e_cs[Cc] * exp_tm1
|
|
432
|
+
Ps_t = -2.0 * _NA_BIGD_SCALAR * tm1 * Psi
|
|
433
|
+
Ps_tt = (4.0 * _NA_BIGD_SCALAR * _NA_BIGD_SCALAR * tm1 * tm1
|
|
434
|
+
- 2.0 * _NA_BIGD_SCALAR) * Psi
|
|
435
|
+
W_t = F_t * delta * Psi + F * delta * Ps_t
|
|
436
|
+
W_tt = F_tt * delta * Psi + 2.0 * F_t * delta * Ps_t + F * delta * Ps_tt
|
|
437
|
+
na_t += float(_NAN[k]) * W_t
|
|
438
|
+
na_tt += float(_NAN[k]) * W_tt
|
|
439
|
+
|
|
440
|
+
return p_t + g_t + na_t, p_tt + g_tt + na_tt
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def residual_tau_derivs(tau, delta):
|
|
444
|
+
"""Return (αʳ_τ, αʳ_ττ) — the residual quantities the Newton needs.
|
|
445
|
+
|
|
446
|
+
Computes only the τ-derivatives; the δ-dependent envelopes are
|
|
447
|
+
loop-invariant at fixed ρ and XLA hoists them across an unrolled Newton.
|
|
448
|
+
"""
|
|
449
|
+
return residual_tau_fast(tau, residual_tau_prep(delta))
|