From f3fbab1cb0dd1a602d26ea6cb7f7c33aa16a8da8 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 15 Aug 2026 11:51:45 -0400 Subject: [PATCH 1/4] docs: specify DataDrivenDiffEq developer interfaces Co-Authored-By: Chris Rackauckas --- docs/src/libs/datadrivendmd/koopman.md | 4 + docs/src/libs/datadrivenlux/dag_sr.md | 10 ++ .../datadrivensparse/sparse_regression.md | 9 ++ docs/src/solvers/common.md | 4 + lib/DataDrivenDMD/Project.toml | 2 + lib/DataDrivenDMD/src/DataDrivenDMD.jl | 36 +++++++ lib/DataDrivenLux/Project.toml | 2 + lib/DataDrivenLux/src/DataDrivenLux.jl | 32 +++++++ lib/DataDrivenLux/src/algorithms/common.jl | 12 +++ lib/DataDrivenSparse/Project.toml | 2 + lib/DataDrivenSparse/src/DataDrivenSparse.jl | 33 +++++++ src/DataDrivenDiffEq.jl | 86 ++++++++++++++--- test/Core/developer_api.jl | 94 +++++++++++++++++++ 13 files changed, 315 insertions(+), 11 deletions(-) diff --git a/docs/src/libs/datadrivendmd/koopman.md b/docs/src/libs/datadrivendmd/koopman.md index 9c02e1366..0493dc316 100644 --- a/docs/src/libs/datadrivendmd/koopman.md +++ b/docs/src/libs/datadrivendmd/koopman.md @@ -29,7 +29,11 @@ A similar result holds for time continuous systems in the form of the Koopman ge ## [Algorithms](@id koopman_algorithms) +The abstract algorithm below is a developer interface for extending the Koopman +solver. Users should generally select one of the concrete algorithms. + ```@docs +DataDrivenDMD.AbstractKoopmanAlgorithm DMDPINV DMDSVD TOTALDMD diff --git a/docs/src/libs/datadrivenlux/dag_sr.md b/docs/src/libs/datadrivenlux/dag_sr.md index 238a845a0..aa8a8a60d 100644 --- a/docs/src/libs/datadrivenlux/dag_sr.md +++ b/docs/src/libs/datadrivenlux/dag_sr.md @@ -3,6 +3,16 @@ DataDrivenLux provides differentiable directed-acyclic-graph structure search for discovering governing equations. +## Developer API + +`AbstractDAGSRAlgorithm` is the extension interface for implementing another +search algorithm. Application code should use the concrete algorithms below. + +```@docs +DataDrivenLux.AbstractDAGSRAlgorithm +DataDrivenLux.CommonAlgOptions +``` + ## Error Models ```@docs diff --git a/docs/src/libs/datadrivensparse/sparse_regression.md b/docs/src/libs/datadrivensparse/sparse_regression.md index 4cd0ca5d6..ba2255509 100644 --- a/docs/src/libs/datadrivensparse/sparse_regression.md +++ b/docs/src/libs/datadrivensparse/sparse_regression.md @@ -48,6 +48,10 @@ Where the matrix of evaluated basis elements $\varPhi_y \in \mathbb R^{\lvert \v ## [Algorithms](@id sparse_algorithms) +The abstract algorithm and proximal operator entries below are developer +interfaces for extending `DataDrivenSparse`. Application code should generally +use the concrete algorithms and operators. + ```@docs DataDrivenSparse.AbstractSparseRegressionAlgorithm STLSQ @@ -60,7 +64,12 @@ SparseLinearSolver ## [Proximal Operators](@id proximal_operators) +Custom proximal operators should subtype `AbstractProximalOperator` and +implement the documented callable and active-set methods. + ```@docs +DataDrivenSparse.AbstractProximalOperator +DataDrivenSparse.active_set! SoftThreshold HardThreshold ClippedAbsoluteDeviation diff --git a/docs/src/solvers/common.md b/docs/src/solvers/common.md index e58c72174..bbaea54bf 100644 --- a/docs/src/solvers/common.md +++ b/docs/src/solvers/common.md @@ -30,6 +30,10 @@ DataDrivenCommonOptions After defining a [`problem`](@ref problem), we choose a method to [`solve`](@ref solve) it. Depending on the input arguments and the type of problem, the function will return a result derived from the algorithm of choice. Different options can be provided, depending on the inference method, for options like rounding, normalization, or the progress bar. An optional [`Basis`](@ref) can be used for lifting the measurements. +The exported `solve` name follows the generic `CommonSolve` interface, which is +documented by its owning package. The examples below describe its use with +DataDrivenDiffEq problems and algorithms. + ```julia solution = solve(DataDrivenProblem, [basis], solver; kwargs...) ``` diff --git a/lib/DataDrivenDMD/Project.toml b/lib/DataDrivenDMD/Project.toml index d55f920cf..fc6926ac7 100644 --- a/lib/DataDrivenDMD/Project.toml +++ b/lib/DataDrivenDMD/Project.toml @@ -11,6 +11,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsAPI = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" [sources] DataDrivenDiffEq = {path = "../.."} @@ -28,6 +29,7 @@ SafeTestsets = "0.1" StableRNGs = "1" Statistics = "1.10" StatsAPI = "1" +SciMLPublic = "1" Symbolics = "7.18.1" Test = "1.10" SciMLTesting = "2.10" diff --git a/lib/DataDrivenDMD/src/DataDrivenDMD.jl b/lib/DataDrivenDMD/src/DataDrivenDMD.jl index a4c15d99e..093a2d34b 100644 --- a/lib/DataDrivenDMD/src/DataDrivenDMD.jl +++ b/lib/DataDrivenDMD/src/DataDrivenDMD.jl @@ -10,13 +10,49 @@ using DocStringExtensions: FIELDS, SIGNATURES, TYPEDEF using Parameters: @unpack using Statistics: mean using StatsAPI: StatsAPI, r2 +using SciMLPublic: @public using LinearAlgebra: Diagonal, Eigen, eigen, svd const _EMPTY_MATRIX = Matrix(undef, 0, 0) +""" + AbstractKoopmanAlgorithm + +Developer interface for algorithms that estimate a Koopman operator or generator. +This interface is intended for DataDrivenDiffEq solver packages and advanced +extensions, not ordinary application code. + +# Interface + +A subtype must implement `alg(X, Y) -> (K, B)`, where `X` and `Y` are lifted data +matrices, `K` is an operator representation accepted by `eigen`, and `B` is the +input map or an empty matrix when no controls are used. A controlled implementation +may additionally implement `alg(X, Y, U) -> (K, B)`. The generic four-argument +forms support a supplied input map or `nothing`. + +To participate in the common `solve` workflow, the subtype must also be usable by +the `DataDrivenDiffEq.get_fit_targets` and `CommonSolve.solve!` methods for +[`InternalDataDrivenProblem`](@ref). A custom algorithm should preserve the +returned matrix dimensions so that the result can be converted back to a +[`DataDrivenDiffEq.Basis`](@ref). + +# Example + +```julia +using LinearAlgebra + +struct MyKoopman <: DataDrivenDMD.AbstractKoopmanAlgorithm end + +function (::MyKoopman)(X, Y) + return (eigen(Y / X), zeros(eltype(X), size(Y, 1), 0)) +end +``` +""" abstract type AbstractKoopmanAlgorithm <: AbstractDataDrivenAlgorithm end +@public AbstractKoopmanAlgorithm + # Results include("./result.jl") export KoopmanResult diff --git a/lib/DataDrivenLux/Project.toml b/lib/DataDrivenLux/Project.toml index 431d19748..064f215c7 100644 --- a/lib/DataDrivenLux/Project.toml +++ b/lib/DataDrivenLux/Project.toml @@ -28,6 +28,7 @@ ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" StatsAPI = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" TransformVariables = "84d833dd-6860-57f9-a1a7-6da5db126cff" WeightInitializers = "d49dbf32-c5c2-4618-8acc-27bb2598ef2d" @@ -65,6 +66,7 @@ SafeTestsets = "0.1" Setfield = "1" StableRNGs = "1" StatsAPI = "1" +SciMLPublic = "1" Symbolics = "7.18.1" Test = "1.10" TransformVariables = "0.8" diff --git a/lib/DataDrivenLux/src/DataDrivenLux.jl b/lib/DataDrivenLux/src/DataDrivenLux.jl index e6127811f..c54b46095 100644 --- a/lib/DataDrivenLux/src/DataDrivenLux.jl +++ b/lib/DataDrivenLux/src/DataDrivenLux.jl @@ -39,11 +39,43 @@ using ForwardDiff: ForwardDiff using Logging: Logging, NullLogger, with_logger using Random: Random, AbstractRNG using Distributed: Distributed, pmap +using SciMLPublic: @public const AD = AbstractDifferentiation abstract type AbstractAlgorithmCache <: AbstractDataDrivenResult end +""" + AbstractDAGSRAlgorithm + +Developer interface for differentiable directed-acyclic-graph symbolic-regression +algorithms. This interface is intended for solver extensions, not ordinary users. + +# Interface + +A subtype must provide an `options` field compatible with [`CommonAlgOptions`](@ref) +and a method `update_parameters!(cache::SearchCache{<:MyAlgorithm})`. The generic +cache initialization supplies the dataset, candidate population, and optimization +state. An algorithm that uses a different graph representation must also specialize +`init_model(alg, basis, dataset, intervals)`. + +The generic `CommonSolve.solve!` path consumes the cache, repeatedly calls +`update_parameters!`, and returns a [`DataDrivenDiffEq.DataDrivenSolution`](@ref). +Custom algorithms should keep the `loss`, `keep`, and population semantics of +`CommonAlgOptions` or document any intentional differences. + +# Example + +```julia +struct MyDAGAlgorithm <: DataDrivenLux.AbstractDAGSRAlgorithm + options::DataDrivenLux.CommonAlgOptions +end + +DataDrivenLux.update_parameters!(cache::DataDrivenLux.SearchCache{<:MyDAGAlgorithm}) = + nothing +``` +""" abstract type AbstractDAGSRAlgorithm <: AbstractDataDrivenAlgorithm end +@public AbstractDAGSRAlgorithm abstract type AbstractSimplex end abstract type AbstractErrorModel end abstract type AbstractErrorDistribution end diff --git a/lib/DataDrivenLux/src/algorithms/common.jl b/lib/DataDrivenLux/src/algorithms/common.jl index 054b69a83..b98049051 100644 --- a/lib/DataDrivenLux/src/algorithms/common.jl +++ b/lib/DataDrivenLux/src/algorithms/common.jl @@ -1,3 +1,13 @@ +""" + CommonAlgOptions(; kwargs...) + +Shared configuration for [`AbstractDAGSRAlgorithm`](@ref) implementations. +Concrete algorithms normally expose these keywords through their own constructor. + +# Fields + +$(FIELDS) +""" @concrete struct CommonAlgOptions populationsize::Int functions @@ -18,6 +28,8 @@ alpha::Real end +@public CommonAlgOptions + function CommonAlgOptions(; populationsize::Int = 100, functions = (sin, exp, cos, log, +, -, /, *), diff --git a/lib/DataDrivenSparse/Project.toml b/lib/DataDrivenSparse/Project.toml index c769168b9..18331a2b9 100644 --- a/lib/DataDrivenSparse/Project.toml +++ b/lib/DataDrivenSparse/Project.toml @@ -12,6 +12,7 @@ Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsAPI = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" [sources] DataDrivenDiffEq = {path = "../.."} @@ -31,6 +32,7 @@ SafeTestsets = "0.1" StableRNGs = "1" Statistics = "1.10" StatsAPI = "1" +SciMLPublic = "1" StatsBase = "0.34" Symbolics = "7.18.1" Test = "1.10" diff --git a/lib/DataDrivenSparse/src/DataDrivenSparse.jl b/lib/DataDrivenSparse/src/DataDrivenSparse.jl index 98a5de7f3..556f905fe 100644 --- a/lib/DataDrivenSparse/src/DataDrivenSparse.jl +++ b/lib/DataDrivenSparse/src/DataDrivenSparse.jl @@ -8,6 +8,7 @@ using CommonSolve: CommonSolve using DocStringExtensions: FIELDS, TYPEDEF, TYPEDFIELDS using Parameters: @unpack using StatsAPI: StatsAPI, StatisticalModel, aicc, coef, dof, nobs, r2, rss +using SciMLPublic: @public using LinearAlgebra: I, cholesky, dot, norm using Printf: @printf @@ -70,8 +71,40 @@ coefficients, thresholds, iterations = alg(X, Y) ``` """ abstract type AbstractSparseRegressionAlgorithm <: AbstractDataDrivenAlgorithm end +@public AbstractSparseRegressionAlgorithm + +""" + AbstractProximalOperator + +Developer interface for thresholding operators used by sparse-regression +algorithms such as [`SR3`](@ref). + +# Interface + +A subtype must implement `operator(x, λ)` as a callable object that updates `x` in +place, `operator(y, x, λ)` as an out-of-place-buffer form, and +`active_set!(mask, operator, x, λ)` to identify the nonzero coefficients. The two +callable forms must preserve the shape and element type of the coefficient array. +Concrete operators may store additional thresholds, but those fields and their +defaults must be documented. +""" abstract type AbstractProximalOperator end +@public AbstractProximalOperator + +""" + active_set!(mask, operator, x, lambda) + +Update the Boolean active-set mask for a sparse-regression proximal operator. + +This is a developer extension point for [`AbstractProximalOperator`](@ref). +`mask` and `x` must have the same shape, and an active entry indicates that the +corresponding coefficient survives thresholding. +""" +function active_set! end + +@public active_set! + abstract type AbstractSparseRegressionCache <: StatisticalModel end function _set!(x::AbstractSparseRegressionCache, y::AbstractSparseRegressionCache) diff --git a/src/DataDrivenDiffEq.jl b/src/DataDrivenDiffEq.jl index 3497a5936..f497f904b 100644 --- a/src/DataDrivenDiffEq.jl +++ b/src/DataDrivenDiffEq.jl @@ -78,13 +78,38 @@ abstract type AbstractDataDrivenFunction{Bool, Bool} end """ AbstractBasis -Supertype for symbolic feature bases accepted by data-driven algorithms. +Supertype for symbolic feature bases accepted by data-driven algorithms. A basis +maps measured states, parameters, time, and optional controls to feature values. # Interface -Subtypes must provide symbolic equations, state and parameter accessors, and callable -in-place and out-of-place evaluation. Solver packages may use [`get_f`](@ref), -[`is_implicit`](@ref), and [`is_controlled`](@ref) to inspect these capabilities. +Subtypes must provide the following interface: + +- `ModelingToolkitBase.equations(b)`, `unknowns(b)`, `parameters(b)`, + `get_observed(b)`, and `get_iv(b)` expose the symbolic system. +- [`states`](@ref), [`controls`](@ref), [`is_implicit`](@ref), and + [`is_controlled`](@ref) describe the feature inputs. +- [`get_f`](@ref) or [`dynamics`](@ref) returns the callable feature evaluator. +- An explicit basis is callable as `b(u, p, t)` and, when controlled, as + `b(u, p, t, c)`. An implicit basis is callable as `b(du, u, p, t)` and, + when controlled, as `b(du, u, p, t, c)`. + +The default accessors use fields named `eqs`, `unknowns`, `ps`, `observed`, `iv`, +`ctrls`, `implicit`, `f`, `name`, and `systems`. A subtype with different storage +must provide equivalent methods explicitly. Solver-specific requirements, such as +[`jacobian`](@ref) for Koopman algorithms, should be documented by that solver. + +# Example + +`Basis` is the standard implementation: + +```julia +using DataDrivenDiffEq, Symbolics + +@variables x +b = Basis([2x], [x]) +b([3.0], [], 0.0) # [6.0] +``` """ abstract type AbstractBasis <: AbstractSystem end @@ -101,8 +126,23 @@ Supertype for algorithms that solve data-driven problems. # Interface An algorithm package must define `CommonSolve.solve!` for -[`InternalDataDrivenProblem`](@ref). It may specialize [`get_fit_targets`](@ref) when its -regression targets differ from the problem's implicit data. +`InternalDataDrivenProblem{A}` and return a [`DataDrivenSolution`](@ref). The +implementation must accept the preprocessed data and options in that internal +problem, and must not require callers to construct the internal representation. + +The default [`get_fit_targets`](@ref) evaluates the basis and returns the problem's +implicit data. An algorithm may specialize it when its regression targets differ +from the problem's implicit data. The algorithm's callable form and keyword +arguments are solver-specific and must be documented by the concrete algorithm. + +# Example + +```julia +struct MyAlgorithm <: DataDrivenDiffEq.AbstractDataDrivenAlgorithm end + +DataDrivenDiffEq.get_fit_targets(::MyAlgorithm, problem, basis) = + (basis(problem), DataDrivenDiffEq.get_implicit_data(problem)) +``` """ abstract type AbstractDataDrivenAlgorithm end @@ -113,8 +153,11 @@ Supertype for algorithm-specific result objects stored by [`DataDrivenSolution`] # Interface -Result types should implement the applicable `StatsAPI.StatisticalModel` accessors and -an `is_success(result)` predicate. +Result types must implement the applicable `StatsAPI.StatisticalModel` accessors: +`coef`, `rss`, `dof`, `nobs`, `loglikelihood`, `nullloglikelihood`, and `r2`. +Solver packages should also provide a success predicate and a return code so that +failed fits can be excluded from model selection. The result fields and the meaning +of each statistic must be documented by the concrete result type. """ abstract type AbstractDataDrivenResult <: StatisticalModel end @@ -126,9 +169,30 @@ Supertype for data containers consumed by data-driven algorithms. # Interface -Problem subtypes must implement [`get_implicit_data`](@ref), [`get_oop_args`](@ref), and -[`remake_problem`](@ref). `N` is the numeric element type, `C` records whether controls -are present, and `K` records whether the problem is direct, discrete, or continuous. +`N` is the numeric element type, `C` records whether controls are present, and `K` +is `DDProbType(1)`, `DDProbType(2)`, or `DDProbType(3)` for direct, discrete, or +continuous data. Problem subtypes must implement: + +- [`get_implicit_data`](@ref): the target matrix used by the default algorithm. +- [`get_oop_args`](@ref): `(X, p, t, U)` aligned with that target matrix. +- [`remake_problem`](@ref): a same-kind problem with selected data replaced by + keyword arguments. +- [`is_valid`](@ref): validation of finite values and compatible sample lengths. + +They must also expose state, control, parameter, time, and observed data through +the corresponding ModelingToolkit accessors or equivalent package methods. For a +discrete problem, inputs and targets must be offset by one sample; for direct and +continuous problems they must have the same sample count. + +# Example + +`DataDrivenProblem` is the reference implementation: + +```julia +X = [1.0 2.0 3.0] +problem = DirectDataDrivenProblem(X, 2 .* X) +get_implicit_data(problem) == 2 .* X +``` """ abstract type AbstractDataDrivenProblem{Number, Bool, DDProbType} end diff --git a/test/Core/developer_api.jl b/test/Core/developer_api.jl index 4045c2715..713c12b8b 100644 --- a/test/Core/developer_api.jl +++ b/test/Core/developer_api.jl @@ -38,6 +38,100 @@ const DEVELOPER_API = ( end end +struct InterfaceBasis <: DataDrivenDiffEq.AbstractBasis + eqs + unknowns + ctrls + ps + observed + iv + implicit + f + name + systems +end + +function (basis::InterfaceBasis)(u, p, t) + return basis.f(u, p, t) +end + +DataDrivenDiffEq.is_implicit(::InterfaceBasis) = false +DataDrivenDiffEq.is_controlled(::InterfaceBasis) = false + +struct InterfaceProblem <: DataDrivenDiffEq.AbstractDataDrivenProblem{ + Float64, false, DataDrivenDiffEq.DDProbType(1), + } + X + t + DX + Y + U + p + name +end + +DataDrivenDiffEq.get_oop_args(problem::InterfaceProblem) = + (problem.X, problem.p, problem.t, problem.U) + +function DataDrivenDiffEq.remake_problem(problem::InterfaceProblem; p = problem.p, kwargs...) + return InterfaceProblem( + problem.X, problem.t, problem.DX, problem.Y, problem.U, p, problem.name + ) +end + +(basis::InterfaceBasis)(problem::InterfaceProblem) = 2 .* problem.X + +struct InterfaceResult <: DataDrivenDiffEq.AbstractDataDrivenResult + value::Float64 +end + +import StatsAPI +StatsAPI.coef(result::InterfaceResult) = result.value +StatsAPI.rss(result::InterfaceResult) = result.value +StatsAPI.dof(::InterfaceResult) = 1 +StatsAPI.nobs(::InterfaceResult) = 1 +StatsAPI.loglikelihood(::InterfaceResult) = 0.0 +StatsAPI.nullloglikelihood(::InterfaceResult) = 0.0 +StatsAPI.r2(::InterfaceResult) = 1.0 + +@testset "Generic extension interfaces" begin + @variables x t + f(u, p, t) = 2 .* u + basis = InterfaceBasis( + [x ~ 2x], [x], Any[], Any[], Any[], t, Any[], f, :interface, Any[] + ) + X = [1.0 2.0 3.0] + problem = InterfaceProblem( + X, [0.0, 1.0, 2.0], zeros(0, 0), 2 .* X, + zeros(0, 0), Float64[], :interface + ) + algorithm = InterfaceTestAlgorithm() + + @test DataDrivenDiffEq.dynamics(basis)([3.0], [], 0.0) == [6.0] + @test DataDrivenDiffEq.get_f(basis) === f + @test all(isequal.(DataDrivenDiffEq.states(basis), [x])) + @test DataDrivenDiffEq.controls(basis) == [] + @test DataDrivenDiffEq.is_direct(problem) + @test DataDrivenDiffEq.is_autonomous(problem) + @test !DataDrivenDiffEq.is_parametrized(problem) + @test DataDrivenDiffEq.has_timepoints(problem) + @test DataDrivenDiffEq.get_implicit_data(problem) == 2 .* X + @test DataDrivenDiffEq.get_oop_args(problem) == + (X, Float64[], [0.0, 1.0, 2.0], zeros(0, 0)) + @test DataDrivenDiffEq.assert_lhs(problem) == (:direct, 0.0) + @test DataDrivenDiffEq.is_valid(problem) + + inputs, targets = DataDrivenDiffEq.get_fit_targets(algorithm, problem, basis) + @test inputs == 2 .* X + @test targets == 2 .* X + @test DataDrivenDiffEq.remake_problem(problem; p = [3.0]).p == [3.0] + + result = InterfaceResult(1.0) + @test StatsAPI.coef(result) == 1.0 + @test StatsAPI.rss(result) == 1.0 + @test StatsAPI.dof(result) == 1 +end + @testset "Developer interface behavior" begin @variables x basis = Basis([x, x^2], [x]) From 93169f13d4a2e5057368d44f55fe2517b32af878 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 15 Aug 2026 18:36:08 -0400 Subject: [PATCH 2/4] docs: complete DataDrivenDiffEq API audit Co-Authored-By: Chris Rackauckas --- docs/src/libs/datadrivenlux/dag_sr.md | 4 + .../datadrivensparse/sparse_regression.md | 6 ++ lib/DataDrivenDMD/src/DataDrivenDMD.jl | 45 ++++++++--- lib/DataDrivenDMD/src/algorithms.jl | 81 +++++++++++++++++++ lib/DataDrivenDMD/src/result.jl | 10 +++ lib/DataDrivenDMD/test/Core/interface.jl | 39 +++++++++ lib/DataDrivenDMD/test/runtests.jl | 3 + lib/DataDrivenLux/src/DataDrivenLux.jl | 69 ++++++++++++++-- lib/DataDrivenLux/src/algorithms/common.jl | 33 ++++++++ .../src/algorithms/crossentropy.jl | 12 +++ .../src/algorithms/randomsearch.jl | 12 +++ lib/DataDrivenLux/src/algorithms/reinforce.jl | 14 ++++ lib/DataDrivenLux/src/algorithms/rewards.jl | 14 ++++ lib/DataDrivenLux/src/caches/cache.jl | 4 + lib/DataDrivenLux/src/caches/candidate.jl | 18 +++++ lib/DataDrivenLux/src/caches/dataset.jl | 11 +++ lib/DataDrivenLux/src/custom_priors.jl | 32 ++++++++ lib/DataDrivenLux/src/lux/graph.jl | 19 +++++ lib/DataDrivenLux/src/lux/layer.jl | 16 ++++ lib/DataDrivenLux/src/lux/node.jl | 12 +++ lib/DataDrivenLux/src/lux/path_state.jl | 11 +++ lib/DataDrivenLux/src/lux/simplex.jl | 33 ++++++++ lib/DataDrivenLux/test/Core/interface.jl | 41 ++++++++++ lib/DataDrivenLux/test/runtests.jl | 1 + lib/DataDrivenSR/src/DataDrivenSR.jl | 16 ++++ lib/DataDrivenSparse/src/DataDrivenSparse.jl | 79 +++++++++++++++++- lib/DataDrivenSparse/src/algorithms/ADMM.jl | 10 +++ .../src/algorithms/Implicit.jl | 16 ++++ lib/DataDrivenSparse/src/algorithms/SR3.jl | 11 +++ lib/DataDrivenSparse/src/algorithms/STLSQ.jl | 10 +++ lib/DataDrivenSparse/src/algorithms/WyNDA.jl | 16 ++++ .../src/algorithms/proximals.jl | 37 +++++++++ lib/DataDrivenSparse/src/solver.jl | 24 +++++- lib/DataDrivenSparse/test/Core/interface.jl | 77 ++++++++++++++++++ lib/DataDrivenSparse/test/runtests.jl | 4 + 35 files changed, 816 insertions(+), 24 deletions(-) create mode 100644 lib/DataDrivenDMD/test/Core/interface.jl create mode 100644 lib/DataDrivenLux/test/Core/interface.jl create mode 100644 lib/DataDrivenSparse/test/Core/interface.jl diff --git a/docs/src/libs/datadrivenlux/dag_sr.md b/docs/src/libs/datadrivenlux/dag_sr.md index aa8a8a60d..18a3a99cb 100644 --- a/docs/src/libs/datadrivenlux/dag_sr.md +++ b/docs/src/libs/datadrivenlux/dag_sr.md @@ -11,6 +11,10 @@ search algorithm. Application code should use the concrete algorithms below. ```@docs DataDrivenLux.AbstractDAGSRAlgorithm DataDrivenLux.CommonAlgOptions +DataDrivenLux.init_model +DataDrivenLux.init_cache +DataDrivenLux.update_parameters! +DataDrivenLux.convert_to_basis ``` ## Error Models diff --git a/docs/src/libs/datadrivensparse/sparse_regression.md b/docs/src/libs/datadrivensparse/sparse_regression.md index ba2255509..a547431c7 100644 --- a/docs/src/libs/datadrivensparse/sparse_regression.md +++ b/docs/src/libs/datadrivensparse/sparse_regression.md @@ -54,6 +54,12 @@ use the concrete algorithms and operators. ```@docs DataDrivenSparse.AbstractSparseRegressionAlgorithm +DataDrivenSparse.AbstractSparseRegressionCache +DataDrivenSparse.get_thresholds +DataDrivenSparse.get_relaxation +DataDrivenSparse.get_proximal +DataDrivenSparse.init_cache +DataDrivenSparse.step! STLSQ ADMM SR3 diff --git a/lib/DataDrivenDMD/src/DataDrivenDMD.jl b/lib/DataDrivenDMD/src/DataDrivenDMD.jl index 093a2d34b..9419e8e80 100644 --- a/lib/DataDrivenDMD/src/DataDrivenDMD.jl +++ b/lib/DataDrivenDMD/src/DataDrivenDMD.jl @@ -26,27 +26,46 @@ extensions, not ordinary application code. # Interface A subtype must implement `alg(X, Y) -> (K, B)`, where `X` and `Y` are lifted data -matrices, `K` is an operator representation accepted by `eigen`, and `B` is the -input map or an empty matrix when no controls are used. A controlled implementation -may additionally implement `alg(X, Y, U) -> (K, B)`. The generic four-argument -forms support a supplied input map or `nothing`. - -To participate in the common `solve` workflow, the subtype must also be usable by -the `DataDrivenDiffEq.get_fit_targets` and `CommonSolve.solve!` methods for -[`InternalDataDrivenProblem`](@ref). A custom algorithm should preserve the -returned matrix dimensions so that the result can be converted back to a +matrices with one observation per column, `K` is an operator representation +convertible by `Matrix`, and `B` is the input map or an empty matrix when no +controls are used. A controlled implementation may additionally implement +`alg(X, Y, U) -> (K, B)`. The generic four-argument forms support a supplied +input map or `nothing` and are provided by this package. + +To participate in the common `solve` workflow, the subtype must be usable by the +generic `DataDrivenDiffEq.get_fit_targets` and `CommonSolve.solve!` methods for +[`InternalDataDrivenProblem`](@ref). The two-argument method is required; the +three-argument method is required when the basis contains controls. The returned +`K` must represent a square operator on the lifted state space, and `B` must have +the corresponding output-by-control shape. A custom algorithm should preserve +these dimensions so that the result can be converted back to a [`DataDrivenDiffEq.Basis`](@ref). +# Arguments + +- `X::AbstractArray`: lifted input data, with features in rows and observations in + columns. +- `Y::AbstractArray`: lifted target data with the same number of columns as `X`. +- `U::AbstractArray`: optional control data with one column per observation. +- `B::AbstractArray` or `nothing`: an optional input map supplied by the common + four-argument adapter. + +# Returns + +Return `(K, B)`. `K` is an operator representation accepted by the result +constructor, and `B` is an input map or an empty matrix when the fit is +uncontrolled. + # Example ```julia -using LinearAlgebra + using LinearAlgebra struct MyKoopman <: DataDrivenDMD.AbstractKoopmanAlgorithm end -function (::MyKoopman)(X, Y) - return (eigen(Y / X), zeros(eltype(X), size(Y, 1), 0)) -end + function (::MyKoopman)(X, Y) + return eigen(Y / X), zeros(eltype(X), size(Y, 1), 0) + end ``` """ abstract type AbstractKoopmanAlgorithm <: AbstractDataDrivenAlgorithm end diff --git a/lib/DataDrivenDMD/src/algorithms.jl b/lib/DataDrivenDMD/src/algorithms.jl index 51d58cf8c..cc5c7a982 100644 --- a/lib/DataDrivenDMD/src/algorithms.jl +++ b/lib/DataDrivenDMD/src/algorithms.jl @@ -46,6 +46,17 @@ K = Y / X where `Y` and `X` are data matrices. Returns a `Eigen` factorization of the operator. +# Arguments + +- `X::AbstractArray`: lifted state data, with one observation per column. +- `Y::AbstractArray`: lifted next-state data with the same number of columns as `X`. +- `U::AbstractArray`: optional control data for the controlled DMDc form. + +# Returns + +Return `(K, B)`, where `K` is an `Eigen` factorization and `B` is an empty matrix +for the uncontrolled form or the learned input map for the controlled form. + # Fields $(FIELDS) @@ -53,6 +64,15 @@ $(FIELDS) # Signatures $(SIGNATURES) + +# Example + +```julia +X = [1.0 2.0; 2.0 4.0] +Y = [2.0 4.0; 4.0 8.0] +K, B = DMDPINV()(X, Y) +isempty(B) +``` """ mutable struct DMDPINV <: AbstractKoopmanAlgorithm end; @@ -89,6 +109,18 @@ where `Y` and `X = U*Σ*V'` are data matrices. The singular value decomposition the `truncation` parameter, which can either be an `Int` indicating an index-based truncation or a `Real` indicating a tolerance-based truncation. Returns a `Eigen` factorization of the operator. +# Arguments + +- `X::AbstractArray`: lifted state data, with one observation per column. +- `Y::AbstractArray`: lifted next-state data with the same number of columns as `X`. +- `U::AbstractArray`: optional control data for the controlled DMDc form. +- `truncation`: an integer rank or a real-valued relative singular-value tolerance. + +# Returns + +Return `(K, B)`, where `K` is an `Eigen` factorization and `B` is the learned input +map or an empty matrix when controls are absent. + # Fields $(FIELDS) @@ -96,6 +128,13 @@ $(FIELDS) # Signatures $(SIGNATURES) + +# Example + +```julia +K, B = DMDSVD(1)([1.0 2.0; 2.0 4.0], [2.0 4.0; 4.0 8.0]) +size(Matrix(K)) == (1, 1) +``` """ mutable struct DMDSVD{T} <: AbstractKoopmanAlgorithm where {T <: Number} """Indicates the truncation""" @@ -159,6 +198,19 @@ If `rtol` ∈ (0, 1) is given, the singular value decomposition is reduced to in entries bigger than `rtol*maximum(Σ)`. If `rtol` is an integer, the reduced SVD up to `rtol` is used for computation. +# Arguments + +- `X::AbstractArray`: lifted input data, with one observation per column. +- `Y::AbstractArray`: lifted target data with the same number of columns as `X`. +- `U::AbstractArray`: optional control data. +- `truncation`: rank or relative singular-value tolerance used for the joint SVD. +- `alg::AbstractKoopmanAlgorithm`: algorithm applied after the joint reduction. + +# Returns + +Return `(K, B)` from `alg` after the data are projected onto the retained singular +subspace. + # Fields $(FIELDS) @@ -166,6 +218,14 @@ $(FIELDS) # Signatures $(SIGNATURES) + +# Example + +```julia +alg = TOTALDMD(1, DMDPINV()) +K, B = alg([1.0 2.0; 2.0 4.0], [2.0 4.0; 4.0 8.0]) +isempty(B) +``` """ mutable struct TOTALDMD{R, A} <: AbstractKoopmanAlgorithm where {R <: Number, A <: AbstractKoopmanAlgorithm} @@ -204,6 +264,20 @@ It is assumed that `K = sqrt(K₁*inv(K₂))`, where `K₁` is the approximation If `truncation` ∈ (0, 1) is given, the singular value decomposition is reduced to include only entries bigger than `truncation*maximum(Σ)`. If `truncation` is an integer, the reduced SVD up to `truncation` is used for computation. +# Arguments + +- `X::AbstractArray`: lifted input data, with one observation per column. +- `Y::AbstractArray`: lifted target data with the same number of columns as `X`. +- `U::AbstractArray`: optional control data. This form delegates to the wrapped + `DMDSVD` algorithm. +- `truncation`: rank or relative singular-value tolerance used by the wrapped + `DMDSVD` algorithm. + +# Returns + +Return `(K, B)`, where `K` is an `Eigen` factorization and `B` is an empty matrix +for uncontrolled data or the learned input map for controlled data. + # Fields $(FIELDS) @@ -211,6 +285,13 @@ $(FIELDS) # Signatures $(SIGNATURES) + +# Example + +```julia +K, B = FBDMD(1)([1.0 2.0; 2.0 4.0], [2.0 4.0; 4.0 8.0]) +isempty(B) +``` """ mutable struct FBDMD{R} <: AbstractKoopmanAlgorithm where {R <: Number} alg::DMDSVD{R} diff --git a/lib/DataDrivenDMD/src/result.jl b/lib/DataDrivenDMD/src/result.jl index 1a1fcefa9..a8d36fafb 100644 --- a/lib/DataDrivenDMD/src/result.jl +++ b/lib/DataDrivenDMD/src/result.jl @@ -6,6 +6,16 @@ Result returned by DataDrivenDMD solvers. # Fields $(FIELDS) + +The `k`, `b`, and `c` fields represent the learned operator, input map, and +output map. `q` and `p` retain update matrices used by the online formulation; +they are developer state and should not be edited by callers. The remaining +fields implement the `StatsAPI.StatisticalModel` interface. + +# Returns + +The constructor returns a result whose operator and maps are compatible with +`get_operator`, `get_inputmap`, and `get_outputmap`. """ struct KoopmanResult{K, B, C, Q, P, T} <: AbstractDataDrivenResult """Matrix representation of the operator / generator""" diff --git a/lib/DataDrivenDMD/test/Core/interface.jl b/lib/DataDrivenDMD/test/Core/interface.jl new file mode 100644 index 000000000..db2616dbc --- /dev/null +++ b/lib/DataDrivenDMD/test/Core/interface.jl @@ -0,0 +1,39 @@ +using DataDrivenDMD +using LinearAlgebra: I, eigen +using Test + +struct InterfaceKoopman <: DataDrivenDMD.AbstractKoopmanAlgorithm end + +function (::InterfaceKoopman)(X::AbstractMatrix, Y::AbstractMatrix) + return eigen(Y / X), zeros(eltype(X), size(Y, 1), 0) +end + +function (::InterfaceKoopman)( + X::AbstractMatrix, Y::AbstractMatrix, U::AbstractMatrix + ) + return eigen(Y / X), zeros(eltype(X), size(Y, 1), size(U, 1)) +end + +@testset "Generic Koopman algorithm interface" begin + X = Matrix{Float64}(I, 2, 2) + Y = [2.0 0.0; 0.0 3.0] + U = zeros(1, 2) + B = zeros(2, 1) + algorithm = InterfaceKoopman() + + K, B0 = algorithm(X, Y) + @test Matrix(K) == Y + @test isempty(B0) + + K, B1 = algorithm(X, Y, U) + @test Matrix(K) == Y + @test size(B1) == (2, 1) + + K, B2 = algorithm(X, Y, U, B) + @test Matrix(K) == Y + @test B2 === B + + K, B3 = algorithm(X, Y, U, nothing) + @test Matrix(K) == Y + @test size(B3) == (2, 1) +end diff --git a/lib/DataDrivenDMD/test/runtests.jl b/lib/DataDrivenDMD/test/runtests.jl index 996e0dbcd..d7bea3c99 100644 --- a/lib/DataDrivenDMD/test/runtests.jl +++ b/lib/DataDrivenDMD/test/runtests.jl @@ -33,6 +33,9 @@ end @safetestset "Nonlinear forced" begin include("./Core/nonlinear_forced.jl") end + @safetestset "Interface" begin + include("./Core/interface.jl") + end end if GROUP == "QA" diff --git a/lib/DataDrivenLux/src/DataDrivenLux.jl b/lib/DataDrivenLux/src/DataDrivenLux.jl index c54b46095..5f0091968 100644 --- a/lib/DataDrivenLux/src/DataDrivenLux.jl +++ b/lib/DataDrivenLux/src/DataDrivenLux.jl @@ -53,10 +53,17 @@ algorithms. This interface is intended for solver extensions, not ordinary users # Interface A subtype must provide an `options` field compatible with [`CommonAlgOptions`](@ref) -and a method `update_parameters!(cache::SearchCache{<:MyAlgorithm})`. The generic -cache initialization supplies the dataset, candidate population, and optimization -state. An algorithm that uses a different graph representation must also specialize -`init_model(alg, basis, dataset, intervals)`. +and methods `init_model(alg, basis, dataset, intervals)` and +`update_parameters!(cache::SearchCache{<:MyAlgorithm})`. The generic cache +initialization supplies the dataset, candidate population, and optimization state. +`init_model` must return a callable Lux model compatible with the basis and +dataset dimensions. `update_parameters!` must mutate `cache.p` or other algorithm +state in place and return `nothing`. An algorithm that uses the default layered +graph can reuse the generic `init_model` method. + +The `init_model` method should retain the package's dispatch shape, +`(::MyAlgorithm, ::Basis, ::Dataset, intervals)`, so it is more specific than the +default method while remaining applicable to the common solver path. The generic `CommonSolve.solve!` path consumes the cache, repeatedly calls `update_parameters!`, and returns a [`DataDrivenDiffEq.DataDrivenSolution`](@ref). @@ -70,8 +77,11 @@ struct MyDAGAlgorithm <: DataDrivenLux.AbstractDAGSRAlgorithm options::DataDrivenLux.CommonAlgOptions end -DataDrivenLux.update_parameters!(cache::DataDrivenLux.SearchCache{<:MyDAGAlgorithm}) = - nothing +DataDrivenLux.init_model(alg, basis, dataset, intervals) = + DataDrivenLux.LayeredDAG( + length(basis), size(dataset.y, 1), 1, (1,), (identity,) + ) +DataDrivenLux.update_parameters!(cache::DataDrivenLux.SearchCache{<:MyDAGAlgorithm}) = nothing ``` """ abstract type AbstractDAGSRAlgorithm <: AbstractDataDrivenAlgorithm end @@ -82,6 +92,53 @@ abstract type AbstractErrorDistribution end abstract type AbstractConfigurationCache <: StatisticalModel end abstract type AbstractRewardScale{risk} end +""" + init_model(alg, basis, dataset, intervals) + +Construct the callable Lux model used by a differentiable symbolic-regression +algorithm. `basis` supplies the feature count, `dataset` supplies target and +control dimensions, and `intervals` contains the interval-evaluated basis values +used to mask invalid inputs. + +# Returns + +Return a model accepted by `LuxCore.initialparameters`, `LuxCore.setup`, and the +call `(model)(inputs, parameters, state)`. A custom algorithm may specialize this +method when it does not use the default [`LayeredDAG`](@ref) representation. +""" +function init_model end + +""" + update_parameters!(cache) + +Update the population parameters for a symbolic-regression search iteration. +The method is called by `update_cache!` after the retained candidates have been +selected. Mutate the cache in place and return `nothing`. +""" +function update_parameters! end + +""" + init_cache(alg::AbstractDAGSRAlgorithm, basis, problem; kwargs...) + +Build the search cache consumed by the common `solve!` implementation. The +default method creates a [`Dataset`](@ref), calls [`init_model`](@ref), samples +the initial population, and initializes the optimizer state. A custom algorithm +may specialize this method when its cache representation differs from +[`SearchCache`](@ref). +""" +function init_cache end + +""" + convert_to_basis(candidate, parameters, options) + +Convert the selected symbolic-regression candidate into a +[`DataDrivenDiffEq.Basis`](@ref). A custom graph implementation must provide this +method if it does not use the package's [`Candidate`](@ref) representation. +""" +function convert_to_basis end + +@public init_model, init_cache, update_parameters!, convert_to_basis + @enum __PROCESSUSE begin SERIAL = 1 THREADED = 2 diff --git a/lib/DataDrivenLux/src/algorithms/common.jl b/lib/DataDrivenLux/src/algorithms/common.jl index b98049051..b4f780129 100644 --- a/lib/DataDrivenLux/src/algorithms/common.jl +++ b/lib/DataDrivenLux/src/algorithms/common.jl @@ -7,6 +7,39 @@ Concrete algorithms normally expose these keywords through their own constructor # Fields $(FIELDS) + +# Keywords + +- `populationsize::Int`: number of candidate graphs retained in the population. +- `functions`: candidate unary and binary functions. +- `arities`: arity corresponding to each entry in `functions`. +- `n_layers::Int`: number of learned graph layers. +- `skip::Bool`: whether each layer receives skip connections. +- `simplex::AbstractSimplex`: map used for categorical path weights. +- `loss`: function used to rank candidates. +- `keep::Union{Real,Int}`: retained fraction or number of candidates. +- `use_protected::Bool`: whether unsafe symbolic operations are replaced by safe + versions. +- `distributed::Bool`: whether candidate optimization uses worker processes. +- `threaded::Bool`: whether candidate optimization uses Julia threads. +- `rng::AbstractRNG`: random-number generator for graph sampling. +- `optimizer`: Optim.jl optimizer for continuous candidate parameters. +- `optim_options`: optional Optim.jl options object. +- `optimiser`: optional Optimisers.jl update rule for search parameters. +- `observed`: optional fixed or fitted observation model. +- `alpha::Real`: exponential-update coefficient used by cross-entropy search. + +# Returns + +Return a configuration object consumed by [`AbstractDAGSRAlgorithm`](@ref) +implementations. + +# Example + +```julia +options = CommonAlgOptions(populationsize = 20, n_layers = 2) +options.populationsize == 20 +``` """ @concrete struct CommonAlgOptions populationsize::Int diff --git a/lib/DataDrivenLux/src/algorithms/crossentropy.jl b/lib/DataDrivenLux/src/algorithms/crossentropy.jl index d9083e190..a468f1501 100644 --- a/lib/DataDrivenLux/src/algorithms/crossentropy.jl +++ b/lib/DataDrivenLux/src/algorithms/crossentropy.jl @@ -7,6 +7,18 @@ $(SIGNATURES) Uses the crossentropy method for discrete optimization to search the space of possible solutions. + +# Keywords + +The constructor accepts `populationsize`, `functions`, `arities`, `n_layers`, +`skip`, `loss`, `keep`, `use_protected`, `distributed`, `threaded`, `rng`, +`optimizer`, `optim_options`, `observed`, and `alpha`, which are forwarded to +[`CommonAlgOptions`](@ref). + +# Returns + +Return a [`AbstractDAGSRAlgorithm`](@ref) that updates categorical graph +parameters using the cross-entropy rule. """ function CrossEntropy(; populationsize = 100, functions = (sin, exp, cos, log, +, -, /, *), diff --git a/lib/DataDrivenLux/src/algorithms/randomsearch.jl b/lib/DataDrivenLux/src/algorithms/randomsearch.jl index 82a58ded9..4734bf216 100644 --- a/lib/DataDrivenLux/src/algorithms/randomsearch.jl +++ b/lib/DataDrivenLux/src/algorithms/randomsearch.jl @@ -7,6 +7,18 @@ $(SIGNATURES) Performs a random search over the space of possible solutions to the symbolic regression problem. + +# Keywords + +The constructor accepts the fields of [`CommonAlgOptions`](@ref): +`populationsize`, `functions`, `arities`, `n_layers`, `skip`, `loss`, `keep`, +`use_protected`, `distributed`, `threaded`, `rng`, `optimizer`, +`optim_options`, `observed`, and `alpha`. + +# Returns + +Return a [`AbstractDAGSRAlgorithm`](@ref) that updates candidate graphs by +resampling without changing their continuous parameters. """ function RandomSearch(; populationsize = 100, functions = (sin, exp, cos, log, +, -, /, *), diff --git a/lib/DataDrivenLux/src/algorithms/reinforce.jl b/lib/DataDrivenLux/src/algorithms/reinforce.jl index 9310b4644..dfb14e632 100644 --- a/lib/DataDrivenLux/src/algorithms/reinforce.jl +++ b/lib/DataDrivenLux/src/algorithms/reinforce.jl @@ -9,6 +9,20 @@ $(SIGNATURES) Uses the REINFORCE algorithm to search over the space of possible solutions to the symbolic regression problem. + +# Keywords + +- `reward`: [`RelativeReward`](@ref) or [`AbsoluteReward`](@ref) transform. +- `ad_backend`: optional AbstractDifferentiation backend. +- `optimiser`: Optimisers.jl update rule for continuous search parameters. +- `populationsize`, `functions`, `arities`, `n_layers`, `skip`, `loss`, `keep`, + `use_protected`, `distributed`, `threaded`, `rng`, `optimizer`, + `optim_options`, `observed`, and `alpha`: forwarded to + [`CommonAlgOptions`](@ref). + +# Returns + +Return a differentiable [`AbstractDAGSRAlgorithm`](@ref) for population search. """ function Reinforce(; reward = RelativeReward(false), populationsize = 100, diff --git a/lib/DataDrivenLux/src/algorithms/rewards.jl b/lib/DataDrivenLux/src/algorithms/rewards.jl index 9da081289..af25b49ca 100644 --- a/lib/DataDrivenLux/src/algorithms/rewards.jl +++ b/lib/DataDrivenLux/src/algorithms/rewards.jl @@ -2,6 +2,13 @@ $(TYPEDEF) Scales the losses in such a way that the minimum loss is equal to one. + +Calling `RelativeReward(risk_seeking)(losses)` returns exponentially scaled +rewards. With `risk_seeking = true`, the minimum reward is shifted to zero. + +# Arguments + +- `risk_seeking::Bool`: whether to subtract the minimum reward after scaling. """ struct RelativeReward{risk} <: AbstractRewardScale{risk} end @@ -20,6 +27,13 @@ end $(TYPEDEF) Scales the losses in such a way that the minimum loss is the most influential reward. + +Calling `AbsoluteReward(risk_seeking)(losses)` uses `exp.(-losses)` directly. +With `risk_seeking = true`, the minimum reward is shifted to zero. + +# Arguments + +- `risk_seeking::Bool`: whether to subtract the minimum reward after scaling. """ struct AbsoluteReward{risk} <: AbstractRewardScale{risk} end diff --git a/lib/DataDrivenLux/src/caches/cache.jl b/lib/DataDrivenLux/src/caches/cache.jl index e5c912692..170e908b9 100644 --- a/lib/DataDrivenLux/src/caches/cache.jl +++ b/lib/DataDrivenLux/src/caches/cache.jl @@ -6,6 +6,10 @@ Optimization cache for DataDrivenLux symbolic regression algorithms. # Fields $(FIELDS) + +The cache owns the candidate population and the current search parameters. It is +mutated by `update_cache!`; callers should treat it as an implementation object +unless they are implementing a new [`AbstractDAGSRAlgorithm`](@ref). """ struct SearchCache{ALG, PTYPE, O} <: AbstractAlgorithmCache alg::ALG diff --git a/lib/DataDrivenLux/src/caches/candidate.jl b/lib/DataDrivenLux/src/caches/candidate.jl index 785ec8ddd..04ccb0636 100644 --- a/lib/DataDrivenLux/src/caches/candidate.jl +++ b/lib/DataDrivenLux/src/caches/candidate.jl @@ -47,6 +47,24 @@ to the symbolic regression problem. # Fields $(FIELDS) + +# Arguments + +- `rng`: random-number generator replicated for this candidate. +- `model`: callable graph model. +- `basis`: feature basis used to evaluate the dataset. +- `dataset::Dataset`: observed data and interval bounds. + +# Keywords + +- `observed::ObservedModel`: observation likelihood model. +- `parameterdist`: distribution and transform for basis parameters. +- `ptype`: element type used for candidate state and parameters. + +# Returns + +Return a candidate with initialized Lux parameters, path state, scales, and +statistical fit values. """ @concrete struct Candidate <: StatisticalModel "Random seed" diff --git a/lib/DataDrivenLux/src/caches/dataset.jl b/lib/DataDrivenLux/src/caches/dataset.jl index e3d9fffa5..6885eccb2 100644 --- a/lib/DataDrivenLux/src/caches/dataset.jl +++ b/lib/DataDrivenLux/src/caches/dataset.jl @@ -7,6 +7,17 @@ targets, controls, time points, and interval bounds for symbolic search. # Fields $(FIELDS) + +# Arguments + +- `X::AbstractMatrix`: state or feature data, with observations in columns. +- `Y::AbstractMatrix`: target data, with target variables in rows. +- `U::AbstractMatrix`: optional control data; defaults to an empty matrix. +- `t::AbstractVector`: optional time points; defaults to equally spaced indices. + +# Returns + +Return a promoted, interval-annotated dataset used by candidate models. """ @concrete struct Dataset{T} x <: AbstractMatrix{T} diff --git a/lib/DataDrivenLux/src/custom_priors.jl b/lib/DataDrivenLux/src/custom_priors.jl index 25631b00c..542e22402 100644 --- a/lib/DataDrivenLux/src/custom_priors.jl +++ b/lib/DataDrivenLux/src/custom_priors.jl @@ -2,6 +2,14 @@ $(TYPEDEF) Additive output error model for observations following `ŷ ~ y + ϵ`. + +When called as `model(distribution, y, y_pred, scale)`, the model evaluates the +log-likelihood of `y_pred` under the distribution centered at `y` with the supplied +scale. + +# Returns + +Return a scalar log-likelihood contribution. """ struct AdditiveError <: AbstractErrorModel end @@ -16,6 +24,13 @@ end $(TYPEDEF) Multiplicative output error model for observations following `ŷ ~ y * (1 + ϵ)`. + +When called as `model(distribution, y, y_pred, scale)`, the scale is multiplied by +`abs(y)` before evaluating the distribution. + +# Returns + +Return a scalar log-likelihood contribution. """ struct MultiplicativeError <: AbstractErrorModel end @@ -122,8 +137,25 @@ end $(TYPEDEF) The error distribution of a models output. + +Construct `ObservedModel(Y; fixed = false)` to create one additive-normal error +distribution per row of the target matrix. Set `fixed = true` to keep the initial +scale fixed during optimization. + +# Arguments + +- `Y::AbstractMatrix`: observed target data, with one target variable per row. + +# Keywords + +- `fixed::Bool`: whether the observation scales are optimized. + +# Returns + +Return an observation model used by [`Candidate`](@ref) likelihood calculations. """ struct ObservedModel{fixed, M} + """One observation distribution per target row.""" observed_distributions::NTuple{M, ObservedDistribution} end diff --git a/lib/DataDrivenLux/src/lux/graph.jl b/lib/DataDrivenLux/src/lux/graph.jl index e454fc1c1..f83268798 100644 --- a/lib/DataDrivenLux/src/lux/graph.jl +++ b/lib/DataDrivenLux/src/lux/graph.jl @@ -7,6 +7,25 @@ different [`FunctionLayer`](@ref)s. # Fields $(FIELDS) + +# Arguments + +- `in_dimension::Int`: number of input signals. +- `out_dimension::Int`: number of output equations. +- `n_layers::Int`: number of learned function layers. +- `arities`: arity for each candidate function. +- `fs`: candidate functions. + +# Keywords + +- `skip::Bool`: retain outputs from preceding layers as inputs. +- `eltype::Type`: element type used for initial weights. +- `input_functions`: optional functions used to generate input signals. + +# Returns + +Return a Lux wrapper model that maps candidate graph parameters and states to +symbolic-regression outputs. """ @concrete struct LayeredDAG <: AbstractLuxWrapperLayer{:layers} layers diff --git a/lib/DataDrivenLux/src/lux/layer.jl b/lib/DataDrivenLux/src/lux/layer.jl index dee9b06b9..2d92fb06d 100644 --- a/lib/DataDrivenLux/src/lux/layer.jl +++ b/lib/DataDrivenLux/src/lux/layer.jl @@ -7,6 +7,22 @@ It accumulates all outputs of the nodes. # Fields $(FIELDS) + +# Arguments + +- `in_dimension::Int`: number of available input signals. +- `arities::Tuple`: arity for each function in `fs`. +- `fs::Tuple`: functions used to construct the nodes. + +# Keywords + +- `skip::Bool`: include a skip connection around the layer. +- `id_offset::Int`: starting layer identifier for path bookkeeping. +- `input_functions`: optional input functions used by each node. + +# Returns + +Return a Lux wrapper layer whose output contains the values of all nodes. """ @concrete struct FunctionLayer <: AbstractLuxWrapperLayer{:nodes} nodes diff --git a/lib/DataDrivenLux/src/lux/node.jl b/lib/DataDrivenLux/src/lux/node.jl index cec574e96..be55e8f36 100644 --- a/lib/DataDrivenLux/src/lux/node.jl +++ b/lib/DataDrivenLux/src/lux/node.jl @@ -7,6 +7,18 @@ and a latent array of weights representing a probability distribution over the i # Fields $(FIELDS) + +# Arguments + +- `f`: unary or binary function represented by the node. +- `arity::Int`: number of inputs consumed by `f`. +- `in_dims::Int`: number of available input signals. +- `id`: `(layer, node)` identifier used in path statistics. +- `input_functions`: optional functions used to construct input masks. + +# Returns + +Return a Lux wrapper layer that samples one function node configuration. """ @concrete struct FunctionNode <: AbstractLuxWrapperLayer{:node} node diff --git a/lib/DataDrivenLux/src/lux/path_state.jl b/lib/DataDrivenLux/src/lux/path_state.jl index 8aadc1af7..896635f2e 100644 --- a/lib/DataDrivenLux/src/lux/path_state.jl +++ b/lib/DataDrivenLux/src/lux/path_state.jl @@ -9,6 +9,17 @@ operators, and node identifiers used to compute path complexity. # Fields $(FIELDS) + +# Arguments + +- `interval::Interval`: interval containing values reachable along the path. +- `path_operators::Tuple`: operators applied along the path. +- `path_ids::Tuple`: `(layer, node)` identifiers for the path nodes. + +# Returns + +Return an immutable path state. Use `update_path` to prepend another operation +without mutating an existing state. """ struct PathState{T, PO <: Tuple, PI <: Tuple} <: AbstractPathState "Accumulated loglikelihood of the state" diff --git a/lib/DataDrivenLux/src/lux/simplex.jl b/lib/DataDrivenLux/src/lux/simplex.jl index a4ca8b258..0ee255ded 100644 --- a/lib/DataDrivenLux/src/lux/simplex.jl +++ b/lib/DataDrivenLux/src/lux/simplex.jl @@ -5,6 +5,17 @@ $(TYPEDEF) Maps an `AbstractVector` to the probability simplex by using `softmax` on each row. + +# Arguments + +- `rng::AbstractRNG`: random-number generator, unused by this deterministic map. +- `xhat`: output buffer with the shape of `x`. +- `x`: unnormalized logits. +- `kappa`: positive temperature; defaults to one. + +# Returns + +Return the output buffer after normalizing each row. """ struct Softmax <: AbstractSimplex end @@ -20,6 +31,17 @@ $(TYPEDEF) Maps an `AbstractVector` to the probability simplex by adding gumbel distributed noise and using `softmax` on each row. +# Arguments + +- `rng::AbstractRNG`: random-number generator used for Gumbel noise. +- `xhat`: output buffer with the shape of `x`. +- `x`: unnormalized logits. +- `kappa`: positive temperature; defaults to one. + +# Returns + +Return the output buffer after adding noise and normalizing each row. + # Fields $(FIELDS) @@ -43,6 +65,17 @@ $(TYPEDEF) Assumes an `AbstractVector` is on the probability simplex. +# Arguments + +- `rng::AbstractRNG`: random-number generator, unused by this map. +- `xhat`: output buffer with the shape of `x`. +- `x`: probabilities that already sum to one along each row. +- `kappa`: accepted for interface compatibility and otherwise unused. + +# Returns + +Return `xhat` after copying `x` into it. + # Fields $(FIELDS) diff --git a/lib/DataDrivenLux/test/Core/interface.jl b/lib/DataDrivenLux/test/Core/interface.jl new file mode 100644 index 000000000..3b9685dd3 --- /dev/null +++ b/lib/DataDrivenLux/test/Core/interface.jl @@ -0,0 +1,41 @@ +using DataDrivenDiffEq +using DataDrivenLux +using IntervalArithmetic: interval +using ModelingToolkit: @variables +using Test + +struct InterfaceAlgorithm <: DataDrivenLux.AbstractDAGSRAlgorithm + options::DataDrivenLux.CommonAlgOptions +end + +DataDrivenLux.init_model( + ::InterfaceAlgorithm, basis::Basis, dataset::DataDrivenLux.Dataset, intervals +) = DataDrivenLux.LayeredDAG( + length(basis), size(dataset.y, 1), 1, (1,), (identity,) +) + +DataDrivenLux.update_parameters!( + ::DataDrivenLux.SearchCache{<:InterfaceAlgorithm} +) = nothing + +@variables x +basis = Basis([x], [x]) +problem = DirectDataDrivenProblem( + reshape([1.0, 2.0, 3.0], 1, :), reshape([2.0, 4.0, 6.0], 1, :) +) +dataset = DataDrivenLux.Dataset(problem) +intervals = [interval(-10.0, 10.0)] +algorithm = InterfaceAlgorithm(DataDrivenLux.CommonAlgOptions()) + +@testset "Generic DAG symbolic-regression interface" begin + model = DataDrivenLux.init_model(algorithm, basis, dataset, intervals) + @test model isa DataDrivenLux.LayeredDAG + + cache = DataDrivenLux.SearchCache{ + InterfaceAlgorithm, DataDrivenLux.__PROCESSUSE(1), Nothing, + }( + algorithm, DataDrivenLux.Candidate[], Int[], Bool[], Int[], Float32[], + dataset, nothing + ) + @test DataDrivenLux.update_parameters!(cache) === nothing +end diff --git a/lib/DataDrivenLux/test/runtests.jl b/lib/DataDrivenLux/test/runtests.jl index 7c724d8d8..68d0cd016 100644 --- a/lib/DataDrivenLux/test/runtests.jl +++ b/lib/DataDrivenLux/test/runtests.jl @@ -25,6 +25,7 @@ end @safetestset "Nodes" include("Core/nodes.jl") @safetestset "Layers" include("Core/layers.jl") @safetestset "Graphs" include("Core/graphs.jl") + @safetestset "Interface" include("Core/interface.jl") end @testset "Caches" begin diff --git a/lib/DataDrivenSR/src/DataDrivenSR.jl b/lib/DataDrivenSR/src/DataDrivenSR.jl index 4b5bb7778..e1c3b92b0 100644 --- a/lib/DataDrivenSR/src/DataDrivenSR.jl +++ b/lib/DataDrivenSR/src/DataDrivenSR.jl @@ -22,6 +22,22 @@ Options for using SymbolicRegression.jl within the `solve` function. Automatically creates [`Options`](https://docs.sciml.ai/SymbolicRegression/stable/api/#Options) with the given specification. Sorts the operators stored in `functions` into unary and binary operators on conversion. +# Keywords + +- `weights`: optional observation weights with the shape of the target data. +- `numprocs`: number of worker processes created for multiprocessing. +- `procs`: already allocated worker process IDs. +- `addprocs_function`: replacement for `Distributed.addprocs` when workers are + allocated by a scheduler. +- `parallelism`: `:serial`, `:multithreading`, or `:multiprocessing`. +- `runtests::Bool`: whether SymbolicRegression runs its environment checks first. +- `eq_options`: [`SymbolicRegression.Options`] used by equation search. + +# Returns + +Return an algorithm object usable with the common `solve` interface. The solver +returns a [`DataDrivenSolution`](@ref) containing the selected symbolic basis. + # Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/DataDrivenSparse.jl b/lib/DataDrivenSparse/src/DataDrivenSparse.jl index 556f905fe..19c9f0476 100644 --- a/lib/DataDrivenSparse/src/DataDrivenSparse.jl +++ b/lib/DataDrivenSparse/src/DataDrivenSparse.jl @@ -86,7 +86,8 @@ place, `operator(y, x, λ)` as an out-of-place-buffer form, and `active_set!(mask, operator, x, λ)` to identify the nonzero coefficients. The two callable forms must preserve the shape and element type of the coefficient array. Concrete operators may store additional thresholds, but those fields and their -defaults must be documented. +defaults must be documented. The in-place form returns the modified `x`; the +buffer form writes `y` and returns it. `active_set!` returns the modified mask. """ abstract type AbstractProximalOperator end @@ -100,12 +101,45 @@ Update the Boolean active-set mask for a sparse-regression proximal operator. This is a developer extension point for [`AbstractProximalOperator`](@ref). `mask` and `x` must have the same shape, and an active entry indicates that the corresponding coefficient survives thresholding. + +# Arguments + +- `mask`: Boolean array with the same shape as `x`. +- `operator::AbstractProximalOperator`: thresholding operator. +- `x`: coefficient array inspected by the operator. +- `lambda`: nonnegative threshold parameter. + +# Returns + +Return the modified `mask`. """ function active_set! end @public active_set! +""" + AbstractSparseRegressionCache + +Developer interface for the mutable cache used by +[`SparseLinearSolver`](@ref). This type is intended for packages implementing a +new [`AbstractSparseRegressionAlgorithm`](@ref), not for constructing user +results directly. + +# Interface + +A cache subtype must provide mutable fields `Ã`, `B̃`, `X`, `X_prev`, and +`active_set`. `Ã` is the feature matrix, `B̃` is the target vector or matrix, +`X` is the current coefficient array, `X_prev` is the previous iterate, and +`active_set` has the same shape as `X`. The generic solver calls `step!(cache, +λ)`, copies a winning cache with `_set!`, and checks convergence with the +`abstol` and `reltol` values from [`DataDrivenCommonOptions`](@ref). + +The cache must also support the `StatsAPI` methods `coef`, `rss`, `dof`, and +`nobs`; the default methods supplied here use the fields above. A custom cache +should preserve the coefficient shape and return a numeric residual from `rss`. +""" abstract type AbstractSparseRegressionCache <: StatisticalModel end +@public AbstractSparseRegressionCache function _set!(x::AbstractSparseRegressionCache, y::AbstractSparseRegressionCache) begin @@ -171,13 +205,56 @@ StatsAPI.r2(x::AbstractSparseRegressionCache) = r2(x, :CoxSnell) include("algorithms/proximals.jl") export SoftThreshold, HardThreshold, ClippedAbsoluteDeviation +""" + get_thresholds(alg::AbstractSparseRegressionAlgorithm) + +Return the scalar threshold or ordered threshold schedule explored by +[`SparseLinearSolver`](@ref). A custom algorithm must return either a scalar or +an iterable that supports `minimum` and iteration. +""" get_thresholds(x::AbstractSparseRegressionAlgorithm) = getfield(x, :thresholds) + +""" + get_relaxation(alg::AbstractSparseRegressionAlgorithm) + +Return an optional relaxation parameter used by an algorithm. The default is +`nothing`; algorithms that expose relaxation should specialize this method and +document how it changes their update rule. +""" get_relaxation(x::AbstractSparseRegressionAlgorithm) = nothing + +""" + get_proximal(alg::AbstractSparseRegressionAlgorithm) + +Return the [`AbstractProximalOperator`](@ref) used by an algorithm. The default +is [`SoftThreshold`](@ref). +""" get_proximal(x::AbstractSparseRegressionAlgorithm) = SoftThreshold() include("solver.jl") export SparseLinearSolver +""" + init_cache(alg, A, B) + +Construct the mutable [`AbstractSparseRegressionCache`](@ref) for a sparse +regression algorithm. `A` contains features by observation and `B` contains +targets by observation. Implement this method for a custom algorithm before +using it with [`SparseLinearSolver`](@ref). +""" +function init_cache end + +""" + step!(cache, lambda) + +Perform one thresholded update of a sparse-regression cache in place. The +implementation must update `cache.X`, `cache.X_prev`, and `cache.active_set` +consistently and return the cache or `nothing`. +""" +function step! end + +@public get_thresholds, get_relaxation, get_proximal, init_cache, step! + function (x::X where {X <: AbstractSparseRegressionAlgorithm})( X, Y; options::DataDrivenCommonOptions = DataDrivenCommonOptions(), diff --git a/lib/DataDrivenSparse/src/algorithms/ADMM.jl b/lib/DataDrivenSparse/src/algorithms/ADMM.jl index c0370e5b4..3e4e0f4da 100644 --- a/lib/DataDrivenSparse/src/algorithms/ADMM.jl +++ b/lib/DataDrivenSparse/src/algorithms/ADMM.jl @@ -12,6 +12,16 @@ It solves the following problem $(FIELDS) +# Arguments + +- `threshold`: positive scalar or iterable of positive sparsity thresholds. +- `ρ`: positive augmented-Lagrangian parameter. + +# Returns + +Return an algorithm object callable as `alg(X, Y; options)`, producing coefficient +matrices, selected thresholds, and iteration counts. + # Example ```julia diff --git a/lib/DataDrivenSparse/src/algorithms/Implicit.jl b/lib/DataDrivenSparse/src/algorithms/Implicit.jl index 0e6848d81..a89b0abec 100644 --- a/lib/DataDrivenSparse/src/algorithms/Implicit.jl +++ b/lib/DataDrivenSparse/src/algorithms/Implicit.jl @@ -7,6 +7,22 @@ solving the explicit problem, as introduced [here](https://royalsocietypublishin \\argmin_{x} \\|x\\|_0 ~s.t.~Ax= 0 ``` +# Arguments + +- `threshold`: threshold passed to the explicit optimizer when `opt` is a type. +- `opt`: an [`AbstractSparseRegressionAlgorithm`](@ref) instance or constructor. + +# Keywords + +- `options::DataDrivenCommonOptions`: convergence and selection settings. +- `necessary_idx`: Boolean mask identifying coefficients that must participate + in each candidate implicit relation. + +# Returns + +Return an implicit sparse-regression algorithm object. Calling it returns the +best cache, threshold, and iteration count for each candidate left-hand side. + # Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/algorithms/SR3.jl b/lib/DataDrivenSparse/src/algorithms/SR3.jl index 9c99ea51d..5f0afd525 100644 --- a/lib/DataDrivenSparse/src/algorithms/SR3.jl +++ b/lib/DataDrivenSparse/src/algorithms/SR3.jl @@ -12,6 +12,17 @@ It solves the following problem Where `R` is a proximal operator, and the result is given by `w`. +# Arguments + +- `threshold`: positive sparsity threshold or threshold schedule. +- `nu`: positive relaxation parameter. +- `R`: [`AbstractProximalOperator`](@ref) used for the relaxed update. + +# Returns + +Return an algorithm object callable as `alg(X, Y; options)`, producing coefficient +matrices, selected thresholds, and iteration counts. + # Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/algorithms/STLSQ.jl b/lib/DataDrivenSparse/src/algorithms/STLSQ.jl index 4a86a9c68..80f9ee0e6 100644 --- a/lib/DataDrivenSparse/src/algorithms/STLSQ.jl +++ b/lib/DataDrivenSparse/src/algorithms/STLSQ.jl @@ -18,6 +18,16 @@ with the additional constraint If the parameter `ρ > 0`, ridge regression will be performed using the normal equations of the corresponding regression problem. +# Arguments + +- `threshold`: positive scalar or iterable of positive thresholds to explore. +- `rho`: nonnegative ridge-regression coefficient. + +# Returns + +Return an algorithm object callable as `alg(X, Y; options)`, producing coefficient +matrices, selected thresholds, and iteration counts. + # Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/algorithms/WyNDA.jl b/lib/DataDrivenSparse/src/algorithms/WyNDA.jl index ec24e7188..45194eb4b 100644 --- a/lib/DataDrivenSparse/src/algorithms/WyNDA.jl +++ b/lib/DataDrivenSparse/src/algorithms/WyNDA.jl @@ -10,6 +10,22 @@ The forgetting factor `λ` controls how quickly older samples are discounted. Values close to one recover a batch least-squares-like fit, while smaller values adapt faster to parameter drift. +# Arguments + +- `λ`: forgetting factor satisfying `0 < λ <= 1`. + +# Keywords + +- `initial_covariance`: positive scalar or square matrix used for the initial + inverse covariance. +- `initial_coefficients`: optional initial coefficient vector or target-by-feature + matrix. + +# Returns + +Return an online algorithm object callable as `alg(X, Y; options)`, producing the +coefficient matrix, the forgetting factor, and the number of observations. + # Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/algorithms/proximals.jl b/lib/DataDrivenSparse/src/algorithms/proximals.jl index 9fdf54647..468c0a367 100644 --- a/lib/DataDrivenSparse/src/algorithms/proximals.jl +++ b/lib/DataDrivenSparse/src/algorithms/proximals.jl @@ -20,6 +20,17 @@ Proximal operator, which implements the soft thresholding operator. sign(x) * max(abs(x) - λ, 0) ``` +# Arguments + +- `x`: coefficient array updated in place, or the source array for the buffered + form. +- `λ`: nonnegative threshold. + +# Returns + +Return the modified array. The three-argument form writes the result into its +first array argument. + See [by Zheng et al., 2018](https://ieeexplore.ieee.org/document/8573778). """ struct SoftThreshold <: AbstractProximalOperator end; @@ -61,6 +72,17 @@ Proximal operator, which implements the hard thresholding operator. abs(x) > sqrt(2*λ) ? x : 0 ``` +# Arguments + +- `x`: coefficient array updated in place, or the source array for the buffered + form. +- `λ`: nonnegative threshold. + +# Returns + +Return the modified array. The three-argument form writes the result into its +first array argument. + See [by Zheng et al., 2018](https://ieeexplore.ieee.org/document/8573778). """ struct HardThreshold <: AbstractProximalOperator end; @@ -104,6 +126,21 @@ abs(x) > ρ ? x : sign(x) * max(abs(x) - λ, 0) Where `ρ = 5λ` per default. +# Arguments + +- `x`: coefficient array updated in place, or the source array for the buffered + form. +- `λ`: nonnegative soft-threshold parameter. + +# Fields + +- `ρ`: optional hard cutoff; `NaN` selects the default `5λ` cutoff. + +# Returns + +Return the modified array. The three-argument form writes the result into its +first array argument. + #Fields $(FIELDS) diff --git a/lib/DataDrivenSparse/src/solver.jl b/lib/DataDrivenSparse/src/solver.jl index 3bc8fb0c3..59c631f38 100644 --- a/lib/DataDrivenSparse/src/solver.jl +++ b/lib/DataDrivenSparse/src/solver.jl @@ -4,23 +4,39 @@ $(TYPEDEF) Sparse regression solver that applies an [`AbstractSparseRegressionAlgorithm`](@ref) to one or more target variables. -## Constructor +# Arguments -```julia -SparseLinearSolver(algorithm; options = DataDrivenCommonOptions()) -``` +- `algorithm::AbstractSparseRegressionAlgorithm`: algorithm used for each target. + +# Keywords + +- `options::DataDrivenCommonOptions`: tolerances, iteration limits, selector, and + progress settings copied into the solver. + +# Returns + +Return a solver object callable as `solver(X, Y)`, where `X` has features in rows +and `Y` has target variables in rows. The call returns one cache, selected +threshold, and iteration count per target. ## Fields $(TYPEDFIELDS) """ struct SparseLinearSolver{A <: AbstractSparseRegressionAlgorithm, T <: Number} + """Sparse-regression algorithm applied to each target.""" algorithm::A + """Absolute convergence tolerance for cache updates.""" abstol::T + """Relative convergence tolerance for cache updates.""" reltol::T + """Maximum number of iterations over all thresholds.""" maxiters::Int + """Whether progress information is printed.""" verbose::Bool + """Whether the underlying algorithm reports progress.""" progress::Bool + """Function used to select the best cache.""" selector::Function end diff --git a/lib/DataDrivenSparse/test/Core/interface.jl b/lib/DataDrivenSparse/test/Core/interface.jl new file mode 100644 index 000000000..1a15ec5d4 --- /dev/null +++ b/lib/DataDrivenSparse/test/Core/interface.jl @@ -0,0 +1,77 @@ +using DataDrivenDiffEq +using DataDrivenSparse +using Test + +mutable struct InterfaceCache <: DataDrivenSparse.AbstractSparseRegressionCache + Ã::Matrix{Float64} + B̃::Vector{Float64} + X::Matrix{Float64} + X_prev::Matrix{Float64} + active_set::BitMatrix +end + +struct InterfaceSparseAlgorithm <: DataDrivenSparse.AbstractSparseRegressionAlgorithm + thresholds::Vector{Float64} +end + +struct InterfaceProximal <: DataDrivenSparse.AbstractProximalOperator end + +function (::InterfaceProximal)(x::AbstractArray, lambda) + x .= ifelse.(abs.(x) .> lambda, x, zero(eltype(x))) + return x +end + +function (::InterfaceProximal)(y::AbstractArray, x::AbstractArray, lambda) + y .= ifelse.(abs.(x) .> lambda, x, zero(eltype(x))) + return y +end + +function DataDrivenSparse.active_set!(mask, ::InterfaceProximal, x, lambda) + mask .= abs.(x) .> lambda + return mask +end + +DataDrivenSparse.get_thresholds(alg::InterfaceSparseAlgorithm) = alg.thresholds + +function DataDrivenSparse.init_cache( + ::InterfaceSparseAlgorithm, A::AbstractMatrix, b::AbstractVector + ) + return InterfaceCache( + Matrix{Float64}(A), Vector{Float64}(b), zeros(1, size(A, 1)), + zeros(1, size(A, 1)), trues(1, size(A, 1)) + ) +end + +function DataDrivenSparse.step!(cache::InterfaceCache, lambda) + cache.X_prev .= cache.X + cache.X .= [2.0 0.0] + cache.active_set .= abs.(cache.X) .> lambda + return nothing +end + +@testset "Generic sparse-regression interface" begin + proximal = InterfaceProximal() + x = [0.1, 2.0] + y = similar(x) + mask = falses(2) + @test proximal(x, 1.0) == [0.0, 2.0] + @test proximal(y, [0.1, 2.0], 1.0) == [0.0, 2.0] + @test DataDrivenSparse.active_set!(mask, proximal, [0.1, 2.0], 1.0) == + BitVector([false, true]) + + algorithm = InterfaceSparseAlgorithm([0.1, 0.5]) + options = DataDrivenCommonOptions(maxiters = 1, verbose = false) + A = [1.0 2.0 3.0; 1.0 1.0 1.0] + b = [2.0, 4.0, 6.0] + + cache = DataDrivenSparse.init_cache(algorithm, A, b) + @test DataDrivenSparse.get_thresholds(algorithm) == [0.1, 0.5] + @test DataDrivenSparse.step!(cache, 0.1) === nothing + @test cache.X == [2.0 0.0] + @test cache.active_set == BitMatrix([true false]) + + solver = DataDrivenSparse.SparseLinearSolver(algorithm; options) + results = solver(A, reshape(b, 1, :)) + @test length(results) == 1 + @test first(results)[1] isa InterfaceCache +end diff --git a/lib/DataDrivenSparse/test/runtests.jl b/lib/DataDrivenSparse/test/runtests.jl index 6b5534e45..b5934bb07 100644 --- a/lib/DataDrivenSparse/test/runtests.jl +++ b/lib/DataDrivenSparse/test/runtests.jl @@ -21,6 +21,10 @@ if GROUP == "All" || GROUP == "Core" || GROUP == "DataDrivenSparse" include("./Core/sparse_linear_solve.jl") end + @safetestset "Interface" begin + include("./Core/interface.jl") + end + @safetestset "Pendulum" begin include("./Core/pendulum.jl") end From f1798c9fdb4d1148364f31014d6e3847638c2148 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 15 Aug 2026 22:57:00 -0400 Subject: [PATCH 3/4] test: use public APIs in DMD and Lux interfaces Co-Authored-By: Chris Rackauckas --- lib/DataDrivenDMD/Project.toml | 4 +++- .../test/Core/nonlinear_autonomous.jl | 1 + .../test/Core/nonlinear_forced.jl | 1 + lib/DataDrivenLux/test/Core/interface.jl | 19 +++++++++++-------- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/DataDrivenDMD/Project.toml b/lib/DataDrivenDMD/Project.toml index fc6926ac7..c7e85f5f1 100644 --- a/lib/DataDrivenDMD/Project.toml +++ b/lib/DataDrivenDMD/Project.toml @@ -30,6 +30,7 @@ StableRNGs = "1" Statistics = "1.10" StatsAPI = "1" SciMLPublic = "1" +SciMLBase = "2.155, 3" Symbolics = "7.18.1" Test = "1.10" SciMLTesting = "2.10" @@ -41,9 +42,10 @@ OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqFunctionMap = "d3585ca7-f5d3-4ba6-8057-292ed1abd90f" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "Test", "OrdinaryDiffEq", "OrdinaryDiffEqFunctionMap", "StableRNGs", "SafeTestsets", "Symbolics"] +test = ["Pkg", "Test", "OrdinaryDiffEq", "OrdinaryDiffEqFunctionMap", "SciMLBase", "StableRNGs", "SafeTestsets", "Symbolics"] diff --git a/lib/DataDrivenDMD/test/Core/nonlinear_autonomous.jl b/lib/DataDrivenDMD/test/Core/nonlinear_autonomous.jl index 1968b4341..3b9648936 100644 --- a/lib/DataDrivenDMD/test/Core/nonlinear_autonomous.jl +++ b/lib/DataDrivenDMD/test/Core/nonlinear_autonomous.jl @@ -6,6 +6,7 @@ using StatsAPI: loglikelihood, r2, rss using StableRNGs using OrdinaryDiffEq using OrdinaryDiffEqFunctionMap +using SciMLBase: DiscreteProblem using Symbolics: @variables rng = StableRNG(42) diff --git a/lib/DataDrivenDMD/test/Core/nonlinear_forced.jl b/lib/DataDrivenDMD/test/Core/nonlinear_forced.jl index 91a7b92b8..985c1ac1f 100644 --- a/lib/DataDrivenDMD/test/Core/nonlinear_forced.jl +++ b/lib/DataDrivenDMD/test/Core/nonlinear_forced.jl @@ -6,6 +6,7 @@ using StatsAPI: dof, r2 using StableRNGs using OrdinaryDiffEq using OrdinaryDiffEqFunctionMap +using SciMLBase: DiscreteProblem using Symbolics: @variables rng = StableRNG(42) diff --git a/lib/DataDrivenLux/test/Core/interface.jl b/lib/DataDrivenLux/test/Core/interface.jl index 3b9685dd3..e39713154 100644 --- a/lib/DataDrivenLux/test/Core/interface.jl +++ b/lib/DataDrivenLux/test/Core/interface.jl @@ -1,7 +1,9 @@ using DataDrivenDiffEq using DataDrivenLux using IntervalArithmetic: interval -using ModelingToolkit: @variables +using StableRNGs +using StatsAPI: rss +using Symbolics: @variables using Test struct InterfaceAlgorithm <: DataDrivenLux.AbstractDAGSRAlgorithm @@ -25,17 +27,18 @@ problem = DirectDataDrivenProblem( ) dataset = DataDrivenLux.Dataset(problem) intervals = [interval(-10.0, 10.0)] -algorithm = InterfaceAlgorithm(DataDrivenLux.CommonAlgOptions()) +algorithm = InterfaceAlgorithm( + DataDrivenLux.CommonAlgOptions(; + populationsize = 1, functions = (identity,), arities = (1,), + keep = 1, loss = rss, rng = StableRNG(100) + ) +) @testset "Generic DAG symbolic-regression interface" begin model = DataDrivenLux.init_model(algorithm, basis, dataset, intervals) @test model isa DataDrivenLux.LayeredDAG - cache = DataDrivenLux.SearchCache{ - InterfaceAlgorithm, DataDrivenLux.__PROCESSUSE(1), Nothing, - }( - algorithm, DataDrivenLux.Candidate[], Int[], Bool[], Int[], Float32[], - dataset, nothing - ) + cache = DataDrivenLux.init_cache(algorithm, basis, problem) + @test cache isa DataDrivenLux.SearchCache{<:InterfaceAlgorithm} @test DataDrivenLux.update_parameters!(cache) === nothing end From 2467ebb333181e40670740c4f9818d0711e1e2a3 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 16 Aug 2026 04:28:05 -0400 Subject: [PATCH 4/4] docs: fix SciMLStyle section heading Co-Authored-By: Chris Rackauckas --- lib/DataDrivenSparse/src/algorithms/proximals.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DataDrivenSparse/src/algorithms/proximals.jl b/lib/DataDrivenSparse/src/algorithms/proximals.jl index 468c0a367..baf3e1cdb 100644 --- a/lib/DataDrivenSparse/src/algorithms/proximals.jl +++ b/lib/DataDrivenSparse/src/algorithms/proximals.jl @@ -141,7 +141,7 @@ Where `ρ = 5λ` per default. Return the modified array. The three-argument form writes the result into its first array argument. -#Fields +# Fields $(FIELDS) # Example