diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 06f9cab7e1..c0723471bd 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -127,7 +127,6 @@ TODO abstract type AbstractSystem end abstract type AbstractTimeDependentSystem <: AbstractSystem end abstract type AbstractTimeIndependentSystem <: AbstractSystem end -abstract type AbstractODESystem <: AbstractTimeDependentSystem end abstract type AbstractMultivariateSystem <: AbstractSystem end abstract type AbstractOptimizationSystem <: AbstractTimeIndependentSystem end abstract type AbstractDiscreteSystem <: AbstractTimeDependentSystem end @@ -162,14 +161,9 @@ include("systems/codegen_utils.jl") include("systems/problem_utils.jl") include("linearization.jl") -include("systems/optimization/constraints_system.jl") -include("systems/optimization/optimizationsystem.jl") include("systems/optimization/modelingtoolkitize.jl") -include("systems/nonlinear/nonlinearsystem.jl") include("systems/nonlinear/homotopy_continuation.jl") -include("systems/diffeqs/odesystem.jl") -include("systems/diffeqs/sdesystem.jl") include("systems/diffeqs/abstractodesystem.jl") include("systems/nonlinear/modelingtoolkitize.jl") include("systems/nonlinear/initializesystem.jl") @@ -177,11 +171,6 @@ include("systems/diffeqs/first_order_transform.jl") include("systems/diffeqs/modelingtoolkitize.jl") include("systems/diffeqs/basic_transformations.jl") -include("systems/discrete_system/discrete_system.jl") -include("systems/discrete_system/implicit_discrete_system.jl") - -include("systems/jumps/jumpsystem.jl") - include("systems/pde/pdesystem.jl") include("systems/sparsematrixclil.jl") @@ -225,7 +214,7 @@ const D = Differential(t) PrecompileTools.@compile_workload begin using ModelingToolkit @variables x(ModelingToolkit.t_nounits) - @named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x], ModelingToolkit.t_nounits) + @named sys = System([ModelingToolkit.D_nounits(x) ~ -x], ModelingToolkit.t_nounits) prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [], jac = true) @mtkmodel __testmod__ begin @constants begin @@ -261,16 +250,14 @@ export AbstractTimeDependentSystem, AbstractTimeIndependentSystem, AbstractMultivariateSystem -export ODESystem, - ODEFunction, ODEFunctionExpr, ODEProblemExpr, convert_system, +export ODEFunction, ODEFunctionExpr, ODEProblemExpr, convert_system, add_accumulations, System export DAEFunctionExpr, DAEProblemExpr -export SDESystem, SDEFunction, SDEFunctionExpr, SDEProblemExpr +export SDEFunction, SDEFunctionExpr, SDEProblemExpr export SystemStructure -export DiscreteSystem, DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr -export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, +export DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr +export ImplicitDiscreteProblem, ImplicitDiscreteFunction, ImplicitDiscreteFunctionExpr -export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr export NonlinearProblem, NonlinearProblemExpr @@ -279,7 +266,6 @@ export IntervalNonlinearProblem, IntervalNonlinearProblemExpr export OptimizationProblem, OptimizationProblemExpr, constraints export SteadyStateProblem, SteadyStateProblemExpr export JumpProblem -export NonlinearSystem, OptimizationSystem, ConstraintsSystem export alias_elimination, flatten export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream, instream diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 28b60b13dc..2a2a47fa63 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -161,7 +161,7 @@ has_var(ex, x) = x ∈ Set(get_variables(ex)) """ (f_oop, f_ip), x_sym, p_sym, io_sys = generate_control_function( - sys::AbstractODESystem, + sys::System, inputs = unbound_inputs(sys), disturbance_inputs = nothing; implicit_dae = false, @@ -193,7 +193,7 @@ t = 0 f[1](x, inputs, p, t) ``` """ -function generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys), +function generate_control_function(sys::AbstractSystem, inputs = unbound_inputs(sys), disturbance_inputs = disturbances(sys); disturbance_argument = false, implicit_dae = false, @@ -344,7 +344,7 @@ The structure represents a model of a disturbance, along with the input variable # Fields: - `input`: The variable affected by the disturbance. - - `model::M`: A model of the disturbance. This is typically an `ODESystem`, but type that implements [`ModelingToolkit.get_disturbance_system`](@ref)`(dist::DisturbanceModel) -> ::ODESystem` is supported. + - `model::M`: A model of the disturbance. This is typically a `System`, but type that implements [`ModelingToolkit.get_disturbance_system`](@ref)`(dist::DisturbanceModel) -> ::System` is supported. """ struct DisturbanceModel{M} input::Any @@ -354,7 +354,7 @@ end DisturbanceModel(input, model; name) = DisturbanceModel(input, model, name) # Point of overloading for libraries, e.g., to be able to support disturbance models from ControlSystemsBase -function get_disturbance_system(dist::DisturbanceModel{<:ODESystem}) +function get_disturbance_system(dist::DisturbanceModel{System}) dist.model end @@ -395,7 +395,7 @@ c = 10 # Damping coefficient eqs = [connect(torque.flange, inertia1.flange_a) connect(inertia1.flange_b, spring.flange_a, damper.flange_a) connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] -model = ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], +model = System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name = :model) model = complete(model) model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] @@ -427,7 +427,7 @@ function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing; kw eqs = [dsys.input.u[1] ~ d dist.input ~ u + dsys.output.u[1]] - augmented_sys = ODESystem(eqs, t, systems = [dsys], name = gensym(:outer)) + augmented_sys = System(eqs, t, systems = [dsys], name = gensym(:outer)) augmented_sys = extend(augmented_sys, sys) (f_oop, f_ip), dvs, p, io_sys = generate_control_function(augmented_sys, all_inputs, diff --git a/src/linearization.jl b/src/linearization.jl index 77f4422b63..6669ba558b 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -19,7 +19,7 @@ The `simplified_sys` has undergone [`structural_simplify`](@ref) and had any occ # Arguments: - - `sys`: An [`ODESystem`](@ref). This function will automatically apply simplification passes on `sys` and return the resulting `simplified_sys`. + - `sys`: A [`System`](@ref) of ODEs. This function will automatically apply simplification passes on `sys` and return the resulting `simplified_sys`. - `inputs`: A vector of variables that indicate the inputs of the linearized input-output model. - `outputs`: A vector of variables that indicate the outputs of the linearized input-output model. - `simplify`: Apply simplification in tearing. @@ -640,7 +640,7 @@ function plant(; name) @variables u(t)=0 y(t)=0 eqs = [D(x) ~ -x + u y ~ x] - ODESystem(eqs, t; name = name) + System(eqs, t; name = name) end function ref_filt(; name) @@ -648,7 +648,7 @@ function ref_filt(; name) @variables u(t)=0 [input = true] eqs = [D(x) ~ -2 * x + u y ~ x] - ODESystem(eqs, t, name = name) + System(eqs, t, name = name) end function controller(kp; name) @@ -657,7 +657,7 @@ function controller(kp; name) eqs = [ u ~ kp * (r - y), ] - ODESystem(eqs, t; name = name) + System(eqs, t; name = name) end @named f = ref_filt() @@ -668,7 +668,7 @@ connections = [f.y ~ c.r # filtered reference to controller reference c.u ~ p.u # controller output to plant input p.y ~ c.y] -@named cl = ODESystem(connections, t, systems = [f, c, p]) +@named cl = System(connections, t, systems = [f, c, p]) lsys0, ssys = linearize(cl, [f.u], [p.x]) desired_order = [f.x, p.x] diff --git a/src/problems/bvproblem.jl b/src/problems/bvproblem.jl new file mode 100644 index 0000000000..5a78101b23 --- /dev/null +++ b/src/problems/bvproblem.jl @@ -0,0 +1,90 @@ +""" +```julia +SciMLBase.BVProblem{iip}(sys::AbstractSystem, u0map, tspan, + parammap = DiffEqBase.NullParameters(); + constraints = nothing, guesses = nothing, + version = nothing, tgrad = false, + jac = true, sparse = true, + simplify = false, + kwargs...) where {iip} +``` + +Create a boundary value problem from the [`System`](@ref). + +`u0map` is used to specify fixed initial values for the states. Every variable +must have either an initial guess supplied using `guesses` or a fixed initial +value specified using `u0map`. + +Boundary value conditions are supplied to Systems in the form of a list of constraints. +These equations should specify values that state variables should take at specific points, +as in `x(0.5) ~ 1`). More general constraints that should hold over the entire solution, +such as `x(t)^2 + y(t)^2`, should be specified as one of the equations used to build the +`System`. + +If a `System` without `constraints` is specified, it will be treated as an initial value problem. + +```julia + @parameters g t_c = 0.5 + @variables x(..) y(t) λ(t) + eqs = [D(D(x(t))) ~ λ * x(t) + D(D(y)) ~ λ * y - g + x(t)^2 + y^2 ~ 1] + cstr = [x(0.5) ~ 1] + @mtkbuild pend = System(eqs, t; constraints = cstrs) + + tspan = (0.0, 1.5) + u0map = [x(t) => 0.6, y => 0.8] + parammap = [g => 1] + guesses = [λ => 1] + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) +``` + +If the `System` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting +`BVProblem` must be solved using BVDAE solvers, such as Ascher. +""" +@fallback_iip_specialize function SciMLBase.BVProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + check_compatibility = true, cse = true, checkbounds = false, eval_expression = false, + eval_module = @__MODULE__, guesses = Dict(), callback = nothing, kwargs...) where { + iip, spec} + check_complete(sys, BVProblem) + check_compatibility && check_compatible_system(BVProblem, sys) + isnothing(callback) || error("BVP solvers do not support callbacks.") + + # Systems without algebraic equations should use both fixed values + guesses + # for initialization. + _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) + fode, u0, p = process_SciMLProblem( + ODEFunction{iip, spec}, sys, _u0map, parammap; guesses, + t = tspan !== nothing ? tspan[1] : tspan, check_compatibility = false, cse, checkbounds, + time_dependent_init = false, kwargs...) + + dvs = unknowns(sys) + stidxmap = Dict([v => i for (i, v) in enumerate(dvs)]) + u0_idxs = has_alg_eqs(sys) ? collect(1:length(dvs)) : [stidxmap[k] for (k, v) in u0map] + fbc = generate_boundary_conditions( + sys, u0, u0_idxs, tspan; expression = Val{false}, cse, checkbounds) + + if (length(constraints(sys)) + length(u0map) > length(dvs)) + @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." + end + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(BVProblem{iip}(fode, fbc, u0, tspan[1], p; kwargs...)) +end + +function check_compatible_system(T::Type{BVProblem}, sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_has_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_continuous(sys, T) + + if !isempty(discrete_events(sys)) || !isempty(continuous_events(sys)) + throw(SystemCompatibilityError("BVP solvers do not support events.")) + end +end diff --git a/src/problems/compatibility.jl b/src/problems/compatibility.jl new file mode 100644 index 0000000000..60a7e9adce --- /dev/null +++ b/src/problems/compatibility.jl @@ -0,0 +1,166 @@ +""" + function check_compatible_system(T::Type, sys::System) + +Check if `sys` can be used to construct a problem/function of type `T`. +""" +function check_compatible_system end + +struct SystemCompatibilityError <: Exception + msg::String +end + +function Base.showerror(io::IO, err::SystemCompatibilityError) + println(io, err.msg) + println(io) + print(io, "To disable this check, pass `check_compatibility = false`.") +end + +function check_time_dependent(sys::System, T) + if !is_time_dependent(sys) + throw(SystemCompatibilityError(""" + `$T` requires a time-dependent system. + """)) + end +end + +function check_time_independent(sys::System, T) + if is_time_dependent(sys) + throw(SystemCompatibilityError(""" + `$T` requires a time-independent system. + """)) + end +end + +function check_is_dde(sys::System) + altT = get_noise_eqs(sys) === nothing ? ODEProblem : SDEProblem + if !is_dde(sys) + throw(SystemCompatibilityError(""" + The system does not have delays. Consider an `$altT` instead. + """)) + end +end + +function check_not_dde(sys::System) + altT = get_noise_eqs(sys) === nothing ? DDEProblem : SDDEProblem + if is_dde(sys) + throw(SystemCompatibilityError(""" + The system has delays. Consider a `$altT` instead. + """)) + end +end + +function check_no_cost(sys::System, T) + cost = ModelingToolkit.cost(sys) + if !_iszero(cost) + throw(SystemCompatibilityError(""" + `$T` will not optimize solutions of systems that have associated cost \ + functions. Solvers for optimal control problems are forthcoming. In order to \ + bypass this error (e.g. to check the cost of a regular solution), pass \ + `allow_cost = true` into the constructor. + """)) + end +end + +function check_has_cost(sys::System, T) + cost = ModelingToolkit.cost(sys) + if _iszero(cost) + throw(SystemCompatibilityError(""" + A system without cost cannot be used to construct a `$T`. + """)) + end +end + +function check_no_constraints(sys::System, T) + if !isempty(constraints(sys)) + throw(SystemCompatibilityError(""" + A system with constraints cannot be used to construct a `$T`. + """)) + end +end + +function check_has_constraints(sys::System, T) + if isempty(constraints(sys)) + throw(SystemCompatibilityError(""" + A system without constraints cannot be used to construct a `$T`. Consider an \ + `ODEProblem` instead. + """)) + end +end + +function check_no_jumps(sys::System, T) + if !isempty(jumps(sys)) + throw(SystemCompatibilityError(""" + A system with jumps cannot be used to construct a `$T`. Consider a \ + `JumpProblem` instead. + """)) + end +end + +function check_has_jumps(sys::System, T) + if isempty(jumps(sys)) + throw(SystemCompatibilityError("`$T` requires a system with jumps.")) + end +end + +function check_no_noise(sys::System, T) + altT = is_dde(sys) ? SDDEProblem : SDEProblem + if get_noise_eqs(sys) !== nothing + throw(SystemCompatibilityError(""" + A system with noise cannot be used to construct a `$T`. Consider an \ + `$altT` instead. + """)) + end +end + +function check_has_noise(sys::System, T) + altT = is_dde(sys) ? DDEProblem : ODEProblem + if get_noise_eqs(sys) === nothing + throw(SystemCompatibilityError(""" + A system without noise cannot be used to construct a `$T`. Consider an \ + `$altT` instead. + """)) + end +end + +function check_is_discrete(sys::System, T) + if !is_discrete_system(sys) + throw(SystemCompatibilityError(""" + `$T` expects a discrete system. Consider an `ODEProblem` instead. If your system \ + is discrete, ensure `structural_simplify` has been run on it. + """)) + end +end + +function check_is_continuous(sys::System, T) + altT = has_alg_equations(sys) ? ImplicitDiscreteProblem : DiscreteProblem + if is_discrete_system(sys) + throw(SystemCompatibilityError(""" + A discrete system cannot be used to construct a `$T`. Consider a `$altT` instead. + """)) + end +end + +function check_is_explicit(sys::System, T, altT) + if has_alg_equations(sys) + throw(SystemCompatibilityError(""" + `$T` expects an explicit system. Consider a `$altT` instead. + """)) + end +end + +function check_is_implicit(sys::System, T, altT) + if !has_alg_equations(sys) + throw(SystemCompatibilityError(""" + `$T` expects an implicit system. Consider a `$altT` instead. + """)) + end +end + +function check_no_equations(sys::System, T) + if !isempty(equations(sys)) + throw(SystemCompatibilityError(""" + A system with equations cannot be used to construct a `$T`. Consider turning the + equations into constraints instead. + """)) + end +end diff --git a/src/problems/daeproblem.jl b/src/problems/daeproblem.jl new file mode 100644 index 0000000000..946aba538e --- /dev/null +++ b/src/problems/daeproblem.jl @@ -0,0 +1,76 @@ +@fallback_iip_specialize function SciMLBase.DAEFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; tgrad = false, jac = false, + t = nothing, eval_expression = false, eval_module = @__MODULE__, sparse = false, + steady_state = false, checkbounds = false, sparsity = false, analytic = nothing, + simplify = false, cse = true, initialization_data = nothing, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, DAEFunction) + check_compatibility && check_compatible_system(DAEFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, implicit_dae = true, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + if jac + _jac = generate_dae_jacobian(sys, dvs, ps; expression = Val{false}, + simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _jac = nothing + end + + observedfun = ObservedFunctionCache( + sys; steady_state, eval_expression, eval_module, checkbounds, cse) + + jac_prototype = if sparse + uElType = u0 === nothing ? Float64 : eltype(u0) + if jac + J1 = calculate_jacobian(sys, sparse = sparse) + derivatives = Differential(get_iv(sys)).(unknowns(sys)) + J2 = calculate_jacobian(sys; sparse = sparse, dvs = derivatives) + similar(J1 + J2, uElType) + else + similar(jacobian_dae_sparsity(sys), uElType) + end + else + nothing + end + + DAEFunction{iip, spec}(f; + sys = sys, + jac = _jac, + jac_prototype = jac_prototype, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.DAEProblem{iip, spec}( + sys::System, du0map, u0map, tspan, parammap = SciMLBase.NullParameters(); + callback = nothing, check_length = true, eval_expression = false, + eval_module = @__MODULE__, check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, DAEProblem) + check_compatibility && check_compatible_system(DAEProblem, sys) + + f, du0, u0, p = process_SciMLProblem(DAEFunction{iip, spec}, sys, u0map, parammap; + du0map, t = tspan !== nothing ? tspan[1] : tspan, check_length, eval_expression, + eval_module, check_compatibility, implicit_dae = true, kwargs...) + + kwargs = process_kwargs(sys; callback, eval_expression, eval_module, kwargs...) + + diffvars = collect_differential_variables(sys) + sts = unknowns(sys) + differential_vars = map(Base.Fix2(in, diffvars), sts) + + # Call `remake` so it runs initialization if it is trivial + return remake(DAEProblem{iip}( + f, du0, u0, tspan, p; kwargs...)) +end diff --git a/src/problems/ddeproblem.jl b/src/problems/ddeproblem.jl new file mode 100644 index 0000000000..5aea37e4d5 --- /dev/null +++ b/src/problems/ddeproblem.jl @@ -0,0 +1,73 @@ +@fallback_iip_specialize function SciMLBase.DDEFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; + eval_expression = false, eval_module = @__MODULE__, checkbounds = false, + initialization_data = nothing, cse = true, check_compatibility = true, + sparse = false, simplify = false, analytic = nothing, kwargs...) where { + iip, spec} + check_complete(sys, DDEFunction) + check_compatibility && check_compatible_system(DDEFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on DDEFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + M = calculate_massmatrix(sys) + _M = concrete_massmatrix(M; sparse, u0) + + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds, cse) + + DDEFunction{iip, spec}(f; + sys = sys, + mass_matrix = _M, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.DDEProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + callback = nothing, check_length = true, cse = true, checkbounds = false, + eval_expression = false, eval_module = @__MODULE__, check_compatibility = true, + u0_constructor = identity, + kwargs...) where {iip, spec} + check_complete(sys, DDEProblem) + check_compatibility && check_compatible_system(DDEProblem, sys) + + f, u0, p = process_SciMLProblem(DDEFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_length, cse, checkbounds, + eval_expression, eval_module, check_compatibility, symbolic_u0 = true, kwargs...) + + h = generate_history( + sys, u0; expression = Val{false}, cse, eval_expression, eval_module, + checkbounds) + u0 = float.(h(p, tspan[1])) + if u0 !== nothing + u0 = u0_constructor(u0) + end + + kwargs = process_kwargs(sys; callback, eval_expression, eval_module, kwargs...) + + # Call `remake` so it runs initialization if it is trivial + return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs...)) +end + +function check_compatible_system(T::Union{Type{DDEFunction}, Type{DDEProblem}}, sys::System) + check_time_dependent(sys, T) + check_is_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_continuous(sys, T) +end diff --git a/src/problems/discreteproblem.jl b/src/problems/discreteproblem.jl new file mode 100644 index 0000000000..8f8f3ca3d1 --- /dev/null +++ b/src/problems/discreteproblem.jl @@ -0,0 +1,89 @@ +@fallback_iip_specialize function SciMLBase.DiscreteFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; + t = nothing, eval_expression = false, eval_module = @__MODULE__, + checkbounds = false, analytic = nothing, simplify = false, cse = true, + initialization_data = nothing, check_compatibility = true, kwargs...) where { + iip, spec} + check_complete(sys, DiscreteFunction) + check_compatibility && check_compatible_system(DiscreteFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on DiscreteFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + observedfun = ObservedFunctionCache( + sys; steady_state = false, eval_expression, eval_module, checkbounds, cse) + + DiscreteFunction{iip, spec}(f; + sys = sys, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.DiscreteProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, DiscreteProblem) + check_compatibility && check_compatible_system(DiscreteProblem, sys) + + dvs = unknowns(sys) + u0map = to_varmap(u0map, dvs) + u0map = shift_u0map_forward(sys, u0map, defaults(sys)) + f, u0, p = process_SciMLProblem(DiscreteFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_compatibility, kwargs...) + u0 = f(u0, p, tspan[1]) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(DiscreteProblem{iip}(f, u0, tspan, p; kwargs...)) +end + +function check_compatible_system( + T::Union{Type{DiscreteFunction}, Type{DiscreteProblem}}, sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_discrete(sys, T) + check_is_explicit(sys, T, ImplicitDiscreteProblem) +end + +function shift_u0map_forward(sys::System, u0map, defs) + iv = get_iv(sys) + updated = AnyDict() + for k in collect(keys(u0map)) + v = u0map[k] + if !((op = operation(k)) isa Shift) + isnothing(getunshifted(k)) && + error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(k)).") + + updated[Shift(iv, 1)(k)] = v + elseif op.steps > 0 + error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(only(arguments(k)))).") + else + updated[Shift(iv, op.steps + 1)(only(arguments(k)))] = v + end + end + for var in unknowns(sys) + op = operation(var) + root = getunshifted(var) + shift = getshift(var) + isnothing(root) && continue + (haskey(updated, Shift(iv, shift)(root)) || haskey(updated, var)) && continue + haskey(defs, root) || error("Initial condition for $var not provided.") + updated[var] = defs[root] + end + return updated +end diff --git a/src/problems/implicitdiscreteproblem.jl b/src/problems/implicitdiscreteproblem.jl new file mode 100644 index 0000000000..06316a28cc --- /dev/null +++ b/src/problems/implicitdiscreteproblem.jl @@ -0,0 +1,60 @@ +@fallback_iip_specialize function SciMLBase.ImplicitDiscreteFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; + t = nothing, eval_expression = false, eval_module = @__MODULE__, + checkbounds = false, analytic = nothing, simplify = false, cse = true, + initialization_data = nothing, check_compatibility = true, kwargs...) where { + iip, spec} + check_complete(sys, ImplicitDiscreteFunction) + check_compatibility && check_compatible_system(ImplicitDiscreteFunction, sys) + + iv = get_iv(sys) + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, implicit_dae = true, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ImplicitDiscreteFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, u0, p, t)) + end + + observedfun = ObservedFunctionCache( + sys; steady_state = false, eval_expression, eval_module, checkbounds, cse) + + ImplicitDiscreteFunction{iip, spec}(f; + sys = sys, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.ImplicitDiscreteProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, ImplicitDiscreteProblem) + check_compatibility && check_compatible_system(ImplicitDiscreteProblem, sys) + + dvs = unknowns(sys) + f, u0, p = process_SciMLProblem( + ImplicitDiscreteFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_compatibility, kwargs...) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(ImplicitDiscreteProblem{iip}(f, u0, tspan, p; kwargs...)) +end + +function check_compatible_system( + T::Union{Type{ImplicitDiscreteFunction}, Type{ImplicitDiscreteProblem}}, sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_discrete(sys, T) + check_is_implicit(sys, T, DiscreteProblem) +end diff --git a/src/problems/initializationproblem.jl b/src/problems/initializationproblem.jl new file mode 100644 index 0000000000..643340480f --- /dev/null +++ b/src/problems/initializationproblem.jl @@ -0,0 +1,161 @@ +struct InitializationProblem{iip, specialization} end + +""" +```julia +InitializationProblem{iip}(sys::AbstractSystem, t, u0map, + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = false, + checkbounds = false, sparse = false, + simplify = false, + linenumbers = true, parallel = SerialForm(), + initialization_eqs = [], + fully_determined = false, + kwargs...) where {iip} +``` + +Generates a NonlinearProblem or NonlinearLeastSquaresProblem from a System +which represents the initialization, i.e. the calculation of the consistent +initial conditions for the given DAE. +""" +@fallback_iip_specialize function InitializationProblem{iip, specialize}( + sys::AbstractSystem, + t, u0map = [], + parammap = DiffEqBase.NullParameters(); + guesses = [], + check_length = true, + warn_initialize_determined = true, + initialization_eqs = [], + fully_determined = nothing, + check_units = true, + use_scc = true, + allow_incomplete = false, + force_time_independent = false, + algebraic_only = false, + time_dependent_init = is_time_dependent(sys), + kwargs...) where {iip, specialize} + if !iscomplete(sys) + error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") + end + if isempty(u0map) && get_initializesystem(sys) !== nothing + isys = get_initializesystem(sys; initialization_eqs, check_units) + simplify_system = false + elseif isempty(u0map) && get_initializesystem(sys) === nothing + isys = generate_initializesystem( + sys; initialization_eqs, check_units, pmap = parammap, + guesses, extra_metadata = (; use_scc), algebraic_only) + simplify_system = true + else + isys = generate_initializesystem( + sys; u0map, initialization_eqs, check_units, time_dependent_init, + pmap = parammap, guesses, extra_metadata = (; use_scc), algebraic_only) + simplify_system = true + end + + # useful for `SteadyStateProblem` since `f` has to be autonomous and the + # initialization should be too + if force_time_independent + idx = findfirst(isequal(get_iv(sys)), get_ps(isys)) + idx === nothing || deleteat!(get_ps(isys), idx) + end + + if simplify_system + isys = structural_simplify(isys; fully_determined) + end + + meta = get_metadata(isys) + if meta isa InitializationSystemMetadata + @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob( + sys, isys) + end + + ts = get_tearing_state(isys) + unassigned_vars = StructuralTransformations.singular_check(ts) + if warn_initialize_determined && !isempty(unassigned_vars) + errmsg = """ + The initialization system is structurally singular. Guess values may \ + significantly affect the initial values of the ODE. The problematic variables \ + are $unassigned_vars. + + Note that the identification of problematic variables is a best-effort heuristic. + """ + @warn errmsg + end + + uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) + + # TODO: throw on uninitialized arrays + filter!(x -> !(x isa Symbolics.Arr), uninit) + if time_dependent_init && !isempty(uninit) + allow_incomplete || throw(IncompleteInitializationError(uninit)) + # for incomplete initialization, we will add the missing variables as parameters. + # they will be updated by `update_initializeprob!` and `initializeprobmap` will + # use them to construct the new `u0`. + newparams = map(toparam, uninit) + append!(get_ps(isys), newparams) + isys = complete(isys) + end + + neqs = length(equations(isys)) + nunknown = length(unknowns(isys)) + + if use_scc + scc_message = "`SCCNonlinearProblem` can only be used for initialization of fully determined systems and hence will not be used here. " + else + scc_message = "" + end + + if warn_initialize_determined && neqs > nunknown + @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + end + if warn_initialize_determined && neqs < nunknown + @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + end + + parammap = recursive_unwrap(anydict(parammap)) + if t !== nothing + parammap[get_iv(sys)] = t + end + filter!(kvp -> kvp[2] !== missing, parammap) + + u0map = to_varmap(u0map, unknowns(sys)) + if isempty(guesses) + guesses = Dict() + end + + filter_missing_values!(u0map) + filter_missing_values!(parammap) + u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), u0map) + + TProb = if neqs == nunknown && isempty(unassigned_vars) + if use_scc && neqs > 0 + if is_split(isys) + SCCNonlinearProblem + else + @warn "`SCCNonlinearProblem` can only be used with `split = true` systems. Simplify your `System` with `split = true` or pass `use_scc = false` to disable this warning" + NonlinearProblem + end + else + NonlinearProblem + end + else + NonlinearLeastSquaresProblem + end + TProb(isys, u0map, parammap; kwargs..., + build_initializeprob = false, is_initializeprob = true) +end + +const INCOMPLETE_INITIALIZATION_MESSAGE = """ + Initialization incomplete. Not all of the state variables of the + DAE system can be determined by the initialization. Missing + variables: + """ + +struct IncompleteInitializationError <: Exception + uninit::Any +end + +function Base.showerror(io::IO, e::IncompleteInitializationError) + println(io, INCOMPLETE_INITIALIZATION_MESSAGE) + println(io, e.uninit) +end diff --git a/src/problems/intervalnonlinearproblem.jl b/src/problems/intervalnonlinearproblem.jl new file mode 100644 index 0000000000..18bcf80697 --- /dev/null +++ b/src/problems/intervalnonlinearproblem.jl @@ -0,0 +1,59 @@ +function SciMLBase.IntervalNonlinearFunction( + sys::System, _d = nothing, u0 = nothing, p = nothing; + eval_expression = false, eval_module = @__MODULE__, + checkbounds = false, analytic = nothing, + cse = true, initialization_data = nothing, + check_compatibility = true, kwargs...) + check_complete(sys, IntervalNonlinearFunction) + check_compatibility && check_compatible_system(IntervalNonlinearFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, scalar = true, + eval_expression, eval_module, checkbounds, cse, + kwargs...) + + observedfun = ObservedFunctionCache( + sys; steady_state = false, eval_expression, eval_module, checkbounds, cse) + + IntervalNonlinearFunction{false}(f; + sys = sys, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +function SciMLBase.IntervalNonlinearProblem( + sys::System, uspan::NTuple{2}, parammap = SciMLBase.NullParameters(); + check_compatibility = true, kwargs...) + check_complete(sys, IntervalNonlinearProblem) + check_compatibility && check_compatible_system(IntervalNonlinearProblem, sys) + + u0map = unknowns(sys) .=> uspan[1] + f, u0, p = process_SciMLProblem(IntervalNonlinearFunction, sys, u0map, parammap; + check_compatibility, kwargs...) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(IntervalNonlinearProblem(f, uspan, p; kwargs...)) +end + +function check_compatible_system( + T::Union{Type{IntervalNonlinearFunction}, Type{IntervalNonlinearProblem}}, sys::System) + check_time_independent(sys, T) + if !isone(length(unknowns(sys))) + throw(SystemCompatibilityError(""" + `$T` requires a system with a single unknown. Found `$(unknowns(sys))`. + """)) + end + if !isone(length(equations(sys))) + throw(SystemCompatibilityError(""" + `$T` requires a system with a single equation. Found `$(equations(sys))`. + """)) + end + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) +end diff --git a/src/problems/jumpproblem.jl b/src/problems/jumpproblem.jl new file mode 100644 index 0000000000..3330f30ec8 --- /dev/null +++ b/src/problems/jumpproblem.jl @@ -0,0 +1,219 @@ +@fallback_iip_specialize function JumpProcesses.JumpProblem{iip, spec}( + sys::System, u0map, tspan::Union{Tuple, Nothing}, pmap = SciMLBase.NullParameters(); + check_compatibility = true, eval_expression = false, eval_module = @__MODULE__, + checkbounds = false, cse = true, aggregator = JumpProcesses.NullAggregator(), + callback = nothing, kwargs...) where {iip, spec} + check_complete(sys, JumpProblem) + check_compatibility && check_compatible_system(JumpProblem, sys) + + has_vrjs = any(x -> x isa VariableRateJump, jumps(sys)) + has_eqs = !isempty(equations(sys)) + has_noise = get_noise_eqs(sys) !== nothing + + if (has_vrjs || has_eqs) + if has_eqs && has_noise + prob = SDEProblem{iip, spec}( + sys, u0map, tspan, pmap; check_compatibility = false, + build_initializeprob = false, checkbounds, cse, kwargs...) + elseif has_eqs + prob = ODEProblem{iip, spec}( + sys, u0map, tspan, pmap; check_compatibility = false, + build_initializeprob = false, checkbounds, cse, kwargs...) + else + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, pmap; + t = tspan === nothing ? nothing : tspan[1], tofloat = false, + check_length = false, build_initializeprob = false) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, + checkbounds, cse) + f = (du, u, p, t) -> (du .= 0; nothing) + df = ODEFunction{true, spec}(f; sys, observed = observedfun) + prob = ODEProblem{true}(df, u0, tspan, p; kwargs...) + end + else + _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false, build_initializeprob = false, cse) + f = DiffEqBase.DISCRETE_INPLACE_DEFAULT + + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds, cse) + + df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun, + initialization_data = get(_f.kwargs, :initialization_data, nothing)) + prob = DiscreteProblem(df, u0, tspan, p; kwargs...) + end + + dvs = unknowns(sys) + unknowntoid = Dict(value(unknown) => i for (i, unknown) in enumerate(dvs)) + js = jumps(sys) + invttype = prob.tspan[1] === nothing ? Float64 : typeof(1 / prob.tspan[2]) + + # handling parameter substitution and empty param vecs + p = (prob.p isa DiffEqBase.NullParameters || prob.p === nothing) ? Num[] : prob.p + + majpmapper = JumpSysMajParamMapper(sys, p; jseqs = js, rateconsttype = invttype) + _majs = filter(x -> x isa MassActionJump, js) + _crjs = filter(x -> x isa ConstantRateJump, js) + vrjs = filter(x -> x isa VariableRateJump, js) + majs = isempty(_majs) ? nothing : assemble_maj(_majs, unknowntoid, majpmapper) + crjs = ConstantRateJump[assemble_crj(sys, j, unknowntoid; eval_expression, eval_module) + for j in _crjs] + vrjs = VariableRateJump[assemble_vrj(sys, j, unknowntoid; eval_expression, eval_module) + for j in vrjs] + jset = JumpSet(Tuple(vrjs), Tuple(crjs), nothing, majs) + + # dep graphs are only for constant rate jumps + nonvrjs = ArrayPartition(_majs, _crjs) + if needs_vartojumps_map(aggregator) || needs_depgraph(aggregator) || + (aggregator isa JumpProcesses.NullAggregator) + jdeps = asgraph(sys; eqs = nonvrjs) + vdeps = variable_dependencies(sys; eqs = nonvrjs) + vtoj = jdeps.badjlist + jtov = vdeps.badjlist + jtoj = needs_depgraph(aggregator) ? eqeq_dependencies(jdeps, vdeps).fadjlist : + nothing + else + vtoj = nothing + jtov = nothing + jtoj = nothing + end + + # handle events, making sure to reset aggregators in the generated affect functions + cbs = process_events(sys; callback, eval_expression, eval_module, + postprocess_affect_expr! = _reset_aggregator!) + + return JumpProblem(prob, aggregator, jset; dep_graph = jtoj, vartojumps_map = vtoj, + jumptovars_map = jtov, scale_rates = false, nocopy = true, + callback = cbs, kwargs...) +end + +function check_compatible_system(T::Union{Type{JumpProblem}}, sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_has_jumps(sys, T) + check_is_continuous(sys, T) +end + +###################### parameter mapper ########################### +struct JumpSysMajParamMapper{U, V, W} + paramexprs::U # the parameter expressions to use for each jump rate constant + sympars::V # parameters(sys) from the underlying JumpSystem + subdict::Any # mapping from an element of parameters(sys) to its current numerical value +end + +function JumpSysMajParamMapper(js::System, p; jseqs = nothing, rateconsttype = Float64) + eqs = (jseqs === nothing) ? jumps(js) : jseqs + majs = MassActionJump[x for x in eqs if x isa MassActionJump] + paramexprs = [maj.scaled_rates for maj in majs] + psyms = reduce(vcat, reorder_parameters(js); init = []) + paramdict = Dict(value(k) => value(v) for (k, v) in zip(psyms, vcat(p...))) + JumpSysMajParamMapper{typeof(paramexprs), typeof(psyms), rateconsttype}(paramexprs, + psyms, + paramdict) +end + +function updateparams!(ratemap::JumpSysMajParamMapper{U, V, W}, + params) where {U <: AbstractArray, V <: AbstractArray, W} + for (i, p) in enumerate(params) + sympar = ratemap.sympars[i] + ratemap.subdict[sympar] = p + end + nothing +end + +function updateparams!(ratemap::JumpSysMajParamMapper{U, V, W}, + params::MTKParameters) where {U <: AbstractArray, V <: AbstractArray, W} + for (i, p) in enumerate(ArrayPartition(params...)) + sympar = ratemap.sympars[i] + ratemap.subdict[sympar] = p + end + nothing +end + +function updateparams!(::JumpSysMajParamMapper{U, V, W}, + params::Nothing) where {U <: AbstractArray, V <: AbstractArray, W} + nothing +end + +# update a maj with parameter vectors +function (ratemap::JumpSysMajParamMapper{U, V, W})(maj::MassActionJump, newparams; + scale_rates, + kwargs...) where {U <: AbstractArray, + V <: AbstractArray, W} + updateparams!(ratemap, newparams) + for i in 1:get_num_majumps(maj) + maj.scaled_rates[i] = convert(W, + value(substitute(ratemap.paramexprs[i], + ratemap.subdict))) + end + scale_rates && JumpProcesses.scalerates!(maj.scaled_rates, maj.reactant_stoch) + nothing +end + +# create the initial parameter vector for use in a MassActionJump +function (ratemap::JumpSysMajParamMapper{ + U, + V, + W +})(params) where {U <: AbstractArray, + V <: AbstractArray, W} + updateparams!(ratemap, params) + [convert(W, value(substitute(paramexpr, ratemap.subdict))) + for paramexpr in ratemap.paramexprs] +end + +##### MTK dispatches for Symbolic jumps ##### +eqtype_supports_collect_vars(j::MassActionJump) = true +function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, + op = Differential) + collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) + for field in (j.reactant_stoch, j.net_stoch) + for el in field + collect_vars!(unknowns, parameters, el, iv; depth, op) + end + end + return nothing +end + +eqtype_supports_collect_vars(j::Union{ConstantRateJump, VariableRateJump}) = true +function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump, VariableRateJump}, + iv; depth = 0, op = Differential) + collect_vars!(unknowns, parameters, j.rate, iv; depth, op) + for eq in j.affect! + (eq isa Equation) && collect_vars!(unknowns, parameters, eq, iv; depth, op) + end + return nothing +end + +### Functions to determine which unknowns a jump depends on +function get_variables!(dep, jump::Union{ConstantRateJump, VariableRateJump}, variables) + jr = value(jump.rate) + (jr isa Symbolic) && get_variables!(dep, jr, variables) + dep +end + +function get_variables!(dep, jump::MassActionJump, variables) + sr = value(jump.scaled_rates) + (sr isa Symbolic) && get_variables!(dep, sr, variables) + for varasop in jump.reactant_stoch + any(isequal(varasop[1]), variables) && push!(dep, varasop[1]) + end + dep +end + +### Functions to determine which unknowns are modified by a given jump +function modified_unknowns!(munknowns, jump::Union{ConstantRateJump, VariableRateJump}, sts) + for eq in jump.affect! + st = eq.lhs + any(isequal(st), sts) && push!(munknowns, st) + end + munknowns +end + +function modified_unknowns!(munknowns, jump::MassActionJump, sts) + for (unknown, stoich) in jump.net_stoch + any(isequal(unknown), sts) && push!(munknowns, unknown) + end + munknowns +end diff --git a/src/problems/nonlinearproblem.jl b/src/problems/nonlinearproblem.jl new file mode 100644 index 0000000000..646322816c --- /dev/null +++ b/src/problems/nonlinearproblem.jl @@ -0,0 +1,103 @@ +@fallback_iip_specialize function SciMLBase.NonlinearFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; jac = false, + eval_expression = false, eval_module = @__MODULE__, sparse = false, + checkbounds = false, sparsity = false, analytic = nothing, + simplify = false, cse = true, initialization_data = nothing, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, NonlinearFunction) + check_compatibility && check_compatible_system(NonlinearFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing + error("u0, and p must be specified for FunctionWrapperSpecialize on NonlinearFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p)) + end + + if jac + _jac = generate_jacobian(sys, dvs, ps; expression = Val{false}, + simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _jac = nothing + end + + observedfun = ObservedFunctionCache( + sys; steady_state = false, eval_expression, eval_module, checkbounds, cse) + + if length(dvs) == length(equations(sys)) + resid_prototype = nothing + else + resid_prototype = calculate_resid_prototype(length(equations(sys)), u0, p) + end + + if sparse + jac_prototype = similar(calculate_jacobian(sys; sparse), eltype(u0)) + else + jac_prototype = nothing + end + + NonlinearFunction{iip, spec}(f; + sys = sys, + jac = _jac, + observed = observedfun, + analytic = analytic, + jac_prototype, + resid_prototype, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.NonlinearProblem{iip, spec}( + sys::System, u0map, parammap = SciMLBase.NullParameters(); + check_length = true, check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, NonlinearProblem) + check_compatibility && check_compatible_system(NonlinearProblem, sys) + + f, u0, p = process_SciMLProblem(NonlinearFunction{iip, spec}, sys, u0map, parammap; + check_length, check_compatibility, kwargs...) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(NonlinearProblem{iip}( + f, u0, p, StandardNonlinearProblem(); kwargs...)) +end + +@fallback_iip_specialize function SciMLBase.NonlinearLeastSquaresProblem{iip, spec}( + sys::System, u0map, parammap = DiffEqBase.NullParameters(); check_length = false, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, NonlinearLeastSquaresProblem) + check_compatibility && check_compatible_system(NonlinearLeastSquaresProblem, sys) + + f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; + check_length, kwargs...) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; kwargs...)) +end + +function check_compatible_system( + T::Union{Type{NonlinearFunction}, Type{NonlinearProblem}, + Type{NonlinearLeastSquaresProblem}}, sys::System) + check_time_independent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) +end + +function calculate_resid_prototype(N, u0, p) + u0ElType = u0 === nothing ? Float64 : eltype(u0) + if SciMLStructures.isscimlstructure(p) + u0ElType = promote_type( + eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), + u0ElType) + end + return zeros(u0ElType, N) +end diff --git a/src/problems/odeproblem.jl b/src/problems/odeproblem.jl new file mode 100644 index 0000000000..a62863216c --- /dev/null +++ b/src/problems/odeproblem.jl @@ -0,0 +1,116 @@ +@fallback_iip_specialize function SciMLBase.ODEFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; tgrad = false, jac = false, + t = nothing, eval_expression = false, eval_module = @__MODULE__, sparse = false, + steady_state = false, checkbounds = false, sparsity = false, analytic = nothing, + simplify = false, cse = true, initialization_data = nothing, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, ODEFunction) + check_compatibility && check_compatible_system(ODEFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + if tgrad + _tgrad = generate_tgrad(sys, dvs, ps; expression = Val{false}, + simplify, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _tgrad = nothing + end + + if jac + _jac = generate_jacobian(sys, dvs, ps; expression = Val{false}, + simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _jac = nothing + end + + M = calculate_massmatrix(sys) + _M = concrete_massmatrix(M; sparse, u0) + + observedfun = ObservedFunctionCache( + sys; steady_state, eval_expression, eval_module, checkbounds, cse) + + _W_sparsity = W_sparsity(sys) + W_prototype = calculate_W_prototype(_W_sparsity; u0, sparse) + + ODEFunction{iip, spec}(f; + sys = sys, + jac = _jac, + tgrad = _tgrad, + mass_matrix = _M, + jac_prototype = W_prototype, + observed = observedfun, + sparsity = sparsity ? _W_sparsity : nothing, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.ODEProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + callback = nothing, check_length = true, eval_expression = false, + eval_module = @__MODULE__, check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, ODEProblem) + check_compatibility && check_compatible_system(ODEProblem, sys) + + f, u0, p = process_SciMLProblem(ODEFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_length, eval_expression, + eval_module, check_compatibility, kwargs...) + + kwargs = process_kwargs(sys; callback, eval_expression, eval_module, kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(ODEProblem{iip}( + f, u0, tspan, p, StandardODEProblem(); kwargs...)) +end + +""" +```julia +SciMLBase.SteadyStateProblem(sys::System, u0map, + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = false, + checkbounds = false, sparse = false, + linenumbers = true, parallel = SerialForm(), + kwargs...) where {iip} +``` + +Generates an SteadyStateProblem from a `System` of ODEs and allows for automatically +symbolically calculating numerical enhancements. +""" +@fallback_iip_specialize function DiffEqBase.SteadyStateProblem{iip, spec}( + sys::System, u0map, + parammap = SciMLBase.NullParameters(); check_length = true, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, SteadyStateProblem) + check_compatibility && check_compatible_system(SteadyStateProblem, sys) + + f, u0, p = process_SciMLProblem(ODEFunction{iip}, sys, u0map, parammap; + steady_state = true, check_length, check_compatibility, + force_initialization_time_independent = true, kwargs...) + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + remake(SteadyStateProblem{iip}(f, u0, p; kwargs...)) +end + +function check_compatible_system( + T::Union{Type{ODEFunction}, Type{ODEProblem}, Type{DAEFunction}, + Type{DAEProblem}, Type{SteadyStateProblem}}, + sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_continuous(sys, T) +end diff --git a/src/problems/optimizationproblem.jl b/src/problems/optimizationproblem.jl new file mode 100644 index 0000000000..80c1dab1f1 --- /dev/null +++ b/src/problems/optimizationproblem.jl @@ -0,0 +1,137 @@ +function SciMLBase.OptimizationFunction(sys::System, args...; kwargs...) + return OptimizationFunction{true}(sys, args...; kwargs...) +end + +function SciMLBase.OptimizationFunction{iip}(sys::System, + _d = nothing, u0 = nothing, p = nothing; grad = false, hess = false, + sparse = false, cons_j = false, cons_h = false, cons_sparse = false, + linenumbers = true, eval_expression = false, eval_module = @__MODULE__, + simplify = false, check_compatibility = true, checkbounds = false, cse = true, + kwargs...) where {iip} + check_complete(sys, OptimizationFunction) + check_compatibility && check_compatible_system(OptimizationFunction, sys) + dvs = unknowns(sys) + ps = parameters(sys) + cstr = constraints(sys) + + f = generate_cost(sys; expression = Val{false}, eval_expression, + eval_module, checkbounds, cse, kwargs...) + + if grad + _grad = generate_cost_gradient(sys; expression = Val{false}, eval_expression, + eval_module, checkbounds, cse, kwargs...) + else + _grad = nothing + end + if hess + _hess, hess_prototype = generate_cost_hessian( + sys; expression = Val{false}, eval_expression, eval_module, + checkbounds, cse, sparse, simplify, return_sparsity = true, kwargs...) + else + _hess = hess_prototype = nothing + end + if isempty(cstr) + cons = lcons = ucons = _cons_j = cons_jac_prototype = _cons_h = nothing + cons_hess_prototype = cons_expr = nothing + else + cons = generate_cons(sys; expression = Val{false}, eval_expression, + eval_module, checkbounds, cse, kwargs...) + if cons_j + _cons_j, cons_jac_prototype = generate_constraint_jacobian( + sys; expression = Val{false}, eval_expression, eval_module, checkbounds, + cse, simplify, sparse = cons_sparse, return_sparsity = true, kwargs...) + else + _cons_j = cons_jac_prototype = nothing + end + if cons_h + _cons_h, cons_hess_prototype = generate_constraint_hessian( + sys; expression = Val{false}, eval_expression, eval_module, checkbounds, + cse, simplify, sparse = cons_sparse, return_sparsity = true, kwargs...) + else + _cons_h = cons_hess_prototype = nothing + end + cons_expr = toexpr.(subs_constants(cstr)) + end + + obj_expr = subs_constants(cost(sys)) + + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds, cse) + + return OptimizationFunction{iip}(f, SciMLBase.NoAD(); + sys = sys, + grad = _grad, + hess = _hess, + hess_prototype = hess_prototype, + cons = cons, + cons_j = _cons_j, + cons_jac_prototype = cons_jac_prototype, + cons_h = _cons_h, + cons_hess_prototype = cons_hess_prototype, + cons_expr = cons_expr, + expr = obj_expr, + observed = observedfun) +end + +function SciMLBase.OptimizationProblem(sys::System, args...; kwargs...) + return OptimizationProblem{true}(sys, args...; kwargs...) +end + +function SciMLBase.OptimizationProblem{iip}( + sys::System, u0map, parammap = SciMLBase.NullParameters(); lb = nothing, ub = nothing, + check_compatibility = true, kwargs...) where {iip} + check_complete(sys, OptimizationProblem) + check_compatibility && check_compatible_system(OptimizationProblem, sys) + + f, u0, p = process_SciMLProblem(OptimizationFunction{iip}, sys, u0map, parammap; + check_compatibility, tofloat = false, check_length = false, kwargs...) + + dvs = unknowns(sys) + int = symtype.(unwrap.(dvs)) .<: Integer + if lb === nothing && ub === nothing + lb = first.(getbounds.(dvs)) + ub = last.(getbounds.(dvs)) + isboolean = symtype.(unwrap.(dvs)) .<: Bool + lb[isboolean] .= 0 + ub[isboolean] .= 1 + else + xor(isnothing(lb), isnothing(ub)) && + throw(ArgumentError("Expected both `lb` and `ub` to be supplied")) + !isnothing(lb) && length(lb) != length(dvs) && + throw(ArgumentError("Expected both `lb` to be of the same length as the vector of optimization variables")) + !isnothing(ub) && length(ub) != length(dvs) && + throw(ArgumentError("Expected both `ub` to be of the same length as the vector of optimization variables")) + end + + ps = parameters(sys) + defs = merge(defaults(sys), to_varmap(parammap, ps), to_varmap(u0map, dvs)) + lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false) + ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false) + + if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) + lb = nothing + ub = nothing + end + + cstr = constraints(sys) + if isempty(cstr) + lcons = ucons = nothing + else + lcons = fill(-Inf, length(cstr)) + ucons = zeros(length(cstr)) + lcons[findall(Base.Fix2(isa, Equation), cstr)] .= 0.0 + end + + kwargs = process_kwargs(sys; kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(OptimizationProblem{iip}(f, u0, p; lb, ub, int, lcons, ucons, kwargs...)) +end + +function check_compatible_system( + T::Union{Type{OptimizationFunction}, Type{OptimizationProblem}}, sys::System) + check_time_independent(sys, T) + check_not_dde(sys) + check_has_cost(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_no_equations(sys, T) +end diff --git a/src/problems/sccnonlinearproblem.jl b/src/problems/sccnonlinearproblem.jl new file mode 100644 index 0000000000..864dcb71ee --- /dev/null +++ b/src/problems/sccnonlinearproblem.jl @@ -0,0 +1,246 @@ +const TypeT = Union{DataType, UnionAll} + +struct CacheWriter{F} + fn::F +end + +function (cw::CacheWriter)(p, sols) + cw.fn(p.caches, sols, p) +end + +function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, + exprs::Dict{TypeT, Vector{Any}}, solsyms, obseqs::Vector{Equation}; + eval_expression = false, eval_module = @__MODULE__, cse = true) + ps = parameters(sys; initial_parameters = true) + rps = reorder_parameters(sys, ps) + obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] + body = map(eachindex(buffer_types), buffer_types) do i, T + Symbol(:tmp, i) ← SetArray(true, :(out[$i]), get(exprs, T, [])) + end + + function argument_name(i::Int) + if i <= length(solsyms) + return :($(generated_argument_name(1))[$i]) + end + return generated_argument_name(i - length(solsyms)) + end + array_assignments = array_variable_assignments(solsyms...; argument_name) + fn = build_function_wrapper( + sys, nothing, :out, + DestructuredArgs(DestructuredArgs.(solsyms), generated_argument_name(1)), + rps...; p_start = 3, p_end = length(rps) + 2, + expression = Val{true}, add_observed = false, cse, + extra_assignments = [array_assignments; obs_assigns; body]) + fn = eval_or_rgf(fn; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(3, 3, is_split(sys))}(fn, nothing) + return CacheWriter(fn) +end + +struct SCCNonlinearFunction{iip} end + +function SCCNonlinearFunction{iip}( + sys::System, _eqs, _dvs, _obs, cachesyms; eval_expression = false, + eval_module = @__MODULE__, cse = true, kwargs...) where {iip} + ps = parameters(sys; initial_parameters = true) + rps = reorder_parameters(sys, ps) + + obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] + + rhss = [eq.rhs - eq.lhs for eq in _eqs] + f_gen = build_function_wrapper(sys, + rhss, _dvs, rps..., cachesyms...; p_start = 2, + p_end = length(rps) + length(cachesyms) + 1, add_observed = false, + extra_assignments = obs_assignments, expression = Val{true}, cse) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) + f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) + + subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, + parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) + if get_index_cache(sys) !== nothing + @set! subsys.index_cache = subset_unknowns_observed( + get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) + @set! subsys.complete = true + end + + return NonlinearFunction{iip}(f; sys = subsys) +end + +function SciMLBase.SCCNonlinearProblem(sys::NonlinearSystem, args...; kwargs...) + SCCNonlinearProblem{true}(sys, args...; kwargs...) +end + +function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, + parammap = SciMLBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, + cse = true, kwargs...) where {iip} + if !iscomplete(sys) || get_tearing_state(sys) === nothing + error("A simplified `NonlinearSystem` is required. Call `structural_simplify` on the system before creating an `SCCNonlinearProblem`.") + end + + if !is_split(sys) + error("The system has been simplified with `split = false`. `SCCNonlinearProblem` is not compatible with this system. Pass `split = true` to `structural_simplify` to use `SCCNonlinearProblem`.") + end + + ts = get_tearing_state(sys) + var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) + + if length(var_sccs) == 1 + return NonlinearProblem{iip}( + sys, u0map, parammap; eval_expression, eval_module, kwargs...) + end + + condensed_graph = MatchedCondensationGraph( + DiCMOBiGraph{true}(complete(ts.structure.graph), + complete(var_eq_matching)), + var_sccs) + toporder = topological_sort_by_dfs(condensed_graph) + var_sccs = var_sccs[toporder] + eq_sccs = map(Base.Fix1(getindex, var_eq_matching), var_sccs) + + dvs = unknowns(sys) + ps = parameters(sys) + eqs = equations(sys) + obs = observed(sys) + + _, u0, p = process_SciMLProblem( + EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) + + explicitfuns = [] + nlfuns = [] + prevobsidxs = BlockArray(undef_blocks, Vector{Int}, Int[]) + # Cache buffer types and corresponding sizes. Stored as a pair of arrays instead of a + # dict to maintain a consistent order of buffers across SCCs + cachetypes = TypeT[] + cachesizes = Int[] + # explicitfun! related information for each SCC + # We need to compute buffer sizes before doing any codegen + scc_cachevars = Dict{TypeT, Vector{Any}}[] + scc_cacheexprs = Dict{TypeT, Vector{Any}}[] + scc_eqs = Vector{Equation}[] + scc_obs = Vector{Equation}[] + # variables solved in previous SCCs + available_vars = Set() + for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) + # subset unknowns and equations + _dvs = dvs[vscc] + _eqs = eqs[escc] + # get observed equations required by this SCC + union!(available_vars, _dvs) + obsidxs = observed_equations_used_by(sys, _eqs; available_vars) + # the ones used by previous SCCs can be precomputed into the cache + setdiff!(obsidxs, prevobsidxs) + _obs = obs[obsidxs] + union!(available_vars, getproperty.(_obs, (:lhs,))) + + # get all subexpressions in the RHS which we can precompute in the cache + # precomputed subexpressions should not contain `banned_vars` + banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) + state = Dict() + for i in eachindex(_obs) + _obs[i] = _obs[i].lhs ~ subexpressions_not_involving_vars!( + _obs[i].rhs, banned_vars, state) + end + for i in eachindex(_eqs) + _eqs[i] = _eqs[i].lhs ~ subexpressions_not_involving_vars!( + _eqs[i].rhs, banned_vars, state) + end + + # map from symtype to cached variables and their expressions + cachevars = Dict{Union{DataType, UnionAll}, Vector{Any}}() + cacheexprs = Dict{Union{DataType, UnionAll}, Vector{Any}}() + # observed of previous SCCs are in the cache + # NOTE: When we get proper CSE, we can substitute these + # and then use `subexpressions_not_involving_vars!` + for i in prevobsidxs + T = symtype(obs[i].lhs) + buf = get!(() -> Any[], cachevars, T) + push!(buf, obs[i].lhs) + + buf = get!(() -> Any[], cacheexprs, T) + push!(buf, obs[i].lhs) + end + + for (k, v) in state + k = unwrap(k) + v = unwrap(v) + T = symtype(k) + buf = get!(() -> Any[], cachevars, T) + push!(buf, v) + buf = get!(() -> Any[], cacheexprs, T) + push!(buf, k) + end + + # update the sizes of cache buffers + for (T, buf) in cachevars + idx = findfirst(isequal(T), cachetypes) + if idx === nothing + push!(cachetypes, T) + push!(cachesizes, 0) + idx = lastindex(cachetypes) + end + cachesizes[idx] = max(cachesizes[idx], length(buf)) + end + + push!(scc_cachevars, cachevars) + push!(scc_cacheexprs, cacheexprs) + push!(scc_eqs, _eqs) + push!(scc_obs, _obs) + blockpush!(prevobsidxs, obsidxs) + end + + for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) + _dvs = dvs[vscc] + _eqs = scc_eqs[i] + _prevobsidxs = reduce(vcat, blocks(prevobsidxs)[1:(i - 1)]; init = Int[]) + _obs = scc_obs[i] + cachevars = scc_cachevars[i] + cacheexprs = scc_cacheexprs[i] + available_vars = [dvs[reduce(vcat, var_sccs[1:(i - 1)]; init = Int[])]; + getproperty.( + reduce(vcat, scc_obs[1:(i - 1)]; init = []), (:lhs,))] + _prevobsidxs = vcat(_prevobsidxs, + observed_equations_used_by( + sys, reduce(vcat, values(cacheexprs); init = []); available_vars)) + if isempty(cachevars) + push!(explicitfuns, Returns(nothing)) + else + solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) + push!(explicitfuns, + CacheWriter(sys, cachetypes, cacheexprs, solsyms, obs[_prevobsidxs]; + eval_expression, eval_module, cse)) + end + + cachebufsyms = Tuple(map(cachetypes) do T + get(cachevars, T, []) + end) + f = SCCNonlinearFunction{iip}( + sys, _eqs, _dvs, _obs, cachebufsyms; eval_expression, eval_module, cse, kwargs...) + push!(nlfuns, f) + end + + if !isempty(cachetypes) + templates = map(cachetypes, cachesizes) do T, n + # Real refers to `eltype(u0)` + if T == Real + T = eltype(u0) + elseif T <: Array && eltype(T) == Real + T = Array{eltype(u0), ndims(T)} + end + BufferTemplate(T, n) + end + p = rebuild_with_caches(p, templates...) + end + + subprobs = [] + for (f, vscc) in zip(nlfuns, var_sccs) + prob = NonlinearProblem(f, u0[vscc], p) + push!(subprobs, prob) + end + + new_dvs = dvs[reduce(vcat, var_sccs)] + new_eqs = eqs[reduce(vcat, eq_sccs)] + @set! sys.unknowns = new_dvs + @set! sys.eqs = new_eqs + @set! sys.index_cache = subset_unknowns_observed( + get_index_cache(sys), sys, new_dvs, getproperty.(obs, (:lhs,))) + return SCCNonlinearProblem(subprobs, explicitfuns, p, true; sys) +end diff --git a/src/problems/sddeproblem.jl b/src/problems/sddeproblem.jl new file mode 100644 index 0000000000..8c001294dd --- /dev/null +++ b/src/problems/sddeproblem.jl @@ -0,0 +1,78 @@ +@fallback_iip_specialize function SciMLBase.SDDEFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; + eval_expression = false, eval_module = @__MODULE__, checkbounds = false, + initialization_data = nothing, cse = true, check_compatibility = true, + sparse = false, simplify = false, analytic = nothing, kwargs...) where { + iip, spec} + check_complete(sys, SDDEFunction) + check_compatibility && check_compatible_system(SDDEFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + g = generate_diffusion_function(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds, cse, kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on SDDEFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + M = calculate_massmatrix(sys) + _M = concrete_massmatrix(M; sparse, u0) + + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds, cse) + + SDDEFunction{iip, spec}(f, g; + sys = sys, + mass_matrix = _M, + observed = observedfun, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.SDDEProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + callback = nothing, check_length = true, cse = true, checkbounds = false, + eval_expression = false, eval_module = @__MODULE__, check_compatibility = true, + u0_constructor = identity, sparse = false, sparsenoise = sparse, + kwargs...) where {iip, spec} + check_complete(sys, SDDEProblem) + check_compatibility && check_compatible_system(SDDEProblem, sys) + + f, u0, p = process_SciMLProblem(SDDEFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_length, cse, checkbounds, + eval_expression, eval_module, check_compatibility, sparse, symbolic_u0 = true, kwargs...) + + h = generate_history( + sys, u0; expression = Val{false}, cse, eval_expression, eval_module, + checkbounds) + u0 = float.(h(p, tspan[1])) + if u0 !== nothing + u0 = u0_constructor(u0) + end + + noise, noise_rate_prototype = calculate_noise_and_rate_prototype(sys, u0; sparsenoise) + kwargs = process_kwargs(sys; callback, eval_expression, eval_module, kwargs...) + + # Call `remake` so it runs initialization if it is trivial + return remake(SDDEProblem{iip}( + f, f.g, u0, h, tspan, p; noise, noise_rate_prototype, kwargs...)) +end + +function check_compatible_system( + T::Union{Type{SDDEFunction}, Type{SDDEProblem}}, sys::System) + check_time_dependent(sys, T) + check_is_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_has_noise(sys, T) + check_is_continuous(sys, T) +end diff --git a/src/problems/sdeproblem.jl b/src/problems/sdeproblem.jl new file mode 100644 index 0000000000..45ed21c9da --- /dev/null +++ b/src/problems/sdeproblem.jl @@ -0,0 +1,107 @@ +@fallback_iip_specialize function SciMLBase.SDEFunction{iip, spec}( + sys::System, _d = nothing, u0 = nothing, p = nothing; tgrad = false, jac = false, + t = nothing, eval_expression = false, eval_module = @__MODULE__, sparse = false, + steady_state = false, checkbounds = false, sparsity = false, analytic = nothing, + simplify = false, cse = true, initialization_data = nothing, + check_compatibility = true, kwargs...) where {iip, spec} + check_complete(sys, SDEFunction) + check_compatibility && check_compatible_system(SDEFunction, sys) + + dvs = unknowns(sys) + ps = parameters(sys) + f = generate_rhs(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + g = generate_diffusion_function(sys, dvs, ps; expression = Val{false}, + eval_expression, eval_module, checkbounds, cse, kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + if tgrad + _tgrad = generate_tgrad(sys, dvs, ps; expression = Val{false}, + simplify, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _tgrad = nothing + end + + if jac + _jac = generate_jacobian(sys, dvs, ps; expression = Val{false}, + simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _jac = nothing + end + + M = calculate_massmatrix(sys) + _M = concrete_massmatrix(M; sparse, u0) + + observedfun = ObservedFunctionCache( + sys; steady_state, eval_expression, eval_module, checkbounds, cse) + + _W_sparsity = W_sparsity(sys) + W_prototype = calculate_W_prototype(_W_sparsity; u0, sparse) + + SDEFunction{iip, spec}(f, g; + sys = sys, + jac = _jac, + tgrad = _tgrad, + mass_matrix = _M, + jac_prototype = W_prototype, + observed = observedfun, + sparsity = sparsity ? _W_sparsity : nothing, + analytic = analytic, + initialization_data) +end + +@fallback_iip_specialize function SciMLBase.SDEProblem{iip, spec}( + sys::System, u0map, tspan, parammap = SciMLBase.NullParameters(); + callback = nothing, check_length = true, eval_expression = false, + eval_module = @__MODULE__, check_compatibility = true, sparse = false, + sparsenoise = sparse, kwargs...) where {iip, spec} + check_complete(sys, SDEProblem) + check_compatibility && check_compatible_system(SDEProblem, sys) + + f, u0, p = process_SciMLProblem(SDEFunction{iip, spec}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, check_length, eval_expression, + eval_module, check_compatibility, sparse, kwargs...) + + noise, noise_rate_prototype = calculate_noise_and_rate_prototype(sys, u0; sparsenoise) + kwargs = process_kwargs(sys; callback, eval_expression, eval_module, kwargs...) + # Call `remake` so it runs initialization if it is trivial + return remake(SDEProblem{iip}(f, u0, tspan, p; noise, noise_rate_prototype, kwargs...)) +end + +function check_compatible_system(T::Union{Type{SDEFunction}, Type{SDEProblem}}, sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_has_noise(sys, T) + check_is_continuous(sys, T) +end + +function calculate_noise_and_rate_prototype(sys::System, u0; sparsenoise = false) + noiseeqs = get_noise_eqs(sys) + if noiseeqs isa AbstractVector + # diagonal noise + noise_rate_prototype = nothing + noise = nothing + elseif size(noiseeqs, 2) == 1 + # scalar noise + noise_rate_prototype = nothing + noise = WienerProcess(0.0, 0.0, 0.0) + elseif sparsenoise + I, J, V = findnz(SparseArrays.sparse(noiseeqs)) + noise_rate_prototype = SparseArrays.sparse(I, J, zero(eltype(u0))) + noise = nothing + else + noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) + noise = nothing + end + return noise, noise_rate_prototype +end diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 4adc817ef8..16d3a75464 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -11,7 +11,7 @@ using SymbolicUtils.Rewriters using SymbolicUtils: maketerm, iscall using ModelingToolkit -using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Differential, +using ModelingToolkit: System, AbstractSystem, var_from_nested_derivative, Differential, unknowns, equations, vars, Symbolic, diff2term_with_unit, shift2term_with_unit, value, operation, arguments, Sym, Term, simplify, symbolic_linear_solve, diff --git a/src/structural_transformation/pantelides.jl b/src/structural_transformation/pantelides.jl index b6877d65f8..eb7bdbd9f3 100644 --- a/src/structural_transformation/pantelides.jl +++ b/src/structural_transformation/pantelides.jl @@ -195,7 +195,7 @@ function pantelides!( eq′ = eq_to_diff[eq′] end # for _ in 1:maxiters pathfound || - error("maxiters=$maxiters reached! File a bug report if your system has a reasonable index (<100), and you are using the default `maxiters`. Try to increase the maxiters by `pantelides(sys::ODESystem; maxiters=1_000_000)` if your system has an incredibly high index and it is truly extremely large.") + error("maxiters=$maxiters reached! File a bug report if your system has a reasonable index (<100), and you are using the default `maxiters`. Try to increase the maxiters by `pantelides(sys::System; maxiters=1_000_000)` if your system has an incredibly high index and it is truly extremely large.") end # for k in 1:neqs′ finalize && for var in 1:ndsts(graph) @@ -206,13 +206,13 @@ function pantelides!( end """ - dae_index_lowering(sys::ODESystem; kwargs...) -> ODESystem + dae_index_lowering(sys::System; kwargs...) -> System Perform the Pantelides algorithm to transform a higher index DAE to an index 1 DAE. `kwargs` are forwarded to [`pantelides!`](@ref). End users are encouraged to call [`structural_simplify`](@ref) instead, which calls this function internally. """ -function dae_index_lowering(sys::ODESystem; kwargs...) +function dae_index_lowering(sys::System; kwargs...) state = TearingState(sys) var_eq_matching = pantelides!(state; finalize = false, kwargs...) return invalidate_cache!(pantelides_reassemble(state, var_eq_matching)) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 552c6d13c3..450b32514d 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -40,7 +40,7 @@ function var_derivative_graph!(s::SystemStructure, v::Int) return var_diff end -function var_derivative!(ts::TearingState{ODESystem}, v::Int) +function var_derivative!(ts::TearingState, v::Int) s = ts.structure var_diff = var_derivative_graph!(s, v) sys = ts.sys @@ -58,7 +58,7 @@ function eq_derivative_graph!(s::SystemStructure, eq::Int) return eq_diff end -function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int; kwargs...) +function eq_derivative!(ts::TearingState, ieq::Int; kwargs...) s = ts.structure eq_diff = eq_derivative_graph!(s, ieq) @@ -740,11 +740,13 @@ function update_simplified_system!( @set! sys.substitutions = Substitutions(subeqs, deps) # Only makes sense for time-dependent - # TODO: generalize to SDE - if sys isa ODESystem + if has_schedule(sys) @set! sys.schedule = Schedule(var_eq_matching, dummy_sub) end - sys = schedule(sys) + if has_isscheduled(sys) + @set! sys.isscheduled = true + end + return sys end """ diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index a48e4d7c31..9269fc381c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -113,17 +113,6 @@ the arguments to the internal [`build_function`](@ref) call. """ function generate_jacobian end -""" -```julia -generate_factorized_W(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys), - expression = Val{true}; sparse = false, kwargs...) -``` - -Generates a function for the factorized W matrix of a system. Extra arguments control -the arguments to the internal [`build_function`](@ref) call. -""" -function generate_factorized_W end - """ ```julia generate_hessian(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys), @@ -244,7 +233,9 @@ Get the independent variable(s) of the system `sys`. See also [`@independent_variables`](@ref) and [`ModelingToolkit.get_iv`](@ref). """ function independent_variables(sys::AbstractSystem) - @warn "Please declare ($(typeof(sys))) as a subtype of `AbstractTimeDependentSystem`, `AbstractTimeIndependentSystem` or `AbstractMultivariateSystem`." + if !(sys isa System) + @warn "Please declare ($(typeof(sys))) as a subtype of `AbstractTimeDependentSystem`, `AbstractTimeIndependentSystem` or `AbstractMultivariateSystem`." + end if isdefined(sys, :iv) return [getfield(sys, :iv)] elseif isdefined(sys, :ivs) @@ -613,17 +604,6 @@ end """ $(TYPEDSIGNATURES) -Mark a system as scheduled. It is only intended in compiler internals. A system -is scheduled after tearing based simplifications where equations are converted -into assignments. -""" -function schedule(sys::AbstractSystem) - has_schedule(sys) ? sys : (@set! sys.isscheduled = true) -end - -""" -$(TYPEDSIGNATURES) - If a system is scheduled, then changing its equations, variables, and parameters is no longer legal. """ @@ -876,11 +856,14 @@ end for prop in [:eqs :tag - :noiseeqs + :noiseeqs # TODO: remove + :noise_eqs :iv :unknowns :ps :tspan + :brownians + :jumps :name :description :var_to_name @@ -1294,7 +1277,29 @@ function namespace_equation(eq::Equation, ivs = independent_variables(sys)) _lhs = namespace_expr(eq.lhs, sys, n; ivs) _rhs = namespace_expr(eq.rhs, sys, n; ivs) - (_lhs ~ _rhs)::Equation + (_lhs ~ _rhs) +end + +function namespace_jump(j::ConstantRateJump, sys) + return ConstantRateJump(namespace_expr(j.rate, sys), namespace_expr(j.affect!, sys)) +end + +function namespace_jump(j::VariableRateJump, sys) + return VariableRateJump(namespace_expr(j.rate, sys), namespace_expr(j.affect!, sys)) +end + +function namespace_jump(j::MassActionJump, sys) + return MassActionJump(namespace_expr(j.scaled_rates, sys), + [namespace_expr(k, sys) => namespace_expr(v, sys) for (k, v) in j.reactant_stoch], + [namespace_expr(k, sys) => namespace_expr(v, sys) for (k, v) in j.net_stoch]) +end + +function namespace_jumps(sys::AbstractSystem) + return [namespace_jump(j, sys) for j in get_jumps(sys)] +end + +function namespace_brownians(sys::AbstractSystem) + return [renamespace(sys, b) for b in brownians(sys)] end function namespace_assignment(eq::Assignment, sys) @@ -1645,7 +1650,7 @@ function defaults(sys::AbstractSystem) # `mapfoldr` is really important!!! We should prefer the base model for # defaults, because people write: # - # `compose(ODESystem(...; defaults=defs), ...)` + # `compose(System(...; defaults=defs), ...)` # # Thus, right associativity is required and crucial for correctness. isempty(systems) ? defs : mapfoldr(namespace_defaults, merge, systems; init = defs) @@ -1708,6 +1713,59 @@ function equations_toplevel(sys::AbstractSystem) return get_eqs(sys) end +function jumps(sys::AbstractSystem) + js = get_jumps(sys) + systems = get_systems(sys) + if isempty(systems) + return js + end + return [js; reduce(vcat, namespace_jumps.(systems); init = [])] +end + +function brownians(sys::AbstractSystem) + bs = get_brownians(sys) + systems = get_systems(sys) + if isempty(systems) + return bs + end + return [bs; reduce(vcat, namespace_brownians.(systems); init = [])] +end + +function cost(sys::AbstractSystem) + cs = get_costs(sys) + consolidate = get_consolidate(sys) + systems = get_systems(sys) + if isempty(systems) + return consolidate(cs, Float64[]) + end + subcosts = [namespace_expr(cost(subsys), subsys) for subsys in systems] + return consolidate(cs, subcosts) +end + +namespace_constraint(eq::Equation, sys) = namespace_equation(eq, sys) + +namespace_constraint(ineq::Inequality, sys) = namespace_inequality(ineq, sys) + +function namespace_inequality(ineq::Inequality, sys, n = nameof(sys)) + _lhs = namespace_expr(ineq.lhs, sys, n) + _rhs = namespace_expr(ineq.rhs, sys, n) + Inequality(_lhs, + _rhs, + ineq.relational_op) +end + +function namespace_constraints(sys) + cstrs = constraints(sys) + isempty(cstrs) && return Vector{Union{Equation, Inequality}}(undef, 0) + map(cstr -> namespace_constraint(cstr, sys), cstrs) +end + +function constraints(sys) + cs = get_constraints(sys) + systems = get_systems(sys) + isempty(systems) ? cs : [cs; reduce(vcat, namespace_constraints.(systems))] +end + """ $(TYPEDSIGNATURES) @@ -1945,20 +2003,13 @@ function toexpr(sys::AbstractSystem) defs_name = push_defaults!(stmt, filtered_defs, var2name) obs_name = push_eqs!(stmt, obs, var2name) - if sys isa ODESystem - iv = get_iv(sys) - ivname = gensym(:iv) - push!(stmt, :($ivname = (@variables $(getname(iv)))[1])) - push!(stmt, - :($ODESystem($eqs_name, $ivname, $stsname, $psname; defaults = $defs_name, - observed = $obs_name, - name = $name, checks = false))) - elseif sys isa NonlinearSystem - push!(stmt, - :($NonlinearSystem($eqs_name, $stsname, $psname; defaults = $defs_name, - observed = $obs_name, - name = $name, checks = false))) - end + iv = get_iv(sys) + ivname = gensym(:iv) + push!(stmt, :($ivname = (@variables $(getname(iv)))[1])) + push!(stmt, + :($System($eqs_name, $ivname, $stsname, $psname; defaults = $defs_name, + observed = $obs_name, + name = $name, checks = false))) expr = :(let $expr @@ -2626,11 +2677,9 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name = name, description = description, gui_metadata = gui_metadata) # collect fields specific to some system types - if basesys isa ODESystem - ieqs = union(get_initialization_eqs(basesys), get_initialization_eqs(sys)) - guesses = merge(get_guesses(basesys), get_guesses(sys)) # prefer `sys` - kwargs = merge(kwargs, (initialization_eqs = ieqs, guesses = guesses)) - end + ieqs = union(get_initialization_eqs(basesys), get_initialization_eqs(sys)) + guesses = merge(get_guesses(basesys), get_guesses(sys)) # prefer `sys` + kwargs = merge(kwargs, (initialization_eqs = ieqs, guesses = guesses)) if has_assertions(basesys) kwargs = merge( @@ -2731,7 +2780,7 @@ function Symbolics.substitute(sys::AbstractSystem, rules::Union{Vector{<:Pair}, # post-walk to avoid infinite recursion @set! sys.systems = map(Base.Fix2(substitute, dict), systems) something(get(rules, nameof(sys), nothing), sys) - elseif sys isa ODESystem + elseif sys isa System rules = todict(map(r -> Symbolics.unwrap(r[1]) => Symbolics.unwrap(r[2]), collect(rules))) eqs = fast_substitute(get_eqs(sys), rules) @@ -2740,9 +2789,15 @@ function Symbolics.substitute(sys::AbstractSystem, rules::Union{Vector{<:Pair}, for (k, v) in get_defaults(sys)) guess = Dict(fast_substitute(k, rules) => fast_substitute(v, rules) for (k, v) in get_guesses(sys)) + noise_eqs = fast_substitute(get_noise_eqs(sys), rules) + costs = fast_substitute(get_costs(sys), rules) + observed = fast_substitute(get_observed(sys), rules) + initialization_eqs = fast_substitute(get_initialization_eqs(sys), rules) + cstrs = fast_substitute(get_constraints(sys), rules) subsys = map(s -> substitute(s, rules), get_systems(sys)) - ODESystem(eqs, get_iv(sys); name = nameof(sys), defaults = defs, - guesses = guess, parameter_dependencies = pdeps, systems = subsys) + System(eqs, get_iv(sys); name = nameof(sys), defaults = defs, + guesses = guess, parameter_dependencies = pdeps, systems = subsys, noise_eqs, + observed, initialization_eqs, constraints = cstrs) else error("substituting symbols is not supported for $(typeof(sys))") end @@ -2809,7 +2864,7 @@ using ModelingToolkit: t, D @parameters p = 1.0, [description = "My parameter", tunable = false] q = 2.0, [description = "Other parameter"] @variables x(t) = 3.0 [unit = u"m"] -@named sys = ODESystem(Equation[], t, [x], [p, q]) +@named sys = System(Equation[], t, [x], [p, q]) ModelingToolkit.dump_parameters(sys) ``` @@ -2850,7 +2905,7 @@ using ModelingToolkit: t, D @parameters p = 1.0, [description = "My parameter", tunable = false] q = 2.0, [description = "Other parameter"] @variables x(t) = 3.0 [unit = u"m"] -@named sys = ODESystem(Equation[], t, [x], [p, q]) +@named sys = System(Equation[], t, [x], [p, q]) ModelingToolkit.dump_unknowns(sys) ``` @@ -3028,7 +3083,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys = ODESystem([eq1, eq2], t) +@named osys = System([eq1, eq2], t) alg_equations(osys) # returns `[0 ~ p - d*X(t)]`. """ @@ -3047,7 +3102,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys = ODESystem([eq1, eq2], t) +@named osys = System([eq1, eq2], t) diff_equations(osys) # returns `[Differential(t)(X(t)) ~ p - d*X(t)]`. """ @@ -3067,8 +3122,8 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) +@named osys1 = System([eq1], t) +@named osys2 = System([eq2], t) has_alg_equations(osys1) # returns `false`. has_alg_equations(osys2) # returns `true`. @@ -3089,8 +3144,8 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) +@named osys1 = System([eq1], t) +@named osys2 = System([eq2], t) has_diff_equations(osys1) # returns `true`. has_diff_equations(osys2) # returns `false`. @@ -3112,9 +3167,9 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) -osys12 = compose(osys1, [osys2]) +@named osys1 = ([eq1], t) +@named osys2 = ([eq2], t) +osys12 = compose(sys1, [osys2]) osys21 = compose(osys2, [osys1]) get_alg_eqs(osys12) # returns `Equation[]`. @@ -3137,8 +3192,8 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) +@named osys1 = tem([eq1], t) +@named osys2 = tem([eq2], t) osys12 = compose(osys1, [osys2]) osys21 = compose(osys2, [osys1]) @@ -3162,8 +3217,8 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) +@named osys1 = System([eq1], t) +@named osys2 = System([eq2], t) osys12 = compose(osys1, [osys2]) osys21 = compose(osys2, [osys1]) @@ -3188,8 +3243,8 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables X(t) eq1 = D(X) ~ p - d*X eq2 = 0 ~ p - d*X -@named osys1 = ODESystem([eq1], t) -@named osys2 = ODESystem([eq2], t) +@named osys1 = tem([eq1], t) +@named osys2 = tem([eq2], t) osys12 = compose(osys1, [osys2]) osys21 = compose(osys2, [osys1]) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 0d1a2830cf..27a0204cb8 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -31,7 +31,7 @@ t = ModelingToolkit.get_iv(P) eqs = [connect(P.output, C.input) connect(C.output, :plant_input, P.input)] -sys = ODESystem(eqs, t, systems = [P, C], name = :feedback_system) +sys = System(eqs, t, systems = [P, C], name = :feedback_system) matrices_S, _ = get_sensitivity(sys, :plant_input) # Compute the matrices of a state-space representation of the (input) sensitivity function. matrices_T, _ = get_comp_sensitivity(sys, :plant_input) @@ -1007,12 +1007,12 @@ See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`open_loop` # """ - generate_control_function(sys::ModelingToolkit.AbstractODESystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) + generate_control_function(sys::ModelingToolkit.AbstractSystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) When called with analysis points as input arguments, we assume that all analysis points corresponds to connections that should be opened (broken). The use case for this is to get rid of input signal blocks, such as `Step` or `Sine`, since these are useful for simulation but are not needed when using the plant model in a controller or state estimator. """ function generate_control_function( - sys::ModelingToolkit.AbstractODESystem, input_ap_name::Union{ + sys::ModelingToolkit.AbstractSystem, input_ap_name::Union{ Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{ Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}} = nothing; diff --git a/src/systems/codegen.jl b/src/systems/codegen.jl new file mode 100644 index 0000000000..eeb5f85de5 --- /dev/null +++ b/src/systems/codegen.jl @@ -0,0 +1,739 @@ +""" + $(TYPEDSIGNATURES) + +Generate the RHS function for the `equations` of a `System`. + +# Arguments + +# Keyword Arguments + +""" +function generate_rhs(sys::System, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); implicit_dae = false, + scalar = false, expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + eqs = equations(sys) + obs = observed(sys) + u = dvs + p = reorder_parameters(sys, ps) + t = get_iv(sys) + ddvs = nothing + extra_assignments = Assignment[] + + # used for DAEProblem and ImplicitDiscreteProblem + if implicit_dae + if is_discrete_system(sys) + # ImplicitDiscrete case + D = Shift(t, 1) + rhss = map(eqs) do eq + # Algebraic equations get shifted forward 1, to match with differential + # equations + _iszero(eq.lhs) ? distribute_shift(D(eq.rhs)) : (eq.rhs - eq.lhs) + end + # Handle observables in algebraic equations, since they are shifted + shifted_obs = Equation[distribute_shift(D(eq)) for eq in obs] + obsidxs = observed_equations_used_by(sys, rhss; obs = shifted_obs) + extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) + for i in obsidxs] + else + D = Differential(t) + rhss = [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] + end + ddvs = map(D, dvs) + else + if !is_discrete_system(sys) + check_operator_variables(eqs, Differential) + check_lhs(eqs, Differential, Set(dvs)) + end + rhss = [eq.rhs for eq in eqs] + end + + if !isempty(assertions(sys)) + rhss[end] += unwrap(get_assertions_expr(sys)) + end + + # TODO: add an optional check on the ordering of observed equations + if scalar + rhss = only(rhss) + u = only(u) + end + + args = (u, p...) + p_start = 2 + if t !== nothing + args = (args..., t) + end + if implicit_dae + args = (ddvs, args...) + p_start += 1 + end + + res = build_function_wrapper(sys, rhss, args...; p_start, extra_assignments, + expression = Val{true}, expression_module = eval_module, kwargs...) + if expression == Val{true} + return res + end + + if res isa Tuple + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + else + f_oop = eval_or_rgf(res; eval_expression, eval_module) + f_iip = nothing + end + return GeneratedFunctionWrapper{(p_start, length(args) - length(p) + 1, is_split(sys))}( + f_oop, f_iip) +end + +function generate_diffusion_function(sys::System, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + eqs = get_noise_eqs(sys) + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) +end + +function calculate_tgrad(sys::System; simplify = false) + # We need to remove explicit time dependence on the unknown because when we + # have `u(t) * t` we want to have the tgrad to be `u(t)` instead of `u'(t) * + # t + u(t)`. + rhs = [detime_dvs(eq.rhs) for eq in full_equations(sys)] + iv = get_iv(sys) + xs = unknowns(sys) + rule = Dict(map((x, xt) -> xt => x, detime_dvs.(xs), xs)) + rhs = substitute.(rhs, Ref(rule)) + tgrad = [expand_derivatives(Differential(iv)(r), simplify) for r in rhs] + reverse_rule = Dict(map((x, xt) -> x => xt, detime_dvs.(xs), xs)) + tgrad = Num.(substitute.(tgrad, Ref(reverse_rule))) + return tgrad +end + +function calculate_jacobian(sys::System; + sparse = false, simplify = false, dvs = unknowns(sys)) + obs = Dict(eq.lhs => eq.rhs for eq in observed(sys)) + rhs = map(eq -> fixpoint_sub(eq.rhs, obs), equations(sys)) + + if sparse + jac = sparsejacobian(rhs, dvs; simplify) + if get_iv(sys) !== nothing + W_s = W_sparsity(sys) + (Is, Js, Vs) = findnz(W_s) + # Add nonzeros of W as non-structural zeros of the Jacobian (to ensure equal + # results for oop and iip Jacobian) + for (i, j) in zip(Is, Js) + iszero(jac[i, j]) && begin + jac[i, j] = 1 + jac[i, j] = 0 + end + end + end + else + jac = jacobian(rhs, dvs; simplify) + end + + return jac +end + +function generate_jacobian(sys::System, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); + simplify = false, sparse = false, eval_expression = false, + eval_module = @__MODULE__, expression = Val{true}, kwargs...) + jac = calculate_jacobian(sys; simplify, sparse, dvs) + p = reorder_parameters(sys, ps) + t = get_iv(sys) + if t === nothing + wrap_code = (identity, identity) + else + wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity) + end + res = build_function_wrapper(sys, jac, dvs, p..., t; wrap_code, expression = Val{true}, + expression_module = eval_module, kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) +end + +function assert_jac_length_header(sys) + W = W_sparsity(sys) + identity, + function add_header(expr) + Func(expr.args, [], expr.body, + [:(@assert $(SymbolicUtils.Code.toexpr(term(findnz, expr.args[1])))[1:2] == + $(findnz(W)[1:2]))]) + end +end + +function generate_tgrad( + sys::System, dvs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); + simplify = false, eval_expression = false, eval_module = @__MODULE__, + expression = Val{true}, kwargs...) + tgrad = calculate_tgrad(sys, simplify = simplify) + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, tgrad, + dvs, + p..., + get_iv(sys); + expression = Val{true}, + expression_module = eval_module, + kwargs...) + + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) +end + +const W_GAMMA = only(@variables ˍ₋gamma) + +function generate_W(sys::System, γ = 1.0, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); + simplify = false, sparse = false, expression = Val{true}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + M = calculate_massmatrix(sys; simplify) + if sparse + M = SparseArrays.sparse(M) + end + J = calculate_jacobian(sys; simplify, sparse, dvs) + W = W_GAMMA * M + J + t = get_iv(sys) + if t !== nothing + wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity) + end + + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, W, dvs, p..., W_GAMMA, t; wrap_code, + p_end = 1 + length(p), kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 4, is_split(sys))}(f_oop, f_iip) +end + +function generate_dae_jacobian(sys::System, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, + expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, + kwargs...) + jac_u = calculate_jacobian(sys; simplify = simplify, sparse = sparse) + t = get_iv(sys) + derivatives = Differential(t).(unknowns(sys)) + jac_du = calculate_jacobian(sys; simplify = simplify, sparse = sparse, + dvs = derivatives) + dvs = unknowns(sys) + jac = W_GAMMA * jac_du + jac_u + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, jac, derivatives, dvs, p..., W_GAMMA, t; + p_start = 3, p_end = 2 + length(p), kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(3, 5, is_split(sys))}(f_oop, f_iip) +end + +function generate_history(sys::System, u0; expression = Val{true}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + p = reorder_parameters(sys) + res = build_function_wrapper(sys, u0, p..., get_iv(sys); expression = Val{true}, + expression_module = eval_module, p_start = 1, p_end = length(p), + similarto = typeof(u0), wrap_delays = false, kwargs...) + + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(1, 2, is_split(sys))}(f_oop, f_iip) +end + +function calculate_massmatrix(sys::System; simplify = false) + eqs = [eq for eq in equations(sys)] + M = zeros(length(eqs), length(eqs)) + for (i, eq) in enumerate(eqs) + if iscall(eq.lhs) && operation(eq.lhs) isa Differential + st = var_from_nested_derivative(eq.lhs)[1] + j = variable_index(sys, st) + M[i, j] = 1 + else + _iszero(eq.lhs) || + error("Only semi-explicit constant mass matrices are currently supported. Faulty equation: $eq.") + end + end + M = simplify ? simplify.(M) : M + # M should only contain concrete numbers + M == I ? I : M +end + +function concrete_massmatrix(M; sparse = false, u0 = nothing) + if sparse && !(u0 === nothing || M === I) + SparseArrays.sparse(M) + elseif u0 === nothing || M === I + M + else + ArrayInterface.restructure(u0 .* u0', M) + end +end + +function jacobian_sparsity(sys::System) + sparsity = torn_system_jacobian_sparsity(sys) + sparsity === nothing || return sparsity + + Symbolics.jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in unknowns(sys)]) +end + +function jacobian_dae_sparsity(sys::System) + J1 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in unknowns(sys)]) + derivatives = Differential(get_iv(sys)).(unknowns(sys)) + J2 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in derivatives]) + J1 + J2 +end + +function W_sparsity(sys::System) + jac_sparsity = jacobian_sparsity(sys) + (n, n) = size(jac_sparsity) + M = calculate_massmatrix(sys) + M_sparsity = M isa UniformScaling ? sparse(I(n)) : + SparseMatrixCSC{Bool, Int64}((!iszero).(M)) + jac_sparsity .| M_sparsity +end + +function calculate_W_prototype(W_sparsity; u0 = nothing, sparse = false) + sparse || return nothing + uElType = u0 === nothing ? Float64 : eltype(u0) + return similar(W_sparsity, uElType) +end + +function isautonomous(sys::System) + tgrad = calculate_tgrad(sys; simplify = true) + all(iszero, tgrad) +end + +function get_bv_solution_symbol(ns) + only(@variables BV_SOLUTION(..)[1:ns]) +end + +function get_constraint_unknown_subs!(subs::Dict, cons::Vector, stidxmap::Dict, iv, sol) + vs = vars(cons) + for v in vs + iscall(v) || continue + op = operation(v) + args = arguments(v) + issym(op) && length(args) == 1 || continue + newv = op(iv) + haskey(stidxmap, newv) || continue + subs[v] = sol(args[1])[stidxmap[newv]] + end +end + +function generate_boundary_conditions(sys::System, u0, u0_idxs, t0; expression = Val{true}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + iv = get_iv(sys) + sts = unknowns(sys) + ps = parameters(sys) + np = length(ps) + ns = length(sts) + stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) + pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) + + sol = get_bv_solution_symbol(ns) + + cons = [con.lhs - con.rhs for con in constraints(sys)] + conssubs = Dict() + get_constraint_unknown_subs!(conssubs, cons, stidxmap, iv, sol) + cons = map(x -> fast_substitute(x, conssubs), cons) + + init_conds = Any[] + for i in u0_idxs + expr = sol(t0)[i] - u0[i] + push!(init_conds, expr) + end + + exprs = vcat(init_conds, cons) + _p = reorder_parameters(sys, ps) + + res = build_function_wrapper(sys, exprs, sol, _p..., iv; output_type = Array, kwargs...) + if expression == Val{true} + return res + end + + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) +end + +function generate_cost(sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + res = build_function_wrapper(sys, obj, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return res + end + f_oop = eval_or_rgf(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, nothing) +end + +function generate_cost_gradient( + sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, simplify = false, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + exprs = Symbolics.gradient(obj, dvs; simplify) + res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + return GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) +end + +function generate_cost_hessian( + sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, simplify = false, + sparse = false, return_sparsity = false, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + sparsity = nothing + if sparse + exprs = Symbolics.sparsehessian(obj, dvs; simplify)::AbstractSparseArray + sparsity = similar(exprs, Float64) + else + exprs = Symbolics.hessian(obj, dvs; simplify) + end + res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return return_sparsity ? (res, sparsity) : res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) + return return_sparsity ? (fn, sparsity) : fn +end + +function canonical_constraints(sys::System) + return map(constraints(sys)) do cstr + Symbolics.canonical_form(cstr).lhs + end +end + +function generate_cons(sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + res = build_function_wrapper(sys, cons, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) + return fn +end + +function generate_constraint_jacobian( + sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, return_sparsity = false, + simplify = false, sparse = false, kwargs...) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + sparsity = nothing + if sparse + jac = Symbolics.sparsejacobian(cons, dvs; simplify)::AbstractSparseArray + sparsity = similar(jac, Float64) + else + jac = Symbolics.jacobian(cons, dvs; simplify) + end + res = build_function_wrapper(sys, jac, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return return_sparsity ? (res, sparsity) : res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) + return return_sparsity ? (fn, sparsity) : fn +end + +function generate_constraint_hessian( + sys::System; expression = Val{true}, eval_expression = false, + eval_module = @__MODULE__, return_sparsity = false, + simplify = false, sparse = false, kwargs...) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + sparsity = nothing + if sparse + hess = map(cons) do cstr + Symbolics.sparsehessian(cstr, dvs; simplify)::AbstractSparseArray + end + sparsity = similar.(hess, Float64) + else + hess = [Symbolics.hessian(cstr, dvs; simplify) for cstr in cons] + end + res = build_function_wrapper(sys, hess, dvs, ps...; expression = Val{true}, kwargs...) + if expression == Val{true} + return return_sparsity ? (res, sparsity) : res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) + return return_sparsity ? (fn, sparsity) : fn +end + +# modifies the expression representing an affect function to +# call reset_aggregated_jumps!(integrator). +# assumes iip +function _reset_aggregator!(expr, integrator) + @assert Meta.isexpr(expr, :function) + body = expr.args[end] + body = quote + $body + $reset_aggregated_jumps!($integrator) + end + expr.args[end] = body + return nothing +end + +function generate_rate_function(js::System, rate) + p = reorder_parameters(js) + build_function_wrapper(js, rate, unknowns(js), p..., + get_iv(js), + expression = Val{true}) +end + +function generate_affect_function(js::System, affect, outputidxs) + compile_affect( + affect, nothing, js, unknowns(js), parameters(js); outputidxs = outputidxs, + expression = Val{true}, checkvars = false) +end + +function assemble_vrj( + js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) + rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) + rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) + outputvars = (value(affect.lhs) for affect in vrj.affect!) + outputidxs = [unknowntoid[var] for var in outputvars] + affect = eval_or_rgf(generate_affect_function(js, vrj.affect!, outputidxs); + eval_expression, eval_module) + VariableRateJump(rate, affect; save_positions = vrj.save_positions) +end + +function assemble_crj( + js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) + rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) + rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) + outputvars = (value(affect.lhs) for affect in crj.affect!) + outputidxs = [unknowntoid[var] for var in outputvars] + affect = eval_or_rgf(generate_affect_function(js, crj.affect!, outputidxs); + eval_expression, eval_module) + ConstantRateJump(rate, affect) +end + +# assemble a numeric MassActionJump from a MT symbolics MassActionJumps +function assemble_maj(majv::Vector{U}, unknowntoid, pmapper) where {U <: MassActionJump} + rs = [numericrstoich(maj.reactant_stoch, unknowntoid) for maj in majv] + ns = [numericnstoich(maj.net_stoch, unknowntoid) for maj in majv] + MassActionJump(rs, ns; param_mapper = pmapper, nocopy = true) +end + +function numericrstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} + rs = Vector{Pair{Int, W}}() + for (wspec, stoich) in mtrs + spec = value(wspec) + if !iscall(spec) && _iszero(spec) + push!(rs, 0 => stoich) + else + push!(rs, unknowntoid[spec] => stoich) + end + end + sort!(rs) + rs +end + +function numericnstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} + ns = Vector{Pair{Int, W}}() + for (wspec, stoich) in mtrs + spec = value(wspec) + !iscall(spec) && _iszero(spec) && + error("Net stoichiometry can not have a species labelled 0.") + push!(ns, unknowntoid[spec] => stoich) + end + sort!(ns) +end + +""" + build_explicit_observed_function(sys, ts; kwargs...) -> Function(s) + +Generates a function that computes the observed value(s) `ts` in the system `sys`, while making the assumption that there are no cycles in the equations. + +## Arguments +- `sys`: The system for which to generate the function +- `ts`: The symbolic observed values whose value should be computed + +## Keywords +- `return_inplace = false`: If true and the observed value is a vector, then return both the in place and out of place methods. +- `expression = false`: Generates a Julia `Expr`` computing the observed value if `expression` is true +- `eval_expression = false`: If true and `expression = false`, evaluates the returned function in the module `eval_module` +- `output_type = Array` the type of the array generated by a out-of-place vector-valued function +- `param_only = false` if true, only allow the generated function to access system parameters +- `inputs = nothing` additinoal symbolic variables that should be provided to the generated function +- `checkbounds = true` checks bounds if true when destructuring parameters +- `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. +- `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. +- `mkarray`: only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. +- `cse = true`: Whether to use Common Subexpression Elimination (CSE) to generate a more efficient function. + +## Returns + +The return value will be either: +* a single function `f_oop` if the input is a scalar or if the input is a Vector but `return_inplace` is false +* the out of place and in-place functions `(f_ip, f_oop)` if `return_inplace` is true and the input is a `Vector` + +The function(s) `f_oop` (and potentially `f_ip`) will be: +* `RuntimeGeneratedFunction`s by default, +* A Julia `Expr` if `expression` is true, +* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true and `expression` is false. + +The signatures will be of the form `g(...)` with arguments: + +- `output` for in-place functions +- `unknowns` if `param_only` is `false` +- `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` +- `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted +- `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` + +For example, a function `g(op, unknowns, p..., inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, +an array of inputs `inputs` is given, and `param_only` is false for a time-dependent system. +""" +function build_explicit_observed_function(sys, ts; + inputs = nothing, + disturbance_inputs = nothing, + disturbance_argument = false, + expression = false, + eval_expression = false, + eval_module = @__MODULE__, + output_type = Array, + checkbounds = true, + ps = parameters(sys; initial_parameters = true), + return_inplace = false, + param_only = false, + op = Operator, + throw = true, + cse = true, + mkarray = nothing) + # TODO: cleanup + is_tuple = ts isa Tuple + if is_tuple + ts = collect(ts) + output_type = Tuple + end + + allsyms = all_symbols(sys) + if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray + ts = map(x -> symbol_to_symbolic(sys, x; allsyms), ts) + else + ts = symbol_to_symbolic(sys, ts; allsyms) + end + + vs = ModelingToolkit.vars(ts; op) + namespace_subs = Dict() + ns_map = Dict{Any, Any}(renamespace(sys, eq.lhs) => eq.lhs for eq in observed(sys)) + for sym in unknowns(sys) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] + end + end + for sym in full_parameters(sys) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] + end + end + allsyms = Set(all_symbols(sys)) + iv = has_iv(sys) ? get_iv(sys) : nothing + for var in vs + var = unwrap(var) + newvar = get(ns_map, var, nothing) + if newvar !== nothing + namespace_subs[var] = newvar + var = newvar + end + if throw && !var_in_varlist(var, allsyms, iv) + Base.throw(ArgumentError("Symbol $var is not present in the system.")) + end + end + ts = fast_substitute(ts, namespace_subs) + + obsfilter = if param_only + if is_split(sys) + let ic = get_index_cache(sys) + eq -> !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) + end + else + Returns(false) + end + else + Returns(true) + end + dvs = if param_only + () + else + (unknowns(sys),) + end + if inputs === nothing + inputs = () + else + ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list + inputs = (inputs,) + end + if disturbance_inputs !== nothing + # Disturbance inputs may or may not be included as inputs, depending on disturbance_argument + ps = setdiff(ps, disturbance_inputs) + end + if disturbance_argument + disturbance_inputs = (disturbance_inputs,) + else + disturbance_inputs = () + end + ps = reorder_parameters(sys, ps) + iv = if is_time_dependent(sys) + (get_iv(sys),) + else + () + end + args = (dvs..., inputs..., ps..., iv..., disturbance_inputs...) + p_start = length(dvs) + length(inputs) + 1 + p_end = length(dvs) + length(inputs) + length(ps) + fns = build_function_wrapper( + sys, ts, args...; p_start, p_end, filter_observed = obsfilter, + output_type, mkarray, try_namespaced = true, expression = Val{true}, cse) + if fns isa Tuple + if expression + return return_inplace ? fns : fns[1] + end + oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( + oop, iip) + return return_inplace ? (f, f) : f + else + if expression + return fns + end + f = eval_or_rgf(fns; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( + f, nothing) + return f + end +end diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index bedfdbcc37..8fba085ec4 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -86,6 +86,74 @@ function array_variable_assignments(args...; argument_name = generated_argument_ return assignments end +""" + $(TYPEDSIGNATURES) + +Check if the variable `var` is a delayed variable, where `iv` is the independent +variable. +""" +function isdelay(var, iv) + iv === nothing && return false + isvariable(var) || return false + isparameter(var) && return false + if iscall(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) + args = arguments(var) + length(args) == 1 || return false + isequal(args[1], iv) || return true + end + return false +end + +""" +The argument of generated functions corresponding to the history function. +""" +const DDE_HISTORY_FUN = Sym{Symbolics.FnType{Tuple{Any, <:Real}, Vector{Real}}}(:___history___) + +""" + $(TYPEDSIGNATURES) + +Turn delayed unknowns in `eqs` into calls to `DDE_HISTORY_FUNCTION`. + +# Arguments + +- `sys`: The system of DDEs. +- `eqs`: The equations to convert. + +# Keyword Arguments + +- `param_arg`: The name of the variable containing the parameter object. +""" +function delay_to_function( + sys::AbstractSystem, eqs = full_equations(sys); param_arg = MTKPARAMETERS_ARG) + delay_to_function(eqs, + get_iv(sys), + Dict{Any, Int}(operation(s) => i for (i, s) in enumerate(unknowns(sys))), + parameters(sys), + DDE_HISTORY_FUN; param_arg) +end +function delay_to_function(eqs::Vector, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG) + delay_to_function.(eqs, (iv,), (sts,), (ps,), (h,); param_arg) +end +function delay_to_function(eq::Equation, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG) + delay_to_function(eq.lhs, iv, sts, ps, h; param_arg) ~ delay_to_function( + eq.rhs, iv, sts, ps, h; param_arg) +end +function delay_to_function(expr, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG) + if isdelay(expr, iv) + v = operation(expr) + time = arguments(expr)[1] + idx = sts[v] + return term(getindex, h(param_arg, time), idx, type = Real) + elseif iscall(expr) + return maketerm(typeof(expr), + operation(expr), + map(x -> delay_to_function(x, iv, sts, ps, h; param_arg), arguments(expr)), + metadata(expr)) + else + return expr + end +end + """ $(TYPEDSIGNATURES) @@ -138,11 +206,11 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, obs = filter(filter_observed, observed(sys)) # turn delayed unknowns into calls to the history function if wrap_delays - history_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start) + param_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start) obs = map(obs) do eq - delay_to_function(sys, eq; history_arg) + delay_to_function(sys, eq; param_arg) end - expr = delay_to_function(sys, expr; history_arg) + expr = delay_to_function(sys, expr; param_arg) # add extra argument args = (args[1:(p_start - 1)]..., DDE_HISTORY_FUN, args[p_start:end]...) p_start += 1 diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl deleted file mode 100644 index 62ddd12a08..0000000000 --- a/src/systems/diffeqs/abstractodesystem.jl +++ /dev/null @@ -1,1560 +0,0 @@ -struct Schedule - var_eq_matching::Any - dummy_sub::Any -end - -""" - is_dde(sys::AbstractSystem) - -Return a boolean indicating whether a system represents a set of delay -differential equations. -""" -is_dde(sys::AbstractSystem) = has_is_dde(sys) && get_is_dde(sys) - -function _check_if_dde(eqs, iv, subsystems) - is_dde = any(ModelingToolkit.is_dde, subsystems) - if !is_dde - vs = Set() - for eq in eqs - vars!(vs, eq) - is_dde = any(vs) do sym - isdelay(unwrap(sym), iv) - end - is_dde && break - end - end - return is_dde -end - -function filter_kwargs(kwargs) - kwargs = Dict(kwargs) - for key in keys(kwargs) - key in DiffEqBase.allowedkeywords || delete!(kwargs, key) - end - pairs(NamedTuple(kwargs)) -end -function gen_quoted_kwargs(kwargs) - kwargparam = Expr(:parameters) - for kw in kwargs - push!(kwargparam.args, Expr(:kw, kw[1], kw[2])) - end - kwargparam -end - -function calculate_tgrad(sys::AbstractODESystem; - simplify = false) - isempty(get_tgrad(sys)[]) || return get_tgrad(sys)[] # use cached tgrad, if possible - - # We need to remove explicit time dependence on the unknown because when we - # have `u(t) * t` we want to have the tgrad to be `u(t)` instead of `u'(t) * - # t + u(t)`. - rhs = [detime_dvs(eq.rhs) for eq in full_equations(sys)] - iv = get_iv(sys) - xs = unknowns(sys) - rule = Dict(map((x, xt) -> xt => x, detime_dvs.(xs), xs)) - rhs = substitute.(rhs, Ref(rule)) - tgrad = [expand_derivatives(Differential(iv)(r), simplify) for r in rhs] - reverse_rule = Dict(map((x, xt) -> x => xt, detime_dvs.(xs), xs)) - tgrad = Num.(substitute.(tgrad, Ref(reverse_rule))) - get_tgrad(sys)[] = tgrad - return tgrad -end - -function calculate_jacobian(sys::AbstractODESystem; - sparse = false, simplify = false, dvs = unknowns(sys)) - if isequal(dvs, unknowns(sys)) - cache = get_jac(sys)[] - if cache isa Tuple && cache[2] == (sparse, simplify) - return cache[1] - end - end - - rhs = [eq.rhs - eq.lhs for eq in full_equations(sys)] #need du terms on rhs for differentiating wrt du - - if sparse - jac = sparsejacobian(rhs, dvs, simplify = simplify) - W_s = W_sparsity(sys) - (Is, Js, Vs) = findnz(W_s) - # Add nonzeros of W as non-structural zeros of the Jacobian (to ensure equal results for oop and iip Jacobian.) - for (i, j) in zip(Is, Js) - iszero(jac[i, j]) && begin - jac[i, j] = 1 - jac[i, j] = 0 - end - end - else - jac = jacobian(rhs, dvs, simplify = simplify) - end - - if isequal(dvs, unknowns(sys)) - get_jac(sys)[] = jac, (sparse, simplify) # cache Jacobian - end - - return jac -end - -function calculate_control_jacobian(sys::AbstractODESystem; - sparse = false, simplify = false) - cache = get_ctrl_jac(sys)[] - if cache isa Tuple && cache[2] == (sparse, simplify) - return cache[1] - end - - rhs = [eq.rhs for eq in full_equations(sys)] - ctrls = controls(sys) - - if sparse - jac = sparsejacobian(rhs, ctrls, simplify = simplify) - else - jac = jacobian(rhs, ctrls, simplify = simplify) - end - - get_ctrl_jac(sys)[] = jac, (sparse, simplify) # cache Jacobian - return jac -end - -function generate_tgrad( - sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - simplify = false, kwargs...) - tgrad = calculate_tgrad(sys, simplify = simplify) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, tgrad, - dvs, - p..., - get_iv(sys); - kwargs...) -end - -function generate_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - simplify = false, sparse = false, kwargs...) - jac = calculate_jacobian(sys; simplify = simplify, sparse = sparse) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, jac, - dvs, - p..., - get_iv(sys); - wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity), - kwargs...) -end - -function assert_jac_length_header(sys) - W = W_sparsity(sys) - identity, - function add_header(expr) - Func(expr.args, [], expr.body, - [:(@assert $(SymbolicUtils.Code.toexpr(term(findnz, expr.args[1])))[1:2] == - $(findnz(W)[1:2]))]) - end -end - -function generate_W(sys::AbstractODESystem, γ = 1.0, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - simplify = false, sparse = false, kwargs...) - @variables ˍ₋gamma - M = calculate_massmatrix(sys; simplify) - sparse && (M = SparseArrays.sparse(M)) - J = calculate_jacobian(sys; simplify, sparse, dvs) - W = ˍ₋gamma * M + J - - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, W, - dvs, - p..., - ˍ₋gamma, - get_iv(sys); - wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity), - p_end = 1 + length(p), - kwargs...) -end - -function generate_control_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - simplify = false, sparse = false, kwargs...) - jac = calculate_control_jacobian(sys; simplify = simplify, sparse = sparse) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, jac, dvs, p..., get_iv(sys); kwargs...) -end - -function generate_dae_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, - kwargs...) - jac_u = calculate_jacobian(sys; simplify = simplify, sparse = sparse) - derivatives = Differential(get_iv(sys)).(unknowns(sys)) - jac_du = calculate_jacobian(sys; simplify = simplify, sparse = sparse, - dvs = derivatives) - dvs = unknowns(sys) - @variables ˍ₋gamma - jac = ˍ₋gamma * jac_du + jac_u - pre = get_preprocess_constants(jac) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, jac, derivatives, dvs, p..., ˍ₋gamma, get_iv(sys); - p_start = 3, p_end = 2 + length(p), kwargs...) -end - -function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - implicit_dae = false, - ddvs = implicit_dae ? map(Differential(get_iv(sys)), dvs) : - nothing, - isdde = false, - kwargs...) - eqs = [eq for eq in equations(sys)] - if !implicit_dae - check_operator_variables(eqs, Differential) - check_lhs(eqs, Differential, Set(dvs)) - end - - rhss = implicit_dae ? [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] : - [eq.rhs for eq in eqs] - - if !isempty(assertions(sys)) - rhss[end] += unwrap(get_assertions_expr(sys)) - end - - # TODO: add an optional check on the ordering of observed equations - u = dvs - p = reorder_parameters(sys, ps) - t = get_iv(sys) - - if implicit_dae - build_function_wrapper(sys, rhss, ddvs, u, p..., t; p_start = 3, kwargs...) - else - build_function_wrapper(sys, rhss, u, p..., t; kwargs...) - end -end - -function isdelay(var, iv) - iv === nothing && return false - isvariable(var) || return false - isparameter(var) && return false - if iscall(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) - args = arguments(var) - length(args) == 1 || return false - isequal(args[1], iv) || return true - end - return false -end -const DDE_HISTORY_FUN = Sym{Symbolics.FnType{Tuple{Any, <:Real}, Vector{Real}}}(:___history___) -const DEFAULT_PARAMS_ARG = Sym{Any}(:ˍ₋arg3) -function delay_to_function( - sys::AbstractODESystem, eqs = full_equations(sys); history_arg = DEFAULT_PARAMS_ARG) - delay_to_function(eqs, - get_iv(sys), - Dict{Any, Int}(operation(s) => i for (i, s) in enumerate(unknowns(sys))), - parameters(sys), - DDE_HISTORY_FUN; history_arg) -end -function delay_to_function(eqs::Vector, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) - delay_to_function.(eqs, (iv,), (sts,), (ps,), (h,); history_arg) -end -function delay_to_function(eq::Equation, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) - delay_to_function(eq.lhs, iv, sts, ps, h; history_arg) ~ delay_to_function( - eq.rhs, iv, sts, ps, h; history_arg) -end -function delay_to_function(expr, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) - if isdelay(expr, iv) - v = operation(expr) - time = arguments(expr)[1] - idx = sts[v] - return term(getindex, h(history_arg, time), idx, type = Real) # BIG BIG HACK - elseif iscall(expr) - return maketerm(typeof(expr), - operation(expr), - map(x -> delay_to_function(x, iv, sts, ps, h; history_arg), arguments(expr)), - metadata(expr)) - else - return expr - end -end - -function calculate_massmatrix(sys::AbstractODESystem; simplify = false) - eqs = [eq for eq in equations(sys)] - M = zeros(length(eqs), length(eqs)) - for (i, eq) in enumerate(eqs) - if iscall(eq.lhs) && operation(eq.lhs) isa Differential - st = var_from_nested_derivative(eq.lhs)[1] - j = variable_index(sys, st) - M[i, j] = 1 - else - _iszero(eq.lhs) || - error("Only semi-explicit constant mass matrices are currently supported. Faulty equation: $eq.") - end - end - M = simplify ? ModelingToolkit.simplify.(M) : M - # M should only contain concrete numbers - M == I ? I : M -end - -function jacobian_sparsity(sys::AbstractODESystem) - sparsity = torn_system_jacobian_sparsity(sys) - sparsity === nothing || return sparsity - - jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in unknowns(sys)]) -end - -function jacobian_dae_sparsity(sys::AbstractODESystem) - J1 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in unknowns(sys)]) - derivatives = Differential(get_iv(sys)).(unknowns(sys)) - J2 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in derivatives]) - J1 + J2 -end - -function W_sparsity(sys::AbstractODESystem) - jac_sparsity = jacobian_sparsity(sys) - (n, n) = size(jac_sparsity) - M = calculate_massmatrix(sys) - M_sparsity = M isa UniformScaling ? sparse(I(n)) : - SparseMatrixCSC{Bool, Int64}((!iszero).(M)) - jac_sparsity .| M_sparsity -end - -function isautonomous(sys::AbstractODESystem) - tgrad = calculate_tgrad(sys; simplify = true) - all(iszero, tgrad) -end - -""" -```julia -DiffEqBase.ODEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, tgrad = false, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create an `ODEFunction` from the [`ODESystem`](@ref). The arguments `dvs` and `ps` -are used to set the order of the dependent variable and parameter vectors, -respectively. -""" -function DiffEqBase.ODEFunction(sys::AbstractODESystem, args...; kwargs...) - ODEFunction{true}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEFunction{true}(sys::AbstractODESystem, args...; - kwargs...) - ODEFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEFunction{false}(sys::AbstractODESystem, args...; - kwargs...) - ODEFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, - dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, tgrad = false, - jac = false, p = nothing, - t = nothing, - eval_expression = false, - sparse = false, simplify = false, - eval_module = @__MODULE__, - steady_state = false, - checkbounds = false, - sparsity = false, - analytic = nothing, - split_idxs = nothing, - initialization_data = nothing, - cse = true, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunction`") - end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, checkbounds = checkbounds, cse, - kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) - - if specialize === SciMLBase.FunctionWrapperSpecialize && iip - if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") - end - f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) - end - - if tgrad - tgrad_gen = generate_tgrad(sys, dvs, ps; - simplify = simplify, - expression = Val{true}, - expression_module = eval_module, cse, - checkbounds = checkbounds, kwargs...) - tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) - _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) - else - _tgrad = nothing - end - - if jac - jac_gen = generate_jacobian(sys, dvs, ps; - simplify = simplify, sparse = sparse, - expression = Val{true}, - expression_module = eval_module, cse, - checkbounds = checkbounds, kwargs...) - jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - - _jac = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(jac_oop, jac_iip) - else - _jac = nothing - end - - M = calculate_massmatrix(sys) - - _M = if sparse && !(u0 === nothing || M === I) - SparseArrays.sparse(M) - elseif u0 === nothing || M === I - M - else - ArrayInterface.restructure(u0 .* u0', M) - end - - observedfun = ObservedFunctionCache( - sys; steady_state, eval_expression, eval_module, checkbounds, cse) - - if sparse - uElType = u0 === nothing ? Float64 : eltype(u0) - W_prototype = similar(W_sparsity(sys), uElType) - else - W_prototype = nothing - end - - @set! sys.split_idxs = split_idxs - - ODEFunction{iip, specialize}(f; - sys = sys, - jac = _jac === nothing ? nothing : _jac, - tgrad = _tgrad === nothing ? nothing : _tgrad, - mass_matrix = _M, - jac_prototype = W_prototype, - observed = observedfun, - sparsity = sparsity ? W_sparsity(sys) : nothing, - analytic = analytic, - initialization_data) -end - -""" -```julia -DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, tgrad = false, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create an `DAEFunction` from the [`ODESystem`](@ref). The arguments `dvs` and -`ps` are used to set the order of the dependent variable and parameter vectors, -respectively. -""" -function DiffEqBase.DAEFunction(sys::AbstractODESystem, args...; kwargs...) - DAEFunction{true}(sys, args...; kwargs...) -end - -function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - ddvs = map(Base.Fix2(diff2term, get_iv(sys)) ∘ Differential(get_iv(sys)), dvs), - version = nothing, p = nothing, - jac = false, - eval_expression = false, - sparse = false, simplify = false, - eval_module = @__MODULE__, - checkbounds = false, - initialization_data = nothing, - cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEFunction`") - end - f_gen = generate_function(sys, dvs, ps; implicit_dae = true, - expression = Val{true}, cse, - expression_module = eval_module, checkbounds = checkbounds, - kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) - - if jac - jac_gen = generate_dae_jacobian(sys, dvs, ps; - simplify = simplify, sparse = sparse, - expression = Val{true}, - expression_module = eval_module, cse, - checkbounds = checkbounds, kwargs...) - jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - - _jac = GeneratedFunctionWrapper{(3, 5, is_split(sys))}(jac_oop, jac_iip) - else - _jac = nothing - end - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - jac_prototype = if sparse - uElType = u0 === nothing ? Float64 : eltype(u0) - if jac - J1 = calculate_jacobian(sys, sparse = sparse) - derivatives = Differential(get_iv(sys)).(unknowns(sys)) - J2 = calculate_jacobian(sys; sparse = sparse, dvs = derivatives) - similar(J1 + J2, uElType) - else - similar(jacobian_dae_sparsity(sys), uElType) - end - else - nothing - end - - DAEFunction{iip}(f; - sys = sys, - jac = _jac === nothing ? nothing : _jac, - jac_prototype = jac_prototype, - observed = observedfun, - initialization_data) -end - -function DiffEqBase.DDEFunction(sys::AbstractODESystem, args...; kwargs...) - DDEFunction{true}(sys, args...; kwargs...) -end - -function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - eval_expression = false, - eval_module = @__MODULE__, - checkbounds = false, - initialization_data = nothing, - cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `DDEFunction`") - end - f_gen = generate_function(sys, dvs, ps; isdde = true, - expression = Val{true}, - expression_module = eval_module, checkbounds = checkbounds, - cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) - - DDEFunction{iip}(f; sys = sys, initialization_data) -end - -function DiffEqBase.SDDEFunction(sys::AbstractODESystem, args...; kwargs...) - SDDEFunction{true}(sys, args...; kwargs...) -end - -function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - eval_expression = false, - eval_module = @__MODULE__, - checkbounds = false, - initialization_data = nothing, - cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `SDDEFunction`") - end - f_gen = generate_function(sys, dvs, ps; isdde = true, - expression = Val{true}, - expression_module = eval_module, checkbounds = checkbounds, - cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) - - g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, - isdde = true, cse, kwargs...) - g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) - g = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(g_oop, g_iip) - - SDDEFunction{iip}(f, g; sys = sys, initialization_data) -end - -""" -```julia -ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, tgrad = false, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create a Julia expression for an `ODEFunction` from the [`ODESystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct ODEFunctionExpr{iip, specialize} end - -function ODEFunctionExpr{iip, specialize}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, tgrad = false, - jac = false, p = nothing, - linenumbers = false, - sparse = false, simplify = false, - steady_state = false, - sparsity = false, - observedfun_exp = nothing, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunctionExpr`") - end - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - - fsym = gensym(:f) - _f = :($fsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})($f_oop, $f_iip)) - tgradsym = gensym(:tgrad) - if tgrad - tgrad_oop, tgrad_iip = generate_tgrad(sys, dvs, ps; - simplify = simplify, - expression = Val{true}, kwargs...) - _tgrad = :($tgradsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})( - $tgrad_oop, $tgrad_iip)) - else - _tgrad = :($tgradsym = nothing) - end - - jacsym = gensym(:jac) - if jac - jac_oop, jac_iip = generate_jacobian(sys, dvs, ps; - sparse = sparse, simplify = simplify, - expression = Val{true}, kwargs...) - _jac = :($jacsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})( - $jac_oop, $jac_iip)) - else - _jac = :($jacsym = nothing) - end - - Msym = gensym(:M) - M = calculate_massmatrix(sys) - if sparse && !(u0 === nothing || M === I) - _M = :($Msym = $(SparseArrays.sparse(M))) - elseif u0 === nothing || M === I - _M = :($Msym = $M) - else - _M = :($Msym = $(ArrayInterface.restructure(u0 .* u0', M))) - end - - jp_expr = sparse ? :($similar($(get_jac(sys)[]), Float64)) : :nothing - ex = quote - let $_f, $_tgrad, $_jac, $_M - ODEFunction{$iip, $specialize}($fsym, - sys = $sys, - jac = $jacsym, - tgrad = $tgradsym, - mass_matrix = $Msym, - jac_prototype = $jp_expr, - sparsity = $(sparsity ? jacobian_sparsity(sys) : nothing), - observed = $observedfun_exp) - end - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function ODEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) - ODEFunctionExpr{true}(sys, args...; kwargs...) -end - -function ODEFunctionExpr{true}(sys::AbstractODESystem, args...; kwargs...) - return ODEFunctionExpr{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function ODEFunctionExpr{false}(sys::AbstractODESystem, args...; kwargs...) - return ODEFunctionExpr{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -""" -```julia -DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, tgrad = false, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create a Julia expression for an `ODEFunction` from the [`ODESystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct DAEFunctionExpr{iip} end - -function DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, tgrad = false, - jac = false, p = nothing, - linenumbers = false, - sparse = false, simplify = false, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `DAEFunctionExpr`") - end - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, - implicit_dae = true, kwargs...) - fsym = gensym(:f) - _f = :($fsym = $(GeneratedFunctionWrapper{(3, 4, is_split(sys))})($f_oop, $f_iip)) - ex = quote - $_f - ODEFunction{$iip}($fsym) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function DAEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) - DAEFunctionExpr{true}(sys, args...; kwargs...) -end - -struct SymbolicTstops{F} - fn::F -end - -function (st::SymbolicTstops)(p, tspan) - unique!(sort!(reduce(vcat, st.fn(p, tspan...)))) -end - -function SymbolicTstops( - sys::AbstractSystem; eval_expression = false, eval_module = @__MODULE__) - tstops = symbolic_tstops(sys) - isempty(tstops) && return nothing - t0 = gensym(:t0) - t1 = gensym(:t1) - tstops = map(tstops) do val - if is_array_of_symbolics(val) || val isa AbstractArray - collect(val) - else - term(:, t0, unwrap(val), t1; type = AbstractArray{Real}) - end - end - rps = reorder_parameters(sys) - tstops, _ = build_function_wrapper(sys, tstops, - rps..., - t0, - t1; - expression = Val{true}, - p_start = 1, p_end = length(rps), add_observed = false, force_SA = true) - tstops = eval_or_rgf(tstops; eval_expression, eval_module) - tstops = GeneratedFunctionWrapper{(1, 3, is_split(sys))}(tstops, nothing) - return SymbolicTstops(tstops) -end - -""" -```julia -DiffEqBase.ODEProblem{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - allow_cost = false, - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - simplify = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates an ODEProblem from an ODESystem and allows for automatically -symbolically calculating numerical enhancements. -""" -function DiffEqBase.ODEProblem(sys::AbstractODESystem, args...; kwargs...) - ODEProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEProblem(sys::AbstractODESystem, - u0map::StaticArray, - args...; - kwargs...) - ODEProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) -end - -function DiffEqBase.ODEProblem{true}(sys::AbstractODESystem, args...; kwargs...) - ODEProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEProblem{false}(sys::AbstractODESystem, args...; kwargs...) - ODEProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], - tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - allow_cost = false, - callback = nothing, - check_length = true, - warn_initialize_determined = true, - eval_expression = false, - eval_module = @__MODULE__, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") - end - - if !isnothing(get_constraintsystem(sys)) - error("An ODESystem with constraints cannot be used to construct a regular ODEProblem. - Consider a BVProblem instead.") - end - - if !isempty(get_costs(sys)) && !allow_cost - error("ODEProblem will not optimize solutions of ODESystems that have associated cost functions. - Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. - to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") - end - - f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) - cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - - kwargs = filter_kwargs(kwargs) - pt = something(get_metadata(sys), StandardODEProblem()) - - kwargs1 = (;) - if cbs !== nothing - kwargs1 = merge(kwargs1, (callback = cbs,)) - end - - tstops = SymbolicTstops(sys; eval_expression, eval_module) - if tstops !== nothing - kwargs1 = merge(kwargs1, (; tstops)) - end - - # Call `remake` so it runs initialization if it is trivial - return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...)) -end -get_callback(prob::ODEProblem) = prob.kwargs[:callback] - -""" -```julia -SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - constraints = nothing, guesses = nothing, - version = nothing, tgrad = false, - jac = true, sparse = true, - simplify = false, - kwargs...) where {iip} -``` - -Create a boundary value problem from the [`ODESystem`](@ref). - -`u0map` is used to specify fixed initial values for the states. Every variable -must have either an initial guess supplied using `guesses` or a fixed initial -value specified using `u0map`. - -Boundary value conditions are supplied to ODESystems -in the form of a ConstraintsSystem. These equations -should specify values that state variables should -take at specific points, as in `x(0.5) ~ 1`). More general constraints that -should hold over the entire solution, such as `x(t)^2 + y(t)^2`, should be -specified as one of the equations used to build the `ODESystem`. - -If an ODESystem without `constraints` is specified, it will be treated as an initial value problem. - -```julia - @parameters g t_c = 0.5 - @variables x(..) y(t) λ(t) - eqs = [D(D(x(t))) ~ λ * x(t) - D(D(y)) ~ λ * y - g - x(t)^2 + y^2 ~ 1] - cstr = [x(0.5) ~ 1] - @mtkbuild pend = ODESystem(eqs, t; constraints = cstrs) - - tspan = (0.0, 1.5) - u0map = [x(t) => 0.6, y => 0.8] - parammap = [g => 1] - guesses = [λ => 1] - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) -``` - -If the `ODESystem` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting -`BVProblem` must be solved using BVDAE solvers, such as Ascher. -""" -function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) - BVProblem{true}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem(sys::AbstractODESystem, - u0map::StaticArray, - args...; - kwargs...) - BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) -end - -function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) - BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) - BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], - tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - guesses = Dict(), - allow_cost = false, - version = nothing, tgrad = false, - callback = nothing, - check_length = true, - warn_initialize_determined = true, - eval_expression = false, - eval_module = @__MODULE__, - cse = true, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") - end - !isnothing(callback) && error("BVP solvers do not support callbacks.") - - if !isempty(get_costs(sys)) && !allow_cost - error("BVProblem will not optimize solutions of ODESystems that have associated cost functions. - Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. - to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") - end - - has_alg_eqs(sys) && - error("The BVProblem constructor currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. - - sts = unknowns(sys) - ps = parameters(sys) - constraintsys = get_constraintsystem(sys) - - if !isnothing(constraintsys) - (length(constraints(constraintsys)) + length(u0map) > length(sts)) && - @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." - end - - # ODESystems without algebraic equations should use both fixed values + guesses - # for initialization. - _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) - f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, _u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, guesses, - check_length, warn_initialize_determined, eval_expression, eval_module, cse, kwargs...) - - stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k, v) in u0map] - - fns = generate_function_bc(sys, u0, u0_idxs, tspan; cse) - bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) - bc(sol, p, t) = bc_oop(sol, p, t) - bc(resid, u, p, t) = bc_iip(resid, u, p, t) - - return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) -end - -get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") - -""" - generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan) - - Given an ODESystem with constraints, generate the boundary condition function to pass to boundary value problem solvers. - Expression uses the constraints and the provided initial conditions. -""" -function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan; kwargs...) - iv = get_iv(sys) - sts = unknowns(sys) - ps = parameters(sys) - np = length(ps) - ns = length(sts) - stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) - - @variables sol(..)[1:ns] - - conssys = get_constraintsystem(sys) - cons = Any[] - if !isnothing(conssys) - cons = [con.lhs - con.rhs for con in constraints(conssys)] - - for st in get_unknowns(conssys) - x = operation(st) - t = only(arguments(st)) - idx = stidxmap[x(iv)] - - cons = map(c -> Symbolics.substitute(c, Dict(x(t) => sol(t)[idx])), cons) - end - end - - init_conds = Any[] - for i in u0_idxs - expr = sol(tspan[1])[i] - u0[i] - push!(init_conds, expr) - end - - exprs = vcat(init_conds, cons) - _p = reorder_parameters(sys, ps) - - build_function_wrapper(sys, exprs, sol, _p..., iv; output_type = Array, kwargs...) -end - -""" -```julia -DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - simplify = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a DAEProblem from an ODESystem and allows for automatically -symbolically calculating numerical enhancements. - -Note: Solvers for DAEProblems like DFBDF, DImplicitEuler, DABDF2 are -generally slower than the ones for ODEProblems. We recommend trying -ODEProblem and its solvers for your problem first. -""" -function DiffEqBase.DAEProblem(sys::AbstractODESystem, args...; kwargs...) - DAEProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - allow_cost = false, - warn_initialize_determined = true, - check_length = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`.") - end - - if !isempty(get_costs(sys)) && !allow_cost - error("DAEProblem will not optimize solutions of ODESystems that have associated cost functions. - Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. - to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") - end - - f, du0, u0, p = process_SciMLProblem(DAEFunction{iip}, sys, u0map, parammap; - implicit_dae = true, du0map = du0map, check_length, - t = tspan !== nothing ? tspan[1] : tspan, - warn_initialize_determined, kwargs...) - diffvars = collect_differential_variables(sys) - sts = unknowns(sys) - differential_vars = map(Base.Fix2(in, diffvars), sts) - kwargs = filter_kwargs(kwargs) - - kwargs1 = (;) - - tstops = SymbolicTstops(sys; eval_expression, eval_module) - if tstops !== nothing - kwargs1 = merge(kwargs1, (; tstops)) - end - - # Call `remake` so it runs initialization if it is trivial - return remake(DAEProblem{iip}( - f, du0, u0, tspan, p; differential_vars = differential_vars, - kwargs..., kwargs1...)) -end - -function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) - p = reorder_parameters(sys) - build_function_wrapper( - sys, u0, p..., get_iv(sys); expression, p_start = 1, p_end = length(p), - similarto = typeof(u0), wrap_delays = false, kwargs...) -end - -function DiffEqBase.DDEProblem(sys::AbstractODESystem, args...; kwargs...) - DDEProblem{true}(sys, args...; kwargs...) -end -function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], - tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - callback = nothing, - check_length = true, - eval_expression = false, - eval_module = @__MODULE__, - u0_constructor = identity, - cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DDEProblem`") - end - f, u0, p = process_SciMLProblem(DDEFunction{iip}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, u0_constructor, cse, - check_length, eval_expression, eval_module, kwargs...) - h_gen = generate_history(sys, u0; expression = Val{true}, cse) - h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) - h = h_oop - u0 = float.(h(p, tspan[1])) - if u0 !== nothing - u0 = u0_constructor(u0) - end - - cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - kwargs = filter_kwargs(kwargs) - - kwargs1 = (;) - if cbs !== nothing - kwargs1 = merge(kwargs1, (callback = cbs,)) - end - # Call `remake` so it runs initialization if it is trivial - return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...)) -end - -function DiffEqBase.SDDEProblem(sys::AbstractODESystem, args...; kwargs...) - SDDEProblem{true}(sys, args...; kwargs...) -end -function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], - tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - callback = nothing, - check_length = true, - sparsenoise = nothing, - eval_expression = false, - eval_module = @__MODULE__, - u0_constructor = identity, - cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SDDEProblem`") - end - f, u0, p = process_SciMLProblem(SDDEFunction{iip}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, eval_expression, eval_module, u0_constructor, - check_length, cse, kwargs...) - h_gen = generate_history(sys, u0; expression = Val{true}, cse) - h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) - h = h_oop - u0 = h(p, tspan[1]) - if u0 !== nothing - u0 = u0_constructor(u0) - end - - cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - kwargs = filter_kwargs(kwargs) - - kwargs1 = (;) - if cbs !== nothing - kwargs1 = merge(kwargs1, (callback = cbs,)) - end - - noiseeqs = get_noiseeqs(sys) - sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) - if noiseeqs isa AbstractVector - noise_rate_prototype = nothing - elseif sparsenoise - I, J, V = findnz(SparseArrays.sparse(noiseeqs)) - noise_rate_prototype = SparseArrays.sparse(I, J, zero(eltype(u0))) - else - noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) - end - # Call `remake` so it runs initialization if it is trivial - return remake(SDDEProblem{iip}(f, f.g, u0, h, tspan, p; - noise_rate_prototype = - noise_rate_prototype, kwargs1..., kwargs...)) -end - -""" -```julia -ODEProblemExpr{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - linenumbers = true, parallel = SerialForm(), - skipzeros = true, fillzeros = true, - simplify = false, - kwargs...) where {iip} -``` - -Generates a Julia expression for constructing an ODEProblem from an -ODESystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct ODEProblemExpr{iip} end - -function ODEProblemExpr{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); check_length = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `ODEProblemExpr`") - end - f, u0, p = process_SciMLProblem( - ODEFunctionExpr{iip}, sys, u0map, parammap; check_length, - t = tspan !== nothing ? tspan[1] : tspan, - kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - kwargs = filter_kwargs(kwargs) - kwarg_params = gen_quoted_kwargs(kwargs) - odep = Expr(:call, :ODEProblem, kwarg_params, :f, :u0, :tspan, :p) - ex = quote - f = $f - u0 = $u0 - tspan = $tspan - p = $p - $odep - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function ODEProblemExpr(sys::AbstractODESystem, args...; kwargs...) - ODEProblemExpr{true}(sys, args...; kwargs...) -end - -""" -```julia -DAEProblemExpr{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - linenumbers = true, parallel = SerialForm(), - skipzeros = true, fillzeros = true, - simplify = false, - kwargs...) where {iip} -``` - -Generates a Julia expression for constructing a DAEProblem from an -ODESystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct DAEProblemExpr{iip} end - -function DAEProblemExpr{iip}(sys::AbstractODESystem, du0map, u0map, tspan, - parammap = DiffEqBase.NullParameters(); check_length = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblemExpr`") - end - f, du0, u0, p = process_SciMLProblem(DAEFunctionExpr{iip}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - implicit_dae = true, du0map = du0map, check_length, - kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - diffvars = collect_differential_variables(sys) - sts = unknowns(sys) - differential_vars = map(Base.Fix2(in, diffvars), sts) - kwargs = filter_kwargs(kwargs) - kwarg_params = gen_quoted_kwargs(kwargs) - push!(kwarg_params.args, Expr(:kw, :differential_vars, :differential_vars)) - prob = Expr(:call, :(DAEProblem{$iip}), kwarg_params, :f, :du0, :u0, :tspan, :p) - ex = quote - f = $f - u0 = $u0 - du0 = $du0 - tspan = $tspan - p = $p - differential_vars = $differential_vars - $prob - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function DAEProblemExpr(sys::AbstractODESystem, args...; kwargs...) - DAEProblemExpr{true}(sys, args...; kwargs...) -end - -""" -```julia -SciMLBase.SteadyStateProblem(sys::AbstractODESystem, u0map, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates an SteadyStateProblem from an ODESystem and allows for automatically -symbolically calculating numerical enhancements. -""" -function SciMLBase.SteadyStateProblem(sys::AbstractODESystem, args...; kwargs...) - SteadyStateProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.SteadyStateProblem{iip}(sys::AbstractODESystem, u0map, - parammap = SciMLBase.NullParameters(); - check_length = true, kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SteadyStateProblem`") - end - f, u0, p = process_SciMLProblem(ODEFunction{iip}, sys, u0map, parammap; - steady_state = true, - check_length, force_initialization_time_independent = true, kwargs...) - kwargs = filter_kwargs(kwargs) - SteadyStateProblem{iip}(f, u0, p; kwargs...) -end - -""" -```julia -SciMLBase.SteadyStateProblemExpr(sys::AbstractODESystem, u0map, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - skipzeros = true, fillzeros = true, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a Julia expression for building a SteadyStateProblem from -an ODESystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct SteadyStateProblemExpr{iip} end - -function SteadyStateProblemExpr{iip}(sys::AbstractODESystem, u0map, - parammap = SciMLBase.NullParameters(); - check_length = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SteadyStateProblemExpr`") - end - f, u0, p = process_SciMLProblem(ODEFunctionExpr{iip}, sys, u0map, parammap; - steady_state = true, - check_length, kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - kwargs = filter_kwargs(kwargs) - kwarg_params = gen_quoted_kwargs(kwargs) - prob = Expr(:call, :SteadyStateProblem, kwarg_params, :f, :u0, :p) - ex = quote - f = $f - u0 = $u0 - p = $p - $prob - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function SteadyStateProblemExpr(sys::AbstractODESystem, args...; kwargs...) - SteadyStateProblemExpr{true}(sys, args...; kwargs...) -end - -function _match_eqs(eqs1, eqs2) - eqpairs = Pair[] - for (i, eq) in enumerate(eqs1) - for (j, eq2) in enumerate(eqs2) - if isequal(eq, eq2) - push!(eqpairs, i => j) - break - end - end - end - eqpairs -end - -function isisomorphic(sys1::AbstractODESystem, sys2::AbstractODESystem) - sys1 = flatten(sys1) - sys2 = flatten(sys2) - - iv2 = only(independent_variables(sys2)) - sys1 = convert_system(ODESystem, sys1, iv2) - s1, s2 = unknowns(sys1), unknowns(sys2) - p1, p2 = parameters(sys1), parameters(sys2) - - (length(s1) != length(s2)) || (length(p1) != length(p2)) && return false - - eqs1 = equations(sys1) - eqs2 = equations(sys2) - - pps = permutations(p2) - psts = permutations(s2) - orig = [p1; s1] - perms = ([x; y] for x in pps for y in psts) - - for perm in perms - rules = Dict(orig .=> perm) - neweqs1 = substitute(eqs1, rules) - eqpairs = _match_eqs(neweqs1, eqs2) - if length(eqpairs) == length(eqs1) - return true - end - end - return false -end - -function flatten_equations(eqs) - mapreduce(vcat, eqs; init = Equation[]) do eq - islhsarr = eq.lhs isa AbstractArray || Symbolics.isarraysymbolic(eq.lhs) - isrhsarr = eq.rhs isa AbstractArray || Symbolics.isarraysymbolic(eq.rhs) - if islhsarr || isrhsarr - islhsarr && isrhsarr || - error("LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must either both be array expressions or both scalar") - size(eq.lhs) == size(eq.rhs) || - error("Size of LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must match: got $(size(eq.lhs)) and $(size(eq.rhs))") - return vec(collect(eq.lhs) .~ collect(eq.rhs)) - else - eq - end - end -end - -struct InitializationProblem{iip, specialization} end - -""" -```julia -InitializationProblem{iip}(sys::AbstractODESystem, t, u0map, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - simplify = false, - linenumbers = true, parallel = SerialForm(), - initialization_eqs = [], - fully_determined = false, - kwargs...) where {iip} -``` - -Generates a NonlinearProblem or NonlinearLeastSquaresProblem from an ODESystem -which represents the initialization, i.e. the calculation of the consistent -initial conditions for the given DAE. -""" -function InitializationProblem(sys::AbstractSystem, args...; kwargs...) - InitializationProblem{true}(sys, args...; kwargs...) -end - -function InitializationProblem(sys::AbstractSystem, t, - u0map::StaticArray, - args...; - kwargs...) - InitializationProblem{false, SciMLBase.FullSpecialize}( - sys, t, u0map, args...; kwargs...) -end - -function InitializationProblem{true}(sys::AbstractSystem, args...; kwargs...) - InitializationProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function InitializationProblem{false}(sys::AbstractSystem, args...; kwargs...) - InitializationProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -const INCOMPLETE_INITIALIZATION_MESSAGE = """ - Initialization incomplete. Not all of the state variables of the - DAE system can be determined by the initialization. Missing - variables: - """ - -struct IncompleteInitializationError <: Exception - uninit::Any -end - -function Base.showerror(io::IO, e::IncompleteInitializationError) - println(io, INCOMPLETE_INITIALIZATION_MESSAGE) - println(io, e.uninit) -end - -function InitializationProblem{iip, specialize}(sys::AbstractSystem, - t, u0map = [], - parammap = DiffEqBase.NullParameters(); - guesses = [], - check_length = true, - warn_initialize_determined = true, - initialization_eqs = [], - fully_determined = nothing, - check_units = true, - use_scc = true, - allow_incomplete = false, - force_time_independent = false, - algebraic_only = false, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") - end - if isempty(u0map) && get_initializesystem(sys) !== nothing - isys = get_initializesystem(sys; initialization_eqs, check_units) - simplify_system = false - elseif isempty(u0map) && get_initializesystem(sys) === nothing - isys = generate_initializesystem( - sys; initialization_eqs, check_units, pmap = parammap, - guesses, extra_metadata = (; use_scc), algebraic_only) - simplify_system = true - else - isys = generate_initializesystem( - sys; u0map, initialization_eqs, check_units, - pmap = parammap, guesses, extra_metadata = (; use_scc), algebraic_only) - simplify_system = true - end - - # useful for `SteadyStateProblem` since `f` has to be autonomous and the - # initialization should be too - if force_time_independent - idx = findfirst(isequal(get_iv(sys)), get_ps(isys)) - idx === nothing || deleteat!(get_ps(isys), idx) - end - - if simplify_system - isys = structural_simplify(isys; fully_determined) - end - - meta = get_metadata(isys) - if meta isa InitializationSystemMetadata - @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob( - sys, isys) - end - - ts = get_tearing_state(isys) - unassigned_vars = StructuralTransformations.singular_check(ts) - if warn_initialize_determined && !isempty(unassigned_vars) - errmsg = """ - The initialization system is structurally singular. Guess values may \ - significantly affect the initial values of the ODE. The problematic variables \ - are $unassigned_vars. - - Note that the identification of problematic variables is a best-effort heuristic. - """ - @warn errmsg - end - - uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) - - # TODO: throw on uninitialized arrays - filter!(x -> !(x isa Symbolics.Arr), uninit) - if is_time_dependent(sys) && !isempty(uninit) - allow_incomplete || throw(IncompleteInitializationError(uninit)) - # for incomplete initialization, we will add the missing variables as parameters. - # they will be updated by `update_initializeprob!` and `initializeprobmap` will - # use them to construct the new `u0`. - newparams = map(toparam, uninit) - append!(get_ps(isys), newparams) - isys = complete(isys) - end - - neqs = length(equations(isys)) - nunknown = length(unknowns(isys)) - - if use_scc - scc_message = "`SCCNonlinearProblem` can only be used for initialization of fully determined systems and hence will not be used here. " - else - scc_message = "" - end - - if warn_initialize_determined && neqs > nunknown - @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" - end - if warn_initialize_determined && neqs < nunknown - @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" - end - - parammap = recursive_unwrap(anydict(parammap)) - if t !== nothing - parammap[get_iv(sys)] = t - end - filter!(kvp -> kvp[2] !== missing, parammap) - - u0map = to_varmap(u0map, unknowns(sys)) - if isempty(guesses) - guesses = Dict() - end - - filter_missing_values!(u0map) - filter_missing_values!(parammap) - u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), u0map) - - TProb = if neqs == nunknown && isempty(unassigned_vars) - if use_scc && neqs > 0 - if is_split(isys) - SCCNonlinearProblem - else - @warn "`SCCNonlinearProblem` can only be used with `split = true` systems. Simplify your `ODESystem` with `split = true` or pass `use_scc = false` to disable this warning" - NonlinearProblem - end - else - NonlinearProblem - end - else - NonlinearLeastSquaresProblem - end - TProb(isys, u0map, parammap; kwargs..., - build_initializeprob = false, is_initializeprob = true) -end diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index a08c83ffb6..b80ba3a820 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -21,7 +21,7 @@ using ModelingToolkit, OrdinaryDiffEq @variables x(t) y(t) D = Differential(t) eqs = [D(x) ~ α*x - β*x*y, D(y) ~ -δ*y + γ*x*y] -@named sys = ODESystem(eqs, t) +@named sys = System(eqs, t) sys2 = liouville_transform(sys) sys2 = complete(sys2) @@ -40,14 +40,14 @@ Optimal Transport Approach Abhishek Halder, Kooktae Lee, and Raktim Bhattacharya https://abhishekhalder.bitbucket.io/F16ACC2013Final.pdf """ -function liouville_transform(sys::AbstractODESystem; kwargs...) +function liouville_transform(sys::System; kwargs...) t = get_iv(sys) @variables trJ - D = ModelingToolkit.Differential(t) + D = Differential(t) neweq = D(trJ) ~ trJ * -tr(calculate_jacobian(sys)) neweqs = [equations(sys); neweq] vars = [unknowns(sys); trJ] - ODESystem( + System( neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs... ) @@ -55,7 +55,7 @@ end """ change_independent_variable( - sys::AbstractODESystem, iv, eqs = []; + sys::System, iv, eqs = []; add_old_diff = false, simplify = true, fold = false ) @@ -95,7 +95,7 @@ By changing the independent variable, it can be reformulated for vertical positi ```julia julia> @variables x(t) y(t); -julia> @named M = ODESystem([D(D(y)) ~ -9.81, D(D(x)) ~ 0.0], t); +julia> @named M = System([D(D(y)) ~ -9.81, D(D(x)) ~ 0.0], t); julia> M = change_independent_variable(M, x); @@ -109,7 +109,7 @@ julia> unknowns(M) ``` """ function change_independent_variable( - sys::AbstractODESystem, iv, eqs = []; + sys::System, iv, eqs = []; add_old_diff = false, simplify = true, fold = false ) iv2_of_iv1 = unwrap(iv) # e.g. u(t) @@ -166,7 +166,7 @@ function change_independent_variable( end # Use the utility function to transform everything in the system! - function transform(sys::AbstractODESystem) + function transform(sys::System) eqs = map(transform, get_eqs(sys)) unknowns = map(transform, get_unknowns(sys)) unknowns = filter(var -> !isequal(var, iv2), unknowns) # remove e.g. u @@ -196,3 +196,168 @@ function change_independent_variable( end return transform(sys) end + +""" +$(TYPEDSIGNATURES) + +Choose correction_factor=-1//2 (1//2) to convert Ito -> Stratonovich (Stratonovich->Ito). +""" +function stochastic_integral_transform(sys::System, correction_factor) + if !isempty(get_systems(sys)) + throw(ArgumentError("The system must be flattened.")) + end + if get_noise_eqs(sys) === nothing + throw(ArgumentError(""" + `$stochastic_integral_transform` expects a system with noise_eqs. If your \ + noise is specified using brownian variables, consider calling \ + `structural_simplify`. + """)) + end + name = nameof(sys) + noise_eqs = get_noise_eqs(sys) + eqs = equations(sys) + dvs = unknowns(sys) + ps = parameters(sys) + # use the general interface + if noise_eqs isa Vector + _eqs = reduce(vcat, [eqs[i].lhs ~ noise_eqs[i] for i in eachindex(dvs)]) + de = System(_eqs, get_iv(sys), dvs, ps, name = name, checks = false) + + jac = calculate_jacobian(de, sparse = false, simplify = false) + ∇σσ′ = simplify.(jac * noise_eqs) + else + dimunknowns, m = size(noise_eqs) + _eqs = reduce(vcat, [eqs[i].lhs ~ noise_eqs[i] for i in eachindex(dvs)]) + de = System(_eqs, get_iv(sys), dvs, ps, name = name, checks = false) + + jac = calculate_jacobian(de, sparse = false, simplify = false) + ∇σσ′ = simplify.(jac * noise_eqs[:, 1]) + for k in 2:m + __eqs = reduce(vcat, + [eqs[i].lhs ~ noise_eqs[Int(i + (k - 1) * dimunknowns)] + for i in eachindex(dvs)]) + de = System(__eqs, get_iv(sys), dvs, dvs, name = name, checks = false) + + jac = calculate_jacobian(de, sparse = false, simplify = false) + ∇σσ′ = ∇σσ′ + simplify.(jac * noise_eqs[:, k]) + end + end + deqs = reduce(vcat, + [eqs[i].lhs ~ eqs[i].rhs + correction_factor * ∇σσ′[i] for i in eachindex(dvs)]) + + return @set sys.eqs = deqs +end + +""" +$(TYPEDSIGNATURES) + +Measure transformation method that allows for a reduction in the variance of an estimator `Exp(g(X_t))`. +Input: Original SDE system and symbolic function `u(t,x)` with scalar output that + defines the adjustable parameters `d` in the Girsanov transformation. Optional: initial + condition for `θ0`. +Output: Modified SDESystem with additional component `θ_t` and initial value `θ0`, as well as + the weight `θ_t/θ0` as observed equation, such that the estimator `Exp(g(X_t)θ_t/θ0)` + has a smaller variance. + +Reference: +Kloeden, P. E., Platen, E., & Schurz, H. (2012). Numerical solution of SDE through computer +experiments. Springer Science & Business Media. + +# Example + +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@parameters α β +@variables x(t) y(t) z(t) + +eqs = [D(x) ~ α*x] +noiseeqs = [β*x] + +@named de = SDESystem(eqs,noiseeqs,t,[x],[α,β]) + +# define u (user choice) +u = x +θ0 = 0.1 +g(x) = x[1]^2 +demod = ModelingToolkit.Girsanov_transform(de, u; θ0=0.1) + +u0modmap = [ + x => x0 +] + +parammap = [ + α => 1.5, + β => 1.0 +] + +probmod = SDEProblem(complete(demod),u0modmap,(0.0,1.0),parammap) +ensemble_probmod = EnsembleProblem(probmod; + output_func = (sol,i) -> (g(sol[x,end])*sol[demod.weight,end],false), + ) + +simmod = solve(ensemble_probmod,EM(),dt=dt,trajectories=numtraj) +``` + +""" +function Girsanov_transform(sys::System, u; θ0 = 1.0) + name = nameof(sys) + + # register new variable θ corresponding to 1D correction process θ(t) + t = get_iv(sys) + D = Differential(t) + @variables θ(t), weight(t) + + # determine the adjustable parameters `d` given `u` + # gradient of u with respect to unknowns + grad = Symbolics.gradient(u, unknowns(sys)) + + noiseeqs = copy(get_noise_eqs(sys)) + if noiseeqs isa Vector + d = simplify.(-(noiseeqs .* grad) / u) + drift_correction = noiseeqs .* d + else + d = simplify.(-noiseeqs * grad / u) + drift_correction = noiseeqs * d + end + + eqs = equations(sys) + dvs = unknowns(sys) + # transformation adds additional unknowns θ: newX = (X,θ) + # drift function for unknowns is modified + # θ has zero drift + deqs = reduce( + vcat, [eqs[i].lhs ~ eqs[i].rhs - drift_correction[i] for i in eachindex(dvs)]) + deqsθ = D(θ) ~ 0 + push!(deqs, deqsθ) + + # diffusion matrix is of size d x m (d unknowns, m noise), with diagonal noise represented as a d-dimensional vector + # for diagonal noise processes with m>1, the noise process will become non-diagonal; extra unknown component but no new noise process. + # new diffusion matrix is of size d+1 x M + # diffusion for state is unchanged + + noiseqsθ = θ * d + + if noiseeqs isa Vector + m = size(noiseeqs) + if m == 1 + push!(noiseeqs, noiseqsθ) + else + noiseeqs = [Array(Diagonal(noiseeqs)); noiseqsθ'] + end + else + noiseeqs = [Array(noiseeqs); noiseqsθ'] + end + + unknown_vars = [dvs; θ] + + # return modified SDE System + @set! sys.eqs = deqs + @set! sys.noise_eqs = noiseeqs + @set! sys.unknowns = unknown_vars + get_defaults(sys)[θ] = θ0 + obs = observed(sys) + @set! sys.observed = [weight ~ θ / θ0; obs] + return sys +end diff --git a/src/systems/diffeqs/first_order_transform.jl b/src/systems/diffeqs/first_order_transform.jl index 97fd6460d9..d017ea362b 100644 --- a/src/systems/diffeqs/first_order_transform.jl +++ b/src/systems/diffeqs/first_order_transform.jl @@ -1,10 +1,10 @@ """ $(TYPEDSIGNATURES) -Takes a Nth order ODESystem and returns a new ODESystem written in first order +Takes a Nth order System and returns a new System written in first order form by defining new variables which represent the N-1 derivatives. """ -function ode_order_lowering(sys::ODESystem) +function ode_order_lowering(sys::System) iv = get_iv(sys) eqs_lowered, new_vars = ode_order_lowering(equations(sys), iv, unknowns(sys)) @set! sys.eqs = eqs_lowered @@ -12,7 +12,7 @@ function ode_order_lowering(sys::ODESystem) return sys end -function dae_order_lowering(sys::ODESystem) +function dae_order_lowering(sys::System) iv = get_iv(sys) eqs_lowered, new_vars = dae_order_lowering(equations(sys), iv, unknowns(sys)) @set! sys.eqs = eqs_lowered diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index b2954f81e4..ab13ecca29 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Generate `ODESystem`, dependent variables, and parameters from an `ODEProblem`. +Generate `System`, dependent variables, and parameters from an `ODEProblem`. """ function modelingtoolkitize( prob::DiffEqBase.ODEProblem; u_names = nothing, p_names = nothing, kwargs...) @@ -95,7 +95,7 @@ function modelingtoolkitize( end filter!(x -> !iscall(x) || !(operation(x) isa Initial), params) filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), default_p) - de = ODESystem(eqs, t, sts, params, + de = System(eqs, t, sts, params, defaults = merge(default_u0, default_p); name = gensym(:MTKizedODE), tspan = prob.tspan, diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl deleted file mode 100644 index cbce569a9b..0000000000 --- a/src/systems/diffeqs/odesystem.jl +++ /dev/null @@ -1,858 +0,0 @@ -""" -$(TYPEDEF) - -A system of ordinary differential equations. - -# Fields -$(FIELDS) - -# Example - -```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D - -@parameters σ ρ β -@variables x(t) y(t) z(t) - -eqs = [D(x) ~ σ*(y-x), - D(y) ~ x*(ρ-z)-y, - D(z) ~ x*y - β*z] - -@named de = ODESystem(eqs,t,[x,y,z],[σ,ρ,β],tspan=(0, 1000.0)) -``` -""" -struct ODESystem <: AbstractODESystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The ODEs defining the system.""" - eqs::Vector{Equation} - """Independent variable.""" - iv::BasicSymbolic{Real} - """ - Dependent (unknown) variables. Must not contain the independent variable. - - N.B.: If `torn_matching !== nothing`, this includes all variables. Actual - ODE unknowns are determined by the `SelectedState()` entries in `torn_matching`. - """ - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Control parameters (some subset of `ps`).""" - ctrls::Vector - """Observed variables.""" - observed::Vector{Equation} - """System of constraints that must be satisfied by the solution to the system.""" - constraintsystem::Union{Nothing, ConstraintsSystem} - """A set of expressions defining the costs of the system for optimal control.""" - costs::Vector - """Takes the cost vector and returns a scalar for optimization.""" - consolidate::Union{Nothing, Function} - """ - Time-derivative matrix. Note: this field will not be defined until - [`calculate_tgrad`](@ref) is called on the system. - """ - tgrad::RefValue{Vector{Num}} - """ - Jacobian matrix. Note: this field will not be defined until - [`calculate_jacobian`](@ref) is called on the system. - """ - jac::RefValue{Any} - """ - Control Jacobian matrix. Note: this field will not be defined until - [`calculate_control_jacobian`](@ref) is called on the system. - """ - ctrl_jac::RefValue{Any} - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact::RefValue{Matrix{Num}} - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact_t::RefValue{Matrix{Num}} - """ - The name of the system. - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{ODESystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - Tearing result specifying how to solve the system. - """ - torn_matching::Union{Matching, Nothing} - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - The schedule for the code generation process. - """ - schedule::Any - """ - Type of the system. - """ - connector_type::Any - """ - Inject assignment statements before the evaluation of the RHS function. - """ - preface::Any - """ - A `Vector{SymbolicContinuousCallback}` that model events. - The integrator will use root finding to guarantee that it steps at each zero crossing. - """ - continuous_events::Vector{SymbolicContinuousCallback} - """ - A `Vector{SymbolicDiscreteCallback}` that models events. Symbolic - analog to `SciMLBase.DiscreteCallback` that executes an affect when a given condition is - true at the end of an integration step. - """ - discrete_events::Vector{SymbolicDiscreteCallback} - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Mapping of conditions which should be true throughout the solution process to corresponding error - messages. These will be added to the equations when calling `debug_system`. - """ - assertions::Dict{BasicSymbolic, String} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - A boolean indicating if the given `ODESystem` represents a system of DDEs. - """ - is_dde::Bool - """ - A list of points to provide to the solver as tstops. Uses the same syntax as discrete - events. - """ - tstops::Vector{Any} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - A list of discrete subsystems. - """ - discrete_subsystems::Any - """ - A list of actual unknowns needed to be solved by solvers. - """ - solved_unknowns::Union{Nothing, Vector{Any}} - """ - A vector of vectors of indices for the split parameters. - """ - split_idxs::Union{Nothing, Vector{Vector{Int}}} - """ - The analysis points removed by transformations, representing connections to be - ignored. The first element of the tuple analysis points connecting systems and - the second are ones connecting variables (for the trivial form of `connect`). - """ - ignored_connections::Union{ - Nothing, Tuple{Vector{IgnoredAnalysisPoint}, Vector{IgnoredAnalysisPoint}}} - """ - The hierarchical parent system before simplification. - """ - parent::Any - - function ODESystem( - tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, - observed, constraints, costs, consolidate, tgrad, - jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, - torn_matching, initializesystem, initialization_eqs, schedule, - connector_type, preface, cevents, - devents, parameter_dependencies, assertions = Dict{BasicSymbolic, String}(), - metadata = nothing, gui_metadata = nothing, is_dde = false, - tstops = [], tearing_state = nothing, substitutions = nothing, - namespacing = true, complete = false, index_cache = nothing, - discrete_subsystems = nothing, solved_unknowns = nothing, - split_idxs = nothing, ignored_connections = nothing, parent = nothing; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - check_equations(deqs, iv) - check_equations(equations(cevents), iv) - check_subsystems(systems) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, deqs) - end - new(tag, deqs, iv, dvs, ps, tspan, var_to_name, - ctrls, observed, constraints, costs, consolidate, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, - initializesystem, initialization_eqs, schedule, connector_type, preface, - cevents, devents, parameter_dependencies, assertions, metadata, - gui_metadata, is_dde, tstops, tearing_state, substitutions, namespacing, - complete, index_cache, - discrete_subsystems, solved_unknowns, split_idxs, ignored_connections, parent) - end -end - -function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; - controls = Num[], - observed = Equation[], - constraintsystem = nothing, - costs = Num[], - consolidate = nothing, - systems = ODESystem[], - tspan = nothing, - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - schedule = nothing, - connector_type = nothing, - preface = nothing, - continuous_events = nothing, - discrete_events = nothing, - parameter_dependencies = Equation[], - assertions = Dict(), - checks = true, - metadata = nothing, - gui_metadata = nothing, - is_dde = nothing, - tstops = [], - discover_from_metadata = true) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - @assert all(control -> any(isequal.(control, ps)), controls) "All controls must also be parameters." - iv′ = value(iv) - ps′ = value.(ps) - ctrl′ = value.(controls) - dvs′ = value.(dvs) - dvs′ = filter(x -> !isdelay(x, iv), dvs′) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :ODESystem, force = true) - end - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - let defaults = discover_from_metadata ? defaults : Dict(), - guesses = discover_from_metadata ? guesses : Dict() - - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - end - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - tgrad = RefValue(EMPTY_TGRAD) - jac = RefValue{Any}(EMPTY_JAC) - ctrl_jac = RefValue{Any}(EMPTY_JAC) - Wfact = RefValue(EMPTY_JAC) - Wfact_t = RefValue(EMPTY_JAC) - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - - if is_dde === nothing - is_dde = _check_if_dde(deqs, iv′, systems) - end - - if !isempty(systems) && !isnothing(constraintsystem) - conssystems = ConstraintsSystem[] - for sys in systems - cons = get_constraintsystem(sys) - cons !== nothing && push!(conssystems, cons) - end - @set! constraintsystem.systems = conssystems - end - costs = wrap.(costs) - - if length(costs) > 1 && isnothing(consolidate) - error("Must specify a consolidation function for the costs vector.") - end - - assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) - - ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, - constraintsystem, costs, consolidate, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, - defaults, guesses, nothing, initializesystem, - initialization_eqs, schedule, connector_type, preface, cont_callbacks, - disc_callbacks, parameter_dependencies, assertions, - metadata, gui_metadata, is_dde, tstops, checks = checks) -end - -function ODESystem(eqs, iv; constraints = Equation[], costs = Num[], kwargs...) - diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) - - for eq in get(kwargs, :parameter_dependencies, Equation[]) - collect_vars!(allunknowns, ps, eq, iv) - end - - for ssys in get(kwargs, :systems, ODESystem[]) - collect_scoped_vars!(allunknowns, ps, ssys, iv) - end - - for v in allunknowns - isdelay(v, iv) || continue - collect_vars!(allunknowns, ps, arguments(v)[1], iv) - end - - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - algevars = setdiff(allunknowns, diffvars) - - consvars = OrderedSet() - constraintsystem = nothing - if !isempty(constraints) - constraintsystem = process_constraint_system(constraints, allunknowns, new_ps, iv) - for st in get_unknowns(constraintsystem) - iscall(st) ? - !in(operation(st)(iv), allunknowns) && push!(consvars, st) : - !in(st, allunknowns) && push!(consvars, st) - end - for p in parameters(constraintsystem) - !in(p, new_ps) && push!(new_ps, p) - end - end - - if !isempty(costs) - coststs, costps = process_costs(costs, allunknowns, new_ps, iv) - for p in costps - !in(p, new_ps) && push!(new_ps, p) - end - end - costs = wrap.(costs) - - return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars, consvars))), - collect(new_ps); constraintsystem, costs, kwargs...) -end - -# NOTE: equality does not check cached Jacobian -function Base.:(==)(sys1::ODESystem, sys2::ODESystem) - sys1 === sys2 && return true - iv1 = get_iv(sys1) - iv2 = get_iv(sys2) - isequal(iv1, iv2) && - isequal(nameof(sys1), nameof(sys2)) && - _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && - _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && - _eq_unordered(get_ps(sys1), get_ps(sys2)) && - _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && - _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && - all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) && - isequal(get_constraintsystem(sys1), get_constraintsystem(sys2)) && - _eq_unordered(get_costs(sys1), get_costs(sys2)) -end - -function flatten(sys::ODESystem, noeqs = false) - systems = get_systems(sys) - if isempty(systems) - return sys - else - return ODESystem(noeqs ? Equation[] : equations(sys), - get_iv(sys), - unknowns(sys), - parameters(sys; initial_parameters = true), - parameter_dependencies = parameter_dependencies(sys), - guesses = guesses(sys), - observed = observed(sys), - continuous_events = continuous_events(sys), - discrete_events = discrete_events(sys), - defaults = defaults(sys), - name = nameof(sys), - description = description(sys), - initialization_eqs = initialization_equations(sys), - assertions = assertions(sys), - is_dde = is_dde(sys), - tstops = symbolic_tstops(sys), - metadata = get_metadata(sys), - checks = false, - # without this, any defaults/guesses obtained from metadata that were - # later removed by the user will be re-added. Right now, we just want to - # retain `defaults(sys)` as-is. - discover_from_metadata = false) - end -end - -ODESystem(eq::Equation, args...; kwargs...) = ODESystem([eq], args...; kwargs...) - -""" - build_explicit_observed_function(sys, ts; kwargs...) -> Function(s) - -Generates a function that computes the observed value(s) `ts` in the system `sys`, while making the assumption that there are no cycles in the equations. - -## Arguments -- `sys`: The system for which to generate the function -- `ts`: The symbolic observed values whose value should be computed - -## Keywords -- `return_inplace = false`: If true and the observed value is a vector, then return both the in place and out of place methods. -- `expression = false`: Generates a Julia `Expr`` computing the observed value if `expression` is true -- `eval_expression = false`: If true and `expression = false`, evaluates the returned function in the module `eval_module` -- `output_type = Array` the type of the array generated by a out-of-place vector-valued function -- `param_only = false` if true, only allow the generated function to access system parameters -- `inputs = nothing` additinoal symbolic variables that should be provided to the generated function -- `checkbounds = true` checks bounds if true when destructuring parameters -- `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. -- `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. -- `mkarray`: only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. -- `cse = true`: Whether to use Common Subexpression Elimination (CSE) to generate a more efficient function. - -## Returns - -The return value will be either: -* a single function `f_oop` if the input is a scalar or if the input is a Vector but `return_inplace` is false -* the out of place and in-place functions `(f_ip, f_oop)` if `return_inplace` is true and the input is a `Vector` - -The function(s) `f_oop` (and potentially `f_ip`) will be: -* `RuntimeGeneratedFunction`s by default, -* A Julia `Expr` if `expression` is true, -* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true and `expression` is false. - -The signatures will be of the form `g(...)` with arguments: - -- `output` for in-place functions -- `unknowns` if `param_only` is `false` -- `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` -- `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted -- `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` - -For example, a function `g(op, unknowns, p..., inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, -an array of inputs `inputs` is given, and `param_only` is false for a time-dependent system. -""" -function build_explicit_observed_function(sys, ts; - inputs = nothing, - disturbance_inputs = nothing, - disturbance_argument = false, - expression = false, - eval_expression = false, - eval_module = @__MODULE__, - output_type = Array, - checkbounds = true, - ps = parameters(sys; initial_parameters = true), - return_inplace = false, - param_only = false, - op = Operator, - throw = true, - cse = true, - mkarray = nothing) - is_tuple = ts isa Tuple - if is_tuple - ts = collect(ts) - output_type = Tuple - end - - allsyms = all_symbols(sys) - if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray - ts = map(x -> symbol_to_symbolic(sys, x; allsyms), ts) - else - ts = symbol_to_symbolic(sys, ts; allsyms) - end - - vs = ModelingToolkit.vars(ts; op) - namespace_subs = Dict() - ns_map = Dict{Any, Any}(renamespace(sys, eq.lhs) => eq.lhs for eq in observed(sys)) - for sym in unknowns(sys) - ns_map[renamespace(sys, sym)] = sym - if iscall(sym) && operation(sym) === getindex - ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] - end - end - for sym in full_parameters(sys) - ns_map[renamespace(sys, sym)] = sym - if iscall(sym) && operation(sym) === getindex - ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] - end - end - allsyms = Set(all_symbols(sys)) - iv = has_iv(sys) ? get_iv(sys) : nothing - for var in vs - var = unwrap(var) - newvar = get(ns_map, var, nothing) - if newvar !== nothing - namespace_subs[var] = newvar - var = newvar - end - if throw && !var_in_varlist(var, allsyms, iv) - Base.throw(ArgumentError("Symbol $var is not present in the system.")) - end - end - ts = fast_substitute(ts, namespace_subs) - - obsfilter = if param_only - if is_split(sys) - let ic = get_index_cache(sys) - eq -> !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) - end - else - Returns(false) - end - else - Returns(true) - end - dvs = if param_only - () - else - (unknowns(sys),) - end - if inputs === nothing - inputs = () - else - ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list - inputs = (inputs,) - end - if disturbance_inputs !== nothing - # Disturbance inputs may or may not be included as inputs, depending on disturbance_argument - ps = setdiff(ps, disturbance_inputs) - end - if disturbance_argument - disturbance_inputs = (disturbance_inputs,) - else - disturbance_inputs = () - end - ps = reorder_parameters(sys, ps) - iv = if is_time_dependent(sys) - (get_iv(sys),) - else - () - end - args = (dvs..., inputs..., ps..., iv..., disturbance_inputs...) - p_start = length(dvs) + length(inputs) + 1 - p_end = length(dvs) + length(inputs) + length(ps) - fns = build_function_wrapper( - sys, ts, args...; p_start, p_end, filter_observed = obsfilter, - output_type, mkarray, try_namespaced = true, expression = Val{true}, cse) - if fns isa Tuple - if expression - return return_inplace ? fns : fns[1] - end - oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) - f = GeneratedFunctionWrapper{( - p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( - oop, iip) - return return_inplace ? (f, f) : f - else - if expression - return fns - end - f = eval_or_rgf(fns; eval_expression, eval_module) - f = GeneratedFunctionWrapper{( - p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( - f, nothing) - return f - end -end - -function populate_delays(delays::Set, obsexprs, histfn, sys, sym) - _vars_util = vars(sym) - for v in _vars_util - v in delays && continue - iscall(v) && issym(operation(v)) && (args = arguments(v); length(args) == 1) && - iscall(only(args)) || continue - - idx = variable_index(sys, operation(v)(get_iv(sys))) - idx === nothing && error("Delay term $v is not an unknown in the system") - push!(delays, v) - push!(obsexprs, v ← histfn(only(args))[idx]) - end -end - -function _eq_unordered(a, b) - # a and b may be multidimensional - # e.g. comparing noiseeqs of SDESystem - a = vec(a) - b = vec(b) - length(a) === length(b) || return false - n = length(a) - idxs = Set(1:n) - for x in a - idx = findfirst(isequal(x), b) - # loop since there might be multiple identical entries in a/b - # and while we might have already matched the first there could - # be a second that is equal to x - while idx !== nothing && !(idx in idxs) - idx = findnext(isequal(x), b, idx + 1) - end - idx === nothing && return false - delete!(idxs, idx) - end - return true -end - -# We have a stand-alone function to convert a `NonlinearSystem` or `ODESystem` -# to an `ODESystem` to connect systems, and we later can reply on -# `structural_simplify` to convert `ODESystem`s to `NonlinearSystem`s. -""" -$(TYPEDSIGNATURES) - -Convert a `NonlinearSystem` to an `ODESystem` or converts an `ODESystem` to a -new `ODESystem` with a different independent variable. -""" -function convert_system(::Type{<:ODESystem}, sys, t; name = nameof(sys)) - isempty(observed(sys)) || - throw(ArgumentError("`convert_system` cannot handle reduced model (i.e. observed(sys) is non-empty).")) - t = value(t) - varmap = Dict() - sts = unknowns(sys) - newsts = similar(sts, Any) - for (i, s) in enumerate(sts) - if iscall(s) - args = arguments(s) - length(args) == 1 || - throw(InvalidSystemException("Illegal unknown: $s. The unknown can have at most one argument like `x(t)`.")) - arg = args[1] - if isequal(arg, t) - newsts[i] = s - continue - end - ns = maketerm(typeof(s), operation(s), Any[t], - SymbolicUtils.metadata(s)) - newsts[i] = ns - varmap[s] = ns - else - ns = variable(getname(s); T = FnType)(t) - newsts[i] = ns - varmap[s] = ns - end - end - sub = Base.Fix2(substitute, varmap) - if sys isa AbstractODESystem - iv = only(independent_variables(sys)) - sub.x[iv] = t # otherwise the Differentials aren't fixed - end - neweqs = map(sub, equations(sys)) - defs = Dict(sub(k) => sub(v) for (k, v) in defaults(sys)) - return ODESystem(neweqs, t, newsts, parameters(sys); defaults = defs, name = name, - checks = false) -end - -""" -$(SIGNATURES) - -Add accumulation variables for `vars`. -""" -function add_accumulations(sys::ODESystem, vars = unknowns(sys)) - avars = [rename(v, Symbol(:accumulation_, getname(v))) for v in vars] - add_accumulations(sys, avars .=> vars) -end - -""" -$(SIGNATURES) - -Add accumulation variables for `vars`. `vars` is a vector of pairs in the form -of - -```julia -[cumulative_var1 => x + y, cumulative_var2 => x^2] -``` -Then, cumulative variables `cumulative_var1` and `cumulative_var2` that computes -the cumulative `x + y` and `x^2` would be added to `sys`. -""" -function add_accumulations(sys::ODESystem, vars::Vector{<:Pair}) - eqs = get_eqs(sys) - avars = map(first, vars) - if (ints = intersect(avars, unknowns(sys)); !isempty(ints)) - error("$ints already exist in the system!") - end - D = Differential(get_iv(sys)) - @set! sys.eqs = [eqs; Equation[D(a) ~ v[2] for (a, v) in zip(avars, vars)]] - @set! sys.unknowns = [get_unknowns(sys); avars] - @set! sys.defaults = merge(get_defaults(sys), Dict(a => 0.0 for a in avars)) -end - -function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, bold = true) - # Print general AbstractSystem information - invoke(Base.show, Tuple{typeof(io), typeof(mime), AbstractSystem}, - io, mime, sys; hint, bold) - - name = nameof(sys) - - # Print initialization equations (unique to ODESystems) - nini = length(initialization_equations(sys)) - nini > 0 && printstyled(io, "\nInitialization equations ($nini):"; bold) - nini > 0 && hint && print(io, " see initialization_equations($name)") - - return nothing -end - -""" -Build the constraint system for the ODESystem. -""" -function process_constraint_system( - constraints::Vector{Equation}, sts, ps, iv; consname = :cons) - isempty(constraints) && return nothing - - constraintsts = OrderedSet() - constraintps = OrderedSet() - for cons in constraints - collect_vars!(constraintsts, constraintps, cons, iv) - end - - # Validate the states. - validate_vars_and_find_ps!(constraintsts, constraintps, sts, iv) - - ConstraintsSystem( - constraints, collect(constraintsts), collect(constraintps); name = consname) -end - -""" -Process the costs for the constraint system. -""" -function process_costs(costs::Vector, sts, ps, iv) - coststs = OrderedSet() - costps = OrderedSet() - for cost in costs - collect_vars!(coststs, costps, cost, iv) - end - - validate_vars_and_find_ps!(coststs, costps, sts, iv) - coststs, costps -end - -""" -Validate that all the variables in an auxiliary system of the ODESystem (constraint or costs) are -well-formed states or parameters. - - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) - - Callable/delay parameters should be parameters of the system - -Return the set of additional parameters found in the system, e.g. in x(p) ~ 3 then p should be added as a -parameter of the system. -""" -function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) - sts = sysvars - - for var in auxvars - if !iscall(var) - occursin(iv, var) && (var ∈ sts || - throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) - elseif length(arguments(var)) > 1 - throw(ArgumentError("Too many arguments for variable $var.")) - elseif length(arguments(var)) == 1 - arg = only(arguments(var)) - operation(var)(iv) ∈ sts || - throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) - - isequal(arg, iv) || isparameter(arg) || arg isa Integer || - arg isa AbstractFloat || - throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) - - isparameter(arg) && push!(auxps, arg) - else - var ∈ sts && - @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." - end - end -end - -""" -Generate a function that takes a solution object and computes the cost function obtained by coalescing the costs vector. -""" -function generate_cost_function(sys::ODESystem, kwargs...) - costs = get_costs(sys) - consolidate = get_consolidate(sys) - iv = get_iv(sys) - - ps = parameters(sys; initial_parameters = false) - sts = unknowns(sys) - np = length(ps) - ns = length(sts) - stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) - - @variables sol(..)[1:ns] - for st in vars(costs) - x = operation(st) - t = only(arguments(st)) - idx = stidxmap[x(iv)] - - costs = map(c -> Symbolics.fast_substitute(c, Dict(x(t) => sol(t)[idx])), costs) - end - - _p = reorder_parameters(sys, ps) - fs = build_function_wrapper(sys, costs, sol, _p..., t; output_type = Array, kwargs...) - vc_oop, vc_iip = eval_or_rgf.(fs) - - cost(sol, p, t) = consolidate(vc_oop(sol, p, t)) - return cost -end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl deleted file mode 100644 index 3fa1302630..0000000000 --- a/src/systems/diffeqs/sdesystem.jl +++ /dev/null @@ -1,931 +0,0 @@ -""" -$(TYPEDEF) - -A system of stochastic differential equations. - -# Fields -$(FIELDS) - -# Example - -```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D - -@parameters σ ρ β -@variables x(t) y(t) z(t) - -eqs = [D(x) ~ σ*(y-x), - D(y) ~ x*(ρ-z)-y, - D(z) ~ x*y - β*z] - -noiseeqs = [0.1*x, - 0.1*y, - 0.1*z] - -@named de = SDESystem(eqs,noiseeqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) -``` -""" -struct SDESystem <: AbstractODESystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The expressions defining the drift term.""" - eqs::Vector{Equation} - """The expressions defining the diffusion term.""" - noiseeqs::AbstractArray - """Independent variable.""" - iv::BasicSymbolic{Real} - """Dependent variables. Must not contain the independent variable.""" - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Control parameters (some subset of `ps`).""" - ctrls::Vector - """Observed variables.""" - observed::Vector{Equation} - """ - Time-derivative matrix. Note: this field will not be defined until - [`calculate_tgrad`](@ref) is called on the system. - """ - tgrad::RefValue - """ - Jacobian matrix. Note: this field will not be defined until - [`calculate_jacobian`](@ref) is called on the system. - """ - jac::RefValue - """ - Control Jacobian matrix. Note: this field will not be defined until - [`calculate_control_jacobian`](@ref) is called on the system. - """ - ctrl_jac::RefValue{Any} - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact::RefValue - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact_t::RefValue - """ - The name of the system. - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{SDESystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Type of the system. - """ - connector_type::Any - """ - A `Vector{SymbolicContinuousCallback}` that model events. - The integrator will use root finding to guarantee that it steps at each zero crossing. - """ - continuous_events::Vector{SymbolicContinuousCallback} - """ - A `Vector{SymbolicDiscreteCallback}` that models events. Symbolic - analog to `SciMLBase.DiscreteCallback` that executes an affect when a given condition is - true at the end of an integration step. - """ - discrete_events::Vector{SymbolicDiscreteCallback} - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Mapping of conditions which should be true throughout the solution process to corresponding error - messages. These will be added to the equations when calling `debug_system`. - """ - assertions::Dict{BasicSymbolic, String} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - """ - Signal for whether the noise equations should be treated as a scalar process. This should only - be `true` when `noiseeqs isa Vector`. - """ - is_scalar_noise::Bool - """ - A boolean indicating if the given `ODESystem` represents a system of DDEs. - """ - is_dde::Bool - isscheduled::Bool - tearing_state::Any - - function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, - tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, - guesses, initializesystem, initialization_eqs, connector_type, - cevents, devents, parameter_dependencies, assertions = Dict{ - BasicSymbolic, Nothing}, - metadata = nothing, gui_metadata = nothing, namespacing = true, - complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, - is_dde = false, - isscheduled = false, - tearing_state = nothing; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - check_equations(deqs, iv) - check_equations(neqs, dvs) - if size(neqs, 1) != length(deqs) - throw(ArgumentError("Noise equations ill-formed. Number of rows must match number of drift equations. size(neqs,1) = $(size(neqs,1)) != length(deqs) = $(length(deqs))")) - end - check_equations(equations(cevents), iv) - if is_scalar_noise && neqs isa AbstractMatrix - throw(ArgumentError("Noise equations ill-formed. Received a matrix of noise equations of size $(size(neqs)), but `is_scalar_noise` was set to `true`. Scalar noise is only compatible with an `AbstractVector` of noise equations.")) - end - check_subsystems(systems) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, deqs, neqs) - end - new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, - defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, - devents, parameter_dependencies, assertions, metadata, gui_metadata, namespacing, - complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled, tearing_state) - end -end - -function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dvs, ps; - controls = Num[], - observed = Num[], - systems = SDESystem[], - tspan = nothing, - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - name = nothing, - description = "", - connector_type = nothing, - checks = true, - continuous_events = nothing, - discrete_events = nothing, - parameter_dependencies = Equation[], - assertions = Dict{BasicSymbolic, String}(), - metadata = nothing, - gui_metadata = nothing, - index_cache = nothing, - parent = nothing, - is_scalar_noise = false, - is_dde = nothing) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - iv′ = value(iv) - dvs′ = value.(dvs) - ps′ = value.(ps) - ctrl′ = value.(controls) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) - - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :SDESystem, force = true) - end - - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - tgrad = RefValue(EMPTY_TGRAD) - jac = RefValue{Any}(EMPTY_JAC) - ctrl_jac = RefValue{Any}(EMPTY_JAC) - Wfact = RefValue(EMPTY_JAC) - Wfact_t = RefValue(EMPTY_JAC) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - if is_dde === nothing - is_dde = _check_if_dde(deqs, iv′, systems) - end - assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) - SDESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, neqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, - initializesystem, initialization_eqs, connector_type, - cont_callbacks, disc_callbacks, parameter_dependencies, assertions, metadata, gui_metadata, - true, false, index_cache, parent, is_scalar_noise, is_dde; checks = checks) -end - -function SDESystem(sys::ODESystem, neqs; kwargs...) - SDESystem(equations(sys), neqs, get_iv(sys), unknowns(sys), parameters(sys); kwargs...) -end - -function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs...) - diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) - - for eq in get(kwargs, :parameter_dependencies, Equation[]) - collect_vars!(allunknowns, ps, eq, iv) - end - - for ssys in get(kwargs, :systems, ODESystem[]) - collect_scoped_vars!(allunknowns, ps, ssys, iv) - end - - for v in allunknowns - isdelay(v, iv) || continue - collect_vars!(allunknowns, ps, arguments(v)[1], iv) - end - - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - - # validate noise equations - noisedvs = OrderedSet() - noiseps = OrderedSet() - collect_vars!(noisedvs, noiseps, noiseeqs, iv) - for dv in noisedvs - dv ∈ allunknowns || - throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) - end - algevars = setdiff(allunknowns, diffvars) - return SDESystem(eqs, noiseeqs, iv, Iterators.flatten((diffvars, algevars)), - [collect(ps); collect(noiseps)]; kwargs...) -end - -function SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) - SDESystem([eq], noiseeqs, args...; kwargs...) -end -function SDESystem(eq::Equation, noiseeq, args...; kwargs...) - SDESystem([eq], [noiseeq], args...; kwargs...) -end - -function Base.:(==)(sys1::SDESystem, sys2::SDESystem) - sys1 === sys2 && return true - iv1 = get_iv(sys1) - iv2 = get_iv(sys2) - isequal(iv1, iv2) && - isequal(nameof(sys1), nameof(sys2)) && - _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && - _eq_unordered(get_noiseeqs(sys1), get_noiseeqs(sys2)) && - isequal(get_is_scalar_noise(sys1), get_is_scalar_noise(sys2)) && - _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && - _eq_unordered(get_ps(sys1), get_ps(sys2)) && - _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && - _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && - all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) -end - -""" - function ODESystem(sys::SDESystem) - -Convert an `SDESystem` to the equivalent `ODESystem` using `@brownian` variables instead -of noise equations. The returned system will not be `iscomplete` and will not have an -index cache, regardless of `iscomplete(sys)`. -""" -function ODESystem(sys::SDESystem) - neqs = get_noiseeqs(sys) - eqs = equations(sys) - is_scalar_noise = get_is_scalar_noise(sys) - nbrownian = if is_scalar_noise - length(neqs) - else - size(neqs, 2) - end - brownvars = map(1:nbrownian) do i - name = gensym(Symbol(:brown_, i)) - only(@brownian $name) - end - if is_scalar_noise - brownterms = reduce(+, neqs .* brownvars; init = 0) - neweqs = map(eqs) do eq - eq.lhs ~ eq.rhs + brownterms - end - else - if neqs isa AbstractVector - neqs = reshape(neqs, (length(neqs), 1)) - end - brownterms = neqs * brownvars - neweqs = map(eqs, brownterms) do eq, brown - eq.lhs ~ eq.rhs + brown - end - end - newsys = ODESystem(neweqs, get_iv(sys), unknowns(sys), parameters(sys); - parameter_dependencies = parameter_dependencies(sys), defaults = defaults(sys), - continuous_events = continuous_events(sys), discrete_events = discrete_events(sys), - assertions = assertions(sys), - name = nameof(sys), description = description(sys), metadata = get_metadata(sys)) - @set newsys.parent = sys -end - -function __num_isdiag_noise(mat) - for i in axes(mat, 1) - nnz = 0 - for j in axes(mat, 2) - if !isequal(mat[i, j], 0) - nnz += 1 - end - end - if nnz > 1 - return (false) - end - end - true -end -function __get_num_diag_noise(mat) - map(axes(mat, 1)) do i - for j in axes(mat, 2) - mij = mat[i, j] - if !isequal(mij, 0) - return mij - end - end - 0 - end -end - -function generate_diffusion_function(sys::SDESystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); isdde = false, kwargs...) - eqs = get_noiseeqs(sys) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) -end - -""" -$(TYPEDSIGNATURES) - -Choose correction_factor=-1//2 (1//2) to convert Ito -> Stratonovich (Stratonovich->Ito). -""" -function stochastic_integral_transform(sys::SDESystem, correction_factor) - name = nameof(sys) - # use the general interface - if typeof(get_noiseeqs(sys)) <: Vector - eqs = vcat([equations(sys)[i].lhs ~ get_noiseeqs(sys)[i] - for i in eachindex(unknowns(sys))]...) - de = ODESystem(eqs, get_iv(sys), unknowns(sys), parameters(sys), name = name, - checks = false) - - jac = calculate_jacobian(de, sparse = false, simplify = false) - ∇σσ′ = simplify.(jac * get_noiseeqs(sys)) - - deqs = vcat([equations(sys)[i].lhs ~ equations(sys)[i].rhs + - correction_factor * ∇σσ′[i] - for i in eachindex(unknowns(sys))]...) - else - dimunknowns, m = size(get_noiseeqs(sys)) - eqs = vcat([equations(sys)[i].lhs ~ get_noiseeqs(sys)[i] - for i in eachindex(unknowns(sys))]...) - de = ODESystem(eqs, get_iv(sys), unknowns(sys), parameters(sys), name = name, - checks = false) - - jac = calculate_jacobian(de, sparse = false, simplify = false) - ∇σσ′ = simplify.(jac * get_noiseeqs(sys)[:, 1]) - for k in 2:m - eqs = vcat([equations(sys)[i].lhs ~ get_noiseeqs(sys)[Int(i + - (k - 1) * - dimunknowns)] - for i in eachindex(unknowns(sys))]...) - de = ODESystem(eqs, get_iv(sys), unknowns(sys), parameters(sys), name = name, - checks = false) - - jac = calculate_jacobian(de, sparse = false, simplify = false) - ∇σσ′ = ∇σσ′ + simplify.(jac * get_noiseeqs(sys)[:, k]) - end - - deqs = vcat([equations(sys)[i].lhs ~ equations(sys)[i].rhs + - correction_factor * ∇σσ′[i] - for i in eachindex(unknowns(sys))]...) - end - - SDESystem(deqs, get_noiseeqs(sys), get_iv(sys), unknowns(sys), parameters(sys), - name = name, description = description(sys), - parameter_dependencies = parameter_dependencies(sys), checks = false) -end - -""" -$(TYPEDSIGNATURES) - -Measure transformation method that allows for a reduction in the variance of an estimator `Exp(g(X_t))`. -Input: Original SDE system and symbolic function `u(t,x)` with scalar output that - defines the adjustable parameters `d` in the Girsanov transformation. Optional: initial - condition for `θ0`. -Output: Modified SDESystem with additional component `θ_t` and initial value `θ0`, as well as - the weight `θ_t/θ0` as observed equation, such that the estimator `Exp(g(X_t)θ_t/θ0)` - has a smaller variance. - -Reference: -Kloeden, P. E., Platen, E., & Schurz, H. (2012). Numerical solution of SDE through computer -experiments. Springer Science & Business Media. - -# Example - -```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D - -@parameters α β -@variables x(t) y(t) z(t) - -eqs = [D(x) ~ α*x] -noiseeqs = [β*x] - -@named de = SDESystem(eqs,noiseeqs,t,[x],[α,β]) - -# define u (user choice) -u = x -θ0 = 0.1 -g(x) = x[1]^2 -demod = ModelingToolkit.Girsanov_transform(de, u; θ0=0.1) - -u0modmap = [ - x => x0 -] - -parammap = [ - α => 1.5, - β => 1.0 -] - -probmod = SDEProblem(complete(demod),u0modmap,(0.0,1.0),parammap) -ensemble_probmod = EnsembleProblem(probmod; - output_func = (sol,i) -> (g(sol[x,end])*sol[demod.weight,end],false), - ) - -simmod = solve(ensemble_probmod,EM(),dt=dt,trajectories=numtraj) -``` - -""" -function Girsanov_transform(sys::SDESystem, u; θ0 = 1.0) - name = nameof(sys) - - # register new variable θ corresponding to 1D correction process θ(t) - t = get_iv(sys) - D = Differential(t) - @variables θ(t), weight(t) - - # determine the adjustable parameters `d` given `u` - # gradient of u with respect to unknowns - grad = Symbolics.gradient(u, unknowns(sys)) - - noiseeqs = get_noiseeqs(sys) - if noiseeqs isa Vector - d = simplify.(-(noiseeqs .* grad) / u) - drift_correction = noiseeqs .* d - else - d = simplify.(-noiseeqs * grad / u) - drift_correction = noiseeqs * d - end - - # transformation adds additional unknowns θ: newX = (X,θ) - # drift function for unknowns is modified - # θ has zero drift - deqs = vcat([equations(sys)[i].lhs ~ equations(sys)[i].rhs - drift_correction[i] - for i in eachindex(unknowns(sys))]...) - deqsθ = D(θ) ~ 0 - push!(deqs, deqsθ) - - # diffusion matrix is of size d x m (d unknowns, m noise), with diagonal noise represented as a d-dimensional vector - # for diagonal noise processes with m>1, the noise process will become non-diagonal; extra unknown component but no new noise process. - # new diffusion matrix is of size d+1 x M - # diffusion for state is unchanged - - noiseqsθ = θ * d - - if noiseeqs isa Vector - m = size(noiseeqs) - if m == 1 - push!(noiseeqs, noiseqsθ) - else - noiseeqs = [Array(Diagonal(noiseeqs)); noiseqsθ'] - end - else - noiseeqs = [Array(noiseeqs); noiseqsθ'] - end - - unknown_vars = [unknowns(sys); θ] - - # return modified SDE System - SDESystem(deqs, noiseeqs, get_iv(sys), unknown_vars, parameters(sys); - defaults = Dict(θ => θ0), observed = [weight ~ θ / θ0], - name = name, description = description(sys), - parameter_dependencies = parameter_dependencies(sys), - checks = false) -end - -function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns(sys), - ps = parameters(sys), - u0 = nothing; - version = nothing, tgrad = false, sparse = false, - jac = false, Wfact = false, eval_expression = false, - sparsity = false, analytic = nothing, - eval_module = @__MODULE__, - checkbounds = false, initialization_data = nothing, - cse = true, kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEFunction`") - end - dvs = scalarize.(dvs) - - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, - cse, kwargs...) - g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) - - f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) - g = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(g_oop, g_iip) - - if tgrad - tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, cse, - kwargs...) - tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) - _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) - else - _tgrad = nothing - end - - if jac - jac_gen = generate_jacobian(sys, dvs, ps; expression = Val{true}, - sparse = sparse, cse, kwargs...) - jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - - _jac = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(jac_oop, jac_iip) - else - _jac = nothing - end - - if Wfact - tmp_Wfact, tmp_Wfact_t = generate_factorized_W(sys, dvs, ps, true; - expression = Val{true}, cse, kwargs...) - Wfact_oop, Wfact_iip = eval_or_rgf.(tmp_Wfact; eval_expression, eval_module) - Wfact_oop_t, Wfact_iip_t = eval_or_rgf.(tmp_Wfact_t; eval_expression, eval_module) - - _Wfact = GeneratedFunctionWrapper{(2, 4, is_split(sys))}(Wfact_oop, Wfact_iip) - _Wfact_t = GeneratedFunctionWrapper{(2, 4, is_split(sys))}(Wfact_oop_t, Wfact_iip_t) - else - _Wfact, _Wfact_t = nothing, nothing - end - - M = calculate_massmatrix(sys) - if sparse - uElType = u0 === nothing ? Float64 : eltype(u0) - W_prototype = similar(W_sparsity(sys), uElType) - else - W_prototype = nothing - end - - _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - SDEFunction{iip, specialize}(f, g; - sys = sys, - jac = _jac === nothing ? nothing : _jac, - tgrad = _tgrad === nothing ? nothing : _tgrad, - mass_matrix = _M, - jac_prototype = W_prototype, - observed = observedfun, - sparsity = sparsity ? W_sparsity(sys) : nothing, - analytic = analytic, - Wfact = _Wfact === nothing ? nothing : _Wfact, - Wfact_t = _Wfact_t === nothing ? nothing : _Wfact_t, - initialization_data) -end - -""" -```julia -DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = sys.unknowns, ps = sys.ps; - version = nothing, tgrad = false, sparse = false, - jac = false, Wfact = false, kwargs...) where {iip} -``` - -Create an `SDEFunction` from the [`SDESystem`](@ref). The arguments `dvs` and `ps` -are used to set the order of the dependent variable and parameter vectors, -respectively. -""" -function DiffEqBase.SDEFunction(sys::SDESystem, args...; kwargs...) - SDEFunction{true}(sys, args...; kwargs...) -end - -function DiffEqBase.SDEFunction{true}(sys::SDESystem, args...; - kwargs...) - SDEFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.SDEFunction{false}(sys::SDESystem, args...; - kwargs...) - SDEFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -""" -```julia -DiffEqBase.SDEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, tgrad = false, - jac = false, Wfact = false, - skipzeros = true, fillzeros = true, - sparse = false, - kwargs...) where {iip} -``` - -Create a Julia expression for an `SDEFunction` from the [`SDESystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct SDEFunctionExpr{iip} end - -function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, tgrad = false, - jac = false, Wfact = false, - sparse = false, linenumbers = false, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEFunctionExpr`") - end - idx = iip ? 2 : 1 - f = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...)[idx] - g = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, kwargs...)[idx] - if tgrad - _tgrad = generate_tgrad(sys, dvs, ps; expression = Val{true}, kwargs...)[idx] - else - _tgrad = :nothing - end - - if jac - _jac = generate_jacobian(sys, dvs, ps; sparse = sparse, expression = Val{true}, - kwargs...)[idx] - else - _jac = :nothing - end - - M = calculate_massmatrix(sys) - _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) - - if sparse - uElType = u0 === nothing ? Float64 : eltype(u0) - W_prototype = similar(W_sparsity(sys), uElType) - else - W_prototype = nothing - end - - if Wfact - tmp_Wfact, tmp_Wfact_t = generate_factorized_W( - sys, dvs, ps; expression = Val{true}, - kwargs...) - _Wfact = tmp_Wfact[idx] - _Wfact_t = tmp_Wfact_t[idx] - else - _Wfact, _Wfact_t = :nothing, :nothing - end - - ex = quote - f = $f - g = $g - tgrad = $_tgrad - jac = $_jac - W_prototype = $W_prototype - Wfact = $_Wfact - Wfact_t = $_Wfact_t - M = $_M - SDEFunction{$iip}(f, g, - jac = jac, - jac_prototype = W_prototype, - tgrad = tgrad, - Wfact = Wfact, - Wfact_t = Wfact_t, - mass_matrix = M) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function SDEFunctionExpr(sys::SDESystem, args...; kwargs...) - SDEFunctionExpr{true}(sys, args...; kwargs...) -end - -function DiffEqBase.SDEProblem{iip, specialize}( - sys::SDESystem, u0map = [], tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - sparsenoise = nothing, check_length = true, - callback = nothing, kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEProblem`") - end - - f, u0, p = process_SciMLProblem( - SDEFunction{iip, specialize}, sys, u0map, parammap; check_length, - t = tspan === nothing ? nothing : tspan[1], kwargs...) - cbs = process_events(sys; callback, kwargs...) - sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) - - noiseeqs = get_noiseeqs(sys) - is_scalar_noise = get_is_scalar_noise(sys) - if noiseeqs isa AbstractVector - noise_rate_prototype = nothing - if is_scalar_noise - noise = WienerProcess(0.0, 0.0, 0.0) - else - noise = nothing - end - elseif sparsenoise - I, J, V = findnz(SparseArrays.sparse(noiseeqs)) - noise_rate_prototype = SparseArrays.sparse(I, J, zero(eltype(u0))) - noise = nothing - else - noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) - noise = nothing - end - - kwargs = filter_kwargs(kwargs) - - # Call `remake` so it runs initialization if it is trivial - return remake(SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, - noise_rate_prototype = noise_rate_prototype, kwargs...)) -end - -function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) - if any(ModelingToolkit.isbrownian, unknowns(sys)) - error("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") - else - error("Cannot construct SDEProblem from a normal ODESystem.") - end -end - -""" -```julia -DiffEqBase.SDEProblem{iip}(sys::SDESystem, u0map, tspan, p = parammap; - version = nothing, tgrad = false, - jac = false, Wfact = false, - checkbounds = false, sparse = false, - sparsenoise = sparse, - skipzeros = true, fillzeros = true, - linenumbers = true, parallel = SerialForm(), - kwargs...) -``` - -Generates an SDEProblem from an SDESystem and allows for automatically -symbolically calculating numerical enhancements. -""" -function DiffEqBase.SDEProblem(sys::SDESystem, args...; kwargs...) - SDEProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.SDEProblem(sys::SDESystem, - u0map::StaticArray, - args...; - kwargs...) - SDEProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) -end - -function DiffEqBase.SDEProblem{true}(sys::SDESystem, args...; kwargs...) - SDEProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function DiffEqBase.SDEProblem{false}(sys::SDESystem, args...; kwargs...) - SDEProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -""" -```julia -DiffEqBase.SDEProblemExpr{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = false, Wfact = false, - checkbounds = false, sparse = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a Julia expression for constructing an ODEProblem from an -ODESystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct SDEProblemExpr{iip} end - -function SDEProblemExpr{iip}(sys::SDESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - sparsenoise = nothing, check_length = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEProblemExpr`") - end - f, u0, p = process_SciMLProblem( - SDEFunctionExpr{iip}, sys, u0map, parammap; check_length, - kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) - - noiseeqs = get_noiseeqs(sys) - is_scalar_noise = get_is_scalar_noise(sys) - if noiseeqs isa AbstractVector - noise_rate_prototype = nothing - if is_scalar_noise - noise = WienerProcess(0.0, 0.0, 0.0) - else - noise = nothing - end - elseif sparsenoise - I, J, V = findnz(SparseArrays.sparse(noiseeqs)) - noise_rate_prototype = SparseArrays.sparse(I, J, zero(eltype(u0))) - noise = nothing - else - T = u0 === nothing ? Float64 : eltype(u0) - noise_rate_prototype = zeros(T, size(get_noiseeqs(sys))) - noise = nothing - end - ex = quote - f = $f - u0 = $u0 - tspan = $tspan - p = $p - noise_rate_prototype = $noise_rate_prototype - noise = $noise - SDEProblem( - f, u0, tspan, p; noise_rate_prototype = noise_rate_prototype, noise = noise, - $(kwargs...)) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function SDEProblemExpr(sys::SDESystem, args...; kwargs...) - SDEProblemExpr{true}(sys, args...; kwargs...) -end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl deleted file mode 100644 index 5f7c986659..0000000000 --- a/src/systems/discrete_system/discrete_system.jl +++ /dev/null @@ -1,431 +0,0 @@ -""" -$(TYPEDEF) -A system of difference equations. -# Fields -$(FIELDS) -# Example -``` -using ModelingToolkit -using ModelingToolkit: t_nounits as t -@parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 -@variables x(t)=1.0 y(t)=0.0 z(t)=0.0 -k = ShiftIndex(t) -eqs = [x(k+1) ~ σ*(y-x), - y(k+1) ~ x*(ρ-z)-y, - z(k+1) ~ x*y - β*z] -@named de = DiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) # or -@named de = DiscreteSystem(eqs) -``` -""" -struct DiscreteSystem <: AbstractDiscreteSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The differential equations defining the discrete system.""" - eqs::Vector{Equation} - """Independent variable.""" - iv::BasicSymbolic{Real} - """Dependent (state) variables. Must not contain the independent variable.""" - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Observed states.""" - observed::Vector{Equation} - """ - The name of the system - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{DiscreteSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `DiscreteProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Inject assignment statements before the evaluation of the RHS function. - """ - preface::Any - """ - Type of the system. - """ - connector_type::Any - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - isscheduled::Bool - - function DiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, - observed, name, description, systems, defaults, guesses, initializesystem, - initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], - metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, namespacing = true, - complete = false, index_cache = nothing, parent = nothing, - isscheduled = false; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - check_subsystems(systems) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, discreteEqs) - end - new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, - preface, connector_type, parameter_dependencies, metadata, gui_metadata, - tearing_state, substitutions, namespacing, complete, index_cache, parent, - isscheduled) - end -end - -""" - $(TYPEDSIGNATURES) -Constructs a DiscreteSystem. -""" -function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; - observed = Num[], - systems = DiscreteSystem[], - tspan = nothing, - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - defaults = _merge(Dict(default_u0), Dict(default_p)), - preface = nothing, - connector_type = nothing, - parameter_dependencies = Equation[], - metadata = nothing, - gui_metadata = nothing, - kwargs...) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - iv′ = value(iv) - dvs′ = value.(dvs) - ps′ = value.(ps) - if any(hasderiv, eqs) || any(hashold, eqs) || any(hassample, eqs) || any(hasdiff, eqs) - error("Equations in a `DiscreteSystem` can only have `Shift` operators.") - end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :DiscreteSystem, force = true) - end - - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - DiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, - defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, - parameter_dependencies, metadata, gui_metadata, kwargs...) -end - -function DiscreteSystem(eqs, iv; kwargs...) - eqs = collect(eqs) - diffvars = OrderedSet() - allunknowns = OrderedSet() - ps = OrderedSet() - iv = value(iv) - for eq in eqs - collect_vars!(allunknowns, ps, eq, iv; op = Shift) - if iscall(eq.lhs) && operation(eq.lhs) isa Shift - isequal(iv, operation(eq.lhs).t) || - throw(ArgumentError("A DiscreteSystem can only have one independent variable.")) - eq.lhs in diffvars && - throw(ArgumentError("The shift variable $(eq.lhs) is not unique in the system of equations.")) - push!(diffvars, eq.lhs) - end - end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - if eq isa Pair - collect_vars!(allunknowns, ps, eq, iv) - else - collect_vars!(allunknowns, ps, eq, iv) - end - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - return DiscreteSystem(eqs, iv, - collect(allunknowns), collect(new_ps); kwargs...) -end - -DiscreteSystem(eq::Equation, args...; kwargs...) = DiscreteSystem([eq], args...; kwargs...) - -function flatten(sys::DiscreteSystem, noeqs = false) - systems = get_systems(sys) - if isempty(systems) - return sys - else - return DiscreteSystem(noeqs ? Equation[] : equations(sys), - get_iv(sys), - unknowns(sys), - parameters(sys), - observed = observed(sys), - defaults = defaults(sys), - guesses = guesses(sys), - initialization_eqs = initialization_equations(sys), - name = nameof(sys), - description = description(sys), - metadata = get_metadata(sys), - checks = false) - end -end - -function generate_function( - sys::DiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - exprs = [eq.rhs for eq in equations(sys)] - generate_custom_function(sys, exprs, dvs, ps; kwargs...) -end - -function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) - iv = get_iv(sys) - updated = AnyDict() - for k in collect(keys(u0map)) - v = u0map[k] - if !((op = operation(k)) isa Shift) - isnothing(getunshifted(k)) && - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(k)).") - - updated[Shift(iv, 1)(k)] = v - elseif op.steps > 0 - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(only(arguments(k)))).") - else - updated[Shift(iv, op.steps + 1)(only(arguments(k)))] = v - end - end - for var in unknowns(sys) - op = operation(var) - root = getunshifted(var) - shift = getshift(var) - isnothing(root) && continue - (haskey(updated, Shift(iv, shift)(root)) || haskey(updated, var)) && continue - haskey(defs, root) || error("Initial condition for $var not provided.") - updated[var] = defs[root] - end - return updated -end - -""" - $(TYPEDSIGNATURES) -Generates an DiscreteProblem from an DiscreteSystem. -""" -function SciMLBase.DiscreteProblem( - sys::DiscreteSystem, u0map = [], tspan = get_tspan(sys), - parammap = SciMLBase.NullParameters(); - eval_module = @__MODULE__, - eval_expression = false, - kwargs... -) - if !iscomplete(sys) - error("A completed `DiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") - end - dvs = unknowns(sys) - ps = parameters(sys) - eqs = equations(sys) - iv = get_iv(sys) - - u0map = to_varmap(u0map, dvs) - u0map = shift_u0map_forward(sys, u0map, defaults(sys)) - f, u0, p = process_SciMLProblem( - DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, build_initializeprob = false) - u0 = f(u0, p, tspan[1]) - DiscreteProblem(f, u0, tspan, p; kwargs...) -end - -function SciMLBase.DiscreteFunction(sys::DiscreteSystem, args...; kwargs...) - DiscreteFunction{true}(sys, args...; kwargs...) -end - -function SciMLBase.DiscreteFunction{true}(sys::DiscreteSystem, args...; kwargs...) - DiscreteFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.DiscreteFunction{false}(sys::DiscreteSystem, args...; kwargs...) - DiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -""" -```julia -SciMLBase.DiscreteFunction{iip}(sys::DiscreteSystem, - dvs = unknowns(sys), - ps = parameters(sys); - kwargs...) where {iip} -``` - -Create an `DiscreteFunction` from the [`DiscreteSystem`](@ref). The arguments `dvs` and `ps` -are used to set the order of the dependent variable and parameter vectors, -respectively. -""" -function SciMLBase.DiscreteFunction{iip, specialize}( - sys::DiscreteSystem, - dvs = unknowns(sys), - ps = parameters(sys), - u0 = nothing; - version = nothing, - p = nothing, - t = nothing, - eval_expression = false, - eval_module = @__MODULE__, - analytic = nothing, cse = true, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed `DiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") - end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) - - if specialize === SciMLBase.FunctionWrapperSpecialize && iip - if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on DiscreteFunction.") - end - f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) - end - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - DiscreteFunction{iip, specialize}(f; - sys = sys, - observed = observedfun, - analytic = analytic) -end - -""" -```julia -DiscreteFunctionExpr{iip}(sys::DiscreteSystem, dvs = states(sys), - ps = parameters(sys); - version = nothing, - kwargs...) where {iip} -``` - -Create a Julia expression for an `DiscreteFunction` from the [`DiscreteSystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct DiscreteFunctionExpr{iip} end -struct DiscreteFunctionClosure{O, I} <: Function - f_oop::O - f_iip::I -end -(f::DiscreteFunctionClosure)(u, p, t) = f.f_oop(u, p, t) -(f::DiscreteFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) - -function DiscreteFunctionExpr{iip}(sys::DiscreteSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, p = nothing, - linenumbers = false, - simplify = false, - kwargs...) where {iip} - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - - fsym = gensym(:f) - _f = :($fsym = $DiscreteFunctionClosure($f_oop, $f_iip)) - - ex = quote - $_f - DiscreteFunction{$iip}($fsym) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function DiscreteFunctionExpr(sys::DiscreteSystem, args...; kwargs...) - DiscreteFunctionExpr{true}(sys, args...; kwargs...) -end - -supports_initialization(::DiscreteSystem) = false diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl deleted file mode 100644 index 3956c089d4..0000000000 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ /dev/null @@ -1,443 +0,0 @@ -""" -$(TYPEDEF) -An implicit system of difference equations. -# Fields -$(FIELDS) -# Example -``` -using ModelingToolkit -using ModelingToolkit: t_nounits as t -@parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 -@variables x(t)=1.0 y(t)=0.0 z(t)=0.0 -k = ShiftIndex(t) -eqs = [x ~ σ*(y-x(k-1)), - y ~ x(k-1)*(ρ-z(k-1))-y, - z ~ x(k-1)*y(k-1) - β*z] -@named ide = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) -``` -""" -struct ImplicitDiscreteSystem <: AbstractDiscreteSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The difference equations defining the discrete system.""" - eqs::Vector{Equation} - """Independent variable.""" - iv::BasicSymbolic{Real} - """Dependent (state) variables. Must not contain the independent variable.""" - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Observed states.""" - observed::Vector{Equation} - """ - The name of the system - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{ImplicitDiscreteSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ImplicitDiscreteProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Inject assignment statements before the evaluation of the RHS function. - """ - preface::Any - """ - Type of the system. - """ - connector_type::Any - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - isscheduled::Bool - - function ImplicitDiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, - observed, name, description, systems, defaults, guesses, initializesystem, - initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], - metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, namespacing = true, - complete = false, index_cache = nothing, parent = nothing, - isscheduled = false; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - check_subsystems(systems) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, discreteEqs) - end - new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, - preface, connector_type, parameter_dependencies, metadata, gui_metadata, - tearing_state, substitutions, namespacing, complete, index_cache, parent, - isscheduled) - end -end - -""" - $(TYPEDSIGNATURES) - -Constructs a ImplicitDiscreteSystem. -""" -function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; - observed = Num[], - systems = ImplicitDiscreteSystem[], - tspan = nothing, - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - defaults = _merge(Dict(default_u0), Dict(default_p)), - preface = nothing, - connector_type = nothing, - parameter_dependencies = Equation[], - metadata = nothing, - gui_metadata = nothing, - kwargs...) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - iv′ = value(iv) - dvs′ = value.(dvs) - ps′ = value.(ps) - if any(hasderiv, eqs) || any(hashold, eqs) || any(hassample, eqs) || any(hasdiff, eqs) - error("Equations in a `ImplicitDiscreteSystem` can only have `Shift` operators.") - end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :ImplicitDiscreteSystem, force = true) - end - - # Copy equations to canonical form, but do not touch array expressions - eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - ImplicitDiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, - defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, - parameter_dependencies, metadata, gui_metadata, kwargs...) -end - -function ImplicitDiscreteSystem(eqs, iv; kwargs...) - eqs = collect(eqs) - diffvars = OrderedSet() - allunknowns = OrderedSet() - ps = OrderedSet() - iv = value(iv) - for eq in eqs - collect_vars!(allunknowns, ps, eq, iv; op = Shift) - if iscall(eq.lhs) && operation(eq.lhs) isa Shift - isequal(iv, operation(eq.lhs).t) || - throw(ArgumentError("An ImplicitDiscreteSystem can only have one independent variable.")) - eq.lhs in diffvars && - throw(ArgumentError("The shift variable $(eq.lhs) is not unique in the system of equations.")) - push!(diffvars, eq.lhs) - end - end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - if eq isa Pair - collect_vars!(allunknowns, ps, eq, iv) - else - collect_vars!(allunknowns, ps, eq, iv) - end - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - return ImplicitDiscreteSystem(eqs, iv, - collect(allunknowns), collect(new_ps); kwargs...) -end - -function ImplicitDiscreteSystem(eq::Equation, args...; kwargs...) - ImplicitDiscreteSystem([eq], args...; kwargs...) -end - -function flatten(sys::ImplicitDiscreteSystem, noeqs = false) - systems = get_systems(sys) - if isempty(systems) - return sys - else - return ImplicitDiscreteSystem(noeqs ? Equation[] : equations(sys), - get_iv(sys), - unknowns(sys), - parameters(sys), - observed = observed(sys), - defaults = defaults(sys), - guesses = guesses(sys), - initialization_eqs = initialization_equations(sys), - name = nameof(sys), - description = description(sys), - metadata = get_metadata(sys), - checks = false) - end -end - -function generate_function( - sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - iv = get_iv(sys) - # Algebraic equations get shifted forward 1, to match with differential equations - exprs = map(equations(sys)) do eq - _iszero(eq.lhs) ? distribute_shift(Shift(iv, 1)(eq.rhs)) : (eq.rhs - eq.lhs) - end - - # Handle observables in algebraic equations, since they are shifted - obs = observed(sys) - shifted_obs = Symbolics.Equation[distribute_shift(Shift(iv, 1)(eq)) for eq in obs] - obsidxs = observed_equations_used_by(sys, exprs; obs = shifted_obs) - extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) - for i in obsidxs] - - u_next = map(Shift(iv, 1), dvs) - u = dvs - build_function_wrapper( - sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) -end - -function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) - iv = get_iv(sys) - updated = AnyDict() - for k in collect(keys(u0map)) - v = u0map[k] - if !((op = operation(k)) isa Shift) - isnothing(getunshifted(k)) && - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(k)).") - - updated[k] = v - elseif op.steps > 0 - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(only(arguments(k)))).") - else - updated[k] = v - end - end - for var in unknowns(sys) - op = operation(var) - root = getunshifted(var) - shift = getshift(var) - isnothing(root) && continue - (haskey(updated, Shift(iv, shift)(root)) || haskey(updated, var)) && continue - haskey(defs, root) || error("Initial condition for $var not provided.") - updated[var] = defs[root] - end - return updated -end - -""" - $(TYPEDSIGNATURES) -Generates an ImplicitDiscreteProblem from an ImplicitDiscreteSystem. -""" -function SciMLBase.ImplicitDiscreteProblem( - sys::ImplicitDiscreteSystem, u0map = [], tspan = get_tspan(sys), - parammap = SciMLBase.NullParameters(); - eval_module = @__MODULE__, - eval_expression = false, - kwargs... -) - if !iscomplete(sys) - error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`.") - end - dvs = unknowns(sys) - ps = parameters(sys) - eqs = equations(sys) - iv = get_iv(sys) - - u0map = to_varmap(u0map, dvs) - u0map = shift_u0map_forward(sys, u0map, defaults(sys)) - f, u0, p = process_SciMLProblem( - ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) - - kwargs = filter_kwargs(kwargs) - ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{true}(sys, args...; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction{true}( - sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction{false}( - sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end -function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( - sys::ImplicitDiscreteSystem, - dvs = unknowns(sys), - ps = parameters(sys), - u0 = nothing; - version = nothing, - p = nothing, - t = nothing, - eval_expression = false, - eval_module = @__MODULE__, - analytic = nothing, cse = true, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") - end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u_next, u, p, t) = f_oop(u_next, u, p, t) - f(resid, u_next, u, p, t) = f_iip(resid, u_next, u, p, t) - - if specialize === SciMLBase.FunctionWrapperSpecialize && iip - if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on ImplicitDiscreteFunction.") - end - f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) - end - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - ImplicitDiscreteFunction{iip, specialize}(f; - sys = sys, - observed = observedfun, - analytic = analytic, - kwargs...) -end - -""" -```julia -ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = states(sys), - ps = parameters(sys); - version = nothing, - kwargs...) where {iip} -``` - -Create a Julia expression for an `ImplicitDiscreteFunction` from the [`ImplicitDiscreteSystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct ImplicitDiscreteFunctionExpr{iip} end -struct ImplicitDiscreteFunctionClosure{O, I} <: Function - f_oop::O - f_iip::I -end -(f::ImplicitDiscreteFunctionClosure)(u_next, u, p, t) = f.f_oop(u_next, u, p, t) -function (f::ImplicitDiscreteFunctionClosure)(resid, u_next, u, p, t) - f.f_iip(resid, u_next, u, p, t) -end - -function ImplicitDiscreteFunctionExpr{iip}( - sys::ImplicitDiscreteSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, p = nothing, - linenumbers = false, - simplify = false, - kwargs...) where {iip} - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - - fsym = gensym(:f) - _f = :($fsym = $ImplicitDiscreteFunctionClosure($f_oop, $f_iip)) - - ex = quote - $_f - ImplicitDiscreteFunction{$iip}($fsym) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function ImplicitDiscreteFunctionExpr(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunctionExpr{true}(sys, args...; kwargs...) -end diff --git a/src/systems/if_lifting.jl b/src/systems/if_lifting.jl index da069cc76e..9fd1958e5b 100644 --- a/src/systems/if_lifting.jl +++ b/src/systems/if_lifting.jl @@ -411,7 +411,10 @@ Lifting proceeds through the following process: * rewrite comparisons to be of the form eqn [op] 0; subtract the RHS from the LHS * replace comparisons with generated parameters; for each comparison eqn [op] 0, generate an event (dependent on op) that sets the parameter """ -function IfLifting(sys::ODESystem) +function IfLifting(sys::System) + if !is_time_dependent(sys) + throw(ArgumentError("IfLifting is only supported for time-dependent systems.")) + end cw = CondRewriter(get_iv(sys)) eqs = copy(equations(sys)) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl deleted file mode 100644 index 57a3aee7df..0000000000 --- a/src/systems/jumps/jumpsystem.jl +++ /dev/null @@ -1,684 +0,0 @@ -const JumpType = Union{VariableRateJump, ConstantRateJump, MassActionJump} - -# modifies the expression representing an affect function to -# call reset_aggregated_jumps!(integrator). -# assumes iip -function _reset_aggregator!(expr, integrator) - @assert Meta.isexpr(expr, :function) - body = expr.args[end] - body = quote - $body - $reset_aggregated_jumps!($integrator) - end - expr.args[end] = body - return nothing -end - -""" -$(TYPEDEF) - -A system of jump processes. - -# Fields -$(FIELDS) - -# Example - -```julia -using ModelingToolkit, JumpProcesses -using ModelingToolkit: t_nounits as t - -@parameters β γ -@variables S(t) I(t) R(t) -rate₁ = β*S*I -affect₁ = [S ~ S - 1, I ~ I + 1] -rate₂ = γ*I -affect₂ = [I ~ I - 1, R ~ R + 1] -j₁ = ConstantRateJump(rate₁,affect₁) -j₂ = ConstantRateJump(rate₂,affect₂) -j₃ = MassActionJump(2*β+γ, [R => 1], [S => 1, R => -1]) -@named js = JumpSystem([j₁,j₂,j₃], t, [S,I,R], [β,γ]) -``` -""" -struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """ - The jumps of the system. Allowable types are `ConstantRateJump`, - `VariableRateJump`, `MassActionJump`. - """ - eqs::U - """The independent variable, usually time.""" - iv::Any - """The dependent variables, representing the state of the system. Must not contain the independent variable.""" - unknowns::Vector - """The parameters of the system. Must not contain the independent variable.""" - ps::Vector - """Array variables.""" - var_to_name::Any - """Observed variables.""" - observed::Vector{Equation} - """The name of the system.""" - name::Symbol - """A description of the system.""" - description::String - """The internal systems. These are required to have unique names.""" - systems::Vector{JumpSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Type of the system. - """ - connector_type::Any - """ - A `Vector{SymbolicContinuousCallback}` that model events. - The integrator will use root finding to guarantee that it steps at each zero crossing. - """ - continuous_events::Vector{SymbolicContinuousCallback} - """ - A `Vector{SymbolicDiscreteCallback}` that models events. Symbolic - analog to `SciMLBase.DiscreteCallback` that executes an affect when a given condition is - true at the end of an integration step. Note, one must make sure to call - `reset_aggregated_jumps!(integrator)` if using a custom affect function that changes any - unknown value or parameter. - """ - discrete_events::Vector{SymbolicDiscreteCallback} - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - isscheduled::Bool - - function JumpSystem{U}( - tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - cevents, devents, - parameter_dependencies, metadata = nothing, gui_metadata = nothing, - namespacing = true, complete = false, index_cache = nothing, isscheduled = false; - checks::Union{Bool, Int} = true) where {U <: ArrayPartition} - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(unknowns, iv) - check_parameters(ps, iv) - check_subsystems(systems) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(unknowns, ps, iv) - check_units(u, ap, iv) - end - new{U}(tag, ap, iv, unknowns, ps, var_to_name, - observed, name, description, systems, defaults, guesses, initializesystem, - initialization_eqs, - connector_type, cevents, devents, parameter_dependencies, metadata, - gui_metadata, namespacing, complete, index_cache, isscheduled) - end -end -function JumpSystem(tag, ap, iv, states, ps, var_to_name, args...; kwargs...) - JumpSystem{typeof(ap)}(tag, ap, iv, states, ps, var_to_name, args...; kwargs...) -end - -function JumpSystem(eqs, iv, unknowns, ps; - observed = Equation[], - systems = JumpSystem[], - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - name = nothing, - description = "", - connector_type = nothing, - checks = true, - continuous_events = nothing, - discrete_events = nothing, - parameter_dependencies = Equation[], - metadata = nothing, - gui_metadata = nothing, - kwargs...) - - # variable processing, similar to ODESystem - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - iv′ = value(iv) - us′ = value.(unknowns) - ps′ = value.(ps) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :JumpSystem, force = true) - end - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, us′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - #! format: off - defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if value(v) !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses) if v !== nothing) - #! format: on - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - - # equation processing - # this and the treatment of continuous events are the only part - # unique to JumpSystems - eqs = scalarize.(eqs) - ap = ArrayPartition( - MassActionJump[], ConstantRateJump[], VariableRateJump[], Equation[]) - for eq in eqs - if eq isa MassActionJump - push!(ap.x[1], eq) - elseif eq isa ConstantRateJump - push!(ap.x[2], eq) - elseif eq isa VariableRateJump - push!(ap.x[3], eq) - elseif eq isa Equation - push!(ap.x[4], eq) - else - error("JumpSystem equations must contain MassActionJumps, ConstantRateJumps, VariableRateJumps, or Equations.") - end - end - - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - - JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, - defaults, guesses, initializesystem, initialization_eqs, connector_type, - cont_callbacks, disc_callbacks, - parameter_dependencies, metadata, gui_metadata, checks = checks) -end - -##### MTK dispatches for JumpSystems ##### -eqtype_supports_collect_vars(j::MassActionJump) = true -function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, - op = Differential) - collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) - for field in (j.reactant_stoch, j.net_stoch) - for el in field - collect_vars!(unknowns, parameters, el, iv; depth, op) - end - end - return nothing -end - -eqtype_supports_collect_vars(j::Union{ConstantRateJump, VariableRateJump}) = true -function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump, VariableRateJump}, - iv; depth = 0, op = Differential) - collect_vars!(unknowns, parameters, j.rate, iv; depth, op) - for eq in j.affect! - (eq isa Equation) && collect_vars!(unknowns, parameters, eq, iv; depth, op) - end - return nothing -end - -########################################## - -has_massactionjumps(js::JumpSystem) = !isempty(equations(js).x[1]) -has_constantratejumps(js::JumpSystem) = !isempty(equations(js).x[2]) -has_variableratejumps(js::JumpSystem) = !isempty(equations(js).x[3]) -has_equations(js::JumpSystem) = !isempty(equations(js).x[4]) - -function generate_rate_function(js::JumpSystem, rate) - consts = collect_constants(rate) - if !isempty(consts) # The SymbolicUtils._build_function method of this case doesn't support postprocess_fbody - csubs = Dict(c => getdefault(c) for c in consts) - rate = substitute(rate, csubs) - end - p = reorder_parameters(js) - build_function_wrapper(js, rate, unknowns(js), p..., - get_iv(js), - expression = Val{true}) -end - -function generate_affect_function(js::JumpSystem, affect, outputidxs) - consts = collect_constants(affect) - if !isempty(consts) # The SymbolicUtils._build_function method of this case doesn't support postprocess_fbody - csubs = Dict(c => getdefault(c) for c in consts) - affect = substitute(affect, csubs) - end - compile_affect( - affect, nothing, js, unknowns(js), parameters(js); outputidxs = outputidxs, - expression = Val{true}, checkvars = false) -end - -function assemble_vrj( - js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) - rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) - outputvars = (value(affect.lhs) for affect in vrj.affect!) - outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, vrj.affect!, outputidxs); - eval_expression, eval_module) - VariableRateJump(rate, affect; save_positions = vrj.save_positions) -end - -function assemble_vrj_expr(js, vrj, unknowntoid) - rate = generate_rate_function(js, vrj.rate) - outputvars = (value(affect.lhs) for affect in vrj.affect!) - outputidxs = ((unknowntoid[var] for var in outputvars)...,) - affect = generate_affect_function(js, vrj.affect!, outputidxs) - quote - rate = $rate - - affect = $affect - VariableRateJump(rate, affect) - end -end - -function assemble_crj( - js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) - rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) - outputvars = (value(affect.lhs) for affect in crj.affect!) - outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, crj.affect!, outputidxs); - eval_expression, eval_module) - ConstantRateJump(rate, affect) -end - -function assemble_crj_expr(js, crj, unknowntoid) - rate = generate_rate_function(js, crj.rate) - outputvars = (value(affect.lhs) for affect in crj.affect!) - outputidxs = ((unknowntoid[var] for var in outputvars)...,) - affect = generate_affect_function(js, crj.affect!, outputidxs) - quote - rate = $rate - - affect = $affect - ConstantRateJump(rate, affect) - end -end - -function numericrstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} - rs = Vector{Pair{Int, W}}() - for (wspec, stoich) in mtrs - spec = value(wspec) - if !iscall(spec) && _iszero(spec) - push!(rs, 0 => stoich) - else - push!(rs, unknowntoid[spec] => stoich) - end - end - sort!(rs) - rs -end - -function numericnstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} - ns = Vector{Pair{Int, W}}() - for (wspec, stoich) in mtrs - spec = value(wspec) - !iscall(spec) && _iszero(spec) && - error("Net stoichiometry can not have a species labelled 0.") - push!(ns, unknowntoid[spec] => stoich) - end - sort!(ns) -end - -# assemble a numeric MassActionJump from a MT symbolics MassActionJumps -function assemble_maj(majv::Vector{U}, unknowntoid, pmapper) where {U <: MassActionJump} - rs = [numericrstoich(maj.reactant_stoch, unknowntoid) for maj in majv] - ns = [numericnstoich(maj.net_stoch, unknowntoid) for maj in majv] - MassActionJump(rs, ns; param_mapper = pmapper, nocopy = true) -end - -""" -```julia -DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan, - parammap = DiffEqBase.NullParameters; - kwargs...) -``` - -Generates a blank DiscreteProblem for a pure jump JumpSystem to utilize as -its `prob.prob`. This is used in the case where there are no ODEs -and no SDEs associated with the system. - -Continuing the example from the [`JumpSystem`](@ref) definition: - -```julia -using DiffEqBase, JumpProcesses -u₀map = [S => 999, I => 1, R => 0] -parammap = [β => 0.1 / 1000, γ => 0.01] -tspan = (0.0, 250.0) -dprob = DiscreteProblem(complete(js), u₀map, tspan, parammap) -``` -""" -function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, - parammap = DiffEqBase.NullParameters(); - eval_expression = false, - eval_module = @__MODULE__, - cse = true, - kwargs...) - if !iscomplete(sys) - error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") - end - - if has_equations(sys) || (!isempty(continuous_events(sys))) - error("The passed in JumpSystem contains `Equation`s or continuous events, please use a problem type that supports these features, such as ODEProblem.") - end - - _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false, build_initializeprob = false, cse) - f = DiffEqBase.DISCRETE_INPLACE_DEFAULT - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun, - initialization_data = get(_f.kwargs, :initialization_data, nothing)) - DiscreteProblem(df, u0, tspan, p; kwargs...) -end - -""" -```julia -DiffEqBase.DiscreteProblemExpr(sys::JumpSystem, u0map, tspan, - parammap = DiffEqBase.NullParameters; kwargs...) -``` - -Generates a blank DiscreteProblem for a JumpSystem to utilize as its -solving `prob.prob`. This is used in the case where there are no ODEs -and no SDEs associated with the system. - -Continuing the example from the [`JumpSystem`](@ref) definition: - -```julia -using DiffEqBase, JumpProcesses -u₀map = [S => 999, I => 1, R => 0] -parammap = [β => 0.1 / 1000, γ => 0.01] -tspan = (0.0, 250.0) -dprob = DiscreteProblem(complete(js), u₀map, tspan, parammap) -``` -""" -struct DiscreteProblemExpr{iip} end - -function DiscreteProblemExpr{iip}(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, - parammap = DiffEqBase.NullParameters(); - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblemExpr`") - end - - _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false) - # identity function to make syms works - quote - f = DiffEqBase.DISCRETE_INPLACE_DEFAULT - u0 = $u0 - p = $p - sys = $sys - tspan = $tspan - df = DiscreteFunction{true, true}(f; sys = sys) - DiscreteProblem(df, u0, tspan, p) - end -end - -""" -```julia -DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan, - parammap = DiffEqBase.NullParameters; - kwargs...) -``` - -Generates a blank ODEProblem for a pure jump JumpSystem to utilize as its `prob.prob`. This -is used in the case where there are no ODEs and no SDEs associated with the system but there -are jumps with an explicit time dependency (i.e. `VariableRateJump`s). If no jumps have an -explicit time dependence, i.e. all are `ConstantRateJump`s or `MassActionJump`s then -`DiscreteProblem` should be preferred for performance reasons. - -Continuing the example from the [`JumpSystem`](@ref) definition: - -```julia -using DiffEqBase, JumpProcesses -u₀map = [S => 999, I => 1, R => 0] -parammap = [β => 0.1 / 1000, γ => 0.01] -tspan = (0.0, 250.0) -oprob = ODEProblem(complete(js), u₀map, tspan, parammap) -``` -""" -function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, - parammap = DiffEqBase.NullParameters(); - eval_expression = false, - eval_module = @__MODULE__, cse = true, - kwargs...) - if !iscomplete(sys) - error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") - end - - # forward everything to be an ODESystem but the jumps and discrete events - if has_equations(sys) - osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); - observed = observed(sys), name = nameof(sys), description = description(sys), - systems = get_systems(sys), defaults = defaults(sys), guesses = guesses(sys), - parameter_dependencies = parameter_dependencies(sys), - metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) - osys = complete(osys; add_initial_parameters = false) - return ODEProblem(osys, u0map, tspan, parammap; check_length = false, - build_initializeprob = false, kwargs...) - else - _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], tofloat = false, - check_length = false, build_initializeprob = false, cse) - f = (du, u, p, t) -> (du .= 0; nothing) - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, - checkbounds = get(kwargs, :checkbounds, false), cse) - df = ODEFunction(f; sys, observed = observedfun) - return ODEProblem(df, u0, tspan, p; kwargs...) - end -end - -""" -```julia -DiffEqBase.JumpProblem(js::JumpSystem, prob, aggregator; kwargs...) -``` - -Generates a JumpProblem from a JumpSystem. - -Continuing the example from the [`DiscreteProblem`](@ref) definition: - -```julia -jprob = JumpProblem(complete(js), dprob, Direct()) -sol = solve(jprob, SSAStepper()) -``` -""" -function JumpProcesses.JumpProblem(js::JumpSystem, prob, - aggregator = JumpProcesses.NullAggregator(); callback = nothing, - eval_expression = false, eval_module = @__MODULE__, kwargs...) - if !iscomplete(js) - error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `JumpProblem`") - end - unknowntoid = Dict(value(unknown) => i for (i, unknown) in enumerate(unknowns(js))) - eqs = equations(js) - invttype = prob.tspan[1] === nothing ? Float64 : typeof(1 / prob.tspan[2]) - - # handling parameter substitution and empty param vecs - p = (prob.p isa DiffEqBase.NullParameters || prob.p === nothing) ? Num[] : prob.p - - majpmapper = JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) - majs = isempty(eqs.x[1]) ? nothing : assemble_maj(eqs.x[1], unknowntoid, majpmapper) - crjs = ConstantRateJump[assemble_crj(js, j, unknowntoid; eval_expression, eval_module) - for j in eqs.x[2]] - vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) - for j in eqs.x[3]] - if prob isa DiscreteProblem - if (!isempty(vrjs) || has_equations(js) || !isempty(continuous_events(js))) - error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps, coupled differential equations, or continuous events.") - end - end - jset = JumpSet(Tuple(vrjs), Tuple(crjs), nothing, majs) - - # dep graphs are only for constant rate jumps - nonvrjs = ArrayPartition(eqs.x[1], eqs.x[2]) - if needs_vartojumps_map(aggregator) || needs_depgraph(aggregator) || - (aggregator isa JumpProcesses.NullAggregator) - jdeps = asgraph(js; eqs = nonvrjs) - vdeps = variable_dependencies(js; eqs = nonvrjs) - vtoj = jdeps.badjlist - jtov = vdeps.badjlist - jtoj = needs_depgraph(aggregator) ? eqeq_dependencies(jdeps, vdeps).fadjlist : - nothing - else - vtoj = nothing - jtov = nothing - jtoj = nothing - end - - # handle events, making sure to reset aggregators in the generated affect functions - cbs = process_events(js; callback, eval_expression, eval_module, - postprocess_affect_expr! = _reset_aggregator!) - - JumpProblem(prob, aggregator, jset; dep_graph = jtoj, vartojumps_map = vtoj, - jumptovars_map = jtov, scale_rates = false, nocopy = true, - callback = cbs, kwargs...) -end - -### Functions to determine which unknowns a jump depends on -function get_variables!(dep, jump::Union{ConstantRateJump, VariableRateJump}, variables) - jr = value(jump.rate) - (jr isa Symbolic) && get_variables!(dep, jr, variables) - dep -end - -function get_variables!(dep, jump::MassActionJump, variables) - sr = value(jump.scaled_rates) - (sr isa Symbolic) && get_variables!(dep, sr, variables) - for varasop in jump.reactant_stoch - any(isequal(varasop[1]), variables) && push!(dep, varasop[1]) - end - dep -end - -### Functions to determine which unknowns are modified by a given jump -function modified_unknowns!(munknowns, jump::Union{ConstantRateJump, VariableRateJump}, sts) - for eq in jump.affect! - st = eq.lhs - any(isequal(st), sts) && push!(munknowns, st) - end - munknowns -end - -function modified_unknowns!(munknowns, jump::MassActionJump, sts) - for (unknown, stoich) in jump.net_stoch - any(isequal(unknown), sts) && push!(munknowns, unknown) - end - munknowns -end - -###################### parameter mapper ########################### -struct JumpSysMajParamMapper{U, V, W} - paramexprs::U # the parameter expressions to use for each jump rate constant - sympars::V # parameters(sys) from the underlying JumpSystem - subdict::Any # mapping from an element of parameters(sys) to its current numerical value -end - -function JumpSysMajParamMapper(js::JumpSystem, p; jseqs = nothing, rateconsttype = Float64) - eqs = (jseqs === nothing) ? equations(js) : jseqs - paramexprs = [maj.scaled_rates for maj in eqs.x[1]] - psyms = reduce(vcat, reorder_parameters(js); init = []) - paramdict = Dict(value(k) => value(v) for (k, v) in zip(psyms, vcat(p...))) - JumpSysMajParamMapper{typeof(paramexprs), typeof(psyms), rateconsttype}(paramexprs, - psyms, - paramdict) -end - -function updateparams!(ratemap::JumpSysMajParamMapper{U, V, W}, - params) where {U <: AbstractArray, V <: AbstractArray, W} - for (i, p) in enumerate(params) - sympar = ratemap.sympars[i] - ratemap.subdict[sympar] = p - end - nothing -end - -function updateparams!(ratemap::JumpSysMajParamMapper{U, V, W}, - params::MTKParameters) where {U <: AbstractArray, V <: AbstractArray, W} - for (i, p) in enumerate(ArrayPartition(params...)) - sympar = ratemap.sympars[i] - ratemap.subdict[sympar] = p - end - nothing -end - -function updateparams!(::JumpSysMajParamMapper{U, V, W}, - params::Nothing) where {U <: AbstractArray, V <: AbstractArray, W} - nothing -end - -# create the initial parameter vector for use in a MassActionJump -function (ratemap::JumpSysMajParamMapper{ - U, - V, - W -})(params) where {U <: AbstractArray, - V <: AbstractArray, W} - updateparams!(ratemap, params) - [convert(W, value(substitute(paramexpr, ratemap.subdict))) - for paramexpr in ratemap.paramexprs] -end - -# update a maj with parameter vectors -function (ratemap::JumpSysMajParamMapper{U, V, W})(maj::MassActionJump, newparams; - scale_rates, - kwargs...) where {U <: AbstractArray, - V <: AbstractArray, W} - updateparams!(ratemap, newparams) - for i in 1:get_num_majumps(maj) - maj.scaled_rates[i] = convert(W, - value(substitute(ratemap.paramexprs[i], - ratemap.subdict))) - end - scale_rates && JumpProcesses.scalerates!(maj.scaled_rates, maj.reactant_stoch) - nothing -end - -supports_initialization(::JumpSystem) = false diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 4632c1b889..90715766ed 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -7,7 +7,7 @@ ModelingToolkit component or connector with metadata $(FIELDS) """ struct Model{F, S} - """The constructor that returns ODESystem.""" + """The constructor that returns a System.""" f::F """ The dictionary with metadata like keyword arguments (:kwargs), base @@ -61,7 +61,7 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(variables = [])) push!(exprs.args, :(parameters = [])) - push!(exprs.args, :(systems = ODESystem[])) + push!(exprs.args, :(systems = System[])) push!(exprs.args, :(equations = Union{Equation, Vector{Equation}}[])) push!(exprs.args, :(defaults = Dict{Num, Union{Number, Symbol, Function}}())) @@ -114,7 +114,7 @@ function _model_macro(mod, name, expr, isconnector) @inline pop_structure_dict!.( Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) - sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; + sys = :($System($(flatten_equations)(equations), $iv, variables, parameters; name, description = $description, systems, gui_metadata = $gui_metadata, defaults)) if length(ext) == 0 diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index ec25b9b660..48f196930c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -1,9 +1,18 @@ +function generate_initializesystem( + sys::AbstractSystem; time_dependent_init = is_time_dependent(sys), kwargs...) + if time_dependent_init + generate_initializesystem_timevarying(sys; kwargs...) + else + generate_initializesystem_timeindependent(sys; kwargs...) + end +end + """ $(TYPEDSIGNATURES) Generate `NonlinearSystem` which initializes a problem from specified initial conditions of an `AbstractTimeDependentSystem`. """ -function generate_initializesystem(sys::AbstractTimeDependentSystem; +function generate_initializesystem_timevarying(sys::AbstractSystem; u0map = Dict(), pmap = Dict(), initialization_eqs = [], @@ -145,7 +154,7 @@ function generate_initializesystem(sys::AbstractTimeDependentSystem; end meta = InitializationSystemMetadata( anydict(u0map), anydict(pmap), additional_guesses, - additional_initialization_eqs, extra_metadata, nothing) + additional_initialization_eqs, extra_metadata, nothing, true) return NonlinearSystem(eqs_ics, vars, pars; @@ -162,7 +171,7 @@ $(TYPEDSIGNATURES) Generate `NonlinearSystem` which initializes a problem from specified initial conditions of an `AbstractTimeDependentSystem`. """ -function generate_initializesystem(sys::AbstractTimeIndependentSystem; +function generate_initializesystem_timeindependent(sys::AbstractSystem; u0map = Dict(), pmap = Dict(), initialization_eqs = [], @@ -246,7 +255,7 @@ function generate_initializesystem(sys::AbstractTimeIndependentSystem; end meta = InitializationSystemMetadata( anydict(u0map), anydict(pmap), additional_guesses, - additional_initialization_eqs, extra_metadata, nothing) + additional_initialization_eqs, extra_metadata, nothing, false) return NonlinearSystem(eqs_ics, vars, pars; @@ -492,6 +501,7 @@ struct InitializationSystemMetadata additional_initialization_eqs::Vector{Equation} extra_metadata::NamedTuple oop_reconstruct_u0_p::Union{Nothing, ReconstructInitializeprob} + time_dependent_init::Bool end function get_possibly_array_fallback_singletons(varmap, p) @@ -601,6 +611,7 @@ function SciMLBase.remake_initialization_data( merge!(guesses, meta.additional_guesses) use_scc = get(meta.extra_metadata, :use_scc, true) initialization_eqs = meta.additional_initialization_eqs + time_dependent_init = meta.time_dependent_init end else # there is no initializeprob, so the original problem construction @@ -648,7 +659,7 @@ function SciMLBase.remake_initialization_data( u0map, pmap, defs, cmap, dvs, ps) floatT = float_type_from_varmap(op) kws = maybe_build_initialization_problem( - sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; + sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; time_dependent_init, use_scc, initialization_eqs, floatT, allow_incomplete = true) return SciMLBase.remake_initialization_data(sys, kws, newu0, t0, newp, newu0, newp) @@ -657,6 +668,7 @@ end function SciMLBase.late_binding_update_u0_p( prob, sys::AbstractSystem, u0, p, t0, newu0, newp) supports_initialization(sys) || return newu0, newp + prob isa IntervalNonlinearProblem && return newu0, newp u0 === missing && return newu0, (p === missing ? copy(newp) : newp) # non-symbolic u0 updates initials... if !(eltype(u0) <: Pair) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl deleted file mode 100644 index 856822492b..0000000000 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ /dev/null @@ -1,997 +0,0 @@ -""" -$(TYPEDEF) - -A nonlinear system of equations. - -# Fields -$(FIELDS) - -# Examples - -```julia -@variables x y z -@parameters σ ρ β - -eqs = [0 ~ σ*(y-x), - 0 ~ x*(ρ-z)-y, - 0 ~ x*y - β*z] -@named ns = NonlinearSystem(eqs, [x,y,z],[σ,ρ,β]) -``` -""" -struct NonlinearSystem <: AbstractTimeIndependentSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """Vector of equations defining the system.""" - eqs::Vector{Equation} - """Unknown variables.""" - unknowns::Vector - """Parameters.""" - ps::Vector - """Array variables.""" - var_to_name::Any - """Observed variables.""" - observed::Vector{Equation} - """ - Jacobian matrix. Note: this field will not be defined until - [`calculate_jacobian`](@ref) is called on the system. - """ - jac::RefValue{Any} - """ - The name of the system. - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{NonlinearSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Type of the system. - """ - connector_type::Any - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - isscheduled::Bool - - function NonlinearSystem( - tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, namespacing = true, - complete = false, index_cache = nothing, parent = nothing, - isscheduled = false; checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(unknowns, ps) - check_units(u, eqs) - check_subsystems(systems) - end - new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, - connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, - substitutions, namespacing, complete, index_cache, parent, isscheduled) - end -end - -function NonlinearSystem(eqs, unknowns, ps; - observed = [], - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - systems = NonlinearSystem[], - connector_type = nothing, - continuous_events = nothing, # this argument is only required for ODESystems, but is added here for the constructor to accept it without error - discrete_events = nothing, # this argument is only required for ODESystems, but is added here for the constructor to accept it without error - checks = true, - parameter_dependencies = Equation[], - metadata = nothing, - gui_metadata = nothing) - continuous_events === nothing || isempty(continuous_events) || - throw(ArgumentError("NonlinearSystem does not accept `continuous_events`, you provided $continuous_events")) - discrete_events === nothing || isempty(discrete_events) || - throw(ArgumentError("NonlinearSystem does not accept `discrete_events`, you provided $discrete_events")) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - length(unique(nameof.(systems))) == length(systems) || - throw(ArgumentError("System names must be unique.")) - (isempty(default_u0) && isempty(default_p)) || - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :NonlinearSystem, force = true) - - # Accept a single (scalar/vector) equation, but make array for consistent internal handling - if !(eqs isa AbstractArray) - eqs = [eqs] - end - - # Copy equations to canonical form, but do not touch array expressions - eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] - - jac = RefValue{Any}(EMPTY_JAC) - - ps′ = value.(ps) - dvs′ = value.(unknowns) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) - - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - NonlinearSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, dvs′, ps′, var_to_name, observed, jac, name, description, systems, defaults, - guesses, initializesystem, initialization_eqs, connector_type, parameter_dependencies, - metadata, gui_metadata, checks = checks) -end - -function NonlinearSystem(eqs; kwargs...) - eqs = collect(eqs) - allunknowns = OrderedSet() - ps = OrderedSet() - for eq in eqs - collect_vars!(allunknowns, ps, eq, nothing) - end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - if eq isa Pair - collect_vars!(allunknowns, ps, eq, nothing) - else - collect_vars!(allunknowns, ps, eq, nothing) - end - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - if symbolic_type(p) == ArraySymbolic() && - Symbolics.shape(unwrap(p)) != Symbolics.Unknown() - for i in eachindex(p) - delete!(new_ps, p[i]) - end - end - push!(new_ps, p) - end - end - - return NonlinearSystem(eqs, collect(allunknowns), collect(new_ps); kwargs...) -end - -""" - $(TYPEDSIGNATURES) - -Convert an `ODESystem` to a `NonlinearSystem` solving for its steady state (where derivatives are zero). -Any differential variable `D(x) ~ f(...)` will be turned into `0 ~ f(...)`. The returned system is not -simplified. If the input system is `complete`d, then so will the returned system. -""" -function NonlinearSystem(sys::AbstractODESystem) - eqs = equations(sys) - obs = observed(sys) - subrules = Dict(D(x) => 0.0 for x in unknowns(sys)) - eqs = map(eqs) do eq - fast_substitute(eq, subrules) - end - - nsys = NonlinearSystem(eqs, unknowns(sys), [parameters(sys); get_iv(sys)]; - parameter_dependencies = parameter_dependencies(sys), - defaults = merge(defaults(sys), Dict(get_iv(sys) => Inf)), guesses = guesses(sys), - initialization_eqs = initialization_equations(sys), name = nameof(sys), - observed = obs) - if iscomplete(sys) - nsys = complete(nsys; split = is_split(sys)) - end - return nsys -end - -function calculate_jacobian(sys::NonlinearSystem; sparse = false, simplify = false) - cache = get_jac(sys)[] - if cache isa Tuple && cache[2] == (sparse, simplify) - return cache[1] - end - - # observed equations may depend on unknowns, so substitute them in first - # TODO: rather keep observed derivatives unexpanded, like "Differential(obs)(expr)"? - obs = Dict(eq.lhs => eq.rhs for eq in observed(sys)) - rhs = map(eq -> fixpoint_sub(eq.rhs, obs), equations(sys)) - vals = [dv for dv in unknowns(sys)] - - if sparse - jac = sparsejacobian(rhs, vals, simplify = simplify) - else - jac = jacobian(rhs, vals, simplify = simplify) - end - get_jac(sys)[] = jac, (sparse, simplify) - return jac -end - -function generate_jacobian( - sys::NonlinearSystem, vs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - sparse = false, simplify = false, kwargs...) - jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, jac, vs, p...; kwargs...) -end - -function calculate_hessian(sys::NonlinearSystem; sparse = false, simplify = false) - obs = Dict(eq.lhs => eq.rhs for eq in observed(sys)) - rhs = map(eq -> fixpoint_sub(eq.rhs, obs), equations(sys)) - vals = [dv for dv in unknowns(sys)] - if sparse - hess = [sparsehessian(rhs[i], vals, simplify = simplify) for i in 1:length(rhs)] - else - hess = [hessian(rhs[i], vals, simplify = simplify) for i in 1:length(rhs)] - end - return hess -end - -function generate_hessian( - sys::NonlinearSystem, vs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - sparse = false, simplify = false, kwargs...) - hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, hess, vs, p...; kwargs...) -end - -function generate_function( - sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - scalar = false, kwargs...) - rhss = [deq.rhs for deq in equations(sys)] - dvs′ = value.(dvs) - if scalar - rhss = only(rhss) - dvs′ = only(dvs) - end - p = reorder_parameters(sys, value.(ps)) - return build_function_wrapper(sys, rhss, dvs′, p...; kwargs...) -end - -function jacobian_sparsity(sys::NonlinearSystem) - jacobian_sparsity([eq.rhs for eq in equations(sys)], - unknowns(sys)) -end - -function hessian_sparsity(sys::NonlinearSystem) - [hessian_sparsity(eq.rhs, - unknowns(sys)) for eq in equations(sys)] -end - -function calculate_resid_prototype(N, u0, p) - u0ElType = u0 === nothing ? Float64 : eltype(u0) - if SciMLStructures.isscimlstructure(p) - u0ElType = promote_type( - eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), - u0ElType) - end - return zeros(u0ElType, N) -end - -""" -```julia -SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create a `NonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments -`dvs` and `ps` are used to set the order of the dependent variable and parameter -vectors, respectively. -""" -function SciMLBase.NonlinearFunction(sys::NonlinearSystem, args...; kwargs...) - NonlinearFunction{true}(sys, args...; kwargs...) -end - -function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; p = nothing, - version = nothing, - jac = false, - eval_expression = false, - eval_module = @__MODULE__, - sparse = false, simplify = false, - initialization_data = nothing, cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearFunction`") - end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, cse, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) - - if jac - jac_gen = generate_jacobian(sys, dvs, ps; - simplify = simplify, sparse = sparse, - expression = Val{true}, cse, kwargs...) - jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - _jac = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(jac_oop, jac_iip) - else - _jac = nothing - end - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) - - if length(dvs) == length(equations(sys)) - resid_prototype = nothing - else - resid_prototype = calculate_resid_prototype(length(equations(sys)), u0, p) - end - - NonlinearFunction{iip}(f; - sys = sys, - jac = _jac === nothing ? nothing : _jac, - resid_prototype = resid_prototype, - jac_prototype = sparse ? - similar(calculate_jacobian(sys, sparse = sparse), - Float64) : nothing, - observed = observedfun, initialization_data) -end - -""" -$(TYPEDSIGNATURES) - -Create an `IntervalNonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments -`dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, -respectively. -""" -function SciMLBase.IntervalNonlinearFunction( - sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; - p = nothing, eval_expression = false, eval_module = @__MODULE__, - initialization_data = nothing, kwargs...) - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearFunction`") - end - if !isone(length(dvs)) || !isone(length(equations(sys))) - error("`IntervalNonlinearFunction` only supports systems with a single equation and a single unknown.") - end - - f_gen = generate_function( - sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) - f = eval_or_rgf(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f, nothing) - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) - - IntervalNonlinearFunction{false}( - f; observed = observedfun, sys = sys, initialization_data) -end - -""" -```julia -SciMLBase.NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys); - version = nothing, - jac = false, - sparse = false, - kwargs...) where {iip} -``` - -Create a Julia expression for a `NonlinearFunction` from the [`NonlinearSystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct NonlinearFunctionExpr{iip} end - -function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; p = nothing, - version = nothing, tgrad = false, - jac = false, - linenumbers = false, - sparse = false, simplify = false, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearFunctionExpr`") - end - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - f = :($(GeneratedFunctionWrapper{(2, 2, is_split(sys))})($f_oop, $f_iip)) - - if jac - jac_oop, jac_iip = generate_jacobian(sys, dvs, ps; - sparse = sparse, simplify = simplify, - expression = Val{true}, kwargs...) - _jac = :($(GeneratedFunctionWrapper{(2, 2, is_split(sys))})($jac_oop, $jac_iip)) - else - _jac = :nothing - end - - jp_expr = sparse ? :(similar($(get_jac(sys)[]), Float64)) : :nothing - if length(dvs) == length(equations(sys)) - resid_expr = :nothing - else - u0ElType = u0 === nothing ? Float64 : eltype(u0) - if SciMLStructures.isscimlstructure(p) - u0ElType = promote_type( - eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), - u0ElType) - end - - resid_expr = :(zeros($u0ElType, $(length(equations(sys))))) - end - ex = quote - f = $f - jac = $_jac - NonlinearFunction{$iip}(f, - jac = jac, - resid_prototype = resid_expr, - jac_prototype = $jp_expr) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -""" -$(TYPEDSIGNATURES) - -Create a Julia expression for an `IntervalNonlinearFunction` from the -[`NonlinearSystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the -dependent variable and parameter vectors, respectively. -""" -function IntervalNonlinearFunctionExpr( - sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys), - u0 = nothing; p = nothing, linenumbers = false, kwargs...) - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearFunctionExpr`") - end - if !isone(length(dvs)) || !isone(length(equations(sys))) - error("`IntervalNonlinearFunctionExpr` only supports systems with a single equation and a single unknown.") - end - - f = generate_function(sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) - f = :($(GeneratedFunctionWrapper{2, 2, is_split(sys)})($f, nothing)) - - ex = quote - f = $f - NonlinearFunction{false}(f) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -""" -```julia -DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - jac = false, sparse = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates an NonlinearProblem from a NonlinearSystem and allows for automatically -symbolically calculating numerical enhancements. -""" -function DiffEqBase.NonlinearProblem(sys::NonlinearSystem, args...; kwargs...) - NonlinearProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - check_length = true, kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") - end - f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; - check_length, kwargs...) - pt = something(get_metadata(sys), StandardNonlinearProblem()) - # Call `remake` so it runs initialization if it is trivial - return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) -end - -function DiffEqBase.NonlinearProblem(sys::AbstractODESystem, args...; kwargs...) - NonlinearProblem(NonlinearSystem(sys), args...; kwargs...) -end - -""" -```julia -DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - jac = false, sparse = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates an NonlinearProblem from a NonlinearSystem and allows for automatically -symbolically calculating numerical enhancements. -""" -function DiffEqBase.NonlinearLeastSquaresProblem(sys::NonlinearSystem, args...; kwargs...) - NonlinearLeastSquaresProblem{true}(sys, args...; kwargs...) -end - -function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - check_length = false, kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearLeastSquaresProblem`") - end - f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; - check_length, kwargs...) - pt = something(get_metadata(sys), StandardNonlinearProblem()) - # Call `remake` so it runs initialization if it is trivial - return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...)) -end - -const TypeT = Union{DataType, UnionAll} - -struct CacheWriter{F} - fn::F -end - -function (cw::CacheWriter)(p, sols) - cw.fn(p.caches, sols, p) -end - -function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, - exprs::Dict{TypeT, Vector{Any}}, solsyms, obseqs::Vector{Equation}; - eval_expression = false, eval_module = @__MODULE__, cse = true) - ps = parameters(sys; initial_parameters = true) - rps = reorder_parameters(sys, ps) - obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] - body = map(eachindex(buffer_types), buffer_types) do i, T - Symbol(:tmp, i) ← SetArray(true, :(out[$i]), get(exprs, T, [])) - end - - function argument_name(i::Int) - if i <= length(solsyms) - return :($(generated_argument_name(1))[$i]) - end - return generated_argument_name(i - length(solsyms)) - end - array_assignments = array_variable_assignments(solsyms...; argument_name) - fn = build_function_wrapper( - sys, nothing, :out, - DestructuredArgs(DestructuredArgs.(solsyms), generated_argument_name(1)), - rps...; p_start = 3, p_end = length(rps) + 2, - expression = Val{true}, add_observed = false, cse, - extra_assignments = [array_assignments; obs_assigns; body]) - fn = eval_or_rgf(fn; eval_expression, eval_module) - fn = GeneratedFunctionWrapper{(3, 3, is_split(sys))}(fn, nothing) - return CacheWriter(fn) -end - -struct SCCNonlinearFunction{iip} end - -function SCCNonlinearFunction{iip}( - sys::NonlinearSystem, _eqs, _dvs, _obs, cachesyms; eval_expression = false, - eval_module = @__MODULE__, cse = true, kwargs...) where {iip} - ps = parameters(sys; initial_parameters = true) - rps = reorder_parameters(sys, ps) - - obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] - - rhss = [eq.rhs - eq.lhs for eq in _eqs] - f_gen = build_function_wrapper(sys, - rhss, _dvs, rps..., cachesyms...; p_start = 2, - p_end = length(rps) + length(cachesyms) + 1, add_observed = false, - extra_assignments = obs_assignments, expression = Val{true}, cse) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) - - subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, - parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) - if get_index_cache(sys) !== nothing - @set! subsys.index_cache = subset_unknowns_observed( - get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) - @set! subsys.complete = true - end - - return NonlinearFunction{iip}(f; sys = subsys) -end - -function SciMLBase.SCCNonlinearProblem(sys::NonlinearSystem, args...; kwargs...) - SCCNonlinearProblem{true}(sys, args...; kwargs...) -end - -function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, - parammap = SciMLBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, - cse = true, kwargs...) where {iip} - if !iscomplete(sys) || get_tearing_state(sys) === nothing - error("A simplified `NonlinearSystem` is required. Call `structural_simplify` on the system before creating an `SCCNonlinearProblem`.") - end - - if !is_split(sys) - error("The system has been simplified with `split = false`. `SCCNonlinearProblem` is not compatible with this system. Pass `split = true` to `structural_simplify` to use `SCCNonlinearProblem`.") - end - - ts = get_tearing_state(sys) - var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) - - if length(var_sccs) == 1 - return NonlinearProblem{iip}( - sys, u0map, parammap; eval_expression, eval_module, kwargs...) - end - - condensed_graph = MatchedCondensationGraph( - DiCMOBiGraph{true}(complete(ts.structure.graph), - complete(var_eq_matching)), - var_sccs) - toporder = topological_sort_by_dfs(condensed_graph) - var_sccs = var_sccs[toporder] - eq_sccs = map(Base.Fix1(getindex, var_eq_matching), var_sccs) - - dvs = unknowns(sys) - ps = parameters(sys) - eqs = equations(sys) - obs = observed(sys) - - _, u0, p = process_SciMLProblem( - EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) - - explicitfuns = [] - nlfuns = [] - prevobsidxs = BlockArray(undef_blocks, Vector{Int}, Int[]) - # Cache buffer types and corresponding sizes. Stored as a pair of arrays instead of a - # dict to maintain a consistent order of buffers across SCCs - cachetypes = TypeT[] - cachesizes = Int[] - # explicitfun! related information for each SCC - # We need to compute buffer sizes before doing any codegen - scc_cachevars = Dict{TypeT, Vector{Any}}[] - scc_cacheexprs = Dict{TypeT, Vector{Any}}[] - scc_eqs = Vector{Equation}[] - scc_obs = Vector{Equation}[] - # variables solved in previous SCCs - available_vars = Set() - for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) - # subset unknowns and equations - _dvs = dvs[vscc] - _eqs = eqs[escc] - # get observed equations required by this SCC - union!(available_vars, _dvs) - obsidxs = observed_equations_used_by(sys, _eqs; available_vars) - # the ones used by previous SCCs can be precomputed into the cache - setdiff!(obsidxs, prevobsidxs) - _obs = obs[obsidxs] - union!(available_vars, getproperty.(_obs, (:lhs,))) - - # get all subexpressions in the RHS which we can precompute in the cache - # precomputed subexpressions should not contain `banned_vars` - banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) - state = Dict() - for i in eachindex(_obs) - _obs[i] = _obs[i].lhs ~ subexpressions_not_involving_vars!( - _obs[i].rhs, banned_vars, state) - end - for i in eachindex(_eqs) - _eqs[i] = _eqs[i].lhs ~ subexpressions_not_involving_vars!( - _eqs[i].rhs, banned_vars, state) - end - - # map from symtype to cached variables and their expressions - cachevars = Dict{Union{DataType, UnionAll}, Vector{Any}}() - cacheexprs = Dict{Union{DataType, UnionAll}, Vector{Any}}() - # observed of previous SCCs are in the cache - # NOTE: When we get proper CSE, we can substitute these - # and then use `subexpressions_not_involving_vars!` - for i in prevobsidxs - T = symtype(obs[i].lhs) - buf = get!(() -> Any[], cachevars, T) - push!(buf, obs[i].lhs) - - buf = get!(() -> Any[], cacheexprs, T) - push!(buf, obs[i].lhs) - end - - for (k, v) in state - k = unwrap(k) - v = unwrap(v) - T = symtype(k) - buf = get!(() -> Any[], cachevars, T) - push!(buf, v) - buf = get!(() -> Any[], cacheexprs, T) - push!(buf, k) - end - - # update the sizes of cache buffers - for (T, buf) in cachevars - idx = findfirst(isequal(T), cachetypes) - if idx === nothing - push!(cachetypes, T) - push!(cachesizes, 0) - idx = lastindex(cachetypes) - end - cachesizes[idx] = max(cachesizes[idx], length(buf)) - end - - push!(scc_cachevars, cachevars) - push!(scc_cacheexprs, cacheexprs) - push!(scc_eqs, _eqs) - push!(scc_obs, _obs) - blockpush!(prevobsidxs, obsidxs) - end - - for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) - _dvs = dvs[vscc] - _eqs = scc_eqs[i] - _prevobsidxs = reduce(vcat, blocks(prevobsidxs)[1:(i - 1)]; init = Int[]) - _obs = scc_obs[i] - cachevars = scc_cachevars[i] - cacheexprs = scc_cacheexprs[i] - available_vars = [dvs[reduce(vcat, var_sccs[1:(i - 1)]; init = Int[])]; - getproperty.( - reduce(vcat, scc_obs[1:(i - 1)]; init = []), (:lhs,))] - _prevobsidxs = vcat(_prevobsidxs, - observed_equations_used_by( - sys, reduce(vcat, values(cacheexprs); init = []); available_vars)) - if isempty(cachevars) - push!(explicitfuns, Returns(nothing)) - else - solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) - push!(explicitfuns, - CacheWriter(sys, cachetypes, cacheexprs, solsyms, obs[_prevobsidxs]; - eval_expression, eval_module, cse)) - end - - cachebufsyms = Tuple(map(cachetypes) do T - get(cachevars, T, []) - end) - f = SCCNonlinearFunction{iip}( - sys, _eqs, _dvs, _obs, cachebufsyms; eval_expression, eval_module, cse, kwargs...) - push!(nlfuns, f) - end - - if !isempty(cachetypes) - templates = map(cachetypes, cachesizes) do T, n - # Real refers to `eltype(u0)` - if T == Real - T = eltype(u0) - elseif T <: Array && eltype(T) == Real - T = Array{eltype(u0), ndims(T)} - end - BufferTemplate(T, n) - end - p = rebuild_with_caches(p, templates...) - end - - subprobs = [] - for (f, vscc) in zip(nlfuns, var_sccs) - prob = NonlinearProblem(f, u0[vscc], p) - push!(subprobs, prob) - end - - new_dvs = dvs[reduce(vcat, var_sccs)] - new_eqs = eqs[reduce(vcat, eq_sccs)] - @set! sys.unknowns = new_dvs - @set! sys.eqs = new_eqs - @set! sys.index_cache = subset_unknowns_observed( - get_index_cache(sys), sys, new_dvs, getproperty.(obs, (:lhs,))) - return SCCNonlinearProblem(subprobs, explicitfuns, p, true; sys) -end - -""" -$(TYPEDSIGNATURES) - -Generate an `IntervalNonlinearProblem` from a `NonlinearSystem` and allow for automatically -symbolically calculating numerical enhancements. -""" -function DiffEqBase.IntervalNonlinearProblem(sys::NonlinearSystem, uspan::NTuple{2}, - parammap = SciMLBase.NullParameters(); kwargs...) - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearProblem`") - end - if !isone(length(unknowns(sys))) || !isone(length(equations(sys))) - error("`IntervalNonlinearProblem` only supports with a single equation and a single unknown.") - end - f, u0, p = process_SciMLProblem( - IntervalNonlinearFunction, sys, unknowns(sys) .=> uspan[1], parammap; kwargs...) - - return IntervalNonlinearProblem(f, uspan, p; filter_kwargs(kwargs)...) -end - -""" -```julia -DiffEqBase.NonlinearProblemExpr{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - jac = false, sparse = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a Julia expression for a NonlinearProblem from a -NonlinearSystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct NonlinearProblemExpr{iip} end - -function NonlinearProblemExpr(sys::NonlinearSystem, args...; kwargs...) - NonlinearProblemExpr{true}(sys, args...; kwargs...) -end - -function NonlinearProblemExpr{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - check_length = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblemExpr`") - end - f, u0, p = process_SciMLProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; - check_length, kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - - ex = quote - f = $f - u0 = $u0 - p = $p - NonlinearProblem(f, u0, p; $(filter_kwargs(kwargs)...)) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -""" -```julia -DiffEqBase.NonlinearLeastSquaresProblemExpr{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - jac = false, sparse = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a Julia expression for a NonlinearProblem from a -NonlinearSystem and allows for automatically symbolically calculating -numerical enhancements. -""" -struct NonlinearLeastSquaresProblemExpr{iip} end - -function NonlinearLeastSquaresProblemExpr(sys::NonlinearSystem, args...; kwargs...) - NonlinearLeastSquaresProblemExpr{true}(sys, args...; kwargs...) -end - -function NonlinearLeastSquaresProblemExpr{iip}(sys::NonlinearSystem, u0map, - parammap = DiffEqBase.NullParameters(); - check_length = false, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblemExpr`") - end - f, u0, p = process_SciMLProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; - check_length, kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - - ex = quote - f = $f - u0 = $u0 - p = $p - NonlinearLeastSquaresProblem(f, u0, p; $(filter_kwargs(kwargs)...)) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -""" -$(TYPEDSIGNATURES) - -Generates a Julia expression for an IntervalNonlinearProblem from a -NonlinearSystem and allows for automatically symbolically calculating -numerical enhancements. -""" -function IntervalNonlinearProblemExpr(sys::NonlinearSystem, uspan::NTuple{2}, - parammap = SciMLBase.NullParameters(); kwargs...) - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearProblemExpr`") - end - if !isone(length(unknowns(sys))) || !isone(length(equations(sys))) - error("`IntervalNonlinearProblemExpr` only supports with a single equation and a single unknown.") - end - f, u0, p = process_SciMLProblem( - IntervalNonlinearFunctionExpr, sys, unknowns(sys) .=> uspan[1], parammap; kwargs...) - linenumbers = get(kwargs, :linenumbers, true) - - ex = quote - f = $f - uspan = $uspan - p = $p - IntervalNonlinearProblem(f, uspan, p; $(filter_kwargs(kwargs)...)) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function flatten(sys::NonlinearSystem, noeqs = false) - systems = get_systems(sys) - if isempty(systems) - return sys - else - return NonlinearSystem(noeqs ? Equation[] : equations(sys), - unknowns(sys), - parameters(sys), - observed = observed(sys), - defaults = defaults(sys), - guesses = guesses(sys), - initialization_eqs = initialization_equations(sys), - name = nameof(sys), - description = description(sys), - metadata = get_metadata(sys), - checks = false) - end -end - -function Base.:(==)(sys1::NonlinearSystem, sys2::NonlinearSystem) - isequal(nameof(sys1), nameof(sys2)) && - _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && - _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && - _eq_unordered(get_ps(sys1), get_ps(sys2)) && - all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) -end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl deleted file mode 100644 index 0f69e6d0b9..0000000000 --- a/src/systems/optimization/constraints_system.jl +++ /dev/null @@ -1,255 +0,0 @@ -""" -$(TYPEDEF) - -A constraint system of equations. - -# Fields -$(FIELDS) - -# Examples - -```julia -@variables x y z -@parameters a b c - -cstr = [0 ~ a*(y-x), - 0 ~ x*(b-z)-y, - 0 ~ x*y - c*z - x^2 + y^2 ≲ 1] -@named ns = ConstraintsSystem(cstr, [x,y,z],[a,b,c]) -``` -""" -struct ConstraintsSystem <: AbstractTimeIndependentSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """Vector of equations defining the system.""" - constraints::Vector{Union{Equation, Inequality}} - """Unknown variables.""" - unknowns::Vector - """Parameters.""" - ps::Vector - """Array variables.""" - var_to_name::Any - """Observed variables.""" - observed::Vector{Equation} - """ - Jacobian matrix. Note: this field will not be defined until - [`calculate_jacobian`](@ref) is called on the system. - """ - jac::RefValue{Any} - """ - The name of the system. - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{ConstraintsSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - Type of the system. - """ - connector_type::Any - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - - function ConstraintsSystem(tag, constraints, unknowns, ps, var_to_name, observed, jac, - name, description, - systems, - defaults, connector_type, metadata = nothing, - tearing_state = nothing, substitutions = nothing, namespacing = true, - complete = false, index_cache = nothing; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(unknowns, ps) - check_units(u, constraints) - check_subsystems(systems) - end - new(tag, constraints, unknowns, ps, var_to_name, - observed, jac, name, description, systems, - defaults, connector_type, metadata, tearing_state, substitutions, - namespacing, complete, index_cache) - end -end - -equations(sys::ConstraintsSystem) = constraints(sys) # needed for Base.show - -function ConstraintsSystem(constraints, unknowns, ps; - observed = [], - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - systems = ConstraintsSystem[], - connector_type = nothing, - continuous_events = nothing, # this argument is only required for ODESystems, but is added here for the constructor to accept it without error - discrete_events = nothing, # this argument is only required for ODESystems, but is added here for the constructor to accept it without error - checks = true, - metadata = nothing) - continuous_events === nothing || isempty(continuous_events) || - throw(ArgumentError("ConstraintsSystem does not accept `continuous_events`, you provided $continuous_events")) - discrete_events === nothing || isempty(discrete_events) || - throw(ArgumentError("ConstraintsSystem does not accept `discrete_events`, you provided $discrete_events")) - - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - - cstr = value.(Symbolics.canonical_form.(vcat(scalarize(constraints)...))) - unknowns′ = value.(scalarize(unknowns)) - ps′ = value.(ps) - - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :ConstraintsSystem, force = true) - end - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - - jac = RefValue{Any}(EMPTY_JAC) - defaults = todict(defaults) - defaults = Dict(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) - - var_to_name = Dict() - process_variables!(var_to_name, defaults, Dict(), unknowns′) - process_variables!(var_to_name, defaults, Dict(), ps′) - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - ConstraintsSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - cstr, unknowns, ps, var_to_name, observed, jac, name, description, systems, - defaults, - connector_type, metadata, checks = checks) -end - -function calculate_jacobian(sys::ConstraintsSystem; sparse = false, simplify = false) - cache = get_jac(sys)[] - if cache isa Tuple && cache[2] == (sparse, simplify) - return cache[1] - end - - lhss = generate_canonical_form_lhss(sys) - vals = [dv for dv in unknowns(sys)] - if sparse - jac = sparsejacobian(lhss, vals, simplify = simplify) - else - jac = jacobian(lhss, vals, simplify = simplify) - end - get_jac(sys)[] = jac, (sparse, simplify) - return jac -end - -function generate_jacobian( - sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - sparse = false, simplify = false, kwargs...) - jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, jac, vs, p...; kwargs...) -end - -function calculate_hessian(sys::ConstraintsSystem; sparse = false, simplify = false) - lhss = generate_canonical_form_lhss(sys) - vals = [dv for dv in unknowns(sys)] - if sparse - hess = [sparsehessian(lhs, vals, simplify = simplify) for lhs in lhss] - else - hess = [hessian(lhs, vals, simplify = simplify) for lhs in lhss] - end - return hess -end - -function generate_hessian( - sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - sparse = false, simplify = false, kwargs...) - hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, hess, vs, p...; kwargs...) -end - -function generate_function(sys::ConstraintsSystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - kwargs...) - lhss = generate_canonical_form_lhss(sys) - p = reorder_parameters(sys, value.(ps)) - func = build_function_wrapper(sys, lhss, value.(dvs), p...; kwargs...) - - cstr = constraints(sys) - lcons = fill(-Inf, length(cstr)) - ucons = zeros(length(cstr)) - lcons[findall(Base.Fix2(isa, Equation), cstr)] .= 0.0 - - return func, lcons, ucons -end - -function jacobian_sparsity(sys::ConstraintsSystem) - lhss = generate_canonical_form_lhss(sys) - jacobian_sparsity(lhss, unknowns(sys)) -end - -function hessian_sparsity(sys::ConstraintsSystem) - lhss = generate_canonical_form_lhss(sys) - [hessian_sparsity(eq, unknowns(sys)) for eq in lhss] -end - -""" -Convert the system of equalities and inequalities into a canonical form: -h(x) = 0 -g(x) <= 0 -""" -function generate_canonical_form_lhss(sys) - lhss = subs_constants([Symbolics.canonical_form(eq).lhs for eq in constraints(sys)]) -end - -function get_cmap(sys::ConstraintsSystem, exprs = nothing) - #Inject substitutions for constants => values - cs = collect_constants([get_constraints(sys); get_observed(sys)]) #ctrls? what else? - if !empty_substitutions(sys) - cs = [cs; collect_constants(get_substitutions(sys).subs)] - end - if exprs !== nothing - cs = [cs; collect_constants(exprs)] - end - # Swap constants for their values - cmap = map(x -> x ~ getdefault(x), cs) - return cmap, cs -end - -supports_initialization(::ConstraintsSystem) = false diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl deleted file mode 100644 index be4567aee5..0000000000 --- a/src/systems/optimization/optimizationsystem.jl +++ /dev/null @@ -1,760 +0,0 @@ -""" -$(TYPEDEF) - -A scalar equation for optimization. - -# Fields -$(FIELDS) - -# Examples - -```julia -@variables x y z -@parameters a b c - -obj = a * (y - x) + x * (b - z) - y + x * y - c * z -cons = [x^2 + y^2 ≲ 1] -@named os = OptimizationSystem(obj, [x, y, z], [a, b, c]; constraints = cons) -``` -""" -struct OptimizationSystem <: AbstractOptimizationSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """Objective function of the system.""" - op::Any - """Unknown variables.""" - unknowns::Array - """Parameters.""" - ps::Vector - """Array variables.""" - var_to_name::Any - """Observed variables.""" - observed::Vector{Equation} - """List of constraint equations of the system.""" - constraints::Vector{Union{Equation, Inequality}} - """The name of the system.""" - name::Symbol - """A description of the system.""" - description::String - """The internal systems. These are required to have unique names.""" - systems::Vector{OptimizationSystem} - """ - The default values to use when initial guess and/or - parameters are not supplied in `OptimizationProblem`. - """ - defaults::Dict - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - If false, then `sys.x` no longer performs namespacing. - """ - namespacing::Bool - """ - If true, denotes the model will not be modified any further. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - isscheduled::Bool - - function OptimizationSystem(tag, op, unknowns, ps, var_to_name, observed, - constraints, name, description, systems, defaults, metadata = nothing, - gui_metadata = nothing, namespacing = true, complete = false, - index_cache = nothing, parent = nothing, isscheduled = false; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(unknowns, ps) - unwrap(op) isa Symbolic && check_units(u, op) - check_units(u, observed) - check_units(u, constraints) - check_subsystems(systems) - end - new(tag, op, unknowns, ps, var_to_name, observed, - constraints, name, description, systems, defaults, metadata, gui_metadata, - namespacing, complete, index_cache, parent, isscheduled) - end -end - -equations(sys::AbstractOptimizationSystem) = objective(sys) # needed for Base.show - -function OptimizationSystem(op, unknowns, ps; - observed = [], - constraints = [], - default_u0 = Dict(), - default_p = Dict(), - defaults = _merge(Dict(default_u0), Dict(default_p)), - name = nothing, - description = "", - systems = OptimizationSystem[], - checks = true, - metadata = nothing, - gui_metadata = nothing) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - constraints = value.(reduce(vcat, scalarize(constraints); init = [])) - unknowns′ = value.(reduce(vcat, scalarize(unknowns); init = [])) - ps′ = value.(ps) - op′ = value(scalarize(op)) - - irreducible_subs = Dict() - for i in eachindex(unknowns′) - var = unknowns′[i] - if hasbounds(var) - irreducible_subs[var] = irrvar = setirreducible(var, true) - unknowns′[i] = irrvar - end - end - op′ = fast_substitute(op′, irreducible_subs) - constraints = fast_substitute.(constraints, (irreducible_subs,)) - - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :OptimizationSystem, force = true) - end - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - defaults = todict(defaults) - defaults = Dict(fast_substitute(value(k), irreducible_subs) => fast_substitute( - value(v), irreducible_subs) - for (k, v) in pairs(defaults) if value(v) !== nothing) - - var_to_name = Dict() - process_variables!(var_to_name, defaults, Dict(), unknowns′) - process_variables!(var_to_name, defaults, Dict(), ps′) - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - OptimizationSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - op′, unknowns′, ps′, var_to_name, - observed, - constraints, - name, description, systems, defaults, metadata, gui_metadata; - checks = checks) -end - -function OptimizationSystem(objective; constraints = [], kwargs...) - allunknowns = OrderedSet() - ps = OrderedSet() - collect_vars!(allunknowns, ps, objective, nothing) - for cons in constraints - collect_vars!(allunknowns, ps, cons, nothing) - end - for ssys in get(kwargs, :systems, OptimizationSystem[]) - collect_scoped_vars!(allunknowns, ps, ssys, nothing) - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - return OptimizationSystem( - objective, collect(allunknowns), collect(new_ps); constraints, kwargs...) -end - -function flatten(sys::OptimizationSystem) - systems = get_systems(sys) - isempty(systems) && return sys - - return OptimizationSystem( - objective(sys), - unknowns(sys), - parameters(sys); - observed = observed(sys), - constraints = constraints(sys), - defaults = defaults(sys), - name = nameof(sys), - metadata = get_metadata(sys), - checks = false - ) -end - -function calculate_gradient(sys::OptimizationSystem) - expand_derivatives.(gradient(objective(sys), unknowns(sys))) -end - -function generate_gradient(sys::OptimizationSystem, vs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); kwargs...) - grad = calculate_gradient(sys) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, grad, vs, p...; kwargs...) -end - -function calculate_hessian(sys::OptimizationSystem) - expand_derivatives.(hessian(objective(sys), unknowns(sys))) -end - -function generate_hessian( - sys::OptimizationSystem, vs = unknowns(sys), ps = parameters( - sys; initial_parameters = true); - sparse = false, kwargs...) - if sparse - hess = sparsehessian(objective(sys), unknowns(sys)) - else - hess = calculate_hessian(sys) - end - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, hess, vs, p...; kwargs...) -end - -function generate_function(sys::OptimizationSystem, vs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); - kwargs...) - eqs = objective(sys) - p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, eqs, vs, p...; kwargs...) -end - -function namespace_objective(sys::AbstractSystem) - op = objective(sys) - namespace_expr(op, sys) -end - -function objective(sys) - op = get_op(sys) - systems = get_systems(sys) - if isempty(systems) - op - else - op + reduce(+, map(sys_ -> namespace_objective(sys_), systems)) - end -end - -namespace_constraint(eq::Equation, sys) = namespace_equation(eq, sys) - -namespace_constraint(ineq::Inequality, sys) = namespace_inequality(ineq, sys) - -function namespace_inequality(ineq::Inequality, sys, n = nameof(sys)) - _lhs = namespace_expr(ineq.lhs, sys, n) - _rhs = namespace_expr(ineq.rhs, sys, n) - Inequality(_lhs, - _rhs, - ineq.relational_op) -end - -function namespace_constraints(sys) - cstrs = constraints(sys) - isempty(cstrs) && return Vector{Union{Equation, Inequality}}(undef, 0) - map(cstr -> namespace_constraint(cstr, sys), cstrs) -end - -function constraints(sys) - cs = get_constraints(sys) - systems = get_systems(sys) - isempty(systems) ? cs : [cs; reduce(vcat, namespace_constraints.(systems))] -end - -hessian_sparsity(sys::OptimizationSystem) = hessian_sparsity(get_op(sys), unknowns(sys)) - -""" -```julia -DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, - parammap = DiffEqBase.NullParameters(); - grad = false, - hess = false, sparse = false, - cons_j = false, cons_h = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates an OptimizationProblem from an OptimizationSystem and allows for automatically -symbolically calculating numerical enhancements. - -Certain solvers require setting `cons_j`, `cons_h` to `true` for constrained-optimization problems. -""" -function DiffEqBase.OptimizationProblem(sys::OptimizationSystem, args...; kwargs...) - DiffEqBase.OptimizationProblem{true}(sys::OptimizationSystem, args...; kwargs...) -end -function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, - parammap = DiffEqBase.NullParameters(); - lb = nothing, ub = nothing, - grad = false, - hess = false, sparse = false, - cons_j = false, cons_h = false, - cons_sparse = false, checkbounds = false, - linenumbers = true, parallel = SerialForm(), - eval_expression = false, eval_module = @__MODULE__, - checks = true, cse = true, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `OptimizationSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `OptimizationProblem`") - end - if haskey(kwargs, :lcons) || haskey(kwargs, :ucons) - Base.depwarn( - "`lcons` and `ucons` are deprecated. Specify constraints directly instead.", - :OptimizationProblem, force = true) - end - - dvs = unknowns(sys) - ps = parameters(sys) - cstr = constraints(sys) - - if isnothing(lb) && isnothing(ub) # use the symbolically specified bounds - lb = first.(getbounds.(dvs)) - ub = last.(getbounds.(dvs)) - isboolean = symtype.(unwrap.(dvs)) .<: Bool - lb[isboolean] .= 0 - ub[isboolean] .= 1 - else # use the user supplied variable bounds - xor(isnothing(lb), isnothing(ub)) && - throw(ArgumentError("Expected both `lb` and `ub` to be supplied")) - !isnothing(lb) && length(lb) != length(dvs) && - throw(ArgumentError("Expected both `lb` to be of the same length as the vector of optimization variables")) - !isnothing(ub) && length(ub) != length(dvs) && - throw(ArgumentError("Expected both `ub` to be of the same length as the vector of optimization variables")) - end - - int = symtype.(unwrap.(dvs)) .<: Integer - - defs = defaults(sys) - defs = mergedefaults(defs, parammap, ps) - defs = mergedefaults(defs, u0map, dvs) - - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) - if parammap isa MTKParameters - p = parammap - elseif has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) - else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false) - end - lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false) - ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false) - - if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) - lb = nothing - ub = nothing - end - - f = let _f = eval_or_rgf( - generate_function( - sys; checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}, wrap_mtkparameters = false, cse); - eval_expression, - eval_module) - __f(u, p) = _f(u, p) - __f(u, p::MTKParameters) = _f(u, p...) - __f - end - obj_expr = subs_constants(objective(sys)) - - if grad - _grad = let (grad_oop, grad_iip) = eval_or_rgf.( - generate_gradient( - sys; checkbounds = checkbounds, - linenumbers = linenumbers, - parallel = parallel, expression = Val{true}, - wrap_mtkparameters = false, cse); - eval_expression, - eval_module) - _grad(u, p) = grad_oop(u, p) - _grad(J, u, p) = (grad_iip(J, u, p); J) - _grad(u, p::MTKParameters) = grad_oop(u, p...) - _grad(J, u, p::MTKParameters) = (grad_iip(J, u, p...); J) - _grad - end - else - _grad = nothing - end - - if hess - _hess = let (hess_oop, hess_iip) = eval_or_rgf.( - generate_hessian( - sys; checkbounds = checkbounds, - linenumbers = linenumbers, - sparse = sparse, parallel = parallel, - expression = Val{true}, wrap_mtkparameters = false, cse); - eval_expression, - eval_module) - _hess(u, p) = hess_oop(u, p) - _hess(J, u, p) = (hess_iip(J, u, p); J) - _hess(u, p::MTKParameters) = hess_oop(u, p...) - _hess(J, u, p::MTKParameters) = (hess_iip(J, u, p...); J) - _hess - end - else - _hess = nothing - end - - if sparse - hess_prototype = hessian_sparsity(sys) - else - hess_prototype = nothing - end - - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds, cse) - - if length(cstr) > 0 - @named cons_sys = ConstraintsSystem(cstr, dvs, ps; checks) - cons_sys = complete(cons_sys) - cons, lcons_, ucons_ = generate_function(cons_sys; checkbounds = checkbounds, - linenumbers = linenumbers, - expression = Val{true}, wrap_mtkparameters = false, cse) - cons = let (cons_oop, cons_iip) = eval_or_rgf.(cons; eval_expression, eval_module) - _cons(u, p) = cons_oop(u, p) - _cons(resid, u, p) = cons_iip(resid, u, p) - _cons(u, p::MTKParameters) = cons_oop(u, p...) - _cons(resid, u, p::MTKParameters) = cons_iip(resid, u, p...) - end - if cons_j - _cons_j = let (cons_jac_oop, cons_jac_iip) = eval_or_rgf.( - generate_jacobian(cons_sys; - checkbounds = checkbounds, - linenumbers = linenumbers, - parallel = parallel, expression = Val{true}, - sparse = cons_sparse, wrap_mtkparameters = false, cse); - eval_expression, - eval_module) - _cons_j(u, p) = cons_jac_oop(u, p) - _cons_j(J, u, p) = (cons_jac_iip(J, u, p); J) - _cons_j(u, p::MTKParameters) = cons_jac_oop(u, p...) - _cons_j(J, u, p::MTKParameters) = (cons_jac_iip(J, u, p...); J) - _cons_j - end - else - _cons_j = nothing - end - if cons_h - _cons_h = let (cons_hess_oop, cons_hess_iip) = eval_or_rgf.( - generate_hessian( - cons_sys; checkbounds = checkbounds, - linenumbers = linenumbers, - sparse = cons_sparse, parallel = parallel, - expression = Val{true}, wrap_mtkparameters = false, cse); - eval_expression, - eval_module) - _cons_h(u, p) = cons_hess_oop(u, p) - _cons_h(J, u, p) = (cons_hess_iip(J, u, p); J) - _cons_h(u, p::MTKParameters) = cons_hess_oop(u, p...) - _cons_h(J, u, p::MTKParameters) = (cons_hess_iip(J, u, p...); J) - _cons_h - end - else - _cons_h = nothing - end - cons_expr = subs_constants(constraints(cons_sys)) - - if !haskey(kwargs, :lcons) && !haskey(kwargs, :ucons) # use the symbolically specified bounds - lcons = lcons_ - ucons = ucons_ - else # use the user supplied constraints bounds - (haskey(kwargs, :lcons) ⊻ haskey(kwargs, :ucons)) && - throw(ArgumentError("Expected both `ucons` and `lcons` to be supplied")) - haskey(kwargs, :lcons) && length(kwargs[:lcons]) != length(cstr) && - throw(ArgumentError("Expected `lcons` to be of the same length as the vector of constraints")) - haskey(kwargs, :ucons) && length(kwargs[:ucons]) != length(cstr) && - throw(ArgumentError("Expected `ucons` to be of the same length as the vector of constraints")) - lcons = haskey(kwargs, :lcons) - ucons = haskey(kwargs, :ucons) - end - - if cons_sparse - cons_jac_prototype = jacobian_sparsity(cons_sys) - cons_hess_prototype = hessian_sparsity(cons_sys) - else - cons_jac_prototype = nothing - cons_hess_prototype = nothing - end - _f = DiffEqBase.OptimizationFunction{iip}(f, - sys = sys, - SciMLBase.NoAD(); - grad = _grad, - hess = _hess, - hess_prototype = hess_prototype, - cons = cons, - cons_j = _cons_j, - cons_h = _cons_h, - cons_jac_prototype = cons_jac_prototype, - cons_hess_prototype = cons_hess_prototype, - expr = obj_expr, - cons_expr = cons_expr, - observed = observedfun) - OptimizationProblem{iip}(_f, u0, p; lb = lb, ub = ub, int = int, - lcons = lcons, ucons = ucons, kwargs...) - else - _f = DiffEqBase.OptimizationFunction{iip}(f, - sys = sys, - SciMLBase.NoAD(); - grad = _grad, - hess = _hess, - hess_prototype = hess_prototype, - expr = obj_expr, - observed = observedfun) - OptimizationProblem{iip}(_f, u0, p; lb = lb, ub = ub, int = int, - kwargs...) - end -end - -""" -```julia -DiffEqBase.OptimizationProblemExpr{iip}(sys::OptimizationSystem, - parammap = DiffEqBase.NullParameters(); - u0 = nothing, - grad = false, - hes = false, sparse = false, - checkbounds = false, - linenumbers = true, parallel = SerialForm(), - kwargs...) where {iip} -``` - -Generates a Julia expression for an OptimizationProblem from an -OptimizationSystem and allows for automatically symbolically -calculating numerical enhancements. -""" -struct OptimizationProblemExpr{iip} end - -function OptimizationProblemExpr(sys::OptimizationSystem, args...; kwargs...) - OptimizationProblemExpr{true}(sys::OptimizationSystem, args...; kwargs...) -end - -function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, - parammap = DiffEqBase.NullParameters(); - lb = nothing, ub = nothing, - grad = false, - hess = false, sparse = false, - cons_j = false, cons_h = false, - checkbounds = false, - linenumbers = false, parallel = SerialForm(), - eval_expression = false, eval_module = @__MODULE__, - kwargs...) where {iip} - if !iscomplete(sys) - error("A completed `OptimizationSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `OptimizationProblemExpr`") - end - if haskey(kwargs, :lcons) || haskey(kwargs, :ucons) - Base.depwarn( - "`lcons` and `ucons` are deprecated. Specify constraints directly instead.", - :OptimizationProblem, force = true) - end - - dvs = unknowns(sys) - ps = parameters(sys) - cstr = constraints(sys) - - if isnothing(lb) && isnothing(ub) # use the symbolically specified bounds - lb = first.(getbounds.(dvs)) - ub = last.(getbounds.(dvs)) - isboolean = symtype.(unwrap.(dvs)) .<: Bool - lb[isboolean] .= 0 - ub[isboolean] .= 1 - else # use the user supplied variable bounds - xor(isnothing(lb), isnothing(ub)) && - throw(ArgumentError("Expected both `lb` and `ub` to be supplied")) - !isnothing(lb) && length(lb) != length(dvs) && - throw(ArgumentError("Expected `lb` to be of the same length as the vector of optimization variables")) - !isnothing(ub) && length(ub) != length(dvs) && - throw(ArgumentError("Expected `ub` to be of the same length as the vector of optimization variables")) - end - - int = symtype.(unwrap.(dvs)) .<: Integer - - defs = defaults(sys) - defs = mergedefaults(defs, parammap, ps) - defs = mergedefaults(defs, u0map, dvs) - - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) - else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false) - end - lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false) - ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false) - - if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) - lb = nothing - ub = nothing - end - - idx = iip ? 2 : 1 - f = generate_function(sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}) - if grad - _grad = eval_or_rgf( - generate_gradient( - sys, checkbounds = checkbounds, linenumbers = linenumbers, - parallel = parallel, expression = Val{true})[idx]; - eval_expression, - eval_module) - else - _grad = :nothing - end - - if hess - _hess = eval_or_rgf( - generate_hessian(sys, checkbounds = checkbounds, linenumbers = linenumbers, - sparse = sparse, parallel = parallel, - expression = Val{false})[idx]; - eval_expression, - eval_module) - else - _hess = :nothing - end - - if sparse - hess_prototype = hessian_sparsity(sys) - else - hess_prototype = nothing - end - - obj_expr = toexpr(subs_constants(objective(sys))) - pairs_arr = if p isa SciMLBase.NullParameters - [Symbol(_s) => Expr(:ref, :x, i) for (i, _s) in enumerate(dvs)] - else - vcat([Symbol(_s) => Expr(:ref, :x, i) for (i, _s) in enumerate(dvs)], - [Symbol(_p) => p[i] for (i, _p) in enumerate(ps)]) - end - rep_pars_vals!(obj_expr, pairs_arr) - - if length(cstr) > 0 - @named cons_sys = ConstraintsSystem(cstr, dvs, ps) - cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, - linenumbers = linenumbers, - expression = Val{true}) - cons = eval_or_rgf(cons; eval_expression, eval_module) - if cons_j - _cons_j = eval_or_rgf( - generate_jacobian(cons_sys; expression = Val{true}, sparse = sparse)[2]; - eval_expression, eval_module) - else - _cons_j = nothing - end - if cons_h - _cons_h = eval_or_rgf( - generate_hessian(cons_sys; expression = Val{true}, sparse = sparse)[2]; - eval_expression, eval_module) - else - _cons_h = nothing - end - - cons_expr = toexpr.(subs_constants(constraints(cons_sys))) - rep_pars_vals!.(cons_expr, Ref(pairs_arr)) - - if !haskey(kwargs, :lcons) && !haskey(kwargs, :ucons) # use the symbolically specified bounds - lcons = lcons_ - ucons = ucons_ - else # use the user supplied constraints bounds - (haskey(kwargs, :lcons) ⊻ haskey(kwargs, :ucons)) && - throw(ArgumentError("Expected both `ucons` and `lcons` to be supplied")) - haskey(kwargs, :lcons) && length(kwargs[:lcons]) != length(cstr) && - throw(ArgumentError("Expected `lcons` to be of the same length as the vector of constraints")) - haskey(kwargs, :ucons) && length(kwargs[:ucons]) != length(cstr) && - throw(ArgumentError("Expected `ucons` to be of the same length as the vector of constraints")) - lcons = haskey(kwargs, :lcons) - ucons = haskey(kwargs, :ucons) - end - - if sparse - cons_jac_prototype = jacobian_sparsity(cons_sys) - cons_hess_prototype = hessian_sparsity(cons_sys) - else - cons_jac_prototype = nothing - cons_hess_prototype = nothing - end - - quote - f = $f - p = $p - u0 = $u0 - grad = $_grad - hess = $_hess - lb = $lb - ub = $ub - int = $int - cons = $cons[1] - lcons = $lcons - ucons = $ucons - cons_j = $_cons_j - cons_h = $_cons_h - _f = OptimizationFunction{iip}(f, SciMLBase.NoAD(); - grad = grad, - hess = hess, - hess_prototype = hess_prototype, - cons = cons, - cons_j = cons_j, - cons_h = cons_h, - cons_jac_prototype = cons_jac_prototype, - cons_hess_prototype = cons_hess_prototype, - expr = obj_expr, - cons_expr = cons_expr) - OptimizationProblem{$iip}( - _f, u0, p; lb = lb, ub = ub, int = int, lcons = lcons, - ucons = ucons, kwargs...) - end - else - quote - f = $f - p = $p - u0 = $u0 - grad = $_grad - hess = $_hess - lb = $lb - ub = $ub - int = $int - _f = OptimizationFunction{iip}(f, SciMLBase.NoAD(); - grad = grad, - hess = hess, - hess_prototype = hess_prototype, - expr = obj_expr) - OptimizationProblem{$iip}(_f, u0, p; lb = lb, ub = ub, int = int, kwargs...) - end - end -end - -function structural_simplify(sys::OptimizationSystem; split = true, kwargs...) - sys = flatten(sys) - cons = constraints(sys) - econs = Equation[] - icons = similar(cons, 0) - for e in cons - if e isa Equation - push!(econs, e) - else - push!(icons, e) - end - end - nlsys = NonlinearSystem(econs, unknowns(sys), parameters(sys); name = :___tmp_nlsystem) - snlsys = structural_simplify(nlsys; fully_determined = false, kwargs...) - obs = observed(snlsys) - subs = Dict(eq.lhs => eq.rhs for eq in observed(snlsys)) - seqs = equations(snlsys) - cons_simplified = similar(cons, length(icons) + length(seqs)) - for (i, eq) in enumerate(Iterators.flatten((seqs, icons))) - cons_simplified[i] = fixpoint_sub(eq, subs) - end - newsts = setdiff(unknowns(sys), keys(subs)) - @set! sys.constraints = cons_simplified - @set! sys.observed = [observed(sys); obs] - neweqs = fixpoint_sub.(equations(sys), (subs,)) - @set! sys.op = length(neweqs) == 1 ? first(neweqs) : neweqs - @set! sys.unknowns = newsts - sys = complete(sys; split) - return sys -end - -supports_initialization(::OptimizationSystem) = false diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 0750585905..b3ce01af29 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -633,7 +633,8 @@ All other keyword arguments are forwarded to `InitializationProblem`. function maybe_build_initialization_problem( sys::AbstractSystem, op::AbstractDict, u0map, pmap, t, defs, guesses, missing_unknowns; implicit_dae = false, - u0_constructor = identity, floatT = Float64, kwargs...) + time_dependent_init = is_time_dependent(sys), u0_constructor = identity, + floatT = Float64, kwargs...) guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) if t === nothing && is_time_dependent(sys) @@ -641,7 +642,7 @@ function maybe_build_initialization_problem( end initializeprob = ModelingToolkit.InitializationProblem{true, SciMLBase.FullSpecialize}( - sys, t, u0map, pmap; guesses, kwargs...) + sys, t, u0map, pmap; guesses, time_dependent_init, kwargs...) if state_values(initializeprob) !== nothing initializeprob = remake(initializeprob; u0 = floatT.(state_values(initializeprob))) end @@ -660,7 +661,10 @@ function maybe_build_initialization_problem( meta = get_metadata(initializeprob.f.sys) - if is_time_dependent(sys) + if time_dependent_init === nothing + time_dependent_init = is_time_dependent(sys) + end + if time_dependent_init all_init_syms = Set(all_symbols(initializeprob)) solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) initializeprobmap = u0_constructor ∘ getu(initializeprob, solved_unknowns) @@ -700,7 +704,7 @@ function maybe_build_initialization_problem( end end - if is_time_dependent(sys) + if time_dependent_init for v in missing_unknowns op[v] = get_temporary_value(v, floatT) end @@ -747,12 +751,11 @@ Initial values provided in terms of other variables will be symbolically evaluat type of the containers (if parameters are not in an `MTKParameters` object). `Dict`s will be turned into `Array`s. -If `sys isa ODESystem`, this will also build the initialization problem and related objects -and pass them to the SciMLFunction as keyword arguments. +This will also build the initialization problem and related objects and pass them to the +SciMLFunction as keyword arguments. Keyword arguments: -- `build_initializeprob`: If `false`, avoids building the initialization problem for an - `ODESystem`. +- `build_initializeprob`: If `false`, avoids building the initialization problem. - `t`: The initial time of the `ODEProblem`. If this is not provided, the initialization problem cannot be built. - `implicit_dae`: Also build a mapping of derivatives of states to values for implicit DAEs, @@ -803,7 +806,7 @@ function process_SciMLProblem( symbolic_u0 = false, warn_cyclic_dependency = false, circular_dependency_max_cycle_length = length(all_symbols(sys)), circular_dependency_max_cycles = 10, - substitution_limit = 100, use_scc = true, + substitution_limit = 100, use_scc = true, time_dependent_init = is_time_dependent(sys), force_initialization_time_independent = false, algebraic_only = false, allow_incomplete = false, is_initializeprob = false, kwargs...) dvs = unknowns(sys) @@ -858,7 +861,7 @@ function process_SciMLProblem( warn_cyclic_dependency, check_units = check_initialization_units, circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, force_time_independent = force_initialization_time_independent, algebraic_only, allow_incomplete, - u0_constructor, floatT) + u0_constructor, floatT, time_dependent_init) kwargs = merge(kwargs, kws) end @@ -986,6 +989,110 @@ function SciMLBase.detect_cycles(sys::AbstractSystem, varmap::Dict{Any, Any}, va return !isempty(cycles) end +function process_kwargs(sys::System; callback = nothing, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + kwargs = filter_kwargs(kwargs) + kwargs1 = (;) + + if is_time_dependent(sys) + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) + if cbs !== nothing + kwargs1 = merge(kwargs1, (callback = cbs,)) + end + + tstops = SymbolicTstops(sys; eval_expression, eval_module) + if tstops !== nothing + kwargs1 = merge(kwargs1, (; tstops)) + end + end + + return merge(kwargs1, kwargs) +end + +function filter_kwargs(kwargs) + kwargs = Dict(kwargs) + for key in keys(kwargs) + key in DiffEqBase.allowedkeywords || delete!(kwargs, key) + end + pairs(NamedTuple(kwargs)) +end + +struct SymbolicTstops{F} + fn::F +end + +function (st::SymbolicTstops)(p, tspan) + unique!(sort!(reduce(vcat, st.fn(p, tspan...)))) +end + +function SymbolicTstops( + sys::AbstractSystem; eval_expression = false, eval_module = @__MODULE__) + tstops = symbolic_tstops(sys) + isempty(tstops) && return nothing + t0 = gensym(:t0) + t1 = gensym(:t1) + tstops = map(tstops) do val + if is_array_of_symbolics(val) || val isa AbstractArray + collect(val) + else + term(:, t0, unwrap(val), t1; type = AbstractArray{Real}) + end + end + rps = reorder_parameters(sys) + tstops, _ = build_function_wrapper(sys, tstops, + rps..., + t0, + t1; + expression = Val{true}, + p_start = 1, p_end = length(rps), add_observed = false, force_SA = true) + tstops = eval_or_rgf(tstops; eval_expression, eval_module) + tstops = GeneratedFunctionWrapper{(1, 3, is_split(sys))}(tstops, nothing) + return SymbolicTstops(tstops) +end + +""" + $(TYPEDSIGNATURES) + +Macro for writing problem/function constructors. Expects a function definition with type +parameters for `iip` and `specialize`. Generates fallbacks with +`specialize = SciMLBase.FullSpecialize` and `iip = true`. +""" +macro fallback_iip_specialize(ex) + @assert Meta.isexpr(ex, :function) + fnname, body = ex.args + @assert Meta.isexpr(fnname, :where) + fnname_call, where_args... = fnname.args + @assert length(where_args) == 2 + iiparg, specarg = where_args + + @assert Meta.isexpr(fnname_call, :call) + fnname_curly, args... = fnname_call.args + args = map(args) do arg + Meta.isexpr(arg, :kw) && return arg.args[1] + return arg + end + + @assert Meta.isexpr(fnname_curly, :curly) + fnname_name, curly_args... = fnname_curly.args + @assert curly_args == where_args + + callexpr_iip = Expr( + :call, Expr(:curly, fnname_name, curly_args[1], SciMLBase.FullSpecialize), args...) + fnname_iip = Expr(:curly, fnname_name, curly_args[1]) + fncall_iip = Expr(:call, fnname_iip, args...) + fnwhere_iip = Expr(:where, fncall_iip, where_args[1]) + fn_iip = Expr(:function, fnwhere_iip, callexpr_iip) + + callexpr_base = Expr(:call, Expr(:curly, fnname_name, true), args...) + fncall_base = Expr(:call, fnname_name, args...) + fn_base = Expr(:function, fncall_base, callexpr_base) + return quote + $fn_base + $fn_iip + Base.@__doc__ $ex + end |> esc +end + ############## # Legacy functions for backward compatibility ############## diff --git a/src/systems/system.jl b/src/systems/system.jl new file mode 100644 index 0000000000..a267876888 --- /dev/null +++ b/src/systems/system.jl @@ -0,0 +1,502 @@ +struct Schedule{V <: BipartiteGraphs.Matching} + """ + Maximal matching of variables to equations calculated during structural simplification. + """ + var_eq_matching::V + """ + Mapping of `Differential`s of variables to corresponding derivative expressions. + """ + dummy_sub::Dict{Any, Any} +end + +struct System <: AbstractSystem + tag::UInt + eqs::Vector{Equation} + # nothing - no noise + # vector - diagonal noise + # matrix - generic form + # column matrix - scalar noise + noise_eqs::Union{Nothing, AbstractVector, AbstractMatrix} + jumps::Vector{Any} + constraints::Vector{Union{Equation, Inequality}} + costs::Vector{<:BasicSymbolic} + consolidate::Any + unknowns::Vector + ps::Vector + brownians::Vector + iv::Union{Nothing, BasicSymbolic{Real}} + observed::Vector{Equation} + parameter_dependencies::Vector{Equation} + var_to_name::Dict{Symbol, Any} + name::Symbol + description::String + defaults::Dict + guesses::Dict + systems::Vector{System} + initialization_eqs::Vector{Equation} + continuous_events::Vector{SymbolicContinuousCallback} + discrete_events::Vector{SymbolicDiscreteCallback} + connector_type::Any + assertions::Dict{BasicSymbolic, String} + metadata::Any + gui_metadata::Any # ? + is_dde::Bool + tstops::Vector{Any} + tearing_state::Any + namespacing::Bool + complete::Bool + index_cache::Union{Nothing, IndexCache} + ignored_connections::Union{ + Nothing, Tuple{Vector{IgnoredAnalysisPoint}, Vector{IgnoredAnalysisPoint}}} + parent::Union{Nothing, System} + isscheduled::Bool + schedule::Union{Schedule, Nothing} + + function System( + tag, eqs, noise_eqs, jumps, constraints, costs, consolidate, unknowns, ps, + brownians, iv, observed, parameter_dependencies, var_to_name, name, description, + defaults, guesses, systems, initialization_eqs, continuous_events, discrete_events, + connector_type, assertions = Dict{BasicSymbolic, String}(), + metadata = nothing, gui_metadata = nothing, + is_dde = false, tstops = [], tearing_state = nothing, namespacing = true, + complete = false, index_cache = nothing, ignored_connections = nothing, + parent = nothing, isscheduled = false, schedule = nothing; checks::Union{ + Bool, Int} = true) + if (checks == true || (checks & CheckComponents) > 0) && iv !== nothing + check_independent_variables([iv]) + check_variables(unknowns, iv) + check_parameters(ps, iv) + check_equations(eqs, iv) + if noise_eqs !== nothing && size(noise_eqs, 1) != length(eqs) + throw(IllFormedNoiseEquationsError(size(noise_eqs, 1), length(eqs))) + end + check_equations(equations(continuous_events), iv) + check_subsystems(systems) + end + if checks == true || (checks & CheckUnits) > 0 + u = __get_unit_type(unknowns, ps, iv) + check_units(u, eqs) + noise_eqs !== nothing && check_units(u, noise_eqs) + isempty(constraints) || check_units(u, constraints) + end + new(tag, eqs, noise_eqs, jumps, constraints, costs, + consolidate, unknowns, ps, brownians, iv, + observed, parameter_dependencies, var_to_name, name, description, defaults, + guesses, systems, initialization_eqs, continuous_events, discrete_events, + connector_type, assertions, metadata, gui_metadata, is_dde, + tstops, tearing_state, namespacing, complete, index_cache, ignored_connections, + parent, isscheduled, schedule) + end +end + +function default_consolidate(costs, subcosts) + return sum(costs; init = 0.0) + sum(subcosts; init = 0.0) +end + +function System(eqs, iv, dvs, ps, brownians = []; + constraints = Union{Equation, Inequality}[], noise_eqs = nothing, jumps = [], + costs = BasicSymbolic[], consolidate = default_consolidate, + observed = Equation[], parameter_dependencies = Equation[], defaults = Dict(), + guesses = Dict(), systems = System[], initialization_eqs = Equation[], + continuous_events = SymbolicContinuousCallback[], discrete_events = SymbolicDiscreteCallback[], + connector_type = nothing, assertions = Dict{BasicSymbolic, String}(), + metadata = nothing, gui_metadata = nothing, is_dde = nothing, tstops = [], + tearing_state = nothing, ignored_connections = nothing, parent = nothing, + description = "", name = nothing, discover_from_metadata = true, checks = true) + name === nothing && throw(NoNameError()) + + iv = iv isa Array ? unwrap.(iv) : unwrap(iv) + ps = unwrap.(ps) + dvs = unwrap.(dvs) + filter!(!Base.Fix2(isdelay, iv), dvs) + brownians = unwrap.(brownians) + + if !(eqs isa AbstractArray) + eqs = [eqs] + end + + if noise_eqs !== nothing + noise_eqs = unwrap.(noise_eqs) + end + + parameter_dependencies, ps = process_parameter_dependencies(parameter_dependencies, ps) + defaults = anydict(defaults) + guesses = anydict(guesses) + var_to_name = anydict() + + let defaults = discover_from_metadata ? defaults : Dict(), + guesses = discover_from_metadata ? guesses : Dict() + + process_variables!(var_to_name, defaults, guesses, dvs) + process_variables!(var_to_name, defaults, guesses, ps) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + process_variables!(var_to_name, defaults, guesses, [eq.lhs for eq in observed]) + process_variables!(var_to_name, defaults, guesses, [eq.rhs for eq in observed]) + end + filter!(!(isnothing ∘ last), defaults) + filter!(!(isnothing ∘ last), guesses) + defaults = anydict([unwrap(k) => unwrap(v) for (k, v) in defaults]) + guesses = anydict([unwrap(k) => unwrap(v) for (k, v) in guesses]) + + sysnames = nameof.(systems) + unique_sysnames = Set(sysnames) + if length(unique_sysnames) != length(sysnames) + throw(NonUniqueSubsystemsError(sysnames, unique_sysnames)) + end + + continuous_events = SymbolicContinuousCallbacks(continuous_events) + discrete_events = SymbolicDiscreteCallbacks(discrete_events) + + if iv === nothing && !isempty(continuous_events) || !isempty(discrete_events) + throw(EventsInTimeIndependentSystemError(continuous_events, discrete_events)) + end + + if is_dde === nothing + is_dde = _check_if_dde(eqs, iv, systems) + end + + assertions = Dict{BasicSymbolic, String}(unwrap(k) => v for (k, v) in assertions) + + System(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), eqs, noise_eqs, jumps, constraints, + costs, consolidate, dvs, ps, brownians, iv, observed, parameter_dependencies, + var_to_name, name, description, defaults, guesses, systems, initialization_eqs, + continuous_events, discrete_events, connector_type, assertions, metadata, gui_metadata, is_dde, + tstops, tearing_state, true, false, nothing, ignored_connections, parent; checks) +end + +function System(eqs, iv; kwargs...) + iv === nothing && return System(eqs; kwargs...) + diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) + brownians = Set() + for x in allunknowns + x = unwrap(x) + if getvariabletype(x) == BROWNIAN + push!(brownians, x) + end + end + setdiff!(allunknowns, brownians) + + for eq in get(kwargs, :parameter_dependencies, Equation[]) + collect_vars!(allunknowns, ps, eq, iv) + end + + cstrs = get(kwargs, :constraints, Equation[]) + cstrunknowns, cstrps = process_constraint_system(cstrs, allunknowns, ps, iv) + union!(allunknowns, cstrunknowns) + union!(ps, cstrps) + + for ssys in get(kwargs, :systems, System[]) + collect_scoped_vars!(allunknowns, ps, ssys, iv) + end + + costs = get(kwargs, :costs, nothing) + if costs !== nothing + costunknowns, costps = process_costs(costs, allunknowns, ps, iv) + union!(allunknowns, costunknowns) + union!(ps, costps) + end + + for v in allunknowns + isdelay(v, iv) || continue + collect_vars!(allunknowns, ps, arguments(v)[1], iv) + end + + new_ps = gather_array_params(ps) + algevars = setdiff(allunknowns, diffvars) + + noiseeqs = get(kwargs, :noise_eqs, nothing) + if noiseeqs !== nothing + # validate noise equations + noisedvs = OrderedSet() + noiseps = OrderedSet() + collect_vars!(noisedvs, noiseps, noiseeqs, iv) + for dv in noisedvs + dv ∈ allunknowns || + throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) + end + end + + return System(eqs, iv, collect(Iterators.flatten((diffvars, algevars))), + collect(new_ps), brownians; kwargs...) +end + +function System(eqs; kwargs...) + eqs = collect(eqs) + + allunknowns = OrderedSet() + ps = OrderedSet() + for eq in eqs + collect_vars!(allunknowns, ps, eq, nothing) + end + for eq in get(kwargs, :parameter_dependencies, Equation[]) + collect_vars!(allunknowns, ps, eq, nothing) + end + for ssys in get(kwargs, :systems, System[]) + collect_scoped_vars!(allunknowns, ps, ssys, nothing) + end + + new_ps = gather_array_params(ps) + + return System(eqs, nothing, collect(allunknowns), collect(new_ps); kwargs...) +end + +function gather_array_params(ps) + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + if symbolic_type(p) == ArraySymbolic() && + Symbolics.shape(unwrap(p)) != Symbolics.Unknown() + for i in eachindex(p) + delete!(new_ps, p[i]) + end + end + push!(new_ps, p) + end + end + return new_ps +end + +""" +Process variables in constraints of the (ODE) System. +""" +function process_constraint_system( + constraints::Vector{Equation}, sts, ps, iv; consname = :cons) + isempty(constraints) && return Set(), Set() + + constraintsts = OrderedSet() + constraintps = OrderedSet() + for cons in constraints + collect_vars!(constraintsts, constraintps, cons, iv) + end + + # Validate the states. + validate_vars_and_find_ps!(constraintsts, constraintps, sts, iv) + + return constraintsts, constraintps +end + +""" +Process the costs for the constraint system. +""" +function process_costs(costs::Vector, sts, ps, iv) + coststs = OrderedSet() + costps = OrderedSet() + for cost in costs + collect_vars!(coststs, costps, cost, iv) + end + + validate_vars_and_find_ps!(coststs, costps, sts, iv) + coststs, costps +end + +""" +Validate that all the variables in an auxiliary system of the (ODE) System (constraint or costs) are +well-formed states or parameters. + - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) + - Callable/delay parameters should be parameters of the system + +Return the set of additional parameters found in the system, e.g. in x(p) ~ 3 then p should be added as a +parameter of the system. +""" +function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) + sts = sysvars + + for var in auxvars + if !iscall(var) + occursin(iv, var) && (var ∈ sts || + throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) + elseif length(arguments(var)) > 1 + throw(ArgumentError("Too many arguments for variable $var.")) + elseif length(arguments(var)) == 1 + arg = only(arguments(var)) + operation(var)(iv) ∈ sts || + throw(ArgumentError("Variable $var is not a variable of the System. Called variables must be variables of the System.")) + + isequal(arg, iv) || isparameter(arg) || arg isa Integer || + arg isa AbstractFloat || + throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) + + isparameter(arg) && push!(auxps, arg) + else + var ∈ sts && + @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." + end + end +end + +""" + $(TYPEDSIGNATURES) + +Check if a system is a (possibly implicit) discrete system. Hybrid systems are turned into +callbacks, so checking if any LHS is shifted is sufficient. If a variable is shifted in +the input equations there _will_ be a `Shift` equation in the simplified system. +""" +function is_discrete_system(sys::System) + any(eq -> isoperator(eq.lhs, Shift), equations(sys)) +end + +SymbolicIndexingInterface.is_time_dependent(sys::System) = get_iv(sys) !== nothing + +""" + is_dde(sys::AbstractSystem) + +Return a boolean indicating whether a system represents a set of delay +differential equations. +""" +is_dde(sys::AbstractSystem) = has_is_dde(sys) && get_is_dde(sys) + +function _check_if_dde(eqs, iv, subsystems) + is_dde = any(ModelingToolkit.is_dde, subsystems) + if !is_dde + vs = Set() + for eq in eqs + vars!(vs, eq) + is_dde = any(vs) do sym + isdelay(unwrap(sym), iv) + end + is_dde && break + end + end + return is_dde +end + +function flatten(sys::System, noeqs = false) + systems = get_systems(sys) + isempty(systems) && return sys + + return System(noeqs ? Equation[] : equations(sys), get_iv(sys), unknowns(sys), + parameters(sys; initial_parameters = true), brownians(sys); + jumps = jumps(sys), constraints = constraints(sys), costs = cost(sys), + consolidate = default_consolidate, observed = observed(sys), + parameter_dependencies = parameter_dependencies(sys), defaults = defaults(sys), + guesses = guesses(sys), continuous_events = continuous_events(sys), + discrete_events = discrete_events(sys), assertions = assertions(sys), + is_dde = is_dde(sys), tstops = symbolic_tstops(sys), + ignored_connections = ignored_connections(sys), + # without this, any defaults/guesses obtained from metadata that were + # later removed by the user will be re-added. Right now, we just want to + # retain `defaults(sys)` as-is. + discover_from_metadata = false, + description = description(sys), name = nameof(sys)) +end + +has_massactionjumps(js::System) = any(x -> x isa MassActionJump, jumps(js)) +has_constantratejumps(js::System) = any(x -> x isa ConstantRateJump, jumps(js)) +has_variableratejumps(js::System) = any(x -> x isa VariableRateJump, jumps(js)) +# TODO: do we need this? it's kind of weird to keep +has_equations(js::System) = !isempty(equations(js)) + +# TODO: hash out the semantics of this +function Base.:(==)(sys1::System, sys2::System) + sys1 === sys2 && return true + iv1 = get_iv(sys1) + iv2 = get_iv(sys2) + isequal(iv1, iv2) && + isequal(nameof(sys1), nameof(sys2)) && + _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && + _eq_unordered(get_noise_eqs(sys1), get_noise_eqs(sys2)) && + _eq_unordered(get_jumps(sys1), get_jumps(sys2)) && + _eq_unordered(get_constraints(sys1), get_constraints(sys2)) && + _eq_unordered(get_costs(sys1), get_costs(sys2)) && + _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && + _eq_unordered(get_ps(sys1), get_ps(sys2)) && + _eq_unordered(get_brownians(sys1), get_brownians(sys2)) && + _eq_unordered(get_observed(sys1), get_observed(sys2)) && + _eq_unordered(get_parameter_dependencies(sys1), get_parameter_dependencies(sys2)) && + _eq_unordered(get_continuous_events(sys1), get_continuous_events(sys2)) && + _eq_unordered(get_discrete_events(sys1), get_discrete_events(sys2)) && + get_assertions(sys1) == get_assertions(sys2) && + get_is_dde(sys1) == get_is_dde(sys2) && + get_tstops(sys1) == get_tstops(sys2) && + all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) +end + +""" + $(TYPEDSIGNATURES) +""" +function check_complete(sys::System, obj) + iscomplete(sys) || throw(SystemNotCompleteError(obj)) +end + +struct SystemNotCompleteError <: Exception + obj::Any +end + +function Base.showerror(io::IO, err::SystemNotCompleteError) + print(io, """ + A completed system is required. Call `complete` or `structural_simplify` on the \ + system before creating a `$(err.obj)`. + """) +end + +struct IllFormedNoiseEquationsError <: Exception + noise_eqs_rows::Int + eqs_length::Int +end + +function Base.showerror(io::IO, err::IllFormedNoiseEquationsError) + print(io, """ + Noise equations are ill-formed. The number of rows much must number of drift \ + equations. `size(neqs, 1) == $(err.noise_eqs_rows) != length(eqs) == \ + $(err.eqs_length)`. + """) +end + +function NoNameError() + ArgumentError(""" + The `name` keyword must be provided. Please consider using the `@named` macro. + """) +end + +struct NonUniqueSubsystemsError <: Exception + names::Vector{Symbol} + uniques::Set{Symbol} +end + +function Base.showerror(io::IO, err::NonUniqueSubsystemsError) + dupes = Set{Symbol}() + for n in err.names + if !(n in err.uniques) + push!(dupes, n) + end + delete!(err.uniques, n) + end + println(io, "System names must be unique. The following system names were duplicated:") + for n in dupes + println(io, " ", n) + end +end + +struct EventsInTimeIndependentSystemError <: Exception + cevents::Vector + devents::Vector +end + +function Base.showerror(io::IO, err::EventsInTimeIndependentSystemError) + println(io, """ + Events are not supported in time-indepent systems. Provide an independent variable to \ + make the system time-dependent or remove the events. + + The following continuous events were provided: + $(err.cevents) + + The following discrete events were provided: + $(err.devents) + """) +end + +function supports_initialization(sys::System) + return isempty(jumps(sys)) && !is_discrete_system(sys) && _iszero(cost(sys)) && + isempty(constraints(sys)) +end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 0f8633f31f..f792183508 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -1,8 +1,3 @@ -function System(eqs::AbstractVector{<:Equation}, iv, args...; name = nothing, - kw...) - ODESystem(eqs, iv, args...; name, kw..., checks = false) -end - const REPEATED_SIMPLIFICATION_MESSAGE = "Structural simplification cannot be applied to a completed system. Double simplification is not allowed." struct RepeatedStructuralSimplificationError <: Exception end @@ -49,7 +44,7 @@ function structural_simplify( for pass in additional_passes newsys = pass(newsys) end - if newsys isa ODESystem || has_parent(newsys) + if has_parent(newsys) @set! newsys.parent = complete(sys; split = false, flatten = false) end newsys = complete(newsys; split) @@ -61,16 +56,13 @@ function structural_simplify( end end -function __structural_simplify(sys::JumpSystem, args...; kwargs...) - return sys -end - -function __structural_simplify(sys::SDESystem, args...; kwargs...) - return __structural_simplify(ODESystem(sys), args...; kwargs...) -end - function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = false, kwargs...) + # TODO: convert noise_eqs to brownians for simplification + if has_noise_eqs(sys) && !isempty(get_noise_eqs(sys)) + throw(ArgumentError("Cannot simplify systems with `noise_eqs`")) + end + sys = expand_connections(sys) state = TearingState(sys) @@ -161,6 +153,21 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal end end +function __num_isdiag_noise(mat) + for i in axes(mat, 1) + nnz = 0 + for j in axes(mat, 2) + if !isequal(mat[i, j], 0) + nnz += 1 + end + end + if nnz > 1 + return (false) + end + end + true +end + """ $(TYPEDSIGNATURES) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index d27e5c93a1..1d8036a265 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -9,7 +9,7 @@ import ..ModelingToolkit: isdiffeq, var_from_nested_derivative, vars!, flatten, independent_variables, SparseMatrixCLIL, AbstractSystem, equations, isirreducible, input_timedomain, TimeDomain, InferredTimeDomain, - VariableType, getvariabletype, has_equations, ODESystem + VariableType, getvariabletype, has_equations, System using ..BipartiteGraphs import ..BipartiteGraphs: invview, complete using Graphs @@ -634,53 +634,29 @@ end function structural_simplify!(state::TearingState, io = nothing; simplify = false, check_consistency = true, fully_determined = true, warn_initialize_determined = true, kwargs...) - if state.sys isa ODESystem - ci = ModelingToolkit.ClockInference(state) - ci = ModelingToolkit.infer_clocks!(ci) - time_domains = merge(Dict(state.fullvars .=> ci.var_domain), - Dict(default_toterm.(state.fullvars) .=> ci.var_domain)) - tss, inputs, continuous_id, id_to_clock = ModelingToolkit.split_system(ci) - cont_io = merge_io(io, inputs[continuous_id]) - sys, input_idxs = _structural_simplify!(tss[continuous_id], cont_io; simplify, - check_consistency, fully_determined, - kwargs...) - if length(tss) > 1 - if continuous_id > 0 - throw(HybridSystemNotSupportedException("Hybrid continuous-discrete systems are currently not supported with the standard MTK compiler. This system requires JuliaSimCompiler.jl, see https://help.juliahub.com/juliasimcompiler/stable/")) - end - # TODO: rename it to something else - discrete_subsystems = Vector{ODESystem}(undef, length(tss)) - # Note that the appended_parameters must agree with - # `generate_discrete_affect`! - appended_parameters = parameters(sys) - for (i, state) in enumerate(tss) - if i == continuous_id - discrete_subsystems[i] = sys - continue - end - dist_io = merge_io(io, inputs[i]) - ss, = _structural_simplify!(state, dist_io; simplify, check_consistency, - fully_determined, kwargs...) - append!(appended_parameters, inputs[i], unknowns(ss)) - discrete_subsystems[i] = ss - end - @set! sys.discrete_subsystems = discrete_subsystems, inputs, continuous_id, - id_to_clock - @set! sys.ps = appended_parameters - @set! sys.defaults = merge(ModelingToolkit.defaults(sys), - Dict(v => 0.0 for v in Iterators.flatten(inputs))) + ci = ModelingToolkit.ClockInference(state) + ci = ModelingToolkit.infer_clocks!(ci) + time_domains = merge(Dict(state.fullvars .=> ci.var_domain), + Dict(default_toterm.(state.fullvars) .=> ci.var_domain)) + tss, inputs, continuous_id, id_to_clock = ModelingToolkit.split_system(ci) + if length(tss) > 1 + if continuous_id == 0 + throw(HybridSystemNotSupportedException(""" + Discrete systems with multiple clocks are not supported with the standard \ + MTK compiler. + """)) + else + throw(HybridSystemNotSupportedException(""" + Hybrid continuous-discrete systems are currently not supported with \ + the standard MTK compiler. This system requires JuliaSimCompiler.jl, \ + see https://help.juliahub.com/juliasimcompiler/stable/ + """)) end - ps = [sym isa CallWithMetadata ? sym : - setmetadata( - sym, VariableTimeDomain, get(time_domains, sym, ContinuousClock())) - for sym in get_ps(sys)] - @set! sys.ps = ps - else - sys, input_idxs = _structural_simplify!(state, io; simplify, check_consistency, - fully_determined, kwargs...) end - has_io = io !== nothing - return has_io ? (sys, input_idxs) : sys + + sys, input_idxs = _structural_simplify!(state, io; simplify, check_consistency, + fully_determined, kwargs...) + return io !== nothing ? (sys, input_idxs) : sys end function _structural_simplify!(state::TearingState, io; simplify = false, diff --git a/src/utils.jl b/src/utils.jl index 2b3cbedab0..8dd281b60a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1202,7 +1202,8 @@ end """ $(TYPEDSIGNATURES) -Find all the unknowns and parameters from the equations of a SDESystem or ODESystem. Return re-ordered equations, differential variables, all variables, and parameters. +Find all the unknowns and parameters from the equations of a System. Return re-ordered +equations, differential variables, all variables, and parameters. """ function process_equations(eqs, iv) if eltype(eqs) <: AbstractVector @@ -1238,7 +1239,7 @@ function process_equations(eqs, iv) diffvar, _ = var_from_nested_derivative(eq.lhs) if check_scope_depth(getmetadata(diffvar, SymScope, LocalScope()), 0) isequal(iv, iv_from_nested_derivative(eq.lhs)) || - throw(ArgumentError("An ODESystem can only have one independent variable.")) + throw(ArgumentError("A system of differential equations can only have one independent variable.")) diffvar in diffvars && throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) !has_diffvar_type(diffvar) && @@ -1297,3 +1298,60 @@ function var_in_varlist(var, varlist::AbstractSet, iv) # delayed variables (isdelay(var, iv) && var_in_varlist(operation(var)(iv), varlist, iv)) end + +""" + $(TYPEDSIGNATURES) + +Check if `a` and `b` contain identical elements, regardless of order. This is not +equivalent to `issetequal` because the latter does not account for identical elements that +have different multiplicities in `a` and `b`. +""" +function _eq_unordered(a::AbstractArray, b::AbstractArray) + # a and b may be multidimensional + # e.g. comparing noiseeqs of SDESystem + a = vec(a) + b = vec(b) + length(a) === length(b) || return false + n = length(a) + idxs = Set(1:n) + for x in a + idx = findfirst(isequal(x), b) + # loop since there might be multiple identical entries in a/b + # and while we might have already matched the first there could + # be a second that is equal to x + while idx !== nothing && !(idx in idxs) + idx = findnext(isequal(x), b, idx + 1) + end + idx === nothing && return false + delete!(idxs, idx) + end + return true +end + +""" + $(TYPEDSIGNATURES) + +Given a list of equations where some may be array equations, flatten the array equations +without scalarizing occurrences of array variables and return the new list of equations. +""" +function flatten_equations(eqs::Vector{Equation}) + mapreduce(vcat, eqs; init = Equation[]) do eq + islhsarr = eq.lhs isa AbstractArray || Symbolics.isarraysymbolic(eq.lhs) + isrhsarr = eq.rhs isa AbstractArray || Symbolics.isarraysymbolic(eq.rhs) + if islhsarr || isrhsarr + islhsarr && isrhsarr || + error(""" + LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must either both be array expressions \ + or both scalar + """) + size(eq.lhs) == size(eq.rhs) || + error(""" + Size of LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must match: got \ + $(size(eq.lhs)) and $(size(eq.rhs)) + """) + return vec(collect(eq.lhs) .~ collect(eq.rhs)) + else + eq + end + end +end