From d9d9a85a4164a239dd75fc0bf60bb653186c8779 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Mon, 27 Jul 2026 02:48:07 -0400 Subject: [PATCH] Adopt strict SurrogatesBase API QA Co-Authored-By: Chris Rackauckas --- Project.toml | 8 +- docs/make.jl | 8 +- docs/src/api.md | 3 +- docs/src/index.md | 60 +------- docs/src/interface.md | 146 +++++++------------ src/SurrogatesBase.jl | 271 ++++++++++++++++++++--------------- test/developer_interfaces.jl | 72 ++++++++++ test/qa/Project.toml | 12 +- test/qa/qa.jl | 95 +----------- 9 files changed, 290 insertions(+), 385 deletions(-) create mode 100644 test/developer_interfaces.jl diff --git a/Project.toml b/Project.toml index 83bc082..9bcf798 100644 --- a/Project.toml +++ b/Project.toml @@ -5,19 +5,15 @@ version = "1.1.3" [compat] AllocCheck = "0.2" -Aqua = "0.8" -JET = "0.9, 0.10, 0.11" LinearAlgebra = "1.10" SafeTestsets = "0.1" -SciMLTesting = "2.1" +SciMLTesting = "2.4" Statistics = "1.10" Test = "1.10" julia = "1.10" [extras] AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" @@ -25,4 +21,4 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AllocCheck", "Aqua", "JET", "LinearAlgebra", "SafeTestsets", "SciMLTesting", "Statistics", "Test"] +test = ["AllocCheck", "LinearAlgebra", "SafeTestsets", "SciMLTesting", "Statistics", "Test"] diff --git a/docs/make.jl b/docs/make.jl index ac28b3b..e0b0016 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,13 +6,10 @@ DocMeta.setdocmeta!( :(using SurrogatesBase) ) -cp("./docs/Manifest.toml", "./docs/src/assets/Manifest.toml", force = true) -cp("./docs/Project.toml", "./docs/src/assets/Project.toml", force = true) - pages = [ "Home" => "index.md", - "interface.md", - "api.md", + "Developer Interface" => "interface.md", + "Public API" => "api.md", ] ENV["GKSwstype"] = "100" @@ -21,6 +18,7 @@ makedocs( modules = [SurrogatesBase], sitename = "SurrogatesBase.jl", clean = true, + checkdocs = :exports, doctest = true, linkcheck = true, format = Documenter.HTML( diff --git a/docs/src/api.md b/docs/src/api.md index ccbe5c0..e2f0681 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,6 +1,7 @@ -# API +# Public API ```@docs +SurrogatesBase SurrogatesBase.AbstractDeterministicSurrogate SurrogatesBase.AbstractStochasticSurrogate SurrogatesBase.update! diff --git a/docs/src/index.md b/docs/src/index.md index 6ef180c..2ff5a27 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -24,7 +24,7 @@ Pkg.add("SurrogatesBase") ## Contributing - Please refer to the - [SciML ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://github.com/SciML/ColPrac/blob/master/README.md) + [SciML ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://docs.sciml.ai/ColPrac/stable/) for guidance on PRs, issues, and other matters relating to contributing to SciML. - See the [SciML Style Guide](https://github.com/SciML/SciMLStyle) for common coding practices and other style decisions. @@ -36,61 +36,3 @@ Pkg.add("SurrogatesBase") [Julia Zulip](https://julialang.zulipchat.com/#narrow/stream/279055-sciml-bridged) + On the [Julia Discourse forums](https://discourse.julialang.org) + See also [SciML Community page](https://sciml.ai/community/) - -## Reproducibility - -```@raw html -
The documentation of this SciML package was built using these direct dependencies, -``` - -```@example -using Pkg # hide -Pkg.status() # hide -``` - -```@raw html -
-``` - -```@raw html -
and using this machine and Julia version. -``` - -```@example -using InteractiveUtils # hide -versioninfo() # hide -``` - -```@raw html -
-``` - -```@raw html -
A more complete overview of all dependencies and their versions is also provided. -``` - -```@example -using Pkg # hide -Pkg.status(; mode = PKGMODE_MANIFEST) # hide -``` - -```@raw html -
-``` - -```@eval -using TOML -using Markdown -version = TOML.parse(read("../../Project.toml", String))["version"] -name = TOML.parse(read("../../Project.toml", String))["name"] -link_manifest = "https://github.com/SciML/" * name * ".jl/tree/gh-pages/v" * version * - "/assets/Manifest.toml" -link_project = "https://github.com/SciML/" * name * ".jl/tree/gh-pages/v" * version * - "/assets/Project.toml" -Markdown.parse("""You can also download the -[manifest]($link_manifest) -file and the -[project]($link_project) -file. -""") -``` diff --git a/docs/src/interface.md b/docs/src/interface.md index ae9aaab..d244e79 100644 --- a/docs/src/interface.md +++ b/docs/src/interface.md @@ -1,128 +1,80 @@ -# The SurrogateBase Interface +# Extending SurrogatesBase -## Deterministic Surrogates - -Deterministic surrogates `s` are subtypes of `SurrogatesBase.AbstractDeterministicSurrogate`, -which is a subtype of `Function`. - -### Required methods - -The method `update!(s, xs, ys)` **must** be implemented and the surrogate **must** be -[callable](https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects) -`s(xs)`, where `xs` is a `Vector` of input points and `ys` is a `Vector` of corresponding evaluations. +SurrogatesBase defines the small public contract shared by deterministic and stochastic surrogate +packages. It supplies interface tags and generic functions only; concrete packages own fitting, +evaluation, posterior representations, and data storage. -Calling `update!(s, xs, ys)` refits the surrogate `s` to include evaluations `ys` at points `xs`. -The result of `s(xs)` is a `Vector` of evaluations of the surrogate at points `xs`, corresponding to approximations of the underlying function at points `xs` respectively. - -For single points `x` and `y`, call these methods via `update!(s, [x], [y])` -and `s([x])`. +## Deterministic Surrogates -### Optional methods +A deterministic implementation subtypes [`AbstractDeterministicSurrogate`](@ref) and is callable. +The minimum interface is: -If the surrogate `s` wants to expose current parameter values, the method `parameters(s)` **must** be implemented. +```julia +(surrogate)(x) +update!(surrogate, new_x, new_y) +``` -If the surrogate `s` has tunable hyperparameters, the methods -`update_hyperparameters!(s, prior)` and `hyperparameters(s)` **must** be implemented. +`x` can be a scalar, one point, or a batch, according to the concrete package's documented domain. +The base interface does not impose a container representation. A batch call must preserve the +correspondence between its inputs and predictions. -Calling `update_hyperparameters!(s, prior)` updates the hyperparameters of the surrogate `s` by performing hyperparameter optimization using the information in `prior`. After the hyperparameters of `s` are updated, `s` is fit to past evaluations. -Calling `hyperparameters(s)` returns current values of hyperparameters. +`update!` is in place. `new_x` and `new_y` represent matching observations: a batch has the same +number of locations and values in the same order, and a scalar update represents exactly one pair. +Implementations may return `nothing`, the updated surrogate, or another implementation-specific +value. Generic code must inspect the mutated surrogate instead of relying on that return value. -### Example +An implementation may additionally expose [`parameters`](@ref), [`hyperparameters`](@ref), and +[`update_hyperparameters!`](@ref). These are optional, so consumers should only call them when a +concrete surrogate documents support. ```julia using SurrogatesBase -struct RBF{T} <: AbstractDeterministicSurrogate - scale::T - centers::Vector{T} - weights::Vector{T} +mutable struct LinearMock <: AbstractDeterministicSurrogate + slope::Float64 end -(rbf::RBF)(xs) = [rbf.weights' * exp.(-rbf.scale * (x .- rbf.centers).^2) - for x in xs] +(surrogate::LinearMock)(x) = surrogate.slope * x -function SurrogatesBase.update!(rbf::RBF, xs, ys) - # Refit the surrogate by updating rbf.weights to include new - # evaluations ys at points xs - return rbf -end - -SurrogatesBase.parameters(rbf::RBF) = rbf.centers, rbf.weights - -SurrogatesBase.hyperparameters(rbf::RBF) = rbf.scale - -function SurrogatesBase.update_hyperparameters!(rbf::RBF, prior) - # update rbf.scale and fit the surrogate by adapting rbf.weights - return rbf +function SurrogatesBase.update!(surrogate::LinearMock, new_x, new_y) + surrogate.slope = last(new_y) / last(new_x) + return nothing end ``` ## Stochastic Surrogates -Stochastic surrogates `s` are subtypes of `SurrogatesBase.AbstractStochasticSurrogate`. - -### Required methods - -The methods `update!(s, xs, ys)` and `finite_posterior(s, xs)` **must** be implemented, where `xs` is a `Vector` of input points and `ys` is a `Vector` of corresponding observed samples. - -Calling `update!(s, xs, ys)` refits the surrogate `s` to include observations `ys` at points `xs`. - -For single points `x` and `y`, call the `update!(s, xs, ys)` via `update!(s, [x], [y])`. - -Calling `finite_posterior(s, xs)` returns an object that provides methods for working with the finite -dimensional posterior distribution at points `xs`. -The following methods might be supported: - -- `mean(finite_posterior(s,xs))` returns a `Vector` of posterior means at `xs` -- `var(finite_posterior(s,xs))` returns a `Vector` of posterior variances at `xs` -- `mean_and_var(finite_posterior(s,xs))` returns a `Tuple` consisting of a `Vector` of posterior means and a `Vector` of posterior variances at `xs` -- `rand(finite_posterior(s,xs))` returns a `Vector`, which is a sample from the joint posterior at points `xs` - -### Optional methods - -If the surrogate `s` wants to expose current parameter values, the method `parameters(s)` **must** be implemented. - -If the surrogate `s` has tunable hyper-parameters, the methods -`update_hyperparameters!(s, prior)` and `hyperparameters(s)` **must** be implemented. - -Calling `update_hyperparameters!(s, prior)` updates the hyperparameters of the surrogate `s` by performing hyperparameter optimization using the information in `prior`. After the hyperparameters of `s` are updated, `s` is fit to past samples. -Calling `hyperparameters(s)` returns current values of hyperparameters. +A stochastic implementation subtypes [`AbstractStochasticSurrogate`](@ref) and implements: +```julia +update!(surrogate, new_x, new_y) +finite_posterior(surrogate, xs) +``` -### Example +The update rules are the same as for deterministic surrogates. `finite_posterior` receives query +inputs in a concrete implementation's documented representation and returns that implementation's +finite-dimensional posterior object. That object must preserve the query ordering and document the +operations it supports, such as `Statistics.mean`, `Statistics.var`, or `rand`. ```julia +using Statistics using SurrogatesBase -mutable struct GaussianProcessSurrogate{D, R, GP, H <: NamedTuple} <: AbstractStochasticSurrogate - xs::Vector{D} - ys::Vector{R} - gp_process::GP - hyperparameters::H +mutable struct ConstantPosteriorSurrogate <: AbstractStochasticSurrogate + mean_value::Float64 end -function SurrogatesBase.update!(g::GaussianProcessSurrogate, new_xs, new_ys) - append!(g.xs, new_xs) - append!(g.ys, new_ys) - # condition the prior `g.gp_process` on new data to obtain a posterior - # update g.gp_process to the posterior process - return g +struct ConstantPosterior + means::Vector{Float64} end -function SurrogatesBase.finite_posterior(g::GaussianProcessSurrogate, xs) - # Return a finite dimensional projection of g.gp_process at points xs. - # The returned object GP_finite supports methods mean(GP_finite) and - # var(GP_finite) for obtaining the vector of means and variances at points xs. -end +Statistics.mean(posterior::ConstantPosterior) = posterior.means -SurrogatesBase.hyperparameters(g::GaussianProcessSurrogate) = g.hyperparameters - -function SurrogatesBase.update_hyperparameters!(g::GaussianProcessSurrogate, prior) - # Use prior on hyperparameters, e.g., parameters uniformly distributed - # between an upper and lower bound, to perform hyperparameter optimization. - # Set g.hyperparameters to the improved hyperparameters. - # Fit a Gaussian process that uses the updated hyperparameters to past - # samples and save it in g.gp_process. - return g +function SurrogatesBase.finite_posterior(surrogate::ConstantPosteriorSurrogate, xs) + return ConstantPosterior(fill(surrogate.mean_value, length(xs))) end -``` \ No newline at end of file +``` + +The package test suite validates these rules using mock subtypes that are driven only through the +public SurrogatesBase functions. This keeps the extension contract independent of implementation +details from any concrete surrogate package. diff --git a/src/SurrogatesBase.jl b/src/SurrogatesBase.jl index 915ba50..31aa2a0 100644 --- a/src/SurrogatesBase.jl +++ b/src/SurrogatesBase.jl @@ -1,48 +1,71 @@ +""" + SurrogatesBase + +Common public interfaces for deterministic and stochastic surrogate implementations. + +See [`AbstractDeterministicSurrogate`](@ref), [`AbstractStochasticSurrogate`](@ref), and the +developer interface guide for extension rules. +""" module SurrogatesBase export AbstractDeterministicSurrogate export AbstractStochasticSurrogate - export update!, parameters export update_hyperparameters!, hyperparameters export finite_posterior """ abstract type AbstractDeterministicSurrogate <: Function end - (s::AbstractDeterministicSurrogate)(xs) -Interface tag for deterministic surrogate models. +Abstract interface for a fitted deterministic surrogate. + +`AbstractDeterministicSurrogate` has no fields. Concrete subtypes own their training data, +fitted state, and domain representation. A subtype is a function and represents a deterministic +approximation such as a regression model or an interpolant. -Subtypes approximate a deterministic function, or a deterministic statistic of a -conditional distribution, from observed data. A deterministic surrogate is callable on a -collection of input points `xs` and should return one surrogate value for each point. +# Interface -# Required Methods +To extend this interface, define a concrete subtype and implement the following methods for that +subtype: - - `(s)(xs)`: evaluate the surrogate at the points in `xs`. - - [`update!(s, new_xs, new_ys)`](@ref): incorporate new observations. + - `(surrogate)(x)`: evaluate the fitted approximation at an input `x` supported by the + implementation. + - [`update!(surrogate, new_x, new_y)`](@ref): incorporate paired observations. + +The base interface does not prescribe whether `x` is scalar, a point container, or a batch of +points. A consuming package may require one of those forms, so implementations must document the +input forms they support. When an implementation supports batched evaluation, its result must +preserve the correspondence between requested inputs and returned predictions. + +`update!` is an in-place interface: it must leave `surrogate` representing the updated fit. Its +return value is intentionally unspecified, because existing implementations return `nothing`, the +surrogate, or an implementation-specific value. Generic callers must use the mutated surrogate and +must not depend on the return value. # Optional Methods - - [`parameters(s)`](@ref): return learned parameter values. - - [`hyperparameters(s)`](@ref): return tunable hyperparameter values. - - [`update_hyperparameters!(s, prior)`](@ref): update tunable hyperparameters. + - [`parameters(surrogate)`](@ref): expose learned parameters or fitted state. + - [`hyperparameters(surrogate)`](@ref): expose tunable fitting configuration. + - [`update_hyperparameters!(surrogate, prior)`](@ref): update that configuration in place. # Examples ```jldoctest -julia> struct ConstantSurrogate{T} <: AbstractDeterministicSurrogate +julia> mutable struct ConstantSurrogate{T} <: AbstractDeterministicSurrogate value::T end -julia> (s::ConstantSurrogate)(xs) = fill(s.value, length(xs)); +julia> (surrogate::ConstantSurrogate)(x) = surrogate.value; + +julia> function SurrogatesBase.update!(surrogate::ConstantSurrogate, new_x, new_y) + surrogate.value = last(new_y) + return nothing + end; julia> surrogate = ConstantSurrogate(1.5); -julia> surrogate([[0.0, 1.0], [1.0, 2.0]]) -2-element Vector{Float64}: - 1.5 - 1.5 +julia> update!(surrogate, [0.0, 1.0], [2.0, 3.0]); surrogate(0.25) +3.0 ``` """ abstract type AbstractDeterministicSurrogate <: Function end @@ -50,185 +73,209 @@ abstract type AbstractDeterministicSurrogate <: Function end """ abstract type AbstractStochasticSurrogate end -Interface tag for stochastic surrogate models. +Abstract interface for an uncertainty-aware surrogate. -Subtypes approximate a conditional distribution, stochastic process, or uncertainty-aware -surrogate from observed data. +`AbstractStochasticSurrogate` has no fields. Concrete subtypes own their observations, fitted +state, and posterior representation. A subtype represents a conditional distribution or stochastic +process approximation rather than a deterministic callable approximation. -# Required Methods +# Interface - - [`update!(s, new_xs, new_ys)`](@ref): incorporate new observations. - - [`finite_posterior(s, xs)`](@ref): return a finite-dimensional posterior object at - the points in `xs`. +To extend this interface, define a concrete subtype and implement the following methods for that +subtype: -# Optional Methods + - [`update!(surrogate, new_x, new_y)`](@ref): incorporate paired observations in place. + - [`finite_posterior(surrogate, xs)`](@ref): construct a posterior object for query inputs `xs`. + +`xs` is normally a collection of query inputs, but its precise representation is owned by the +concrete surrogate. The returned posterior object is also implementation-defined. It should expose +the statistical operations promised by the concrete implementation, such as `Statistics.mean`, +`Statistics.var`, or `rand`. Its values must correspond to the supplied query inputs. + +As for [`AbstractDeterministicSurrogate`](@ref), the `update!` return value is unspecified. Generic +callers must use the mutated surrogate rather than its return value. - - [`parameters(s)`](@ref): return learned parameter values. - - [`hyperparameters(s)`](@ref): return tunable hyperparameter values. - - [`update_hyperparameters!(s, prior)`](@ref): update tunable hyperparameters. +# Optional Methods -See also [`finite_posterior`](@ref). + - [`parameters(surrogate)`](@ref): expose learned parameters or fitted state. + - [`hyperparameters(surrogate)`](@ref): expose tunable fitting configuration. + - [`update_hyperparameters!(surrogate, prior)`](@ref): update that configuration in place. """ abstract type AbstractStochasticSurrogate end """ - update!(s, new_xs::AbstractVector, new_ys::AbstractVector) - -Incorporate observations `new_ys` at points `new_xs` into the surrogate `s`. + update!(surrogate, new_x, new_y) -Implementations usually mutate and return `s`. For deterministic surrogates, `new_ys` -contains function evaluations or deterministic statistics. For stochastic surrogates, -`new_ys` contains observed samples from the modeled conditional distribution. +Incorporate paired observations into `surrogate` in place. # Arguments - - `s`: surrogate to refit or update. - - `new_xs`: input points to add to `s`. - - `new_ys`: observed values corresponding to `new_xs`. + - `surrogate`: an [`AbstractDeterministicSurrogate`](@ref) or + [`AbstractStochasticSurrogate`](@ref) concrete implementation to update. + - `new_x`: one input or a batch of input locations accepted by the concrete implementation. + - `new_y`: observed value or values paired with `new_x`. + +# Interface Contract -Use `update!(s, eachslice(X; dims = 2), new_ys)` when columns of a matrix `X` are the input -points. +Concrete surrogate implementations must extend `update!` for their own subtype. For batched +updates, `new_x` and `new_y` must encode the same number of observations in the same order. For +single-observation updates, they must encode one paired input and value. The method must update the +surrogate state; no particular return value is part of this interface. # Examples ```jldoctest julia> mutable struct UpdateExampleSurrogate <: AbstractDeterministicSurrogate - xs::Vector{Float64} - ys::Vector{Float64} + values::Vector{Float64} end -julia> (s::UpdateExampleSurrogate)(xs) = fill(last(s.ys), length(xs)); +julia> (surrogate::UpdateExampleSurrogate)(x) = last(surrogate.values); -julia> function SurrogatesBase.update!(s::UpdateExampleSurrogate, new_xs, new_ys) - append!(s.xs, new_xs) - append!(s.ys, new_ys) - return s +julia> function SurrogatesBase.update!(surrogate::UpdateExampleSurrogate, new_x, new_y) + append!(surrogate.values, new_y) + return nothing end; -julia> surrogate = UpdateExampleSurrogate(Float64[], Float64[]); +julia> surrogate = UpdateExampleSurrogate(Float64[]); -julia> update!(surrogate, [1.0, 2.0], [3.0, 4.0]) === surrogate -true - -julia> surrogate.ys -2-element Vector{Float64}: - 3.0 - 4.0 +julia> update!(surrogate, [1.0, 2.0], [3.0, 4.0]); surrogate(0.0) +4.0 ``` """ function update! end """ - parameters(s) + parameters(surrogate) + +Return learned parameters or fitted state exposed by `surrogate`. + +# Arguments -Return the current learned parameter values of the surrogate `s`. + - `surrogate`: a concrete surrogate implementation that documents this optional method. -This is an optional interface method for surrogate implementations that expose fitted -parameters separately from tunable hyperparameters. +# Returns + +An implementation-defined representation of learned parameters or fitted state. The returned object +may be a scalar, tuple, named tuple, array, or model object. + +# Interface Contract + +`parameters` is optional. Extend it only for concrete surrogate subtypes that expose learned state; +calling it for a subtype without a method raises a `MethodError`. # Examples ```jldoctest julia> struct ParameterExampleSurrogate <: AbstractDeterministicSurrogate - weights::Vector{Float64} + weight::Float64 end -julia> (s::ParameterExampleSurrogate)(xs) = fill(sum(s.weights), length(xs)); +julia> (surrogate::ParameterExampleSurrogate)(x) = surrogate.weight * x; -julia> SurrogatesBase.parameters(s::ParameterExampleSurrogate) = s.weights; +julia> SurrogatesBase.parameters(surrogate::ParameterExampleSurrogate) = (; weight = surrogate.weight); -julia> parameters(ParameterExampleSurrogate([1.0, 2.0])) -2-element Vector{Float64}: - 1.0 - 2.0 +julia> parameters(ParameterExampleSurrogate(2.0)) +(weight = 2.0,) ``` """ function parameters end """ - update_hyperparameters!(s, prior) - -Update tunable hyperparameters of the surrogate `s` using information in `prior`. + update_hyperparameters!(surrogate, prior) -Implementations usually mutate and return `s`. After changing hyperparameters, the -surrogate should be refit to its existing observations when the hyperparameters affect the -fitted representation. +Update the tunable fitting configuration of `surrogate` in place. # Arguments - - `s`: surrogate whose hyperparameters are updated. - - `prior`: implementation-defined prior, bounds, or configuration used by the update. + - `surrogate`: a concrete surrogate implementation with tunable hyperparameters. + - `prior`: implementation-defined prior, bounds, or optimization configuration. + +# Interface Contract + +This optional method must leave `surrogate` consistent with its updated hyperparameters. If those +hyperparameters affect the fitted representation, the implementation must refit or invalidate that +representation before subsequent evaluation. The return value is not part of the interface. # Examples ```jldoctest -julia> mutable struct HyperparameterUpdateExample <: AbstractDeterministicSurrogate +julia> mutable struct HyperparameterExample <: AbstractDeterministicSurrogate scale::Float64 end -julia> (s::HyperparameterUpdateExample)(xs) = fill(s.scale, length(xs)); +julia> (surrogate::HyperparameterExample)(x) = surrogate.scale * x; -julia> function SurrogatesBase.update_hyperparameters!(s::HyperparameterUpdateExample, prior) - s.scale = (s.scale + prior.scale) / 2 - return s +julia> function SurrogatesBase.update_hyperparameters!(surrogate::HyperparameterExample, prior) + surrogate.scale = prior.scale + return nothing end; -julia> surrogate = HyperparameterUpdateExample(2.0); - -julia> update_hyperparameters!(surrogate, (; scale = 4.0)) === surrogate -true +julia> surrogate = HyperparameterExample(2.0); -julia> surrogate.scale -3.0 +julia> update_hyperparameters!(surrogate, (; scale = 4.0)); surrogate(0.5) +2.0 ``` - -See also [`hyperparameters`](@ref). """ function update_hyperparameters! end """ - hyperparameters(s) + hyperparameters(surrogate) + +Return the tunable fitting configuration exposed by `surrogate`. -Return the current tunable hyperparameter values of the surrogate `s`. +# Arguments + + - `surrogate`: a concrete surrogate implementation that documents this optional method. + +# Returns -This is an optional interface method for surrogate implementations with configuration -values that control fitting or posterior construction. +An implementation-defined representation of tunable configuration, commonly a named tuple or a +small immutable configuration object. + +# Interface Contract + +`hyperparameters` is optional. When both this method and [`update_hyperparameters!`](@ref) are +implemented, the returned configuration must describe the setting used for subsequent evaluations. # Examples ```jldoctest julia> struct HyperparameterReadExample <: AbstractDeterministicSurrogate - settings::NamedTuple + scale::Float64 end -julia> (s::HyperparameterReadExample)(xs) = fill(s.settings.scale, length(xs)); +julia> (surrogate::HyperparameterReadExample)(x) = surrogate.scale * x; -julia> SurrogatesBase.hyperparameters(s::HyperparameterReadExample) = s.settings; +julia> SurrogatesBase.hyperparameters(surrogate::HyperparameterReadExample) = + (; scale = surrogate.scale); -julia> hyperparameters(HyperparameterReadExample((; scale = 2.0))) +julia> hyperparameters(HyperparameterReadExample(2.0)) (scale = 2.0,) ``` - -See also [`update_hyperparameters!`](@ref). """ function hyperparameters end """ - finite_posterior(s::AbstractStochasticSurrogate, xs::AbstractVector) + finite_posterior(surrogate, xs) + +Construct the finite-dimensional posterior represented by `surrogate` at query inputs `xs`. + +# Arguments + + - `surrogate`: an [`AbstractStochasticSurrogate`](@ref) concrete implementation. + - `xs`: query inputs in a representation accepted by that implementation. -Return a finite-dimensional posterior object at points `xs`. +# Returns -The returned object represents the joint posterior over the requested points. An -`AbstractStochasticSurrogate` implementation may support some or all of the following -methods on that object: +An implementation-defined posterior object for the requested query inputs. Concrete implementations +must document the statistical operations they support, for example `Statistics.mean`, +`Statistics.var`, or `rand`. - - `mean(finite_posterior(s, xs))`: posterior means at `xs`. - - `var(finite_posterior(s, xs))`: posterior variances at `xs`. - - `mean_and_var(finite_posterior(s, xs))`: posterior means and variances at `xs`. - - `rand(finite_posterior(s, xs))`: a sample from the joint posterior at `xs`. +# Interface Contract -Use `mean(finite_posterior(s, eachslice(X; dims = 2)))` when columns of a matrix `X` are -the input points. +Concrete stochastic surrogate implementations must extend this function for their own subtype. The +posterior must preserve correspondence with `xs`; for a batch, the returned statistics and samples +must use the same query ordering. This function has no fallback implementation. # Examples @@ -243,10 +290,10 @@ julia> struct PosteriorExample means::Vector{Float64} end -julia> Statistics.mean(p::PosteriorExample) = p.means; +julia> Statistics.mean(posterior::PosteriorExample) = posterior.means; -julia> function SurrogatesBase.finite_posterior(s::PosteriorExampleSurrogate, xs) - return PosteriorExample(fill(s.value, length(xs))) +julia> function SurrogatesBase.finite_posterior(surrogate::PosteriorExampleSurrogate, xs) + PosteriorExample(fill(surrogate.value, length(xs))) end; julia> posterior = finite_posterior(PosteriorExampleSurrogate(1.25), [0.0, 1.0]); diff --git a/test/developer_interfaces.jl b/test/developer_interfaces.jl new file mode 100644 index 0000000..f39d578 --- /dev/null +++ b/test/developer_interfaces.jl @@ -0,0 +1,72 @@ +using SurrogatesBase + +using Statistics +using Test + +mutable struct DeterministicMockSurrogate <: AbstractDeterministicSurrogate + slope::Float64 + offset::Float64 +end + +(surrogate::DeterministicMockSurrogate)(x::Real) = surrogate.slope * x + surrogate.offset + +function SurrogatesBase.update!( + surrogate::DeterministicMockSurrogate, new_x, new_y + ) + surrogate.offset = last(new_y) - surrogate.slope * last(new_x) + return nothing +end + +SurrogatesBase.parameters(surrogate::DeterministicMockSurrogate) = + (; slope = surrogate.slope, offset = surrogate.offset) +SurrogatesBase.hyperparameters(surrogate::DeterministicMockSurrogate) = (; slope = surrogate.slope) + +function SurrogatesBase.update_hyperparameters!( + surrogate::DeterministicMockSurrogate, prior + ) + surrogate.slope = prior.slope + return nothing +end + +mutable struct StochasticMockSurrogate <: AbstractStochasticSurrogate + mean_value::Float64 +end + +struct MockPosterior + means::Vector{Float64} +end + +Statistics.mean(posterior::MockPosterior) = posterior.means + +function SurrogatesBase.update!(surrogate::StochasticMockSurrogate, new_x, new_y) + surrogate.mean_value = sum(new_y) / length(new_y) + return nothing +end + +SurrogatesBase.parameters(surrogate::StochasticMockSurrogate) = (; mean = surrogate.mean_value) + +function SurrogatesBase.finite_posterior(surrogate::StochasticMockSurrogate, xs) + return MockPosterior(fill(surrogate.mean_value, length(xs))) +end + +@testset "Public surrogate extension contracts" begin + @testset "Deterministic surrogate" begin + surrogate = DeterministicMockSurrogate(2.0, 1.0) + + @test surrogate(3.0) == 7.0 + @test isnothing(update!(surrogate, [1.0, 2.0], [5.0, 7.0])) + @test surrogate(2.0) == 7.0 + @test parameters(surrogate) == (; slope = 2.0, offset = 3.0) + @test hyperparameters(surrogate) == (; slope = 2.0) + @test isnothing(update_hyperparameters!(surrogate, (; slope = 4.0))) + @test surrogate(2.0) == 11.0 + end + + @testset "Stochastic surrogate" begin + surrogate = StochasticMockSurrogate(0.0) + + @test isnothing(update!(surrogate, [1.0, 2.0], [2.0, 6.0])) + @test parameters(surrogate) == (; mean = 4.0) + @test mean(finite_posterior(surrogate, [0.0, 1.0, 2.0])) == fill(4.0, 3) + end +end diff --git a/test/qa/Project.toml b/test/qa/Project.toml index 769851f..54cf9f0 100644 --- a/test/qa/Project.toml +++ b/test/qa/Project.toml @@ -1,19 +1,9 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SurrogatesBase = "89f642e6-4179-4274-8202-c11f4bd9a72c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Aqua = "0.8" -JET = "0.9, 0.10, 0.11" -LinearAlgebra = "1.10" -SafeTestsets = "0.1" -SciMLTesting = "2.1" -Statistics = "1.10" -Test = "1.10" +SciMLTesting = "2.4" julia = "1.10" diff --git a/test/qa/qa.jl b/test/qa/qa.jl index 685f1ea..c13a8f4 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -1,96 +1,3 @@ -using SciMLTesting -using SurrogatesBase -using JET -using LinearAlgebra -using Test -import Statistics +using SciMLTesting, SurrogatesBase run_qa(SurrogatesBase) - -# JET.report_call type-stability analysis of concrete user-defined surrogates. -# This goes beyond run_qa's package-level JET.test_package: it checks that the -# interface contract (update!/finite_posterior/parameters and the call method) -# stays inferable for downstream subtypes. -# -# On Julia 1.12+, LinearAlgebra.norm_recursive_check has a type inference issue -# that surfaces as JET false positives through `norm`. We ignore LinearAlgebra -# and Base frames to filter those stdlib issues while still checking our own code. -const JET_CONFIG = ( - ignored_modules = ( - JET.AnyFrameModule(LinearAlgebra), - JET.AnyFrameModule(Base), - ), -) - -@testset "JET report_call type stability" begin - @testset "DummySurrogate type stability" begin - struct JETDummySurrogate{X, Y} <: AbstractDeterministicSurrogate - xs::Vector{X} - ys::Vector{Y} - end - (s::JETDummySurrogate)(x) = s.ys[argmin([norm(x - ξ) for ξ in s.xs])] - function SurrogatesBase.update!(s::JETDummySurrogate, new_xs, new_ys) - append!(s.xs, new_xs) - append!(s.ys, new_ys) - end - - d = JETDummySurrogate(Vector{Vector{Float64}}(), Vector{Int}()) - SurrogatesBase.update!(d, [[10.3, 0.1], [1.9, 2.1]], [5, 6]) - - result = JET.report_call(d, Tuple{Vector{Float64}}; JET_CONFIG...) - @test length(JET.get_reports(result)) == 0 - - result = JET.report_call( - SurrogatesBase.update!, - Tuple{ - JETDummySurrogate{Vector{Float64}, Int}, Vector{Vector{Float64}}, - Vector{Int}, - }; JET_CONFIG... - ) - @test length(JET.get_reports(result)) == 0 - end - - @testset "Stochastic surrogate type stability" begin - mutable struct JETDummyStochasticSurrogate{X, Y} <: AbstractStochasticSurrogate - xs::Vector{X} - ys::Vector{Y} - ys_mean::Y - end - function SurrogatesBase.update!(s::JETDummyStochasticSurrogate, new_xs, new_ys) - append!(s.xs, new_xs) - append!(s.ys, new_ys) - s.ys_mean = (s.ys_mean * (length(s.xs) - length(new_xs)) + sum(new_ys)) / - length(s.xs) - end - SurrogatesBase.parameters(s::JETDummyStochasticSurrogate) = s.ys_mean - - struct JETFiniteDummyStochasticSurrogate{X} - means::Vector{X} - end - Statistics.mean(s::JETFiniteDummyStochasticSurrogate) = s.means - function JETFiniteDummyStochasticSurrogate(s, xs) - return JETFiniteDummyStochasticSurrogate(s.ys_mean .* ones(length(xs))) - end - function SurrogatesBase.finite_posterior(s::JETDummyStochasticSurrogate, xs) - JETFiniteDummyStochasticSurrogate(s, xs) - end - - result = JET.report_call( - SurrogatesBase.update!, - Tuple{ - JETDummyStochasticSurrogate{Vector{Float64}, Float64}, - Vector{Vector{Float64}}, Vector{Float64}, - }; JET_CONFIG... - ) - @test length(JET.get_reports(result)) == 0 - - result = JET.report_call( - SurrogatesBase.finite_posterior, - Tuple{ - JETDummyStochasticSurrogate{Vector{Float64}, Float64}, - Vector{Vector{Float64}}, - }; JET_CONFIG... - ) - @test length(JET.get_reports(result)) == 0 - end -end