From e00cba34993ec68fd487d46527442456776364f2 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sat, 23 May 2026 23:07:29 -0700 Subject: [PATCH 01/14] add settings and skills --- .claude/settings.json | 69 ++ .claude/skills/base-show/SKILL.md | 557 ++++++++++++ .claude/skills/docstrings/SKILL.md | 543 +++++++++++ .claude/skills/error-message-manager/SKILL.md | 858 ++++++++++++++++++ 4 files changed, 2027 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .claude/skills/base-show/SKILL.md create mode 100644 .claude/skills/docstrings/SKILL.md create mode 100644 .claude/skills/error-message-manager/SKILL.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..4990e57e8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(julia --project *)", + "Read(*)", + "FileWrite(src/*, test/*, docs/*, examples/*, ai/*)", + "Edit(src/*)", + "Write(src/*)", + "Bash(git log*)", + "Bash(git diff*)", + "Bash(git status*)", + "Bash(git show*)", + "Bash(git blame*)", + "Bash(git ls-files*)", + "Bash(git rev-parse*)", + "Bash(git describe*)", + "Bash(git shortlog*)", + "Bash(git tag -l*)", + "Bash(git tag --list*)", + "Bash(git stash list*)", + "Bash(git stash show*)", + "Bash(git remote -v*)", + "Bash(git remote show*)", + "Bash(git branch -l*)", + "Bash(git branch --list*)", + "Bash(git branch -a*)", + "Bash(git branch -r*)" + ], + "deny": [ + "Read(./.env)", + "Bash(git commit*)", + "Bash(git add*)", + "Bash(git push*)", + "Bash(git pull*)", + "Bash(git merge*)", + "Bash(git rebase*)", + "Bash(git checkout*)", + "Bash(git switch*)", + "Bash(git restore*)", + "Bash(git reset*)", + "Bash(git cherry-pick*)", + "Bash(git clean*)", + "Bash(git rm*)" + ] + }, + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "printf '\\n---\\nSKILL ROUTING: Review the available skills listed in the system context. If one clearly matches this task, invoke it via the Skill tool before proceeding. If no skill clearly matches, use AskUserQuestion to ask the user: (a) whether they would like to apply any of the listed skills to this task anyway, (b) whether they would like to create a new skill using the skill-creator skill — in which case propose a suggested skill name and one-line description tailored to this task — or (c) proceed without a skill. Also use AskUserQuestion to ask the user whether they would like to use subagents (e.g., Explore, Plan, general-purpose) for this task, and if so, how (e.g., for research, parallel work, or isolated execution). Do NOT proceed with the task until the user has answered.\\n'" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "julia --project=.dev .dev/climaformat.jl . 2>/dev/null || echo 'Formatting skipped as error encountered when calling `julia --project=.dev .dev/climaformat.jl`'" + } + ] + } + ] + } +} diff --git a/.claude/skills/base-show/SKILL.md b/.claude/skills/base-show/SKILL.md new file mode 100644 index 000000000..4b0e5e045 --- /dev/null +++ b/.claude/skills/base-show/SKILL.md @@ -0,0 +1,557 @@ +--- +name: base-show +description: > + Add concise Base.show and Base.summary methods to Julia types whose default REPL + representation is unhelpful or overwhelming. Use this skill whenever the user + mentions that a type prints badly in the REPL, asks to improve how an object is + displayed or printed, wants a custom show, summary, or repr for a Julia type, or + says the REPL output is noisy, verbose, or hard to read. Also trigger when the user + asks to "make the REPL output nicer", "add a show method", "add a summary method", + "customize display", or "fix what prints when I type a variable name". This skill + produces compact, informative Base.show and Base.summary methods and matching unit + tests — invoke it proactively whenever show, summary, display, print, repr, + or REPL output is mentioned in a Julia context. +--- + +# base-show + +Add concise `Base.show(io::IO, ::MIME"text/plain", x::T)` and `Base.summary(io::IO, +x::T)` methods to Julia types whose default REPL representation is unhelpful or +overwhelming. Julia's default show dumps every field recursively; types that hold +DataFrames, large dictionaries, nested arrays, or many scalar fields produce screens +of unreadable text at the REPL. + +`Base.show(io, MIME"text/plain", x)` must also handle the `:compact` IOContext key. +When Julia renders an object as an element inside a container (e.g. printing a +`Vector{MyType}`), it sets `:compact => true` on `io`. Without a compact branch the +full multi-line output is repeated for every element, producing an unreadable wall of +text. The compact branch must produce exactly one line (no newlines), giving the same +kind of at-a-glance hint as `Base.summary`. + +This skill produces both methods and accompanying unit tests so that interactive use +of the package is pleasant without losing key summary information. + +## Workflow + +### Step 0 — Audit existing show methods (retrofit mode) + +Skip this step if you are adding show methods to types that have none. Apply it when +the user asks to retrofit existing show methods — e.g. to add the compact branch to +methods that were written before this protocol existed. + +**Find MIME methods that lack the compact branch:** + +``` +grep -n 'MIME"text/plain"' src/show.jl +``` + +For each match, check whether the function body contains `get(io, :compact`. Any that +do not are candidates for retrofit. + +**Detect the old forwarding anti-pattern (infinite-recursion risk):** + +``` +grep -nA2 'function Base\.show(io::IO, x::' src/ | grep 'show(io, MIME' +``` + +If this matches, a 2-arg `show(io, x)` is calling the MIME method — the *wrong* +direction. Once the MIME method gains a compact branch that calls `show(io, x)`, you +get infinite recursion. Flag every match and reverse the direction: the 2-arg method +becomes the compact one-liner, and the MIME method calls it via `show(io, x)` in its +compact branch. + +**Identify pre-existing bespoke 2-arg shows:** + +A bespoke 2-arg show is one that already exists but does not follow summary style — +for example, it may omit the type name entirely or use a different format. Check each +existing `Base.show(io::IO, x::T)` against its paired `Base.summary`. If the outputs +differ substantially, the 2-arg show is bespoke and needs a custom compact test (see +Step 4). + +### Step 1 — Enumerate concrete types + +List every concrete (non-abstract) struct defined in the package source: + +``` +grep -nrE '^(mutable )?struct ' src/ +``` + +Exclude `abstract type` declarations — they cannot be instantiated and do not need +show methods. + +### Step 2 — Classify show noisiness + +For each concrete type, decide whether its default show output would be noisy. A type +is noisy if it holds at least one of: + +- A `DataFrame` or similar tabular collection +- A `Dict` with potentially many entries +- A large or variable-length `Array` +- Another struct that is itself noisy +- More than approximately six fields in total + +Also run: + +``` +grep -nrE 'Base\.(show|summary)' src/ +``` + +Skip any type that already has a custom `Base.show` or `Base.summary` method — do not +overwrite existing customization. + +### Step 3 — Write show and summary methods + +For each noisy type without existing methods, write **both** a `Base.show` and a +`Base.summary` method. + +**`Base.show`** — always write two overloads together: + +```julia +# 3-arg MIME method: full REPL display, with compact fallback +function Base.show(io::IO, ::MIME"text/plain", x::T) + if get(io, :compact, false) + show(io, x) # delegate to the 2-arg compact method + else + println(io, "T") + println(io, " field_name : ", summary_value) + # ... + end +end + +# 2-arg method: single-line compact representation (no newline) +function Base.show(io::IO, x::T) + print(io, "T (key_hint)") +end +``` + +The 3-arg (MIME) non-compact branch must: + +- Print the type name (and any cheap size hints) on the first line. +- Follow with 1–5 concise summary lines: counts, sizes, or ranges of important fields. + Never print collection contents. +- Produce at most 10 lines of output for any valid instance, including edge cases such + as empty collections or zero-element structs. + +The 2-arg method (compact representation) must: + +- Produce exactly one line with no trailing newline. +- Match `Base.summary` style: type name followed by the most essential identifying hint + in parentheses — e.g. `"Emulator (GaussianProcess, 5→2)"` or `"MCMCWrapper (3 params, RWMHSampling)"`. +- Remain O(1): no loops, no collection materialisation. + +Julia calls the 2-arg method when rendering elements inside containers (arrays, dicts, +etc.), passing `io` with `:compact => true`. The MIME method's compact branch delegates +to it so both paths produce the same single-line output. + +**`Base.summary`** — single-line description used when the object appears inside a +container or is printed in a broader context (e.g., as an element of a `Vector`): + +```julia +function Base.summary(io::IO, x::T) + print(io, "T (key_hint)") +end +``` + +The method must: + +- Fit on one line — no newlines. +- Convey the most important size or identity hint (e.g., number of elements, key + dimension), so the reader immediately knows what they are looking at. +- Remain cheap: O(1) field accesses only. + +Good examples of what to put in the hint: `"GaussianProcess, 5→2"`, `"3 params, RWMHSampling"`, +`"ZScoreScaling"`. Avoid repeating the type name verbatim as the only content — add value. + +**Placement**: place both methods adjacent to their type definition in the same source +file, or gather all show/summary methods in a dedicated `src/show.jl` included from +the main module file. Follow whatever convention is already present in the package; +default to `src/show.jl` if no prior convention exists. + +If creating `src/show.jl`, add `include("show.jl")` to the main module file after the +type definitions it references. + +### Step 4 — Write unit tests + +Write one test block per type, covering `show` (full and compact), and `summary`. Each +test block must: + +- Construct a minimal valid instance of the type. +- For full show: capture output with `sprint(show, MIME("text/plain"), instance)` and + assert that it contains the type name and that line count does not exceed 10. +- For compact show: capture `out2 = sprint(show, instance)` (2-arg) and assert it + contains the type name and has no `'\n'`. Also capture + `out3 = sprint(show, MIME("text/plain"), instance; context=:compact => true)` and + assert `out2 == out3` — both compact paths must agree. +- For `summary`: capture output with `sprint(summary, instance)` and assert that it + contains the type name and produces exactly one line (no `'\n'` in output). + +**Bespoke 2-arg shows (retrofit case):** Some types may already have a 2-arg show +that intentionally does not include the type name or follow summary style — the method +is doing something custom. Using a shared `check_compact(x, typename)` helper will +fail the typename assertion for these. Instead, write a hand-rolled compact test: + +```julia +s2 = sprint(show, instance) +@test !occursin('\n', s2) # no newline +@test s2 == sprint(show, MIME("text/plain"), instance; context = :compact => true) # paths agree +``` + +Avoid asserting exact strings so that cosmetic changes to the output do not break tests. + +### Step 5 — Verify + +Run the package test suite: + +``` +julia --project -e 'using Pkg; Pkg.test()' +``` + +Confirm that all new tests pass and no pre-existing tests regress. + +### Step 6 — Offer to improve the skill + +After the tests pass and the REPL output looks good, ask the user: "Would you like to improve the **base-show** skill itself using skill-creator? You can suggest changes to the workflow or quality criteria, or I can analyse what came up during this session to identify improvements to the skill." + +## Common patterns + +### Two-overload pattern (always write both together) + +Always define the 2-arg and 3-arg MIME overloads as a pair. The MIME method's compact +branch calls the 2-arg method, so both display paths (REPL and in-container) converge +on the same one-liner without repetition: + +```julia +function Base.show(io::IO, ::MIME"text/plain", x::MyProcess) + if get(io, :compact, false) + show(io, x) + else + println(io, "MyProcess") + # ... full multi-line body ... + end +end + +function Base.show(io::IO, x::MyProcess) + print(io, "MyProcess (", nameof(typeof(x.backend)), ", dim=", x.dim, ")") +end +``` + +Without the 2-arg method, `[em]` in a `Vector` falls back to Julia's default field +dump. Without the compact branch in the MIME method, the same dump appears whenever +the object is embedded in a container that happens to call `show(io, MIME"text/plain", +x)` with `:compact => true`. + +### Truncate long collections with "… and N more" + +When a type holds a variable-length collection, cap the loop to keep output bounded: + +```julia +function Base.show(io::IO, ::MIME"text/plain", x::Emulator) + if get(io, :compact, false); show(io, x); return; end + enc = get_encoder_schedule(x) + n = length(enc) + max_show = 6 + println(io, "Emulator (", length(enc), " encoder", n == 1 ? "" : "s", ")") + for i in 1:min(n, max_show) + println(io, " [", i, "] ", sprint(show, enc[i])) + end + n > max_show && println(io, " … and ", n - max_show, " more") +end +``` + +### Conditional fields + +Only print a field when it carries information: + +```julia +if !isnothing(x.prior_mean) + println(io, " prior_dim: ", length(x.prior_mean)) +end +``` + +### Pluralisation in summary + +Match English grammar for counts that can be 0 or 1: + +```julia +print(io, "MCMCWrapper (", n, " param", n == 1 ? "" : "s", ", dim=", dim, ")") +``` + +### Arrow notation for mappings + +Use `→` in summary when the type represents a transformation between spaces: + +```julia +print(io, "PairedDataContainer (", m_in, "×", n_in, " → ", m_out, "×", n_out, ")") +``` + +### Unicode in mathematical contexts + +Use `×` for matrix dimensions, `→` for transformations, `∞` for unbounded constraints, +and `|u|` for set sizes. These are rendered cleanly in all modern Julia terminals and +communicate mathematical meaning concisely. + +```julia +# Constraint summary: Constraint{NoConstraint} (−∞, ∞) +lb = get(bounds, "lower_bound", "-∞") +ub = get(bounds, "upper_bound", "∞") +print(io, "Constraint{$(T)} ($(lb), $(ub))") +``` + +### Use nameof for parametric type identity + +When a type carries a type-parameter that identifies its variant, use `nameof` rather +than printing the full parameterised name: + +```julia +# Sampler{Float64} (prior_dim=12) — not the raw Sampler{Float64, ...} dump +print(io, "Sampler{", nameof(get_sampler_type(x)), "} (prior_dim=", length(x.prior_mean), ")") +``` + +### Section separators in show.jl + +When collecting all methods in a dedicated `show.jl`, organise by type family with +aligned comment rulers: + +```julia +# ── Utilities ───────────────────────────────────────────────────────────────── +# ── Emulators ───────────────────────────────────────────────────────────────── +# ── MachineLearningTools ────────────────────────────────────────────────────── +# ── MarkovChainMonteCarlo ───────────────────────────────────────────────────── +``` + +## Quality criteria + +| Criterion | Priority | Definition | +|---|---|---| +| Coverage | High | Every type classified as noisy in Step 2 has a `Base.show` (both overloads) and a `Base.summary` method. | +| Compact support | High | The 3-arg MIME `show` checks `get(io, :compact, false)` and calls the 2-arg `show(io, x)` in the compact branch. The 2-arg method produces exactly one line with no newline. | +| Brevity — show | High | Full (non-compact) show output is at most 10 lines for any valid instance, including edge cases. | +| Brevity — summary | High | Summary output is exactly one line (no newlines) for any valid instance. | +| Safety | High | Neither method throws on any valid instance. | +| Allocation-safety | High | All data access is O(1): use `length()`, `size()`, `isempty()`, or `first()` on lazy iterators. Never call `collect()`, `sort()`, `filter()`, or any function that materialises a new collection. | +| Test robustness | Medium | Tests assert structural properties, not exact strings. Cosmetic changes do not break tests. | +| No regression | High | Pre-existing tests continue to pass; no unintended changes to other source files. | + +## Formatting rules + +- **MIME show signature**: `Base.show(io::IO, ::MIME"text/plain", x::MyType)` +- **MIME show structure**: always starts with `if get(io, :compact, false); show(io, x); else ... end`. +- **MIME show full branch — first line**: type name via `println(io, "TypeName")`. Cheap size hints may follow on the same line. +- **MIME show full branch — subsequent lines**: indented two spaces for readability. +- **2-arg show signature**: `Base.show(io::IO, x::MyType)` +- **2-arg show content**: one `print` call (no `println`), type name followed by a parenthesised hint matching `Base.summary` style, e.g. `print(io, "MyType (847 basins)")`. +- **summary signature**: `Base.summary(io::IO, x::MyType)` +- **summary content**: one `print` call (no `println`), type name followed by a parenthesised hint, e.g. `print(io, "MyType (847 basins)")`. +- **No collection contents**: print only counts, sizes, or ranges — never iterate and print elements. +- **No allocations**: use `length()`, `size()`, `isempty()`, and `first()` on lazy iterators such as `values(dict)`. Do not call `collect()`, `sort()`, or any function that copies a collection. +- **Tests — MIME full show**: use `sprint(show, MIME("text/plain"), x)` to capture output without side effects. +- **Tests — compact show**: use `sprint(show, MIME("text/plain"), x; context=:compact => true)` to exercise the compact branch, and `sprint(show, x)` to test the 2-arg method directly. +- **Tests — summary**: use `sprint(summary, x)` to capture the one-line description. + +## Examples + +### Example 1 — emulator wrapping an ML tool and training data + +```julia +# Scenario: Emulator wraps a MachineLearningTool and two PairedDataContainers +# (raw and encoded training data). The default show recursively dumps nested matrix +# contents, making it unreadable. + +# Before (default Julia show) +julia> em +Emulator{Float64, Vector{...}}(machine_learning_tool=GaussianProcess{GPJL, Float64, ...}( + models=[...], kernel=SEIso(0.0, 0.0), ...), io_pairs=PairedDataContainer{Float64}(...), ...) + +# After — custom show (two overloads) +function Base.show(io::IO, ::MIME"text/plain", x::Emulator) + if get(io, :compact, false) + show(io, x) + else + mlt = get_machine_learning_tool(x) + n_in, n_train = size(get_io_pairs(x).inputs.data) + n_out = size(get_io_pairs(x).outputs.data, 1) + println(io, "Emulator") + println(io, " machine_learning_tool : ", nameof(typeof(mlt))) + println(io, " input_dim : ", n_in) + println(io, " output_dim : ", n_out) + println(io, " n_train : ", n_train, " samples") + println(io, " encoders : ", length(get_encoder_schedule(x))) + end +end + +function Base.show(io::IO, x::Emulator) + mlt = get_machine_learning_tool(x) + n_in = size(get_io_pairs(x).inputs.data, 1) + n_out = size(get_io_pairs(x).outputs.data, 1) + print(io, "Emulator (", nameof(typeof(mlt)), ", ", n_in, "→", n_out, ")") +end + +# julia> em +# Emulator +# machine_learning_tool : GaussianProcess +# input_dim : 50 +# output_dim : 5 +# n_train : 100 samples +# encoders : 2 + +# julia> [em_gp, em_rf] +# 2-element Vector{Emulator{Float64, ...}}: +# Emulator (GaussianProcess, 50→5) +# Emulator (RandomFeature, 50→5) + +# After — custom summary (arrow notation for emulator mapping; matches 2-arg show) +function Base.summary(io::IO, x::Emulator) + mlt = get_machine_learning_tool(x) + n_in = size(get_io_pairs(x).inputs.data, 1) + n_out = size(get_io_pairs(x).outputs.data, 1) + print(io, "Emulator (", nameof(typeof(mlt)), ", ", n_in, "→", n_out, ")") +end +``` + +### Example 2 — multi-field configuration object + +```julia +# Scenario: MCMCWrapper holds prior, encoded prior, observations, the emulated +# log-posterior density model, the MCMC sampler, and an encoder pipeline. +# The default show dumps every nested object at full depth. + +# Before (default Julia show) +julia> mcmc +MCMCWrapper{...}(prior=ParameterDistribution(...), encoded_prior=ParameterDistribution(...), + observations=[...], log_posterior_map=AdvancedMH.DensityModel(...), + mh_proposal_sampler=RWMetropolisHastings{...}(...), ...) + +# After — custom show (two overloads) +function Base.show(io::IO, ::MIME"text/plain", x::MCMCWrapper) + if get(io, :compact, false) + show(io, x) + else + n_par = ndims(x.prior) + n_obs = length(x.observations) + sampler = x.mh_proposal_sampler + println(io, "MCMCWrapper") + println(io, " prior_dim : ", n_par, " parameter", n_par == 1 ? "" : "s") + println(io, " n_obs : ", n_obs, " observation sample", n_obs == 1 ? "" : "s") + println(io, " sampler : ", nameof(typeof(sampler))) + println(io, " encoders : ", length(x.encoder_schedule)) + end +end + +function Base.show(io::IO, x::MCMCWrapper) + n_par = ndims(x.prior) + sampler = x.mh_proposal_sampler + print(io, "MCMCWrapper (", n_par, " param", n_par == 1 ? "" : "s", + ", ", nameof(typeof(sampler)), ")") +end + +# julia> mcmc +# MCMCWrapper +# prior_dim : 3 parameters +# n_obs : 10 observation samples +# sampler : RWMetropolisHastings +# encoders : 2 + +# julia> [mcmc_a, mcmc_b] +# 2-element Vector{MCMCWrapper{...}}: +# MCMCWrapper (3 params, RWMetropolisHastings) +# MCMCWrapper (3 params, BarkerMetropolisHastings) + +# After — summary (matches 2-arg show) +function Base.summary(io::IO, x::MCMCWrapper) + n_par = ndims(x.prior) + sampler = x.mh_proposal_sampler + print(io, "MCMCWrapper (", n_par, " param", n_par == 1 ? "" : "s", + ", ", nameof(typeof(sampler)), ")") +end +``` + +### Example 3 — parametric type with backend dispatch + +```julia +# Scenario: GaussianProcess{GPPackage, FT, VV} holds fitted GP models and +# hyperparameters for each output dimension. The `GPPackage` type parameter +# identifies which GP backend was used (GPJL, SKLJL, or AGPJL). The default +# show recurses into kernel objects and model arrays. + +# Before (default Julia show) +julia> gp +GaussianProcess{GPJL, Float64, Vector{Float64}}( + models=[GaussianProcesses.GPE{...}(...)], + kernel=SEIso(0.0, 0.0), noise_learn=true, alg_reg_noise=0.001, + prediction_type=YType(), regularization=[...]) + +# After — custom show (two overloads) +function Base.show(io::IO, ::MIME"text/plain", x::GaussianProcess{P}) where {P} + if get(io, :compact, false) + show(io, x) + else + n_models = length(x.models) + println(io, "GaussianProcess") + println(io, " package : ", nameof(P)) + println(io, " n_models : ", n_models, + " (one per output dimension)") + println(io, " noise_learn : ", x.noise_learn) + println(io, " pred_type : ", nameof(typeof(x.prediction_type))) + end +end + +function Base.show(io::IO, x::GaussianProcess{P}) where {P} + n = length(x.models) + print(io, "GaussianProcess{", nameof(P), "} (", + n, " model", n == 1 ? "" : "s", ")") +end + +# julia> gp +# GaussianProcess +# package : GPJL +# n_models : 5 (one per output dimension) +# noise_learn : true +# pred_type : YType + +# julia> [gp_jl, gp_skl] +# 2-element Vector{GaussianProcess}: +# GaussianProcess{GPJL} (5 models) +# GaussianProcess{SKLJL} (5 models) + +# After — summary (matches 2-arg show) +function Base.summary(io::IO, x::GaussianProcess{P}) where {P} + n = length(x.models) + print(io, "GaussianProcess{", nameof(P), "} (", + n, " model", n == 1 ? "" : "s", ")") +end +``` + +### Unit tests + +```julia +@testset "Emulator show" begin + using CalibrateEmulateSample.Emulators + gp = GaussianProcess(GPJL()) + em = Emulator(gp, PairedDataContainer(rand(3, 20), rand(2, 20))) + out = sprint(show, MIME("text/plain"), em) + @test occursin("Emulator", out) + @test count(==('\n'), out) <= 10 +end + +@testset "Emulator show compact" begin + using CalibrateEmulateSample.Emulators + gp = GaussianProcess(GPJL()) + em = Emulator(gp, PairedDataContainer(rand(3, 20), rand(2, 20))) + # exercise via the 2-arg method directly + out2 = sprint(show, em) + @test occursin("Emulator", out2) + @test !occursin('\n', out2) + # exercise via the MIME method with compact context + out3 = sprint(show, MIME("text/plain"), em; context=:compact => true) + @test out2 == out3 # both paths must agree +end + +@testset "Emulator summary" begin + using CalibrateEmulateSample.Emulators + gp = GaussianProcess(GPJL()) + em = Emulator(gp, PairedDataContainer(rand(3, 20), rand(2, 20))) + out = sprint(summary, em) + @test occursin("Emulator", out) + @test !occursin('\n', out) # must be exactly one line +end +``` diff --git a/.claude/skills/docstrings/SKILL.md b/.claude/skills/docstrings/SKILL.md new file mode 100644 index 000000000..b26d456c6 --- /dev/null +++ b/.claude/skills/docstrings/SKILL.md @@ -0,0 +1,543 @@ +--- +name: docstrings +description: > + Add or normalise Julia docstrings on public symbols (exported types, functions, + and constants) so the package's public API is fully self-documenting and the + Documenter.jl docs build passes its checkdocs check. After writing docstrings, + also updates docs/src/API/ pages so every exported symbol appears exactly once, + organised into logical categories, with stale entries removed. + Invoke this skill whenever the user mentions: docstring, missing doc, + undocumented symbol, API doc, checkdocs warning, docs/src/API, @docs block, + or asks to document a type or function, sync the API pages, or keep the API + index up to date. Also use it when the user asks to "write docs for" or "add + docs to" source files, or when a CI failure mentions missing or incomplete + docstrings. +--- + +# docstrings + +Add or normalise Julia docstrings on public symbols (exported types, functions, +and constants) across the package source. The goal is complete, consistent API +documentation that renders correctly under Documenter.jl and follows whichever +docstring convention is already established in the package — typically +DocStringExtensions macros such as `$(TYPEDEF)`, `$(TYPEDFIELDS)`, and +`$(TYPEDSIGNATURES)`. Completing this skill makes the package's public API fully +self-documenting and satisfies any `checkdocs` requirement in the docs build. + +## Workflow + +### Step 1 — Detect the existing convention + +Use an Explore subagent to read 2–3 symbols that already have complete docstrings +to calibrate style. This avoids consuming the main context window with large file +reads. Ask the Explore agent to return the verbatim docstring text for each symbol. + +Identify: + +- Whether DocStringExtensions macros are used, and which ones (`$(TYPEDEF)`, + `$(TYPEDFIELDS)`, `$(TYPEDSIGNATURES)`, `$(METHODLIST)`). +- How prose is structured relative to macro-generated content (e.g. does prose + come before or after `$(TYPEDFIELDS)`?). +- What field documentation pattern is preferred: inline string literals above + each struct field vs. a separate prose block. +- Which format is used for struct docstrings: the **old format** (an indented + type-name header on the first line, no `$(TYPEDEF)`, manual `# Constructor` + section), or the **new format** (prose only, `$(TYPEDEF)` for the signature, + `$(METHODLIST)` for constructors). Normalise old-format structs to new-format + during Step 3. + +This detected baseline becomes the style target for every new or normalised +docstring. Do not impose a different convention — match what is already there. + +### Step 2 — Enumerate candidates + +Discover the package name from `Project.toml` (the `name =` field). Then run: + +``` +grep -nE '^(function |struct |abstract type |mutable struct |const )' src/**/*.jl +``` + +**Cross-file exports**: exported names may be declared in a central module file +(e.g. `src/CalibrateEmulateSample.jl`, or the per-module files `src/Emulator.jl`, +`src/MarkovChainMonteCarlo.jl`, `src/Utilities.jl`) while the definition lives in a +different file. Read each module file for all `export` statements so you catch every +public symbol regardless of where it is defined. + +For each exported symbol, check whether a non-empty docstring immediately precedes +the definition. Produce a prioritised list: + +1. **Missing entirely** — no docstring at all. +2. **Old-format struct** — indented type name on first line, no `$(TYPEDEF)`, + or redundant manual `# Constructor` / `# Constructors` section alongside + `$(METHODLIST)`. +3. **Empty or stub** — only a bare macro line (e.g. `$(TYPEDSIGNATURES)`) with + no prose. +4. **Incomplete** — prose present but key sections absent (missing `# Arguments`, + `# Examples`, or field strings not describing semantic role). + +**Also scan every function in the file for old-style docstrings**, regardless of +whether it is exported. An old-style docstring is one that uses an indented +function-name header (e.g. ` my_func(arg1, arg2)`) and/or an `Args:` / +`Arguments:` block with the `` `name` - description `` format. Convert these to +the `$(TYPEDSIGNATURES)` convention in the same editing pass — the whole file +should end up stylistically uniform. + +### Step 3 — Draft docstrings + +For each candidate, write a docstring that matches the detected convention. + +#### Getters and simple accessors + +Simple getters for exported process/struct types (e.g. +`get_prior_mean(p::MyProcess)`) are public API and must be documented if +exported. A one-line `$(TYPEDSIGNATURES)` + short prose sentence suffices; no +`# Arguments` or `# Examples` is needed unless the semantics are non-obvious. + +#### Old-format struct docstrings + +If a struct docstring starts with an indented type name (e.g. ` MyStruct{...}`), +convert it to the new format: + +- Remove the indented type name from the docstring. +- Add `$(TYPEDEF)` immediately after the opening prose sentence. +- Replace any manual `# Constructor` or `# Constructors` section (listing + function signatures) with a `# Constructors` section containing only + `$(METHODLIST)`. If `$(METHODLIST)` is already present alongside the manual + list, remove the manual list. +- Preserve any genuine prose that was in the old `# Constructor` section if it + explains non-obvious behaviour; discard boilerplate signature repetition. + +#### Named constructors (factory functions) + +`$(METHODLIST)` only lists methods whose name matches the struct type. Exported functions +that **build an instance of the struct but carry a different name** — factory functions such +as `forward_map_wrapper` for `ForwardMapWrapper` — are invisible to `$(METHODLIST)` +and must be surfaced manually in the struct docstring. + +When such functions exist, add a prose note inside the `# Constructors` section, +immediately before `$(METHODLIST)`: + +```julia +""" +Wrapper for an explicit forward map `G(θ)` that can be used in place of an +`Emulator` when the forward model is cheap enough to call directly during MCMC. + +$(TYPEDEF) + +# Fields + +$(TYPEDFIELDS) + +# Constructors + +For most cases, prefer the `forward_map_wrapper()` factory function +(see its own docstring for details and a usage example). + +$(METHODLIST) +""" +struct ForwardMapWrapper{FT, VV, PD, NI} + ... +end +``` + +The note should: +- Name the function in backticks. +- Give a one-line hint about when to prefer it over the direct constructor. +- Optionally include a minimal usage snippet if the factory is the primary entry point + and no separate `# Examples` block exists on the factory function itself. + +The factory function still needs its **own full docstring** (`$(TYPEDSIGNATURES)`, +`# Arguments`, `# Examples` if non-trivial). The struct-level note is a pointer, +not a replacement. + +**Detecting named constructors during Step 2:** when enumerating candidates, flag +exported functions whose name differs from any type name but whose body or doc +clearly returns an instance of a specific struct. Common signals: the function name +ends with a domain term (e.g. `constrained_gaussian`, `from_file`), its return +statement calls the struct constructor directly, or the existing codebase already +mentions the relationship somewhere in prose. + +#### Multiple dispatch — one docstring per concept + +When a function has multiple dispatch methods, document **only** the primary +user-facing overload and leave all other overloads undocumented. Competing +docstrings fragment the rendered API docs and create maintenance burden. + +The primary overload is the method whose argument type is the **broadest +user-facing type** — e.g. `Emulator` rather than `GaussianProcess{GPJL}` +or `ScalarRandomFeatureInterface`. + +**Type-parameter specialisations count as overloads.** If the same function name +is defined for `where {FT, P <: MyProcess{FT, TypeA}}` and +`where {FT, P <: MyProcess{FT, TypeB}}`, these are two dispatch methods of the +same concept. Document only one — typically the first defined, or the more +general one — and leave the rest undocumented. + +Some functions serve as internal dispatch hooks — specialised methods called by the +package framework rather than by users directly (e.g. a backend-dispatched internal +update). These must **not** be documented even if they are exported, because +documenting them fragments the API and creates maintenance burden. Identify them by +checking whether the function is mentioned in user-facing tutorials or the +`docs/src/*.md` narrative pages; if it only appears in internal call chains, skip it. + +#### Old-style function docstrings (all functions, not just exported) + +Convert any docstring that uses an indented function-name header or an `Args:` / +`Arguments:` section to the `$(TYPEDSIGNATURES)` style, even for internal +(non-exported) helpers. The canonical old-style markers are: + +- First line indented with spaces: ` my_func(arg, ...)` — replace with `$(TYPEDSIGNATURES)`. +- Argument block labelled `Args:` or `Arguments:` with `` `name` - description `` lines — replace + with a `# Arguments` section using `` - `name`: description `` format. + +Doing this in the same pass keeps the file stylistically uniform and prevents +old-style docstrings from persisting as invisible technical debt. + +#### General rules + +- Use the same macro set as the best-documented symbols already in the package. +- Preserve any inline field string literals already present above struct fields — + do not merge them into the struct-level docstring. +- Prose should answer: what does this symbol represent or do, when would a caller + use it, and what are the physical units of key quantities. +- Do not duplicate content that macros generate automatically (e.g. do not + restate field types when `$(TYPEDFIELDS)` already renders them). +- Physical quantities: always include units in square brackets, e.g. `[m/day]`. +- For functions with more than two arguments, or whose argument semantics are + not obvious from the name alone, add a `# Arguments` section listing each + parameter as `` - `name`: description [unit if applicable] ``. +- For every non-trivial public function where a minimal runnable example can be + written, add a `# Examples` section with a `jldoctest` block so Documenter.jl + can verify the example stays correct as the code evolves. + +### Step 4 — Apply edits + +When editing files that contain non-ASCII characters (e.g. author names with +accented letters like "Garbuno-Iñigo" or "Nüsken"), the file may store +characters in Unicode NFD form while the Edit tool normalises to NFC, causing +match failures. If an Edit call fails with a "not found" error on a string you +can see in the file, use a Python one-liner to apply the replacement with +NFD-normalised strings: + +```bash +python3 - <<'EOF' +import unicodedata, pathlib +p = pathlib.Path("src/MyFile.jl") +text = p.read_text() +old = unicodedata.normalize('NFD', "the old string here") +new = unicodedata.normalize('NFD', "the new string here") +p.write_text(text.replace(old, new, 1)) +EOF +``` + +After a Python edit, re-read the file before making any further Edit calls to +the same file (the Edit tool tracks file state from the last Read). + +### Step 5 — Sync `docs/src/API/` pages + +After all source-file edits are applied, update the Documenter.jl API pages so +that every exported, documented symbol appears exactly once, organised into +logical categories. The goal is that a reader browsing `docs/src/API/` sees a +complete, non-redundant index of the public API — nothing missing, nothing +stale. + +#### 5a — Build the source-to-page map + +Read `docs/make.jl` and extract the `api` array to see which display name maps +to which page path (e.g. `"Inversion" => "API/Inversion.md"`). For each page, +read its `@meta` block to find `CurrentModule = ...`. This tells you which +module's exports the page is responsible for. + +#### 5b — Collect current `@docs` entries per page + +For each API page, extract every symbol entry listed inside ` ```@docs ``` ` +blocks. Some entries carry type-signature qualifiers (e.g. +`predict(emulator::Emulator)`) — track both the raw entry string and +the base name (everything before the first `(`). + +#### 5c — Find missing and stale entries + +**Exported but not defined (phantom exports).** Before anything else, check +that every exported name actually resolves to a definition — a function, type, +or constant — somewhere in the source files of that module. If an exported name +has no definition anywhere, it is a phantom export: remove the `export` +statement (or just that name from a multi-name `export` line) from the source +file. Do not add phantom exports to any API page. + +**Missing from the API.** A symbol is **missing** from a page when it is +exported from that page's `CurrentModule`, its base name does not appear in any +`@docs` block on any API page, and it has a definition in the source. If it +lacks a docstring, go back and write one now (following the conventions from +Steps 1–3) before adding it to the API page — an undocumented entry in a +`@docs` block will cause the docs build to error. Every exported, defined +symbol must end up with a docstring and an API page entry. + +**Stale API entries.** An entry is **stale** when the base name is no longer +exported from the module, or the symbol no longer has a definition in the +source. + +Run all three checks before making edits so you can see the full diff in one +pass. + +#### 5d — Place missing symbols into appropriate sections + +Insert each missing symbol into the section of its API page that best matches +its role. Use the existing section headings on the page as the primary guide — +`## Getter functions`, `## Error metrics`, etc. are already established +categories; add the new symbol to the most thematically fitting one. + +When no existing section fits, create a new `##` heading that names the +functional group (e.g. `## Accelerators`, `## Utility functions`) and open a +fresh ` ```@docs ``` ` block below it. Avoid catch-all sections like +`## Miscellaneous`; if you find yourself reaching for that, split more finely. + +Broad heuristics for categorisation when the page has no existing sections to +guide you: + +- Struct / abstract type → primary types section (first block on page) +- Functions starting with `get_` → `## Getter functions` +- Functions starting with `compute_`, `construct_`, `build_` → a computation + or construction section +- Update or step functions → an operations section +- Error-metric functions → `## Error metrics` +- Scheduler or controller types/functions → their own named section + +For a multiple-dispatch function where only the primary overload is documented +(per Step 3), list only that overload. If the existing page convention uses +type-qualified entries (e.g. `foo(x::MyType)`), follow that convention; +otherwise use the plain name. + +#### 5e — Remove stale entries + +For each stale API entry: + +1. Delete the line from its `@docs` block. If that empties the block, delete + the block. If that empties the section, delete the section heading too. +2. If the symbol is stale because it is no longer defined (phantom export), + also remove the `export` statement from the source file. For multi-name + export lines (e.g. `export foo, bar, baz`), remove only the stale name and + leave the rest intact. + +#### 5f — Ensure no symbol appears on two pages + +Each base name must appear on at most one API page. If you find a duplicate, +keep it on the page whose `CurrentModule` matches the module where the symbol +is defined, and remove it from the other page. + +### Step 6 — Verify + +Find the package name from `Project.toml`, then confirm the package loads +without error: + +``` +julia --project -e 'import Pkg; Pkg.instantiate(); using CalibrateEmulateSample' +``` + +If a docs build is configured (`docs/make.jl` is present), run it and resolve +any `checkdocs` warnings introduced by the new docstrings. + +### Step 7 — Offer to improve the skill + +Once the docs build is clean, ask the user: "Would you like to improve the +**docstrings** skill itself using skill-creator? You can share suggestions, or I +can analyse patterns from this session — recurring edge cases, formatting +decisions, or anything that felt awkward — to refine the skill for next time." + +## Formatting rules + +These rules encode the conventions most Julia packages following DocStringExtensions +expect. Apply them consistently. + +- **Triple-quoted strings** for all docstrings. +- **First line**: concise one-line summary — imperative mood for functions + (`"Return the..."`, `"Compute..."`), noun phrase for types and constants. +- **Second line**: blank. +- **Body**: prose, then any macro invocations. `$(TYPEDSIGNATURES)` must be the + very first line of a function docstring and is the sole source of the method + signature — never write a manual indented signature as well. +- **No trailing whitespace** inside the docstring. +- **No emojis.** +- **Physical units** in square brackets: `[m/day]`, `[kg/m³]`, `[day]`, etc. +- **Field string literals** (the string above each struct field) are distinct + from the struct-level docstring. Preserve both; do not merge them. +- Field string literals must describe the field's *semantic role*, not its type. + Never write a type name inside brackets (e.g. `"[Date]"`, `"[Dict]"`) — + `$(TYPEDFIELDS)` already renders the type. Reserve square-bracket notation + exclusively for physical units. +- Avoid vague labels such as "data object" or "container". Say what the field + represents in domain terms (e.g. "mapping of basin ID to forcing timeseries" + rather than "dictionary of forcing timeseries data objects"). +- **Multiple-dispatch — one docstring per concept**: Document only the primary + user-facing overload (the method taking the top-level composite type). All + other dispatch methods remain undocumented. Do **not** add `$(METHODLIST)` to + function docstrings — `$(TYPEDSIGNATURES)` already surfaces all overloads. + `$(METHODLIST)` belongs only in struct docstrings (inside `# Constructors`). +- **`# Arguments` section**: add after the opening prose for any function with + more than two parameters, or where argument semantics are non-obvious. Format: + `` - `name`: description [unit] ``. +- **`# Examples` section**: add for every non-trivial public function where a + minimal runnable example is feasible. Use `jldoctest` blocks with `julia> ` + prompts and include expected output. +- In every `jldoctest` block, separate each `julia> ` prompt from the next with + a blank line. Documenter.jl rejects blocks where two prompts appear + consecutively without an intervening blank line. If a statement produces no + output, end it with a semicolon and add a blank line before the next prompt. +- If the doctest references any name from the package, the first statement must + be `julia> using CalibrateEmulateSample` (followed by a blank line). Do not assume + the package is already in scope. + +## Quality criteria + +| Criterion | Weight | What to check | +|---|---|---| +| **Completeness** | High | Every exported symbol has a non-empty docstring after the task is applied. | +| **Convention parity** | High | New docstrings use the same macro set and structural pattern as the best-documented symbols already present. Old-format struct docstrings have been normalised. | +| **Informativeness** | Medium | Prose answers "what, when, why". Units present for physical quantities. `# Arguments` section present where needed. `# Examples` jldoctest block present for non-trivial public functions. | +| **No duplication** | Medium | Prose does not duplicate macro-generated content. Field string literals do not restate the field's type. No redundant manual `# Constructor` section alongside `$(METHODLIST)`. | +| **API page coverage** | High | Every exported, documented symbol appears exactly once across `docs/src/API/` pages. No stale entries. Symbols are grouped into descriptive sections. | +| **Correctness** | High | Package loads without error; docs build (if configured) completes without new warnings. | + +## Examples + +### Struct: old format → new format + +```julia +## Before — OLD format: indented type name, no $(TYPEDEF), manual # Constructor section + +""" + GaussianProcess{GPPackage, FT <: AbstractFloat, VV <: AbstractVector} <: MachineLearningTool + +A Gaussian process emulator dispatched by `GPPackage` (e.g., `GPJL`, `SKLJL`, or `AGPJL`). + +# Constructor +GaussianProcess(package::GPJL(); kernel=nothing, noise_learn=true) # GaussianProcesses.jl backend +GaussianProcess(package::SKLJL(); kernel=nothing, noise_learn=true) # scikit-learn backend + +# Fields + +$(TYPEDFIELDS) + +# Constructors + +$(METHODLIST) +""" +struct GaussianProcess{GPPackage, FT <: AbstractFloat, VV <: AbstractVector} <: MachineLearningTool + ... +end + +## After — NEW format: prose only, $(TYPEDEF) for signature, $(METHODLIST) for constructors + +""" +A Gaussian process emulator parameterised by `GPPackage` (e.g., `GPJL`, `SKLJL`, or `AGPJL`), +dispatching to different GP backends via the type parameter. + +$(TYPEDEF) + +# Fields + +$(TYPEDFIELDS) + +# Constructors + +$(METHODLIST) +""" +struct GaussianProcess{GPPackage, FT <: AbstractFloat, VV <: AbstractVector} <: MachineLearningTool + ... +end +``` + +### Function: stub docstring improved + +```julia +## Before — function with an empty stub docstring + +""" +$(TYPEDSIGNATURES) +""" +function advance(x::MyStruct, dt::Float64) + ... +end + +## After — prose, Arguments, and Examples sections added + +""" +$(TYPEDSIGNATURES) + +Advance `x` by one time step of length `dt` [days] and return the updated state. + +# Arguments +- `x`: current state to advance. +- `dt`: time step length [days]. + +# Examples +```jldoctest +julia> using CalibrateEmulateSample + +julia> m = MyStruct(1.0, Date(2000, 1, 1), 10); + +julia> advance(m, 0.5) +... +``` +""" +function advance(x::MyStruct, dt::Float64) + ... +end +``` + +### Multiple-dispatch: type-parameter specialisations + +```julia +## Before — both specialisations documented (anti-pattern) + +""" +$(TYPEDSIGNATURES) + +Predict using the GaussianProcesses.jl backend. +""" +function predict(gp::GP, new_inputs; kwargs...) where {FT, GP <: GaussianProcess{GPJL, FT}} + ... +end + +""" +$(TYPEDSIGNATURES) + +Predict using the scikit-learn backend. +""" +function predict(gp::GP, new_inputs; kwargs...) where {FT, GP <: GaussianProcess{SKLJL, FT}} + ... +end + +## After — only the first overload documented; second left bare + +""" +$(TYPEDSIGNATURES) + +Return the emulator predictions (posterior mean and covariance) for `new_inputs`. +Dispatches to the GP backend specified by the `GPPackage` type parameter. +""" +function predict(gp::GP, new_inputs; kwargs...) where {FT, GP <: GaussianProcess{GPJL, FT}} + ... +end + +function predict(gp::GP, new_inputs; kwargs...) where {FT, GP <: GaussianProcess{SKLJL, FT}} + ... +end +``` + +### Simple getter: minimal docstring + +```julia +## Before — getter with no docstring + +get_machine_learning_tool(emulator::Emulator) = emulator.machine_learning_tool + +## After — one-liner is enough + +""" +$(TYPEDSIGNATURES) + +Return the `MachineLearningTool` (e.g. `GaussianProcess`, `ScalarRandomFeatureInterface`) +stored in `emulator`. +""" +get_machine_learning_tool(emulator::Emulator) = emulator.machine_learning_tool +``` diff --git a/.claude/skills/error-message-manager/SKILL.md b/.claude/skills/error-message-manager/SKILL.md new file mode 100644 index 000000000..8afb6c0bf --- /dev/null +++ b/.claude/skills/error-message-manager/SKILL.md @@ -0,0 +1,858 @@ +--- +name: error-message-manager +description: > + Rewrite vague, delayed, or low-context Julia error messages into structured, + actionable diagnostics. Invoke this skill whenever the user mentions: error + message, improve errors, rewrite @assert, ArgumentError, DimensionMismatch, + DomainError, vague error, error rewrite, Julia exception, diagnostic, throw, + validation, early check, assert to throw, loop context, catch and rethrow, + warn string, or asks to improve how the code fails. Also use it when reviewing + code for user-facing clarity, when a user says errors are confusing or + unhelpful, or when auditing a module for low-context exceptions. Use it + proactively when you see bare @assert, error("..."), throw(ErrorException(...)), + @warn string(...), or catch blocks that do not include the original exception + in their re-throw in Julia code you are reading or editing. +--- + +# error-message-manager + +Rewrite vague, delayed, or low-context Julia error messages into structured, +actionable diagnostics. The goal is errors that tell the user exactly what went +wrong, what was expected, what was received, and—whenever a likely fix exists— +what to do next. Prefer catching mistakes early (at API boundaries) over letting +them propagate into cryptic numerical failures. + +## Workflow + +### Step 0 — Offer an Explore agent for multi-file scope + +If the user's request covers more than one file — a whole directory, a module, or +the entire repo — offer to spawn an Explore agent before doing any file reads +yourself. The agent runs all the reads in parallel without flooding the main +context, and returns a structured inventory you can act on directly. + +**When to offer**: any time the target is a directory path (e.g. `src/`) or a +vague scope like "the whole package" or "all the source files". + +**Offer text** (adapt as needed): +> "This spans multiple files — I'd recommend spawning an Explore agent to survey +> all `throw`/`@assert`/`error` sites in parallel. It keeps the audit fast and +> leaves the main context clean for the actual rewrites. Want me to do that?" + +**Agent prompt to use** (fill in `` and ``): + +``` +Audit `` for error-raising patterns. For every `@assert`, `error(`, or +`throw(` site in every `.jl` file: + +1. Record: file, line number, exception type (or "bare @assert" / "bare error"), + and the full message text (including multiline strings). +2. Classify message quality: + - "good" — has `$(expr)` interpolation showing the actual received value, and + is either short (≤3 lines) or already in a `_throw_` helper function + - "long-inline" — message content is good, but the body exceeds 3 lines and + the throw is written inline (not in a `_throw_` helper) + - "vague" — missing a received value, or no Expected/Got structure + - "missing" — bare `@assert` with no message at all +3. Note whether the site is at an API boundary (user-facing input) or an internal + invariant (would require a package bug to fire). + +Return a markdown table with columns: + File | Line | Exception type | Quality | Notes (one-line note on what's wrong if vague/missing/long-inline) + +Focus only on sites that are "vague", "missing", or "long-inline" — skip "good" ones. +``` + +**How to use the result**: treat the returned table as your working inventory for +Steps 1 and 2. You do not need to re-read the flagged files yourself to classify — +go straight to reading only the lines that need rewrites (Step 3 onwards). + +--- + +### Step 1 — Audit the target scope + +Identify which files or functions to address. If the user named a specific +function, start there. If the request is repo-wide, run: + +``` +rg -n '(@assert[^(]|@assert\(|error\(string\(|throw\(ErrorException)' src/ +``` + +Then collect all message-less `@assert` calls: + +``` +rg -n '@assert' src/ | grep -v '"' +``` + +Also flag `@warn` calls that use string concatenation instead of interpolation: + +``` +rg -n '@warn\s+string\(' src/ +``` + +And flag `catch` blocks that discard the original exception when re-throwing: + +``` +rg -n 'catch\s' src/ +``` + +For each `catch` hit, check whether the subsequent `throw` or `error` call +interpolates the caught variable (e.g. `$e` or `sprint(showerror, e)`). If it +does not, the original exception type and message are silently lost. + +For each hit, record: file, line, the condition being checked, and whether it +guards user-provided input (API boundary) or an internal invariant. + +### Step 2 — Classify each site + +Use this table to choose the right exception type: + +| Condition | Exception | +|---|---| +| Invalid user-provided argument | `ArgumentError` | +| Array/matrix shape mismatch | `DimensionMismatch` | +| Inconsistent argument types across parameters | `ArgumentError` | +| Mathematically invalid value (negative variance, etc.) | `DomainError` | +| Invalid index | `BoundsError` | +| Internal invariant that should never fire | `error(...)` | +| Missing interface implementation | `MethodError` or structured `ArgumentError` | + +Avoid `ErrorException` unless there is no better choice. + +**Type mismatches vs dimension mismatches**: an `@assert isa(x_mean, AbstractVector{FT})` inside an +`if isa(x, AbstractMatrix{FT})` branch is checking that the user supplied *consistent* arguments +(matrix ensemble → vector mean), not that two arrays have matching sizes. Use `ArgumentError`, not +`DimensionMismatch`, for this pattern. + +Distinguish **API boundary** sites (where the user passed something wrong — prefer +typed exceptions with actionable messages) from **internal invariant** sites +(where a bug in the package itself would have to exist — bare `error(...)` with a +clear note is fine there). + +**Loop-body errors**: if the throw is inside a `for` or `while` loop, treat the +loop index and key per-iteration state as required context. Without this, the user +sees "matrix is not positive definite" with no idea whether it happened on +iteration 2 or iteration 200. Always capture `i` (or the loop variable) and the +state that changed between iterations — the ensemble step count, the parameter +vector being updated, the ensemble member index, etc. For *nested* loops, include +both the outer and inner loop variables — the outer variable says which group or +batch failed; the inner variable says which element within it failed. See the +loop-context example in the Canonical examples section below. + +**`catch e` losing the original exception**: when a Julia exception is caught and +a new one is thrown, the new message must include the original exception. If it +does not, the user loses the root cause (e.g. `PosDefException`, `SingularException`) +and has no way to distinguish a code bug from a numerical issue. Use +`sprint(showerror, e)` rather than `$e` alone — it formats the exception type and +message together: + +```julia +# anti-pattern — root cause vanishes +catch e + throw(ArgumentError("Matrix factorization failed.")) +end + +# correct — root cause preserved +catch e + throw(ArgumentError(""" +Matrix factorization failed. + +Caused by: $(sprint(showerror, e)) + +Suggestion: + ... +""")) +end +``` + +Only suppress the original exception if it is a well-known internal Julia error +(e.g. `SingularException`) and you are intentionally providing a higher-level +fallback — and even then, log it at `@debug` level. + +**`@warn string(...)` concatenation**: `@warn string("...", x, "...")` is the +warning-side equivalent of `error(string(...))` — it's noisy, hard to read, and +doesn't benefit from Julia's interpolation. Rewrite as `@warn "... $x ..."`. +`@warn` messages that use structured strings are also easier to grep and suppress +selectively. + +**Double-gated invariants**: if a helper is only ever called after the public API has already +checked the same condition (e.g., `get_vector_of_parameterized` is called from `construct_prior` +only when `d.args[1] == Symbol("VectorOfParameterized")` is true), the check inside the helper is +an internal invariant even though it looks like a user-data check. Use a single-line `error(...)` +rather than a full structured `ArgumentError`: + +```julia +# internal invariant — the caller already validated this +d.args[1] == Symbol("VectorOfParameterized") || error( + "Internal error: get_vector_of_parameterized called with non-VectorOfParameterized expression (got $(d.args[1]))", +) +``` + +### Step 2.5 — Decide: inline or helper? + +Before writing the rewrite, decide whether the error belongs inline or should be +extracted into a `_throw_(...)` helper function. + +**When to extract** — pull the error into a helper when either condition holds: + +- **Length** (primary trigger): the message body exceeds 3 lines. Extract + unconditionally — single call site, non-loop context, no surrounding complexity + required. A full Expected / Got / Suggestion block always crosses this threshold. + Even a one-off long block left inline establishes a pattern that makes entire files + hard to scan, and accumulates quickly once a few exceptions are made. +- **Duplication**: the same error shape (same summary line, same Expected / Got / + Suggestion skeleton) appears at ≥2 call sites. Extract even when each block is + short — the wording drifts silently over time and the call sites collapse to + readable one-liners. + +Inline is appropriate only for genuinely short messages (≤3 lines) at a single +call site. A single summary line, or a summary plus one Got line, is the ceiling +for inline. When in doubt, count — if it doesn't fit in 3 lines, extract. + +**Where helpers go** + +Default: a `## Error helpers` section at the **bottom of the source file**, above +`end # module`. Keeping helpers near their callers preserves traceability — the +reader sees the throw site, jumps to the bottom of the same file, and finds the +message without switching files. + +Promote to a shared `src/ErrorMessages.jl` (or the repo's equivalent top-level +utility file) only when **≥2 different source files** call the same helper. Discover +which file to use by reading the top-level module file (e.g. `src/PackageName.jl`) +for its `include(...)` list — then add `include("ErrorMessages.jl")` as the first +`include` so every subsequent file sees the helpers without any `using`/`import`. + +**Naming convention** + +``` +_throw_(positional_required_facts...; kwargs_for_optional_context...) +``` + +- Underscore prefix → unexported private helper. +- Verb prefix `_throw_` → the function unconditionally raises; callers know there + is no return value. +- Suffix describes the failure mode: `_dim_mismatch`, `_missing_keys`, + `_bad_obs_type`, `_not_iterable`. + +**Signature convention** + +Pass the facts that are *always* present as positional arguments (the offending +value, the expected vs got summary). Pass *optional* context as keyword arguments +with `nothing` defaults — especially loop context (`index`, `total`, `iter`, +`phase`). Build optional sections inside the helper by checking `isnothing(...)`. +This keeps call sites compact and lets the same helper serve both loop and non-loop +contexts (see the *Helper with optional loop context* canonical example). + +**Performance: use `@noinline`** + +Prefix every helper with `@noinline`. This prevents Julia from inlining the cold +error path into the surrounding hot code, keeping numerical kernels unaffected: + +```julia +@noinline function _throw_x_not_iterable(x; where::Symbol) + throw(ArgumentError(...)) +end +``` + +**What NOT to do** + +- Don't create a catch-all `_throw_arg_error(msg::String)` — that just shifts the + inline triple-quoted block to another file without any DRY benefit. +- Don't use macros (`@check_dim(...)`) — they're magical and harder to debug than + plain functions. +- Don't bundle all context into one opaque `context::NamedTuple` — explicit kwargs + are clearer to call and easier to extend. + +### Step 3 — Rewrite with the canonical layout + +Use this structure for every user-facing exception: + +```julia +throw(ArgumentError(""" +Short one-line summary of the failure. + +Expected: + + +Got: + + +Loop context: + iteration = $iter (of $n_iter) + = $(summary_of_state) + +Context: + + +Suggestion: + +""")) +``` + +Section rules: +- **Summary**: always present; one line; imperative or declarative. +- **Expected / Got**: strongly preferred for any mismatch check; use `$(expr)` + interpolation to show actual values. +- **Loop context**: include whenever the throw is inside a `for` or `while` loop. + Always report the loop index and the key state that varies between iterations + (e.g., the EKI step number, the ensemble member index, or the parameter being + updated). This is what lets the user reproduce the failure without adding + `println` debugging. Omit for errors that can only fire at a fixed point in the + code (before the loop starts or after it ends). +- **Context**: include when the same error can arise from multiple call sites and + naming the calling function or struct helps the user orient. +- **Suggestion**: include whenever a likely fix exists. Omit rather than write a + generic platitude. +- Never dump full matrices or large arrays. Prefer `size(x)`, `eltype(x)`, + `typeof(x)`, or a scalar summary statistic. + +### Step 4 — Move validation early + +If the current code lets an invalid input reach a numerical routine before +failing (e.g., `cholesky` on a non-symmetric matrix, `inv` on a singular one), +add an explicit guard at the API boundary: + +```julia +# Before: error surfaces deep in cholesky +cov_chol = cholesky(C) + +# After: check at the boundary, raise immediately +issymmetric(C) || throw(ArgumentError(""" +Covariance matrix must be symmetric. + +Got: + size(C) = $(size(C)) + norm(C - C') = $(norm(C - C')) + +Suggestion: + Pass a symmetric matrix, e.g. `C = (C + C') / 2`. +""")) +cov_chol = cholesky(C) +``` + +Use `||` for single-condition guards. For multi-condition guards, use `if/throw`. + +When using `||` with a multiline triple-quoted throw, the closing `))` goes on its own line +immediately after the closing `"""`: + +```julia +condition || throw(ArgumentError(""" +Summary line. + +Expected: + ... + +Got: + ... +""")) # ← closing )) on the line right after the closing """ +``` + +This is the only layout that keeps indentation correct — triple-quoted strings in Julia do not +strip leading whitespace, so indenting the message body would include those spaces in the string. + +### Step 5 — Preserve domain language + +Write messages in terms the user understands, not in terms of internal Julia +dispatch or linear algebra internals. For example: + +- Say "ensemble member count" not "size(x, 2)" +- Say "parameter covariance matrix" not "the second argument to cholesky" +- Say "observation noise covariance" not "Γ_y" +- Say "emulator training samples" not "io_pairs column count" or "n_train" +- Say "encoder pipeline" not "encoder_schedule vector" +- Say "fraction of variance to retain" not "retain_var threshold" +- Say "MCMC step size" or "proposal variance" not "proposal standard deviation scalar" +- Say "data processor" when referring to `ElementwiseScaler`, `Decorrelator`, + `CanonicalCorrelation`, or `LikelihoodInformed` — not "utility type" or "struct" + +### Step 6 — Apply rewrites + +Edit each site, keeping the surrounding code untouched. Confirm the package +still loads: + +``` +julia --project -e 'using CalibrateEmulateSample' +``` + +### Step 7 — Add @test_throws tests + +Before writing any test, check whether coverage already exists. Grep the matching +`test//runtests.jl` for the public API function that reaches the rewritten +site: + +```bash +grep -n '@test_throws' test//runtests.jl | grep '' +``` + +Three outcomes: + +| Situation | Action | +|---|---| +| `@test_throws ` already present | Skip — do not add a duplicate | +| `@test_throws ` already present | Update the existing line to the new type | +| No coverage at all | Add a new test | + +**Check message content, not just type**: In Julia 1.8+, `@test_throws` returns +the caught exception, so you can pin key diagnostic text in the same block: + +```julia +let thrown = @test_throws ArgumentError f(bad_input) + @test contains(thrown.value.msg, "retain_var") # Got section present + @test contains(thrown.value.msg, repr(bad_input)) # received value interpolated +end +``` + +Add at least one `contains` check per new error site. Without this, a refactor can +preserve the exception type while silently dropping the Got section or the +interpolated value, and no test will catch it. Check for: a phrase from the summary +line, the Got section label (e.g. `"retain_var"`, `"ensemble size"`), and +`repr(bad_value)` when the message uses `repr`. The `let` block is the cleanest +form — it keeps the type assertion and the content assertions co-located. + +For every site that needs a new test, add it in the matching `test//runtests.jl`: + +```julia +let thrown = @test_throws ArgumentError Decorrelator(rand(5, 10); retain_var = -0.1) + @test contains(thrown.value.msg, "retain_var") + @test contains(thrown.value.msg, "-0.1") +end +``` + +Use the specific exception type — never bare `@test_throws Exception`. The test +should construct the minimal invalid input that triggers the new error, without +duplicating happy-path coverage. + +**Update existing tests that used the wrong type.** If the file already has a +`@test_throws ErrorException` (or any other type) for a site you're rewriting to +`ArgumentError`, update that existing test in the same edit. Leaving a stale +`@test_throws ErrorException` will cause it to pass against the old code but fail +once your rewrite lands — or vice versa. + +**Testing unexported helpers.** If the site is inside an unexported helper (e.g., +`construct_constraint`, `construct_2d_array`), do not `import` the internal +directly. Instead, test through the nearest exported public API function that +calls it, using invalid input that propagates to the helper: + +```julia +# construct_constraint is unexported — test via get_parameter_distribution +no_constraint_dict = Dict("uq_param" => Dict("prior" => "Parameterized(Normal(0.0, 1.0))")) +let thrown = @test_throws ArgumentError get_parameter_distribution(no_constraint_dict, "uq_param") + @test contains(thrown.value.msg, "constraint") +end +``` + +This keeps tests coupled to the public contract and avoids brittleness when +internal function names change. + +### Step 8 — Offer to improve the skill + +Once the rewrites and tests are clean, offer: "Would you like to improve the +**error-message-manager** skill itself using skill-creator? You can share +suggestions, or I can analyse patterns from this session—recurring edge cases, +exception-type decisions, or anything that felt awkward—to refine the skill for +next time." + +--- + +## Style rules + +- **Triple-quoted strings** for all multiline messages. +- **No full matrix dumps**. Use `size(x)`, `eltype(x)`, `norm(x - ...)`, or + `extrema(x)` instead. +- **Interpolate actual values** in Got sections so the user sees the numbers, + not just variable names. For `String`-typed arguments use `$(repr(x))` rather + than `$(x)` — it adds the surrounding quotes so the output clearly reads as a + string value (e.g. `Got: sigma_points = "bad"` instead of `Got: sigma_points = bad`). +- **Raise early**: prefer guarding at the function entry point over deep inside a + helper. +- **No `@assert` for user-facing validation**. `@assert` is a debugging tool; + it can be compiled out. Use explicit `throw` instead. +- **Loop state in Got / Loop context**: when a throw is inside a `for` or `while` + loop, always name the iteration index and the key state from that iteration. + A "convergence failed" message without the step count forces the user to add + `println` debugging to reproduce the failure. When the same loop-context check + recurs at multiple sites, the loop variables become optional kwargs on a + `_throw_` helper — see the *Helper with optional loop context* canonical + example. +- **Preserve the original exception in `catch` blocks**: if you catch `e` and + throw a new exception, include `$(sprint(showerror, e))` in the new message. + Dropping `e` silently discards the root cause. +- **`@warn` with interpolation, not `string()`**: replace `@warn string("x=", x)` + with `@warn "x = $x"`. String concatenation in warnings is harder to read and + grep. +- **Single-line messages are fine** when the failure is unambiguous and no + Expected/Got context would add clarity. +- **Extract into `_throw_(...)` helpers** whenever the message body exceeds + 3 lines, or when the same Expected / Got / Suggestion skeleton appears at ≥2 + call sites (even if short). A full Expected / Got / Suggestion block always + exceeds 3 lines and must be a helper — inline is only appropriate for ≤3-line + messages at a single call site. Place the helper in a `## Error helpers` section + at the bottom of the source file; promote to a shared `src/ErrorMessages.jl` only + when ≥2 different source files share the helper. Use `@noinline`, positional args + for required facts, and `nothing`-defaulted kwargs for optional context such as + loop indices. Render each optional section only when its kwarg is non-`nothing`. + +--- + +## Canonical before/after examples + +> **Length rule applies to all examples below.** Each example shows the canonical +> message *format* (Expected / Got / Suggestion sections, interpolation, etc.). When +> the message body exceeds 3 lines — which any structured block with Expected / Got / +> Suggestion sections does — the throw must go in a `_throw_(...)` helper per +> Step 2.5, not inline. The first example below models this explicitly. Subsequent +> examples show the message body format; apply the same helper extraction whenever +> the resulting message exceeds 3 lines. + +### Replace a vague `error(string(...))` + +The after-message has 10 lines (well above the 3-line threshold), so it goes into +a `_throw_` helper — extract unconditionally at this length even though there is +only one call site. + +```julia +# Before — in a data-processor constructor, a vague dimension-reduction guard +if retain_var <= 0.0 || retain_var > 1.0 + error(string("retain_var=", retain_var, " is invalid.")) +end + +# After — helper in the ## Error helpers section at the bottom of the file +@noinline function _throw_invalid_retain_var(retain_var) + throw(ArgumentError(""" +Invalid `retain_var` for dimension reduction. + +Expected: + 0 < retain_var ≤ 1.0 + +Got: + retain_var = $retain_var + +Suggestion: + `retain_var` is the fraction of explained variance to retain after SVD + truncation. Pass a value in (0, 1] — for example, `retain_var = 0.95` + to keep 95% of variance. +""")) +end + +# Call site collapses to a single guard line: +(0.0 < retain_var ≤ 1.0) || _throw_invalid_retain_var(retain_var) +``` + +### Replace a bare `@assert` on an API boundary + +```julia +# Before +@assert(haskey(param_info, "constraint")) + +# After +haskey(param_info, "constraint") || throw(ArgumentError(""" +Parameter info dict is missing the required "constraint" key. + +Got keys: + $(collect(keys(param_info))) + +Suggestion: + Ensure the TOML entry for this parameter includes a `constraint = ...` field. +""")) +``` + +### Replace a single-line string-value error (use `repr`) + +```julia +# Before +throw(ArgumentError("sigma_points type is not recognized. Select from \"symmetric\" or \"simplex\". ")) + +# After +throw(ArgumentError(""" +Unrecognized sigma_points type. + +Expected: + "symmetric" or "simplex" + +Got: + sigma_points = $(repr(sigma_points)) +""")) +``` + +Using `repr(sigma_points)` rather than `$(sigma_points)` keeps the string +quotes visible in the output, making it unambiguous that the user passed a +`String` value (and making copy-paste errors easy to spot). + +### Replace a dimension-mismatch `@assert` + +```julia +# Before +@assert size(x, 2) == length(mean_weights) + +# After +size(x, 2) == length(mean_weights) || throw(DimensionMismatch(""" +Ensemble size does not match the number of quadrature weights. + +Expected: + size(x, 2) == length(mean_weights) + +Got: + size(x, 2) = $(size(x, 2)) + length(mean_weights) = $(length(mean_weights)) +""")) +``` + +### Preserve the original exception when catching and re-throwing + +```julia +# Before — PosDefException or SingularException silently discarded +try + cov_chol = cholesky(cov_u) +catch e + error("Covariance matrix factorization failed.") +end + +# After — root cause preserved, matrix state shown +try + cov_chol = cholesky(cov_u) +catch e + throw(ArgumentError(""" +Covariance matrix factorization failed during empirical Gaussian sampling. + +Got: + size(cov_u) = $(size(cov_u)) + isposdef(cov_u) = $(isposdef(cov_u)) + +Caused by: $(sprint(showerror, e)) + +Suggestion: + The ensemble may have collapsed. Pass a non-zero `inflation` keyword + argument to regularise the sample covariance. +""")) +end +``` + +`sprint(showerror, e)` formats as `"LinearAlgebra.PosDefException: matrix is not +Hermitian; Cholesky factorization failed."` — far more informative than `string(e)`. +Only suppress `e` when you are intentionally providing a higher-level fallback (e.g. +falling back to `pinv`) and still emit it at `@debug` level. + +### Rewrite `@warn string(...)` to use interpolation + +```julia +# Before +@warn string("Emulator output covariance has negative eigenvalues.", "\n Clamping to zero.") + +# After +@warn "Emulator output covariance has negative eigenvalues — clamping to zero before sampling." + +# Before (with values) +@warn string("SVD truncation removed ", n_removed, " dimensions (retain_var=", retain_var, ").", "\nCheck that retain_var is not too aggressive.") + +# After +@warn "SVD truncation removed $n_removed dimension$(n_removed == 1 ? "" : "s") (retain_var=$retain_var). Reduce retain_var if emulator accuracy suffers." +``` + +### Add loop context to an error thrown inside an iteration loop + +```julia +# Before — user sees "Cholesky factorization failed" with no idea which output dim +for j in 1:n_out + try + cov_chol = cholesky(C_j) + catch e + error("Cholesky factorization failed") + end +end + +# After — guard before cholesky, expose output dimension index and diagnostic state +for j in 1:n_out + isposdef(C_j) || throw(ArgumentError(""" +Emulator output covariance is not positive definite for output dimension $j. + +Expected: + A positive-definite covariance matrix for every output dimension. + +Got: + output dimension = $j / $n_out + size(C_j) = $(size(C_j)) + minimum eigval = $(minimum(eigvals(Symmetric(C_j)))) + +Suggestion: + Increase `alg_reg_noise` in the GaussianProcess constructor to regularise + the covariance, or check that training data for output $j is not degenerate. +""")) + cov_chol = cholesky(C_j) +end +``` + +Key points: +- **Move the guard before the failing call** so the message fires with the full + loop state still in scope. Catching a `PosDefException` after the fact and + re-throwing loses the loop index and the matrix state. +- **Report the loop variable** (`j`, `i`, `iter`) and its upper bound so the user + knows whether the failure is early (dimension 1/10, likely degenerate training data) + or late (dimension 9/10, likely numerical accumulation). +- **Include one diagnostic scalar** — the minimum eigenvalue, the norm of the + update step, the condition number — rather than dumping the full matrix. + +When this same loop-context error needs to be thrown at multiple sites, the loop +variables (`i`, `N_iter`) naturally become optional kwargs on a `_throw_` +helper. The call site stays a single line and the loop-awareness travels with the +helper everywhere it is used — see the *Helper with optional loop context* example +below. + +### Extract a duplicated error into a helper + +`encode_data` and `decode_data` in `src/Emulator.jl` each validate the same +precondition: that the input array has the right number of dimensions. Before +extraction, nearly identical 10-line blocks appear at both call sites: + +```julia +# Before — same shape check in both encode_data and decode_data + +# in encode_data: +if !(x isa AbstractMatrix) + throw(ArgumentError(""" +encode_data: input must be a matrix. + +Expected: + An AbstractMatrix with one column per sample. + +Got: + typeof(x) = $(typeof(x)) + ndims(x) = $(ndims(x)) + +Suggestion: + Reshape your input to a matrix — use `reshape(x, :, 1)` for a single sample. +""")) +end + +# in decode_data: byte-for-byte identical except the summary reads "decode_data". +``` + +After extraction, both functions call a single helper defined once in a +`## Error helpers` section at the bottom of the file: + +```julia +# After — helper at the bottom of src/Emulator.jl + +## Error helpers + +@noinline function _throw_not_matrix(x; where::Symbol) + throw(ArgumentError(""" +$where: input must be a matrix. + +Expected: + An AbstractMatrix with one column per sample. + +Got: + typeof(x) = $(typeof(x)) + ndims(x) = $(ndims(x)) + +Suggestion: + Reshape your input to a matrix — use `reshape(x, :, 1)` for a single sample. +""")) +end + +# Both call sites now collapse to a single readable guard line: +function encode_data(encoder_schedule, x) + x isa AbstractMatrix || _throw_not_matrix(x; where = :encode_data) + # ... algorithm body visible immediately ... +end + +function decode_data(encoder_schedule, x) + x isa AbstractMatrix || _throw_not_matrix(x; where = :decode_data) + # ... algorithm body visible immediately ... +end +``` + +Key points: +- The `where::Symbol` kwarg embeds the calling function name in the message so + diagnostics stay specific even though the body is shared. Pass a `Symbol` literal + (`where = :my_func`) — symbols are cheap and render cleanly with `$where`. +- Both functions now have one guard line each instead of a 10-line block; the + algorithm body is immediately visible. +- `@noinline` keeps the error path out of the hot encode/decode path. +- The helper lives at the bottom of the same file — one jump away, no new file. + +### Helper with optional loop context + +The `for proc in encoder_schedule` loop in `src/Utilities.jl` applies each data +processor in turn but currently reports no position if one fails — the user sees +"encode failed" with no idea which processor in the pipeline triggered the error. +Extracting into a helper adds the index and makes the same helper reusable wherever +that validation appears: + +```julia +# Before — inline block, no loop index in the message +for proc in encoder_schedule + if !applicable(encode_data, proc, x) + throw(ArgumentError(""" +Data processor does not support encode_data. + +Expected: + All processors in encoder_schedule to implement encode_data. + +Got: + typeof(proc) = $(typeof(proc)) + +Suggestion: + Ensure every processor in the encoder_schedule implements encode_data. +""")) + end +end + +# After — helper with optional loop context at the bottom of the file + +@noinline function _throw_proc_missing_encode(proc; index = nothing, total = nothing) + loop_ctx = isnothing(index) ? "" : """ + +Loop context: + processor index = $index (of $total)""" + throw(ArgumentError(""" +Data processor does not implement encode_data.$loop_ctx + +Expected: + Every element of encoder_schedule to implement encode_data(proc, x). + +Got: + typeof(proc) = $(typeof(proc)) + +Suggestion: + Check that each processor in your encoder_schedule is a recognised data + processor type (e.g. Decorrelator, ElementwiseScaler, CanonicalCorrelation). +""")) +end + +# Call site — loop now reports position: +for (i, proc) in enumerate(encoder_schedule) + applicable(encode_data, proc, x) || + _throw_proc_missing_encode(proc; index = i, total = length(encoder_schedule)) +end + +# The same helper works outside a loop — omit the kwargs and the Loop context +# section is silently suppressed: +applicable(encode_data, proc, x) || _throw_proc_missing_encode(proc) +``` + +Key points: +- `index` and `total` default to `nothing`; the `Loop context:` section is + rendered only when they are provided. No special-casing at any call site. +- Switching `for proc in ...` to `for (i, proc) in enumerate(...)` is the only + loop-side change needed to expose the index. +- The user now knows *which* processor failed, not just that one of them did. +- The same helper can be called from a non-loop site (e.g. single-processor + validation) with zero kwargs and produces a clean message without a Loop context + section. + +--- + +## Non-goals + +- Do not rewrite every low-level exception in the package. Focus on user-facing + API boundaries and sites explicitly identified. +- Do not suppress Julia stack traces. The goal is clearer diagnostics, not + silenced errors. +- Do not add verbosity for its own sake. A short, clear message beats a long, + generic one. +- Do not expose internal linear algebra variable names or dispatch details when + domain-level terminology exists. +- Do not extract truly short errors (≤3 lines) at a single call site — the + inline form is easier to grep and keeps cause and message co-located. A single + summary line, or a summary plus one Got line, is the ceiling for inline. From cb509be634741f9881dc26d5c7f84d134170baf5 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 00:37:09 -0700 Subject: [PATCH 02/14] apply base-show and iterate --- .claude/skills/base-show/SKILL.md | 24 ++ src/CalibrateEmulateSample.jl | 2 + src/Utilities/canonical_correlation.jl | 13 - src/Utilities/decorrelator.jl | 9 - src/Utilities/elementwise_scaler.jl | 6 - src/Utilities/likelihood_informed.jl | 9 - src/show.jl | 384 +++++++++++++++++++++++++ test/Show/runtests.jl | 253 ++++++++++++++++ 8 files changed, 663 insertions(+), 37 deletions(-) create mode 100644 src/show.jl create mode 100644 test/Show/runtests.jl diff --git a/.claude/skills/base-show/SKILL.md b/.claude/skills/base-show/SKILL.md index 4b0e5e045..6c346ef4a 100644 --- a/.claude/skills/base-show/SKILL.md +++ b/.claude/skills/base-show/SKILL.md @@ -170,6 +170,30 @@ default to `src/show.jl` if no prior convention exists. If creating `src/show.jl`, add `include("show.jl")` to the main module file after the type definitions it references. +**Submodule getter pitfall** — when `show.jl` is collected at the top level of a +package that uses submodules, a getter function defined *inside a submodule* is only +accessible from `show.jl` if it is exported from that submodule. An unexported getter +such as `get_noise_injector` will raise a confusing `MethodError: no method matching +show(...)` at runtime — the error message names the *show* method because Julia +cannot resolve the getter call inside the show body, and the nearest enclosing method +frame is the show method itself. + +Before writing a show body that calls getters, check whether each getter is exported: + +``` +grep -n "export.*getter_name" src/SubModule.jl +``` + +If the getter is not exported, use direct field access (`x.field`) instead. This is +safe — show methods are implementation code, not public API, so coupling to field +names is acceptable. As a cross-check, after writing all methods, grep show.jl for +every function call that is not a Julia built-in and confirm each one appears in an +`export` statement in its defining submodule: + +``` +grep -oP '(?<=\()[\w_]+(?=\()' src/show.jl | sort -u +``` + ### Step 4 — Write unit tests Write one test block per type, covering `show` (full and compact), and `summary`. Each diff --git a/src/CalibrateEmulateSample.jl b/src/CalibrateEmulateSample.jl index 63a47b23b..2dd77c2cf 100644 --- a/src/CalibrateEmulateSample.jl +++ b/src/CalibrateEmulateSample.jl @@ -25,4 +25,6 @@ include("Emulator.jl") # Internal deps, light external deps include("MarkovChainMonteCarlo.jl") +include("show.jl") + end # module diff --git a/src/Utilities/canonical_correlation.jl b/src/Utilities/canonical_correlation.jl index 0020ad79b..f3fe4648e 100644 --- a/src/Utilities/canonical_correlation.jl +++ b/src/Utilities/canonical_correlation.jl @@ -71,19 +71,6 @@ returns the `apply_to` field of the `CanonicalCorrelation`. """ get_apply_to(cc::CanonicalCorrelation) = cc.apply_to -function Base.show(io::IO, cc::CanonicalCorrelation) - - out = "CanonicalCorrelation:" - if length(get_apply_to(cc)) > 0 - out *= " apply_to=$(get_apply_to(cc)[1])" - end - if get_retain_var(cc) < 1.0 - out *= " retain_var=$(get_retain_var(cc))" - end - print(io, out) -end - - function initialize_processor!( cc::CanonicalCorrelation, in_data::MM, diff --git a/src/Utilities/decorrelator.jl b/src/Utilities/decorrelator.jl index c6a37cdae..ddbf90916 100644 --- a/src/Utilities/decorrelator.jl +++ b/src/Utilities/decorrelator.jl @@ -205,15 +205,6 @@ returns the `decorrelate_with` field of the `Decorrelator`. """ get_decorrelate_with(dd::Decorrelator) = dd.decorrelate_with -function Base.show(io::IO, dd::Decorrelator) - out = "Decorrelator" - out *= ": decorrelate_with=$(get_decorrelate_with(dd))" - if get_retain_var(dd) < 1.0 - out *= ", retain_var=$(get_retain_var(dd))" - end - print(io, out) -end - """ $(TYPEDSIGNATURES) diff --git a/src/Utilities/elementwise_scaler.jl b/src/Utilities/elementwise_scaler.jl index ec6449f1b..b7d0f0940 100644 --- a/src/Utilities/elementwise_scaler.jl +++ b/src/Utilities/elementwise_scaler.jl @@ -138,12 +138,6 @@ returns the `struct_decoder_mat` field of the `ElementwiseScaler`. For Consisten get_decoder_mat(es::ElementwiseScaler) = es.struct_decoder_mat -function Base.show(io::IO, es::ElementwiseScaler) - out = "ElementwiseScaler: $(get_type(es))" - print(io, out) -end - - function initialize_processor!( es::ElementwiseScaler, data::MM, diff --git a/src/Utilities/likelihood_informed.jl b/src/Utilities/likelihood_informed.jl index 75156c701..2a9f51381 100644 --- a/src/Utilities/likelihood_informed.jl +++ b/src/Utilities/likelihood_informed.jl @@ -63,15 +63,6 @@ get_retain_info(li::LikelihoodInformed) = li.retain_info get_iters(li::LikelihoodInformed) = li.iters get_grad_type(li::LikelihoodInformed) = li.grad_type -function Base.show(io::IO, li::LikelihoodInformed) - out = "LikelihoodInformed" - out *= ": iters=$(get_iters(li)), grad_type=$(get_grad_type(li))" - if get_retain_info(li) < 1.0 - out *= ", retain_info=$(get_retain_info(li))" - end - print(io, out) -end - function initialize_processor!( li::LikelihoodInformed, in_data::MM, diff --git a/src/show.jl b/src/show.jl new file mode 100644 index 000000000..b71a702b8 --- /dev/null +++ b/src/show.jl @@ -0,0 +1,384 @@ +# src/show.jl +# All Base.show and Base.summary methods for CalibrateEmulateSample.jl. +# Included from CalibrateEmulateSample.jl after all submodule includes. + +using .Utilities +using .Emulators +using .MarkovChainMonteCarlo +import .MarkovChainMonteCarlo: RWMetropolisHastings, pCNMetropolisHastings, + BarkerMetropolisHastings, AutodiffProtocol + +# ── Utilities ───────────────────────────────────────────────────────────────── + +# ElementwiseScaler + +function Base.show(io::IO, es::ElementwiseScaler) + print(io, "ElementwiseScaler: $(get_type(es))") +end + +function Base.show(io::IO, ::MIME"text/plain", es::ElementwiseScaler) + if get(io, :compact, false) + show(io, es) + else + println(io, "ElementwiseScaler") + println(io, " scaling method : $(get_type(es))") + print(io, " initialized : $(!isempty(es.shift))") + end +end + +function Base.summary(io::IO, es::ElementwiseScaler) + print(io, "ElementwiseScaler ($(get_type(es)))") +end + +# Decorrelator + +function Base.show(io::IO, dd::Decorrelator) + out = "Decorrelator: decorrelate_with=$(get_decorrelate_with(dd))" + if get_retain_var(dd) < 1.0 + out *= ", retain_var=$(get_retain_var(dd))" + end + print(io, out) +end + +function Base.show(io::IO, ::MIME"text/plain", dd::Decorrelator) + if get(io, :compact, false) + show(io, dd) + else + println(io, "Decorrelator") + println(io, " decorrelate_with : $(get_decorrelate_with(dd))") + if get_retain_var(dd) < 1.0 + println(io, " retain_var : $(get_retain_var(dd))") + end + print(io, " initialized : $(!isempty(dd.data_mean))") + end +end + +function Base.summary(io::IO, dd::Decorrelator) + print(io, "Decorrelator (decorrelate_with=$(get_decorrelate_with(dd)))") +end + +# CanonicalCorrelation + +function Base.show(io::IO, cc::CanonicalCorrelation) + out = "CanonicalCorrelation:" + if length(get_apply_to(cc)) > 0 + out *= " apply_to=$(get_apply_to(cc)[1])" + end + if get_retain_var(cc) < 1.0 + out *= " retain_var=$(get_retain_var(cc))" + end + print(io, out) +end + +function Base.show(io::IO, ::MIME"text/plain", cc::CanonicalCorrelation) + if get(io, :compact, false) + show(io, cc) + else + println(io, "CanonicalCorrelation") + if length(get_apply_to(cc)) > 0 + println(io, " apply_to : $(get_apply_to(cc)[1])") + end + if get_retain_var(cc) < 1.0 + println(io, " retain_var : $(get_retain_var(cc))") + end + print(io, " initialized : $(!isempty(cc.data_mean))") + end +end + +function Base.summary(io::IO, cc::CanonicalCorrelation) + print(io, "CanonicalCorrelation (retain_var=$(get_retain_var(cc)))") +end + +# LikelihoodInformed + +function Base.show(io::IO, li::LikelihoodInformed) + out = "LikelihoodInformed: iters=$(get_iters(li)), grad_type=$(get_grad_type(li))" + if get_retain_info(li) < 1.0 + out *= ", retain_info=$(get_retain_info(li))" + end + print(io, out) +end + +function Base.show(io::IO, ::MIME"text/plain", li::LikelihoodInformed) + if get(io, :compact, false) + show(io, li) + else + println(io, "LikelihoodInformed") + println(io, " iters : $(get_iters(li))") + println(io, " grad_type : $(get_grad_type(li))") + if get_retain_info(li) < 1.0 + println(io, " retain_info : $(get_retain_info(li))") + end + print(io, " initialized : $(!isempty(li.data_mean))") + end +end + +function Base.summary(io::IO, li::LikelihoodInformed) + print(io, "LikelihoodInformed (iters=$(get_iters(li)), grad_type=$(get_grad_type(li)))") +end + +# NoiseInjector + +function Base.show(io::IO, ni::NoiseInjector) + print(io, "NoiseInjector (use_noise=$(ni.use_noise), K=$(size(ni.K,1))×$(size(ni.K,2)))") +end + +function Base.show(io::IO, ::MIME"text/plain", ni::NoiseInjector) + if get(io, :compact, false) + show(io, ni) + else + println(io, "NoiseInjector") + println(io, " use_noise : $(ni.use_noise)") + println(io, " scaling : $(ni.scaling)") + println(io, " K size : $(size(ni.K,1))×$(size(ni.K,2))") + print(io, " encoder_schedule : $(length(ni.encoder_schedule)) entries") + end +end + +function Base.summary(io::IO, ni::NoiseInjector) + print(io, "NoiseInjector (use_noise=$(ni.use_noise), K=$(size(ni.K,1))×$(size(ni.K,2)))") +end + +# ── Emulators ───────────────────────────────────────────────────────────────── + +# Helper: print one encoder line — dimension reduction + processor chain — for +# a given space ("in" or "out"). Skips silently when no encoder acts on that space. +function _show_encoder_line(io::IO, enc_sch, raw_dim, space, label) + enc_dim = get_encoded_dim(enc_sch, space) + isnothing(enc_dim) && return + print(io, " ", label, ": ", raw_dim, " → ", enc_dim, " ") + first_name = true + for (p, a) in enc_sch + if a == space + first_name || print(io, " → ") + print(io, nameof(typeof(p))) + first_name = false + end + end + println(io) +end + +# GaussianProcess + +function Base.show(io::IO, x::GaussianProcess{P}) where {P} + n = length(x.models) + print(io, "GaussianProcess{", nameof(P), "} (", n, " model", n == 1 ? "" : "s", ")") +end + +function Base.show(io::IO, ::MIME"text/plain", x::GaussianProcess{P}) where {P} + if get(io, :compact, false) + show(io, x) + else + n = length(x.models) + println(io, "GaussianProcess{", nameof(P), "}") + println(io, " n_models : ", n, " (one per output dimension)") + println(io, " noise_learn : ", x.noise_learn) + print(io, " pred_type : ", nameof(typeof(x.prediction_type))) + end +end + +function Base.summary(io::IO, x::GaussianProcess{P}) where {P} + n = length(x.models) + print(io, "GaussianProcess{", nameof(P), "} (", n, " model", n == 1 ? "" : "s", ")") +end + +# ScalarRandomFeatureInterface + +function Base.show(io::IO, x::ScalarRandomFeatureInterface) + print(io, "ScalarRandomFeatureInterface (", get_input_dim(x), "→1, ", + get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") +end + +function Base.show(io::IO, ::MIME"text/plain", x::ScalarRandomFeatureInterface) + if get(io, :compact, false) + show(io, x) + else + println(io, "ScalarRandomFeatureInterface") + println(io, " input_dim : ", get_input_dim(x)) + println(io, " n_features : ", get_n_features(x)) + println(io, " kernel : ", nameof(typeof(get_kernel_structure(x)))) + println(io, " decomp : ", x.feature_decomposition) + print(io, " built : ", !isempty(x.rfms)) + end +end + +function Base.summary(io::IO, x::ScalarRandomFeatureInterface) + print(io, "ScalarRandomFeatureInterface (", get_input_dim(x), "→1, ", + get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") +end + +# VectorRandomFeatureInterface + +function Base.show(io::IO, x::VectorRandomFeatureInterface) + print(io, "VectorRandomFeatureInterface (", get_input_dim(x), "→", get_output_dim(x), ", ", + get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") +end + +function Base.show(io::IO, ::MIME"text/plain", x::VectorRandomFeatureInterface) + if get(io, :compact, false) + show(io, x) + else + println(io, "VectorRandomFeatureInterface") + println(io, " input_dim : ", get_input_dim(x)) + println(io, " output_dim : ", get_output_dim(x)) + println(io, " n_features : ", get_n_features(x)) + println(io, " kernel : ", nameof(typeof(get_kernel_structure(x)))) + println(io, " decomp : ", x.feature_decomposition) + print(io, " built : ", !isempty(x.rfms)) + end +end + +function Base.summary(io::IO, x::VectorRandomFeatureInterface) + print(io, "VectorRandomFeatureInterface (", get_input_dim(x), "→", get_output_dim(x), ", ", + get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") +end + +# Emulator + +function Base.show(io::IO, x::Emulator) + mlt = get_machine_learning_tool(x) + n_in, n_out = size(get_io_pairs(x), 1) + print(io, "Emulator (", nameof(typeof(mlt)), ", ", n_in, "→", n_out, ")") +end + +function Base.show(io::IO, ::MIME"text/plain", x::Emulator) + if get(io, :compact, false) + show(io, x) + else + mlt = get_machine_learning_tool(x) + n_in, n_out = size(get_io_pairs(x), 1) + n_train = size(DataContainers.get_inputs(get_io_pairs(x)), 2) + enc_sch = get_encoder_schedule(x) + println(io, "Emulator") + println(io, " machine_learning_tool : ", nameof(typeof(mlt))) + println(io, " input_dim : ", n_in) + println(io, " output_dim : ", n_out) + println(io, " n_train : ", n_train, " samples") + _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") + _show_encoder_line(io, enc_sch, n_out, "out", "encoder (output) ") + end +end + +function Base.summary(io::IO, x::Emulator) + mlt = get_machine_learning_tool(x) + n_in, n_out = size(get_io_pairs(x), 1) + print(io, "Emulator (", nameof(typeof(mlt)), ", ", n_in, "→", n_out, ")") +end + +# ForwardMapWrapper + +function Base.show(io::IO, x::ForwardMapWrapper) + n_in, n_out = size(get_io_pairs(x), 1) + print(io, "ForwardMapWrapper (", n_in, "→", n_out, ", prior_dim=", ndims(get_prior(x)), ")") +end + +function Base.show(io::IO, ::MIME"text/plain", x::ForwardMapWrapper) + if get(io, :compact, false) + show(io, x) + else + n_in, n_out = size(get_io_pairs(x), 1) + ni = x.noise_injector + println(io, "ForwardMapWrapper") + println(io, " input_dim : ", n_in) + println(io, " output_dim : ", n_out) + println(io, " prior_dim : ", ndims(get_prior(x))) + println(io, " encoders : ", length(get_encoder_schedule(x))) + print(io, " noise_inject : ", !isnothing(ni) && ni.use_noise) + end +end + +function Base.summary(io::IO, x::ForwardMapWrapper) + n_in, n_out = size(get_io_pairs(x), 1) + print(io, "ForwardMapWrapper (", n_in, "→", n_out, ", prior_dim=", ndims(get_prior(x)), ")") +end + +# ── MachineLearningTools ────────────────────────────────────────────────────── + +# ── MarkovChainMonteCarlo ───────────────────────────────────────────────────── + +# RWMetropolisHastings + +function Base.show(io::IO, x::RWMetropolisHastings{PT, ADT}) where {PT, ADT <: AutodiffProtocol} + print(io, "RWMetropolisHastings{$(nameof(ADT))}") +end + +function Base.show(io::IO, ::MIME"text/plain", x::RWMetropolisHastings{PT, ADT}) where {PT, ADT <: AutodiffProtocol} + if get(io, :compact, false) + show(io, x) + else + print(io, "RWMetropolisHastings{$(nameof(ADT))}") + end +end + +function Base.summary(io::IO, x::RWMetropolisHastings{PT, ADT}) where {PT, ADT <: AutodiffProtocol} + print(io, "RWMetropolisHastings{$(nameof(ADT))}") +end + +# pCNMetropolisHastings + +function Base.show(io::IO, x::pCNMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + print(io, "pCNMetropolisHastings{$(nameof(T))}") +end + +function Base.show(io::IO, ::MIME"text/plain", x::pCNMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + if get(io, :compact, false) + show(io, x) + else + print(io, "pCNMetropolisHastings{$(nameof(T))}") + end +end + +function Base.summary(io::IO, x::pCNMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + print(io, "pCNMetropolisHastings{$(nameof(T))}") +end + +# BarkerMetropolisHastings + +function Base.show(io::IO, x::BarkerMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + print(io, "BarkerMetropolisHastings{$(nameof(T))}") +end + +function Base.show(io::IO, ::MIME"text/plain", x::BarkerMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + if get(io, :compact, false) + show(io, x) + else + print(io, "BarkerMetropolisHastings{$(nameof(T))}") + end +end + +function Base.summary(io::IO, x::BarkerMetropolisHastings{D, T}) where {D, T <: AutodiffProtocol} + print(io, "BarkerMetropolisHastings{$(nameof(T))}") +end + +# MCMCWrapper + +function Base.show(io::IO, mcmc::MCMCWrapper) + n_par = ndims(mcmc.prior) + sampler_name = nameof(typeof(mcmc.mh_proposal_sampler)) + print(io, "MCMCWrapper (", n_par, " param", n_par == 1 ? "" : "s", ", ", sampler_name, ")") +end + +function Base.show(io::IO, ::MIME"text/plain", mcmc::MCMCWrapper) + if get(io, :compact, false) + show(io, mcmc) + else + n_par = ndims(mcmc.prior) + n_obs = length(mcmc.observations) + obs_dim = isempty(mcmc.observations) ? nothing : length(first(mcmc.observations)) + sampler_name = nameof(typeof(mcmc.mh_proposal_sampler)) + enc_sch = get_encoder_schedule(mcmc) + println(io, "MCMCWrapper") + println(io, " prior_dim : ", n_par, " parameter", n_par == 1 ? "" : "s") + isnothing(obs_dim) || println(io, " obs_dim : ", obs_dim) + println(io, " n_obs : ", n_obs, " sample", n_obs == 1 ? "" : "s") + println(io, " sampler : ", sampler_name) + _show_encoder_line(io, enc_sch, n_par, "in", "encoder (input) ") + isnothing(obs_dim) || _show_encoder_line(io, enc_sch, obs_dim, "out", "encoder (output)") + end +end + +function Base.summary(io::IO, mcmc::MCMCWrapper) + n_par = ndims(mcmc.prior) + sampler_name = nameof(typeof(mcmc.mh_proposal_sampler)) + print(io, "MCMCWrapper (", n_par, " param", n_par == 1 ? "" : "s", ", ", sampler_name, ")") +end diff --git a/test/Show/runtests.jl b/test/Show/runtests.jl new file mode 100644 index 000000000..fbded5330 --- /dev/null +++ b/test/Show/runtests.jl @@ -0,0 +1,253 @@ +using Test +using LinearAlgebra +using Distributions +using GaussianProcesses + +using CalibrateEmulateSample +using CalibrateEmulateSample.Utilities +using CalibrateEmulateSample.Emulators +using CalibrateEmulateSample.MarkovChainMonteCarlo +using CalibrateEmulateSample.DataContainers +using CalibrateEmulateSample.ParameterDistributions +import AdvancedMH +import AbstractMCMC + +const MCMC = MarkovChainMonteCarlo + +@testset "Show" begin + + # ── Utilities ────────────────────────────────────────────────────────────── + + @testset "ElementwiseScaler" begin + es = quartile_scale() + out = sprint(show, MIME("text/plain"), es) + @test occursin("ElementwiseScaler", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, es) + @test occursin("ElementwiseScaler", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), es; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, es) + @test occursin("ElementwiseScaler", outs) + @test !occursin('\n', outs) + end + + @testset "Decorrelator" begin + dd = decorrelate_sample_cov() + out = sprint(show, MIME("text/plain"), dd) + @test occursin("Decorrelator", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, dd) + @test occursin("Decorrelator", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), dd; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, dd) + @test occursin("Decorrelator", outs) + @test !occursin('\n', outs) + end + + @testset "CanonicalCorrelation" begin + cc = canonical_correlation() + out = sprint(show, MIME("text/plain"), cc) + @test occursin("CanonicalCorrelation", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, cc) + @test occursin("CanonicalCorrelation", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), cc; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, cc) + @test occursin("CanonicalCorrelation", outs) + @test !occursin('\n', outs) + end + + @testset "LikelihoodInformed" begin + li = likelihood_informed() + out = sprint(show, MIME("text/plain"), li) + @test occursin("LikelihoodInformed", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, li) + @test occursin("LikelihoodInformed", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), li; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, li) + @test occursin("LikelihoodInformed", outs) + @test !occursin('\n', outs) + end + + @testset "NoiseInjector" begin + ni = NoiseInjector(rand(3, 2), rand(2, 1), rand(3, 1), nothing, 0.5, true, []) + out = sprint(show, MIME("text/plain"), ni) + @test occursin("NoiseInjector", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, ni) + @test occursin("NoiseInjector", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), ni; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, ni) + @test occursin("NoiseInjector", outs) + @test !occursin('\n', outs) + end + + # ── Emulators ────────────────────────────────────────────────────────────── + + @testset "GaussianProcess" begin + gp = GaussianProcess(GPJL()) + out = sprint(show, MIME("text/plain"), gp) + @test occursin("GaussianProcess", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, gp) + @test occursin("GaussianProcess", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), gp; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, gp) + @test occursin("GaussianProcess", outs) + @test !occursin('\n', outs) + end + + @testset "ScalarRandomFeatureInterface" begin + srfi = ScalarRandomFeatureInterface(100, 3) + out = sprint(show, MIME("text/plain"), srfi) + @test occursin("ScalarRandomFeatureInterface", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, srfi) + @test occursin("ScalarRandomFeatureInterface", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), srfi; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, srfi) + @test occursin("ScalarRandomFeatureInterface", outs) + @test !occursin('\n', outs) + end + + @testset "VectorRandomFeatureInterface" begin + vrfi = VectorRandomFeatureInterface(100, 3, 2) + out = sprint(show, MIME("text/plain"), vrfi) + @test occursin("VectorRandomFeatureInterface", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, vrfi) + @test occursin("VectorRandomFeatureInterface", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), vrfi; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, vrfi) + @test occursin("VectorRandomFeatureInterface", outs) + @test !occursin('\n', outs) + end + + @testset "Emulator" begin + n, p, d = 20, 3, 2 + iop = PairedDataContainer(rand(p, n), rand(d, n), data_are_columns = true) + gp = GaussianProcess(GPJL()) + em = Emulator(gp, iop; encoder_schedule = []) + out = sprint(show, MIME("text/plain"), em) + @test occursin("Emulator", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, em) + @test occursin("Emulator", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), em; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, em) + @test occursin("Emulator", outs) + @test !occursin('\n', outs) + end + + @testset "ForwardMapWrapper" begin + prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 3) + iop = PairedDataContainer(rand(3, 5), rand(2, 5), data_are_columns = true) + ni = NoiseInjector(rand(2, 3), rand(3, 1), rand(2, 1), nothing, 0.5, true, []) + fmw = ForwardMapWrapper{Float64, Vector{Any}, typeof(prior), typeof(ni)}( + identity, prior, iop, iop, [], ni, + ) + out = sprint(show, MIME("text/plain"), fmw) + @test occursin("ForwardMapWrapper", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, fmw) + @test occursin("ForwardMapWrapper", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), fmw; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, fmw) + @test occursin("ForwardMapWrapper", outs) + @test !occursin('\n', outs) + end + + # ── MarkovChainMonteCarlo ────────────────────────────────────────────────── + + @testset "RWMetropolisHastings" begin + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + rw = MCMC.RWMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) + out = sprint(show, MIME("text/plain"), rw) + @test occursin("RWMetropolisHastings", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, rw) + @test occursin("RWMetropolisHastings", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), rw; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, rw) + @test occursin("RWMetropolisHastings", outs) + @test !occursin('\n', outs) + end + + @testset "pCNMetropolisHastings" begin + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + pcn = MCMC.pCNMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) + out = sprint(show, MIME("text/plain"), pcn) + @test occursin("pCNMetropolisHastings", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, pcn) + @test occursin("pCNMetropolisHastings", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), pcn; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, pcn) + @test occursin("pCNMetropolisHastings", outs) + @test !occursin('\n', outs) + end + + @testset "BarkerMetropolisHastings" begin + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + barkr = MCMC.BarkerMetropolisHastings{typeof(prop), ForwardDiffProtocol}(prop) + out = sprint(show, MIME("text/plain"), barkr) + @test occursin("BarkerMetropolisHastings", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, barkr) + @test occursin("BarkerMetropolisHastings", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), barkr; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, barkr) + @test occursin("BarkerMetropolisHastings", outs) + @test !occursin('\n', outs) + end + + @testset "MCMCWrapper" begin + prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 2) + obs = [rand(2) for _ in 1:3] + log_post = AdvancedMH.DensityModel(x -> -0.5 * sum(x .^ 2)) + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + rw_sampler = MCMC.RWMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) + mcmcw = MCMCWrapper{typeof(obs), typeof(obs), Vector{Any}}( + prior, prior, obs, obs, log_post, rw_sampler, (;), [], + ) + out = sprint(show, MIME("text/plain"), mcmcw) + @test occursin("MCMCWrapper", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, mcmcw) + @test occursin("MCMCWrapper", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), mcmcw; context = :compact => true) + @test out2 == out3 + outs = sprint(summary, mcmcw) + @test occursin("MCMCWrapper", outs) + @test !occursin('\n', outs) + end + +end From 8d06e2a9838065317b78d6b4f4a981e61014323d Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 10:25:58 -0700 Subject: [PATCH 03/14] update fmw show/summary --- src/show.jl | 6 ++++-- test/runtests.jl | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/show.jl b/src/show.jl index b71a702b8..eef963130 100644 --- a/src/show.jl +++ b/src/show.jl @@ -277,12 +277,14 @@ function Base.show(io::IO, ::MIME"text/plain", x::ForwardMapWrapper) show(io, x) else n_in, n_out = size(get_io_pairs(x), 1) - ni = x.noise_injector + enc_sch = get_encoder_schedule(x) + ni = x.noise_injector println(io, "ForwardMapWrapper") println(io, " input_dim : ", n_in) println(io, " output_dim : ", n_out) println(io, " prior_dim : ", ndims(get_prior(x))) - println(io, " encoders : ", length(get_encoder_schedule(x))) + _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") + _show_encoder_line(io, enc_sch, n_out, "out", "encoder (output) ") print(io, " noise_inject : ", !isnothing(ni) && ni.use_noise) end end diff --git a/test/runtests.jl b/test/runtests.jl index 58b6cf1e8..2943b6256 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -29,7 +29,7 @@ end end end - for submodule in ["Emulator", "GaussianProcess", "RandomFeature", "MarkovChainMonteCarlo", "Utilities"] + for submodule in ["Emulator", "GaussianProcess", "RandomFeature", "MarkovChainMonteCarlo", "Utilities", "Show"] if all_tests || has_submodule(submodule) || "CalibrateEmulateSample" in ARGS include_test(submodule) end From 78a567004a99d0b8d320f90ee7228a1709ccbad3 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 11:24:06 -0700 Subject: [PATCH 04/14] apply and iterate on docstrings skill --- .claude/skills/docstrings/SKILL.md | 45 +++++- docs/src/API/Emulators.md | 18 +++ docs/src/API/GaussianProcess.md | 2 + docs/src/API/MarkovChainMonteCarlo.md | 8 + docs/src/API/RandomFeatures.md | 7 + src/Emulator.jl | 149 ++++++++++-------- src/MachineLearningTools/GaussianProcess.jl | 40 +++-- src/MachineLearningTools/RandomFeature.jl | 63 ++++++-- .../ScalarRandomFeature.jl | 64 ++++---- .../VectorRandomFeature.jl | 66 ++++---- src/MarkovChainMonteCarlo.jl | 75 ++++----- src/Utilities.jl | 102 +++++++----- src/Utilities/elementwise_scaler.jl | 20 +++ src/Utilities/likelihood_informed.jl | 21 +++ 14 files changed, 422 insertions(+), 258 deletions(-) diff --git a/.claude/skills/docstrings/SKILL.md b/.claude/skills/docstrings/SKILL.md index b26d456c6..16ae6514e 100644 --- a/.claude/skills/docstrings/SKILL.md +++ b/.claude/skills/docstrings/SKILL.md @@ -77,10 +77,10 @@ the definition. Produce a prioritised list: **Also scan every function in the file for old-style docstrings**, regardless of whether it is exported. An old-style docstring is one that uses an indented -function-name header (e.g. ` my_func(arg1, arg2)`) and/or an `Args:` / -`Arguments:` block with the `` `name` - description `` format. Convert these to -the `$(TYPEDSIGNATURES)` convention in the same editing pass — the whole file -should end up stylistically uniform. +function-name header (e.g. ` my_func(arg1, arg2)`), a `$(FUNCTIONNAME)(args...)` +header line, and/or an `Args:` / `Arguments:` block with the `` `name` - description `` +format. Convert these to the `$(TYPEDSIGNATURES)` convention in the same editing +pass — the whole file should end up stylistically uniform. ### Step 3 — Draft docstrings @@ -182,11 +182,14 @@ checking whether the function is mentioned in user-facing tutorials or the #### Old-style function docstrings (all functions, not just exported) -Convert any docstring that uses an indented function-name header or an `Args:` / -`Arguments:` section to the `$(TYPEDSIGNATURES)` style, even for internal -(non-exported) helpers. The canonical old-style markers are: +Convert any docstring that uses an indented function-name header, a `$(FUNCTIONNAME)` +header, or an `Args:` / `Arguments:` section to the `$(TYPEDSIGNATURES)` style, even +for internal (non-exported) helpers. The canonical old-style markers are: - First line indented with spaces: ` my_func(arg, ...)` — replace with `$(TYPEDSIGNATURES)`. +- First line is `$(FUNCTIONNAME)(args...)` or `$(DocStringExtensions.FUNCTIONNAME)(args...)` — + replace the entire header line with `$(TYPEDSIGNATURES)` and convert any signature + description into prose below it. - Argument block labelled `Args:` or `Arguments:` with `` `name` - description `` lines — replace with a `# Arguments` section using `` - `name`: description `` format. @@ -209,6 +212,18 @@ old-style docstrings from persisting as invisible technical debt. - For every non-trivial public function where a minimal runnable example can be written, add a `# Examples` section with a `jldoctest` block so Documenter.jl can verify the example stays correct as the code evolves. +- Always use the **unqualified** macro forms: `$(TYPEDSIGNATURES)`, `$(TYPEDEF)`, + `$(TYPEDFIELDS)`, `$(METHODLIST)` — never `$(DocStringExtensions.TYPEDSIGNATURES)` + etc. Julia propagates `using DocStringExtensions` from a module to all files it + `include`s, so the short forms are always in scope within a package. +- Only write `` [`name`](@ref) `` cross-reference links to symbols that have a + `@docs` entry on some API page. For concrete subtypes that are described under + their abstract parent's docstring but not individually listed in `@docs`, use + plain backtick code instead (e.g. `` `GPJL` `` rather than `` [`GPJL`](@ref) ``). + Broken `@ref` links cause docs build errors that are hard to trace back to the + offending docstring. +- Fix obvious typos (missing quotes, misspelled words) in existing docstrings + encountered during the editing pass rather than deferring them. ### Step 4 — Apply edits @@ -272,11 +287,19 @@ Steps 1–3) before adding it to the API page — an undocumented entry in a `@docs` block will cause the docs build to error. Every exported, defined symbol must end up with a docstring and an API page entry. +**Re-exported symbols.** Before adding a symbol to a page, verify the symbol is +*defined* in the page's `CurrentModule` — not merely imported or re-exported from +another module via `import` or `using ... : name`. If it is imported, it belongs +on the source module's page only; adding it to the importing module's page creates +a duplicate `@docs` entry that causes a docs build error. A reliable check: grep +the source files of `CurrentModule` for the function or type definition — if the +only match is an `import` statement, it lives elsewhere. + **Stale API entries.** An entry is **stale** when the base name is no longer exported from the module, or the symbol no longer has a definition in the source. -Run all three checks before making edits so you can see the full diff in one +Run all four checks before making edits so you can see the full diff in one pass. #### 5d — Place missing symbols into appropriate sections @@ -357,6 +380,12 @@ expect. Apply them consistently. signature — never write a manual indented signature as well. - **No trailing whitespace** inside the docstring. - **No emojis.** +- **Backtick all identifiers in prose**: any `snake_case` name or quoted string key + that appears in prose outside a code fence or existing backtick span must be + wrapped in backticks. This is especially important for Dict option keys in + `# Arguments` sub-lists (e.g. `` `"cov_sample_multiplier"` `` not + `"cov_sample_multiplier"`) — without backticks, Documenter.jl's markdown parser + may treat underscores as italic markers. - **Physical units** in square brackets: `[m/day]`, `[kg/m³]`, `[day]`, etc. - **Field string literals** (the string above each struct field) are distinct from the struct-level docstring. Preserve both; do not merge them. diff --git a/docs/src/API/Emulators.md b/docs/src/API/Emulators.md index e37aede0c..6e09e0732 100644 --- a/docs/src/API/Emulators.md +++ b/docs/src/API/Emulators.md @@ -13,4 +13,22 @@ encode_data decode_data encode_structure_matrix decode_structure_matrix +``` + +## Forward map wrapper + +```@docs +ForwardMapWrapper +forward_map_wrapper +``` + +## Getter functions + +```@docs +get_machine_learning_tool +get_io_pairs +get_encoded_io_pairs +get_encoder_schedule +get_forward_map +get_prior ``` \ No newline at end of file diff --git a/docs/src/API/GaussianProcess.md b/docs/src/API/GaussianProcess.md index 065edec0f..a9b0dbc35 100644 --- a/docs/src/API/GaussianProcess.md +++ b/docs/src/API/GaussianProcess.md @@ -18,4 +18,6 @@ GaussianProcess( build_models!(::GaussianProcess{GPJL}, ::PairedDataContainer{FT}, input_structure_mats, output_structure_mats) where {FT <: AbstractFloat} optimize_hyperparameters!(::GaussianProcess{GPJL}) predict(::GaussianProcess{GPJL}, ::AbstractMatrix{FT}) where {FT <: AbstractFloat} +get_params +get_param_names ``` \ No newline at end of file diff --git a/docs/src/API/MarkovChainMonteCarlo.md b/docs/src/API/MarkovChainMonteCarlo.md index f60baf453..cf0518b60 100644 --- a/docs/src/API/MarkovChainMonteCarlo.md +++ b/docs/src/API/MarkovChainMonteCarlo.md @@ -19,6 +19,7 @@ MCMCWrapper( sample get_posterior optimize_stepsize +get_sample_kwargs ``` See [AbstractMCMC sampling API](@ref) for background on our use of Turing.jl's @@ -30,6 +31,7 @@ MCMC sampling. ```@docs MCMCProtocol AutodiffProtocol +GradFreeProtocol ForwardDiffProtocol ReverseDiffProtocol RWMHSampling @@ -50,3 +52,9 @@ EmulatorPosteriorModel MCMCState accept_ratio ``` + +## Diagnostics + +```@docs +esjd +``` diff --git a/docs/src/API/RandomFeatures.md b/docs/src/API/RandomFeatures.md index 7f41d8287..9ae2e26a1 100644 --- a/docs/src/API/RandomFeatures.md +++ b/docs/src/API/RandomFeatures.md @@ -5,6 +5,7 @@ CurrentModule = CalibrateEmulateSample.Emulators ``` ## Kernel and Covariance structure ```@docs +CovarianceStructureType OneDimFactor DiagonalFactor CholeskyFactor @@ -12,9 +13,14 @@ LowRankFactor HierarchicalLowRankFactor SeparableKernel NonseparableKernel +cov_structure_from_string calculate_n_hyperparameters hyperparameters_from_flat build_default_prior +get_eps +get_input_cov_structure +get_output_cov_structure +get_cov_structure ``` ## Scalar interface @@ -50,4 +56,5 @@ get_optimizer_options optimize_hyperparameters!(::ScalarRandomFeatureInterface) optimize_hyperparameters!(::VectorRandomFeatureInterface) shrinkage_cov +nice_cov ``` diff --git a/src/Emulator.jl b/src/Emulator.jl index d03d6827a..4f5d114a7 100644 --- a/src/Emulator.jl +++ b/src/Emulator.jl @@ -80,28 +80,29 @@ end """ $(TYPEDSIGNATURES) -Gets the `machine_learning_tool` field of the `Emulator` +Return the `MachineLearningTool` (e.g. `GaussianProcess`, `ScalarRandomFeatureInterface`) +stored in `emulator`. """ get_machine_learning_tool(emulator::Emulator) = emulator.machine_learning_tool """ $(TYPEDSIGNATURES) -Gets the `io_pairs` field of the `Emulator` +Return the original (unencoded) training `PairedDataContainer` stored in `emulator`. """ get_io_pairs(emulator::Emulator) = emulator.io_pairs """ $(TYPEDSIGNATURES) -Gets the `encoded_io_pairs` field of the `Emulator` +Return the encoded training `PairedDataContainer` stored in `emulator`. """ get_encoded_io_pairs(emulator::Emulator) = emulator.encoded_io_pairs """ $(TYPEDSIGNATURES) -Gets the `encoder_schedule` field of the `Emulator` +Return the initialised encoder schedule stored in `emulator`. """ get_encoder_schedule(emulator::Emulator) = emulator.encoder_schedule @@ -109,16 +110,22 @@ get_encoder_schedule(emulator::Emulator) = emulator.encoder_schedule ### Forward Map Wrapper """ -$(TYPEDEF) -This can replace an `Emulator`, but stores the original forward map. The forward map must be definable as a function `f`. To apply `f` properly this object also builds and stores an encoder `E` and a parameter distribution (i.e. the prior) containing physical constraints `c`. +Wrapper that stores an explicit forward map `f` in place of a trained `Emulator`. +When `predict` is called this object evaluates `E_out ∘ f ∘ c(x)`, where `c` is the +constraint transformation from the prior and `E_out` is the output encoder. -When predict() is called this map will call `E_{out}∘f∘c(x)` +$(TYPEDEF) # Fields + $(TYPEDFIELDS) -# Constructors: -- `forward_map_wrapper(forward_map, prior, input_output_pairs; encoder_schedule=nothing, encoder_kwargs=NamedTuple())` +# Constructors + +Prefer the [`forward_map_wrapper`](@ref) factory function for construction — it +builds and initialises the encoder schedule automatically from training data. + +$(METHODLIST) """ struct ForwardMapWrapper{FT <: Real, VV <: AbstractVector, PD <: ParameterDistribution, NI <: NoiseInjector} "function that represents the forward map" @@ -137,42 +144,42 @@ end """ $(TYPEDSIGNATURES) -Gets the `forward_map` field of the `ForwardMapWrapper` +Return the forward-map function stored in `fmw`. """ get_forward_map(fmw::ForwardMapWrapper) = fmw.forward_map """ $(TYPEDSIGNATURES) -Gets the `prior` field of the `ForwardMapWrapper` +Return the `ParameterDistribution` prior stored in `fmw`. """ get_prior(fmw::ForwardMapWrapper) = fmw.prior """ $(TYPEDSIGNATURES) -Gets the `io_pairs` field of the `ForwardMapWrapper` +Return the original (unencoded) training `PairedDataContainer` stored in `fmw`. """ get_io_pairs(fmw::ForwardMapWrapper) = fmw.io_pairs """ $(TYPEDSIGNATURES) -Gets the `encoded_io_pairs` field of the `ForwardMapWrapper` +Return the encoded training `PairedDataContainer` stored in `fmw`. """ get_encoded_io_pairs(fmw::ForwardMapWrapper) = fmw.encoded_io_pairs """ $(TYPEDSIGNATURES) -Gets the `encoder_schedule` field of the `ForwardMapWrapper` +Return the initialised encoder schedule stored in `fmw`. """ get_encoder_schedule(fmw::ForwardMapWrapper) = fmw.encoder_schedule """ $(TYPEDSIGNATURES) -Gets the `noise_injector` field of the `ForwardMapWrapper` +Return the `NoiseInjector` stored in `fmw`, used for null-space noise injection when decoding. """ get_noise_injector(fmw::ForwardMapWrapper) = fmw.noise_injector @@ -181,16 +188,21 @@ get_noise_injector(fmw::ForwardMapWrapper) = fmw.noise_injector """ $(TYPEDSIGNATURES) -Constructor of the Emulator object, +Construct an `Emulator` from a machine-learning tool and paired training data, fitting +the encoder schedule and building the underlying model in encoded space. -Positional Arguments - - `machine_learning_tool`: the selected machine learning tool object (e.g. Gaussian process / Random feature interface) - - `input_output_pairs`: the paired input-output data points stored in a `PairedDataContainer` +# Arguments -Keyword Arguments - - `encoder_schedule`[=`nothing`]: the schedule of data encoding/decoding. This will be passed into the method `create_encoder_schedule` internally. `nothing` sets sets a default schedule `[(decorrelate_sample_cov(), "in_and_out")]`, or `[(decorrelate_sample_cov(), "in"), (decorrelate_structure_mat(), "out")]` if an `encoder_kwargs` has a key `:obs_noise_cov`. Pass `[]` for no encoding. - - `encoder_kwargs`[=`NamedTuple()`]: a Dict or NamedTuple with keyword arguments to be passed to `initialize_and_encode_with_schedule!` -Other keywords are passed to the machine learning tool initialization +- `machine_learning_tool`: the `MachineLearningTool` to train (e.g. `GaussianProcess`, `ScalarRandomFeatureInterface`). +- `input_output_pairs`: training data as a `PairedDataContainer`. +- `encoder_schedule` (keyword, default `nothing`): encoding/decoding pipeline passed to + [`create_encoder_schedule`](@ref). `nothing` builds a default schedule using + [`decorrelate_sample_cov`](@ref) for both input and output, or + `(decorrelate_sample_cov(), decorrelate_structure_mat())` when `:obs_noise_cov` is + present in `encoder_kwargs`. Pass `[]` to disable encoding. +- `encoder_kwargs` (keyword, default `NamedTuple()`): forwarded to + [`initialize_and_encode_with_schedule!`](@ref). +- Additional keywords are forwarded to the machine-learning tool initialiser. """ function Emulator( machine_learning_tool::MachineLearningTool, @@ -284,22 +296,26 @@ end """ $(TYPEDSIGNATURES) -Makes a prediction using the emulator on new inputs (each new inputs given as data columns). +Return emulator predictions (mean and covariance) at `new_inputs`, where each input +is a column of the matrix. -Keyword args -- `encode` [=`nothing`]: For the input encoder `Eᵢ`, and output decoder `Dₒ` stored in the emulator, we have learnt a predict method method `G` in the encoded space. Interpret the keyword as follows: - - `nothing` : applies Dₒ∘G∘Eᵢ(x) (nothing is encoded) - most common for user interaction - - `"in"` : applies Dₒ∘G(z) (the inputs are provided as encoded (z=Eᵢx)) - - `"out"` : applies G∘Eᵢ(x) (the outputs are returned as encoded) - - `"in_and_out"`: applies G(z) (inputs (z=Eᵢx) and outputs are both encoded) - internally called by `Sample` method -- `add_obs_noise_cov`[=`false`]: When returning the prediction covariance, whether to add the observational noise - - `false`: Only return the uncertainty given by the machine learning tool - most common for user emulator validation - - `true` : Return the sum of emulator and observational uncertainty - internally called by `Sample` method -- All other kwargs are passed into the machine learning tool. +# Arguments -Return type of N inputs: (in the output space) - - 1-D: mean [1 x N], cov [1 x N] - - p-D: mean [p x N], cov N x [p x p] +- `emulator`: trained `Emulator` to query. +- `new_inputs`: `[input_dim × N]` matrix of query points. +- `encode` (keyword, default `nothing`): controls which encoding stages are applied. + Let `Eᵢ`/`Eₒ` be input/output encoders and `G` the model in encoded space: + - `nothing` → `Dₒ∘G∘Eᵢ(x)` — standard user-facing call. + - `"in"` → `Dₒ∘G(z)` — inputs already encoded as `z = Eᵢx`. + - `"out"` → `G∘Eᵢ(x)` — outputs returned in encoded space. + - `"in_and_out"` → `G(z)` — inputs encoded, outputs in encoded space (used internally by `sample`). +- `add_obs_noise_cov` (keyword, default `false`): when `true`, adds the stored + observational noise covariance to the returned uncertainty (used internally by `sample`). +- Additional keywords are forwarded to the machine-learning tool `predict` method. + +Returns `(mean, cov)` where for `N` inputs: +- 1-D output: `mean` is `[1 × N]`, `cov` is `[1 × N]` variances. +- p-D output: `mean` is `[p × N]`, `cov` is a length-N iterator of `[p × p]` covariance matrices. """ function predict( emulator::Emulator{FT}, @@ -401,18 +417,23 @@ end """ $(TYPEDSIGNATURES) -Constructor of the `ForwardMapWrapper` object. Behaves similarly to constructing the `Emulator` but additionally requires the `prior` parameter distribution. +Construct a [`ForwardMapWrapper`](@ref) from an explicit forward map, a prior, and +paired training data. Behaves similarly to the `Emulator` constructor but additionally +requires `prior` so that inputs can be constrained and null-space noise can be injected +when the encoder is lossy. -Positional Arguments - - `forward_map`: a function that represents the forward map `F(x)` mapping physical parameters (in constrained space) to outputs - - `prior`: a `ParameterDistribution` object describing the prior on the physical parameters. - - `input_output_pairs`: the paired input-output data points stored in a `PairedDataContainer` +# Arguments -Keyword Arguments - - `encoder_schedule`[=`nothing`]: the schedule of data encoding/decoding. This will be passed into the method `create_encoder_schedule` internally. `nothing` sets sets a default schedule `[(decorrelate_sample_cov(), "in_and_out")]`, or `[(decorrelate_sample_cov(), "in"), (decorrelate_structure_mat(), "out")]` if an `encoder_kwargs` has a key `:obs_noise_cov`. Pass `[]` for no encoding. - - `encoder_kwargs`[=`NamedTuple()`]: a Dict or NamedTuple with keyword arguments to be passed to `initialize_and_encode_with_schedule!` - - `noise_injector_threshold`[=`0.001`]: A threshold to implementing noise injection when decoding from an lossily encoded space. If the variance loss due to encoding is `>noise_injector_threshold` then additional noise is added to the null-space (consistent with the prior correlation structure). - - `noise_injector_scaling`[=`1.0`]: a multiplicative scaling that is applied to injected noise samples. (1.0 is "consistent" with Gaussian theory, but may cause instability in Non-Gaussian problems and can be reduced) +- `forward_map`: function `F(x)` mapping physical (constrained) parameters to model outputs. +- `prior`: `ParameterDistribution` describing the prior on physical parameters. +- `input_output_pairs`: training data as a `PairedDataContainer`. +- `encoder_schedule` (keyword, default `nothing`): encoding pipeline; see [`Emulator`](@ref) constructor for details. +- `encoder_kwargs` (keyword, default `NamedTuple()`): forwarded to [`initialize_and_encode_with_schedule!`](@ref). +- `noise_injector_threshold` (keyword, default `0.001`): if the variance lost by the + encoder exceeds this threshold, noise consistent with the prior is injected into the + null-space when decoding encoded MCMC samples. +- `noise_injector_scaling` (keyword, default `1.0`): multiplicative scale applied to + injected noise; values below 1.0 can improve robustness for non-Gaussian problems. """ function forward_map_wrapper( forward_map::Function, @@ -458,22 +479,24 @@ end """ $(TYPEDSIGNATURES) -Makes a prediction using the ForwardMapWrapper on new inputs (each new inputs given as data columns). - -Keyword args -- `encode` [=`nothing`]: For the output encoder `Eₒ`, and input decoder `Dᵢ` stored in the `ForwardMapWrapper`, we have provided the forward map `G` in the decoded space. Interpret the keyword as follows: - - `nothing` : applies G(x) (nothing is encoded) - most common for user interaction - - `"in"` : applies G∘Dᵢ(z) (the inputs are provided as encoded (x=Dᵢz)) - - `"out"` : applies Eₒ∘G(x) (the outputs are returned as encoded) - - `"in_and_out"`: applies Eₒ∘G∘Dᵢ(z) (inputs (x=Dᵢz) and outputs are both encoded) - internally called by `Sample` method -- `add_obs_noise_cov`[=`false`]: When returning the prediction covariance, whether to add the observational noise - - `false`: Only return the uncertainty given by the machine learning tool - most common for user emulator validation - - `true` : Return the sum of emulator and observational uncertainty - internally called by `Sample` method -- All other kwargs are passed into the machine learning tool. - -Return type of N inputs: (in the output space) - - 1-D: mean [1 x N], cov [1 x N] - - p-D: mean [p x N], cov N x [p x p] +Return predictions (mean and covariance) from the `ForwardMapWrapper` at `new_inputs`, +where each input is a column. The forward map runs in the physical (decoded) space and is +encoded/decoded as requested. + +# Arguments + +- `fmw`: `ForwardMapWrapper` to query. +- `new_inputs`: `[input_dim × N]` matrix of query points. +- `encode` (keyword, default `nothing`): controls encoding stages applied to inputs/outputs. + Let `Di`/`Eo` be input decoder/output encoder and `G` the forward map in decoded space: + - `nothing` → `G(x)` — standard user-facing call. + - `"in"` → `G∘Di(z)` — inputs are encoded as `z = Ei(x)`. + - `"out"` → `Eo∘G(x)` — outputs returned in encoded space. + - `"in_and_out"` → `Eo∘G∘Di(z)` — used internally by `sample`. +- `add_obs_noise_cov` (keyword, default `false`): when `true`, adds observational noise + covariance to the returned uncertainty (used internally by `sample`). + +Returns `(mean, cov)` with the same shape conventions as [`predict`](@ref). """ function predict( fmw::FMW, diff --git a/src/MachineLearningTools/GaussianProcess.jl b/src/MachineLearningTools/GaussianProcess.jl index 1cdd73e72..e35ba8858 100644 --- a/src/MachineLearningTools/GaussianProcess.jl +++ b/src/MachineLearningTools/GaussianProcess.jl @@ -35,7 +35,7 @@ export get_param_names """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type to dispatch which GP package to use: @@ -49,7 +49,7 @@ struct SKLJL <: GaussianProcessesPackage end struct AGPJL <: GaussianProcessesPackage end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Predict type for `GPJL` in GaussianProcesses.jl: - `YType` @@ -60,13 +60,13 @@ struct YType <: PredictionType end struct FType <: PredictionType end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Structure holding training input and the fitted Gaussian process regression models. # Fields -$(DocStringExtensions.TYPEDFIELDS) +$(TYPEDFIELDS) """ struct GaussianProcess{GPPackage, FT, VV <: AbstractVector} <: MachineLearningTool @@ -86,13 +86,17 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) - - `package` - GaussianProcessPackage object. - - `kernel` - GaussianProcesses kernel object. Default is a Squared Exponential kernel. - - `noise_learn` - Boolean to additionally learn white noise in decorrelated space. Default is true. - - `alg_reg_noise` - Float to fix the (small) regularization parameter of algorithms when `noise_learn = true` - - `prediction_type` - PredictionType object. Default predicts data, not latent function (FType()). +Construct a `GaussianProcess` for the chosen backend `package`. + +# Arguments + +- `package`: one of `GPJL`, `SKLJL`, or `AGPJL` to select the GP backend. +- `kernel`: kernel object compatible with the chosen backend. Defaults to a squared-exponential kernel. +- `noise_learn`: if `true`, learns additive white noise via the kernel. Default `true`. +- `alg_reg_noise`: small regularisation added by the fitting algorithm when `noise_learn = true`. Default `1e-3`. +- `prediction_type`: `YType()` (predict observations) or `FType()` (predict latent function). Default `YType()`. """ function GaussianProcess( package::GPPkg; @@ -131,14 +135,20 @@ end # First we create the GPJL implementation """ -Gets flattened kernel hyperparameters from a (vector of) `GaussianProcess{GPJL}` model(s). Extends GaussianProcess.jl method. +$(TYPEDSIGNATURES) + +Return the flattened kernel hyperparameters from each model in `gp`. Extends the +`GaussianProcesses.jl` method for the `GPJL` backend. """ function GaussianProcesses.get_params(gp::GaussianProcess{GPJL}) return [get_params(model.kernel) for model in gp.models] end """ -Gets the flattened names of kernel hyperparameters from a (vector of) `GaussianProcess{GPJL}` model(s). Extends GaussianProcess.jl method. +$(TYPEDSIGNATURES) + +Return the flattened names of kernel hyperparameters for each model in `gp`. Extends the +`GaussianProcesses.jl` method for the `GPJL` backend. """ function GaussianProcesses.get_param_names(gp::GaussianProcess{GPJL}) return [get_param_names(model.kernel) for model in gp.models] @@ -146,7 +156,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Method to build Gaussian process models based on the package. """ @@ -228,7 +238,7 @@ function build_models!( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Optimize Gaussian process hyperparameters using in-build package method. @@ -282,7 +292,7 @@ predict(gp::GaussianProcess{GPJL}, new_inputs::AbstractMatrix{FT}, ::FType) wher _predict(gp, new_inputs, GaussianProcesses.predict_f) """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Predict means and covariances in decorrelated output space using Gaussian process models. The use of stored `FType` and `YType` to control this method is deprecated, the return covariance is now determined by the `predict(` kwarg `add_obs_noise_cov` """ diff --git a/src/MachineLearningTools/RandomFeature.jl b/src/MachineLearningTools/RandomFeature.jl index 4383d7f56..63e72cf6f 100644 --- a/src/MachineLearningTools/RandomFeature.jl +++ b/src/MachineLearningTools/RandomFeature.jl @@ -28,17 +28,23 @@ struct EnsembleThreading <: MultithreadType end # Different types of covariance representation in the prior +""" +Abstract supertype for covariance structure parameterisations used in random-feature +kernels. Concrete subtypes — [`OneDimFactor`](@ref), [`DiagonalFactor`](@ref), +[`CholeskyFactor`](@ref), [`LowRankFactor`](@ref), [`HierarchicalLowRankFactor`](@ref) +— control the number of hyperparameters and the shape of the feature distribution. +""" abstract type CovarianceStructureType end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) covariance structure for a one-dimensional space """ struct OneDimFactor <: CovarianceStructureType end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) builds a diagonal covariance structure """ @@ -47,10 +53,17 @@ struct DiagonalFactor{FT <: AbstractFloat} <: CovarianceStructureType eps::FT end DiagonalFactor() = DiagonalFactor(Float64(1.0)) + +""" +$(TYPEDSIGNATURES) + +Return the nugget regularisation term `eps` added to the diagonal of the covariance matrix +to ensure positive-definiteness. +""" get_eps(df::DiagonalFactor) = df.eps """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) builds a general positive-definite covariance structure """ @@ -62,7 +75,7 @@ CholeskyFactor() = CholeskyFactor(Float64(1.0)) get_eps(cf::CholeskyFactor) = cf.eps """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) builds a covariance structure that deviates from the identity with a low-rank perturbation. This perturbation is diagonalized in the low-rank space """ @@ -77,7 +90,7 @@ get_eps(lrf::LowRankFactor) = lrf.eps """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) builds a covariance structure that deviates from the identity with a more general low-rank perturbation """ @@ -102,7 +115,7 @@ rank(hlrf::HierarchicalLowRankFactor) = hlrf.rank # and add a string id to cov_structure_from_string """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) creates some simple covariance structures from strings: ["onedim", "diagonal", "cholesky", "lowrank", hierlowrank"]. See the covariance Structures for more details. """ @@ -132,7 +145,7 @@ cov_structure_from_string(cst::CST, args...; kwargs...) where {CST <: Covariance abstract type KernelStructureType end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Builds a separable kernel, i.e. one that accounts for input and output covariance structure separately """ @@ -140,25 +153,41 @@ struct SeparableKernel{CST1 <: CovarianceStructureType, CST2 <: CovarianceStruct input_cov_structure::CST1 output_cov_structure::CST2 end +""" +$(TYPEDSIGNATURES) + +Return the input-space `CovarianceStructureType` stored in `kernel_structure`. +""" get_input_cov_structure(kernel_structure::SeparableKernel) = kernel_structure.input_cov_structure + +""" +$(TYPEDSIGNATURES) + +Return the output-space `CovarianceStructureType` stored in `kernel_structure`. +""" get_output_cov_structure(kernel_structure::SeparableKernel) = kernel_structure.output_cov_structure """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Builds a nonseparable kernel, i.e. one that accounts for a joint input and output covariance structure """ struct NonseparableKernel{CST <: CovarianceStructureType} <: KernelStructureType cov_structure::CST end +""" +$(TYPEDSIGNATURES) + +Return the joint-space `CovarianceStructureType` stored in `kernel_structure`. +""" get_cov_structure(kernel_structure::NonseparableKernel) = kernel_structure.cov_structure # calculate_n_hyperparameters """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) calculates the number of hyperparameters generated by the choice of covariance structure """ @@ -173,7 +202,7 @@ calculate_n_hyperparameters(d::Int, hlrf::HierarchicalLowRankFactor) = """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) reshapes a list of hyperparameters into a covariance matrix based on the selected structure """ @@ -184,7 +213,7 @@ function hyperparameters_from_flat(x::V, df::DiagonalFactor) where {V <: Abstrac end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Converts a flat array into a cholesky factor. """ @@ -328,7 +357,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Builds a prior distribution for the kernel hyperparameters to initialize optimization. The parameter distributions built from these priors will be scaled such that the input and output range of the data is O(1). """ @@ -428,7 +457,7 @@ function hyperparameters_from_flat( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Calculates key quantities (mean, covariance, and coefficients) of the objective function to be optimized for hyperparameter training. Buffers: mean_store, cov_store, buffer are provided to aid memory management. """ @@ -518,7 +547,7 @@ function calculate_mean_cov_and_coeffs( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Calculate the empirical covariance, additionally applying a shrinkage operator (here the Ledoit Wolf 2004 shrinkage operation). Known to have better stability properties than Monte-Carlo for low sample sizes """ @@ -570,7 +599,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Calculate the empirical covariance, additionally applying the Noise Informed Covariance Estimator (NICE) Vishnay et al. 2024. """ @@ -646,7 +675,7 @@ function correct_covariance( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Estimation of objective function (marginal log-likelihood) variability for optimization of hyperparameters. Calculated empirically by calling many samples of the objective at a "reasonable" value. `multithread_type` is used as dispatch, either `ensemble` (thread over the empirical samples) or `tullio` (serial samples, and thread the linear algebra within each sample) """ @@ -751,7 +780,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Evaluate an objective function (marginal log-likelihood) over an ensemble of hyperparameter values (used for EKI). `multithread_type` is used as dispatch, either `ensemble` (thread over the ensemble members) or `tullio` (serial samples, and thread the linear algebra within each sample) """ diff --git a/src/MachineLearningTools/ScalarRandomFeature.jl b/src/MachineLearningTools/ScalarRandomFeature.jl index 3b610551e..14664a6c8 100644 --- a/src/MachineLearningTools/ScalarRandomFeature.jl +++ b/src/MachineLearningTools/ScalarRandomFeature.jl @@ -2,12 +2,12 @@ export ScalarRandomFeatureInterface # getters already exported in VRFI """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Structure holding the Scalar Random Feature models. # Fields -$(DocStringExtensions.TYPEDFIELDS) +$(TYPEDFIELDS) """ struct ScalarRandomFeatureInterface{S <: AbstractString, RNG <: AbstractRNG, KST <: KernelStructureType} <: @@ -37,49 +37,49 @@ struct ScalarRandomFeatureInterface{S <: AbstractString, RNG <: AbstractRNG, KST end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the rfms field """ get_rfms(srfi::ScalarRandomFeatureInterface) = srfi.rfms """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the fitted_features field """ get_fitted_features(srfi::ScalarRandomFeatureInterface) = srfi.fitted_features """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets batch_sizes the field """ get_batch_sizes(srfi::ScalarRandomFeatureInterface) = srfi.batch_sizes """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the n_features field """ get_n_features(srfi::ScalarRandomFeatureInterface) = srfi.n_features """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the input_dim field """ get_input_dim(srfi::ScalarRandomFeatureInterface) = srfi.input_dim """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the rng field """ EKP.get_rng(srfi::ScalarRandomFeatureInterface) = srfi.rng """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the regularization field """ @@ -87,28 +87,28 @@ get_regularization(srfi::ScalarRandomFeatureInterface) = srfi.regularization """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the kernel_structure field """ get_kernel_structure(srfi::ScalarRandomFeatureInterface) = srfi.kernel_structure """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the feature_decomposition field """ get_feature_decomposition(srfi::ScalarRandomFeatureInterface) = srfi.feature_decomposition """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the optimizer_options field """ get_optimizer_options(srfi::ScalarRandomFeatureInterface) = srfi.optimizer_options """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the optimizer field """ @@ -116,7 +116,7 @@ get_optimizer(srfi::ScalarRandomFeatureInterface) = srfi.optimizer """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Constructs a `ScalarRandomFeatureInterface <: MachineLearningTool` interface for the `RandomFeatures.jl` package for multi-input and single- (or decorrelated-)output emulators. - `n_features` - the number of random features @@ -126,20 +126,20 @@ Constructs a `ScalarRandomFeatureInterface <: MachineLearningTool` interface for - `rng = Random.GLOBAL_RNG` - random number generator - `feature_decomposition = "cholesky"` - choice of how to store decompositions of random features, `cholesky` or `svd` available - `optimizer_options = nothing` - Dict of options to pass into EKI optimization of hyperparameters (defaults created in `ScalarRandomFeatureInterface` constructor): - - "prior": the prior for the hyperparameter optimization - - "n_ensemble": number of ensemble members - - "n_iteration": number of eki iterations - - "cov_sample_multiplier": increase for more samples to estimate covariance matrix in optimization (default 10.0, minimum 0.0) - - "scheduler": Learning rate Scheduler (a.k.a. EKP timestepper) Default: DataMisfitController - - "inflation": additive inflation ∈ [0,1] with 0 being no inflation - - "train_fraction": e.g. 0.8 (default) means 80:20 train - test split - - "n_features_opt": fix the number of features for optimization (default `n_features`, as used for prediction) - - "multithread": how to multithread. "ensemble" (default) threads across ensemble members "tullio" threads random feature matrix algebra - - "accelerator": use EKP accelerators (default is no acceleration) - - "verbose" => false: verbose optimizer statements - - "cov_correction" => "nice": type of conditioning to improve estimated covariance. "shrinkage", "shrinkage_corr" (Ledoit Wolfe 03), "nice" for (Vishny, Morzfeld et al. 2024) - - "overfit" => 1.0: if > 1.0 forcibly overfit/under-regularize the optimizer cost, (vice versa for < 1.0). - - "n_cross_val_sets" => 2: train fraction creates (default 5) train-test data subsets, then use 'n_cross_val_sets' of these stacked in the loss function. If set to 0, train=test on the full data provided ignoring "train_fraction". + - `"prior"`: the prior for the hyperparameter optimization + - `"n_ensemble"`: number of ensemble members + - `"n_iteration"`: number of eki iterations + - `"cov_sample_multiplier"`: increase for more samples to estimate covariance matrix in optimization (default 10.0, minimum 0.0) + - `"scheduler"`: Learning rate Scheduler (a.k.a. EKP timestepper) Default: DataMisfitController + - `"inflation"`: additive inflation ∈ [0,1] with 0 being no inflation + - `"train_fraction"`: e.g. 0.8 (default) means 80:20 train - test split + - `"n_features_opt"`: fix the number of features for optimization (default `n_features`, as used for prediction) + - `"multithread"`: how to multithread. "ensemble" (default) threads across ensemble members "tullio" threads random feature matrix algebra + - `"accelerator"`: use EKP accelerators (default is no acceleration) + - `"verbose"` => false: verbose optimizer statements + - `"cov_correction"` => "nice": type of conditioning to improve estimated covariance. "shrinkage", "shrinkage_corr" (Ledoit Wolfe 03), "nice" for (Vishny, Morzfeld et al. 2024) + - `"overfit"` => 1.0: if > 1.0 forcibly overfit/under-regularize the optimizer cost, (vice versa for < 1.0). + - `"n_cross_val_sets"` => 2: train fraction creates (default 5) train-test data subsets, then use 'n_cross_val_sets' of these stacked in the loss function. If set to 0, train=test on the full data provided ignoring "train_fraction". """ function ScalarRandomFeatureInterface( n_features::Int, @@ -253,7 +253,7 @@ function hyperparameter_distribution_from_flat( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Builds the random feature method from hyperparameters. We use cosine activation functions and a MatrixVariateNormal(M,U,V) distribution (from `Distributions.jl`) with mean M=0, and input covariance U built using a `CovarianceStructureType`. """ @@ -330,7 +330,7 @@ RFM_from_hyperparameters( ) """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Builds the random feature method from hyperparameters. We use cosine activation functions and a Multivariate Normal distribution (from `Distributions.jl`) with mean M=0, and input covariance U built with the `CovarianceStructureType`. """ @@ -614,7 +614,7 @@ function build_models!( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Empty method, as optimization takes place within the build_models stage """ @@ -623,7 +623,7 @@ function optimize_hyperparameters!(srfi::ScalarRandomFeatureInterface, args...; end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Prediction of emulator mean at new inputs (passed in as columns in a matrix), and a prediction of the total covariance at new inputs equal to (emulator covariance + noise covariance). """ diff --git a/src/MachineLearningTools/VectorRandomFeature.jl b/src/MachineLearningTools/VectorRandomFeature.jl index f3ab4d8bb..cd19f1147 100644 --- a/src/MachineLearningTools/VectorRandomFeature.jl +++ b/src/MachineLearningTools/VectorRandomFeature.jl @@ -13,12 +13,12 @@ export get_rfms, get_optimizer """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Structure holding the Vector Random Feature models. # Fields -$(DocStringExtensions.TYPEDFIELDS) +$(TYPEDFIELDS) """ struct VectorRandomFeatureInterface{S <: AbstractString, RNG <: AbstractRNG, KST <: KernelStructureType} <: @@ -50,91 +50,91 @@ struct VectorRandomFeatureInterface{S <: AbstractString, RNG <: AbstractRNG, KST end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the rfms field """ get_rfms(vrfi::VectorRandomFeatureInterface) = vrfi.rfms """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the fitted_features field """ get_fitted_features(vrfi::VectorRandomFeatureInterface) = vrfi.fitted_features """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the batch_sizes field """ get_batch_sizes(vrfi::VectorRandomFeatureInterface) = vrfi.batch_sizes """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the n_features field """ get_n_features(vrfi::VectorRandomFeatureInterface) = vrfi.n_features """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the input_dim field """ get_input_dim(vrfi::VectorRandomFeatureInterface) = vrfi.input_dim """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the output_dim field """ get_output_dim(vrfi::VectorRandomFeatureInterface) = vrfi.output_dim """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the rng field """ EKP.get_rng(vrfi::VectorRandomFeatureInterface) = vrfi.rng """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the regularization field """ get_regularization(vrfi::VectorRandomFeatureInterface) = vrfi.regularization """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the kernel_structure field """ get_kernel_structure(vrfi::VectorRandomFeatureInterface) = vrfi.kernel_structure """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the feature_decomposition field """ get_feature_decomposition(vrfi::VectorRandomFeatureInterface) = vrfi.feature_decomposition """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Gets the optimizer_options field """ get_optimizer_options(vrfi::VectorRandomFeatureInterface) = vrfi.optimizer_options """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) gets the optimizer field """ get_optimizer(vrfi::VectorRandomFeatureInterface) = vrfi.optimizer """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Constructs a `VectorRandomFeatureInterface <: MachineLearningTool` interface for the `RandomFeatures.jl` package for multi-input and multi-output emulators. - `n_features` - the number of random features @@ -145,21 +145,21 @@ Constructs a `VectorRandomFeatureInterface <: MachineLearningTool` interface for - `rng = Random.GLOBAL_RNG` - random number generator - `feature_decomposition = "cholesky"` - choice of how to store decompositions of random features, `cholesky` or `svd` available - `optimizer_options = nothing` - Dict of options to pass into EKI optimization of hyperparameters (defaults created in `VectorRandomFeatureInterface` constructor): - - "prior": the prior for the hyperparameter optimization + - `"prior"`: the prior for the hyperparameter optimization - "prior_in_scale"/"prior_out_scale": use these to tune the input/output prior scale. - - "n_ensemble": number of ensemble members - - "n_iteration": number of eki iterations - - "scheduler": Learning rate Scheduler (a.k.a. EKP timestepper) Default: DataMisfitController - - "cov_sample_multiplier": increase for more samples to estimate covariance matrix in optimization (default 10.0, minimum 0.0) - - "inflation": additive inflation ∈ [0,1] with 0 being no inflation - - "train_fraction": e.g. 0.8 (default) means 80:20 train - test split - - "n_features_opt": fix the number of features for optimization (default `n_features`, as used for prediction) - - "multithread": how to multithread. "ensemble" (default) threads across ensemble members "tullio" threads random feature matrix algebra - - "accelerator": use EKP accelerators (default is no acceleration) - - "verbose" => false, verbose optimizer statements to check convergence, priors and optimal parameters. - - "cov_correction" => "nice": type of conditioning to improve estimated covariance. "shrinkage", "shrinkage_corr" (Ledoit Wolfe 03), "nice" for (Vishny, Morzfeld et al. 2024) - - "overfit" => 1.0: if > 1.0 forcibly overfit/under-regularize the optimizer cost, (vice versa for < 1.0). - - "n_cross_val_sets" => 2, train fraction creates (default 5) train-test data subsets, then use 'n_cross_val_sets' of these stacked in the loss function. If set to 0, train=test on the full data provided ignoring "train_fraction". + - `"n_ensemble"`: number of ensemble members + - `"n_iteration"`: number of eki iterations + - `"scheduler"`: Learning rate Scheduler (a.k.a. EKP timestepper) Default: DataMisfitController + - `"cov_sample_multiplier"`: increase for more samples to estimate covariance matrix in optimization (default 10.0, minimum 0.0) + - `"inflation"`: additive inflation ∈ [0,1] with 0 being no inflation + - `"train_fraction"`: e.g. 0.8 (default) means 80:20 train - test split + - `"n_features_opt"`: fix the number of features for optimization (default `n_features`, as used for prediction) + - `"multithread"`: how to multithread. "ensemble" (default) threads across ensemble members "tullio" threads random feature matrix algebra + - `"accelerator"`: use EKP accelerators (default is no acceleration) + - `"verbose"` => false, verbose optimizer statements to check convergence, priors and optimal parameters. + - `"cov_correction"` => "nice": type of conditioning to improve estimated covariance. "shrinkage", "shrinkage_corr" (Ledoit Wolfe 03), "nice" for (Vishny, Morzfeld et al. 2024) + - `"overfit"` => 1.0: if > 1.0 forcibly overfit/under-regularize the optimizer cost, (vice versa for < 1.0). + - `"n_cross_val_sets"` => 2, train fraction creates (default 5) train-test data subsets, then use 'n_cross_val_sets' of these stacked in the loss function. If set to 0, train=test on the full data provided ignoring "train_fraction". """ function VectorRandomFeatureInterface( n_features::Int, @@ -300,7 +300,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Builds the random feature method from hyperparameters. We use cosine activation functions and a Matrixvariate Normal distribution with mean M=0, and input(output) covariance U(V) built with a `CovarianceStructureType`. """ @@ -355,7 +355,7 @@ end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Build Vector Random Feature model for the input-output pairs subject to regularization, and optimizes the hyperparameters with EKP. """ @@ -643,7 +643,7 @@ function build_models!( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Empty method, as optimization takes place within the build_models stage """ @@ -652,7 +652,7 @@ function optimize_hyperparameters!(vrfi::VectorRandomFeatureInterface, args...; end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Prediction of data observation (not latent function) at new inputs (passed in as columns in a matrix). That is, we add the observational noise into predictions. """ diff --git a/src/MarkovChainMonteCarlo.jl b/src/MarkovChainMonteCarlo.jl index 528ab23a3..72897b0df 100644 --- a/src/MarkovChainMonteCarlo.jl +++ b/src/MarkovChainMonteCarlo.jl @@ -52,28 +52,28 @@ export EmulatorPosteriorModel, # some possible types of autodiff used """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type used to dispatch different autodifferentiation methods where different emulators have a different compatability with autodiff packages """ abstract type AutodiffProtocol end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type to construct samplers for emulators not compatible with autodifferentiation """ abstract type GradFreeProtocol <: AutodiffProtocol end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type to construct samplers for emulators compatible with `ForwardDiff.jl` autodifferentiation """ abstract type ForwardDiffProtocol <: AutodiffProtocol end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type to construct samplers for emulators compatible with `ReverseDiff.jl` autodifferentiation """ @@ -95,7 +95,7 @@ end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Type used to dispatch different methods of the [`MetropolisHastingsSampler`](@ref) constructor, corresponding to different sampling algorithms. @@ -103,7 +103,7 @@ constructor, corresponding to different sampling algorithms. abstract type MCMCProtocol end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) [`MCMCProtocol`](@ref) which uses Metropolis-Hastings sampling that generates proposals for new parameters as as vanilla random walk, based on the covariance of `prior`. @@ -123,7 +123,7 @@ AdvancedMH.logratio_proposal_density( ) = AdvancedMH.logratio_proposal_density(sampler.proposal, transition_prev.params, candidate) """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Constructor for all `Sampler` objects, with one method for each supported MCMC algorithm. @@ -147,7 +147,7 @@ function MetropolisHastingsSampler( end """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) [`MCMCProtocol`](@ref) which uses Metropolis-Hastings sampling that generates proposals for new parameters according to the preconditioned Crank-Nicholson (pCN) algorithm, which is @@ -178,7 +178,7 @@ end #------ The following are gradient-based samplers """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) [`MCMCProtocol`](@ref) which uses Metropolis-Hastings sampling that generates proposals for new parameters according to the Barker proposal. @@ -251,7 +251,7 @@ autodiff_hessian(model::AdvancedMH.DensityModel, params, sampler::MH) where {MH # Use emulated model in sampler """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Defines the internal log-density function over a vector of observation samples using an assumed conditionally indepedent likelihood, that is with a log-likelihood of `ℓ(y,θ) = sum^n_i log( p(y_i|θ) )`. @@ -281,7 +281,7 @@ function emulator_log_density_model( end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Factory which constructs `AdvancedMH.DensityModel` objects given an (Encoded) prior on the model parameters (`encoded_prior`) and an [`Emulator`](@ref) of the log-likelihood of the data given @@ -301,14 +301,14 @@ end # Record MH accept/reject decision in MCMCState object """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Extends the `AdvancedMH.Transition` (which encodes the current state of the MC during sampling) with a boolean flag to record whether this state is new (arising from accepting a Metropolis-Hastings proposal) or old (from rejecting a proposal). # Fields -$(DocStringExtensions.TYPEDFIELDS) +$(TYPEDFIELDS) """ struct MCMCState{T, L <: Real} <: AdvancedMH.AbstractTransition "Sampled value of the parameters at the current state of the MCMC chain." @@ -510,14 +510,14 @@ end # Top-level object to contain model and sampler (but not state) """ -$(DocStringExtensions.TYPEDEF) +$(TYPEDEF) Top-level class holding all configuration information needed for MCMC sampling: the prior, emulated likelihood and sampling algorithm (`DensityModel` and `Sampler`, respectively, in AbstractMCMC's terminology). # Fields -$(DocStringExtensions.TYPEDFIELDS) +$(TYPEDFIELDS) """ struct MCMCWrapper{VV1 <: AbstractVector, VV2 <: AbstractVector, VV3 <: AbstractVector} "[`ParameterDistribution`](https://clima.github.io/EnsembleKalmanProcesses.jl/dev/parameter_distributions/) object describing the prior distribution on parameter values." @@ -554,7 +554,7 @@ get_encoder_schedule(mcmc::MCMCWrapper) = mcmc.encoder_schedule """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Constructor for [`MCMCWrapper`](@ref) that will perform an MCMC sampling in the encoded space given by `em_or_fmw` (`Emulator` or `ForwardMapWrapper`). It creates and wraps an instance of [`EmulatorPosteriorModel`](@ref), for sampling from the Emulator (predicting means and covariances), and @@ -675,37 +675,16 @@ function MCMCWrapper( return MCMCWrapper(mcmc_alg, observations, prior, em_or_fmw; kwargs...) end """ - $(DocStringExtensions.FUNCTIONNAME)([rng,] mcmc::MCMCWrapper, args...; kwargs...) - -Extends the `sample` methods of AbstractMCMC (which extends StatsBase) to sample from the -emulated posterior, using the MCMC sampling algorithm and [`Emulator`](@ref) configured in -[`MCMCWrapper`](@ref). Returns a [`MCMCChains.Chains`](https://beta.turing.ml/MCMCChains.jl/dev/) -object containing the samples. - -Supported methods are: - -- `sample([rng, ]mcmc, N; kwargs...)` - - Return a `MCMCChains.Chains` object containing `N` samples from the emulated posterior. - -- `sample([rng, ]mcmc, isdone; kwargs...)` - - Sample from the `model` with the Markov chain Monte Carlo `sampler` until a convergence - criterion `isdone` returns `true`, and return the samples. The function `isdone` has the - signature - - ```julia - isdone(rng, model, sampler, samples, state, iteration; kwargs...) - ``` +$(TYPEDSIGNATURES) - where `state` and `iteration` are the current state and iteration of the sampler, - respectively. It should return `true` when sampling should end, and `false` otherwise. +Extend the `AbstractMCMC.sample` interface to draw from the emulated posterior configured +in `mcmc`. Returns a `MCMCChains.Chains` object. -- `sample([rng, ]mcmc, parallel_type, N, nchains; kwargs...)` +Supported call patterns: - Sample `nchains` Monte Carlo Markov chains in parallel according to `parallel_type`, which - may be `MCMCThreads()` or `MCMCDistributed()` for thread and parallel sampling, - respectively. +- `sample([rng, ]mcmc, N; kwargs...)` — draw exactly `N` samples. +- `sample([rng, ]mcmc, isdone; kwargs...)` — sample until `isdone(rng, model, sampler, samples, state, iteration; kwargs...)` returns `true`. +- `sample([rng, ]mcmc, parallel_type, N, nchains; kwargs...)` — run `nchains` chains in parallel via `MCMCThreads()` or `MCMCDistributed()`. """ function sample(rng::Random.AbstractRNG, mcmc::MCMCWrapper, args...; kwargs...) # any explicit function kwargs override defaults in mcmc object @@ -719,7 +698,7 @@ sample(mcmc::MCMCWrapper, args...; kwargs...) = sample(Random.GLOBAL_RNG, mcmc, # Search for a MCMC stepsize that yields a good MH acceptance rate """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Fraction of MC proposals in `chain` which were accepted (according to Metropolis-Hastings.) """ @@ -750,7 +729,7 @@ function _find_mcmc_step_log(it, stepsize, acc_ratio, chain::MCMCChains.Chains) end """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Uses a heuristic to return a stepsize for the `mh_proposal_sampler` element of [`MCMCWrapper`](@ref) which yields fast convergence of the Markov chain. @@ -812,7 +791,7 @@ end optimize_stepsize(mcmc::MCMCWrapper; kwargs...) = optimize_stepsize(Random.GLOBAL_RNG, mcmc; kwargs...) """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Returns a `ParameterDistribution` object corresponding to the empirical distribution of the samples in `chain`. @@ -871,7 +850,7 @@ end # other diagnostic utilities """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Computes the expected squared jump distance of the chain. """ diff --git a/src/Utilities.jl b/src/Utilities.jl index 2ebce7011..31183149d 100644 --- a/src/Utilities.jl +++ b/src/Utilities.jl @@ -43,14 +43,17 @@ const StructureMatrix = Union{UniformScaling, AbstractMatrix, AbstractVector, Li const StructureVector = Union{AbstractVector, AbstractMatrix} # In case of a matrix, the columns should be seen as vectors """ -$(DocStringExtensions.TYPEDSIGNATURES) +$(TYPEDSIGNATURES) + +Extract and flatten the training data from an `EnsembleKalmanProcess` into a +`PairedDataContainer` suitable for training an `Emulator`. -Extract and flatten the training points needed to train an Emulator. returned as a `PairedDataContainer` +# Arguments -- `ekp` - EnsembleKalmanProcess holding the parameters and the data that were produced - during the Ensemble Kalman (EK) process. -- `train_iterations` - Number (e.g. 1:train_iterations), or indices train_iterations=`3:2:9`, for EKP iterations to extract. -- `g_final[=nothing]` - EKP will typically store one extra input data iteration. If desired, the user can add output data for this final iteration directly with `g_final`. It should be of type `<: AbstractMatrix`, sized consistently as with return values from `get_g(ekp,1)`. +- `ekp`: `EnsembleKalmanProcess` holding the parameter ensemble and forward-model outputs. +- `train_iterations`: integer `n` (uses iterations `1:n`) or an index vector such as `3:2:9`. +- `g_final` (keyword, default `nothing`): optional `AbstractMatrix` of forward-model outputs + for the final parameter ensemble (not yet stored in `ekp`), sized as `get_g(ekp, 1)`. """ function get_training_points( ekp::EKP.EnsembleKalmanProcess{FT, IT, P}, @@ -243,24 +246,21 @@ end """ $(TYPEDSIGNATURES) -Produces a linear map of type `LinearMap` that can evaluates the stacked actions of the structure matrix in compact form. by calling say `linear_map.f(x)` or `linear_map.fc(x)` to obtain `Ax`, or `A'x`. This particular type can be used by packages like `TSVD.jl` or `IterativeSolvers.jl` for further computations. +Return a `LinearMap` that evaluates the action `A*x` (and `A'*x`) of a structure matrix +`A` in compact SVD+diagonal form, without building the full dense matrix. The map can be +used directly with `TSVD.jl`, `IterativeSolvers.jl`, and similar packages. -This compact map constructs the following form of the Linear map f: +Internally decomposes each block as `U * S * Vt + D` and stacks the results. -1. get compact form svd-plus-d form "USVt + D" of the `blocks` -2. create the f via stacking `A.U * A.S * A.Vt * xblock + A.D * xblock for (A,xblock) in (As, x)` +# Arguments -kwargs: ------- -When computing the svd internally from an abstract matrix -- `svd_dim_max=3000`: this switches to an approximate svd approach when applying to covariance matrices above dimension 3000 -- `psvd_or_tsvd="psvd"`: use psvd or tsvd for approximating svd for large matrices -- `tsvd_max_rank=50`: when using tsvd, what max rank to use. high rank = higher accuracy -- `psvd_kwargs=(; rtol=1e-2)`: when using psvd, what kwargs to pass. lower rtol = higher accuracy - -Recommended: quick & inaccurate -> slow and more accurate -- very large matrices - start with tsvd with very low rank, and increase -- mid-size matrices - psvd with very high rtol, and decrease +- `A`: structure matrix (or vector of structure matrices for a block-diagonal layout). +- `svd_dim_max` (keyword, default `3000`): matrices larger than this dimension use an + approximate SVD instead of the exact `LinearAlgebra.svd`. +- `psvd_or_tsvd` (keyword, default `"psvd"`): approximation algorithm for large matrices; + `"psvd"` (randomised, `LowRankApprox.jl`) or `"tsvd"` (truncated, `TSVD.jl`). +- `tsvd_max_rank` (keyword, default `50`): maximum rank when using `"tsvd"`. +- `psvd_kwargs` (keyword, default `(; rtol=1e-1)`): keyword arguments forwarded to `psvd`. """ function create_compact_linear_map( A; @@ -355,13 +355,16 @@ end """ $(TYPEDSIGNATURES) -Approximately computes the norm of a `LinearMap` object. For `Amap` associated with matrix `A`, `norm_linear_map(Amap,p)≈norm(A,p)`. Can be aliased as `norm()` +Approximate the `p`-norm of a `LinearMap` `A` using random matrix–vector products, +satisfying `norm_linear_map(A, p) ≈ norm(Matrix(A), p)`. Can be called via `norm(A, p)`. -kwargs ------- -- n_eval(=nothing): number of mat-vec products to apply in the approximation (larger is more accurate). default performs `size(map,2)` products -- rng(=Random.default_rng()): random number generator +# Arguments +- `A`: the `LinearMap` to evaluate. +- `p` (default `2`): norm order. +- `n_eval` (keyword, default `nothing`): number of matrix–vector products; defaults to + `size(A, 2)` (exact for `p=2`, approximate otherwise). +- `rng` (keyword, default `Random.default_rng()`): random number generator. """ function norm_linear_map(A::LM, p::Real = 2; n_eval = nothing, rng = Random.default_rng()) where {LM <: LinearMap} m, n = size(A) @@ -384,22 +387,37 @@ LinearAlgebra.norm(A::LM, p::Real = 2; lm_kwargs...) where {LM <: LinearMap} = n # Data processing tooling: abstract type DataProcessor end -abstract type PairedDataContainerProcessor <: DataProcessor end # tools that operate on inputs and outputs -abstract type DataContainerProcessor <: DataProcessor end # tools that operate on only inputs or outputs + +""" +Abstract supertype for processors that operate jointly on input–output data pairs +(e.g. [`CanonicalCorrelation`](@ref), [`LikelihoodInformed`](@ref)). +""" +abstract type PairedDataContainerProcessor <: DataProcessor end + +""" +Abstract supertype for processors that operate independently on one data container +— either inputs or outputs — (e.g. [`Decorrelator`](@ref), [`ElementwiseScaler`](@ref)). +""" +abstract type DataContainerProcessor <: DataProcessor end # define how to have equality # this gets messy with LinearMaps, """ $(TYPEDSIGNATURES) -Tests equality for a LinearMap on a standard basis of the input space. Note that this operation requires a matrix multiply per input dimension so can be expensive. +Test whether two `LinearMap`s `A` and `B` act identically on a standard basis of the +input space. Requires one matrix–vector product per basis vector tested. + +# Arguments -Kwargs: -------- -- n_eval (=nothing): the number of basis vectors to compare against (randomly selected without replacement if `n_eval < size(A,1)`) -- tol (=2*eps()): the tolerance for equality on evaluation per entry -- rng (=default_rng()): When provided, and `n_eval < size(A,1)`; a random subset of the basis is compared, using this `rng`. -- up_to_sign(=false): Only assess equality up to a sign-error (sufficient for e.g. encoder/decoder matrices) +- `A`, `B`: `LinearMap`s to compare; must have compatible sizes. +- `n_eval` (keyword, default `nothing`): number of basis vectors to compare; when less + than `size(A, 2)`, a random subset is used. +- `tol` (keyword, default `2*eps()`): per-entry absolute tolerance for equality. +- `rng` (keyword, default `Random.default_rng()`): random number generator used when + `n_eval < size(A, 2)`. +- `up_to_sign` (keyword, default `false`): when `true`, equality is checked up to + componentwise sign differences (sufficient for comparing encoder/decoder matrices). """ function isequal_linear( A::LM1, @@ -588,7 +606,7 @@ function _initialize_and_encode_data!( end """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Create a flatter encoder schedule for the from the user's proposed schedule of the form: @@ -638,7 +656,7 @@ create_encoder_schedule(schedule_in::TT) where {TT <: Tuple} = create_encoder_sc # Functions to encode/decode with uninitialized schedule (require structure matrices as input) """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in the created encoder schedule (See [`create_encoder_schedule`](@ref)), and initializes it, and encodes the paired data container, and structure matrices with it. """ @@ -739,7 +757,7 @@ end # Functions to encode/decode with initialized schedule """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in an already initialized encoder schedule, and encodes a `DataContainer`, the `in_or_out` string indicates if the data is input `"in"` or output `"out"` data (and thus encoded differently) """ @@ -765,7 +783,7 @@ function encode_with_schedule( end """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in an already initialized encoder schedule, and encodes a structure matrix, the `in_or_out` string indicates if the structure matrix is for input `"in"` or output `"out"` space (and thus encoded differently) """ @@ -825,7 +843,7 @@ end """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in an already initialized encoder schedule, and decodes a `DataContainer`, and structure matrices with it, the `in_or_out` string indicates if the data is input `"in"` or output `"out"` data (and thus decoded differently) """ @@ -859,7 +877,7 @@ function decode_with_schedule( end """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in an already initialized encoder schedule, and decodes a `DataContainer`, the `in_or_out` string indicates if the data is input `"in"` or output `"out"` data (and thus decoded differently) """ @@ -886,7 +904,7 @@ function decode_with_schedule( end """ -$TYPEDSIGNATURES +$(TYPEDSIGNATURES) Takes in an already initialized encoder schedule, and decodes a structure matrix, the `in_or_out` string indicates if the structure matrix is for input `"in"` or output `"out"` space (and thus decoded differently) """ diff --git a/src/Utilities/elementwise_scaler.jl b/src/Utilities/elementwise_scaler.jl index b7d0f0940..2aa2d6f17 100644 --- a/src/Utilities/elementwise_scaler.jl +++ b/src/Utilities/elementwise_scaler.jl @@ -39,9 +39,29 @@ struct ElementwiseScaler{ struct_decoder_mat::VV5 end +""" +Abstract supertype for the family of elementwise affine scaling strategies. Concrete +subtypes — [`QuartileScaling`](@ref), [`MinMaxScaling`](@ref), [`ZScoreScaling`](@ref) — +are used as type parameters of [`ElementwiseScaler`](@ref) to select the transformation. +""" abstract type UnivariateAffineScaling end + +""" +Elementwise scaling strategy that centres each dimension on its median and scales by the +interquartile range (``Q_3 - Q_1``). Use via [`quartile_scale`](@ref). +""" abstract type QuartileScaling <: UnivariateAffineScaling end + +""" +Elementwise scaling strategy that maps each dimension to ``[0,1]`` using its minimum and +maximum. Use via [`minmax_scale`](@ref). +""" abstract type MinMaxScaling <: UnivariateAffineScaling end + +""" +Elementwise scaling strategy that standardises each dimension to zero mean and unit +variance. Use via [`zscore_scale`](@ref). +""" abstract type ZScoreScaling <: UnivariateAffineScaling end """ diff --git a/src/Utilities/likelihood_informed.jl b/src/Utilities/likelihood_informed.jl index 2a9f51381..eaee97eb7 100644 --- a/src/Utilities/likelihood_informed.jl +++ b/src/Utilities/likelihood_informed.jl @@ -59,8 +59,29 @@ end get_encoder_mat(li::LikelihoodInformed) = li.encoder_mat get_decoder_mat(li::LikelihoodInformed) = li.decoder_mat get_data_mean(li::LikelihoodInformed) = li.data_mean + +""" +$(TYPEDSIGNATURES) + +Return the `retain_info` threshold stored in `li`, controlling how much of the +KL-divergence reduction from the full posterior is retained by the subspace. +""" get_retain_info(li::LikelihoodInformed) = li.retain_info + +""" +$(TYPEDSIGNATURES) + +Return the iteration indices stored in `li` that specify which distribution samples +(indexed by algorithm-time `α`) are used to construct the likelihood-informed subspace. +""" get_iters(li::LikelihoodInformed) = li.iters + +""" +$(TYPEDSIGNATURES) + +Return the gradient-approximation type stored in `li`: `:linreg` (global linear regression) +or `:localsl` (localized statistical linearization). +""" get_grad_type(li::LikelihoodInformed) = li.grad_type function initialize_processor!( From 30a2c564a66fecbc600e21920fcb57fcda412e38 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 14:41:40 -0700 Subject: [PATCH 05/14] applied and iterated on error-message-manager --- .claude/skills/error-message-manager/SKILL.md | 42 ++++++++++++--- src/Emulator.jl | 52 +++++++++++------- src/MachineLearningTools/RandomFeature.jl | 54 +++++++++++++++++-- .../ScalarRandomFeature.jl | 14 +---- .../VectorRandomFeature.jl | 14 +---- src/MarkovChainMonteCarlo.jl | 30 ++++++----- src/Utilities.jl | 29 ++++++---- src/Utilities/canonical_correlation.jl | 26 +++++++-- src/Utilities/decorrelator.jl | 8 ++- src/Utilities/likelihood_informed.jl | 34 ++++++++---- test/Emulator/runtests.jl | 9 +++- test/GaussianProcess/runtests.jl | 4 +- test/RandomFeature/runtests.jl | 5 +- test/Utilities/runtests.jl | 10 +++- 14 files changed, 227 insertions(+), 104 deletions(-) diff --git a/.claude/skills/error-message-manager/SKILL.md b/.claude/skills/error-message-manager/SKILL.md index 8afb6c0bf..df6fa1827 100644 --- a/.claude/skills/error-message-manager/SKILL.md +++ b/.claude/skills/error-message-manager/SKILL.md @@ -42,25 +42,34 @@ vague scope like "the whole package" or "all the source files". **Agent prompt to use** (fill in `` and ``): ``` -Audit `` for error-raising patterns. For every `@assert`, `error(`, or -`throw(` site in every `.jl` file: +Audit `` for error-raising patterns. Before reading individual files, grep +for two structurally-broken patterns that are the highest priority to fix: + + rg -n 'throw\s*\(\s*[A-Z]\w+\s*,' # throw(Type, "string") — always MethodError + rg -n 'throw\s*\(\s*"' # throw("raw string") — throws non-exception + +Then, for every `@assert`, `error(`, or `throw(` site in every `.jl` file: 1. Record: file, line number, exception type (or "bare @assert" / "bare error"), and the full message text (including multiline strings). 2. Classify message quality: + - "broken" — structurally wrong throw that will never produce the intended + exception: `throw(Type, "string")` (always MethodError), or `throw("string")` + (throws a raw String, not catchable as a typed exception) - "good" — has `$(expr)` interpolation showing the actual received value, and is either short (≤3 lines) or already in a `_throw_` helper function - - "long-inline" — message content is good, but the body exceeds 3 lines and - the throw is written inline (not in a `_throw_` helper) + - "long-inline" — message content is good, but the message string body exceeds + 3 lines and the throw is written inline (not in a `_throw_` helper) - "vague" — missing a received value, or no Expected/Got structure - "missing" — bare `@assert` with no message at all 3. Note whether the site is at an API boundary (user-facing input) or an internal invariant (would require a package bug to fire). Return a markdown table with columns: - File | Line | Exception type | Quality | Notes (one-line note on what's wrong if vague/missing/long-inline) + File | Line | Exception type | Quality | Notes (one-line note on what's wrong) -Focus only on sites that are "vague", "missing", or "long-inline" — skip "good" ones. +Focus only on sites that are "broken", "vague", "missing", or "long-inline" — skip "good" ones. +Fix "broken" sites first — they silently misbehave rather than just being unhelpful. ``` **How to use the result**: treat the returned table as your working inventory for @@ -209,6 +218,10 @@ Inline is appropriate only for genuinely short messages (≤3 lines) at a single call site. A single summary line, or a summary plus one Got line, is the ceiling for inline. When in doubt, count — if it doesn't fit in 3 lines, extract. +Count lines of the *message string body*, not lines of the surrounding `throw(...)` +call. A single-line message formatted as `throw(\n ArgumentError(\n "msg"\n )\n)` across +five code lines is still a 1-line message and does not require extraction. + **Where helpers go** Default: a `## Error helpers` section at the **bottom of the source file**, above @@ -222,6 +235,11 @@ which file to use by reading the top-level module file (e.g. `src/PackageName.jl for its `include(...)` list — then add `include("ErrorMessages.jl")` as the first `include` so every subsequent file sees the helpers without any `using`/`import`. +**Include-chained files**: if the two source files are both `include`-d by a common +parent file (e.g. `ScalarRandomFeature.jl` and `VectorRandomFeature.jl` are both +included by `RandomFeature.jl`), add the shared helper to that parent file *before* +the `include(...)` lines — no new file needed, and no `using`/`import` required. + **Naming convention** ``` @@ -384,6 +402,18 @@ site: grep -n '@test_throws' test//runtests.jl | grep '' ``` +**Before changing an exception type**, also grep for existing tests that assert the +*old* type — they will silently become stale after your edit: + +```bash +grep -rn '@test_throws OldType' test/ +``` + +Update every hit in the same edit that changes the source. A stale +`@test_throws WrongType` will either pass against old code and fail once your +rewrite lands, or pass forever without catching a regression — either way it's +a test that no longer protects anything. + Three outcomes: | Situation | Action | diff --git a/src/Emulator.jl b/src/Emulator.jl index 4f5d114a7..13f27ee32 100644 --- a/src/Emulator.jl +++ b/src/Emulator.jl @@ -39,12 +39,17 @@ include(joinpath("MachineLearningTools", "RandomFeature.jl")) # Random Freatures # etc. # defaults in error, all MachineLearningTools require these functions. -function throw_define_mlt(mlt) - throw( - ErrorException( - "Unknown MachineLearningTool defined, please use a known implementation. Please check all methods are defined for the MLT received: \n $mlt", - ), - ) +@noinline function throw_define_mlt(mlt) + throw(ArgumentError(""" +Unknown machine learning tool type — $(typeof(mlt)) does not implement the required emulator interface. + +Got: + typeof(mlt) = $(typeof(mlt)) + +Suggestion: + Implement `build_models!`, `optimize_hyperparameters!`, and `predict` for your custom type, + or use a built-in type such as `GaussianProcess` or `ScalarRandomFeatureInterface`. +""")) end function build_models!(mlt, iopairs, input_structure_mats, output_structure_mats, mlt_kwargs...) throw_define_mlt(mlt) @@ -339,13 +344,7 @@ function predict( check_dim = in_already_encoded ? encoded_input_dim : input_dim N_samples = size(new_inputs, 2) - if size(new_inputs, 1) != check_dim - throw( - ArgumentError( - "Emulator object `io_pairs` (resp. `encoded_io_pairs`) and new inputs do not have consistent dimensions, expected $(check_dim), received $(size(new_inputs,1))", - ), - ) - end + size(new_inputs, 1) != check_dim && _throw_input_dim_mismatch(size(new_inputs, 1), check_dim; caller = :Emulator) # encode the new input data @@ -519,13 +518,7 @@ function predict( check_dim = in_already_encoded ? encoded_input_dim : input_dim N_samples = size(new_inputs, 2) - if size(new_inputs, 1) != check_dim - throw( - ArgumentError( - "ForwardMapWrapper object `io_pairs` (resp. `encoded_io_pairs`) and new inputs do not have consistent dimensions, expected $(check_dim), received $(size(new_inputs,1))", - ), - ) - end + size(new_inputs, 1) != check_dim && _throw_input_dim_mismatch(size(new_inputs, 1), check_dim; caller = :ForwardMapWrapper) prior = get_prior(fmw) if in_already_encoded @@ -581,7 +574,26 @@ function predict( end +## Error helpers + +@noinline function _throw_input_dim_mismatch(actual::Int, expected::Int; caller::Symbol) + throw(DimensionMismatch(""" +$caller: new_inputs row count does not match the expected input dimension. + +Expected: + size(new_inputs, 1) == $expected + +Got: + size(new_inputs, 1) = $actual + +Suggestion: + Pass new_inputs with $expected rows (one per input dimension). + If inputs are already in the encoded space, set `in_already_encoded = true`. +""")) +end + ### Deprecated keywords + function deprecate_transform_to_real(encode, add_obs_noise_cov, transform_to_real) if !isnothing(transform_to_real) @warn( diff --git a/src/MachineLearningTools/RandomFeature.jl b/src/MachineLearningTools/RandomFeature.jl index 63e72cf6f..cb20c22ce 100644 --- a/src/MachineLearningTools/RandomFeature.jl +++ b/src/MachineLearningTools/RandomFeature.jl @@ -131,11 +131,7 @@ function cov_structure_from_string(s::S, d::Int) where {S <: AbstractString} elseif s == "hierlowrank" return HierarchicalLowRankFactor(Int(ceil(sqrt(d)))) else - throw( - ArgumentError( - "Recieved unknown `input_cov_structure` keyword $(s), \n please choose from [\"onedim\",\"diagonal\",\"cholesky\", \"lowrank\"] or provide a CovarianceStructureType", - ), - ) + _throw_unknown_cov_structure(s) end end cov_structure_from_string(cst::CST, args...; kwargs...) where {CST <: CovarianceStructureType} = cst @@ -1106,5 +1102,53 @@ function calculate_ensemble_mean_and_coeffnorm( end +## Error helpers + +@noinline function _throw_unknown_cov_structure(s) + throw(ArgumentError(""" +Unknown `input_cov_structure` keyword. + +Expected: + One of: "onedim", "diagonal", "cholesky", "lowrank", or a CovarianceStructureType value + +Got: + input_cov_structure = $(repr(s)) + +Suggestion: + Pass a recognised string keyword or construct a CovarianceStructureType directly. +""")) +end + +@noinline function _throw_unknown_multithread(multithread) + throw(ArgumentError(""" +Unknown `multithread` optimizer option. + +Expected: + "tullio" (lets Tullio.jl control threading in RandomFeatures.jl) + "ensemble" (threads over ensemble members) + +Got: + multithread = $(repr(multithread)) +""")) +end + +@noinline function _throw_cross_val_split(n_test, n_data, n_cross_val_sets) + throw(ArgumentError(""" +`n_cross_val_sets` is too large for the available data and train/test split. + +Expected: + n_cross_val_sets ≤ $(Int(floor(n_data / n_test))) + +Got: + n_data = $n_data + n_test = $n_test (= n_data - n_train) + n_cross_val_sets = $n_cross_val_sets + +Suggestion: + Reduce `n_cross_val_sets` to at most $(Int(floor(n_data / n_test))), or increase + `train_fraction` to produce smaller test sets. +""")) +end + include("ScalarRandomFeature.jl") include("VectorRandomFeature.jl") diff --git a/src/MachineLearningTools/ScalarRandomFeature.jl b/src/MachineLearningTools/ScalarRandomFeature.jl index 14664a6c8..419194457 100644 --- a/src/MachineLearningTools/ScalarRandomFeature.jl +++ b/src/MachineLearningTools/ScalarRandomFeature.jl @@ -386,13 +386,7 @@ function build_models!( n_train = Int(floor(train_fraction * n_data)) n_test = n_data - n_train - if n_test * n_cross_val_sets > n_data - throw( - ArgumentError( - "train/test split produces cross validation test sets of size $(n_test), out of $(n_data). \"n_cross_val_sets\" optimizer_options keyword < $(Int(floor(n_data/n_test))). Received $n_cross_val_sets", - ), - ) - end + n_test * n_cross_val_sets > n_data && _throw_cross_val_split(n_test, n_data, n_cross_val_sets) for i in 1:n_cross_val_sets tmp = idx_shuffle[((i - 1) * n_test + 1):(i * n_test)] @@ -425,11 +419,7 @@ function build_models!( elseif multithread == "tullio" multithread_type = TullioThreading() else - throw( - ArgumentError( - "Unknown optimizer option for multithreading, please choose from \"tullio\" (allows Tullio.jl to control threading in RandomFeatures.jl), or \"ensemble\" (threading is done over the ensemble)", - ), - ) + _throw_unknown_multithread(multithread) end # scale up the prior so that default priors are always "reasonable" prior_in_scale = 1.0 ./ std(input_values, dims = 2) diff --git a/src/MachineLearningTools/VectorRandomFeature.jl b/src/MachineLearningTools/VectorRandomFeature.jl index cd19f1147..27bb4fb75 100644 --- a/src/MachineLearningTools/VectorRandomFeature.jl +++ b/src/MachineLearningTools/VectorRandomFeature.jl @@ -397,11 +397,7 @@ function build_models!( elseif multithread == "tullio" multithread_type = TullioThreading() else - throw( - ArgumentError( - "Unknown optimizer option for multithreading, please choose from \"tullio\" (allows Tullio.jl to control threading in RandomFeatures.jl, or \"loops\" (threading optimization loops)", - ), - ) + _throw_unknown_multithread(multithread) end prior = build_default_prior(input_dim, output_dim, kernel_structure) rng = get_rng(vrfi) @@ -438,13 +434,7 @@ function build_models!( n_train = Int(floor(train_fraction * n_data)) # 20% split n_test = n_data - n_train - if n_test * n_cross_val_sets > n_data - throw( - ArgumentError( - "train/test split produces cross validation test sets of size $(n_test), out of $(n_data). \"n_cross_val_sets\" optimizer_options keyword < $(Int(floor(n_data/n_test))). Received $n_cross_val_sets", - ), - ) - end + n_test * n_cross_val_sets > n_data && _throw_cross_val_split(n_test, n_data, n_cross_val_sets) for i in 1:n_cross_val_sets diff --git a/src/MarkovChainMonteCarlo.jl b/src/MarkovChainMonteCarlo.jl index 72897b0df..9038426df 100644 --- a/src/MarkovChainMonteCarlo.jl +++ b/src/MarkovChainMonteCarlo.jl @@ -214,11 +214,7 @@ function autodiff_gradient(model::AdvancedMH.DensityModel, params, autodiff_prot elseif autodiff_protocol == ReverseDiffProtocol return ReverseDiff.gradient(x -> AdvancedMH.logdensity(model, x), params) else - throw( - ArgumentError( - "Calling `autodiff_gradient(...)` on a sampler with protocol $(autodiff_protocol) that has *no* gradient implementation.\n Please select from a protocol with a gradient implementation (e.g., `ForwardDiffProtocol`).", - ), - ) + _throw_no_autodiff_impl(:gradient, autodiff_protocol) end end @@ -232,12 +228,7 @@ function autodiff_hessian(model::AdvancedMH.DensityModel, params, autodiff_proto elseif autodiff_protocol == ReverseDiffProtocol return Symmetric(ReverseDiff.hessian(x -> AdvancedMH.logdensity(model, x), params)) else - throw( - ArgumentError( - "Calling `autodiff_hessian(...)` on a sampler with protocol $(autodiff_protocol) that has *no* hessian implementation.\n Please select from a protocol with a hessian implementation (e.g., `ForwardDiffProtocol`).", - ), - ) - + _throw_no_autodiff_impl(:hessian, autodiff_protocol) end end @@ -706,7 +697,7 @@ function accept_ratio(chain::MCMCChains.Chains) if :accepted in names(chain, :internals) return mean(chain, :accepted) else - throw("MH `accepted` not recorded in chain: $(names(chain, :internals)).") + error("MH `:accepted` not recorded in MCMC chain — available internals: $(names(chain, :internals)).") end end @@ -785,7 +776,7 @@ function optimize_stepsize( return stepsize end end - throw("Failed to choose suitable stepsize in $(max_iter) iterations.") + error("optimize_stepsize: acceptance rate $(round(acc_ratio; sigdigits = 3)) did not reach target $(target_acc) ± 0.1 within $(max_iter) iterations — last stepsize: $(round(stepsize; sigdigits = 3)).") end # use default rng if none given optimize_stepsize(mcmc::MCMCWrapper; kwargs...) = optimize_stepsize(Random.GLOBAL_RNG, mcmc; kwargs...) @@ -865,6 +856,19 @@ function esjd(chain::MCMCChains.Chains) end +## Error helpers + +@noinline function _throw_no_autodiff_impl(op::Symbol, autodiff_protocol) + throw(ArgumentError(""" +`autodiff_$(op)` called on a sampler with no $(op) implementation for protocol $(autodiff_protocol). + +Got: + autodiff_protocol = $(autodiff_protocol) + +Suggestion: + Select a protocol with a $(op) implementation, e.g. `ForwardDiffProtocol`. +""")) +end diff --git a/src/Utilities.jl b/src/Utilities.jl index 31183149d..ee7260277 100644 --- a/src/Utilities.jl +++ b/src/Utilities.jl @@ -223,12 +223,16 @@ function encoder_kwargs_from( if !isnothing(final_samples_out) if (isa(final_samples_out, AbstractMatrix)) # add one matrix if length(samples_out) > 0 - @assert(size(samples_out[1]) == size(final_samples_out)) + size(samples_out[1]) == size(final_samples_out) || error( + "size of final_samples_out ($(size(final_samples_out))) does not match existing samples_out entries ($(size(samples_out[1])))", + ) end push!(samples_out, final_samples_out) else # add a vector of matrices if length(samples_out) > 0 - @assert all(size(samples_out[1]) == size(so) for so in final_samples_out) + all(size(samples_out[1]) == size(so) for so in final_samples_out) || error( + "not all final_samples_out entries have size $(size(samples_out[1])) to match existing samples_out", + ) end samples_out = reduce(vcat, [samples_out, final_samples_out]) end @@ -278,15 +282,9 @@ function create_compact_linear_map( ds = [] batches = [] shift = 0 - for a in Avec + for (i, a) in enumerate(Avec) bsize = 0 - if isa(a, UniformScaling) - throw( - ArgumentError( - "Detected `UniformScaling` (i.e. \"λI\") StructureMatrix, and unable to infer dimensionality. \n Please recast this as a diagonal matrix, defining \"λI(d)\" for dimension d", - ), - ) - end + isa(a, UniformScaling) && _throw_uniform_scaling_elem(i, length(Avec)) if isa(a, AbstractMatrix) if size(a, 1) <= svd_dim_max svda = svd(a) @@ -1258,6 +1256,17 @@ function decode_and_add_noise( return decode_and_add_noise(noise_injector, samples) end +## Error helpers + +@noinline function _throw_uniform_scaling_elem(i::Int, total::Int) + throw(ArgumentError(""" +Structure matrix $i (of $total) is a `UniformScaling` ("λI"), whose dimension cannot be inferred. + +Suggestion: + Replace `λ * I` with `Diagonal(fill(λ, d))` where `d` is the required matrix dimension. +""")) +end + # Processors include("Utilities/canonical_correlation.jl") diff --git a/src/Utilities/canonical_correlation.jl b/src/Utilities/canonical_correlation.jl index f3fe4648e..1b9b99f66 100644 --- a/src/Utilities/canonical_correlation.jl +++ b/src/Utilities/canonical_correlation.jl @@ -97,11 +97,7 @@ function initialize_processor!( if length(get_encoder_mat(cc)) == 0 if size(in_data, 2) < size(in_data, 1) || size(out_data, 2) < size(out_data, 1) - throw( - ArgumentError( - "CanonicalCorrelation implementation not defined for # data samples < dimensions, please obtain more samples, or perform prior dimension reduction approaches until this is satisfied", - ), - ) + _throw_insufficient_cca_samples(in_data, out_data) end # Individually decompose in and out @@ -190,3 +186,23 @@ function _decode_structure_matrix(cc::CanonicalCorrelation, enc_structure_matrix decoder_mat = get_decoder_mat(cc)[1] return decoder_mat * enc_structure_matrix * decoder_mat' end + +## Error helpers + +@noinline function _throw_insufficient_cca_samples(in_data, out_data) + throw(ArgumentError(""" +CanonicalCorrelation requires at least as many data samples as input and output dimensions. + +Expected: + size(in_data, 2) ≥ size(in_data, 1) (samples ≥ input dimension) + size(out_data, 2) ≥ size(out_data, 1) (samples ≥ output dimension) + +Got: + size(in_data) = $(size(in_data)) ($(size(in_data,2)) samples, $(size(in_data,1)) input dims) + size(out_data) = $(size(out_data)) ($(size(out_data,2)) samples, $(size(out_data,1)) output dims) + +Suggestion: + Obtain more data samples, or apply a dimension-reduction step (e.g. Decorrelator) + before using CanonicalCorrelation. +""")) +end diff --git a/src/Utilities/decorrelator.jl b/src/Utilities/decorrelator.jl index ddbf90916..e9ac49b3e 100644 --- a/src/Utilities/decorrelator.jl +++ b/src/Utilities/decorrelator.jl @@ -238,11 +238,9 @@ function initialize_processor!( map2 = create_compact_linear_map(decorrelation_action2) decorrelation_map = map1 + map2 # x -> dm.maps[1].f(x) + dm.maps[2].f(x) else - throw( - ArgumentError( - "Keyword `decorrelate_with` must be taken from [\"sample_cov\", \"structure_mat\", \"combined\"]. Received $(decorrelate_with)", - ), - ) + throw(ArgumentError( + "Invalid `decorrelate_with` keyword: expected one of \"sample_cov\", \"structure_mat\", or \"combined\", got $(repr(decorrelate_with))", + )) end n_totvar_samples = get_n_totvar_samples(dd) diff --git a/src/Utilities/likelihood_informed.jl b/src/Utilities/likelihood_informed.jl index eaee97eb7..f417ae631 100644 --- a/src/Utilities/likelihood_informed.jl +++ b/src/Utilities/likelihood_informed.jl @@ -46,12 +46,7 @@ function likelihood_informed(; retain_info = 1, iters = 1, grad_type = :linreg) if !isa(iters, AbstractVector) iters = [iters] end - if !(eltype(iters) <: Integer) - throw( - ArgumentError, - "Iterations must be passed as an Int or Vec{Int}. This corresponds to which of the structure-vectors are used to construct the subspace", - ) - end + eltype(iters) <: Integer || _throw_invalid_iters(iters) LikelihoodInformed([], [], [], retain_info, nothing, iters, grad_type) end @@ -163,7 +158,7 @@ function initialize_processor!( grad = (samples_out .- samples_out_mean) / (samples_in .- samples_in_mean) fill(grad, size(samples_in, 2)) else - @assert get_grad_type(li) == :localsl + get_grad_type(li) == :localsl || error("Internal error: unhandled grad_type $(repr(get_grad_type(li))) in initialize_processor!") map(eachcol(samples_in)) do u # TODO: It might be interesting to introduce a parameter to weight this distance with. @@ -204,7 +199,7 @@ function initialize_processor!( diagnostic_mats[it] = hermitianpart(mean(grad * grad' for grad in grads)) else # @assert apply_to == "out" && ( !(α ≈ 0 && obs_whitened) || length(alphas[iters]) > 1 ) - @assert apply_to == "out" + apply_to == "out" || error("Internal error: unexpected apply_to=$(repr(apply_to)) in the output-space diagnostic branch") # Need to represent the "f" and "egrad" functions for this α f = (_, Vs) -> begin @@ -272,8 +267,8 @@ function initialize_processor!( end encoder_mat = decomp.vectors[:, 1:trunc_val]' else # using diagnostic_f's and diagnostic_egrads - @assert length(diagnostic_fs) > 0 && length(diagnostic_egrads) > 0 - @assert apply_to == "out" + (length(diagnostic_fs) > 0 && length(diagnostic_egrads) > 0) || error("Internal error: no diagnostic functions were accumulated (length(diagnostic_fs)=$(length(diagnostic_fs)))") + apply_to == "out" || error("Internal error: unexpected apply_to=$(repr(apply_to)) in the matrix-free diagnostic branch") diagnostic_f, diagnostic_egrad, samples_mean = if length(iters) > 1 alpha_weight = zeros(length(iters)) @@ -381,3 +376,22 @@ function _decode_structure_matrix(li::LikelihoodInformed, structure_matrix::SM) decoder_mat = get_decoder_mat(li)[1] return decoder_mat * structure_matrix * decoder_mat' end + +## Error helpers + +@noinline function _throw_invalid_iters(iters) + throw(ArgumentError(""" +`iters` must be an Int or Vector{Int}. + +Expected: + eltype(iters) <: Integer + +Got: + typeof(iters) = $(typeof(iters)) + eltype(iters) = $(eltype(iters)) + +Suggestion: + Pass an integer iteration index, e.g. `iters = 1`, or a vector `iters = [1, 2]`. + These indices specify which EKI algorithm-time samples are used to build the subspace. +""")) +end diff --git a/test/Emulator/runtests.jl b/test/Emulator/runtests.jl index 128962414..75f26670a 100644 --- a/test/Emulator/runtests.jl +++ b/test/Emulator/runtests.jl @@ -41,7 +41,9 @@ struct MLTester <: Emulators.MachineLearningTool end # Unknown ML type mlt = MLTester() - @test_throws ErrorException emulator = Emulator(mlt, io_pairs) + let thrown = @test_throws ArgumentError Emulator(mlt, io_pairs) + @test contains(thrown.value.msg, "does not implement the required emulator interface") + end # test getters & defaults gp = GaussianProcess(GPJL()) @@ -142,7 +144,10 @@ end y_pred_enc, y_cov_enc = EM.predict(fmw, x_test; encode = "out") y_test_enc = encode_data(fmw, y_test, "out") @test all(isapprox(norm(y_pred_enc - y_test_enc), 0; atol = sqrt(d * m) * tol)) - @test_throws ArgumentError EM.predict(fmw, x_test') + let thrown = @test_throws DimensionMismatch EM.predict(fmw, x_test') + @test contains(thrown.value.msg, "ForwardMapWrapper") + @test contains(thrown.value.msg, "new_inputs row count") + end # with out enc. fmw = forward_map_wrapper(G, prior, io_pairs, encoder_kwargs = (; obs_noise_cov = Σ)) diff --git a/test/GaussianProcess/runtests.jl b/test/GaussianProcess/runtests.jl index 1b566221e..e911fea17 100644 --- a/test/GaussianProcess/runtests.jl +++ b/test/GaussianProcess/runtests.jl @@ -74,7 +74,9 @@ using CalibrateEmulateSample.Utilities Emulators.optimize_hyperparameters!(em1) - @test_throws ErrorException Emulators.optimize_hyperparameters!(10) # not an mlt + let thrown = @test_throws ArgumentError Emulators.optimize_hyperparameters!(10) # not an mlt + @test contains(thrown.value.msg, "does not implement the required emulator interface") + end μ1, σ1² = Emulators.predict(em1, new_inputs) diff --git a/test/RandomFeature/runtests.jl b/test/RandomFeature/runtests.jl index 2fdeddacb..274e27ffe 100644 --- a/test/RandomFeature/runtests.jl +++ b/test/RandomFeature/runtests.jl @@ -78,7 +78,10 @@ rng = Random.MersenneTwister(seed) @test cov_structure_from_string(cs, d) == cc end @test cov_structure_from_string(OneDimFactor()) == OneDimFactor() - @test_throws ArgumentError cov_structure_from_string("bad-string", d) + let thrown = @test_throws ArgumentError cov_structure_from_string("bad-string", d) + @test contains(thrown.value.msg, "input_cov_structure") + @test contains(thrown.value.msg, repr("bad-string")) + end # [2. ] Kernel Structures d = 6 diff --git a/test/Utilities/runtests.jl b/test/Utilities/runtests.jl index 8703e6cd3..e74f6621c 100644 --- a/test/Utilities/runtests.jl +++ b/test/Utilities/runtests.jl @@ -165,7 +165,10 @@ end A[4].svd_cov.U * Diagonal(A[4].svd_cov.S) * A[4].svd_cov.Vt + A[4].diag_cov - @test_throws ArgumentError create_compact_linear_map(3 * I) # needs dims + let thrown = @test_throws ArgumentError create_compact_linear_map(3 * I) # needs dims + @test contains(thrown.value.msg, "UniformScaling") + @test contains(thrown.value.msg, "Diagonal") + end for svd_type in ["psvd", "tsvd"] psvd_kwargs = (; rtol = 1e-3) # make very small for testing @@ -280,7 +283,10 @@ end @test get_decoder_mat(ll3) == [3] @test get_data_mean(ll3) == [4] @test_throws ArgumentError likelihood_informed(grad_type = :bad_type) - @test_throws ArgumentError likelihood_informed(iters = 1.3) + let thrown = @test_throws ArgumentError likelihood_informed(iters = 1.3) + @test contains(thrown.value.msg, "eltype(iters)") + @test contains(thrown.value.msg, "Float64") + end # test equalities cc = canonical_correlation() From 2df76f09f221868c38ff917d54cbfcb55b0a5e94 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 21:28:09 -0700 Subject: [PATCH 06/14] updated manifest --- docs/Manifest.toml | 160 +++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 101b149f2..9567c231a 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.9" +julia_version = "1.12.0" manifest_format = "2.0" -project_hash = "e4d965798d55f5903483b71f545533e62ea32fdf" +project_hash = "5255fc92659f2799f0f4c5144942145e13d951ed" [[deps.ADTypes]] git-tree-sha1 = "bbc22a9a08a0ef6460041086d8a7b27940ed4ffd" @@ -91,10 +91,10 @@ version = "0.1.44" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [[deps.Adapt]] -deps = ["LinearAlgebra", "Requires"] -git-tree-sha1 = "0761717147821d696c9470a7a86364b2fbd22fd8" +deps = ["LinearAlgebra"] +git-tree-sha1 = "28e1637322d4019ed2577cbec9268fab9b7da117" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "4.5.2" +version = "4.6.0" weakdeps = ["SparseArrays", "StaticArrays"] [deps.Adapt.extensions] @@ -153,9 +153,9 @@ version = "3.5.2+0" [[deps.ArrayInterface]] deps = ["Adapt", "LinearAlgebra"] -git-tree-sha1 = "54f895554d05c83e3dd59f6a396671dae8999573" +git-tree-sha1 = "3d0cabd25fab32390e3bcb82cd67e700aebd9816" uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" -version = "7.24.0" +version = "7.25.0" [deps.ArrayInterface.extensions] ArrayInterfaceAMDGPUExt = "AMDGPU" @@ -255,9 +255,9 @@ version = "0.2.7" [[deps.Cairo_jll]] deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] -git-tree-sha1 = "d0efe2c6fdcdaa1c161d206aa8b933788397ec71" +git-tree-sha1 = "1fa950ebc3e37eccd51c6a8fe1f92f7d86263522" uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" -version = "1.18.6+0" +version = "1.18.7+0" [[deps.CalibrateEmulateSample]] deps = ["AbstractGPs", "AbstractMCMC", "AdvancedMH", "ChunkSplitters", "Conda", "Distributions", "DocStringExtensions", "EnsembleKalmanProcesses", "ForwardDiff", "GaussianProcesses", "KernelFunctions", "LinearAlgebra", "LinearMaps", "LowRankApprox", "MCMCChains", "Manifolds", "Manopt", "Pkg", "Printf", "ProgressBars", "PyCall", "Random", "RandomFeatures", "ReverseDiff", "ScikitLearn", "StableRNGs", "Statistics", "StatsBase", "TSVD"] @@ -354,7 +354,7 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" +version = "1.3.0+1" [[deps.CompositionsBase]] git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" @@ -446,9 +446,9 @@ version = "1.15.1" [[deps.DifferentiationInterface]] deps = ["ADTypes", "LinearAlgebra"] -git-tree-sha1 = "d0250552e42bf7cc36479fd38a6e30004c9e8c2b" +git-tree-sha1 = "2147a95a217cc8a78ec96ee03581adf129468e49" uuid = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" -version = "0.7.17" +version = "0.7.18" [deps.DifferentiationInterface.extensions] DifferentiationInterfaceChainRulesCoreExt = "ChainRulesCore" @@ -460,6 +460,7 @@ version = "0.7.17" DifferentiationInterfaceForwardDiffExt = ["ForwardDiff", "DiffResults"] DifferentiationInterfaceGPUArraysCoreExt = ["GPUArraysCore", "Adapt"] DifferentiationInterfaceGTPSAExt = "GTPSA" + DifferentiationInterfaceHyperHessiansExt = "HyperHessians" DifferentiationInterfaceMooncakeExt = "Mooncake" DifferentiationInterfacePolyesterForwardDiffExt = ["PolyesterForwardDiff", "ForwardDiff", "DiffResults"] DifferentiationInterfaceReverseDiffExt = ["ReverseDiff", "DiffResults"] @@ -484,6 +485,7 @@ version = "0.7.17" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" GTPSA = "b27dd330-f138-47c5-815b-40db9dd9b6e8" + HyperHessians = "06b494a0-c8e0-40cc-ad32-d99506a00a6c" Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" PolyesterForwardDiff = "98d1487c-24ca-40b6-b7ab-df2af84e126b" ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" @@ -557,9 +559,9 @@ version = "0.2.4" [[deps.EnsembleKalmanProcesses]] deps = ["Convex", "Distributions", "DocStringExtensions", "FFMPEG", "GaussianRandomFields", "Interpolations", "LinearAlgebra", "MathOptInterface", "Optim", "QuadGK", "Random", "RecipesBase", "SCS", "SparseArrays", "Statistics", "StatsBase", "TOML"] -git-tree-sha1 = "bb39ccc7bd5f7e8f078a987820044d85d61df72b" +git-tree-sha1 = "301a8de328bbfa7e049cfa59cdd7131b6d803c81" uuid = "aa8a2aa5-91d8-4396-bcef-d4f2ec43552d" -version = "2.7.0" +version = "2.7.1" [deps.EnsembleKalmanProcesses.extensions] EnsembleKalmanProcessesMakieExt = "Makie" @@ -610,9 +612,9 @@ version = "3.3.12+0" [[deps.FastGaussQuadrature]] deps = ["LinearAlgebra", "SpecialFunctions", "StaticArrays"] -git-tree-sha1 = "0044e9f5e49a57e88205e8f30ab73928b05fe5b6" +git-tree-sha1 = "ad29c8cb0ea5fb1816926d2a49683f890a8daf64" uuid = "442a2c76-b920-505d-bb47-c5924d526838" -version = "1.1.0" +version = "1.2.0" [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" @@ -725,9 +727,9 @@ version = "1.5.0" [[deps.Git_LFS_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "bb8471f313ed941f299aa53d32a94ab3bee08844" +git-tree-sha1 = "8c66e385d631bb934ff05e76d4a566c640c8df69" uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" -version = "3.7.0+0" +version = "3.7.1+0" [[deps.Git_jll]] deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] @@ -883,15 +885,15 @@ version = "1.0.0" [[deps.JLLWrappers]] deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.1" +version = "1.8.0" [[deps.JSON]] deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] -git-tree-sha1 = "fe23330af47b8ab4e135b2ff65f7398c3a2bfc65" +git-tree-sha1 = "f76f7560267b840e492180f9899b472f30b88450" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "1.5.2" +version = "1.6.0" [deps.JSON.extensions] JSONArrowExt = ["ArrowTypes"] @@ -899,6 +901,11 @@ version = "1.5.2" [deps.JSON.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + [[deps.KernelDensity]] deps = ["Distributions", "DocStringExtensions", "FFTA", "Interpolations", "StatsBase"] git-tree-sha1 = "4260cfc991b8885bf747801fb60dd4503250e478" @@ -968,24 +975,24 @@ uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" version = "0.6.4" [[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.6.0+0" +version = "8.11.1+1" [[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" version = "1.11.0" [[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.7.2+0" +version = "1.9.0+0" [[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" +version = "1.11.3+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -1024,7 +1031,7 @@ version = "7.5.1" [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.11.0" +version = "1.12.0" [[deps.LinearMaps]] deps = ["LinearAlgebra"] @@ -1147,12 +1154,12 @@ version = "0.4.5" [[deps.Manifolds]] deps = ["ADTypes", "DifferentiationInterface", "Graphs", "Kronecker", "LinearAlgebra", "ManifoldDiff", "ManifoldsBase", "Markdown", "MatrixEquations", "Quaternions", "Random", "SimpleWeightedGraphs", "SpecialFunctions", "StaticArrays", "Statistics", "StatsBase", "Tullio"] -git-tree-sha1 = "709b90386258b79674859d61fc5dd397bd9c941d" +git-tree-sha1 = "371b2e632fd211b0655bca3ce45d120312b9aaa0" uuid = "1cead3c2-87b3-11e9-0ccd-23c62b72b94e" -version = "0.11.22" +version = "0.11.25" [deps.Manifolds.extensions] - ManifoldsBoundaryValueDiffEqExt = "BoundaryValueDiffEq" + ManifoldsBoundaryValueDiffEqExt = "BoundaryValueDiffEqMIRK" ManifoldsDistributionsExt = ["Distributions", "RecursiveArrayTools"] ManifoldsHybridArraysExt = "HybridArrays" ManifoldsNLsolveExt = "NLsolve" @@ -1163,7 +1170,7 @@ version = "0.11.22" ManifoldsTestExt = "Test" [deps.Manifolds.weakdeps] - BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" + BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" @@ -1194,9 +1201,9 @@ version = "2.3.5" [[deps.Manopt]] deps = ["ColorSchemes", "ColorTypes", "Colors", "DataStructures", "Dates", "Glossaries", "LinearAlgebra", "ManifoldDiff", "ManifoldsBase", "Markdown", "Preferences", "Printf", "Random", "SparseArrays", "Statistics"] -git-tree-sha1 = "656cfa06ad863c778799c6b116e6f8310844a470" +git-tree-sha1 = "88b6b3dbf8d585bd8623a718c916eb064b5bcdfc" uuid = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5" -version = "0.5.36" +version = "0.5.38" [deps.Manopt.extensions] ManoptJuMPExt = "JuMP" @@ -1222,7 +1229,7 @@ uuid = "d125e4d3-2237-4719-b19c-fa641b8a4667" version = "0.1.8" [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" version = "1.11.0" @@ -1248,14 +1255,9 @@ version = "1.51.0" [[deps.MatrixEquations]] deps = ["LinearAlgebra", "LinearMaps"] -git-tree-sha1 = "51f3fade0b4ff2cf90b36b3312425460631abb56" +git-tree-sha1 = "ff17d35ab669720c3dd9cb0a214eb75425542dc8" uuid = "99c1a7ee-ab34-5fd5-8076-27c950a045f4" -version = "2.5.6" - -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.6+0" +version = "2.5.8" [[deps.Missings]] deps = ["DataAPI"] @@ -1269,7 +1271,7 @@ version = "1.11.0" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.12.12" +version = "2025.5.20" [[deps.MuladdMacro]] git-tree-sha1 = "cac9cc5499c25554cba55cd3c30543cff5ca4fab" @@ -1278,9 +1280,9 @@ version = "0.2.4" [[deps.MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] -git-tree-sha1 = "7c25249fc13a070f5ba433c50e21e22bb33c6fb0" +git-tree-sha1 = "dc5b2c4c111c46bc79ac4405eeb563523b39c004" uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" -version = "1.7.1" +version = "1.8.0" [[deps.NLSolversBase]] deps = ["ADTypes", "DifferentiationInterface", "Distributed", "FiniteDiff", "ForwardDiff"] @@ -1320,7 +1322,7 @@ version = "1.0.0" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" +version = "1.3.0" [[deps.Nullables]] git-tree-sha1 = "8f87854cc8f3685a60689d8edecaa29d2251979b" @@ -1343,20 +1345,20 @@ uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" version = "1.3.6+0" [[deps.OpenBLAS32_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "dd0d9979377e43918a80486a562ddedcc6d9bdf3" +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "libblastrampoline_jll"] +git-tree-sha1 = "565175ce692c065e50ad32efbb61ba69b1586593" uuid = "656ef2d0-ae68-5445-9ca0-591084a874a2" -version = "0.3.33+0" +version = "0.3.33+1" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.27+1" +version = "0.3.29+0" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.5+0" +version = "0.8.7+0" [[deps.OpenSSH_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] @@ -1365,10 +1367,9 @@ uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" version = "10.3.1+0" [[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "2ac022577e5eac7da040de17776d51bb770cd895" +deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.6+0" +version = "3.5.1+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -1400,7 +1401,7 @@ version = "1.8.1" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.42.0+1" +version = "10.44.0+1" [[deps.PDMats]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] @@ -1422,14 +1423,14 @@ version = "2.8.4" [[deps.Pixman_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] -git-tree-sha1 = "db76b1ecd5e9715f3d043cec13b2ec93ce015d53" +git-tree-sha1 = "e4a6721aa89e62e5d4217c0b21bd714263779dda" uuid = "30392449-352a-5448-841d-b1acce4e97dc" -version = "0.44.2+0" +version = "0.46.4+0" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.11.0" +version = "1.12.0" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -1455,9 +1456,9 @@ version = "0.2.4" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" +version = "1.3.4" [[deps.Preferences]] deps = ["TOML"] @@ -1489,6 +1490,7 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" version = "1.11.0" [[deps.Profile]] +deps = ["StyledStrings"] uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" version = "1.11.0" @@ -1540,7 +1542,7 @@ uuid = "94ee1d12-ae83-5a48-8b1c-48b8ff168ae0" version = "0.7.7" [[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" version = "1.11.0" @@ -1551,9 +1553,9 @@ version = "1.11.0" [[deps.RandomFeatures]] deps = ["Distributions", "DocStringExtensions", "EnsembleKalmanProcesses", "FFMPEG", "LinearAlgebra", "LoopVectorization", "Random", "SpecialFunctions", "Statistics", "StatsBase", "Tullio"] -git-tree-sha1 = "8e63cac591d3ffe80ecfa7ba189951009fb642fc" +git-tree-sha1 = "d3287acc41da9c36ea021d79e9f25f48cfba71a3" uuid = "36c3bae2-c0c3-419d-b3b4-eebadd35c5e5" -version = "0.3.4" +version = "0.3.5" [[deps.RangeArrays]] git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5" @@ -1677,9 +1679,9 @@ version = "0.5.0" [[deps.SentinelArrays]] deps = ["Dates", "Random"] -git-tree-sha1 = "ebe7e59b37c400f694f52b58c93d26201387da70" +git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80" uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" -version = "1.4.9" +version = "1.4.10" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -1698,9 +1700,9 @@ version = "1.11.0" [[deps.SimpleTraits]] deps = ["InteractiveUtils", "MacroTools"] -git-tree-sha1 = "be8eeac05ec97d379347584fa9fe2f5f76795bcb" +git-tree-sha1 = "7ddb0b49c109481b046972c0e4ab02b2127d6a75" uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" -version = "0.9.5" +version = "0.9.6" [[deps.SimpleWeightedGraphs]] deps = ["Graphs", "LinearAlgebra", "Markdown", "SparseArrays"] @@ -1721,7 +1723,7 @@ version = "1.2.2" [[deps.SparseArrays]] deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.11.0" +version = "1.12.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] @@ -1819,9 +1821,9 @@ version = "0.4.4" [[deps.StructUtils]] deps = ["Dates", "UUIDs"] -git-tree-sha1 = "dd974aefe288ef2898733aecf40858dc86742d74" +git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8" uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" -version = "2.8.1" +version = "2.8.2" [deps.StructUtils.extensions] StructUtilsMeasurementsExt = ["Measurements"] @@ -1844,7 +1846,7 @@ uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" [[deps.SuiteSparse_jll]] deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.7.0+0" +version = "7.8.3+2" [[deps.TOML]] deps = ["Dates"] @@ -2008,7 +2010,7 @@ version = "1.6.0+0" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" +version = "1.3.1+2" [[deps.ZygoteRules]] deps = ["ChainRulesCore", "MacroTools"] @@ -2031,7 +2033,7 @@ version = "0.17.4+0" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.11.0+0" +version = "5.13.1+1" [[deps.libdrm_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libpciaccess_jll"] @@ -2066,18 +2068,18 @@ version = "1.3.8+0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.59.0+0" +version = "1.64.0+1" [[deps.oneTBB_jll]] deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] -git-tree-sha1 = "1350188a69a6e46f799d3945beef36435ed7262f" +git-tree-sha1 = "da8c1f6eee04831f14edcfa5dae611d309807e57" uuid = "1317d2d5-d96f-522e-a858-c73665f53c3e" -version = "2022.0.0+1" +version = "2022.3.0+0" [[deps.p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" +version = "17.5.0+2" [[deps.x264_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] From 043d20d31759fa010f581e88ab55f1b895400d49 Mon Sep 17 00:00:00 2001 From: odunbar Date: Sun, 24 May 2026 22:45:44 -0700 Subject: [PATCH 07/14] format --- src/Emulator.jl | 3 +- src/MarkovChainMonteCarlo.jl | 4 +- src/Utilities/decorrelator.jl | 8 ++- src/Utilities/likelihood_informed.jl | 13 ++-- src/show.jl | 93 +++++++++++++++++++--------- test/Show/runtests.jl | 27 ++++---- 6 files changed, 95 insertions(+), 53 deletions(-) diff --git a/src/Emulator.jl b/src/Emulator.jl index 13f27ee32..b6dcf577e 100644 --- a/src/Emulator.jl +++ b/src/Emulator.jl @@ -518,7 +518,8 @@ function predict( check_dim = in_already_encoded ? encoded_input_dim : input_dim N_samples = size(new_inputs, 2) - size(new_inputs, 1) != check_dim && _throw_input_dim_mismatch(size(new_inputs, 1), check_dim; caller = :ForwardMapWrapper) + size(new_inputs, 1) != check_dim && + _throw_input_dim_mismatch(size(new_inputs, 1), check_dim; caller = :ForwardMapWrapper) prior = get_prior(fmw) if in_already_encoded diff --git a/src/MarkovChainMonteCarlo.jl b/src/MarkovChainMonteCarlo.jl index 9038426df..9d9bf0804 100644 --- a/src/MarkovChainMonteCarlo.jl +++ b/src/MarkovChainMonteCarlo.jl @@ -776,7 +776,9 @@ function optimize_stepsize( return stepsize end end - error("optimize_stepsize: acceptance rate $(round(acc_ratio; sigdigits = 3)) did not reach target $(target_acc) ± 0.1 within $(max_iter) iterations — last stepsize: $(round(stepsize; sigdigits = 3)).") + error( + "optimize_stepsize: acceptance rate $(round(acc_ratio; sigdigits = 3)) did not reach target $(target_acc) ± 0.1 within $(max_iter) iterations — last stepsize: $(round(stepsize; sigdigits = 3)).", + ) end # use default rng if none given optimize_stepsize(mcmc::MCMCWrapper; kwargs...) = optimize_stepsize(Random.GLOBAL_RNG, mcmc; kwargs...) diff --git a/src/Utilities/decorrelator.jl b/src/Utilities/decorrelator.jl index e9ac49b3e..89836f0c3 100644 --- a/src/Utilities/decorrelator.jl +++ b/src/Utilities/decorrelator.jl @@ -238,9 +238,11 @@ function initialize_processor!( map2 = create_compact_linear_map(decorrelation_action2) decorrelation_map = map1 + map2 # x -> dm.maps[1].f(x) + dm.maps[2].f(x) else - throw(ArgumentError( - "Invalid `decorrelate_with` keyword: expected one of \"sample_cov\", \"structure_mat\", or \"combined\", got $(repr(decorrelate_with))", - )) + throw( + ArgumentError( + "Invalid `decorrelate_with` keyword: expected one of \"sample_cov\", \"structure_mat\", or \"combined\", got $(repr(decorrelate_with))", + ), + ) end n_totvar_samples = get_n_totvar_samples(dd) diff --git a/src/Utilities/likelihood_informed.jl b/src/Utilities/likelihood_informed.jl index f417ae631..8162beed6 100644 --- a/src/Utilities/likelihood_informed.jl +++ b/src/Utilities/likelihood_informed.jl @@ -158,7 +158,8 @@ function initialize_processor!( grad = (samples_out .- samples_out_mean) / (samples_in .- samples_in_mean) fill(grad, size(samples_in, 2)) else - get_grad_type(li) == :localsl || error("Internal error: unhandled grad_type $(repr(get_grad_type(li))) in initialize_processor!") + get_grad_type(li) == :localsl || + error("Internal error: unhandled grad_type $(repr(get_grad_type(li))) in initialize_processor!") map(eachcol(samples_in)) do u # TODO: It might be interesting to introduce a parameter to weight this distance with. @@ -199,7 +200,8 @@ function initialize_processor!( diagnostic_mats[it] = hermitianpart(mean(grad * grad' for grad in grads)) else # @assert apply_to == "out" && ( !(α ≈ 0 && obs_whitened) || length(alphas[iters]) > 1 ) - apply_to == "out" || error("Internal error: unexpected apply_to=$(repr(apply_to)) in the output-space diagnostic branch") + apply_to == "out" || + error("Internal error: unexpected apply_to=$(repr(apply_to)) in the output-space diagnostic branch") # Need to represent the "f" and "egrad" functions for this α f = (_, Vs) -> begin @@ -267,8 +269,11 @@ function initialize_processor!( end encoder_mat = decomp.vectors[:, 1:trunc_val]' else # using diagnostic_f's and diagnostic_egrads - (length(diagnostic_fs) > 0 && length(diagnostic_egrads) > 0) || error("Internal error: no diagnostic functions were accumulated (length(diagnostic_fs)=$(length(diagnostic_fs)))") - apply_to == "out" || error("Internal error: unexpected apply_to=$(repr(apply_to)) in the matrix-free diagnostic branch") + (length(diagnostic_fs) > 0 && length(diagnostic_egrads) > 0) || error( + "Internal error: no diagnostic functions were accumulated (length(diagnostic_fs)=$(length(diagnostic_fs)))", + ) + apply_to == "out" || + error("Internal error: unexpected apply_to=$(repr(apply_to)) in the matrix-free diagnostic branch") diagnostic_f, diagnostic_egrad, samples_mean = if length(iters) > 1 alpha_weight = zeros(length(iters)) diff --git a/src/show.jl b/src/show.jl index eef963130..41a31b011 100644 --- a/src/show.jl +++ b/src/show.jl @@ -5,8 +5,7 @@ using .Utilities using .Emulators using .MarkovChainMonteCarlo -import .MarkovChainMonteCarlo: RWMetropolisHastings, pCNMetropolisHastings, - BarkerMetropolisHastings, AutodiffProtocol +import .MarkovChainMonteCarlo: RWMetropolisHastings, pCNMetropolisHastings, BarkerMetropolisHastings, AutodiffProtocol # ── Utilities ───────────────────────────────────────────────────────────────── @@ -22,7 +21,7 @@ function Base.show(io::IO, ::MIME"text/plain", es::ElementwiseScaler) else println(io, "ElementwiseScaler") println(io, " scaling method : $(get_type(es))") - print(io, " initialized : $(!isempty(es.shift))") + print(io, " initialized : $(!isempty(es.shift))") end end @@ -49,7 +48,7 @@ function Base.show(io::IO, ::MIME"text/plain", dd::Decorrelator) if get_retain_var(dd) < 1.0 println(io, " retain_var : $(get_retain_var(dd))") end - print(io, " initialized : $(!isempty(dd.data_mean))") + print(io, " initialized : $(!isempty(dd.data_mean))") end end @@ -81,7 +80,7 @@ function Base.show(io::IO, ::MIME"text/plain", cc::CanonicalCorrelation) if get_retain_var(cc) < 1.0 println(io, " retain_var : $(get_retain_var(cc))") end - print(io, " initialized : $(!isempty(cc.data_mean))") + print(io, " initialized : $(!isempty(cc.data_mean))") end end @@ -109,7 +108,7 @@ function Base.show(io::IO, ::MIME"text/plain", li::LikelihoodInformed) if get_retain_info(li) < 1.0 println(io, " retain_info : $(get_retain_info(li))") end - print(io, " initialized : $(!isempty(li.data_mean))") + print(io, " initialized : $(!isempty(li.data_mean))") end end @@ -131,7 +130,7 @@ function Base.show(io::IO, ::MIME"text/plain", ni::NoiseInjector) println(io, " use_noise : $(ni.use_noise)") println(io, " scaling : $(ni.scaling)") println(io, " K size : $(size(ni.K,1))×$(size(ni.K,2))") - print(io, " encoder_schedule : $(length(ni.encoder_schedule)) entries") + print(io, " encoder_schedule : $(length(ni.encoder_schedule)) entries") end end @@ -173,7 +172,7 @@ function Base.show(io::IO, ::MIME"text/plain", x::GaussianProcess{P}) where {P} println(io, "GaussianProcess{", nameof(P), "}") println(io, " n_models : ", n, " (one per output dimension)") println(io, " noise_learn : ", x.noise_learn) - print(io, " pred_type : ", nameof(typeof(x.prediction_type))) + print(io, " pred_type : ", nameof(typeof(x.prediction_type))) end end @@ -185,8 +184,16 @@ end # ScalarRandomFeatureInterface function Base.show(io::IO, x::ScalarRandomFeatureInterface) - print(io, "ScalarRandomFeatureInterface (", get_input_dim(x), "→1, ", - get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") + print( + io, + "ScalarRandomFeatureInterface (", + get_input_dim(x), + "→1, ", + get_n_features(x), + " features, ", + nameof(typeof(get_kernel_structure(x))), + "(…))", + ) end function Base.show(io::IO, ::MIME"text/plain", x::ScalarRandomFeatureInterface) @@ -198,20 +205,38 @@ function Base.show(io::IO, ::MIME"text/plain", x::ScalarRandomFeatureInterface) println(io, " n_features : ", get_n_features(x)) println(io, " kernel : ", nameof(typeof(get_kernel_structure(x)))) println(io, " decomp : ", x.feature_decomposition) - print(io, " built : ", !isempty(x.rfms)) + print(io, " built : ", !isempty(x.rfms)) end end function Base.summary(io::IO, x::ScalarRandomFeatureInterface) - print(io, "ScalarRandomFeatureInterface (", get_input_dim(x), "→1, ", - get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") + print( + io, + "ScalarRandomFeatureInterface (", + get_input_dim(x), + "→1, ", + get_n_features(x), + " features, ", + nameof(typeof(get_kernel_structure(x))), + "(…))", + ) end # VectorRandomFeatureInterface function Base.show(io::IO, x::VectorRandomFeatureInterface) - print(io, "VectorRandomFeatureInterface (", get_input_dim(x), "→", get_output_dim(x), ", ", - get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") + print( + io, + "VectorRandomFeatureInterface (", + get_input_dim(x), + "→", + get_output_dim(x), + ", ", + get_n_features(x), + " features, ", + nameof(typeof(get_kernel_structure(x))), + "(…))", + ) end function Base.show(io::IO, ::MIME"text/plain", x::VectorRandomFeatureInterface) @@ -224,13 +249,23 @@ function Base.show(io::IO, ::MIME"text/plain", x::VectorRandomFeatureInterface) println(io, " n_features : ", get_n_features(x)) println(io, " kernel : ", nameof(typeof(get_kernel_structure(x)))) println(io, " decomp : ", x.feature_decomposition) - print(io, " built : ", !isempty(x.rfms)) + print(io, " built : ", !isempty(x.rfms)) end end function Base.summary(io::IO, x::VectorRandomFeatureInterface) - print(io, "VectorRandomFeatureInterface (", get_input_dim(x), "→", get_output_dim(x), ", ", - get_n_features(x), " features, ", nameof(typeof(get_kernel_structure(x))), "(…))") + print( + io, + "VectorRandomFeatureInterface (", + get_input_dim(x), + "→", + get_output_dim(x), + ", ", + get_n_features(x), + " features, ", + nameof(typeof(get_kernel_structure(x))), + "(…))", + ) end # Emulator @@ -245,7 +280,7 @@ function Base.show(io::IO, ::MIME"text/plain", x::Emulator) if get(io, :compact, false) show(io, x) else - mlt = get_machine_learning_tool(x) + mlt = get_machine_learning_tool(x) n_in, n_out = size(get_io_pairs(x), 1) n_train = size(DataContainers.get_inputs(get_io_pairs(x)), 2) enc_sch = get_encoder_schedule(x) @@ -254,7 +289,7 @@ function Base.show(io::IO, ::MIME"text/plain", x::Emulator) println(io, " input_dim : ", n_in) println(io, " output_dim : ", n_out) println(io, " n_train : ", n_train, " samples") - _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") + _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") _show_encoder_line(io, enc_sch, n_out, "out", "encoder (output) ") end end @@ -277,15 +312,15 @@ function Base.show(io::IO, ::MIME"text/plain", x::ForwardMapWrapper) show(io, x) else n_in, n_out = size(get_io_pairs(x), 1) - enc_sch = get_encoder_schedule(x) - ni = x.noise_injector + enc_sch = get_encoder_schedule(x) + ni = x.noise_injector println(io, "ForwardMapWrapper") println(io, " input_dim : ", n_in) println(io, " output_dim : ", n_out) println(io, " prior_dim : ", ndims(get_prior(x))) - _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") + _show_encoder_line(io, enc_sch, n_in, "in", "encoder (input) ") _show_encoder_line(io, enc_sch, n_out, "out", "encoder (output) ") - print(io, " noise_inject : ", !isnothing(ni) && ni.use_noise) + print(io, " noise_inject : ", !isnothing(ni) && ni.use_noise) end end @@ -364,17 +399,17 @@ function Base.show(io::IO, ::MIME"text/plain", mcmc::MCMCWrapper) if get(io, :compact, false) show(io, mcmc) else - n_par = ndims(mcmc.prior) - n_obs = length(mcmc.observations) - obs_dim = isempty(mcmc.observations) ? nothing : length(first(mcmc.observations)) + n_par = ndims(mcmc.prior) + n_obs = length(mcmc.observations) + obs_dim = isempty(mcmc.observations) ? nothing : length(first(mcmc.observations)) sampler_name = nameof(typeof(mcmc.mh_proposal_sampler)) - enc_sch = get_encoder_schedule(mcmc) + enc_sch = get_encoder_schedule(mcmc) println(io, "MCMCWrapper") println(io, " prior_dim : ", n_par, " parameter", n_par == 1 ? "" : "s") isnothing(obs_dim) || println(io, " obs_dim : ", obs_dim) println(io, " n_obs : ", n_obs, " sample", n_obs == 1 ? "" : "s") println(io, " sampler : ", sampler_name) - _show_encoder_line(io, enc_sch, n_par, "in", "encoder (input) ") + _show_encoder_line(io, enc_sch, n_par, "in", "encoder (input) ") isnothing(obs_dim) || _show_encoder_line(io, enc_sch, obs_dim, "out", "encoder (output)") end end diff --git a/test/Show/runtests.jl b/test/Show/runtests.jl index fbded5330..fe186a2be 100644 --- a/test/Show/runtests.jl +++ b/test/Show/runtests.jl @@ -160,11 +160,9 @@ const MCMC = MarkovChainMonteCarlo @testset "ForwardMapWrapper" begin prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 3) - iop = PairedDataContainer(rand(3, 5), rand(2, 5), data_are_columns = true) - ni = NoiseInjector(rand(2, 3), rand(3, 1), rand(2, 1), nothing, 0.5, true, []) - fmw = ForwardMapWrapper{Float64, Vector{Any}, typeof(prior), typeof(ni)}( - identity, prior, iop, iop, [], ni, - ) + iop = PairedDataContainer(rand(3, 5), rand(2, 5), data_are_columns = true) + ni = NoiseInjector(rand(2, 3), rand(3, 1), rand(2, 1), nothing, 0.5, true, []) + fmw = ForwardMapWrapper{Float64, Vector{Any}, typeof(prior), typeof(ni)}(identity, prior, iop, iop, [], ni) out = sprint(show, MIME("text/plain"), fmw) @test occursin("ForwardMapWrapper", out) @test count(==('\n'), out) <= 10 @@ -182,7 +180,7 @@ const MCMC = MarkovChainMonteCarlo @testset "RWMetropolisHastings" begin prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) - rw = MCMC.RWMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) + rw = MCMC.RWMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) out = sprint(show, MIME("text/plain"), rw) @test occursin("RWMetropolisHastings", out) @test count(==('\n'), out) <= 10 @@ -198,7 +196,7 @@ const MCMC = MarkovChainMonteCarlo @testset "pCNMetropolisHastings" begin prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) - pcn = MCMC.pCNMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) + pcn = MCMC.pCNMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) out = sprint(show, MIME("text/plain"), pcn) @test occursin("pCNMetropolisHastings", out) @test count(==('\n'), out) <= 10 @@ -213,7 +211,7 @@ const MCMC = MarkovChainMonteCarlo end @testset "BarkerMetropolisHastings" begin - prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) barkr = MCMC.BarkerMetropolisHastings{typeof(prop), ForwardDiffProtocol}(prop) out = sprint(show, MIME("text/plain"), barkr) @test occursin("BarkerMetropolisHastings", out) @@ -229,14 +227,13 @@ const MCMC = MarkovChainMonteCarlo end @testset "MCMCWrapper" begin - prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 2) - obs = [rand(2) for _ in 1:3] - log_post = AdvancedMH.DensityModel(x -> -0.5 * sum(x .^ 2)) - prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) + prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 2) + obs = [rand(2) for _ in 1:3] + log_post = AdvancedMH.DensityModel(x -> -0.5 * sum(x .^ 2)) + prop = AdvancedMH.RandomWalkProposal(MvNormal(zeros(2), I(2))) rw_sampler = MCMC.RWMetropolisHastings{typeof(prop), GradFreeProtocol}(prop) - mcmcw = MCMCWrapper{typeof(obs), typeof(obs), Vector{Any}}( - prior, prior, obs, obs, log_post, rw_sampler, (;), [], - ) + mcmcw = + MCMCWrapper{typeof(obs), typeof(obs), Vector{Any}}(prior, prior, obs, obs, log_post, rw_sampler, (;), []) out = sprint(show, MIME("text/plain"), mcmcw) @test occursin("MCMCWrapper", out) @test count(==('\n'), out) <= 10 From 873daef903daba7436c98cac18a0994ce719162a Mon Sep 17 00:00:00 2001 From: odunbar Date: Mon, 25 May 2026 00:45:57 -0700 Subject: [PATCH 08/14] resolve silent test fail with skill change --- .claude/skills/error-message-manager/SKILL.md | 39 ++++++++++--------- test/MarkovChainMonteCarlo/runtests.jl | 4 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/.claude/skills/error-message-manager/SKILL.md b/.claude/skills/error-message-manager/SKILL.md index df6fa1827..e87a1eee4 100644 --- a/.claude/skills/error-message-manager/SKILL.md +++ b/.claude/skills/error-message-manager/SKILL.md @@ -394,26 +394,35 @@ julia --project -e 'using CalibrateEmulateSample' ### Step 7 — Add @test_throws tests -Before writing any test, check whether coverage already exists. Grep the matching -`test//runtests.jl` for the public API function that reaches the rewritten -site: +#### Step 7a — Scan for stale @test_throws (run this before editing any source line) + +Whenever you change an exception type (e.g. `ErrorException` → `ArgumentError`), +run this grep **before** touching the source file: ```bash -grep -n '@test_throws' test//runtests.jl | grep '' +grep -rn '@test_throws OldType' test/ ``` -**Before changing an exception type**, also grep for existing tests that assert the -*old* type — they will silently become stale after your edit: +Every hit is a test that will break or silently go stale the moment your source +edit lands. Update each hit **in the same edit as the source change** — never as a +follow-up. This is not optional: a stale `@test_throws WrongType` either fails CI +on the new code or passes forever without catching a regression — either way it +no longer protects anything. + +This step is easy to skip because source and tests live in different files and the +stale test produces no compile error — it only surfaces when the test suite runs. +Running the grep first makes the problem visible before it bites you. + +#### Step 7b — Check existing coverage + +Before writing any test, check whether coverage already exists. Grep the matching +`test//runtests.jl` for the public API function that reaches the rewritten +site: ```bash -grep -rn '@test_throws OldType' test/ +grep -n '@test_throws' test//runtests.jl | grep '' ``` -Update every hit in the same edit that changes the source. A stale -`@test_throws WrongType` will either pass against old code and fail once your -rewrite lands, or pass forever without catching a regression — either way it's -a test that no longer protects anything. - Three outcomes: | Situation | Action | @@ -452,12 +461,6 @@ Use the specific exception type — never bare `@test_throws Exception`. The tes should construct the minimal invalid input that triggers the new error, without duplicating happy-path coverage. -**Update existing tests that used the wrong type.** If the file already has a -`@test_throws ErrorException` (or any other type) for a site you're rewriting to -`ArgumentError`, update that existing test in the same edit. Leaving a stale -`@test_throws ErrorException` will cause it to pass against the old code but fail -once your rewrite lands — or vice versa. - **Testing unexported helpers.** If the site is inside an unexported helper (e.g., `construct_constraint`, `construct_2d_array`), do not `import` the internal directly. Instead, test through the nearest exported public API function that diff --git a/test/MarkovChainMonteCarlo/runtests.jl b/test/MarkovChainMonteCarlo/runtests.jl index ab954c002..5a516f8b1 100644 --- a/test/MarkovChainMonteCarlo/runtests.jl +++ b/test/MarkovChainMonteCarlo/runtests.jl @@ -659,7 +659,9 @@ end # GPJL doesnt support ForwardDiff bad_mcmc_params = deepcopy(mcmc_params) bad_mcmc_params[:mcmc_alg] = mcmc_algs[2] - @test_throws ErrorException mcmc_test_template(prior, σ2_y, em_1; bad_mcmc_params...) + let thrown = @test_throws ArgumentError mcmc_test_template(prior, σ2_y, em_1; bad_mcmc_params...) + @test contains(thrown.value.msg, "does not implement the required emulator interface") + end for alg in mcmc_algs mcmc_params_ad = deepcopy(mcmc_params) From 827c5c3e72839c113007cabba09c23b906e8dee0 Mon Sep 17 00:00:00 2001 From: odunbar Date: Mon, 25 May 2026 01:04:12 -0700 Subject: [PATCH 09/14] downgrade docs manifest --- docs/Manifest.toml | 70 +++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 9567c231a..0158d3c84 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.0" +julia_version = "1.11.5" manifest_format = "2.0" -project_hash = "5255fc92659f2799f0f4c5144942145e13d951ed" +project_hash = "e4d965798d55f5903483b71f545533e62ea32fdf" [[deps.ADTypes]] git-tree-sha1 = "bbc22a9a08a0ef6460041086d8a7b27940ed4ffd" @@ -354,7 +354,7 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" +version = "1.1.1+0" [[deps.CompositionsBase]] git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" @@ -901,11 +901,6 @@ version = "1.6.0" [deps.JSON.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - [[deps.KernelDensity]] deps = ["Distributions", "DocStringExtensions", "FFTA", "Interpolations", "StatsBase"] git-tree-sha1 = "4260cfc991b8885bf747801fb60dd4503250e478" @@ -975,24 +970,24 @@ uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" version = "0.6.4" [[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.11.1+1" +version = "8.6.0+0" [[deps.LibGit2]] -deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" version = "1.11.0" [[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.9.0+0" +version = "1.7.2+0" [[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.3+1" +version = "1.11.0+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -1031,7 +1026,7 @@ version = "7.5.1" [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.12.0" +version = "1.11.0" [[deps.LinearMaps]] deps = ["LinearAlgebra"] @@ -1229,7 +1224,7 @@ uuid = "d125e4d3-2237-4719-b19c-fa641b8a4667" version = "0.1.8" [[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" version = "1.11.0" @@ -1259,6 +1254,11 @@ git-tree-sha1 = "ff17d35ab669720c3dd9cb0a214eb75425542dc8" uuid = "99c1a7ee-ab34-5fd5-8076-27c950a045f4" version = "2.5.8" +[[deps.MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.6+0" + [[deps.Missings]] deps = ["DataAPI"] git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" @@ -1271,7 +1271,7 @@ version = "1.11.0" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.5.20" +version = "2023.12.12" [[deps.MuladdMacro]] git-tree-sha1 = "cac9cc5499c25554cba55cd3c30543cff5ca4fab" @@ -1322,7 +1322,7 @@ version = "1.0.0" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.3.0" +version = "1.2.0" [[deps.Nullables]] git-tree-sha1 = "8f87854cc8f3685a60689d8edecaa29d2251979b" @@ -1353,12 +1353,12 @@ version = "0.3.33+1" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.29+0" +version = "0.3.27+1" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.7+0" +version = "0.8.5+0" [[deps.OpenSSH_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] @@ -1367,9 +1367,10 @@ uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" version = "10.3.1+0" [[deps.OpenSSL_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "2ac022577e5eac7da040de17776d51bb770cd895" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.1+0" +version = "3.5.6+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -1401,7 +1402,7 @@ version = "1.8.1" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.44.0+1" +version = "10.42.0+1" [[deps.PDMats]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] @@ -1430,7 +1431,7 @@ version = "0.46.4+0" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.0" +version = "1.11.0" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -1456,9 +1457,9 @@ version = "0.2.4" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" +git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.4" +version = "1.2.1" [[deps.Preferences]] deps = ["TOML"] @@ -1490,7 +1491,6 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" version = "1.11.0" [[deps.Profile]] -deps = ["StyledStrings"] uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" version = "1.11.0" @@ -1542,7 +1542,7 @@ uuid = "94ee1d12-ae83-5a48-8b1c-48b8ff168ae0" version = "0.7.7" [[deps.REPL]] -deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] +deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" version = "1.11.0" @@ -1723,7 +1723,7 @@ version = "1.2.2" [[deps.SparseArrays]] deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.12.0" +version = "1.11.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] @@ -1846,7 +1846,7 @@ uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" [[deps.SuiteSparse_jll]] deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.8.3+2" +version = "7.7.0+0" [[deps.TOML]] deps = ["Dates"] @@ -2010,7 +2010,7 @@ version = "1.6.0+0" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.3.1+2" +version = "1.2.13+1" [[deps.ZygoteRules]] deps = ["ChainRulesCore", "MacroTools"] @@ -2033,7 +2033,7 @@ version = "0.17.4+0" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.13.1+1" +version = "5.11.0+0" [[deps.libdrm_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libpciaccess_jll"] @@ -2068,7 +2068,7 @@ version = "1.3.8+0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.64.0+1" +version = "1.59.0+0" [[deps.oneTBB_jll]] deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] @@ -2079,7 +2079,7 @@ version = "2022.3.0+0" [[deps.p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.5.0+2" +version = "17.4.0+2" [[deps.x264_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] From 5ffb96de571c5536959a10cfba37b188386618ea Mon Sep 17 00:00:00 2001 From: odunbar Date: Mon, 25 May 2026 12:13:25 -0700 Subject: [PATCH 10/14] add confirmation test when we make a suggestion --- .claude/skills/error-message-manager/SKILL.md | 28 +++++++++++++++++++ src/Emulator.jl | 2 +- test/Emulator/runtests.jl | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.claude/skills/error-message-manager/SKILL.md b/.claude/skills/error-message-manager/SKILL.md index e87a1eee4..51c3c4f43 100644 --- a/.claude/skills/error-message-manager/SKILL.md +++ b/.claude/skills/error-message-manager/SKILL.md @@ -477,6 +477,34 @@ end This keeps tests coupled to the public contract and avoids brittleness when internal function names change. +#### Step 7c — Add a resolution test when the Suggestion is a concrete fix + +When the Suggestion section gives a **single concrete API change** — a keyword +argument, a reshape call, a different constructor argument — add a resolution test +immediately after the `@test_throws` block. The test calls the same function with +the suggested fix applied and asserts the result is valid. If the suggestion is +ever wrong or the API changes, this test will fail and flag the stale message. + +```julia +# @test_throws block (Step 7b) +let thrown = @test_throws DimensionMismatch predict(emulator, bad_inputs) + @test contains(thrown.value.msg, "encode") + @test contains(thrown.value.msg, "\"in\"") +end + +# Resolution test (Step 7c) — following the suggestion must not throw +@test predict(emulator, bad_inputs; encode = "in") isa AbstractMatrix +``` + +Use a plain `@test f(...) isa T` — Julia's test runner errors if `f(...)` throws, +so no special macro is needed. Pick the narrowest type assertion that makes sense +(`isa AbstractMatrix`, `isa AbstractVector`, `isa Nothing`). + +**Only apply this when the Suggestion is a single-call fix.** Skip it for advisory +suggestions ("check that your data is not degenerate", "increase `alg_reg_noise`", +"pass a symmetric matrix") where no minimal corrected call can be constructed from +the same bad input. + ### Step 8 — Offer to improve the skill Once the rewrites and tests are clean, offer: "Would you like to improve the diff --git a/src/Emulator.jl b/src/Emulator.jl index b6dcf577e..cb0514f57 100644 --- a/src/Emulator.jl +++ b/src/Emulator.jl @@ -589,7 +589,7 @@ Got: Suggestion: Pass new_inputs with $expected rows (one per input dimension). - If inputs are already in the encoded space, set `in_already_encoded = true`. + If inputs are already in the encoded space, pass `encode = "in"` to predict(...). """)) end diff --git a/test/Emulator/runtests.jl b/test/Emulator/runtests.jl index 75f26670a..fcf2c98ae 100644 --- a/test/Emulator/runtests.jl +++ b/test/Emulator/runtests.jl @@ -148,6 +148,8 @@ end @test contains(thrown.value.msg, "ForwardMapWrapper") @test contains(thrown.value.msg, "new_inputs row count") end + # Resolution test (Step 7c): passing already-encoded inputs with encode = "in" must not throw + @test EM.predict(fmw, encode_data(fmw, x_test, "in"); encode = "in") isa Tuple # with out enc. fmw = forward_map_wrapper(G, prior, io_pairs, encoder_kwargs = (; obs_noise_cov = Σ)) From 5a38c56345b32e9d0ce961d7b19f507f0937cf63 Mon Sep 17 00:00:00 2001 From: odunbar Date: Mon, 25 May 2026 12:21:03 -0700 Subject: [PATCH 11/14] fine tune skill --- .claude/skills/error-message-manager/SKILL.md | 39 +++++++++++++------ test/Utilities/runtests.jl | 2 + 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.claude/skills/error-message-manager/SKILL.md b/.claude/skills/error-message-manager/SKILL.md index 51c3c4f43..02483fd17 100644 --- a/.claude/skills/error-message-manager/SKILL.md +++ b/.claude/skills/error-message-manager/SKILL.md @@ -479,11 +479,20 @@ internal function names change. #### Step 7c — Add a resolution test when the Suggestion is a concrete fix -When the Suggestion section gives a **single concrete API change** — a keyword -argument, a reshape call, a different constructor argument — add a resolution test -immediately after the `@test_throws` block. The test calls the same function with -the suggested fix applied and asserts the result is valid. If the suggestion is -ever wrong or the API changes, this test will fail and flag the stale message. +When the Suggestion section gives a **concrete, demonstrable fix**, add a resolution +test immediately after the `@test_throws` block. The test applies the fix and asserts +the result is valid. If the suggestion is ever wrong or the API changes, this test +will fail and flag the stale message. + +A fix qualifies as concrete when it can be demonstrated in a short test expression: +- A keyword argument change (`encode = "in"`) +- A reshape or transpose of an argument (`reshape(x, :, 1)`) +- Replacing one construction with another (`Diagonal(fill(λ, d))` instead of `λ * I`) +- A different constructor or type choice (`ForwardDiffProtocol` instead of a custom one) + +The fix doesn't have to be on the exact same call expression as the error — it's fine +to construct corrected input in a line or two before the assertion. What matters is +that the test is short, self-contained, and directly demonstrates the suggestion. ```julia # @test_throws block (Step 7b) @@ -494,16 +503,22 @@ end # Resolution test (Step 7c) — following the suggestion must not throw @test predict(emulator, bad_inputs; encode = "in") isa AbstractMatrix + +# Another example — fix requires constructing corrected input first +let thrown = @test_throws ArgumentError create_compact_linear_map(3 * I) + @test contains(thrown.value.msg, "Diagonal") +end +@test size(create_compact_linear_map(Diagonal(fill(3.0, 3)))) == (3, 3) ``` -Use a plain `@test f(...) isa T` — Julia's test runner errors if `f(...)` throws, -so no special macro is needed. Pick the narrowest type assertion that makes sense -(`isa AbstractMatrix`, `isa AbstractVector`, `isa Nothing`). +Use a plain `@test f(...) isa T` or `@test size(f(...)) == (m, n)` — Julia's test +runner errors if `f(...)` throws, so no special macro is needed. Pick the narrowest +assertion that makes sense for the return type. -**Only apply this when the Suggestion is a single-call fix.** Skip it for advisory -suggestions ("check that your data is not degenerate", "increase `alg_reg_noise`", -"pass a symmetric matrix") where no minimal corrected call can be constructed from -the same bad input. +**Skip the resolution test** for advisory suggestions where the fix requires +domain knowledge or data the test can't construct: "check that your data is not +degenerate", "increase `alg_reg_noise`", "pass a symmetric matrix". The dividing +line is whether the fix can be expressed as a small, self-contained code change. ### Step 8 — Offer to improve the skill diff --git a/test/Utilities/runtests.jl b/test/Utilities/runtests.jl index e74f6621c..d6d555cae 100644 --- a/test/Utilities/runtests.jl +++ b/test/Utilities/runtests.jl @@ -169,6 +169,8 @@ end @test contains(thrown.value.msg, "UniformScaling") @test contains(thrown.value.msg, "Diagonal") end + # Resolution test (Step 7c): replacing λI with Diagonal(fill(λ, d)) must not throw + @test size(create_compact_linear_map(Diagonal(fill(3.0, 3)))) == (3, 3) for svd_type in ["psvd", "tsvd"] psvd_kwargs = (; rtol = 1e-3) # make very small for testing From d3fc8a6a8c43b80d72de1940da54c4933efae13a Mon Sep 17 00:00:00 2001 From: odunbar Date: Tue, 26 May 2026 10:59:11 -0700 Subject: [PATCH 12/14] use EMM to improve codecov --- test/MarkovChainMonteCarlo/runtests.jl | 6 ++++- test/RandomFeature/runtests.jl | 31 ++++++++++++++++++++++++++ test/Utilities/runtests.jl | 23 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/test/MarkovChainMonteCarlo/runtests.jl b/test/MarkovChainMonteCarlo/runtests.jl index 5a516f8b1..17e9e5fb2 100644 --- a/test/MarkovChainMonteCarlo/runtests.jl +++ b/test/MarkovChainMonteCarlo/runtests.jl @@ -654,7 +654,11 @@ end bad_mcmc_params = deepcopy(mcmc_params) bad_mcmc_params[:mcmc_alg] = bad_mcmc_alg - @test_throws ArgumentError mcmc_test_template(prior, σ2_y, em_1; bad_mcmc_params...) + let thrown = @test_throws ArgumentError mcmc_test_template(prior, σ2_y, em_1; bad_mcmc_params...) + @test contains(thrown.value.msg, "autodiff_gradient") + @test contains(thrown.value.msg, "GradFreeProtocol") + @test contains(thrown.value.msg, "ForwardDiffProtocol") + end # GPJL doesnt support ForwardDiff bad_mcmc_params = deepcopy(mcmc_params) diff --git a/test/RandomFeature/runtests.jl b/test/RandomFeature/runtests.jl index 274e27ffe..cb5de9e92 100644 --- a/test/RandomFeature/runtests.jl +++ b/test/RandomFeature/runtests.jl @@ -342,6 +342,37 @@ rng = Random.MersenneTwister(seed) ) @test_throws ArgumentError Emulator(srfi_bad, iopairs) + # test cross-validation split too large (Scalar RF): train_fraction=0.5 → n_test=25, 25*3=75 > n=50 + let srfi_bad_cv = ScalarRandomFeatureInterface( + n_features, + input_dim, + kernel_structure = scalar_ks, + rng = rng, + optimizer_options = Dict("train_fraction" => 0.5, "n_cross_val_sets" => 3), + ) + thrown = @test_throws ArgumentError Emulator(srfi_bad_cv, iopairs) + @test contains(thrown.value.msg, "n_cross_val_sets") + @test contains(thrown.value.msg, "n_test") + @test contains(thrown.value.msg, "train_fraction") + @test contains(thrown.value.msg, string(n)) + end + + # test cross-validation split too large (Vector RF) + let vrfi_bad_cv = VectorRandomFeatureInterface( + n_features, + input_dim, + output_dim, + kernel_structure = vector_ks, + rng = rng, + optimizer_options = Dict("train_fraction" => 0.5, "n_cross_val_sets" => 3), + ) + thrown = @test_throws ArgumentError Emulator(vrfi_bad_cv, iopairs) + @test contains(thrown.value.msg, "n_cross_val_sets") + @test contains(thrown.value.msg, "n_test") + @test contains(thrown.value.msg, "train_fraction") + @test contains(thrown.value.msg, string(n)) + end + # test under repeats @test_logs (:info,) (:warn,) (:info,) (:warn,) Emulator( srfi, diff --git a/test/Utilities/runtests.jl b/test/Utilities/runtests.jl index d6d555cae..27db23c26 100644 --- a/test/Utilities/runtests.jl +++ b/test/Utilities/runtests.jl @@ -46,7 +46,7 @@ using CalibrateEmulateSample.ParameterDistributions @test_throws ArgumentError get_training_points(ekp, 1:2, g_final = g_ens_final') #positive definiteness - mat = reshape(collect(-3:(-3 + 10^2 - 1)), 10, 10) + mat = reshape(collect(-3:(-3 + 10 ^ 2 - 1)), 10, 10) tol = 1e12 * eps() @test !isposdef(mat) @@ -586,7 +586,28 @@ end schedule2b = create_encoder_schedule(sch2b) @test_throws ArgumentError initialize_and_encode_with_schedule!(schedule2b, io_pairs; prior_cov = prior_cov) + # test _throw_insufficient_cca_samples: 5 input dims but only 3 samples → triggers + let cc_sch = create_encoder_schedule((canonical_correlation(), "in")) + bad_in = rand(rng, 5, 3) + bad_out = rand(rng, 3, 3) + bad_io = PairedDataContainer(bad_in, bad_out) + thrown = @test_throws ArgumentError initialize_and_encode_with_schedule!(cc_sch, bad_io) + @test contains(thrown.value.msg, "CanonicalCorrelation") + @test contains(thrown.value.msg, "samples") + @test contains(thrown.value.msg, "(5, 3)") + end + # test invalid decorrelate_with value + let bad_sch = create_encoder_schedule((decorrelate(decorrelate_with = "bad_value"), "in")) + thrown = @test_throws ArgumentError initialize_and_encode_with_schedule!(bad_sch, io_pairs) + @test contains(thrown.value.msg, "decorrelate_with") + @test contains(thrown.value.msg, repr("bad_value")) + end + # resolution: a valid decorrelate_with does not throw + @test initialize_and_encode_with_schedule!( + create_encoder_schedule((decorrelate(decorrelate_with = "sample_cov"), "in")), + io_pairs, + ) isa Tuple # combine a few lossless encoding schedules (lossless requires samples>dims) samples = 150 # for full test coverage have samples in_dim < samples < out_dim From 093e843ba4dc48f23fa7e48f611466393503247b Mon Sep 17 00:00:00 2001 From: odunbar Date: Tue, 26 May 2026 13:34:53 -0700 Subject: [PATCH 13/14] codecov --- test/Show/runtests.jl | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/Show/runtests.jl b/test/Show/runtests.jl index fe186a2be..a0e457501 100644 --- a/test/Show/runtests.jl +++ b/test/Show/runtests.jl @@ -48,6 +48,20 @@ const MCMC = MarkovChainMonteCarlo @test !occursin('\n', outs) end + @testset "Decorrelator retain_var branch" begin + dd = decorrelate_sample_cov(; retain_var = 0.9) + out = sprint(show, MIME("text/plain"), dd) + @test occursin("Decorrelator", out) + @test occursin("retain_var", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, dd) + @test occursin("Decorrelator", out2) + @test occursin("retain_var", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), dd; context = :compact => true) + @test out2 == out3 + end + @testset "CanonicalCorrelation" begin cc = canonical_correlation() out = sprint(show, MIME("text/plain"), cc) @@ -63,6 +77,21 @@ const MCMC = MarkovChainMonteCarlo @test !occursin('\n', outs) end + @testset "CanonicalCorrelation apply_to branch" begin + cc = canonical_correlation() + push!(get_apply_to(cc), "in") + out = sprint(show, MIME("text/plain"), cc) + @test occursin("CanonicalCorrelation", out) + @test occursin("apply_to", out) + @test count(==('\n'), out) <= 10 + out2 = sprint(show, cc) + @test occursin("CanonicalCorrelation", out2) + @test occursin("apply_to", out2) + @test !occursin('\n', out2) + out3 = sprint(show, MIME("text/plain"), cc; context = :compact => true) + @test out2 == out3 + end + @testset "LikelihoodInformed" begin li = likelihood_informed() out = sprint(show, MIME("text/plain"), li) @@ -158,6 +187,17 @@ const MCMC = MarkovChainMonteCarlo @test !occursin('\n', outs) end + @testset "Emulator encoder line branch" begin + n, p, d = 20, 3, 2 + iop = PairedDataContainer(rand(p, n), rand(d, n), data_are_columns = true) + gp = GaussianProcess(GPJL()) + em = Emulator(gp, iop) # default encoder schedule: initialized, non-empty + out = sprint(show, MIME("text/plain"), em) + @test occursin("Emulator", out) + @test occursin("→", out) # _show_encoder_line printed at least one encoder line + @test count(==('\n'), out) <= 10 + end + @testset "ForwardMapWrapper" begin prior = constrained_gaussian("x", 0.0, 1.0, -Inf, Inf; repeats = 3) iop = PairedDataContainer(rand(3, 5), rand(2, 5), data_are_columns = true) From b75cf891643b370a644b11a8a598fd5f7bbd6bed Mon Sep 17 00:00:00 2001 From: odunbar Date: Tue, 26 May 2026 13:46:07 -0700 Subject: [PATCH 14/14] format --- test/Utilities/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Utilities/runtests.jl b/test/Utilities/runtests.jl index 27db23c26..2b267bcae 100644 --- a/test/Utilities/runtests.jl +++ b/test/Utilities/runtests.jl @@ -46,7 +46,7 @@ using CalibrateEmulateSample.ParameterDistributions @test_throws ArgumentError get_training_points(ekp, 1:2, g_final = g_ens_final') #positive definiteness - mat = reshape(collect(-3:(-3 + 10 ^ 2 - 1)), 10, 10) + mat = reshape(collect(-3:(-3 + 10^2 - 1)), 10, 10) tol = 1e12 * eps() @test !isposdef(mat)