Skip to content

Reject observable-space data at HamiltonianSINDy's entry point - #34

Merged
michakraus merged 9 commits into
mainfrom
sindy/part-25
Oct 16, 2026
Merged

michakraus merged 9 commits into
mainfrom
sindy/part-25

Conversation

@michakraus

Copy link
Copy Markdown
Member

What

SparseIdentification refuses observable-space data at the entry point, by dispatch on the space parameter, instead of fitting it in silence. A new bridge file src/reducedcomplexitymodeling.jl holds two identify methods: one for RCM.TrainingData{<:IntrinsicSpace, StateData, CanonicalHamiltonianSystem}, and one for any other RCM.TrainingData that throws, naming the space and telling the caller to map the data to canonical (q, p) and tag it IntrinsicSpace. TrainingData is renamed to SINDyData, so the type no longer collides with ReducedComplexityModeling's.

Verdict

The part's last critic round: Verdict: PASS. All six required mutants are CAUGHT, each dying at the assertion it is meant to hit, and no new assertion passes for the wrong reason.

evidence outcome
edits 1 and 2 — startswith(err.msg, …) at test/reducedcomplexitymodeling.jl:131 and :343 V1 and P1 fail at exactly those lines
edit 3 — the layout guards N13 at :279/:287, and N14 at :297/:300, where the previous round's isempty guard had masked it
edit 4 — the Float32.(time) fit at :235 V4 dies; 0 of 10 steps inside √eps(Float64)
edit 5 — K10's evidence the grep prints lines 3, 37, 44, 48, 154; the two calls print 0.02 and 0.01
round 2's other eleven, re-run 11 CAUGHT, 0 SURVIVED of 11

src/ is untouched through the fix rounds: git diff d3c286d -- src/ is 0 bytes. V5 survives, as the ruling permits, and equals the head on every time vector whose values share one type.

Checks

  • run-tests.jl <worktree> full: exit 0, PASS in 55 s; the Reduced Complexity Modeling group 58.
  • docs build: exit 0.

Recorded, not fixed

The pre-pull-request branch verification, run at the tree's requirement, left three findings in the new src/reducedcomplexitymodeling.jl. All three are recorded as K11–K13 rather than fixed, because the arbitrator had frozen src/ for the test round; none binds §L's type-stability rule, which binds a method a time step calls, not an offline setup routine. The eight TrainingData call sites under scripts/ are K7's and are handled separately.

Decisions the part carries

  • advisor — rename TrainingData to SINDyData, so SparseIdentification's own type no longer collides with RCM's;
  • advisor — identify is kept as the entry point rather than becoming a method of RCM's train;
  • advisor — the bridge file src/reducedcomplexitymodeling.jl, with its two methods;
  • arbitrator — the test round that fixed the tests alone;
  • author — the three src/ findings recorded with their measurements, and the scripts left to their own task.

`TrainingData` is also the name of `ReducedComplexityModeling`'s type for data on a
solution space, so a session that `using`ed both packages resolved the name to neither and
could use neither. The type keeps its fields, constructors, selectors and accessors; the
name is the only thing that changes, and the package has never been registered.

The bridge that consumes a `ReducedComplexityModeling` `TrainingData` is deliberately not in
this commit: `SparseIdentification` cannot yet take that package as a dependency, because
`ReducedComplexityModeling` 0.2.0 requires `AbstractNeuralNetworks = "0.6"` while every
`GeometricOptimizers` this package admits declares a weak dependency on
`AbstractNeuralNetworks = "0.7"` or `"0.8"`, so no environment can hold both.

`TrajectoryData`'s and `HamiltonianSINDy`'s docstrings now state that an untagged state is
taken to be canonical `(q, p)`, and `KNOWN_ISSUES.md` gains K10 for the `TrajectoryData` `Δt`
that the fit does not read.
ReducedComplexityModeling tags its training data with the space the data live in. A `TrainingData`
on an `IntrinsicSpace` whose states are `StateData` on a `CanonicalHamiltonianSystem` is exactly
what the Hamiltonian flow-map fit assumes, so `identify` now builds the state pairs from it — a
`d×N` matrix of consecutive states, or RCM's `d×M×N` ensemble with consecutive times paired within
each member — and forwards to the unchanged `TrajectoryData` path. Every other `TrainingData` is
refused at the entry point with an `ArgumentError` naming the method, the space, the data type and
the system type, as this package does for every unsupported (problem, method) pair, so
observable-space data is never fitted in silence.

The states of a pair must be one step of `method.integrator_timestep` apart, to `√eps`, because
`sparsify` integrates the flow map with the method's step and never reads the data's (K10). A
`TrainingData` built at another step is refused rather than fitted for the interval it does not
describe; one whose times accumulated round-off is at the step and fits.

`ReducedComplexityModeling` becomes a dependency at `"0.3"`. Version 0.2.0 could not be held beside
any `GeometricOptimizers` this package admits: it required `AbstractNeuralNetworks = "0.6"` while
0.7.0 and 0.8.0 both declare a weak dependency on `"0.8"`. Nothing is re-exported and the
conversion is private — no `TrajectoryData(::TrainingData)` method and no new public name, so
`TrainingData` stays RCM's type and `TrajectoryData` stays this package's.

`test/reducedcomplexitymodeling.jl` covers the five checks, and asserts the identified coefficients
of the harmonic oscillator as ½ on `q²` and on `p²` with every other coefficient zero. That is the
assertion that kills both mutants K8 records, so K8 leaves `KNOWN_ISSUES.md` and the CHANGELOG
names it.
The suite was red: `test/reducedcomplexitymodeling.jl` imports `LinearAlgebra` and `test/Project.toml`
had no entry for it. `Pkg.test()` gives the test project only that file's dependencies plus the
package and takes `@stdlib` off the load path, so the run died at the import while a named selection
of the same file passed. The entry is added with no `[compat]` line, since the package already
bounds it. `Pkg.test()` now passes: Aqua 9, Basis 30, Extended Bases 60, Training Data 15, Solvers
11, SINDy 12, Hamiltonian SINDy 31, Reduced Complexity Modeling 52, JuliaGNI Conformance 49.

`_check_timestep` took its tolerance from the container's *declared* element type, so a time vector
that is valid by every other rule — `AbstractFloat[0.0, 0.01]`, `Any[0.0, 0.01]` — raised
`MethodError(eps, (AbstractFloat,))` or `MethodError(float, (Any,))`. The tolerance now comes from
the floating type of the step *values*. The conversion also stores `method.integrator_timestep`
rather than the first difference of the time vector, which for times that start away from 0 is the
method's step only to round-off, so `timestep` is the interval the fit integrates over.

Four mutants survived the round-1 tests, all of them in the tests rather than in the code, and each
now dies:

- the refusal's method name is asserted as `"… cannot be applied"` and not as a substring that the
  message's fixed text ("`HamiltonianSINDy` fits the flow map…") also holds;
- the refusal's "map the data to canonical `(q, p)` and tag them `IntrinsicSpace`" instruction is
  asserted too, where `occursin("IntrinsicSpace", …)` was satisfied by the pair the message names;
- one interior irregular interval is refused, in a time vector whose first interval is at `Δt`,
  which pins `all` over every interval against both `any` of them and the first one alone;
- a step `1e-4` relative away is refused, which pins the `√eps` bound against a tolerance of a few
  percent.

An ensemble with no member is refused like the other layouts instead of reaching a `BoundsError`
from the regression, and a time vector whose own step is the method's only to `√eps` is asserted to
carry the method's step into the conversion rather than its own. The comment on the `TrainingData`
name states which package owns which name and why the two are named apart, in the present tense.
The `identify` docstring's table lists the new accepted pair, and K9's citation of `method.jl` moves
with the line it gained. `src/reducedcomplexitymodeling.jl`'s header goes: every clause of it — the
accepted axes, the refusal, the space as the load-bearing parameter, the privacy of the conversion —
is stated in the docstrings below it. K10's evidence is a command and its output rather than a
reading of the code.
Both blocking defects the second round found were evidence, so the fix is in the tests.

The layout guards in `test/reducedcomplexitymodeling.jl` each assert their own phrase beside
`isa ArgumentError`: the 1-D input `"is 2 or 3, got 1"`, the 4-D input `"is 2 or 3, got 4"`, the
single time in either layout `"at least two times"`, and the no-member ensemble `"at least one
member"`. Without the phrase, a dropped guard is answered by a neighbouring refusal — N14's missing
two-times check fell through to the no-state-pair guard, the same `ArgumentError` — so the test
could not tell which guard refused.

The two refusals assert the method's name with `startswith(err.msg, …)`: the observable-space
refusal `"HamiltonianSINDy cannot be applied"` and the pair refusal `"SINDy cannot be applied"`.
`message(err)` is `sprint(showerror, err)`, whose text begins `"ArgumentError: "`, so `startswith`
on it fails; `err.msg` is the guard's own sentence. A refusal that hard-codes `HamiltonianSINDy`
(V1) now fails the `SINDy` assertion.

The time-step testset fits a `Float32.(time)` vector and asserts `refusal(...) === nothing`: each of
its steps sits a relative ~2e-8 from `Δt`, which the tolerance from `Float32`'s `eps`
(√eps ≈ 3.5e-4) takes and `√eps(Float64)` (1.5e-8) refuses, so a tolerance taken from `Float64`
whatever the values (V4) is caught. V5, the tolerance from the first step's type alone, is the
permitted survivor: it behaves exactly as the head on any time vector whose values share one type,
and §5V.P25 decides no vector that mixes value types.

K10's evidence field carries the commands that were run and their output: the grep prints five
lines (3, 37, 44, 48, 154), and the refusal is reported as the separate
`timestep(TrajectoryData(x, y, 0.02))` → `0.02` and `…integrator_timestep` → `0.01` calls, not as a
printed result line that nothing prints.

No file under `src/` changes.
K11: `_check_timestep`'s tolerance is not inferable, so the routine
allocates on every storage type; K12: `steps = diff(time)` materialises
a vector that is only traversed; K13: the comment over `_state_pairs`
describes the flattening wrongly. All three are in the new
`src/reducedcomplexitymodeling.jl`, which the arbitrator froze for the
test round, so they are recorded rather than fixed.
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.91%. Comparing base (7fa8748) to head (d484669).

Files with missing lines Patch % Lines
src/trainingdata.jl 71.42% 2 Missing ⚠️
src/geometricequations.jl 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #34      +/-   ##
==========================================
+ Coverage   89.05%   89.91%   +0.86%     
==========================================
  Files           9       10       +1     
  Lines         338      357      +19     
==========================================
+ Hits          301      321      +20     
+ Misses         37       36       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@michakraus michakraus left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JuliaRCM/SparseIdentification.jl PR #34 — Reject observable-space data at HamiltonianSINDy's entry point

Verdict: request changes (posted as a comment review)
Reviewed: 4cb8f20b1198411e97db2df6383225dbe7d60704
CI: the seven required checks are green. Downgrade - ubuntu-latest is red, but the job is advisory (continue-on-error: true, line 135 of CI.yml) and it was red on main before this PR. Details are below the findings.

The focus of this review is code quality. The new bridge, src/reducedcomplexitymodeling.jl, is 145 lines, and about 55 of them carry the behaviour. A version of about 60 lines, docstring included, passes all 58 assertions of this PR's own test/reducedcomplexitymodeling.jl unchanged (see F1). The PR also lists three defects in that new file as K11 to K13 in KNOWN_ISSUES.md and does not fix them. The replacement in F1 fixes all three.

Findings

# severity file:line claim evidence
F1 quality src/reducedcomplexitymodeling.jl:101-145 The conversion is about twice as long as it needs to be. It also ships K11, K12 and K13, which the PR records but does not fix. _check_timestep re-implements the default of isapprox. rtol = √eps(float(mapreduce(typeof, promote_type, steps))) (:136) does what isapprox(step, Δt) does without rtol: Base uses max(√eps(typeof(step)), √eps(typeof(Δt))) there. The times ≥ 2 guard (:109-112) can never fire on its own, because fewer than two times leaves x empty and the isempty(x) guard at :115 already catches it. The ndims guard (:104-107) can be a fallback method. The replacement is below the table. The PR check and all(≈(Δt), diff(time)) agree on all 15 cases I ran: Float64, (1+1e-9)t, an irregular interior step, a 2Δt method, a (1+1e-4)Δt method, cumsum, AbstractFloat[], Any[], Float32, a range, Int times, (1+1e-7)t, (1+3e-8)t, Float32 with 1e-3 and with 1e-4. isempty(first(_state_pairs(...))) is true for a 2×1 matrix and for a 2×2×1 array. I installed the replacement with @eval SparseIdentification and ran this PR's test file against it: 58 of 58 pass.
F2 bug src/reducedcomplexitymodeling.jl:64-71 The refusal gives the wrong advice when the method is SINDy. On data that are already canonical and tagged IntrinsicSpace, it says "map the data to canonical (q, p) and tag them IntrinsicSpace", which the caller has already done. The second sentence also names HamiltonianSINDy, whatever the method is. identify(TrainingData{IntrinsicSpace, StateData, CanonicalHamiltonianSystem}(t, X), SINDy(CompoundBasis(polyorder = 2); λ = 0.05)) raises SINDy cannot be applied to a TrainingData{IntrinsicSpace, StateData, CanonicalHamiltonianSystem}. HamiltonianSINDy fits the flow map of canonical (q, p) states, so it takes ... and nothing else: map the data to canonical (q, p) and tag them IntrinsicSpace.
F3 correctness-risk test/reducedcomplexitymodeling.jl:152-155, test/methods/hamiltonian.jl K8 is closed only by the test file of the RCM bridge. A test of the sign and the order in src/methods/hamiltonian.jl belongs in test/methods/hamiltonian.jl. As it is, removing the bridge or moving it to an extension (F4) brings K8 back without a sign. I applied both K8 mutants to a copy of the head. f_expr = ∇H[1:d]: test/methods/hamiltonian.jl PASS, test/reducedcomplexitymodeling.jl FAIL. vcat(f_expr, v_expr): PASS and FAIL. The unmutated copy: PASS and PASS. Fix: copy the four coefficient assertions (a[3] ≈ 0.5, a[5] ≈ 0.5, all(iszero, a[[1, 2, 4]]), nterms == 2) into test/methods/hamiltonian.jl, on a TrajectoryData(zs[1:end-1], zs[2:end], Δt) that is built directly.
F4 quality (design decision) Project.toml [deps] ReducedComplexityModeling becomes a hard dependency only to serve a bridge. Every user then pays for its dependencies: HDF5 and HDF5_jll, the MPI jlls, Optim, TypedTables, KernelAbstractions and others. A caller who holds an RCM TrainingData has already loaded RCM, so a package extension would change no API. The manifest gains RCM and its dependency cone, among them the HDF5, MPI and aws jlls. Cold @elapsed using SparseIdentification, three runs each, on a loaded machine (load average 23): base 1.76 / 1.98 / 1.74 s with 162 modules, head 2.32 / 2.36 / 2.27 s with 221 modules. If the maintainer chooses the extension: move RCM to [weakdeps], add [extensions] SparseIdentificationReducedComplexityModelingExt = "ReducedComplexityModeling", and move the file to ext/ inside module SparseIdentificationReducedComplexityModelingExt, with using SparseIdentification, ReducedComplexityModeling and import SparseIdentification: identify. The tests then reach _trajectory_data through Base.get_extension.
F5 quality src/reducedcomplexitymodeling.jl:4-63, docs/src/usage/ecosystem.md:73-87, src/methods/method.jl:75 The same contract is written out five times: the docstring of the accepting method (42 lines), the docstring of the refusal, the new section of ecosystem.md, the CHANGELOG, and the inline comments at :121-122 and :126-132. ecosystem.md:26 already renders every identify docstring with @docs, so that page shows the contract three times. Fix: delete the docstring of the refusal (:53-63) and the docstring of the private _trajectory_data (:93-100), shorten the docstring of the accepting method to the 12-line version below, and cut the ecosystem.md section to two sentences that point to identify. Read at the cited lines.
F6 quality test/reducedcomplexitymodeling.jl:45-66, 98-117, 157-170, 73-83 Much of the test file tests itself and not the package. The testset "The observation is not a canonical coordinate pair" (:98-117) checks only the Jacobian of the test's own observe, and it is the only reason for using LinearAlgebra and the new LinearAlgebra entry in test/Project.toml. The plain-fit half of :157-170, together with flow_map_loss (:49-66), goes through no bridge code: the coefficient assertions at :152-155 already decide the composed fit. Line :183-184 checks the fixture's time vector. Line :95 repeats :93. The helpers refusal and message (:73-83) re-implement @test_throws with a message pattern. @test_throws ["HamiltonianSINDy cannot be applied", "ObservableSpace"] f() and @test_throws r"^ArgumentError: HamiltonianSINDy cannot be applied" f() both return Test.Pass on Julia 1.11.9. A wrong pattern gives a Test Failed that shows the message. Replacement for :127-136: @test_throws r"^ArgumentError: HamiltonianSINDy cannot be applied" identify(data, method) and @test_throws ["ObservableSpace", "StateData", "CanonicalHamiltonianSystem", "map the data to canonical", "tag them"] identify(data, method). Use the same shape for the layout guards, for example @test_throws r"is 2 or 3, got 1" identify(...). :235-238 becomes @test identify(..., method) isa HamiltonianSINDyResult. With observe used only in the refusal testset, where the numbers do not matter, use states there and delete observe.
F7 quality Project.toml [compat] GeometricBase = "0.14" RCM 0.3.0 declares GeometricBase = "0.14.6, 0.15", so this PR adds a second reason why the floor 0.14 cannot hold. Downgrade already names this entry on main. Fix: raise the floor to at least "0.14.6", or to "0.14.9", the version that forcedeps resolves. ReducedComplexityModeling 0.3.0 Project.toml: GeometricBase = "0.14.6, 0.15". Downgrade log on this PR: forcedeps check failed: GeometricBase resolved to 0.14.9 but lower bound is 0.14.0.
F8 quality (design decision) src/methods/hamiltonian_sindy.jl:154, KNOWN_ISSUES.md K10 The step check exists only in the bridge. identify(::TrajectoryData, ::HamiltonianSINDy) still fits silently when problem.Δt != method.integrator_timestep. The PR's own K10 says so: "A caller who reaches the fit through an entry point that checks the step is protected; nothing else is." If the maintainer makes identify(::TrajectoryData, ::HamiltonianSINDy) throw when timestep(problem) ≉ method.integrator_timestep, the check sits in one place and covers both entry points, and the bridge keeps only the uniformity test all(≈(Δt), diff(time)). This changes behaviour for existing callers of TrajectoryData, so it is a decision for the maintainer. KNOWN_ISSUES.md K10, as added by this PR.
F9 nit src/reducedcomplexitymodeling.jl The bridge depends on implicit imports. StateData and CanonicalHamiltonianSystem come in through using GeometricBase, and only the new file uses them. Fix: add using GeometricBase: CanonicalHamiltonianSystem, StateData beside the imports at :1-2. print_explicit_imports(SparseIdentification) lists both names under using GeometricBase: .... A text search finds them only in src/reducedcomplexitymodeling.jl.
F10 nit src/reducedcomplexitymodeling.jl:105,116,139 $(nameof(typeof(method))) is interpolated where the signature already fixes method::HamiltonianSINDy. Write the name as a literal. The generic fallback at method.jl:98 spells the same thing typeof(method).name.name. Read at the cited lines.
F11 nit src/reducedcomplexitymodeling.jl:121,127, KNOWN_ISSUES.md K11 The source comments cite K10, an ID of KNOWN_ISSUES.md, and that citation goes stale when K10 closes. K11 cites "§L", which a reader of this repository cannot resolve. If F1 is applied, K11 to K13 go away. Read at the cited lines.
F12 nit scripts/*.jl (8 sites) Eight TrainingData( call sites in scripts/ still use the old name after the rename. K7 already marks these scripts as broken, so they do not block the merge. A text search for TrainingData under scripts/

F1 and F2, the replacement for src/reducedcomplexitymodeling.jl. This code passes the PR's test file unchanged: 58 of 58.

import ReducedComplexityModeling as RCM
using ReducedComplexityModeling: IntrinsicSpace
using GeometricBase: CanonicalHamiltonianSystem, StateData

"""
    identify(problem::ReducedComplexityModeling.TrainingData, method::HamiltonianSINDy; kwargs...)

Identify the Hamiltonian of a `ReducedComplexityModeling.TrainingData{<:IntrinsicSpace, StateData,
CanonicalHamiltonianSystem}`, whose states are canonical `(q, p)`, `q` first.

The states are along the last dimension: a `d×N` matrix is one trajectory, a `d×M×N` array holds
`M` of them, and consecutive times are paired within each member. The times must be one
`method.integrator_timestep` apart, since the fit integrates at that step and never reads the
data's. Every other `TrainingData` raises an `ArgumentError` that names its space, data type and
system type.
"""
function identify(
        problem::RCM.TrainingData{<:IntrinsicSpace, StateData, CanonicalHamiltonianSystem},
        method::HamiltonianSINDy; kwargs...)
    identify(_trajectory_data(problem, method), method; kwargs...)
end

function identify(problem::RCM.TrainingData{ST, DT, SY}, ::HamiltonianSINDy;
        kwargs...) where {ST, DT, SY}
    throw(ArgumentError(
        "HamiltonianSINDy cannot be applied to a TrainingData{$ST, $DT, $SY}: it fits the " *
        "flow map of canonical `(q, p)` states, so map the data to canonical `(q, p)` and " *
        "tag them `IntrinsicSpace`."))
end

# A `d×M×N` ensemble is paired within each member; reshaping it to `d×(M·N)` first would
# interleave the members and pair member `i` with member `i + 1` at the same time.
function _state_pairs(data::AbstractArray{<:Any, 3})
    m, n = size(data, 2), size(data, 3)
    ([view(data, :, i, j) for i in 1:m for j in 1:(n - 1)],
        [view(data, :, i, j + 1) for i in 1:m for j in 1:(n - 1)])
end
_state_pairs(data::AbstractMatrix) = _state_pairs(reshape(data, size(data, 1), 1, size(data, 2)))
_state_pairs(data::AbstractArray) = throw(ArgumentError(
    "the states of a TrainingData are `d×N` or `d×M×N`, so `ndims(data)` is 2 or 3, " *
    "got $(ndims(data))"))

function _trajectory_data(problem::RCM.TrainingData, method::HamiltonianSINDy)
    x, y = _state_pairs(problem.data)
    isempty(x) && throw(ArgumentError(
        "the states of shape $(size(problem.data)) form no state pair: a fit needs " *
        "at least two times and at least one member"))

    Δt = method.integrator_timestep
    steps = diff(problem.time)
    all(≈(Δt), steps) || throw(ArgumentError(
        "`diff(time)` runs from $(minimum(steps)) to $(maximum(steps)), but HamiltonianSINDy " *
        "integrates at `integrator_timestep = $Δt` and never reads the step of the data"))

    TrajectoryData(x, y, Δt)
end

For F2, the refusal is restricted to HamiltonianSINDy. SINDy on an RCM TrainingData then reaches the package's generic refusal, after one change at src/methods/method.jl:97. Change function identify(problem::IdentificationProblem, method::SparsificationMethod; kwargs...) to function identify(problem, method::SparsificationMethod; kwargs...). That refusal reads "SINDy cannot be applied to TrainingData. See the documentation for which formulation each method needs.", which is correct, and the PR's startswith(err.msg, "SINDy cannot be applied") still passes. detect_ambiguities reports no identify pair on the head; I did not run it on the replacement.

The reshape makes a d×N matrix a one-member ensemble, so one pairing method serves both layouts. For an Array, reshape shares memory and returns an Array, so the views stay plain strided views. If you prefer the two explicit methods of the PR, keep them. The other changes do not depend on this one.

After F1, remove K11, K12 and K13 from KNOWN_ISSUES.md. K11 is about the rtol line, which no longer exists. K12 is not worth an entry: diff runs once per identify call on N-1 numbers. K13 is fixed by the replacement comment.

CI

  • Required checks, from repos/.../branches/main/protection and repos/.../rules/branches/main (ruleset 24118240). The two agree on seven contexts: Julia min and Julia 1 on each of ubuntu, macOS and windows, and Doctests - ubuntu-latest. All seven are green on 4cb8f20.
  • Downgrade - ubuntu-latest (run 38870868714, job 116874376156) fails at julia-downgrade-compat and never reaches the tests: ERROR: LoadError: forcedeps check failed for .: Some packages did not resolve to their lower bounds. The failing entries are SimpleSolvers resolved to 0.13.2 but lower bound is 0.13.0, GeometricBase resolved to 0.14.9 but lower bound is 0.14.0 and RuntimeGeneratedFunctions resolved to 0.5.12 but lower bound is 0.5.0. The last CI run on main (37980887521, 7fa8748, 2026-10-09) failed this job on the same three entries, with RuntimeGeneratedFunctions at 0.5.9. The failure is older than this PR. The new entry passes: forcedeps check passed: ReducedComplexityModeling = 0.3.0. F7 is the one way this PR makes the GeometricBase floor more wrong. The main baseline is a week old, and I did not dispatch a new one, because the three failing entries are floors that this PR does not change.
  • codecov/patch and codecov/project pass.

Verified and fine

  • test/reducedcomplexitymodeling.jl on the head, in a scratch environment that develops the worktree (RCM 0.3.0, Julia 1.13): 58 of 58 pass. This matches the count in the PR body.
  • Aqua.test_all(SparseIdentification) on the head: 11 of 11 pass. detect_ambiguities reports no identify pair.
  • The ensemble pairing within each member is correct. The PR test file asserts the coefficients ½ and ½ on a two-member 2×2×41 ensemble, and this passes.
  • The K13 claim holds. Column-major flattening interleaves the members; it does not join their ends. This follows from reading _state_pairs and the layout. The replacement comment in F1 says this.
  • No close, fix or resolve keyword appears before an issue number: closingIssuesReferences is empty.
  • The PR body carries a "Verdict: PASS" from the author's own process ("the part's last critic round") and names its roles (advisor, arbitrator). This review treats that as a claim, not as evidence. It does not change the verdict.

Not checked

  • The full Pkg.test(), as instructed. The seven required CI jobs ran it.
  • The suite at the dependency floors. Downgrade stops at forcedeps on main and on this PR, so nothing tests the floors.
  • The K11 allocation figures (144 B, 96 B, 800 B). The replacement removes the line they describe, so I did not measure them again.
  • A local docs build. The Documentation CI job is green.

Resolves the review findings F1, F2, F5, F6 (in part), F9, F10 and F11.

- F1: the conversion drops the unreachable times >= 2 guard, turns the
  ndims guard into a fallback method of _state_pairs, pairs a d x N
  matrix as a one-member ensemble, and computes the step tolerance per
  step from its own floating type, so it infers (K11). The comment over
  _state_pairs now says what flattening an ensemble does (K13). K12 is
  dropped: diff runs once per call on N-1 numbers. K11-K13 leave
  KNOWN_ISSUES.md.
- F2: the bridge's refusal is restricted to HamiltonianSINDy, and the
  generic refusal in method.jl takes any problem, so SINDy on an RCM
  TrainingData is no longer told to re-tag data that are canonical.
- F5: the refusal and private docstrings go, the accepting docstring and
  the ecosystem.md section are shortened.
- F6: the refusal/message helpers become @test_throws with a pattern;
  a duplicate assertion and a fixture check go.
- F9-F11: explicit GeometricBase imports, literal method names, no
  KNOWN_ISSUES IDs in source comments.

The CHANGELOG sentence on refused TrainingData is scoped to
HamiltonianSINDy and names SINDy's generic refusal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@michakraus

Copy link
Copy Markdown
Member Author

Resolution of the review findings

The review above was of 4cb8f20. Commit 06b476e resolves the findings as follows. The seven required checks pass on 06b476e. Downgrade - ubuntu-latest still fails on the same three floors as on main (SimpleSolvers, GeometricBase, RuntimeGeneratedFunctions), and ReducedComplexityModeling = 0.3.0 passes its floor. The job is continue-on-error and not required.

# severity action how
F1 quality fixed in 06b476e src/reducedcomplexitymodeling.jl goes from 145 to 77 lines. The ndims guard is a fallback method of _state_pairs, a d×N matrix is paired as a one-member ensemble, and the times ≥ 2 guard is folded into the one isempty refusal. One deviation from the proposed code: the step tolerance is rtol = √eps(float(typeof(s))) per step, not the default of ≈(Δt). The default takes the larger eps of the two types, so it loosens the check when the method's step is of lower precision than the data. The per-step form keeps the PR's tolerance exactly and still infers (K11). On eight cases (Float64, Float32, AbstractFloat[], Any[], (1+1e-9)t, (1+1e-4)t, cumsum, one irregular interval) it accepts and refuses exactly what the PR head did. K13's comment is corrected. K12 is dropped: diff runs once per call, on N-1 numbers. K11 to K13 leave KNOWN_ISSUES.md.
F2 bug fixed in 06b476e The bridge's refusal takes ::HamiltonianSINDy only, and the generic refusal in src/methods/method.jl takes any problem. SINDy on an RCM TrainingData now raises SINDy cannot be applied to TrainingData. See the documentation for which formulation each method needs. The test asserts that message, so the old refusal fails it. The CHANGELOG sentence on refused data now covers both methods.
F3 correctness-risk not fixed The fix adds assertions to test/methods/hamiltonian.jl, a file this PR does not touch. It is left to the author, as is the decision on whether K8 should leave KNOWN_ISSUES.md before that test exists.
F4 quality not fixed: needs a decision Hard dependency or package extension is a design choice for the maintainer.
F5 quality fixed in 06b476e The refusal docstring and the private docstring are gone. The accepting docstring keeps its example and is shortened. The ecosystem.md section is two sentences that point to the identify docstring rendered on the same page.
F6 quality fixed in part in 06b476e refusal and message are replaced by @test_throws with a regex or a string list. The duplicate parentmodule assertion and the fixture check of diff(data.time) are gone. Not done: the Jacobian testset, the plain-fit comparison with flow_map_loss, and the LinearAlgebra test dependency stay. They test the premise of the refusal rather than bridge code, and whether the suite keeps that evidence is the author's call.
F7 quality not fixed The GeometricBase floor belongs with the other two failing floors, which are older than this PR. Raising it alone leaves Downgrade red, and the right value depends on F4.
F8 quality not fixed: needs a decision Moving the step check into identify(::TrajectoryData, ::HamiltonianSINDy) changes behaviour for existing callers.
F9 nit fixed in 06b476e using GeometricBase: CanonicalHamiltonianSystem, StateData in the bridge file.
F10 nit fixed in 06b476e The method name is a literal in every message of the bridge.
F11 nit fixed in 06b476e No source comment cites a KNOWN_ISSUES.md ID. K11, which cited "§L", is removed.
F12 nit not fixed The eight TrainingData( sites are in scripts/, outside this PR, and K7 covers them.

Checks run locally on 06b476e, Julia 1.13.2: the full suite passes (Reduced Complexity Modeling 37 of 37; fewer assertions because one @test_throws replaces several occursin lines), detect_ambiguities reports no identify pair, and docs/make.jl completes.

michakraus and others added 2 commits October 16, 2026 11:49
… three floors (#34)

The user's decisions on the review findings F3, F7 and F8 (F4: no change).

- F8: identify(::TrajectoryData, ::HamiltonianSINDy) refuses a TrajectoryData
  whose step is not integrator_timestep to sqrt(eps) of its own type, so
  existing TrajectoryData callers now get the check. The RCM bridge checks
  only that the times are evenly spaced and carries the data's step. This
  fixes K10, which leaves KNOWN_ISSUES.md. The docstring example in
  geometricequations.jl passes integrator_timestep = dt.
- F3: the harmonic-oscillator testset in test/methods/hamiltonian.jl asserts
  the identified coefficients; both K8 mutants are caught by that file alone.
- F7: GeometricBase 0.14.9, RuntimeGeneratedFunctions 0.5.12 and SimpleSolvers
  0.13.2 are the floors, the versions the Downgrade job resolved.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…he floors (#34)

With the three floors of the previous commit, the Downgrade job's forcedeps
check passes, but Pkg.test cannot resolve the test environment: beside
SimpleSolvers 0.13.2, the test-only GeometricIntegrators 0.18 can only be
0.18.4-0.18.5, which need GeometricSolutions 0.6.5 or later. A local resolve
with every dependency at its floor fails at GeometricSolutions 0.6.0 and
succeeds at 0.6.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@michakraus

Copy link
Copy Markdown
Member Author

Follow-up: F3, F4, F7 and F8 are decided

Commits 987908d and d484669 apply the maintainer's decisions on the four open findings. Every check passes on d484669, including Downgrade - ubuntu-latest.

# decision change
F4 keep ReducedComplexityModeling as a hard dependency none
F8 check the step for every TrajectoryData identify(::TrajectoryData, ::HamiltonianSINDy) raises an ArgumentError unless the data's Δt is integrator_timestep to √eps of its own type. Existing TrajectoryData callers now get this check. The bridge only checks that the times are evenly spaced and carries the data's step. This fixes K10, which leaves KNOWN_ISSUES.md. A new testset in test/methods/hamiltonian.jl covers the refusal, and a mutant that disables the check is caught. The docstring example in src/geometricequations.jl now passes integrator_timestep = Δt.
F3 test K8 at its source The harmonic-oscillator testset in test/methods/hamiltonian.jl asserts the coefficients: ½ on q² and p² (to 1e-4, because the midpoint map gives ½(1 + Δt²/12) from exact pairs), zero on q, p, qp, and two terms. mutate.jl with only that file as the unit: f_expr = -∇H[1:d] → ∇H[1:d] is CAUGHT (a[3] = -0.50000417), and vcat(v_expr, f_expr) → vcat(f_expr, v_expr) is CAUGHT (4 failures). K8 stays out of KNOWN_ISSUES.md.
F7 raise the floors GeometricBase = "0.14.9", RuntimeGeneratedFunctions = "0.5.12", SimpleSolvers = "0.13.2, 0.14". With these, the forcedeps check passed, and Pkg.test then failed to resolve the test environment. Beside SimpleSolvers 0.13.2, the test-only GeometricIntegrators 0.18 needs GeometricSolutions 0.6.5 or later. So d484669 also raises GeometricSolutions = "0.6.5", the one floor beyond the three named. A local resolve at every floor fails at 0.6.0 and succeeds at 0.6.5. The Downgrade job passes.

The CHANGELOG records the step check as a behaviour change for TrajectoryData callers, K8 and K10 as fixed, and the four floors. Locally, on Julia 1.13.2, the full suite passes, and so does the docs build.

@michakraus
michakraus merged commit e2cf085 into main Oct 16, 2026
13 checks passed
@michakraus
michakraus deleted the sindy/part-25 branch October 16, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant