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
57 changes: 57 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/sh
#
# Run the test suite before a push, and refuse the push if it fails.
#
# Enable with
#
# git config core.hooksPath .githooks
#
# and skip a single push with `SIMPLESPLINES_SKIP_TESTS=1 git push` — for a
# documentation-only change, say, or when the failure is already known and being
# pushed to a branch on purpose.

set -eu

# git feeds the hook one `<local ref> <local oid> <remote ref> <remote oid>` line per ref
# being pushed, and an all-zero local oid means that ref is being deleted. Read them:
# a push that only deletes has nothing to test, and a hook that exits without draining
# leaves git writing into a closed pipe.
refs=0
content=0
while read -r _local_ref local_oid _remote_ref _remote_oid; do
refs=$((refs + 1))
case "$local_oid" in
*[!0]*) content=1 ;;
esac
done

if [ "$refs" -gt 0 ] && [ "$content" -eq 0 ]; then
printf 'pre-push: nothing but ref deletions, skipping the test suite.\n' >&2
exit 0
fi

if [ "${SIMPLESPLINES_SKIP_TESTS:-0}" != "0" ]; then
printf 'pre-push: SIMPLESPLINES_SKIP_TESTS is set, skipping the test suite.\n' >&2
exit 0
fi

if ! command -v julia >/dev/null 2>&1; then
printf 'pre-push: julia is not on PATH, cannot run the test suite; refusing the push.\n' >&2
printf ' Set SIMPLESPLINES_SKIP_TESTS=1 to push anyway.\n' >&2
exit 1
fi

# git runs its hooks from the top level of the work tree, but asking rather than assuming
# also gives the right answer in a linked worktree.
root=$(git rev-parse --show-toplevel)

printf 'pre-push: running the SimpleSplines test suite...\n' >&2

if julia --project="$root" -e 'using Pkg; Pkg.test()'; then
printf 'pre-push: tests passed.\n' >&2
exit 0
fi

printf 'pre-push: tests FAILED; refusing the push.\n' >&2
printf ' Set SIMPLESPLINES_SKIP_TESTS=1 to push anyway.\n' >&2
exit 1
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
# One pull request for all action bumps rather than one per action.
groups:
github-actions:
patterns:
- "*"
68 changes: 29 additions & 39 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,65 +16,55 @@ jobs:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
# A pre-release or nightly failure is information, not a broken build.
continue-on-error: ${{ matrix.experimental }}
permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created
actions: write
contents: read
strategy:
fail-fast: false
matrix:
# 'min' resolves the lower bound of the julia compat entry in Project.toml, so the
# matrix follows the declared support window instead of having to be edited alongside
# it; '1' is the current stable release.
version:
- '1.10'
- '1.12'
- 'nightly'
- 'min'
- '1'
os:
- ubuntu-latest
- macOS-latest
- windows-latest
arch:
- x64
# 'default' is the runner's native architecture, which is aarch64 on macOS-latest;
# a hardcoded 'x64' would test Julia under Rosetta there.
- default
experimental:
- false
include:
- version: 'lts'
os: ubuntu-latest
arch: default
experimental: false
- version: 'pre'
os: ubuntu-latest
arch: default
experimental: true
- version: 'nightly'
os: ubuntu-latest
arch: default
experimental: true
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v2
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v7
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
docs:
name: Documentation
runs-on: ubuntu-latest
permissions:
actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created
contents: write
statuses: write
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- uses: julia-actions/cache@v2
- name: Configure doc environment
shell: julia --project=docs --color=yes {0}
run: |
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-docdeploy@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
- name: Run doctests
shell: julia --project=docs --color=yes {0}
run: |
using Documenter: DocMeta, doctest
using SimpleSplines
DocMeta.setdocmeta!(SimpleSplines, :DocTestSetup, :(using SimpleSplines); recursive=true)
doctest(SimpleSplines)
11 changes: 11 additions & 0 deletions .github/workflows/CompatHelper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ on:
schedule:
- cron: 0 0 * * *
workflow_dispatch:
permissions:
actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created
contents: write
pull-requests: write
jobs:
CompatHelper:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# The runner images no longer ship a Julia, so one has to be installed rather than
# assumed; without this the `julia -e` steps below fail before CompatHelper starts.
- uses: julia-actions/setup-julia@v3
with:
version: '1'
- uses: julia-actions/cache@v3
- name: Pkg.add("CompatHelper")
run: julia -e 'using Pkg; Pkg.add("CompatHelper")'
- name: CompatHelper.main()
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/Documenter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Documentation
on:
push:
branches:
- main
tags: ['*']
pull_request:
workflow_dispatch:
concurrency:
# Skip intermediate builds: always.
# Cancel intermediate builds: only if it is a pull request build.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
build:
name: Documentation
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created
contents: write
pull-requests: read
statuses: write
steps:
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
with:
version: '1'
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
# julia-docdeploy resolves the docs environment against the checkout and instantiates
# it itself, so there is no separate step for that here.
- uses: julia-actions/julia-docdeploy@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
- name: Run doctests
shell: julia --project=docs --color=yes {0}
run: |
using Documenter: DocMeta, doctest
using SimpleSplines
DocMeta.setdocmeta!(SimpleSplines, :DocTestSetup, :(using SimpleSplines); recursive=true)
doctest(SimpleSplines)
5 changes: 3 additions & 2 deletions .github/workflows/Register.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ on:
jobs:
register:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
contents: write
steps:
- uses: julia-actions/RegisterAction@latest
- uses: julia-actions/RegisterAction@v0.3.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/TagBot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
TagBot:
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: JuliaRegistries/TagBot@v1
with:
Expand Down
19 changes: 19 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Authors

SimpleSplines' development is coordinated by a group of *principal developers*, who are also
its main contributors and who can be contacted in case of questions about SimpleSplines. In
addition, there are *contributors* who have provided substantial additions or modifications.
Together, these two groups form "The SimpleSplines Authors" as mentioned in the
[LICENSE](LICENSE.md) file.

## Principal Developers

* [Michael Kraus](https://www.michael-kraus.org/),
Max Planck Institute for Plasma Physics, Garching, Germany

## Contributors

Everyone who has contributed to SimpleSplines, the principal developers above included, in
alphabetical order:

* Michael Kraus
82 changes: 81 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,97 @@ was the difference between 6.0 ms and 0.4 ms for assembling one Hessian.

The constant assemblies — `mass_matrix`, `stiffness_matrix`, `derivative_matrix` and any
`mixed_matrix` — are memoised on first use. They do not depend on the field, but a downstream
time integrator asks for them inside every Newton iteration of every step.
time integrator asks for them inside every Newton iteration of every step. `basis_integrals`
is one of these constants too, and is now assembled with the quadrature and returned by
reference rather than recomputed per call.

The paths a time integrator actually runs per step allocate nothing. Evaluation is
allocation-free at any degree and derivative order on any mesh; a `CirculantMass` solve is
allocation-free; and `l2_projection!` is now allocation-free end to end on a uniform mesh,
the `f ⊙ w` product going into a buffer held by the quadrature and the load vector being
formed with `mul!` straight into the output. The buffer is taken only when the product lands
in the quadrature's element type, so a wider sample — a complex `f` — is still projected,
through a product of its own, rather than narrowed into it. On a non-uniform mesh a CHOLMOD
temporary remains, CHOLMOD having no in-place `ldiv!`.

That buffer is mutable state on a struct that reads as immutable, as the memoising `cache`
behind `mixed_matrix` already was, so the `SplineQuadrature` docstring now warns that one
quadrature must not be shared between threads.

### Dependencies

`BSplineKit` was dropped: the basis is implemented here, and its variable-coefficient
assemblies do not map onto that package's Galerkin interface. `FFTW` and `SparseArrays` were
added for the above, and `CompactBasisFunctions` for the shared `Basis` hierarchy.

### Repository and CI

The documentation build moved out of `CI.yml` into its own `Documenter.yml` workflow, 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 gained a badge for it.

The workflows were brought up to current action versions — `actions/checkout@v7`,
`julia-actions/setup-julia@v3`, `julia-actions/cache@v3`, `codecov/codecov-action@v7` — and
the test matrix now 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 and are `continue-on-error`, since an upcoming-release
failure is information rather than a broken build.

`arch` is now `default` rather than `x64`, which is what tests Julia natively on the ARM64
macOS runners instead of under Rosetta.

`AUTHORS.md` was added and `LICENSE` renamed to `LICENSE.md`, whose copyright line now names
"The SimpleSplines Authors" and points at it.

The `pre-push` hook the README asks the reader to enable now exists in `.githooks/`. It runs
the test suite and refuses the push if it fails; `SIMPLESPLINES_SKIP_TESTS=1` overrides it.

### Fixed

- `QuadratureRules` compat was `"0.1"`, which could not co-resolve with
`CompactBasisFunctions`; it is now `"0.2"`.
- `LinearAlgebra` compat was `"1.12.0"` alongside `julia = "1.10"`, which contradicted the
1.10 row of the CI matrix; it is now `"1"`.
- `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 CI runner, so the
documentation build could only ever have succeeded locally; the entry is removed and the
dependency now 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; it now sets Julia up first.

## Open Issues

### `weighted_matrix` allocates a fresh matrix per call

`weighted_matrix` is the one assembly that cannot be memoised — it depends on the field, so a
downstream time integrator asks for a *different* one inside every Newton iteration of every
step, which is exactly the call pattern under which allocation matters most. Measured at
`N = 128`, `p = 3`, `nq = 5`:

| | bytes |
|:--|--:|
| `f .* q.w` temporary | 5 kB |
| `Φₐ * Diagonal(f ⊙ w)` | 47 kB |
| `(Φₐ D) * Φᵦᵀ` sparse-sparse product | 208 kB |
| **total per call** | **260 kB** |

The `f ⊙ w` temporary is the same one `l2_projection!` no longer pays and could be removed the
same way, with the buffer the quadrature already holds. The other 255 kB are not a temporary
at all: they are the result, a freshly built `SparseMatrixCSC` with its `colptr`, `rowval` and
`nzval` allocated and its structure recomputed from scratch.

That structure does not depend on `f`. For a fixed `(a, b)` the sparsity pattern of
`Φₐ diag(f ⊙ w) Φᵦᵀ` is the same for every coefficient — a basis function overlaps only the
`2p+1` others whose supports meet its own — so the pattern could be assembled once per
`(a, b)`, cached beside the `mixed_matrix` results, and only `nzval` refilled per
call. That turns 260 kB into zero.

What it needs is an in-place entry point, `weighted_matrix!(A, q, f, a, b)`, since the present
signature has nowhere to write. Callers holding a matrix across steps would use it and callers
wanting a value would keep the allocating form. Deferred rather than done because it widens
the API, and because the sparse triple product would have to be written out by hand against
the cached pattern instead of delegating to `SparseArrays`, which is the part that needs to be
got right rather than merely written.
2 changes: 1 addition & 1 deletion LICENSE → LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Michael Kraus
Copyright (c) 2025-present The SimpleSplines Authors (see [AUTHORS.md](AUTHORS.md))

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaDEC.github.io/SimpleSplines.jl/stable/)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaDEC.github.io/SimpleSplines.jl/dev/)
[![Build Status](https://github.com/JuliaDEC/SimpleSplines.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaDEC/SimpleSplines.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Documentation](https://github.com/JuliaDEC/SimpleSplines.jl/actions/workflows/Documenter.yml/badge.svg?branch=main)](https://github.com/JuliaDEC/SimpleSplines.jl/actions/workflows/Documenter.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/JuliaDEC/SimpleSplines.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaDEC/SimpleSplines.jl)
[![PkgEval](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/S/SimpleSplines.svg)](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/S/SimpleSplines.html)

Expand All @@ -19,3 +20,15 @@ To run the test suite before every push, enable the repository's git hooks:
```sh
git config core.hooksPath .githooks
```

`.githooks/pre-push` refuses the push if the suite fails. To push regardless — a
documentation-only change, or a failure that is already known — set

```sh
SIMPLESPLINES_SKIP_TESTS=1 git push
```

## License

SimpleSplines is licensed under the [MIT License](LICENSE.md). See [AUTHORS.md](AUTHORS.md)
for the list of authors the copyright refers to.
Loading