Skip to content

Modernise CI, split out the docs build, and make the per-step paths allocation-free - #5

Merged
michakraus merged 4 commits into
mainfrom
modernise-ci-and-tidy-repo
Aug 21, 2026
Merged

Modernise CI, split out the docs build, and make the per-step paths allocation-free#5
michakraus merged 4 commits into
mainfrom
modernise-ci-and-tidy-repo

Conversation

@michakraus

Copy link
Copy Markdown
Member

Verifies the package, modernises the GitHub workflows, splits the documentation build into its own workflow, and adds the repository metadata that was missing. Three commits, reviewable independently.

Verification

check result
Pkg.test() on Julia 1.13.0-rc2 5,116 tests pass
docs build + doctests clean, no Documenter warnings
clean resolve from the registry, no dev paths resolves and loads

Allocations

The Cox-de Boor kernel was already exactly allocation-free and type-stable — 0 B for evaluate at d = 0…3, p = 1,3,5, on uniform, graded and random meshes, 0 B for spline reconstruction and for a 1000-point sweep, and 0 B for a CirculantMass solve, so mass.jl's claim about that path holds. Two things on per-step paths were not:

  • l2_projection! was in-place only in its output, building the f ⊙ w product and the load vector fresh on every call — 6.3 kB per projection at N = 128. The quadrature now holds the f ⊙ w buffer and the load vector is formed with mul! straight into û. 0 B now at N = 64/128/256 on a uniform mesh; on a non-uniform mesh what remains is entirely CHOLMOD's temporary, which has no in-place ldiv!.
  • basis_integrals recomputed Φ₀ * w every call, 1.1 kB a time, although ∫ φᵢ dx is as much a constant of the discretisation as the mass matrix. Assembled once at construction and returned by reference now. 0 B.

Both pinned by tests, in the style of the existing mass_solve! allocation test. Results verified unchanged: l2_projection(q, f) ≈ û on both mesh families, basis_integrals still M * 1 summing to L.

weighted_matrix still allocates 260 kB a call and is not fixed here — it needs a weighted_matrix! entry point and a hand-written sparse triple product against a cached pattern, which widens the API. Recorded under ## Open Issues in the changelog with the measured breakdown and the shape of the fix.

Workflows

The docs job moves out of CI.yml into Documenter.yml, so a docs failure and a test failure are separate signals and the docs job is not queued behind the test matrix. README gains a badge for it.

Actions go to checkout@v7, setup-julia@v3, cache@v3, codecov-action@v7; RegisterAction is pinned so Dependabot can bump it, which @latest prevented. Dependabot groups action bumps into one PR. Every referenced tag was checked to resolve rather than assumed.

The matrix names Julia versions by alias instead of by number: min resolves the lower bound of the julia compat entry, so it tracks the declared support window instead of needing to be edited alongside it; lts and 1 cover long-term-support and current stable; pre and nightly run on Linux only under continue-on-error. arch: default replaces x64, which tests natively on the ARM64 macOS runners rather than under Rosetta.

Two live workflow bugs fixed on the way:

  • docs/Project.toml pinned CompactBasisFunctions to an absolute path on a developer's machine via [sources]. That path does not exist on a runner, so the documentation build could only ever have succeeded locally. 0.3.1 is registered, so the entry goes; verified by deleting docs/Manifest.toml, re-resolving from the registry and rebuilding the docs clean.
  • CompatHelper.yml invoked julia without installing it. The runner images no longer ship a Julia, so it failed before CompatHelper started.

Repository metadata

AUTHORS.md added, following the NeuralNetworkParameters.jl wording. LICENSE renamed to LICENSE.md, with its copyright line now naming "The SimpleSplines Authors" and pointing at it — otherwise AUTHORS.md's reference to what the LICENSE mentions would not be true. README gains a License section.

.githooks/pre-push added. The README already told the reader to run git config core.hooksPath .githooks, but .githooks/ did not exist, so the instruction was silently a no-op. It runs the suite and refuses the push on failure, with SIMPLESPLINES_SKIP_TESTS=1 as a documented override; both paths exercised.

Note on the branch

Committed to a branch rather than to main directly, though the previous commit went straight to main. Fast-forward and drop the PR if that is the convention you want.

🤖 Generated with Claude Code

michakraus and others added 3 commits August 21, 2026 19:18
`l2_projection!` was in-place only in its output: it built the `f ⊙ w` product
and the load vector as fresh arrays on every call, 6.3 kB per projection at
N = 128. The quadrature now holds an `f ⊙ w` buffer and the load vector is
formed with `mul!` straight into `û`, which takes a uniform-mesh projection to
zero allocations end to end -- the `CirculantMass` solve was already
allocation-free. On a non-uniform mesh the CHOLMOD temporary remains, CHOLMOD
having no in-place `ldiv!`.

The shared buffer makes the method non-reentrant across threads, so the
docstring says so and points at the allocating `l2_projection` as the way out.
It also gains the sample-length check the allocating method already had; the
buffer would otherwise have turned a wrong-length `f` into a broadcast error
rather than a named one.

`basis_integrals` recomputed `Φ₀ * w` on every call, 1.1 kB a time, although
∫ φ_i dx is as much a constant of the discretisation as the mass matrix. It is
now assembled with the quadrature and returned by reference, like
`mass_matrix`, and the docstring records that it must not be mutated.

Both properties are pinned by tests, in the style of the existing
`mass_solve!` allocation test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split the documentation build out of `CI.yml` into its own `Documenter.yml`, so
that a docs failure and a test failure are separate signals and the docs job is
not queued behind the test matrix. The README gains a badge for it.

Bring the actions up to current versions -- `actions/checkout@v7`,
`julia-actions/setup-julia@v3`, `julia-actions/cache@v3`,
`codecov/codecov-action@v7`, and `RegisterAction` pinned so Dependabot can bump
it, which `@latest` prevented. Dependabot now groups action bumps into one pull
request.

The test matrix names Julia versions by alias rather than by number: `min`
resolves the lower bound of the `julia` compat entry, so the matrix tracks the
declared support window instead of having to be edited alongside it; `lts` and
`1` cover the long-term-support and current stable releases; `pre` and
`nightly` run on Linux only under `continue-on-error`, an upcoming-release
failure being information rather than a broken build. `arch` becomes `default`,
which tests Julia natively on the ARM64 macOS runners instead of under Rosetta.

Two workflow bugs fixed on the way:

  - `docs/Project.toml` pinned `CompactBasisFunctions` to an absolute path on a
    developer's machine through a `[sources]` entry. That path does not exist on
    a runner, so the documentation build could only ever have succeeded locally.
    0.3.1 is registered, so the entry goes and the dependency resolves from the
    registry.
  - `CompatHelper.yml` invoked `julia` without installing it. The runner images
    no longer ship a Julia, so the workflow failed before CompatHelper started.

Add `AUTHORS.md` and rename `LICENSE` to `LICENSE.md`, whose copyright line now
names "The SimpleSplines Authors" and points at it -- otherwise AUTHORS.md's
reference to what the LICENSE mentions would not be true.

Add the `.githooks/pre-push` hook the README already told the reader to enable.
`.githooks/` did not exist, so `git config core.hooksPath .githooks` was
silently a no-op. It runs the test suite and refuses the push if it fails, with
`SIMPLESPLINES_SKIP_TESTS=1` as the documented override.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`weighted_matrix` is the one assembly that cannot be memoised, since it depends
on the field, so a time integrator asks for a different one inside every Newton
iteration -- the call pattern under which allocation matters most. It costs
260 kB a call at N = 128, p = 3, and only 5 kB of that is a temporary; the rest
is a freshly built SparseMatrixCSC whose structure is recomputed each time even
though the sparsity pattern of Phi_a diag(f w) Phi_b' does not depend on f.

Recorded rather than fixed: the fix needs a `weighted_matrix!` entry point,
because the present signature has nowhere to write, and the sparse triple
product has to be written out by hand against a cached pattern instead of
delegating to SparseArrays. That widens the API, so it is the caller's call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 10:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

`l2_projection!` took the quadrature's `f ⊙ w` buffer unconditionally, which narrowed the
element types the method accepts: a complex sample threw an `InexactError` where the
allocating `l2_projection` -- the very method the docstring offers as the interchangeable
alternative -- returns a `ComplexF64` result, and a `BigFloat` sample was silently computed
through `Float64`. The buffer is now taken only when the product lands in its element type,
which is a test on types alone and so resolved at compile time: the ordinary path still
allocates nothing (0 B at N = 128 on a uniform mesh), and a wider sample gets a product of
its own. Verified equal to `l2_projection` to the last bit on a graded mesh.

The in-place and allocating methods are separate implementations, and only the uniform mesh
compared them, so the suite now checks them against each other across all three mesh
families and degrees 1 to 4, and pins the complex case.

The non-reentrancy warning moves from `l2_projection!` to `SplineQuadrature`, where it
belongs: the shared buffer is the second piece of mutable state on that struct, not the
first, the memoising `cache` behind `mixed_matrix` being equally unsynchronised.

  - `CompatHelper.yml` gained `julia-actions/cache` without the `actions: write` permission
    the action needs to prune the caches it created, which `CI.yml` grants with a comment
    saying so; the daily run would have logged a permission failure.
  - `Documenter.yml` instantiated the docs environment in a step of its own, which
    `julia-actions/julia-docdeploy` then did again -- it runs `Pkg.develop` and
    `Pkg.instantiate` on `docs/` itself. The step goes, and with it a comment describing
    work it was not doing.
  - `LICENSE.md` had lost its copyright year in the rename.
  - `AUTHORS.md` introduced contributors as being "in addition" to the principal developers
    and then listed a principal developer among them.
  - `.githooks/pre-push` never read the ref lines git feeds it, so it left git writing into
    a closed pipe and ran the whole suite for a push that only deletes a branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Review

Verified rather than assumed, where a claim could be checked: every action tag this PR
references resolves (actions/checkout@v7.0.1, julia-actions/setup-julia@v3.0.2,
julia-actions/cache@v3.3.0, codecov/codecov-action@v7.0.0,
julia-actions/RegisterAction@v0.3.2), the include: rows do create new matrix combinations
rather than overwriting version, both Manifest.toml and docs/Manifest.toml are
gitignored so removing the [sources] entry is sufficient, basis_integrals returning by
reference is safe because nothing in src/ or test/ mutates it, mass_solve! really is
alias-safe in both representations, and no branch protection depended on the old
Julia 1.10 - … - x64 job names. All ten CI jobs plus the docs build pass on the head
commit. The two workflow bugs it fixes are real ones.

Eight things came out of it, one of them a behavioural regression. All are fixed in
4527bd1 on this branch; the findings are recorded here so the reasoning is on the PR and not
only in a commit message.

1. l2_projection! no longer accepts what l2_projection accepts (major)

q.scratch .= q.w .* f writes into a Vector{T} owned by the quadrature, so f must now
be convertible to the quadrature's element type. Measured on GradedMesh(16, 2π), p = 3
— non-uniform, so FactorizedMass, on which the complex path is otherwise supported:

call on 80866d0
q.mass \ (Φ * (q.w .* fc)), fc::Vector{ComplexF64} — the pre-PR expression ok, ComplexF64
l2_projection(q, fc) — allocating, untouched by the PR ok, ComplexF64
l2_projection!(ûc, q, fc) InexactError
l2_projection!(ûb, q, fb), fb::Vector{BigFloat} ok, but computed through Float64

So the two methods disagree about their domain, and the docstring points at the allocating
one as the interchangeable alternative. The BigFloat row is the worse half of it: the
narrowing is silent.

Fixed by taking the buffer only when the product lands in its element type:

S  = promote_type(eltype(q.w), eltype(f))
fw = S === eltype(q.scratch) ? q.scratch : similar(f, S)

A test on types alone, hence resolved when the method is compiled. Re-measured after the
fix: 0 bytes for Float64 at N = 128 on a uniform mesh (unchanged), 1296 bytes on a
graded one (the CHOLMOD temporary, as documented), and the complex result now equals
l2_projection's exactly — max difference 0.0.

Out of scope, worth knowing: a complex f on a UniformMesh still fails inside
mass_solve!, whose rfft plan cannot take a complex argument. That predates this PR and
is in mass.jl.

2. The non-reentrancy warning is on the wrong docstring (minor)

l2_projection!'s docstring says the shared buffer is "what makes this method
non-reentrant". But SplineQuadrature was already non-reentrant before the buffer existed:
mixed_matrix memoises through get!(q.cache, …) on a plain Dict, which concurrent
callers can corrupt. Warning about one and not the other implies the rest of the struct is
shareable. The warning now sits on SplineQuadrature and names both.

3. l2_projection! had no coverage off the uniform mesh (minor)

The two methods are separate implementations now, and the testset only compared them on
UniformMesh(32, 2π) — nothing would have caught the FactorizedMass path drifting. They
are now compared across all three mesh families and degrees 1 to 4, and the complex case
from finding 1 is pinned.

4. CompatHelper.yml adds julia-actions/cache without actions: write (minor)

The cache action needs that permission to prune the caches it created — CI.yml grants it
with a comment saying exactly this, and the action's README documents it. The new top-level
permissions: block granted only contents: write and pull-requests: write, so every
nightly run would have logged a permission failure on the way out. Permission added; the
cache itself is worth keeping, since it holds CompatHelper.jl and its dependencies.

5. Documenter.yml's instantiate step is redundant, and its comment describes work it is not doing (minor)

julia-actions/julia-docdeploy@v1 runs Pkg.develop(PackageSpec(path=pwd())) and
Pkg.instantiate() against docs/ itself — from its action.yml, not from memory. The
explicit step duplicated the resolve, and the comment ("so instantiating the docs
environment is all that is needed to resolve it against the checkout") credited the resolve
to the step that was not performing it. Dropped, which leaves exactly the standard template.

6. LICENSE.md lost its copyright year in the rename (minor)

Copyright (c) The SimpleSplines Authors — the file it replaced said 2025, and Trixi.jl,
whose AUTHORS.md wording this follows, uses Copyright (c) 2020-present The Trixi.jl Authors. Now 2025-present.

7. AUTHORS.md contradicts itself (nit)

The preamble says "In addition, there are contributors", and the Contributors section
then lists the sole principal developer. Reworded so the list reads as including them.

8. .githooks/pre-push never reads its stdin (nit)

git push feeds the hook one <local ref> <local oid> <remote ref> <remote oid> line per
ref; exiting without draining leaves git writing into a closed pipe. Reading them also gives
the hook something it did not have: an all-zero local oid means a deletion, and a push that
only deletes branches has nothing to test. It now drains stdin, skips the suite for a
deletion-only push, and still runs it when stdin is empty (the hook invoked by hand). All
four paths exercised. The worktree comment above git rev-parse --show-toplevel is reworded
too — it was arguing against something it then did.

Not changed

  • min and lts overlap in the 1.10 series while the compat floor is 1.10. Harmless, and
    it resolves itself when the floor moves — min pins the declared floor exactly, lts
    tracks the head of the LTS series, so the rows are not the same test even today.
  • The ## Open Issues section in the changelog stays as it is, per the author's call. It is
    not a Keep-a-Changelog section and the byte counts in it will age, but the alternative
    (the issue tracker) loses the reasoning that goes with them.

Verification of the fixes: full suite passes (5,130 tests, up 14), the docs build and
doctests are clean with no cross-reference warnings from the new @refs, both @allocated
tests still hold, and the hook's four paths were run by hand.

@michakraus
michakraus merged commit 4527bd1 into main Aug 21, 2026
10 checks passed
@michakraus
michakraus deleted the modernise-ci-and-tidy-repo branch August 21, 2026 12:49
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.

2 participants