Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "GeometricBase"
uuid = "9a0b12b7-583b-4f04-aa1f-d8551b6addc9"
version = "0.15.0"
version = "0.15.1"
authors = ["Michael Kraus"]

[deps]
Expand Down
8 changes: 5 additions & 3 deletions src/data/state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions test/data/state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading