Repository navigation
Conversation
Every example built its ODEProblem without `jac`, so `prob.f.jac` was `nothing` and FBDF rebuilt a dense Jacobian by automatic differentiation at every step. `jac=true, sparse=true` makes ModelingToolkit generate and compile the analytic, block-tridiagonal Jacobian ahead of time instead. The `autodiff=` keyword these solves passed to FBDF is dead once a Jacobian is supplied, so it is dropped, and the ADTypes import with it where nothing else used it. Adds a CasADi benchmark so the Python side of docs/julia_vs_python.md can be measured with an analytic sparse Jacobian too, and a quasi-steady/dynamic scaling benchmark with a figure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #33 +/- ##
=======================================
Coverage 92.28% 92.28%
=======================================
Files 4 4
Lines 350 350
=======================================
Hits 323 323
Misses 27 27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The Python examples solved the same models as the Julia ones, but with a Jacobian derived by hand where they had one at all. Tether_01-05 had none and let IDA finite-difference it; Tether_06, 06c, 07 and 08 carried 86 to 150 lines of pen-and-paper calculus each. Each model is now written once as a CasADi expression graph. CasADi differentiates it for the exact Jacobian and finds the block-tridiagonal sparsity by itself, and SUNDIALS' CVODES integrates it with a sparse Newton solve - the same fixed-leading-coefficient BDF family as the Julia examples' FBDF. That makes the Julia/Python comparison a comparison of ecosystems rather than of a compiled model against interpreted callbacks. The ten examples lose about 700 lines, and all ten still reproduce the Julia trajectories within the tolerances test/test_tether_*.jl apply. Tether_03b keeps its event handling by hand: CasADi's `zero` event detection is experimental and aborts on that model with "tout too far back in direction of integration", so it bisects for the crossing between output points instead. Tether_06c's events fire once per segment and do work through CasADi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
The script imported `Tether` from `QuasiSteady` while also importing `TetherComponents`, which defines a component of the same name. menu3.jl runs every quasi-steady example into the same `Main`, so the unqualified import would shadow the component for anything running after it. Also adds the benchmark to menu3.jl, next to benchmark_qsm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
The paper described Python examples built on Assimulo's IDA with a Jacobian derived by hand, and reported Julia as 13 to 30 times faster. Neither is true any more: both implementations now generate an analytic sparse Jacobian from the model, and they land within about 20% of each other. Rather than drop the old number, the comparison section explains it: it measured compiled Julia against a hand-derived Jacobian evaluated in interpreted NumPy, 707 us per call against 33 us for the same Jacobian compiled by CasADi. It measured the binding, not the language. The section also keeps the parts that do not favour Julia - the one-time compilation cost, the install time, and event handling, where CasADi's support is experimental and one example has to locate its crossings by bisection. Cites CasADi in place of Assimulo, and extends the AI usage disclosure to cover the Jacobian and CasADi work. The paper is now built by .github/workflows/draft-paper.yml with the journal's own inara image, so a PDF comes out of every push that touches paper/ without a local Docker or LaTeX toolchain. paper/build used a third-party image that does not produce the journal's layout; it now uses inara too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
…tainer The comparison section had grown prose about how the measurement was arrived at and what earlier versions of the package used to claim. Neither belongs in the paper: it states what the software does, and docs/julia_vs_python.md carries the history. Rewritten to give the setup, the numbers and the trade-offs. Names CVODES rather than "the SUNDIALS solvers", since the fairness of the comparison rests on both sides integrating with a BDF, and quotes the Jacobian's sparsity as a percentage instead of a non-zero count. paper/build no longer pulls a 731 MB image to produce five pages. pandoc and a local LaTeX installation give a readable PDF in a couple of seconds, which is what proofreading needs; the journal's own layout comes from the CI workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
The section read as a run of numbers in prose. They are a table: four segment counts against the four Jacobian strategies, which also shows at a glance that sparsity gains more than the analytic derivative alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
CasADi evaluates the model as a tape of elementary operations inside its own library rather than as compiled machine code. Asking it to compile the tape with jit=True gains a further 1.7-1.9x, with bit-identical trajectories, for a compilation step of 0.6 to 7.3 s and a C compiler at run time. The examples leave it off - they are run once, and the compilation costs more than the solve saves - but the paper reported the two ecosystems as comparable without saying that one of them had another factor of 1.8 in hand, which was not the whole picture. The paper now also states what that speed costs. A CasADi model is a closed expression graph handed to one of four integrators; a ModelingToolkit model is a symbolic object that any DifferentialEquations.jl solver can take, that can be composed acausally, and that can be simplified before solving - the ten-segment model is written as 386 equations and reduced to the 66 actually integrated. The benchmark tables were also labelled with the wrong CPU: they were taken on an Intel Core i7-11850H, not the Ryzen 9 7950X that the older numbers in README.md came from. README.md and docs/src/index.md still claimed Julia was 16 to 30 times faster and carried Python timings from before the CasADi rewrite. The table now gives lines of code only, counted with the rule it already stated, and points at docs/julia_vs_python.md for timings rather than keeping a second copy that goes stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg
This branch has not been deployed
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.
Two commits. The first gives the Julia time simulations an analytic sparse Jacobian; the second does the same for Python by replacing Assimulo and every hand-written Jacobian with CasADi. Together they make
docs/julia_vs_python.mda comparison of two ecosystems rather than of compiled Julia against interpreted NumPy callbacks.1. Julia:
jac=true, sparse=trueEvery example built its
ODEProblemwithoutjac, soprob.f.jacwasnothingandFBDFrebuilt a dense Jacobian by automatic differentiation at every step. Verified directly:ModelingToolkit now generates and compiles the analytic, block-tridiagonal Jacobian ahead of time — which is what
paper.md:100already claimed was happening. Scope: time simulations only; the steady-state solves are untouched.Median ±IQR,
BenchmarkTools, 12 s per configuration:jacjac+sparse(after)The analytic Jacobian alone buys little — the dense factorization then dominates. It is the two together that pay. Cost:
jac=trueadds 0.4 s (5 segments) to 4.1 s (40) of build time.autodiff=is dead once a Jacobian is supplied (FBDF()andFBDF(autodiff=AutoForwardDiff())measure the same), so it is dropped along with the now-unusedADTypesimports.Trajectories unchanged — every example run before and after, compared on final tether shape and whole-trajectory sum:
Tether_06b,Tether_06_videoandTether_08_videohave no test coverage and were run by hand; all three succeed.2. Python: Assimulo and the hand-written Jacobians are gone
Tether_01–05had no Jacobian at all and let IDA finite-difference it.Tether_06,06c,07and08carried 86–150 lines of pen-and-paper calculus each. Each model is now one CasADi expression graph;ca.jacobiandoes the rest, and CVODES integrates it with a sparse Newton solve.All ten reproduce the Julia trajectories within the tolerances
test/test_tether_*.jlapply — run locally against freshly generated Julia CSVs:CondaPkg.tomlswapsassimuloforcasadiand addsscipy. A side benefit: the whole Python side is now pip-installable and runnable without a Fortran/SUNDIALS build.Where Python is genuinely behind
Tether_03bis the one place. CasADi does have event detection — azeroentry in the DAE dictionary — but it is experimental, and on that model it aborts with "tout too far back in direction of integration": after restarting at an event it cannot interpolate back to the requested output point. It fails whether the run starts on, above or below the threshold, and stepping interval-by-interval does not help. SoTether_03b.pywalks the output grid itself, watches the sign of the indicator, bisects for the crossing and restarts there — about twenty lines against four for Julia'sContinuousCallback.Tether_06c.py, whose events fire once per segment, uses CasADi's event support successfully.3. The comparison, now like for like
With both sides analytic and sparse, Julia
FBDFand SUNDIALS CVODES-BDF land within about 20% of each other:jac+sparseThe previously reported 13–30x was compiled Julia against a hand-written Jacobian evaluated in interpreted NumPy — 707 µs per call against 33 µs for the same Jacobian as a CasADi function. It measured the binding, not the language.
docs/julia_vs_python.md,README.md,docs/src/index.md,docs/src/examples.md,docs/src/references.mdanddocs/src/python.mdare updated accordingly.4. Quasi-steady vs dynamic
examples/quasisteady/benchmark_scaling.jl+docs/images/qsm_vs_dynamic.png, both per simulated second:Two traps, both hit while writing this and both now recorded in
docs/quasisteady.md: comparing a per-step!cost against a per-simulated-second cost inflates the ratio by 50x atdt = 0.02; and benchmarkingstep!in a loop without moving the kite measures nothing, becausestep!writes the converged answer back intote.state_vec.This bears on Uwe's "only 1.5x faster" against Williams' 1000x: that is a whole-script comparison where the dynamic side's one-off symbolic build dominates, while these are solve times only.
Other findings
Tether_11is left alone. Its time simulation goesUnstablewith any non-finite-difference Jacobian, including plainFBDF()with nojacat all — so this predates the change. Worth its own issue.Tether_09does no integration (nf=1, both ends fixed, starts at steady state), so the change is inert there.sparse=truenever sets acolorvec, so sparse AD still costs N residual evaluations per Jacobian; only the factorization benefits..zenodo.jsonlisted one creator whileCITATION.cfflisted two. Both now list three.Authorship
Bart van de Lint added as third author (
paper.md,CITATION.cff,.zenodo.json), affiliation Open Source AWE. No ORCID — add one if you have it.Verified locally
Full suite in a clean Julia session, including the
CondaPkgresolve and every Pythonexample through
run_python:CondaPkginstalls CasADi 3.7.2 from conda-forge, not the 3.8.0 used while developing;the
zeroevent support and bothcsparse/lapacklulinear solvers are present in 3.7.2,checked explicitly.
One bug surfaced this way:
examples/quasisteady/benchmark_scaling.jlimportedTetherfrom
QuasiSteadywhile also importingTetherComponents, which defines a component ofthe same name.
menu3.jlruns every quasi-steady example into the sameMain, so thatwould have shadowed the component for anything running afterwards. Fixed in the third
commit, which also adds the benchmark to
menu3.jl.🤖 Generated with Claude Code
https://claude.ai/code/session_01Lw7QQPcUvFcYNV4pSBs7Lg