From 1d38e7a4ce7e9300c7fb54042375b7d47f248c4d Mon Sep 17 00:00:00 2001 From: Michael Kraus Date: Sun, 11 Oct 2026 22:47:07 +0200 Subject: [PATCH] Keep the vector field of an empty state variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State dropped a vector field wherever `all(ismissing, x)` held, which is true of every empty array, so a state variable with a zero extent had no `q̇` and `integrate` raised `FieldError` on any problem whose state is empty. A field is now dropped only where `_vectorfield` returns `missing`. Version 0.15.1, so that a dependant can bound the release that carries the fix. --- CHANGELOG.md | 8 ++++++++ KNOWN_ISSUES.md | 15 +++++++++++++++ Project.toml | 2 +- src/data/state.jl | 8 +++++--- test/data/state.jl | 18 ++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1cc920..7387381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ makes it worth keeping. `src/public.jl`. The file tests the public names of `src/methods.jl`, but `test/methods.jl` already exists, so it moved to `test/integration/`. +### Fixed + +- **`State` keeps the vector field of an empty state variable.** A field was dropped where + `all(ismissing, x)` held, which is true of every empty array, so a state variable with a zero + extent lost its field and a `6 × 0` particle matrix had no `q̇` to fill — `integrate` raised + `FieldError` on any problem whose state is empty. A field is now dropped only where + `_vectorfield` returns `missing`, so a vector field mirrors the shape of its state variable. + ## [0.15.0] — 2026-10-01 ### Breaking Changes diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 71fbbac..d55ea5c 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -47,3 +47,18 @@ Defects found in review and not yet fixed. 1.14.3–2.2.2 by Julia. - **kind:** not verified - **found:** PR #32 + +### K6 · `L2norm(x, y)` raises on an empty argument, so a nonlinear solve of an empty state stops in its status. + +- **location:** `src/utils/norms.jl:2-5` +- **evidence:** `L2norm(x, y)` is `mapfoldl(z -> (z[1] - z[2])^2, +, zip(x, y))`, and `mapfoldl` with + `+` refuses an empty collection: `L2norm(Float64[], Float64[])` gives + `ArgumentError: reducing over an empty collection is not allowed; consider supplying init to the + reducer`. The one-argument method at `:14-17` guards with `isempty(x) && return zero(T)`, so a + norm of an empty array is `0`; the two-argument method has no such guard. Its callers include + `SimpleSolvers` `src/nonlinear/nonlinear_solver_status.jl:143,145`, which `residuals(state)` + computes at every iteration of every nonlinear solve — the step an implicit integrator takes — + and GeometricOptimizers' line searches. So a solve over an empty state fails in the status, with + a message that names no solver and no integrator. +- **kind:** defect +- **found:** 2026-10-11 diff --git a/Project.toml b/Project.toml index 07d3048..510e962 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "GeometricBase" uuid = "9a0b12b7-583b-4f04-aa1f-d8551b6addc9" -version = "0.15.0" +version = "0.15.1" authors = ["Michael Kraus"] [deps] diff --git a/src/data/state.jl b/src/data/state.jl index 64f6029..ad85f1f 100644 --- a/src/data/state.jl +++ b/src/data/state.jl @@ -76,9 +76,11 @@ function State(initialtime::Real, ics::NamedTuple; initialize = true) vectorfield = map(x -> _vectorfield(x), ics) # remove all fields that are missing, i.e., that correspond to a variable without vectorfield. - # `_vectorfield` returns the scalar `missing` for those, so that case is tested on its own: - # `all(ismissing, missing)` cannot iterate a scalar, and a broadcast here would allocate. - _novectorfield(x) = ismissing(x) || all(ismissing, x) + # A field is dropped only where `_vectorfield` returns `missing`, which it does for a time, + # algebraic or vector-field variable and nowhere else. A vector field mirrors the shape of its + # state variable, so an empty state variable keeps its field: `all(ismissing, x)` is true of + # every empty array, which would drop the vector field of a `6 × 0` state. + _novectorfield(x) = ismissing(x) vectorfield_filtered = NamedTuple{filter(k -> !_novectorfield(vectorfield[k]), keys(vectorfield))}(vectorfield) # create vector field symbols with dotted solution symbols diff --git a/test/data/state.jl b/test/data/state.jl index 6ca9fda..d51d496 100644 --- a/test/data/state.jl +++ b/test/data/state.jl @@ -171,3 +171,21 @@ end @test isnan(st) end + +@testset "$(rpad("State of an Empty or Scalar State Variable",80))" begin + # A vector field mirrors the shape of its state variable, so a state variable with a zero + # extent keeps its field. `all(ismissing, x)` is true of every empty array, and dropping the + # `q̇` of a `6 × 0` state leaves it without a vector field. + for q in (zeros(6, 0), zeros(0), zeros(3, 0, 2), fill(1.0)) + st = State(0.0, (q = StateVariable(q),)) + @test :q̇ ∈ keys(st) + @test size(st.q̇) == size(st.q) + end + + # a field is still dropped where `_vectorfield` returns `missing` + st = State(0.0, (t = TimeVariable(1.0), q = StateVariable(zeros(6, 0)), + λ = AlgebraicVariable(zeros(0)))) + @test :q̇ ∈ keys(st) + @test :q ∈ keys(vectorfield(st)) + @test length(vectorfield(st)) == 1 +end