Semi-analytic Jacobian for 1D flames#2139
Draft
speth wants to merge 28 commits into
Draft
Conversation
…o Python Add `Sim1D.eval_jacobian()` to trigger `evalSSJacobian()` from Python, and `Domain1D.global_component_index(name, j)` to map a component/point pair to its index in the global solution vector. Both are needed by the upcoming analytic Jacobian comparison harness. Includes a test that verifies the assembled Jacobian matches a manual finite-difference residual perturbation to within 2% (the tolerance accounts for frozen transport properties in the solver FD vs full recomputation in sim.eval()). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-domain `jacobian_mode` flag ("finite-difference" or
"analytic"), two virtual hooks (`hasAnalyticJacobian(j,n)` and
`evalJacobianAnalytic(x, jac)`), and the column-skipping logic in
`OneDim::evalJacobian` that calls `evalJacobianAnalytic` after the FD
loop. No domain claims any columns yet, so this is a behavioral no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an overload netProductionRates_ddCi(SparseMatrix&) that fills a caller-owned sparse matrix and reuses its nonzero pattern across calls. The pattern of d(wdot_k)/d(c_m) is fixed for a given mechanism and derivative settings, so it is built once (delegating to the allocating path) and subsequent calls overwrite the value array in place with no allocation or symbolic SpGEMM. The BulkKinetics override fuses the stoichiometry-matrix product directly into the compressed column storage using an O(K) dense scatter column (m_rbuf3) rather than a K x K dense buffer, mirroring calculateCompositionDerivatives() with ddX=false. Result is numerically identical to the allocating netProductionRates_ddCi() (verified to <1e-12 relative). No new Python API. Benchmark (gri30, optimize=n library): ~1447 us/call allocating vs ~883 us/call reused = 1.64x on the primitive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the compare_modes harness and TestAnalyticVsFD, and fixes two bugs found by the FD oracle: - The capability probe ran inside evalJacobianAnalytic (after the FD column-skip loop), so claimed columns were never skipped and the analytic post-pass double-counted them. The probe now runs lazily from the const hasAnalyticJacobian()/usingAnalyticJacobian() queries. - The 1/(rho cp) chain term in the energy row had the wrong sign (FD-verified: d(1/(rho cp))/dY_m = (1/(rho cp))(wbar/W_m - cpm/cp), giving -B*rcp*(...) for the residual rsd = -u dTdz - B/(rho cp)). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add TestAnalyticConfigMatrix with four tests covering: axisymmetric counterflow diffusion flame, unstrained burner flame, Soret diffusion enabled, and two-point control. All pass without new C++ changes, confirming Tasks 1-7 cover these row/column variants correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add TestAnalyticSolve.test_full_solve_matches_fd which solves a GRI-3.0 stoichiometric CH4/air free flame twice — once with the default finite-difference Jacobian and once with analytic mode — exercising regridding, time stepping, and workspace reallocation. Asserts flame speeds agree to within 1e-4 relative tolerance (actual: ~7.6e-7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'Jacobian Evaluation Modes' subsection to the nonlinear-solver reference page describing the new opt-in 'analytic' mode, its fallback conditions, and the frozen-transport approximation. Update the surrounding paragraph to match. Add a corresponding entry to the Cantera 4.0 release notes under 'New features'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecycle Address final-review findings on the analytic 1D Jacobian: - The banded MultiJac solver caches the steady-state diagonal in m_ssdiag, updated only when a diagonal entry is written and never cleared by reset(). The analytic species fill skipped emitting entries that evaluated to exactly zero, so an exactly-zero claimed species-row diagonal would leave m_ssdiag stale and corrupt the transient diagonal. Always emit the (j == p, k == m) diagonal, mirroring the forced-diagonal write in OneDim::evalJacobian's FD loop. (In practice the species diagonal is never bit-exactly zero, so this is insurance rather than an observed failure.) - Document that m_ddC's sparse pattern is assumed fixed for the lifetime of the grid: growing the kinetics derivativeSettings() pattern (e.g. disabling skip-third-bodies) without an intervening resize() is unsupported. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tateSystem Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cate per-metric accessors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tats test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route every updateTransient() call site in the 1D solver (steady solve, time-step setup, steady-mode switch, and the Newton re-evaluation path) through a single timed helper so factor_time/factorizations also account for factorizations during time stepping, not just steady Newton solves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Key changes: - Extend analytic Jacobian to cover grid points 1 and N-2 (previously 2..N-3); boundary residuals have no dependence on neighboring species so those rows are safely skipped. Minimum grid size drops from 6 to 3. - Refactor fluxJacobian to template<bool AtQ> computing only the needed endpoint derivative, eliminating the scratch buffer and halving the inner-loop work. - Remove unused x parameter from computeWdotDerivatives. - Rename fuseStoichProduct → accumulateStoichProduct; explain template necessity (StoichManagerN::derivatives returns Eigen::Map, not SparseMatrix). - Move factorizeJacobian() body to SteadyStateSystem.cpp, localizing <chrono>. - Inline the single-use accumulateGridTime lambda in SteadyStateSystem::solve. - Reorganize Flow1D::hasAnalyticJacobian/evalJacobianAnalytic into the main public section rather than a mid-class public: block. - Fix math-mode formula in radiationPolyFactor docstring. - Fix Doxygen group comment in SteadyStateSystem.h staging accumulators. - Remove unused Cython pxd getters (gridSizeStats etc.); add pyi stubs for new API (jacobian_mode, global_component_index, eval_jacobian, solver_stats). - Add analytic-jacobian.md derivation page to Sphinx docs. - Update release note with mechanism-size scaling explanation. - Speed up test_radiation by using h2o2.yaml instead of GRI 3.0. - Update TestAnalyticVsFD claimed-column range to match new j >= 1 condition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
accumulateStoichProduct now scans for product entries that the supplied pattern cannot store and throws instead of silently dropping them (and leaking into the next column). Compiled out under NDEBUG. Aso, merge the three analytic-vs-FD comparison classes into one TestAnalyticVsFD with compare_modes as a member; solve the comparison configs with auto=False to avoid masking unexpected failures; fold the deprecated-alias check into test_solver_stats; and replace GRI 3.0 with h2o2 (loose refinement) in test_full_solve_matches_fd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Overview: the Jacobian is reused across Newton steps (only re-evaluated
on stalls), not rebuilt every iteration; the finite-difference build costs
N_v *local* (3-point) residual evaluations, not N_v full ones, thanks to
the block-tridiagonal structure; and N_v = (K+c)N including the non-species
components, so the N_v - K(N-2) saving is c*N + 2K remaining FD columns.
- Adopt the discretization.md notation throughout: grid points as subscripts,
Y_{k,j}/X_{k,j}, and the midpoint diffusive flux j_{k,j+1/2}. Resolves the
overloaded F_k^{(j)} (previously both residual and flux) and the mixed
Y_m(q)/X_n^{(q)}/D_{mq} indexing.
- Drop the "AtQ" implementation jargon in favor of left/right endpoint, and
use p for the perturbed column point and j for the residual row point.
- Correct the species residual to the divided-by-rho_j form actually
assembled in Flow1D::evalSpecies (consistent with the 1/rho_j Jacobian
factors), and write out the density-chain and right-endpoint flux terms.
- Define the Kronecker delta in the Notation section ($\delta_{km}$ was used
in the flux derivatives but never introduced; it is unrelated to the
diffusion symbol).
- Rename the diffusion prefactor D -> \tilde{D} and define it explicitly as
the composite flux prefactor rho*W_k/Wbar * D_{k,mix} (molar basis) actually
stored in Flow1D::m_diff, distinguishing it from a bare mixture-averaged
diffusion coefficient D_{k,mix}, and note its mass-basis form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces a three-valued jacobian_mode. 'auto' (the new default) uses the analytic Jacobian where supported and silently falls back to finite differences otherwise; the capability probe no longer warns on fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In explicit jacobian_mode = "analytic", OneDim::evalJacobian now validates each domain before the finite-difference column loop and raises if the kinetics lacks composition derivatives or multicomponent transport is active. Internal/transient conditions (adjoint force-full-update, fewer than 3 points) still fall back silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inistic IonFlow overrides the species residual and diffusive-flux physics (ion drift, electric-field coupling) without supplying matching analytic Jacobian derivatives, so the inherited Flow1D analytic columns describe a different residual than IonFlow solves. With the analytic Jacobian now the default, this broke the stage-two ion-flame solve. Add an analyticJacobianSupported() hook (false for IonFlow) so "auto" silently uses finite differences and explicit "analytic" raises for ion flames. Also pin finite differences in the high-pressure regrid test helper, whose exact step-count assertions depend on the Jacobian method. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2139 +/- ##
==========================================
+ Coverage 77.81% 78.01% +0.19%
==========================================
Files 452 452
Lines 53700 54050 +350
Branches 8976 9056 +80
==========================================
+ Hits 41789 42169 +380
+ Misses 8886 8845 -41
- Partials 3025 3036 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes proposed in this pull request
The 1D flame solver currently computes a finite difference Jacobian for use with the Newton's method solver. The main efficiency this exploits is the block tridiagonal pattern of the Jacobian, where the state at each grid point only affects the residuals at that grid point and the immediate neighbors, so everything scales linearly with the number of grid points$N$ . However, the number of function evaluations to compute each block row is $O(K)$ and the cost is also $O(K)$ , making this Jacobian evaluation an $O(NK^2)$ cost. Using a banded matrix solver, where the bandwidth is proportional to $K$ , the factorization cost is $O(NK^3)$ and the backsubstitution/solve cost is $O(NK^2)$ . It turns out, though, that the constant factors on these different operations are quite different, and for a range of mechanism sizes, the Jacobian construction cost dominates.
This cost can be dramatically reduced by computing a semi-analytical Jacobian, where the derivatives with respect to species mass fractions are computed analytically and derivatives with respect to the remaining state variables are computed using the existing finite difference approach. Derivatives of the reaction rate expressions make use of the existing
Kineticsderivatives, while derivatives of the transport-related terms are new additions to theFlow1Dclass.Currently, the analytic Jacobian works for ideal gases using mixture-averaged transport, including the mixture-averaged approximation for the Soret diffusion term. It is enabled by default, but automatically falls back to the finite difference form for other flow domains (specifically,
IonFlow), transport models, or equations of state. It can also be manually switched via theDomain1D.jacobian_modeproperty.This PR also revises the performance statistics available from the 1D solver, introducing an
AnyMap-based store accessible viaSim1D.solver_statsthat provides break downs of run time and call count for function evaluations, Jacobian evaluations, Jacobian factorizations, and linear solves for each grid adaptation step.If applicable, provide an example illustrating new features this pull request is introducing
The following is a performance comparison on a laminar flame speed calculation, using mechanisms of various sizes, including a couple from NUI Galway converted to YAML format.
Collating the output shows the resulting performance benefits (times in seconds; using OpenBLAS with
OMP_NUM_THREADS=1).The benefit for very small mechanisms is limited, but grows for medium-sized mechanisms. Eventually, for larger mechanisms, even though the Jacobian evaluation cost is reduced by a factor of ~9, the factorization step becomes the dominant contributor to the overall runtime and the relative speedup starts to decline.
AI Statement (required)
logic and implementation decisions. All generated code and documentation were reviewed and understood by the contributor. Implemented using Claude Code (Opus 4.8) and superpowers.
Checklist
scons build&scons test) and unit tests address code coverage