diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index f5ae4b1df3..7a7556efa3 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -32,7 +32,7 @@ jobs: - {user: SciML, repo: NeuralPDE.jl, group: NNPDE} - {user: SciML, repo: DataDrivenDiffEq.jl, group: Downstream} - {user: SciML, repo: StructuralIdentifiability.jl, group: All} - - {user: SciML, repo: ModelingToolkitStandardLibrary.jl} + - {user: SciML, repo: ModelingToolkitStandardLibrary.jl, group: Core} - {user: SciML, repo: ModelOrderReduction.jl, group: All} - {user: SciML, repo: MethodOfLines.jl, group: Interface} - {user: SciML, repo: MethodOfLines.jl, group: 2D_Diffusion} diff --git a/Project.toml b/Project.toml index b6fc8d58bc..24632bfaf3 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.8.0" +version = "9.25.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" @@ -32,7 +32,8 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" @@ -63,10 +64,12 @@ MTKDeepDiffsExt = "DeepDiffs" [compat] AbstractTrees = "0.3, 0.4" ArrayInterface = "6, 7" +BifurcationKit = "0.3" Combinatorics = "1" Compat = "3.42, 4" ConstructionBase = "1" DataStructures = "0.17, 0.18" +DeepDiffs = "1" DiffEqBase = "6.103.0" DiffEqCallbacks = "2.16, 3" DiffRules = "0.1, 1.0" @@ -89,7 +92,9 @@ Libdl = "1" LinearAlgebra = "1" MLStyle = "0.4.17" NaNMath = "0.3, 1" -OrdinaryDiffEq = "6.73.0" +NonlinearSolve = "3.12" +OrderedCollections = "1" +OrdinaryDiffEq = "6.82.0" PrecompileTools = "1" RecursiveArrayTools = "2.3, 3" Reexport = "0.2, 1" @@ -102,9 +107,9 @@ SimpleNonlinearSolve = "0.1.0, 1" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -SymbolicIndexingInterface = "0.3.11" -SymbolicUtils = "1.0" -Symbolics = "5.26" +SymbolicIndexingInterface = "0.3.12" +SymbolicUtils = "2.1" +Symbolics = "5.32" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" @@ -115,14 +120,17 @@ AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" +DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Ipopt_jll = "9cc047cb-c261-5740-88fc-0cf96f7bdcc7" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" @@ -136,4 +144,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] diff --git a/docs/Project.toml b/docs/Project.toml index 5d39558eb3..9fecafecd6 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -13,6 +13,7 @@ Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" @@ -27,14 +28,15 @@ Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12" ModelingToolkit = "8.33, 9" -NonlinearSolve = "0.3, 1, 2, 3" +NonlinearSolve = "3" Optim = "1.7" Optimization = "3.9" OptimizationOptimJL = "0.1" OrdinaryDiffEq = "6.31" Plots = "1.36" +SciMLStructures = "1.1" StochasticDiffEq = "6" SymbolicIndexingInterface = "0.3.1" -SymbolicUtils = "1" +SymbolicUtils = "2" Symbolics = "5" Unitful = "1.12" diff --git a/docs/pages.jl b/docs/pages.jl index c3c4adfda6..ff499177bf 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -17,7 +17,8 @@ pages = [ "Basic Examples" => Any["examples/higher_order.md", "examples/spring_mass.md", "examples/modelingtoolkitize_index_reduction.md", - "examples/parsing.md"], + "examples/parsing.md", + "examples/remake.md"], "Advanced Examples" => Any["examples/tearing_parallelism.md", "examples/sparse_jacobians.md", "examples/perturbation.md"]], @@ -30,6 +31,7 @@ pages = [ "basics/MTKModel_Connector.md", "basics/Validation.md", "basics/DependencyGraphs.md", + "basics/Precompilation.md", "basics/FAQ.md"], "System Types" => Any["systems/ODESystem.md", "systems/SDESystem.md", diff --git a/docs/src/basics/FAQ.md b/docs/src/basics/FAQ.md index fa73815059..44f97c2b25 100644 --- a/docs/src/basics/FAQ.md +++ b/docs/src/basics/FAQ.md @@ -186,7 +186,7 @@ p, replace, alias = SciMLStructures.canonicalize(Tunable(), prob.p) This error can come up after running `structural_simplify` on a system that generates dummy derivatives (i.e. variables with `ˍt`). For example, here even though all the variables are defined with initial values, the `ODEProblem` generation will throw an error that defaults are missing from the variable map. -``` +```julia using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @@ -197,13 +197,13 @@ eqs = [x1 + x2 + 1 ~ 0 2 * D(D(x1)) + D(D(x2)) + D(D(x3)) + D(x4) + 4 ~ 0] @named sys = ODESystem(eqs, t) sys = structural_simplify(sys) -prob = ODEProblem(sys, [], (0,1)) +prob = ODEProblem(sys, [], (0, 1)) ``` We can solve this problem by using the `missing_variable_defaults()` function -``` -prob = ODEProblem(sys, ModelingToolkit.missing_variable_defaults(sys), (0,1)) +```julia +prob = ODEProblem(sys, ModelingToolkit.missing_variable_defaults(sys), (0, 1)) ``` This function provides 0 for the default values, which is a safe assumption for dummy derivatives of most models. However, the 2nd argument allows for a different default value or values to be used if needed. @@ -221,12 +221,26 @@ julia> ModelingToolkit.missing_variable_defaults(sys, [1,2,3]) Use the `u0_constructor` keyword argument to map an array to the desired container type. For example: -``` +```julia using ModelingToolkit, StaticArrays using ModelingToolkit: t_nounits as t, D_nounits as D -sts = @variables x1(t)=0.0 +sts = @variables x1(t) = 0.0 eqs = [D(x1) ~ 1.1 * x1] @mtkbuild sys = ODESystem(eqs, t) -prob = ODEProblem{false}(sys, [], (0,1); u0_constructor = x->SVector(x...)) +prob = ODEProblem{false}(sys, [], (0, 1); u0_constructor = x -> SVector(x...)) +``` + +## Using a custom independent variable + +When possible, we recommend `using ModelingToolkit: t_nounits as t, D_nounits as D` as the independent variable and its derivative. +However, if you want to use your own, you can do so: + +```julia +using ModelingToolkit + +@independent_variables x +D = Differential(x) +@variables y(x) +@named sys = ODESystem([D(y) ~ x], x) ``` diff --git a/docs/src/basics/MTKModel_Connector.md b/docs/src/basics/MTKModel_Connector.md index 639f212653..7b92e19ceb 100644 --- a/docs/src/basics/MTKModel_Connector.md +++ b/docs/src/basics/MTKModel_Connector.md @@ -39,6 +39,7 @@ Let's explore these in more detail with the following example: ```@example mtkmodel-example using ModelingToolkit +using ModelingToolkit: t @mtkmodel ModelA begin @parameters begin @@ -191,6 +192,7 @@ getdefault(model_c3.model_a.k_array[2]) ```@example mtkmodel-example using ModelingToolkit +using ModelingToolkit: t @mtkmodel M begin @parameters begin @@ -262,6 +264,7 @@ A simple connector can be defined with syntax similar to following example: ```@example connector using ModelingToolkit +using ModelingToolkit: t @connector Pin begin v(t) = 0.0, [description = "Voltage"] @@ -344,6 +347,7 @@ The if-elseif-else statements can be used inside `@equations`, `@parameters`, ```@example branches-in-components using ModelingToolkit +using ModelingToolkit: t @mtkmodel C begin end @@ -404,12 +408,13 @@ The conditional parts are reflected in the `structure`. For `BranchOutsideTheBlo ```julia julia> BranchOutsideTheBlock.structure -Dict{Symbol, Any} with 6 entries: +Dict{Symbol, Any} with 7 entries: :components => Any[(:if, :flag, Vector{Union{Expr, Symbol}}[[:sys1, :C]], Any[])] :kwargs => Dict{Symbol, Dict}(:flag=>Dict{Symbol, Bool}(:value=>1)) :structural_parameters => Dict{Symbol, Dict}(:flag=>Dict{Symbol, Bool}(:value=>1)) :independent_variable => t - :parameters => Dict{Symbol, Dict{Symbol, Any}}(:a2 => Dict(:type => AbstractArray{Real}, :condition => (:if, :flag, Dict{Symbol, Any}(:kwargs => Dict{Any, Any}(:a1 => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)), :parameters => Any[Dict{Symbol, Dict{Symbol, Any}}(:a1 => Dict(:type => AbstractArray{Real}))]), Dict{Symbol, Any}(:variables => Any[Dict{Symbol, Dict{Symbol, Any}}()], :kwargs => Dict{Any, Any}(:a2 => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)), :parameters => Any[Dict{Symbol, Dict{Symbol, Any}}(:a2 => Dict(:type => AbstractArray{Real}))]))), :a1 => Dict(:type => AbstractArray{Real}, :condition => (:if, :flag, Dict{Symbol, Any}(:kwargs => Dict{Any, Any}(:a1 => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)), :parameters => Any[Dict{Symbol, Dict{Symbol, Any}}(:a1 => Dict(:type => AbstractArray{Real}))]), Dict{Symbol, Any}(:variables => Any[Dict{Symbol, Dict{Symbol, Any}}()], :kwargs => Dict{Any, Any}(:a2 => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)), :parameters => Any[Dict{Symbol, Dict{Symbol, Any}}(:a2 => Dict(:type => AbstractArray{Real}))])))) + :parameters => Dict{Symbol, Dict{Symbol, Any}}(:a2 => Dict(:type=>AbstractArray{Real}, :condition=>(:if, :flag, Dict{Symbol, Any}(:kwargs=>Dict{Any, Any}(:a1=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real)), :parameters=>Any[Dict{Symbol, Dict{Symbol, Any}}(:a1=>Dict(:type=>AbstractArray{Real}))]), Dict{Symbol, Any}(:variables=>Any[Dict{Symbol, Dict{Symbol, Any}}()], :kwargs=>Dict{Any, Any}(:a2=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real)), :parameters=>Any[Dict{Symbol, Dict{Symbol, Any}}(:a2=>Dict(:type=>AbstractArray{Real}))]))), :a1 => Dict(:type=>AbstractArray{Real}, :condition=>(:if, :flag, Dict{Symbol, Any}(:kwargs=>Dict{Any, Any}(:a1=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real)), :parameters=>Any[Dict{Symbol, Dict{Symbol, Any}}(:a1=>Dict(:type=>AbstractArray{Real}))]), Dict{Symbol, Any}(:variables=>Any[Dict{Symbol, Dict{Symbol, Any}}()], :kwargs=>Dict{Any, Any}(:a2=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real)), :parameters=>Any[Dict{Symbol, Dict{Symbol, Any}}(:a2=>Dict(:type=>AbstractArray{Real}))])))) + :defaults => Dict{Symbol, Any}(:a1=>10) :equations => Any[(:if, :flag, ["a1 ~ 0"], ["a2 ~ 0"])] ``` diff --git a/docs/src/basics/Precompilation.md b/docs/src/basics/Precompilation.md new file mode 100644 index 0000000000..97111f0d6b --- /dev/null +++ b/docs/src/basics/Precompilation.md @@ -0,0 +1,117 @@ +# Working with Precompilation and Binary Building + +## tl;dr, I just want precompilation to work + +The tl;dr is, if you want to make precompilation work then instead of + +```julia +ODEProblem(sys, u0, tspan, p) +``` + +use: + +```julia +ODEProblem(sys, u0, tspan, p, eval_module = @__MODULE__, eval_expression = true) +``` + +As a full example, here's an example of a module that would precompile effectively: + +```julia +module PrecompilationMWE +using ModelingToolkit + +@variables x(ModelingToolkit.t_nounits) +@named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x + 1], ModelingToolkit.t_nounits) +prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [], + eval_expression = true, eval_module = @__MODULE__) + +end +``` + +If you use that in your package's code then 99% of the time that's the right answer to get +precompilation working. + +## I'm doing something fancier and need a bit more of an explanation + +Oh you dapper soul, time for the bigger explanation. Julia's `eval` function evaluates a +function into a module at a specified world-age. If you evaluate a function within a function +and try to call it from within that same function, you will hit a world-age error. This looks like: + +```julia +function worldageerror() + f = eval(:((x) -> 2x)) + f(2) +end +``` + +``` +julia> worldageerror() +ERROR: MethodError: no method matching (::var"#5#6")(::Int64) + +Closest candidates are: + (::var"#5#6")(::Any) (method too new to be called from this world context.) + @ Main REPL[12]:2 +``` + +This is done for many reasons, in particular if the code that is called within a function could change +at any time, then Julia functions could not ever properly optimize because the meaning of any function +or dispatch could always change and you would lose performance by guarding against that. For a full +discussion of world-age, see [this paper](https://arxiv.org/abs/2010.07516). + +However, this would be greatly inhibiting to standard ModelingToolkit usage because then something as +simple as building an ODEProblem in a function and then using it would get a world age error: + +```julia +function wouldworldage() + prob = ODEProblem(sys, [], (0.0, 1.0)) + sol = solve(prob) +end +``` + +The reason is because `prob.f` would be constructed via `eval`, and thus `prob.f` could not be called +in the function, which means that no solve could ever work in the same function that generated the +problem. That does mean that: + +```julia +function wouldworldage() + prob = ODEProblem(sys, [], (0.0, 1.0)) +end +sol = solve(prob) +``` + +is fine, or putting + +```julia +prob = ODEProblem(sys, [], (0.0, 1.0)) +sol = solve(prob) +``` + +at the top level of a module is perfectly fine too. They just cannot happen in the same function. + +This would be a major limitation to ModelingToolkit, and thus we developed +[RuntimeGeneratedFunctions](https://github.com/SciML/RuntimeGeneratedFunctions.jl) to get around +this limitation. It will not be described beyond that, it is dark art and should not be investigated. +But it does the job. But that does mean that it plays... oddly with Julia's compilation. + +There are ways to force RuntimeGeneratedFunctions to perform their evaluation and caching within +a given module, but that is not recommended because it does not play nicely with Julia v1.9's +introduction of package images for binary caching. + +Thus when trying to make things work with precompilation, we recommend using `eval`. This is +done by simply adding `eval_expression=true` to the problem constructor. However, this is not +a silver bullet because the moment you start using eval, all potential world-age restrictions +apply, and thus it is recommended this is simply used for evaluating at the top level of modules +for the purpose of precompilation and ensuring binaries of your MTK functions are built correctly. + +However, there is one caveat that `eval` in Julia works depending on the module that it is given. +If you have `MyPackage` that you are precompiling into, or say you are using `juliac` or PackageCompiler +or some other static ahead-of-time (AOT) Julia compiler, then you don't want to accidentally `eval` +that function to live in ModelingToolkit and instead want to make sure it is `eval`'d to live in `MyPackage` +(since otherwise it will not cache into the binary). ModelingToolkit cannot know that in advance, and thus +you have to pass in the module you wish for the functions to "live" in. This is done via the `eval_module` +argument. + +Hence `ODEProblem(sys, u0, tspan, p, eval_module=@__MODULE__, eval_expression=true)` will work if you +are running this expression in the scope of the module you wish to be precompiling. However, if you are +attempting to AOT compile a different module, this means that `eval_module` needs to be appropriately +chosen. And, because `eval_expression=true`, all caveats of world-age apply. diff --git a/docs/src/basics/Validation.md b/docs/src/basics/Validation.md index c9c662f13b..79c5d0d214 100644 --- a/docs/src/basics/Validation.md +++ b/docs/src/basics/Validation.md @@ -8,15 +8,19 @@ Units may be assigned with the following syntax. ```@example validation using ModelingToolkit, DynamicQuantities -@variables t [unit = u"s"] x(t) [unit = u"m"] g(t) w(t) [unit = "Hz"] +@independent_variables t [unit = u"s"] +@variables x(t) [unit = u"m"] g(t) w(t) [unit = u"Hz"] -@variables(t, [unit = u"s"], x(t), [unit = u"m"], g(t), w(t), [unit = "Hz"]) +@parameters(t, [unit = u"s"]) +@variables(x(t), [unit = u"m"], g(t), w(t), [unit = u"Hz"]) +@parameters begin + t, [unit = u"s"] +end @variables(begin - t, [unit = u"s"], x(t), [unit = u"m"], g(t), - w(t), [unit = "Hz"] + w(t), [unit = u"Hz"] end) # Simultaneously set default value (use plain numbers, not quantities) @@ -46,10 +50,11 @@ Example usage below. Note that `ModelingToolkit` does not force unit conversions ```@example validation using ModelingToolkit, DynamicQuantities +@independent_variables t [unit = u"ms"] @parameters τ [unit = u"ms"] -@variables t [unit = u"ms"] E(t) [unit = u"kJ"] P(t) [unit = u"MW"] +@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] D = Differential(t) -eqs = eqs = [D(E) ~ P - E / τ, +eqs = [D(E) ~ P - E / τ, 0 ~ P] ModelingToolkit.validate(eqs) ``` @@ -70,10 +75,11 @@ An example of an inconsistent system: at present, `ModelingToolkit` requires tha ```@example validation using ModelingToolkit, DynamicQuantities +@independent_variables t [unit = u"ms"] @parameters τ [unit = u"ms"] -@variables t [unit = u"ms"] E(t) [unit = u"J"] P(t) [unit = u"MW"] +@variables E(t) [unit = u"J"] P(t) [unit = u"MW"] D = Differential(t) -eqs = eqs = [D(E) ~ P - E / τ, +eqs = [D(E) ~ P - E / τ, 0 ~ P] ModelingToolkit.validate(eqs) #Returns false while displaying a warning message ``` @@ -115,7 +121,8 @@ In order for a function to work correctly during both validation & execution, th ```julia using ModelingToolkit, DynamicQuantities -@variables t [unit = u"ms"] E(t) [unit = u"J"] P(t) [unit = u"MW"] +@independent_variables t [unit = u"ms"] +@variables E(t) [unit = u"J"] P(t) [unit = u"MW"] D = Differential(t) eqs = [D(E) ~ P - E / 1u"ms"] ModelingToolkit.validate(eqs) #Returns false while displaying a warning message @@ -129,8 +136,9 @@ Instead, they should be parameterized: ```@example validation3 using ModelingToolkit, DynamicQuantities +@independent_variables t [unit = u"ms"] @parameters τ [unit = u"ms"] -@variables t [unit = u"ms"] E(t) [unit = u"kJ"] P(t) [unit = u"MW"] +@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] D = Differential(t) eqs = [D(E) ~ P - E / τ] ModelingToolkit.validate(eqs) #Returns true diff --git a/docs/src/examples/remake.md b/docs/src/examples/remake.md new file mode 100644 index 0000000000..f4396ef7a7 --- /dev/null +++ b/docs/src/examples/remake.md @@ -0,0 +1,158 @@ +# Optimizing through an ODE solve and re-creating MTK Problems + +Solving an ODE as part of an `OptimizationProblem`'s loss function is a common scenario. +In this example, we will go through an efficient way to model such scenarios using +ModelingToolkit.jl. + +First, we build the ODE to be solved. For this example, we will use a Lotka-Volterra model: + +```@example Remake +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@parameters α β γ δ +@variables x(t) y(t) +eqs = [D(x) ~ (α - β * y) * x + D(y) ~ (δ * x - γ) * y] +@mtkbuild odesys = ODESystem(eqs, t) +``` + +To create the "data" for optimization, we will solve the system with a known set of +parameters. + +```@example Remake +using OrdinaryDiffEq + +odeprob = ODEProblem( + odesys, [x => 1.0, y => 1.0], (0.0, 10.0), [α => 1.5, β => 1.0, γ => 3.0, δ => 1.0]) +timesteps = 0.0:0.1:10.0 +sol = solve(odeprob, Tsit5(); saveat = timesteps) +data = Array(sol) +# add some random noise +data = data + 0.01 * randn(size(data)) +``` + +Now we will create the loss function for the Optimization solve. This will require creating +an `ODEProblem` with the parameter values passed to the loss function. Creating a new +`ODEProblem` is expensive and requires differentiating through the code generation process. +This can be bug-prone and is unnecessary. Instead, we will leverage the `remake` function. +This allows creating a copy of an existing problem with updating state/parameter values. It +should be noted that the types of the values passed to the loss function may not agree with +the types stored in the existing `ODEProblem`. Thus, we cannot use `setp` to modify the +problem in-place. Here, we will use the `replace` function from SciMLStructures.jl since +it allows updating the entire `Tunable` portion of the parameter object which contains the +parameters to optimize. + +```@example Remake +using SymbolicIndexingInterface: parameter_values, state_values +using SciMLStructures: Tunable, replace, replace! + +function loss(x, p) + odeprob = p[1] # ODEProblem stored as parameters to avoid using global variables + ps = parameter_values(odeprob) # obtain the parameter object from the problem + ps = replace(Tunable(), ps, x) # create a copy with the values passed to the loss function + # remake the problem, passing in our new parameter object + newprob = remake(odeprob; p = ps) + timesteps = p[2] + sol = solve(newprob, AutoTsit5(Rosenbrock23()); saveat = timesteps) + truth = p[3] + data = Array(sol) + return sum((truth .- data) .^ 2) / length(truth) +end +``` + +Note how the problem, timesteps and true data are stored as model parameters. This helps +avoid referencing global variables in the function, which would slow it down significantly. + +We could have done the same thing by passing `remake` a map of parameter values. For example, +let us enforce that the order of ODE parameters in `x` is `[α β γ δ]`. Then, we could have +done: + +```julia +remake(odeprob; p = [α => x[1], β => x[2], γ => x[3], δ => x[4]]) +``` + +However, passing a symbolic map to `remake` is significantly slower than passing it a +parameter object directly. Thus, we use `replace` to speed up the process. In general, +`remake` is the most flexible method, but the flexibility comes at a cost of performance. + +We can perform the optimization as below: + +```@example Remake +using Optimization +using OptimizationOptimJL + +# manually create an OptimizationFunction to ensure usage of `ForwardDiff`, which will +# require changing the types of parameters from `Float64` to `ForwardDiff.Dual` +optfn = OptimizationFunction(loss, Optimization.AutoForwardDiff()) +# parameter object is a tuple, to store differently typed objects together +optprob = OptimizationProblem( + optfn, rand(4), (odeprob, timesteps, data), lb = 0.1zeros(4), ub = 3ones(4)) +sol = solve(optprob, BFGS()) +``` + +To identify which values correspond to which parameters, we can `replace!` them into the +`ODEProblem`: + +```@example Remake +replace!(Tunable(), parameter_values(odeprob), sol.u) +odeprob.ps[[α, β, γ, δ]] +``` + +`replace!` operates in-place, so the values being replaced must be of the same type as those +stored in the parameter object, or convertible to that type. For demonstration purposes, we +can construct a loss function that uses `replace!`, and calculate gradients using +`AutoFiniteDiff` rather than `AutoForwardDiff`. + +```@example Remake +function loss2(x, p) + odeprob = p[1] # ODEProblem stored as parameters to avoid using global variables + newprob = remake(odeprob) # copy the problem with `remake` + # update the parameter values in-place + replace!(Tunable(), parameter_values(newprob), x) + timesteps = p[2] + sol = solve(newprob, AutoTsit5(Rosenbrock23()); saveat = timesteps) + truth = p[3] + data = Array(sol) + return sum((truth .- data) .^ 2) / length(truth) +end + +# use finite-differencing to calculate derivatives +optfn2 = OptimizationFunction(loss2, Optimization.AutoFiniteDiff()) +optprob2 = OptimizationProblem( + optfn2, rand(4), (odeprob, timesteps, data), lb = 0.1zeros(4), ub = 3ones(4)) +sol = solve(optprob2, BFGS()) +``` + +# Re-creating the problem + +There are multiple ways to re-create a problem with new state/parameter values. We will go +over the various methods, listing their use cases. + +## Pure `remake` + +This method is the most generic. It can handle symbolic maps, initializations of +parameters/states dependent on each other and partial updates. However, this comes at the +cost of performance. `remake` is also not always inferable. + +## `remake` and `setp`/`setu` + +Calling `remake(prob)` creates a copy of the existing problem. This new problem has the +exact same types as the original one, and the `remake` call is fully inferred. +State/parameter values can be modified after the copy by using `setp` and/or `setu`. This +is most appropriate when the types of state/parameter values does not need to be changed, +only their values. + +## `replace` and `remake` + +`replace` returns a copy of a parameter object, with the appropriate portion replaced by new +values. This is useful for changing the type of an entire portion, such as during the +optimization process described above. `remake` is used in this case to create a copy of the +problem with updated state/unknown values. + +## `remake` and `replace!` + +`replace!` is similar to `replace`, except that it operates in-place. This means that the +parameter values must be of the same types. This is useful for cases where bulk parameter +replacement is required without needing to change types. For example, optimization methods +where the gradient is not computed using dual numbers (as demonstrated above). diff --git a/docs/src/systems/JumpSystem.md b/docs/src/systems/JumpSystem.md index a83f741eb9..5bd0d50602 100644 --- a/docs/src/systems/JumpSystem.md +++ b/docs/src/systems/JumpSystem.md @@ -12,6 +12,7 @@ JumpSystem - `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the jump system. - `get_ps(sys)` or `parameters(sys)`: The parameters of the jump system. - `get_iv(sys)`: The independent variable of the jump system. + - `discrete_events(sys)`: The set of discrete events in the jump system. ## Transformations diff --git a/docs/src/systems/ODESystem.md b/docs/src/systems/ODESystem.md index 241b68b2b6..6cc34725c4 100644 --- a/docs/src/systems/ODESystem.md +++ b/docs/src/systems/ODESystem.md @@ -13,6 +13,16 @@ ODESystem - `get_ps(sys)` or `parameters(sys)`: The parameters of the ODE. - `get_iv(sys)`: The independent variable of the ODE. - `get_u0_p(sys, u0map, parammap)` Numeric arrays for the initial condition and parameters given `var => value` maps. + - `continuous_events(sys)`: The set of continuous events in the ODE. + - `discrete_events(sys)`: The set of discrete events in the ODE. + - `alg_equations(sys)`: The algebraic equations (i.e. that does not contain a differential) that defines the ODE. + - `get_alg_eqs(sys)`: The algebraic equations (i.e. that does not contain a differential) that defines the ODE. Only returns equations of the current-level system. + - `diff_equations(sys)`: The differential equations (i.e. that contain a differential) that defines the ODE. + - `get_diff_eqs(sys)`: The differential equations (i.e. that contain a differential) that defines the ODE. Only returns equations of the current-level system. + - `has_alg_equations(sys)`: Returns `true` if the ODE contains any algebraic equations (i.e. that does not contain a differential). + - `has_alg_eqs(sys)`: Returns `true` if the ODE contains any algebraic equations (i.e. that does not contain a differential). Only considers the current-level system. + - `has_diff_equations(sys)`: Returns `true` if the ODE contains any differential equations (i.e. that does contain a differential). + - `has_diff_eqs(sys)`: Returns `true` if the ODE contains any differential equations (i.e. that does contain a differential). Only considers the current-level system. ## Transformations diff --git a/docs/src/systems/SDESystem.md b/docs/src/systems/SDESystem.md index 455f79689e..5789d2d9cb 100644 --- a/docs/src/systems/SDESystem.md +++ b/docs/src/systems/SDESystem.md @@ -19,6 +19,16 @@ sde = SDESystem(ode, noiseeqs) - `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the SDE. - `get_ps(sys)` or `parameters(sys)`: The parameters of the SDE. - `get_iv(sys)`: The independent variable of the SDE. + - `continuous_events(sys)`: The set of continuous events in the SDE. + - `discrete_events(sys)`: The set of discrete events in the SDE. + - `alg_equations(sys)`: The algebraic equations (i.e. that does not contain a differential) that defines the ODE. + - `get_alg_eqs(sys)`: The algebraic equations (i.e. that does not contain a differential) that defines the ODE. Only returns equations of the current-level system. + - `diff_equations(sys)`: The differential equations (i.e. that contain a differential) that defines the ODE. + - `get_diff_eqs(sys)`: The differential equations (i.e. that contain a differential) that defines the ODE. Only returns equations of the current-level system. + - `has_alg_equations(sys)`: Returns `true` if the ODE contains any algebraic equations (i.e. that does not contain a differential). + - `has_alg_eqs(sys)`: Returns `true` if the ODE contains any algebraic equations (i.e. that does not contain a differential). Only considers the current-level system. + - `has_diff_equations(sys)`: Returns `true` if the ODE contains any differential equations (i.e. that does contain a differential). + - `has_diff_eqs(sys)`: Returns `true` if the ODE contains any differential equations (i.e. that does contain a differential). Only considers the current-level system. ## Transformations diff --git a/docs/src/tutorials/SampledData.md b/docs/src/tutorials/SampledData.md index d2d9294bdb..614e8b65c7 100644 --- a/docs/src/tutorials/SampledData.md +++ b/docs/src/tutorials/SampledData.md @@ -97,7 +97,8 @@ H(z) = \dfrac{b_2 z^2 + b_1 z + b_0}{a_2 z^2 + a_1 z + a_0} may thus be modeled as ```julia -@variables t y(t) [description = "Output"] u(t) [description = "Input"] +t = ModelingToolkit.t_nounits +@variables y(t) [description = "Output"] u(t) [description = "Input"] k = ShiftIndex(Clock(t, dt)) eqs = [ a2 * y(k) + a1 * y(k - 1) + a0 * y(k - 2) ~ b2 * u(k) + b1 * u(k - 1) + b0 * u(k - 2) diff --git a/docs/src/tutorials/discrete_system.md b/docs/src/tutorials/discrete_system.md index 666125e20e..8f6828fde7 100644 --- a/docs/src/tutorials/discrete_system.md +++ b/docs/src/tutorials/discrete_system.md @@ -43,7 +43,7 @@ the Fibonacci series: ``` The "default value" here should be interpreted as the value of `x` at all past timesteps. -For example, here `x(k-1)` and `x(k-2)` will be `1.0`, and the inital value of `x(k)` will +For example, here `x(k-1)` and `x(k-2)` will be `1.0`, and the initial value of `x(k)` will thus be `2.0`. During problem construction, the _past_ value of a variable should be provided. For example, providing `[x => 1.0]` while constructing this problem will error. Provide `[x(k-1) => 1.0]` instead. Note that values provided during problem construction diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 8af6a512e2..f40c28991f 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -194,6 +194,13 @@ may not be analytically satisfiable!**. In our case here, if you sit down with a long enough you will see that `λ = 0` is required for this equation, but since we chose `λ = 1` we end up with a set of equations that are impossible to satisfy. +!!! note + + If you would prefer to have an error instead of a warning in the context of non-fully + determined systems, pass the keyword argument `fully_determined = true` into the + problem constructor. Additionally, any warning about not being fully determined can + be suppressed via passing `warn_initialize_determined = false`. + ## Diving Deeper: Constructing the Initialization System To get a better sense of the initialization system and to help debug it, you can construct diff --git a/docs/src/tutorials/nonlinear.md b/docs/src/tutorials/nonlinear.md index 2043bd4be1..fc525cc988 100644 --- a/docs/src/tutorials/nonlinear.md +++ b/docs/src/tutorials/nonlinear.md @@ -9,24 +9,20 @@ We use (unknown) variables for our nonlinear system. ```@example nonlinear using ModelingToolkit, NonlinearSolve +# Define a nonlinear system @variables x y z @parameters σ ρ β +eqs = [0 ~ σ * (y - x) + 0 ~ x * (ρ - z) - y + 0 ~ x * y - β * z] +guesses = [x => 1.0, y => 0.0, z => 0.0] +ps = [σ => 10.0, ρ => 26.0, β => 8 / 3] +@mtkbuild ns = NonlinearSystem(eqs) -# Define a nonlinear system -eqs = [0 ~ σ * (y - x), - 0 ~ x * (ρ - z) - y, - 0 ~ x * y - β * z] -@mtkbuild ns = NonlinearSystem(eqs, [x, y, z], [σ, ρ, β]) - -guess = [x => 1.0, - y => 0.0, - z => 0.0] - -ps = [σ => 10.0 - ρ => 26.0 - β => 8 / 3] +guesses = [x => 1.0, y => 0.0, z => 0.0] +ps = [σ => 10.0, ρ => 26.0, β => 8 / 3] -prob = NonlinearProblem(ns, guess, ps) +prob = NonlinearProblem(ns, guesses, ps) sol = solve(prob, NewtonRaphson()) ``` @@ -34,6 +30,6 @@ We can similarly ask to generate the `NonlinearProblem` with the analytical Jacobian function: ```@example nonlinear -prob = NonlinearProblem(ns, guess, ps, jac = true) +prob = NonlinearProblem(ns, guesses, ps, jac = true) sol = solve(prob, NewtonRaphson()) ``` diff --git a/docs/src/tutorials/ode_modeling.md b/docs/src/tutorials/ode_modeling.md index ef354071d9..2448f88ac6 100644 --- a/docs/src/tutorials/ode_modeling.md +++ b/docs/src/tutorials/ode_modeling.md @@ -315,9 +315,9 @@ plot(solve(prob)) More on this topic may be found in [Composing Models and Building Reusable Components](@ref acausal). -## Initial Guess +## Default Initial Condition -It is often a good idea to specify reasonable values for the initial unknown and the +It is often a good idea to specify reasonable values for the initial value of unknowns and the parameters of a model component. Then, these do not have to be explicitly specified when constructing the `ODEProblem`. ```@example ode2 @@ -334,15 +334,15 @@ parameters of a model component. Then, these do not have to be explicitly specif end ``` -While defining the model `UnitstepFOLFactory`, an initial guess of 0.0 is assigned to `x(t)` and 1.0 to `τ`. -Additionally, these initial guesses can be modified while creating instances of `UnitstepFOLFactory` by passing arguments. +While defining the model `UnitstepFOLFactory`, an initial condition of 0.0 is assigned to `x(t)` and 1.0 to `τ`. +Additionally, these initial conditions can be modified while creating instances of `UnitstepFOLFactory` by passing arguments. ```@example ode2 @mtkbuild fol = UnitstepFOLFactory(; x = 0.1) sol = ODEProblem(fol, [], (0.0, 5.0), []) |> solve ``` -In non-DSL definitions, one can pass `defaults` dictionary to set the initial guess of the symbolic variables. +In non-DSL definitions, one can pass `defaults` dictionary to set the initial conditions of the symbolic variables. ```@example ode3 using ModelingToolkit diff --git a/ext/MTKBifurcationKitExt.jl b/ext/MTKBifurcationKitExt.jl index e687cb9adf..b709b2eec0 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/ext/MTKBifurcationKitExt.jl @@ -27,7 +27,7 @@ struct ObservableRecordFromSolution{S, T} plot_var, bif_idx, u0_vals, - p_vals) where {S, T} + p_vals) obs_eqs = observed(nsys) target_obs_idx = findfirst(isequal(plot_var, eq.lhs) for eq in observed(nsys)) state_end_idxs = length(unknowns(nsys)) @@ -103,8 +103,9 @@ function BifurcationKit.BifurcationProblem(nsys::NonlinearSystem, # Converts the input state guess. u0_bif_vals = ModelingToolkit.varmap_to_vars(u0_bif, unknowns(nsys); - defaults = nsys.defaults) - p_vals = ModelingToolkit.varmap_to_vars(ps, parameters(nsys); defaults = nsys.defaults) + defaults = ModelingToolkit.get_defaults(nsys)) + p_vals = ModelingToolkit.varmap_to_vars( + ps, parameters(nsys); defaults = ModelingToolkit.get_defaults(nsys)) # Computes bifurcation parameter and the plotting function. bif_idx = findfirst(isequal(bif_par), parameters(nsys)) diff --git a/format/Project.toml b/format/Project.toml deleted file mode 100644 index f3aab8b8bf..0000000000 --- a/format/Project.toml +++ /dev/null @@ -1,2 +0,0 @@ -[deps] -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" diff --git a/mo.diff b/mo.diff new file mode 100644 index 0000000000..abb2a3b037 --- /dev/null +++ b/mo.diff @@ -0,0 +1,582 @@ +27,28d26 +< Base.parentmodule(m::Model) = parentmodule(m.f) +< +40,46c38,40 +< dict = Dict{Symbol, Any}( +< :constants => Dict{Symbol, Dict}(), +< :defaults => Dict{Symbol, Any}(), +< :kwargs => Dict{Symbol, Dict}(), +< :structural_parameters => Dict{Symbol, Dict}() +< ) +< comps = Union{Symbol, Expr}[] +--- +> dict = Dict{Symbol, Any}() +> dict[:kwargs] = Dict{Symbol, Any}() +> comps = Symbol[] +51,52d44 +< c_evts = [] +< d_evts = [] +54d45 +< where_types = Expr[] +60d50 +< push!(exprs.args, :(defaults = Dict{Num, Union{Number, Symbol, Function}}())) +66c56 +< sps, c_evts, d_evts, dict, mod, arg, kwargs, where_types) +--- +> sps, dict, mod, arg, kwargs) +73,74c63 +< mod, ps, vs, where_types, +< parse_top_level_branch(condition, x.args)...) +--- +> mod, ps, vs, parse_top_level_branch(condition, x.args)...) +78,79c67 +< mod, ps, vs, where_types, +< parse_top_level_branch(condition, x.args, y)...) +--- +> mod, ps, vs, parse_top_level_branch(condition, x.args, y)...) +86,87c74 +< parse_variable_arg!( +< exprs.args, vs, dict, mod, arg, :variables, kwargs, where_types) +--- +> parse_variable_arg!(exprs.args, vs, dict, mod, arg, :variables, kwargs) +95c82 +< iv = dict[:independent_variable] = get_t(mod, :t) +--- +> iv = dict[:independent_variable] = variable(:t) +106,108d92 +< @inline pop_structure_dict!.( +< Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) +< +110c94 +< name, systems, gui_metadata = $gui_metadata, defaults)) +--- +> name, systems, gui_metadata = $gui_metadata)) +121,139c105 +< !isempty(c_evts) && push!(exprs.args, +< :($Setfield.@set!(var"#___sys___".continuous_events=$SymbolicContinuousCallback.([ +< $(c_evts...) +< ])))) +< +< !isempty(d_evts) && push!(exprs.args, +< :($Setfield.@set!(var"#___sys___".discrete_events=$SymbolicDiscreteCallback.([ +< $(d_evts...) +< ])))) +< +< f = if length(where_types) == 0 +< :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) +< else +< f_with_where = Expr(:where) +< push!(f_with_where.args, +< :($(Symbol(:__, name, :__))(; name, $(kwargs...))), where_types...) +< :($f_with_where = $exprs) +< end +< +--- +> f = :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) +143,169c109,110 +< pop_structure_dict!(dict, key) = length(dict[key]) == 0 && pop!(dict, key) +< +< function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, +< varclass, where_types) +< if indices isa Nothing +< push!(kwargs, Expr(:kw, Expr(:(::), a, Union{Nothing, type}), nothing)) +< dict[:kwargs][getname(var)] = Dict(:value => def, :type => type) +< else +< vartype = gensym(:T) +< push!(kwargs, +< Expr(:kw, +< Expr(:(::), a, +< Expr(:curly, :Union, :Nothing, Expr(:curly, :AbstractArray, vartype))), +< nothing)) +< push!(where_types, :($vartype <: $type)) +< dict[:kwargs][getname(var)] = Dict(:value => def, :type => AbstractArray{type}) +< end +< if dict[varclass] isa Vector +< dict[varclass][1][getname(var)][:type] = AbstractArray{type} +< else +< dict[varclass][getname(var)][:type] = type +< end +< end +< +< function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; +< def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, +< type::Type = Real) +--- +> function parse_variable_def!(dict, mod, arg, varclass, kwargs; +> def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing) +182c123,125 +< (:dist, VariableDistribution)] +--- +> (:dist, VariableDistribution), +> (:binary, VariableBinary), +> (:integer, VariableInteger)] +187,199c130,133 +< var = generate_var!(dict, a, varclass; indices, type) +< update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, +< varclass, where_types) +< return var, def, Dict() +< end +< Expr(:(::), a, type) => begin +< type = getfield(mod, type) +< parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; def, type) +< end +< Expr(:(::), Expr(:call, a, b), type) => begin +< type = getfield(mod, type) +< def = _type_check!(def, a, type, varclass) +< parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; def, type) +--- +> push!(kwargs, Expr(:kw, a, nothing)) +> var = generate_var!(dict, a, varclass; indices) +> dict[:kwargs][getname(var)] = def +> (var, def, Dict()) +202,205c136,139 +< var = generate_var!(dict, a, b, varclass, mod; indices, type) +< update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, +< varclass, where_types) +< return var, def, Dict() +--- +> push!(kwargs, Expr(:kw, a, nothing)) +> var = generate_var!(dict, a, b, varclass; indices) +> dict[:kwargs][getname(var)] = def +> (var, def, Dict()) +211,217c145,146 +< var, def, _ = parse_variable_def!( +< dict, mod, a, varclass, kwargs, where_types; def, type) +< if dict[varclass] isa Vector +< dict[varclass][1][getname(var)][:default] = def +< else +< dict[varclass][getname(var)][:default] = def +< end +--- +> var, def, _ = parse_variable_def!(dict, mod, a, varclass, kwargs; def) +> dict[varclass][getname(var)][:default] = def +222d150 +< key == VariableConnectType && (mt = nameof(mt)) +236,237c164,165 +< var, def, _ = parse_variable_def!( +< dict, mod, a, varclass, kwargs, where_types; type) +--- +> @info 166 a b +> var, def, _ = parse_variable_def!(dict, mod, a, varclass, kwargs) +257,258c185,186 +< parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; +< def, indices, type) +--- +> parse_variable_def!(dict, mod, a, varclass, kwargs; +> def, indices) +265,268c193,194 +< indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, +< type = Real) +< var = indices === nothing ? Symbolics.variable(a; T = type) : +< first(@variables $a[indices...]::type) +--- +> indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing) +> var = indices === nothing ? Symbolics.variable(a) : first(@variables $a[indices...]) +276,277c202 +< indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, +< type = Real) +--- +> indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing) +284c209 +< generate_var(a, varclass; indices, type) +--- +> generate_var(a, varclass; indices) +287,290c212,214 +< function generate_var!(dict, a, b, varclass, mod; +< indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, +< type = Real) +< iv = b == :t ? get_t(mod, b) : generate_var(b, :variables) +--- +> function generate_var!(dict, a, b, varclass; +> indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing) +> iv = generate_var(b, :variables) +301c225 +< Symbolics.variable(a, T = SymbolicUtils.FnType{Tuple{Any}, type})(iv) +--- +> Symbolics.variable(a, T = SymbolicUtils.FnType{Tuple{Any}, Real})(iv) +304c228 +< first(@variables $a(iv)[indices...]::type) +--- +> first(@variables $a(iv)[indices...]) +312,325d235 +< # Use the `t` defined in the `mod`. When it is unavailable, generate a new `t` with a warning. +< function get_t(mod, t) +< try +< get_var(mod, t) +< catch e +< if e isa UndefVarError +< @warn("Could not find a predefined `t` in `$mod`; generating a new one within this model.\nConsider defining it or importing `t` (or `t_nounits`, `t_unitful` as `t`) from ModelingToolkit.") +< variable(:t) +< else +< throw(e) +< end +< end +< end +< +365a276 +> @info typeof(m) typeof(v) m v +380,381c291,292 +< function parse_model!(exprs, comps, ext, eqs, icon, vs, ps, sps, c_evts, d_evts, +< dict, mod, arg, kwargs, where_types) +--- +> function parse_model!(exprs, comps, ext, eqs, icon, vs, ps, sps, +> dict, mod, arg, kwargs) +389c300 +< parse_variables!(exprs, vs, dict, mod, body, :variables, kwargs, where_types) +--- +> parse_variables!(exprs, vs, dict, mod, body, :variables, kwargs) +391c302 +< parse_variables!(exprs, ps, dict, mod, body, :parameters, kwargs, where_types) +--- +> parse_variables!(exprs, ps, dict, mod, body, :parameters, kwargs) +396,401d306 +< elseif mname == Symbol("@constants") +< parse_constants!(exprs, dict, body, mod) +< elseif mname == Symbol("@continuous_events") +< parse_continuous_events!(c_evts, dict, body) +< elseif mname == Symbol("@discrete_events") +< parse_discrete_events!(d_evts, dict, body) +405,406d309 +< elseif mname == Symbol("@defaults") +< parse_system_defaults!(exprs, arg, dict) +412,476d314 +< function parse_constants!(exprs, dict, body, mod) +< Base.remove_linenums!(body) +< for arg in body.args +< MLStyle.@match arg begin +< Expr(:(=), Expr(:(::), a, type), Expr(:tuple, b, metadata)) || Expr(:(=), Expr(:(::), a, type), b) => begin +< type = getfield(mod, type) +< b = _type_check!(get_var(mod, b), a, type, :constants) +< push!(exprs, +< :($(Symbolics._parse_vars( +< :constants, type, [:($a = $b), metadata], toconstant)))) +< dict[:constants][a] = Dict(:value => b, :type => type) +< if @isdefined metadata +< for data in metadata.args +< dict[:constants][a][data.args[1]] = data.args[2] +< end +< end +< end +< Expr(:(=), a, Expr(:tuple, b, metadata)) => begin +< push!(exprs, +< :($(Symbolics._parse_vars( +< :constants, Real, [:($a = $b), metadata], toconstant)))) +< dict[:constants][a] = Dict{Symbol, Any}(:value => get_var(mod, b)) +< for data in metadata.args +< dict[:constants][a][data.args[1]] = data.args[2] +< end +< end +< Expr(:(=), a, b) => begin +< push!(exprs, +< :($(Symbolics._parse_vars( +< :constants, Real, [:($a = $b)], toconstant)))) +< dict[:constants][a] = Dict(:value => get_var(mod, b)) +< end +< _ => error("""Malformed constant definition `$arg`. Please use the following syntax: +< ``` +< @constants begin +< var = value, [description = "This is an example constant."] +< end +< ``` +< """) +< end +< end +< end +< +< push_additional_defaults!(dict, a, b::Number) = dict[:defaults][a] = b +< push_additional_defaults!(dict, a, b::QuoteNode) = dict[:defaults][a] = b.value +< function push_additional_defaults!(dict, a, b::Expr) +< dict[:defaults][a] = readable_code(b) +< end +< +< function parse_system_defaults!(exprs, defaults_body, dict) +< for default_arg in defaults_body.args[end].args +< # for arg in default_arg.args +< MLStyle.@match default_arg begin +< # For cases like `p => 1` and `p => f()`. In both cases the definitions of +< # `a`, here `p` and when `b` is a function, here `f` are available while +< # defining the model +< Expr(:call, :(=>), a, b) => begin +< push!(exprs, :(defaults[$a] = $b)) +< push_additional_defaults!(dict, a, b) +< end +< _ => error("Invalid `defaults` entry $default_arg $(typeof(a)) $(typeof(b))") +< end +< end +< end +< +481,488d318 +< Expr(:(=), Expr(:(::), a, type), b) => begin +< type = getfield(mod, type) +< b = _type_check!(get_var(mod, b), a, type, :structural_parameters) +< push!(sps, a) +< push!(kwargs, Expr(:kw, Expr(:(::), a, type), b)) +< dict[:structural_parameters][a] = dict[:kwargs][a] = Dict( +< :value => b, :type => type) +< end +492c322 +< dict[:structural_parameters][a] = dict[:kwargs][a] = Dict(:value => b) +--- +> dict[:kwargs][a] = b +497c327 +< dict[:structural_parameters][a] = dict[:kwargs][a] = Dict(:value => nothing) +--- +> dict[:kwargs][a] = nothing +521c351 +< dict[:kwargs][x] = Dict(:value => nothing) +--- +> dict[:kwargs][x] = nothing +525c355 +< dict[:kwargs][x] = Dict(:value => nothing) +--- +> dict[:kwargs][x] = nothing +531c361 +< dict[:kwargs][x] = Dict(:value => nothing) +--- +> dict[:kwargs][x] = nothing +601,602c431,432 +< function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_types) +< name, ex = parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) +--- +> function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs) +> name, ex = parse_variable_arg(dict, mod, arg, varclass, kwargs) +607,608c437,438 +< function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) +< vv, def, metadata_with_exprs = parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types) +--- +> function parse_variable_arg(dict, mod, arg, varclass, kwargs) +> vv, def, metadata_with_exprs = parse_variable_def!(dict, mod, arg, varclass, kwargs) +628,629c458 +< function handle_conditional_vars!( +< arg, conditional_branch, mod, varclass, kwargs, where_types) +--- +> function handle_conditional_vars!(arg, conditional_branch, mod, varclass, kwargs) +634,635c463 +< name, ex = parse_variable_arg( +< conditional_dict, mod, _arg, varclass, kwargs, where_types) +--- +> name, ex = parse_variable_arg(conditional_dict, mod, _arg, varclass, kwargs) +693c521 +< function parse_variables!(exprs, vs, dict, mod, body, varclass, kwargs, where_types) +--- +> function parse_variables!(exprs, vs, dict, mod, body, varclass, kwargs) +705,706c533 +< kwargs, +< where_types) +--- +> kwargs) +716,717c543 +< kwargs, +< where_types) +--- +> kwargs) +722c548 +< kwargs, where_types) +--- +> kwargs) +731,732c557 +< _ => parse_variable_arg!( +< exprs, vs, dict, mod, arg, varclass, kwargs, where_types) +--- +> _ => parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs) +737c562 +< function handle_y_vars(y, dict, mod, varclass, kwargs, where_types) +--- +> function handle_y_vars(y, dict, mod, varclass, kwargs) +744,747c569,570 +< kwargs, +< where_types) +< _y_expr, _conditional_dict = handle_y_vars( +< y.args[end], dict, mod, varclass, kwargs, where_types) +--- +> kwargs) +> _y_expr, _conditional_dict = handle_y_vars(y.args[end], dict, mod, varclass, kwargs) +752c575 +< handle_conditional_vars!(y, conditional_y_expr, mod, varclass, kwargs, where_types) +--- +> handle_conditional_vars!(y, conditional_y_expr, mod, varclass, kwargs) +813,830d635 +< function parse_continuous_events!(c_evts, dict, body) +< dict[:continuous_events] = [] +< Base.remove_linenums!(body) +< for arg in body.args +< push!(c_evts, arg) +< push!(dict[:continuous_events], readable_code.(c_evts)...) +< end +< end +< +< function parse_discrete_events!(d_evts, dict, body) +< dict[:discrete_events] = [] +< Base.remove_linenums!(body) +< for arg in body.args +< push!(d_evts, arg) +< push!(dict[:discrete_events], readable_code.(d_evts)...) +< end +< end +< +856c661 +< function component_args!(a, b, varexpr, kwargs; index_name = nothing) +--- +> function component_args!(a, b, expr, varexpr, kwargs) +865,876c670,674 +< varname, _varname = _rename(a, x) +< b.args[i] = Expr(:kw, x, _varname) +< push!(varexpr.args, :((if $varname !== nothing +< $_varname = $varname +< elseif @isdefined $x +< # Allow users to define a var in `structural_parameters` and set +< # that as positional arg of subcomponents; it is useful for cases +< # where it needs to be passed to multiple subcomponents. +< $_varname = $x +< end))) +< push!(kwargs, Expr(:kw, varname, nothing)) +< # dict[:kwargs][varname] = nothing +--- +> _v = _rename(a, x) +> b.args[i] = Expr(:kw, x, _v) +> push!(varexpr.args, :((@isdefined $x) && ($_v = $x))) +> push!(kwargs, Expr(:kw, _v, nothing)) +> # dict[:kwargs][_v] = nothing +879c677 +< component_args!(a, arg, varexpr, kwargs) +--- +> component_args!(a, arg, expr, varexpr, kwargs) +882,891c680,684 +< varname, _varname = _rename(a, x) +< b.args[i] = Expr(:kw, x, _varname) +< if isnothing(index_name) +< push!(varexpr.args, :($_varname = $varname === nothing ? $y : $varname)) +< else +< push!(varexpr.args, +< :($_varname = $varname === nothing ? $y : $varname[$index_name])) +< end +< push!(kwargs, Expr(:kw, varname, nothing)) +< # dict[:kwargs][varname] = nothing +--- +> _v = _rename(a, x) +> b.args[i] = Expr(:kw, x, _v) +> push!(varexpr.args, :($_v = $_v === nothing ? $y : $_v)) +> push!(kwargs, Expr(:kw, _v, nothing)) +> # dict[:kwargs][_v] = nothing +898,901c691,692 +< model_name(name, range) = Symbol.(name, :_, collect(range)) +< +< function _parse_components!(body, kwargs) +< local expr +--- +> function _parse_components!(exprs, body, kwargs) +> expr = Expr(:block) +903c694,695 +< comps = Vector{Union{Union{Expr, Symbol}, Expr}}[] +--- +> # push!(exprs, varexpr) +> comps = Vector{Union{Symbol, Expr}}[] +906,908c698,699 +< Base.remove_linenums!(body) +< arg = body.args[end] +< +--- +> for arg in body.args +> arg isa LineNumberNode && continue +910,927d700 +< Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:(=), c, d)))) => begin +< array_varexpr = Expr(:block) +< +< push!(comp_names, :($a...)) +< push!(comps, [a, b.args[1], d]) +< b = deepcopy(b) +< +< component_args!(a, b, array_varexpr, kwargs; index_name = c) +< +< expr = _named_idxs(a, d, :($c -> $b); extra_args = array_varexpr) +< end +< Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:filter, e, Expr(:(=), c, d))))) => begin +< error("List comprehensions with conditional statements aren't supported.") +< end +< Expr(:(=), a, Expr(:comprehension, Expr(:generator, b, Expr(:(=), c, d), e...))) => begin +< # Note that `e` is of the form `Tuple{Expr(:(=), c, d)}` +< error("More than one index isn't supported while building component array") +< end +932,943d704 +< Expr(:(=), a, Expr(:for, Expr(:(=), c, d), b)) => begin +< Base.remove_linenums!(b) +< array_varexpr = Expr(:block) +< push!(array_varexpr.args, b.args[1:(end - 1)]...) +< push!(comp_names, :($a...)) +< push!(comps, [a, b.args[end].args[1], d]) +< b = deepcopy(b) +< +< component_args!(a, b.args[end], array_varexpr, kwargs; index_name = c) +< +< expr = _named_idxs(a, d, :($c -> $(b.args[end])); extra_args = array_varexpr) +< end +948c709 +< component_args!(a, b, varexpr, kwargs) +--- +> component_args!(a, b, expr, varexpr, kwargs) +951c712 +< expr = :(@named $arg) +--- +> push!(expr.args, arg) +959c720 +< +--- +> end +966c727,729 +< push!(blk.args, expr_vec) +--- +> push!(blk.args, :(@named begin +> $(expr_vec.args...) +> end)) +973c736 +< comp_names, comps, expr_vec, varexpr = _parse_components!(x, kwargs) +--- +> comp_names, comps, expr_vec, varexpr = _parse_components!(ifexpr, x, kwargs) +989c752 +< comp_names, comps, expr_vec, varexpr = _parse_components!(y, kwargs) +--- +> comp_names, comps, expr_vec, varexpr = _parse_components!(exprs, y, kwargs) +1014,1016c777,779 +< # Either the arg is top level component declaration or an invalid cause - both are handled by `_parse_components` +< _ => begin +< comp_names, comps, expr_vec, varexpr = _parse_components!(:(begin +--- +> Expr(:(=), a, b) => begin +> comp_names, comps, expr_vec, varexpr = _parse_components!(exprs, +> :(begin +1022c785,787 +< push!(exprs, varexpr, expr_vec) +--- +> push!(exprs, varexpr, :(@named begin +> $(expr_vec.args...) +> end)) +1023a789 +> _ => error("Couldn't parse the component body $compbody") +1030d795 +< (compname, Symbol(:_, compname)) +1091c856 +< ps, vs, where_types, component_blk, equations_blk, parameter_blk, variable_blk) +--- +> ps, vs, component_blk, equations_blk, parameter_blk, variable_blk) +1096c861 +< end), :parameters, kwargs, where_types) +--- +> end), :parameters, kwargs) +1102c867 +< end), :variables, kwargs, where_types) +--- +> end), :variables, kwargs) +1114,1126d878 +< end +< +< function _type_check!(val, a, type, class) +< if val isa type +< return val +< else +< try +< return convert(type, val) +< catch e +< throw(TypeError(Symbol("`@mtkmodel`"), +< "`$class`, while assigning to `$a`", type, typeof(val))) +< end +< end diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 71fd768231..94d49918d7 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -4,74 +4,76 @@ $(DocStringExtensions.README) module ModelingToolkit using PrecompileTools, Reexport @recompile_invalidations begin - using DocStringExtensions - using Compat - using AbstractTrees - using DiffEqBase, SciMLBase, ForwardDiff - using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap - using Distributed - using StaticArrays, LinearAlgebra, SparseArrays, LabelledArrays - using InteractiveUtils - using Latexify, Unitful, ArrayInterface - using Setfield, ConstructionBase - using JumpProcesses - using DataStructures - using SpecialFunctions, NaNMath - using RuntimeGeneratedFunctions - using RuntimeGeneratedFunctions: drop_expr - using Base.Threads - using DiffEqCallbacks - using Graphs - import ExprTools: splitdef, combinedef - import Libdl - using DocStringExtensions - using Base: RefValue - using Combinatorics - import Distributions - import FunctionWrappersWrappers - using URIs: URI - using SciMLStructures - - using RecursiveArrayTools - - using SymbolicIndexingInterface - export independent_variables, unknowns, parameters, full_parameters - import SymbolicUtils - import SymbolicUtils: istree, arguments, operation, similarterm, promote_symtype, - Symbolic, isadd, ismul, ispow, issym, FnType, - @rule, Rewriters, substitute, metadata, BasicSymbolic, - Sym, Term - using SymbolicUtils.Code - import SymbolicUtils.Code: toexpr - import SymbolicUtils.Rewriters: Chain, Postwalk, Prewalk, Fixpoint - import JuliaFormatter - - using MLStyle - - using Reexport + using StaticArrays using Symbolics - using Symbolics: degree - using Symbolics: _parse_vars, value, @derivatives, get_variables, - exprs_occur_in, solve_for, build_expr, unwrap, wrap, - VariableSource, getname, variable, Connection, connect, - NAMESPACE_SEPARATOR, set_scalar_metadata, setdefaultval, - initial_state, transition, activeState, entry, - ticksInState, timeInState, fixpoint_sub, fast_substitute - import Symbolics: rename, get_variables!, _solve, hessian_sparsity, - jacobian_sparsity, isaffine, islinear, _iszero, _isone, - tosymbol, lower_varname, diff2term, var_from_nested_derivative, - BuildTargets, JuliaTarget, StanTarget, CTarget, MATLABTarget, - ParallelForm, SerialForm, MultithreadedForm, build_function, - rhss, lhss, prettify_expr, gradient, - jacobian, hessian, derivative, sparsejacobian, sparsehessian, - substituter, scalarize, getparent, hasderiv, hasdiff - - import DiffEqBase: @add_kwonly - import OrdinaryDiffEq - - import Graphs: SimpleDiGraph, add_edge!, incidence_matrix end +import SymbolicUtils +import SymbolicUtils: iscall, arguments, operation, maketerm, promote_symtype, + Symbolic, isadd, ismul, ispow, issym, FnType, + @rule, Rewriters, substitute, metadata, BasicSymbolic, + Sym, Term +using SymbolicUtils.Code +import SymbolicUtils.Code: toexpr +import SymbolicUtils.Rewriters: Chain, Postwalk, Prewalk, Fixpoint +using DocStringExtensions +using SpecialFunctions, NaNMath +using DiffEqCallbacks +using Graphs +import ExprTools: splitdef, combinedef +import OrderedCollections + +using SymbolicIndexingInterface +using LinearAlgebra, SparseArrays, LabelledArrays +using InteractiveUtils +using JumpProcesses +using DataStructures +using Base.Threads +using Latexify, Unitful, ArrayInterface +using Setfield, ConstructionBase +import Libdl +using DocStringExtensions +using Base: RefValue +using Combinatorics +import Distributions +import FunctionWrappersWrappers +using URIs: URI +using SciMLStructures +using Compat +using AbstractTrees +using DiffEqBase, SciMLBase, ForwardDiff +using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap +using Distributed +import JuliaFormatter +using MLStyle +using NonlinearSolve +using Reexport +using RecursiveArrayTools +import Graphs: SimpleDiGraph, add_edge!, incidence_matrix + +using RuntimeGeneratedFunctions +using RuntimeGeneratedFunctions: drop_expr + +using Symbolics: degree +using Symbolics: _parse_vars, value, @derivatives, get_variables, + exprs_occur_in, solve_for, build_expr, unwrap, wrap, + VariableSource, getname, variable, Connection, connect, + NAMESPACE_SEPARATOR, set_scalar_metadata, setdefaultval, + initial_state, transition, activeState, entry, hasnode, + ticksInState, timeInState, fixpoint_sub, fast_substitute +const NAMESPACE_SEPARATOR_SYMBOL = Symbol(NAMESPACE_SEPARATOR) +import Symbolics: rename, get_variables!, _solve, hessian_sparsity, + jacobian_sparsity, isaffine, islinear, _iszero, _isone, + tosymbol, lower_varname, diff2term, var_from_nested_derivative, + BuildTargets, JuliaTarget, StanTarget, CTarget, MATLABTarget, + ParallelForm, SerialForm, MultithreadedForm, build_function, + rhss, lhss, prettify_expr, gradient, + jacobian, hessian, derivative, sparsejacobian, sparsehessian, + substituter, scalarize, getparent, hasderiv, hasdiff + +import DiffEqBase: @add_kwonly +export independent_variables, unknowns, parameters, full_parameters, continuous_events, + discrete_events @reexport using Symbolics @reexport using UnPack RuntimeGeneratedFunctions.init(@__MODULE__) @@ -125,6 +127,7 @@ using .BipartiteGraphs include("variables.jl") include("parameters.jl") +include("independent_variables.jl") include("constants.jl") include("utils.jl") @@ -181,13 +184,13 @@ for S in subtypes(ModelingToolkit.AbstractSystem) end const t_nounits = let - only(@parameters t) + only(@independent_variables t) end const t_unitful = let - only(@parameters t [unit = Unitful.u"s"]) + only(@independent_variables t [unit = Unitful.u"s"]) end const t = let - only(@parameters t [unit = DQ.u"s"]) + only(@independent_variables t [unit = DQ.u"s"]) end const D_nounits = Differential(t_nounits) @@ -233,8 +236,8 @@ export Differential, expand_derivatives, @derivatives export Equation, ConstrainedEquation export Term, Sym export SymScope, LocalScope, ParentScope, DelayParentScope, GlobalScope -export independent_variable, equations, controls, - observed, full_equations +export independent_variable, equations, controls, observed, full_equations +export initialization_equations, guesses, defaults, parameter_dependencies export structural_simplify, expand_connections, linearize, linearization_function export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function @@ -255,16 +258,19 @@ export toexpr, get_variables export simplify, substitute export build_function export modelingtoolkitize -export initializesystem, generate_initializesystem +export generate_initializesystem -export @variables, @parameters, @constants, @brownian +export alg_equations, diff_equations, has_alg_equations, has_diff_equations +export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs + +export @variables, @parameters, @independent_variables, @constants, @brownian export @named, @nonamespace, @namespace, extend, compose, complete export debug_system #export Continuous, Discrete, sampletime, input_timedomain, output_timedomain #export has_discrete_domain, has_continuous_domain #export is_discrete_domain, is_continuous_domain, is_hybrid_domain -export Sample, Hold, Shift, ShiftIndex +export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime export Clock #, InferredDiscrete, end # module diff --git a/src/clock.jl b/src/clock.jl index 7ca1707724..5df6cfb022 100644 --- a/src/clock.jl +++ b/src/clock.jl @@ -21,7 +21,7 @@ function is_continuous_domain(x) end function get_time_domain(x) - if istree(x) && operation(x) isa Operator + if iscall(x) && operation(x) isa Operator output_timedomain(x) else getmetadata(x, TimeDomain, nothing) @@ -130,7 +130,7 @@ is_concrete_time_domain(x) = x isa Union{AbstractClock, Continuous} SolverStepClock() SolverStepClock(t) -A clock that ticks at each solver step (sometimes referred to as "continuous sample time"). This clock **does generally not have equidistant tick intervals**, instead, the tick interval depends on the adaptive step-size slection of the continuous solver, as well as any continuous event handling. If adaptivity of the solver is turned off and there are no continuous events, the tick interval will be given by the fixed solver time step `dt`. +A clock that ticks at each solver step (sometimes referred to as "continuous sample time"). This clock **does generally not have equidistant tick intervals**, instead, the tick interval depends on the adaptive step-size selection of the continuous solver, as well as any continuous event handling. If adaptivity of the solver is turned off and there are no continuous events, the tick interval will be given by the fixed solver time step `dt`. Due to possibly non-equidistant tick intervals, this clock should typically not be used with discrete-time systems that assume a fixed sample time, such as PID controllers and digital filters. """ diff --git a/src/constants.jl b/src/constants.jl index bd2c6508fc..a0a38fd057 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -27,6 +27,8 @@ toconstant(s::Num) = wrap(toconstant(value(s))) $(SIGNATURES) Define one or more constants. + +See also [`@independent_variables`](@ref), [`@parameters`](@ref) and [`@variables`](@ref). """ macro constants(xs...) Symbolics._parse_vars(:constants, diff --git a/src/debugging.jl b/src/debugging.jl index 6fd75052d0..3e72fdd0e5 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -28,9 +28,9 @@ end debug_sub(eq::Equation) = debug_sub(eq.lhs) ~ debug_sub(eq.rhs) function debug_sub(ex) - istree(ex) || return ex + iscall(ex) || return ex f = operation(ex) args = map(debug_sub, arguments(ex)) f in LOGGED_FUN ? logged_fun(f, args...) : - similarterm(ex, f, args, metadata = metadata(ex)) + maketerm(typeof(ex), f, args, symtype(t), metadata(ex)) end diff --git a/src/discretedomain.jl b/src/discretedomain.jl index 68e8e17b03..cb723e159f 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -1,5 +1,12 @@ using Symbolics: Operator, Num, Term, value, recursive_hasoperator +struct SampleTime <: Operator + SampleTime() = SymbolicUtils.term(SampleTime, type = Real) +end +SymbolicUtils.promote_symtype(::Type{<:SampleTime}, t...) = Real +Base.nameof(::SampleTime) = :SampleTime +SymbolicUtils.isbinop(::SampleTime) = false + # Shift """ @@ -15,8 +22,6 @@ $(FIELDS) ```jldoctest julia> using Symbolics -julia> @variables t; - julia> Δ = Shift(t) (::Shift) (generic function with 2 methods) ``` @@ -29,6 +34,9 @@ struct Shift <: Operator end Shift(steps::Int) = new(nothing, steps) normalize_to_differential(s::Shift) = Differential(s.t)^s.steps +Base.nameof(::Shift) = :Shift +SymbolicUtils.isbinop(::Shift) = false + function (D::Shift)(x, allow_zero = false) !allow_zero && D.steps == 0 && return x Term{symtype(x)}(D, Any[x]) @@ -36,7 +44,7 @@ end function (D::Shift)(x::Num, allow_zero = false) !allow_zero && D.steps == 0 && return x vt = value(x) - if istree(vt) + if iscall(vt) op = operation(vt) if op isa Sample error("Cannot shift a `Sample`. Create a variable to represent the sampled value and shift that instead") @@ -90,7 +98,7 @@ $(FIELDS) ```jldoctest julia> using Symbolics -julia> @variables t; +julia> t = ModelingToolkit.t_nounits julia> Δ = Sample(t, 0.01) (::Sample) (generic function with 2 methods) @@ -105,6 +113,8 @@ Sample(x) = Sample()(x) (D::Sample)(x) = Term{symtype(x)}(D, Any[x]) (D::Sample)(x::Num) = Num(D(value(x))) SymbolicUtils.promote_symtype(::Sample, x) = x +Base.nameof(::Sample) = :Sample +SymbolicUtils.isbinop(::Sample) = false Base.show(io::IO, D::Sample) = print(io, "Sample(", D.clock, ")") @@ -134,6 +144,8 @@ end (D::Hold)(x) = Term{symtype(x)}(D, Any[x]) (D::Hold)(x::Num) = Num(D(value(x))) SymbolicUtils.promote_symtype(::Hold, x) = x +Base.nameof(::Hold) = :Hold +SymbolicUtils.isbinop(::Hold) = false Hold(x) = Hold()(x) @@ -154,7 +166,9 @@ The `ShiftIndex` operator allows you to index a signal and obtain a shifted disc # Examples ``` -julia> @variables t x(t); +julia> t = ModelingToolkit.t_nounits; + +julia> @variables x(t); julia> k = ShiftIndex(t, 0.1); @@ -176,7 +190,6 @@ end function (xn::Num)(k::ShiftIndex) @unpack clock, steps = k x = value(xn) - t = clock.t # Verify that the independent variables of k and x match and that the expression doesn't have multiple variables vars = Symbolics.get_variables(x) length(vars) == 1 || @@ -184,8 +197,11 @@ function (xn::Num)(k::ShiftIndex) args = Symbolics.arguments(vars[]) # args should be one element vector with the t in x(t) length(args) == 1 || error("Cannot shift an expression with multiple independent variables $x.") - isequal(args[], t) || - error("Independent variable of $xn is not the same as that of the ShiftIndex $(k.t)") + t = args[] + if hasfield(typeof(clock), :t) + isequal(t, clock.t) || + error("Independent variable of $xn is not the same as that of the ShiftIndex $(k.t)") + end # d, _ = propagate_time_domain(xn) # if d != clock # this is only required if the variable has another clock diff --git a/src/independent_variables.jl b/src/independent_variables.jl new file mode 100644 index 0000000000..beff5cc2b0 --- /dev/null +++ b/src/independent_variables.jl @@ -0,0 +1,11 @@ +""" + @independent_variables t₁ t₂ ... + +Define one or more independent variables. For example: + + @independent_variables t + @variables x(t) +""" +macro independent_variables(ts...) + :(@parameters $(ts...)) |> esc # TODO: treat independent variables separately from variables and parameters +end diff --git a/src/inputoutput.jl b/src/inputoutput.jl index b486bb664c..e07d2ed976 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -19,8 +19,8 @@ function outputs(sys) lhss = [eq.lhs for eq in o] unique([filter(isoutput, unknowns(sys)) filter(isoutput, parameters(sys)) - filter(x -> istree(x) && isoutput(x), rhss) # observed can return equations with complicated expressions, we are only looking for single Terms - filter(x -> istree(x) && isoutput(x), lhss)]) + filter(x -> iscall(x) && isoutput(x), rhss) # observed can return equations with complicated expressions, we are only looking for single Terms + filter(x -> iscall(x) && isoutput(x), lhss)]) end """ @@ -119,8 +119,8 @@ function same_or_inner_namespace(u, var) nv = get_namespace(var) nu == nv || # namespaces are the same startswith(nv, nu) || # or nv starts with nu, i.e., nv is an inner namespace to nu - occursin('₊', string(getname(var))) && - !occursin('₊', string(getname(u))) # or u is top level but var is internal + occursin(NAMESPACE_SEPARATOR, string(getname(var))) && + !occursin(NAMESPACE_SEPARATOR, string(getname(u))) # or u is top level but var is internal end function inner_namespace(u, var) @@ -128,8 +128,8 @@ function inner_namespace(u, var) nv = get_namespace(var) nu == nv && return false startswith(nv, nu) || # or nv starts with nu, i.e., nv is an inner namespace to nu - occursin('₊', string(getname(var))) && - !occursin('₊', string(getname(u))) # or u is top level but var is internal + occursin(NAMESPACE_SEPARATOR, string(getname(var))) && + !occursin(NAMESPACE_SEPARATOR, string(getname(u))) # or u is top level but var is internal end """ @@ -139,11 +139,11 @@ Return the namespace of a variable as a string. If the variable is not namespace """ function get_namespace(x) sname = string(getname(x)) - parts = split(sname, '₊') + parts = split(sname, NAMESPACE_SEPARATOR) if length(parts) == 1 return "" end - join(parts[1:(end - 1)], '₊') + join(parts[1:(end - 1)], NAMESPACE_SEPARATOR) end """ @@ -195,6 +195,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu disturbance_inputs = disturbances(sys); implicit_dae = false, simplify = false, + eval_expression = false, + eval_module = @__MODULE__, kwargs...) isempty(inputs) && @warn("No unbound inputs were found in system.") @@ -240,7 +242,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu end process = get_postprocess_fbody(sys) f = build_function(rhss, args...; postprocess_fbody = process, - expression = Val{false}, kwargs...) + expression = Val{true}, kwargs...) + f = eval_or_rgf.(f; eval_expression, eval_module) (; f, dvs, ps, io_sys = sys) end @@ -395,7 +398,7 @@ model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.i `f_oop` will have an extra state corresponding to the integrator in the disturbance model. This state will not be affected by any input, but will affect the dynamics from where it enters, in this case it will affect additively from `model.torque.tau.u`. """ -function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing) +function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing; kwargs...) t = get_iv(sys) @variables d(t)=0 [disturbance = true] @variables u(t)=0 [input = true] # New system input @@ -418,6 +421,6 @@ function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing) augmented_sys = extend(augmented_sys, sys) (f_oop, f_ip), dvs, p = generate_control_function(augmented_sys, all_inputs, - [d]) + [d]; kwargs...) (f_oop, f_ip), augmented_sys, dvs, p end diff --git a/src/parameters.jl b/src/parameters.jl index dfaca86d95..fb6b0b4d6e 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -15,9 +15,9 @@ function isparameter(x) isparameter(p) || (hasmetadata(p, Symbolics.VariableSource) && getmetadata(p, Symbolics.VariableSource)[1] == :parameters) - elseif istree(x) && operation(x) isa Symbolic + elseif iscall(x) && operation(x) isa Symbolic varT === PARAMETER || isparameter(operation(x)) - elseif istree(x) && operation(x) == (getindex) + elseif iscall(x) && operation(x) == (getindex) isparameter(arguments(x)[1]) elseif x isa Symbolic varT === PARAMETER @@ -54,6 +54,8 @@ tovar(s::Num) = Num(tovar(value(s))) $(SIGNATURES) Define one or more known parameters. + +See also [`@independent_variables`](@ref), [`@variables`](@ref) and [`@constants`](@ref). """ macro parameters(xs...) Symbolics._parse_vars(:parameters, diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index b9aaca3cb6..5b9c911928 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -7,7 +7,7 @@ using Symbolics: unwrap, linear_expansion, fast_substitute using SymbolicUtils using SymbolicUtils.Code using SymbolicUtils.Rewriters -using SymbolicUtils: similarterm, istree +using SymbolicUtils: maketerm, iscall using ModelingToolkit using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Differential, @@ -50,6 +50,8 @@ using SparseArrays using SimpleNonlinearSolve +using DocStringExtensions + export tearing, partial_state_selection, dae_index_lowering, check_consistency export dummy_derivative export build_torn_function, build_observed_function, ODAEProblem diff --git a/src/structural_transformation/bipartite_tearing/modia_tearing.jl b/src/structural_transformation/bipartite_tearing/modia_tearing.jl index 97d3b4588e..cef2f5f6d7 100644 --- a/src/structural_transformation/bipartite_tearing/modia_tearing.jl +++ b/src/structural_transformation/bipartite_tearing/modia_tearing.jl @@ -79,37 +79,46 @@ function tear_graph_modia(structure::SystemStructure, isder::F = nothing, @unpack graph, solvable_graph = structure var_eq_matching = maximal_matching(graph, eqfilter, varfilter, U) - var_eq_matching = complete(var_eq_matching, - max(length(var_eq_matching), - maximum(x -> x isa Int ? x : 0, var_eq_matching, init = 0))) + matching_len = max(length(var_eq_matching), + maximum(x -> x isa Int ? x : 0, var_eq_matching, init = 0)) + var_eq_matching = complete(var_eq_matching, matching_len) full_var_eq_matching = copy(var_eq_matching) var_sccs = find_var_sccs(graph, var_eq_matching) - vargraph = DiCMOBiGraph{true}(graph) + vargraph = DiCMOBiGraph{true}(graph, 0, Matching(matching_len)) ict = IncrementalCycleTracker(vargraph; dir = :in) ieqs = Int[] filtered_vars = BitSet() + free_eqs = free_equations(graph, var_sccs, var_eq_matching, varfilter) + is_overdetemined = !isempty(free_eqs) for vars in var_sccs for var in vars if varfilter(var) push!(filtered_vars, var) if var_eq_matching[var] !== unassigned - push!(ieqs, var_eq_matching[var]) + ieq = var_eq_matching[var] + push!(ieqs, ieq) end end var_eq_matching[var] = unassigned end tear_graph_block_modia!(var_eq_matching, ict, solvable_graph, ieqs, - filtered_vars, - isder) - - # clear cache - vargraph.ne = 0 - for var in vars - vargraph.matching[var] = unassigned + filtered_vars, isder) + # If the systems is overdetemined, we cannot assume the free equations + # will not form algebraic loops with equations in the sccs. + if !is_overdetemined + vargraph.ne = 0 + for var in vars + vargraph.matching[var] = unassigned + end end empty!(ieqs) empty!(filtered_vars) end + if is_overdetemined + free_vars = findall(x -> !(x isa Int), var_eq_matching) + tear_graph_block_modia!(var_eq_matching, ict, solvable_graph, free_eqs, + BitSet(free_vars), isder) + end return var_eq_matching, full_var_eq_matching, var_sccs end diff --git a/src/structural_transformation/pantelides.jl b/src/structural_transformation/pantelides.jl index 2d80e28f40..7cbd934ff8 100644 --- a/src/structural_transformation/pantelides.jl +++ b/src/structural_transformation/pantelides.jl @@ -123,7 +123,8 @@ end Perform Pantelides algorithm. """ -function pantelides!(state::TransformationState; finalize = true, maxiters = 8000) +function pantelides!( + state::TransformationState; finalize = true, maxiters = 8000, kwargs...) @unpack graph, solvable_graph, var_to_diff, eq_to_diff = state.structure neqs = nsrcs(graph) nvars = nv(var_to_diff) @@ -181,7 +182,7 @@ function pantelides!(state::TransformationState; finalize = true, maxiters = 800 ecolor[eq] || continue # introduce a new equation neqs += 1 - eq_derivative!(state, eq) + eq_derivative!(state, eq; kwargs...) end for var in eachindex(vcolor) diff --git a/src/structural_transformation/partial_state_selection.jl b/src/structural_transformation/partial_state_selection.jl index f47b6a973e..53dfc669e0 100644 --- a/src/structural_transformation/partial_state_selection.jl +++ b/src/structural_transformation/partial_state_selection.jl @@ -173,7 +173,7 @@ function dummy_derivative_graph!(state::TransformationState, jac = nothing; state_priority = nothing, log = Val(false), kwargs...) state.structure.solvable_graph === nothing && find_solvables!(state; kwargs...) complete!(state.structure) - var_eq_matching = complete(pantelides!(state)) + var_eq_matching = complete(pantelides!(state; kwargs...)) dummy_derivative_graph!(state.structure, var_eq_matching, jac, state_priority, log) end diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 0c510eef75..6e1bc5d148 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -56,7 +56,7 @@ function eq_derivative_graph!(s::SystemStructure, eq::Int) return eq_diff end -function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int) +function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int; kwargs...) s = ts.structure eq_diff = eq_derivative_graph!(s, ieq) @@ -75,7 +75,8 @@ function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int) add_edge!(s.graph, eq_diff, s.var_to_diff[var]) end s.solvable_graph === nothing || - find_eq_solvables!(ts, eq_diff; may_be_zero = true, allow_symbolic = false) + find_eq_solvables!( + ts, eq_diff; may_be_zero = true, allow_symbolic = false, kwargs...) return eq_diff end @@ -85,6 +86,14 @@ function tearing_sub(expr, dict, s) s ? simplify(expr) : expr end +""" +$(TYPEDSIGNATURES) + +Like `equations(sys)`, but includes substitutions done by the tearing process. +These equations matches generated numerical code. + +See also [`equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). +""" function full_equations(sys::AbstractSystem; simplify = false) empty_substitutions(sys) && return equations(sys) substitutions = get_substitutions(sys) @@ -92,7 +101,7 @@ function full_equations(sys::AbstractSystem; simplify = false) @unpack subs = substitutions solved = Dict(eq.lhs => eq.rhs for eq in subs) neweqs = map(equations(sys)) do eq - if istree(eq.lhs) && operation(eq.lhs) isa Union{Shift, Differential} + if iscall(eq.lhs) && operation(eq.lhs) isa Union{Shift, Differential} return tearing_sub(eq.lhs, solved, simplify) ~ tearing_sub(eq.rhs, solved, simplify) else @@ -217,10 +226,18 @@ function check_diff_graph(var_to_diff, fullvars) end =# -function tearing_reassemble(state::TearingState, var_eq_matching; - simplify = false, mm = nothing) +function tearing_reassemble(state::TearingState, var_eq_matching, + full_var_eq_matching = nothing; simplify = false, mm = nothing) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure + extra_vars = Int[] + if full_var_eq_matching !== nothing + for v in 𝑑vertices(state.structure.graph) + eq = full_var_eq_matching[v] + eq isa Int && continue + push!(extra_vars, v) + end + end neweqs = collect(equations(state)) # Terminology and Definition: @@ -532,6 +549,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching; eq_to_diff = new_eq_to_diff diff_to_var = invview(var_to_diff) + old_fullvars = fullvars @set! state.structure.graph = complete(graph) @set! state.structure.var_to_diff = var_to_diff @set! state.structure.eq_to_diff = eq_to_diff @@ -542,21 +560,54 @@ function tearing_reassemble(state::TearingState, var_eq_matching; end sys = state.sys - @set! sys.eqs = neweqs - @set! sys.unknowns = Any[v - for (i, v) in enumerate(fullvars) - if diff_to_var[i] === nothing && ispresent(i)] - @set! sys.substitutions = Substitutions(subeqs, deps) obs_sub = dummy_sub - for eq in equations(sys) + for eq in neweqs isdiffeq(eq) || continue obs_sub[eq.lhs] = eq.rhs end # TODO: compute the dependency correctly so that we don't have to do this obs = [fast_substitute(observed(sys), obs_sub); subeqs] + + # HACK: Substitute non-scalarized symbolic arrays of observed variables + # E.g. if `p[1] ~ (...)` and `p[2] ~ (...)` then substitute `p => [p[1], p[2]]` in all equations + # ideally, we want to support equations such as `p ~ [p[1], p[2]]` which will then be handled + # by the topological sorting and dependency identification pieces + obs_arr_subs = Dict() + + for eq in obs + lhs = eq.lhs + iscall(lhs) || continue + operation(lhs) === getindex || continue + Symbolics.shape(lhs) !== Symbolics.Unknown() || continue + arg1 = arguments(lhs)[1] + haskey(obs_arr_subs, arg1) && continue + obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] + end + for i in eachindex(neweqs) + neweqs[i] = fast_substitute(neweqs[i], obs_arr_subs; operator = Symbolics.Operator) + end + for i in eachindex(obs) + obs[i] = fast_substitute(obs[i], obs_arr_subs; operator = Symbolics.Operator) + end + for i in eachindex(subeqs) + subeqs[i] = fast_substitute(subeqs[i], obs_arr_subs; operator = Symbolics.Operator) + end + + @set! sys.eqs = neweqs @set! sys.observed = obs + unknowns = Any[v + for (i, v) in enumerate(fullvars) + if diff_to_var[i] === nothing && ispresent(i)] + if !isempty(extra_vars) + for v in extra_vars + push!(unknowns, old_fullvars[v]) + end + end + @set! sys.unknowns = unknowns + @set! sys.substitutions = Substitutions(subeqs, deps) + # Only makes sense for time-dependent # TODO: generalize to SDE if sys isa ODESystem @@ -570,19 +621,7 @@ end function tearing(state::TearingState; kwargs...) state.structure.solvable_graph === nothing && find_solvables!(state; kwargs...) complete!(state.structure) - @unpack graph = state.structure - algvars = BitSet(findall(v -> isalgvar(state.structure, v), 1:ndsts(graph))) - aeqs = algeqs(state.structure) - var_eq_matching′, = tear_graph_modia(state.structure; - varfilter = var -> var in algvars, - eqfilter = eq -> eq in aeqs) - var_eq_matching = Matching{Union{Unassigned, SelectedState}}(var_eq_matching′) - for var in 1:ndsts(graph) - if isdiffvar(state.structure, var) - var_eq_matching[var] = SelectedState() - end - end - var_eq_matching + tearing_with_dummy_derivatives(state.structure, ()) end """ @@ -594,8 +633,9 @@ instead, which calls this function internally. """ function tearing(sys::AbstractSystem, state = TearingState(sys); mm = nothing, simplify = false, kwargs...) - var_eq_matching = tearing(state) - invalidate_cache!(tearing_reassemble(state, var_eq_matching; mm, simplify)) + var_eq_matching, full_var_eq_matching = tearing(state) + invalidate_cache!(tearing_reassemble( + state, var_eq_matching, full_var_eq_matching; mm, simplify)) end """ diff --git a/src/structural_transformation/tearing.jl b/src/structural_transformation/tearing.jl index aa62a449dd..d37eedc853 100644 --- a/src/structural_transformation/tearing.jl +++ b/src/structural_transformation/tearing.jl @@ -68,3 +68,16 @@ function algebraic_variables_scc(state::TearingState) return var_eq_matching, var_sccs end + +function free_equations(graph, vars_scc, var_eq_matching, varfilter::F) where {F} + ne = nsrcs(graph) + seen_eqs = falses(ne) + for vars in vars_scc, var in vars + varfilter(var) || continue + ieq = var_eq_matching[var] + if ieq isa Int + seen_eqs[ieq] = true + end + end + findall(!, seen_eqs) +end diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 3ae8fb224f..7f5039f872 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -181,7 +181,9 @@ end function find_eq_solvables!(state::TearingState, ieq, to_rm = Int[], coeffs = nothing; may_be_zero = false, - allow_symbolic = false, allow_parameter = true, kwargs...) + allow_symbolic = false, allow_parameter = true, + conservative = false, + kwargs...) fullvars = state.fullvars @unpack graph, solvable_graph = state.structure eq = equations(state)[ieq] @@ -220,6 +222,7 @@ function find_eq_solvables!(state::TearingState, ieq, to_rm = Int[], coeffs = no coeffs === nothing || push!(coeffs, convert(Int, a)) else all_int_vars = false + conservative && continue end if a != 0 add_edge!(solvable_graph, ieq, j) @@ -445,7 +448,7 @@ function simplify_shifts(var) t2 = op2.t return simplify_shifts(ModelingToolkit.Shift(t1 === nothing ? t2 : t1, s1 + s2)(vv2)) else - return similarterm(var, operation(var), simplify_shifts.(arguments(var)), - Symbolics.symtype(var); metadata = unwrap(var).metadata) + return maketerm(typeof(var), operation(var), simplify_shifts.(arguments(var)), + Symbolics.symtype(var), unwrap(var).metadata) end end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c808aa4057..dcff371ecf 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -153,15 +153,16 @@ generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys), Generate a function to evaluate `exprs`. `exprs` is a symbolic expression or array of symbolic expression involving symbolic variables in `sys`. The symbolic variables -may be subsetted using `dvs` and `ps`. All `kwargs` except `postprocess_fbody` and `states` -are passed to the internal [`build_function`](@ref) call. The returned function can be called -as `f(u, p, t)` or `f(du, u, p, t)` for time-dependent systems and `f(u, p)` or `f(du, u, p)` -for time-independent systems. If `split=true` (the default) was passed to [`complete`](@ref), +may be subsetted using `dvs` and `ps`. All `kwargs` are passed to the internal +[`build_function`](@ref) call. The returned function can be called as `f(u, p, t)` or +`f(du, u, p, t)` for time-dependent systems and `f(u, p)` or `f(du, u, p)` for +time-independent systems. If `split=true` (the default) was passed to [`complete`](@ref), [`structural_simplify`](@ref) or [`@mtkbuild`](@ref), `p` is expected to be an `MTKParameters` object. """ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys), - ps = parameters(sys); wrap_code = nothing, kwargs...) + ps = parameters(sys); wrap_code = nothing, postprocess_fbody = nothing, states = nothing, + expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, kwargs...) if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system.") end @@ -170,37 +171,63 @@ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys if wrap_code === nothing wrap_code = isscalar ? identity : (identity, identity) end - pre, sol_states = get_substitutions_and_solved_unknowns(sys) - - if is_time_dependent(sys) - return build_function(exprs, + pre, sol_states = get_substitutions_and_solved_unknowns(sys, isscalar ? [exprs] : exprs) + if postprocess_fbody === nothing + postprocess_fbody = pre + end + if states === nothing + states = sol_states + end + fnexpr = if is_time_dependent(sys) + build_function(exprs, dvs, p..., get_iv(sys); kwargs..., - postprocess_fbody = pre, - states = sol_states, + postprocess_fbody, + states, wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs) + wrap_array_vars(sys, exprs; dvs), + expression = Val{true} ) else - return build_function(exprs, + build_function(exprs, dvs, p...; kwargs..., - postprocess_fbody = pre, - states = sol_states, + postprocess_fbody, + states, wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs) + wrap_array_vars(sys, exprs; dvs), + expression = Val{true} ) end + if expression == Val{true} + return fnexpr + end + if fnexpr isa Tuple + return eval_or_rgf.(fnexpr; eval_expression, eval_module) + else + return eval_or_rgf(fnexpr; eval_expression, eval_module) + end +end + +function wrap_assignments(isscalar, assignments; let_block = false) + function wrapper(expr) + Func(expr.args, [], Let(assignments, expr.body, let_block)) + end + if isscalar + wrapper + else + wrapper, wrapper + end end function wrap_array_vars(sys::AbstractSystem, exprs; dvs = unknowns(sys)) isscalar = !(exprs isa AbstractArray) array_vars = Dict{Any, AbstractArray{Int}}() for (j, x) in enumerate(dvs) - if istree(x) && operation(x) == getindex + if iscall(x) && operation(x) == getindex arg = arguments(x)[1] inds = get!(() -> Int[], array_vars, arg) push!(inds, j) @@ -328,6 +355,13 @@ function independent_variables(sys::AbstractMultivariateSystem) return getfield(sys, :ivs) end +""" +$(TYPEDSIGNATURES) + +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 isdefined(sys, :iv) @@ -347,7 +381,7 @@ function SymbolicIndexingInterface.is_variable(sys::AbstractSystem, sym) end if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing return is_variable(ic, sym) || - istree(sym) && operation(sym) === getindex && + iscall(sym) && operation(sym) === getindex && is_variable(ic, first(arguments(sym))) end return any(isequal(sym), variable_symbols(sys)) || @@ -360,8 +394,9 @@ function SymbolicIndexingInterface.is_variable(sys::AbstractSystem, sym::Symbol) return is_variable(ic, sym) end return any(isequal(sym), getname.(variable_symbols(sys))) || - count('₊', string(sym)) == 1 && - count(isequal(sym), Symbol.(nameof(sys), :₊, getname.(variable_symbols(sys)))) == + count(NAMESPACE_SEPARATOR, string(sym)) == 1 && + count(isequal(sym), + Symbol.(nameof(sys), NAMESPACE_SEPARATOR_SYMBOL, getname.(variable_symbols(sys)))) == 1 end @@ -373,7 +408,7 @@ function SymbolicIndexingInterface.variable_index(sys::AbstractSystem, sym) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing return if (idx = variable_index(ic, sym)) !== nothing idx - elseif istree(sym) && operation(sym) === getindex && + elseif iscall(sym) && operation(sym) === getindex && (idx = variable_index(ic, first(arguments(sym)))) !== nothing idx[arguments(sym)[(begin + 1):end]...] else @@ -394,9 +429,10 @@ function SymbolicIndexingInterface.variable_index(sys::AbstractSystem, sym::Symb idx = findfirst(isequal(sym), getname.(variable_symbols(sys))) if idx !== nothing return idx - elseif count('₊', string(sym)) == 1 + elseif count(NAMESPACE_SEPARATOR, string(sym)) == 1 return findfirst(isequal(sym), - Symbol.(nameof(sys), :₊, getname.(variable_symbols(sys)))) + Symbol.( + nameof(sys), NAMESPACE_SEPARATOR_SYMBOL, getname.(variable_symbols(sys)))) end return nothing end @@ -411,7 +447,7 @@ function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym) sym = unwrap(sym) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing return is_parameter(ic, sym) || - istree(sym) && operation(sym) === getindex && + iscall(sym) && operation(sym) === getindex && is_parameter(ic, first(arguments(sym))) end if unwrap(sym) isa Int @@ -426,9 +462,10 @@ function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::Symbol return is_parameter(ic, sym) end return any(isequal(sym), getname.(parameter_symbols(sys))) || - count('₊', string(sym)) == 1 && + count(NAMESPACE_SEPARATOR, string(sym)) == 1 && count(isequal(sym), - Symbol.(nameof(sys), :₊, getname.(parameter_symbols(sys)))) == 1 + Symbol.(nameof(sys), NAMESPACE_SEPARATOR_SYMBOL, getname.(parameter_symbols(sys)))) == + 1 end function SymbolicIndexingInterface.parameter_index(sys::AbstractSystem, sym) @@ -436,7 +473,7 @@ function SymbolicIndexingInterface.parameter_index(sys::AbstractSystem, sym) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing return if (idx = parameter_index(ic, sym)) !== nothing idx - elseif istree(sym) && operation(sym) === getindex && + elseif iscall(sym) && operation(sym) === getindex && (idx = parameter_index(ic, first(arguments(sym)))) !== nothing ParameterIndex(idx.portion, (idx.idx..., arguments(sym)[(begin + 1):end]...)) else @@ -461,9 +498,10 @@ function SymbolicIndexingInterface.parameter_index(sys::AbstractSystem, sym::Sym idx = findfirst(isequal(sym), getname.(parameter_symbols(sys))) if idx !== nothing return idx - elseif count('₊', string(sym)) == 1 + elseif count(NAMESPACE_SEPARATOR, string(sym)) == 1 return findfirst(isequal(sym), - Symbol.(nameof(sys), :₊, getname.(parameter_symbols(sys)))) + Symbol.( + nameof(sys), NAMESPACE_SEPARATOR_SYMBOL, getname.(parameter_symbols(sys)))) end return nothing end @@ -489,6 +527,46 @@ function SymbolicIndexingInterface.is_observed(sys::AbstractSystem, sym) !is_independent_variable(sys, sym) && symbolic_type(sym) != NotSymbolic() end +function SymbolicIndexingInterface.observed( + sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__) + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + if sym isa Symbol + _sym = get(ic.symbol_to_variable, sym, nothing) + if _sym === nothing + throw(ArgumentError("Symbol $sym does not exist in the system")) + end + sym = _sym + elseif sym isa AbstractArray && symbolic_type(sym) isa NotSymbolic && + any(x -> x isa Symbol, sym) + sym = map(sym) do s + if s isa Symbol + _s = get(ic.symbol_to_variable, s, nothing) + if _s === nothing + throw(ArgumentError("Symbol $s does not exist in the system")) + end + return _s + end + return unwrap(s) + end + end + end + _fn = build_explicit_observed_function(sys, sym; eval_expression, eval_module) + + if is_time_dependent(sys) + return let _fn = _fn + fn1(u, p, t) = _fn(u, p, t) + fn1(u, p::MTKParameters, t) = _fn(u, p..., t) + fn1 + end + else + return let _fn = _fn + fn2(u, p) = _fn(u, p) + fn2(u, p::MTKParameters) = _fn(u, p...) + fn2 + end + end +end + function SymbolicIndexingInterface.default_values(sys::AbstractSystem) return merge( Dict(eq.lhs => eq.rhs for eq in observed(sys)), @@ -578,11 +656,29 @@ for prop in [:eqs :split_idxs :parent :index_cache] - fname1 = Symbol(:get_, prop) - fname2 = Symbol(:has_, prop) + fname_get = Symbol(:get_, prop) + fname_has = Symbol(:has_, prop) @eval begin - $fname1(sys::AbstractSystem) = getfield(sys, $(QuoteNode(prop))) - $fname2(sys::AbstractSystem) = isdefined(sys, $(QuoteNode(prop))) + """ + $(TYPEDSIGNATURES) + + Get the internal field `$($(QuoteNode(prop)))` of a system `sys`. + It only includes `$($(QuoteNode(prop)))` local to `sys`; not those of its subsystems, + like `unknowns(sys)`, `parameters(sys)` and `equations(sys)` does. + This is equivalent to, but preferred over `sys.$($(QuoteNode(prop)))`. + + See also [`has_$($(QuoteNode(prop)))`](@ref). + """ + $fname_get(sys::AbstractSystem) = getfield(sys, $(QuoteNode(prop))) + + """ + $(TYPEDSIGNATURES) + + Returns whether the system `sys` has the internal field `$($(QuoteNode(prop)))`. + + See also [`get_$($(QuoteNode(prop)))`](@ref). + """ + $fname_has(sys::AbstractSystem) = isdefined(sys, $(QuoteNode(prop))) end end @@ -732,28 +828,43 @@ function _apply_to_variables(f::F, ex) where {F} if isvariable(ex) return f(ex) end - istree(ex) || return ex - similarterm(ex, _apply_to_variables(f, operation(ex)), + iscall(ex) || return ex + maketerm(typeof(ex), _apply_to_variables(f, operation(ex)), map(Base.Fix1(_apply_to_variables, f), arguments(ex)), - metadata = metadata(ex)) + symtype(ex), metadata(ex)) end abstract type SymScope end struct LocalScope <: SymScope end -function LocalScope(sym::Union{Num, Symbolic}) +function LocalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym - setmetadata(sym, SymScope, LocalScope()) + if iscall(sym) && operation(sym) === getindex + args = arguments(sym) + a1 = setmetadata(args[1], SymScope, LocalScope()) + maketerm(typeof(sym), operation(sym), [a1, args[2:end]...], + symtype(sym), metadata(sym)) + else + setmetadata(sym, SymScope, LocalScope()) + end end end struct ParentScope <: SymScope parent::SymScope end -function ParentScope(sym::Union{Num, Symbolic}) +function ParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym - setmetadata(sym, SymScope, - ParentScope(getmetadata(value(sym), SymScope, LocalScope()))) + if iscall(sym) && operation(sym) === getindex + args = arguments(sym) + a1 = setmetadata(args[1], SymScope, + ParentScope(getmetadata(value(args[1]), SymScope, LocalScope()))) + maketerm(typeof(sym), operation(sym), [a1, args[2:end]...], + symtype(sym), metadata(sym)) + else + setmetadata(sym, SymScope, + ParentScope(getmetadata(value(sym), SymScope, LocalScope()))) + end end end @@ -761,18 +872,33 @@ struct DelayParentScope <: SymScope parent::SymScope N::Int end -function DelayParentScope(sym::Union{Num, Symbolic}, N) +function DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}, N) apply_to_variables(sym) do sym - setmetadata(sym, SymScope, - DelayParentScope(getmetadata(value(sym), SymScope, LocalScope()), N)) + if iscall(sym) && operation(sym) == getindex + args = arguments(sym) + a1 = setmetadata(args[1], SymScope, + DelayParentScope(getmetadata(value(args[1]), SymScope, LocalScope()), N)) + maketerm(typeof(sym), operation(sym), [a1, args[2:end]...], + symtype(sym), metadata(sym)) + else + setmetadata(sym, SymScope, + DelayParentScope(getmetadata(value(sym), SymScope, LocalScope()), N)) + end end end -DelayParentScope(sym::Union{Num, Symbolic}) = DelayParentScope(sym, 1) +DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) = DelayParentScope(sym, 1) struct GlobalScope <: SymScope end -function GlobalScope(sym::Union{Num, Symbolic}) +function GlobalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym - setmetadata(sym, SymScope, GlobalScope()) + if iscall(sym) && operation(sym) == getindex + args = arguments(sym) + a1 = setmetadata(args[1], SymScope, GlobalScope()) + maketerm(typeof(sym), operation(sym), [a1, args[2:end]...], + symtype(sym), metadata(sym)) + else + setmetadata(sym, SymScope, GlobalScope()) + end end end @@ -784,9 +910,16 @@ function renamespace(sys, x) x = unwrap(x) if x isa Symbolic T = typeof(x) - if istree(x) && operation(x) isa Operator - return similarterm(x, operation(x), - Any[renamespace(sys, only(arguments(x)))])::T + if iscall(x) && operation(x) isa Operator + return maketerm(typeof(x), operation(x), + Any[renamespace(sys, only(arguments(x)))], + symtype(x), metadata(x))::T + end + if iscall(x) && operation(x) === getindex + args = arguments(x) + return maketerm( + typeof(x), operation(x), vcat(renamespace(sys, args[1]), args[2:end]), + symtype(x), metadata(x))::T end let scope = getmetadata(x, SymScope, LocalScope()) if scope isa LocalScope @@ -809,7 +942,7 @@ function renamespace(sys, x) elseif x isa AbstractSystem rename(x, renamespace(sys, nameof(x))) else - Symbol(getname(sys), :₊, x) + Symbol(getname(sys), NAMESPACE_SEPARATOR_SYMBOL, x) end end @@ -823,12 +956,29 @@ function namespace_defaults(sys) for (k, v) in pairs(defs)) end +function namespace_guesses(sys) + guess = guesses(sys) + Dict(unknowns(sys, k) => namespace_expr(v, sys) for (k, v) in guess) +end + +function namespace_parameter_dependencies(sys) + pdeps = parameter_dependencies(sys) + Dict(parameters(sys, k) => namespace_expr(v, sys) for (k, v) in pdeps) +end + function namespace_equations(sys::AbstractSystem, ivs = independent_variables(sys)) eqs = equations(sys) isempty(eqs) && return Equation[] map(eq -> namespace_equation(eq, sys; ivs), eqs) end +function namespace_initialization_equations( + sys::AbstractSystem, ivs = independent_variables(sys)) + eqs = initialization_equations(sys) + isempty(eqs) && return Equation[] + map(eq -> namespace_equation(eq, sys; ivs), eqs) +end + function namespace_equation(eq::Equation, sys, n = nameof(sys); @@ -844,11 +994,12 @@ function namespace_assignment(eq::Assignment, sys) Assignment(_lhs, _rhs) end -function namespace_expr(O, sys, n = nameof(sys); ivs = independent_variables(sys)) +function namespace_expr( + O, sys, n = nameof(sys); ivs = independent_variables(sys)) O = unwrap(O) if any(isequal(O), ivs) return O - elseif istree(O) + elseif iscall(O) T = typeof(O) renamed = let sys = sys, n = n, T = T map(a -> namespace_expr(a, sys, n; ivs)::Any, arguments(O)) @@ -857,13 +1008,14 @@ function namespace_expr(O, sys, n = nameof(sys); ivs = independent_variables(sys # Use renamespace so the scope is correct, and make sure to use the # metadata from the rescoped variable rescoped = renamespace(n, O) - similarterm(O, operation(rescoped), renamed, - metadata = metadata(rescoped)) + maketerm(typeof(rescoped), operation(rescoped), renamed, + symtype(rescoped), + metadata(rescoped)) elseif Symbolics.isarraysymbolic(O) # promote_symtype doesn't work for array symbolics - similarterm(O, operation(O), renamed, symtype(O), metadata = metadata(O)) + maketerm(typeof(O), operation(O), renamed, symtype(O), metadata(O)) else - similarterm(O, operation(O), renamed, metadata = metadata(O)) + maketerm(typeof(O), operation(O), renamed, symtype(O), metadata(O)) end elseif isvariable(O) renamespace(n, O) @@ -876,6 +1028,14 @@ function namespace_expr(O, sys, n = nameof(sys); ivs = independent_variables(sys end end _nonum(@nospecialize x) = x isa Num ? x.val : x + +""" +$(TYPEDSIGNATURES) + +Get the unknown variables of the system `sys` and its subsystems. + +See also [`ModelingToolkit.get_unknowns`](@ref). +""" function unknowns(sys::AbstractSystem) sts = get_unknowns(sys) systems = get_systems(sys) @@ -896,6 +1056,13 @@ function unknowns(sys::AbstractSystem) unique(nonunique_unknowns) end +""" +$(TYPEDSIGNATURES) + +Get the parameters of the system `sys` and its subsystems. + +See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). +""" function parameters(sys::AbstractSystem) ps = get_ps(sys) if ps == SciMLBase.NullParameters() @@ -908,7 +1075,7 @@ function parameters(sys::AbstractSystem) result = unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) if has_parameter_dependencies(sys) && - (pdeps = get_parameter_dependencies(sys)) !== nothing + (pdeps = parameter_dependencies(sys)) !== nothing filter(result) do sym !haskey(pdeps, sym) end @@ -919,19 +1086,52 @@ end function dependent_parameters(sys::AbstractSystem) if has_parameter_dependencies(sys) && - (pdeps = get_parameter_dependencies(sys)) !== nothing - collect(keys(pdeps)) + !isempty(parameter_dependencies(sys)) + collect(keys(parameter_dependencies(sys))) else [] end end +""" +$(TYPEDSIGNATURES) +Get the parameter dependencies of the system `sys` and its subsystems. + +See also [`defaults`](@ref) and [`ModelingToolkit.get_parameter_dependencies`](@ref). +""" +function parameter_dependencies(sys::AbstractSystem) + pdeps = get_parameter_dependencies(sys) + if isnothing(pdeps) + pdeps = Dict() + end + systems = get_systems(sys) + isempty(systems) && return pdeps + for subsys in systems + pdeps = merge(pdeps, namespace_parameter_dependencies(subsys)) + end + # @info pdeps + return pdeps +end + function full_parameters(sys::AbstractSystem) vcat(parameters(sys), dependent_parameters(sys)) end +""" +$(TYPEDSIGNATURES) + +Get the guesses for variables in the initialization system of the system `sys` and its subsystems. + +See also [`initialization_equations`](@ref) and [`ModelingToolkit.get_guesses`](@ref). +""" function guesses(sys::AbstractSystem) - get_guesses(sys) + guess = get_guesses(sys) + systems = get_systems(sys) + isempty(systems) && return guess + for subsys in systems + guess = merge(guess, namespace_guesses(subsys)) + end + return guess end # required in `src/connectors.jl:437` @@ -954,6 +1154,15 @@ end Base.@deprecate default_u0(x) defaults(x) false Base.@deprecate default_p(x) defaults(x) false + +""" +$(TYPEDSIGNATURES) + +Get the default values of the system sys and its subsystems. +If they are not explicitly provided, variables and parameters are initialized to these values. + +See also [`initialization_equations`](@ref), [`parameter_dependencies`](@ref) and [`ModelingToolkit.get_defaults`](@ref). +""" function defaults(sys::AbstractSystem) systems = get_systems(sys) defs = get_defaults(sys) @@ -966,7 +1175,15 @@ function defaults(sys::AbstractSystem) isempty(systems) ? defs : mapfoldr(namespace_defaults, merge, systems; init = defs) end +function defaults_and_guesses(sys::AbstractSystem) + merge(guesses(sys), defaults(sys)) +end + unknowns(sys::Union{AbstractSystem, Nothing}, v) = renamespace(sys, v) +for vType in [Symbolics.Arr, Symbolics.Symbolic{<:AbstractArray}] + @eval unknowns(sys::AbstractSystem, v::$vType) = renamespace(sys, v) + @eval parameters(sys::AbstractSystem, v::$vType) = toparam(unknowns(sys, v)) +end parameters(sys::Union{AbstractSystem, Nothing}, v) = toparam(unknowns(sys, v)) for f in [:unknowns, :parameters] @eval function $f(sys::AbstractSystem, vs::AbstractArray) @@ -976,6 +1193,15 @@ end flatten(sys::AbstractSystem, args...) = sys +""" +$(TYPEDSIGNATURES) + +Get the flattened equations of the system `sys` and its subsystems. +It may include some abbreviations and aliases of observables. +It is often the most useful way to inspect the equations of a system. + +See also [`full_equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). +""" function equations(sys::AbstractSystem) eqs = get_eqs(sys) systems = get_systems(sys) @@ -990,6 +1216,27 @@ function equations(sys::AbstractSystem) end end +""" +$(TYPEDSIGNATURES) + +Get the initialization equations of the system `sys` and its subsystems. + +See also [`guesses`](@ref), [`defaults`](@ref), [`parameter_dependencies`](@ref) and [`ModelingToolkit.get_initialization_eqs`](@ref). +""" +function initialization_equations(sys::AbstractSystem) + eqs = get_initialization_eqs(sys) + systems = get_systems(sys) + if isempty(systems) + return eqs + else + eqs = Equation[eqs; + reduce(vcat, + namespace_initialization_equations.(get_systems(sys)); + init = Equation[])] + return eqs + end +end + function preface(sys::AbstractSystem) has_preface(sys) || return nothing pre = get_preface(sys) @@ -1027,7 +1274,7 @@ function time_varying_as_func(x, sys::AbstractTimeDependentSystem) # than pass in a value in place of x(t). # # This is done by just making `x` the argument of the function. - if istree(x) && + if iscall(x) && issym(operation(x)) && !(length(arguments(x)) == 1 && isequal(arguments(x)[1], get_iv(sys))) return operation(x) @@ -1051,11 +1298,55 @@ end ### ### System utils ### +struct ObservedFunctionCache{S} + sys::S + dict::Dict{Any, Any} + steady_state::Bool + eval_expression::Bool + eval_module::Module +end + +function ObservedFunctionCache( + sys; steady_state = false, eval_expression = false, eval_module = @__MODULE__) + return ObservedFunctionCache(sys, Dict(), steady_state, eval_expression, eval_module) +end + +# This is hit because ensemble problems do a deepcopy +function Base.deepcopy_internal(ofc::ObservedFunctionCache, stackdict::IdDict) + sys = deepcopy(ofc.sys) + dict = deepcopy(ofc.dict) + steady_state = ofc.steady_state + eval_expression = ofc.eval_expression + eval_module = ofc.eval_module + newofc = ObservedFunctionCache(sys, dict, steady_state, eval_expression, eval_module) + stackdict[ofc] = newofc + return newofc +end + +function (ofc::ObservedFunctionCache)(obsvar, args...) + obs = get!(ofc.dict, value(obsvar)) do + SymbolicIndexingInterface.observed( + ofc.sys, obsvar; eval_expression = ofc.eval_expression, + eval_module = ofc.eval_module) + end + if ofc.steady_state + obs = let fn = obs + fn1(u, p, t = Inf) = fn(u, p, t) + fn1 + end + end + if args === () + return obs + else + return obs(args...) + end +end + function push_vars!(stmt, name, typ, vars) isempty(vars) && return vars_expr = Expr(:macrocall, typ, nothing) for s in vars - if istree(s) + if iscall(s) f = nameof(operation(s)) args = arguments(s) ex = :($f($(args...))) @@ -1072,7 +1363,7 @@ function round_trip_expr(t, var2name) name = get(var2name, t, nothing) name !== nothing && return name issym(t) && return nameof(t) - istree(t) || return t + iscall(t) || return t f = round_trip_expr(operation(t), var2name) args = map(Base.Fix2(round_trip_expr, var2name), arguments(t)) return :($f($(args...))) @@ -1083,7 +1374,7 @@ function round_trip_eq(eq::Equation, var2name) syss = get_systems(eq.rhs) call = Expr(:call, connect) for sys in syss - strs = split(string(nameof(sys)), "₊") + strs = split(string(nameof(sys)), NAMESPACE_SEPARATOR) s = Symbol(strs[1]) for st in strs[2:end] s = Expr(:., s, Meta.quot(Symbol(st))) @@ -1495,8 +1786,7 @@ function default_to_parentscope(v) uv isa Symbolic || return v apply_to_variables(v) do sym if !hasmetadata(uv, SymScope) - setmetadata(sym, SymScope, - ParentScope(getmetadata(value(sym), SymScope, LocalScope()))) + ParentScope(sym) else sym end @@ -1555,7 +1845,7 @@ function component_post_processing(expr, isconnector) args = sig.args[2:end] quote - function $fname($(args...)) + $Base.@__doc__ function $fname($(args...)) # we need to create a closure to escape explicit return in `body`. res = (() -> $body)() if $isdefined(res, :gui_metadata) && $getfield(res, :gui_metadata) === nothing @@ -1655,7 +1945,7 @@ function io_preprocessing(sys::AbstractSystem, inputs, end """ - lin_fun, simplified_sys = linearization_function(sys::AbstractSystem, inputs, outputs; simplify = false, initialize = true, kwargs...) + lin_fun, simplified_sys = linearization_function(sys::AbstractSystem, inputs, outputs; simplify = false, initialize = true, initialization_solver_alg = TrustRegion(), kwargs...) Return a function that linearizes the system `sys`. The function [`linearize`](@ref) provides a higher-level and easier to use interface. @@ -1680,6 +1970,7 @@ The `simplified_sys` has undergone [`structural_simplify`](@ref) and had any occ - `outputs`: A vector of variables that indicate the outputs of the linearized input-output model. - `simplify`: Apply simplification in tearing. - `initialize`: If true, a check is performed to ensure that the operating point is consistent (satisfies algebraic equations). If the op is not consistent, initialization is performed. + - `initialization_solver_alg`: A NonlinearSolve algorithm to use for solving for a feasible set of state and algebraic variables that satisfies the specified operating point. - `kwargs`: Are passed on to `find_solvables!` See also [`linearize`](@ref) which provides a higher-level interface. @@ -1690,6 +1981,9 @@ function linearization_function(sys::AbstractSystem, inputs, op = Dict(), p = DiffEqBase.NullParameters(), zero_dummy_der = false, + initialization_solver_alg = TrustRegion(), + eval_expression = false, eval_module = @__MODULE__, + warn_initialize_determined = true, kwargs...) inputs isa AbstractVector || (inputs = [inputs]) outputs isa AbstractVector || (outputs = [outputs]) @@ -1703,37 +1997,113 @@ function linearization_function(sys::AbstractSystem, inputs, op = merge(defs, op) end sys = ssys - x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op) - u0, _p, _ = get_u0_p(sys, x0, p; use_union = false, tofloat = true) - ps = parameters(sys) + u0map = Dict(k => v for (k, v) in op if is_variable(ssys, k)) + initsys = structural_simplify( + generate_initializesystem( + sys, u0map = u0map, guesses = guesses(sys), algebraic_only = true), + fully_determined = false) + + # HACK: some unknowns may not be involved in any initialization equations, and are + # thus removed from the system during `structural_simplify`. + # This causes `getu(initsys, unknowns(sys))` to fail, so we add them back as parameters + # for now. + missing_unknowns = setdiff(unknowns(sys), all_symbols(initsys)) + if !isempty(missing_unknowns) + if warn_initialize_determined + @warn "Initialization system is underdetermined. No equations for $(missing_unknowns). Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false." + end + new_parameters = [parameters(initsys); missing_unknowns] + @set! initsys.ps = new_parameters + initsys = complete(initsys) + end + + if p isa SciMLBase.NullParameters + p = Dict() + else + p = todict(p) + end + x0 = merge(defaults_and_guesses(sys), op) if has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, p, u0) + sys_ps = MTKParameters(sys, p, x0; eval_expression, eval_module) + else + sys_ps = varmap_to_vars(p, parameters(sys); defaults = x0) + end + p[get_iv(sys)] = NaN + if has_index_cache(initsys) && get_index_cache(initsys) !== nothing + oldps = MTKParameters(initsys, p, merge(guesses(sys), defaults(sys), op); + eval_expression, eval_module) + initsys_ps = parameters(initsys) + p_getter = build_explicit_observed_function( + sys, initsys_ps; eval_expression, eval_module) + + u_getter = isempty(unknowns(initsys)) ? (_...) -> nothing : + build_explicit_observed_function( + sys, unknowns(initsys); eval_expression, eval_module) + get_initprob_u_p = let p_getter, + p_setter! = setp(initsys, initsys_ps), + u_getter = u_getter + + function (u, p, t) + state = ProblemState(; u, p, t) + p_setter!(oldps, p_getter(state)) + newu = u_getter(state) + return newu, oldps + end + end else - p = _p - p, split_idxs = split_parameters_by_type(p) - if p isa Tuple - ps = Base.Fix1(getindex, ps).(split_idxs) - ps = (ps...,) #if p is Tuple, ps should be Tuple + get_initprob_u_p = let p_getter = getu(sys, parameters(initsys)), + u_getter = build_explicit_observed_function( + sys, unknowns(initsys); eval_expression, eval_module) + + function (u, p, t) + state = ProblemState(; u, p, t) + return u_getter(state), p_getter(state) + end end end + initfn = NonlinearFunction(initsys; eval_expression, eval_module) + initprobmap = build_explicit_observed_function( + initsys, unknowns(sys); eval_expression, eval_module) + ps = full_parameters(sys) + h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) lin_fun = let diff_idxs = diff_idxs, alge_idxs = alge_idxs, input_idxs = input_idxs, sts = unknowns(sys), - fun = ODEFunction{true, SciMLBase.FullSpecialize}(sys, unknowns(sys), ps; p = p), - h = build_explicit_observed_function(sys, outputs), - chunk = ForwardDiff.Chunk(input_idxs) + get_initprob_u_p = get_initprob_u_p, + fun = ODEFunction{true, SciMLBase.FullSpecialize}( + sys, unknowns(sys), ps; eval_expression, eval_module), + initfn = initfn, + initprobmap = initprobmap, + h = h, + chunk = ForwardDiff.Chunk(input_idxs), + sys_ps = sys_ps, + initialize = initialize, + initialization_solver_alg = initialization_solver_alg, + sys = sys function (u, p, t) + if !isa(p, MTKParameters) + p = todict(p) + newps = deepcopy(sys_ps) + for (k, v) in p + if is_parameter(sys, k) + setp(sys, k)(newps, v) + end + end + p = newps + end + if u !== nothing # Handle systems without unknowns length(sts) == length(u) || error("Number of unknown variables ($(length(sts))) does not match the number of input unknowns ($(length(u)))") if initialize && !isempty(alge_idxs) # This is expensive and can be omitted if the user knows that the system is already initialized residual = fun(u, p, t) if norm(residual[alge_idxs]) > √(eps(eltype(residual))) - prob = ODEProblem(fun, u, (t, t + 1), p) - integ = init(prob, OrdinaryDiffEq.Rodas5P()) - u = integ.u + initu0, initp = get_initprob_u_p(u, p, t) + initprob = NonlinearLeastSquaresProblem(initfn, initu0, initp) + nlsol = solve(initprob, initialization_solver_alg) + u = initprobmap(nlsol) end end uf = SciMLBase.UJacobianWrapper(fun, t, p) @@ -1790,6 +2160,7 @@ where `x` are differential unknown variables, `z` algebraic variables, `u` input """ function linearize_symbolic(sys::AbstractSystem, inputs, outputs; simplify = false, allow_input_derivatives = false, + eval_expression = false, eval_module = @__MODULE__, kwargs...) sys, diff_idxs, alge_idxs, input_idxs = io_preprocessing( sys, inputs, outputs; simplify, @@ -1799,10 +2170,11 @@ function linearize_symbolic(sys::AbstractSystem, inputs, ps = full_parameters(sys) p = reorder_parameters(sys, ps) - fun = generate_function(sys, sts, ps; expression = Val{false})[1] + fun_expr = generate_function(sys, sts, ps; expression = Val{true})[1] + fun = eval_or_rgf(fun_expr; eval_expression, eval_module) dx = fun(sts, p..., t) - h = build_explicit_observed_function(sys, outputs) + h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) y = h(sts, p..., t) fg_xz = Symbolics.jacobian(dx, sts) @@ -1943,11 +2315,10 @@ This example builds the following feedback interconnection and linearizes it fro ```julia using ModelingToolkit -@variables t +using ModelingToolkit: t_nounits as t, D_nounits as D function plant(; name) @variables x(t) = 1 @variables u(t)=0 y(t)=0 - D = Differential(t) eqs = [D(x) ~ -x + u y ~ x] ODESystem(eqs, t; name = name) @@ -1956,7 +2327,6 @@ end function ref_filt(; name) @variables x(t)=0 y(t)=0 @variables u(t)=0 [input = true] - D = Differential(t) eqs = [D(x) ~ -2 * x + u y ~ x] ODESystem(eqs, t, name = name) @@ -1998,8 +2368,8 @@ lsys_sym, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) """ function linearize(sys, lin_fun; t = 0.0, op = Dict(), allow_input_derivatives = false, p = DiffEqBase.NullParameters()) - x0 = merge(defaults(sys), op) - u0, p2, _ = get_u0_p(sys, x0, p; use_union = false, tofloat = true) + x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op) + u0, defs = get_u0(sys, x0, p) if has_index_cache(sys) && get_index_cache(sys) !== nothing if p isa SciMLBase.NullParameters p = op @@ -2010,9 +2380,8 @@ function linearize(sys, lin_fun; t = 0.0, op = Dict(), allow_input_derivatives = elseif p isa Vector p = merge(Dict(parameters(sys) .=> p), op) end - p2 = MTKParameters(sys, p, Dict(unknowns(sys) .=> u0)) end - linres = lin_fun(u0, p2, t) + linres = lin_fun(u0, p, t) f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u = linres nx, nu = size(f_u) @@ -2149,6 +2518,13 @@ function Base.showerror(io::IO, e::ExtraEquationsSystemException) print(io, "ExtraEquationsSystemException: ", e.msg) end +struct HybridSystemNotSupportedException <: Exception + msg::String +end +function Base.showerror(io::IO, e::HybridSystemNotSupportedException) + print(io, "HybridSystemNotSupportedException: ", e.msg) +end + function AbstractTrees.children(sys::ModelingToolkit.AbstractSystem) ModelingToolkit.get_systems(sys) end @@ -2166,13 +2542,13 @@ function check_eqs_u0(eqs, dvs, u0; check_length = true, kwargs...) if u0 !== nothing if check_length if !(length(eqs) == length(dvs) == length(u0)) - throw(ArgumentError("Equations ($(length(eqs))), unknowns ($(length(dvs))), and initial conditions ($(length(u0))) are of different lengths. To allow a different number of equations than unknowns use kwarg check_length=false.")) + throw(ArgumentError("Equations ($(length(eqs))), unknowns ($(length(dvs))), and initial conditions ($(length(u0))) are of different lengths.")) end elseif length(dvs) != length(u0) throw(ArgumentError("Unknowns ($(length(dvs))) and initial conditions ($(length(u0))) are of different lengths.")) end elseif check_length && (length(eqs) != length(dvs)) - throw(ArgumentError("Equations ($(length(eqs))) and Unknowns ($(length(dvs))) are of different lengths. To allow these to differ use kwarg check_length=false.")) + throw(ArgumentError("Equations ($(length(eqs))) and Unknowns ($(length(dvs))) are of different lengths.")) end return nothing end @@ -2200,8 +2576,10 @@ end """ $(TYPEDSIGNATURES) -extend the `basesys` with `sys`, the resulting system would inherit `sys`'s name +Extend the `basesys` with `sys`, the resulting system would inherit `sys`'s name by default. + +See also [`compose`](@ref). """ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nameof(sys), gui_metadata = get_gui_metadata(sys)) @@ -2217,23 +2595,30 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nam end end + # collect fields common to all system types eqs = union(get_eqs(basesys), get_eqs(sys)) sts = union(get_unknowns(basesys), get_unknowns(sys)) ps = union(get_ps(basesys), get_ps(sys)) + dep_ps = union_nothing(parameter_dependencies(basesys), parameter_dependencies(sys)) obs = union(get_observed(basesys), get_observed(sys)) cevs = union(get_continuous_events(basesys), get_continuous_events(sys)) devs = union(get_discrete_events(basesys), get_discrete_events(sys)) defs = merge(get_defaults(basesys), get_defaults(sys)) # prefer `sys` + meta = union_nothing(get_metadata(basesys), get_metadata(sys)) syss = union(get_systems(basesys), get_systems(sys)) + args = length(ivs) == 0 ? (eqs, sts, ps) : (eqs, ivs[1], sts, ps) + kwargs = (parameter_dependencies = dep_ps, observed = obs, continuous_events = cevs, + discrete_events = devs, defaults = defs, systems = syss, metadata = meta, + name = name, gui_metadata = gui_metadata) - if length(ivs) == 0 - T(eqs, sts, ps, observed = obs, defaults = defs, name = name, systems = syss, - continuous_events = cevs, discrete_events = devs, gui_metadata = gui_metadata) - elseif length(ivs) == 1 - T(eqs, ivs[1], sts, ps, observed = obs, defaults = defs, name = name, - systems = syss, continuous_events = cevs, discrete_events = devs, - 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 + + return T(args...; kwargs...) end function Base.:(&)(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nameof(sys)) @@ -2243,8 +2628,10 @@ end """ $(SIGNATURES) -compose multiple systems together. The resulting system would inherit the first +Compose multiple systems together. The resulting system would inherit the first system's name. + +See also [`extend`](@ref). """ function compose(sys::AbstractSystem, systems::AbstractArray; name = nameof(sys)) nsys = length(systems) @@ -2265,7 +2652,7 @@ end """ missing_variable_defaults(sys::AbstractSystem, default = 0.0; subset = unknowns(sys)) -returns a `Vector{Pair}` of variables set to `default` which are missing from `get_defaults(sys)`. The `default` argument can be a single value or vector to set the missing defaults respectively. +Returns a `Vector{Pair}` of variables set to `default` which are missing from `get_defaults(sys)`. The `default` argument can be a single value or vector to set the missing defaults respectively. """ function missing_variable_defaults( sys::AbstractSystem, default = 0.0; subset = unknowns(sys)) @@ -2293,8 +2680,10 @@ end keytype(::Type{<:Pair{T, V}}) where {T, V} = T function Symbolics.substitute(sys::AbstractSystem, rules::Union{Vector{<:Pair}, Dict}) - if has_continuous_domain(sys) && get_continuous_events(sys) !== nothing || - has_discrete_events(sys) && get_discrete_events(sys) !== nothing + if has_continuous_domain(sys) && get_continuous_events(sys) !== nothing && + !isempty(get_continuous_events(sys)) || + has_discrete_events(sys) && get_discrete_events(sys) !== nothing && + !isempty(get_discrete_events(sys)) @warn "`substitute` only supports performing substitutions in equations. This system has events, which will not be updated." end if keytype(eltype(rules)) <: Symbol @@ -2348,12 +2737,22 @@ See also: [`ModelingToolkit.dump_variable_metadata`](@ref), [`ModelingToolkit.du """ function dump_parameters(sys::AbstractSystem) defs = defaults(sys) - map(dump_variable_metadata.(parameters(sys))) do meta + pdeps = parameter_dependencies(sys) + metas = map(dump_variable_metadata.(parameters(sys))) do meta if haskey(defs, meta.var) meta = merge(meta, (; default = defs[meta.var])) end meta end + pdep_metas = map(collect(keys(pdeps))) do sym + val = pdeps[sym] + meta = dump_variable_metadata(sym) + meta = merge(meta, + (; dependency = pdeps[sym], + default = symbolic_evaluate(pdeps[sym], merge(defs, pdeps)))) + return meta + end + return vcat(metas, pdep_metas) end """ @@ -2384,3 +2783,241 @@ function dump_unknowns(sys::AbstractSystem) meta end end + +### Functions for accessing algebraic/differential equations in systems ### + +""" + is_diff_equation(eq) + +Return `true` if the input is a differential equation, i.e. an equation that contains a +differential term. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p d +@variables X(t) +eq1 = D(X) ~ p - d*X +eq2 = 0 ~ p - d*X + +is_diff_equation(eq1) # true +is_diff_equation(eq2) # false +``` +""" +function is_diff_equation(eq) + (eq isa Equation) || (return false) + isdefined(eq, :lhs) && hasnode(is_derivative, wrap(eq.lhs)) && + (return true) + isdefined(eq, :rhs) && hasnode(is_derivative, wrap(eq.rhs)) && + (return true) + return false +end + +""" + is_alg_equation(eq) + +Return `true` if the input is an algebraic equation, i.e. an equation that does not contain +any differentials. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p d +@variables X(t) +eq1 = D(X) ~ p - d*X +eq2 = 0 ~ p - d*X + +is_alg_equation(eq1) # false +is_alg_equation(eq2) # true +``` +""" +function is_alg_equation(eq) + return (eq isa Equation) && !is_diff_equation(eq) +end + +""" + alg_equations(sys::AbstractSystem) + +For a system, returns a vector of all its algebraic equations (i.e. that does not contain any +differentials). + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p d +@variables X(t) +eq1 = D(X) ~ p - d*X +eq2 = 0 ~ p - d*X +@named osys = ODESystem([eq1, eq2], t) + +alg_equations(osys) # returns `[0 ~ p - d*X(t)]`. +""" +alg_equations(sys::AbstractSystem) = filter(is_alg_equation, equations(sys)) + +""" + diff_equations(sys::AbstractSystem) + +For a system, returns a vector of all its differential equations (i.e. that does contain a differential). + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p d +@variables X(t) +eq1 = D(X) ~ p - d*X +eq2 = 0 ~ p - d*X +@named osys = ODESystem([eq1, eq2], t) + +diff_equations(osys) # returns `[Differential(t)(X(t)) ~ p - d*X(t)]`. +""" +diff_equations(sys::AbstractSystem) = filter(is_diff_equation, equations(sys)) + +""" + has_alg_equations(sys::AbstractSystem) + +For a system, returns true if it contain at least one algebraic equation (i.e. that does not contain any +differentials). + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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) + +has_alg_equations(osys1) # returns `false`. +has_alg_equations(osys2) # returns `true`. +``` +""" +has_alg_equations(sys::AbstractSystem) = any(is_alg_equation, equations(sys)) + +""" + has_diff_equations(sys::AbstractSystem) + +For a system, returns true if it contain at least one differential equation (i.e. that contain a differential). + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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) + +has_diff_equations(osys1) # returns `true`. +has_diff_equations(osys2) # returns `false`. +``` +""" +has_diff_equations(sys::AbstractSystem) = any(is_diff_equation, equations(sys)) + +""" + get_alg_eqs(sys::AbstractSystem) + +For a system, returns a vector of all algebraic equations (i.e. that does not contain any +differentials) in its *top-level system*. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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]) +osys21 = compose(osys2, [osys1]) + +get_alg_eqs(osys12) # returns `Equation[]`. +get_alg_eqs(osys21) # returns `[0 ~ p - d*X(t)]`. +``` +""" +get_alg_eqs(sys::AbstractSystem) = filter(is_alg_equation, get_eqs(sys)) + +""" + get_diff_eqs(sys::AbstractSystem) + +For a system, returns a vector of all differential equations (i.e. that does contain a differential) +in its *top-level system*. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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]) +osys21 = compose(osys2, [osys1]) + +get_diff_eqs(osys12) # returns `[Differential(t)(X(t)) ~ p - d*X(t)]`. +get_diff_eqs(osys21) # returns `Equation[]``. +``` +""" +get_diff_eqs(sys::AbstractSystem) = filter(is_diff_equation, get_eqs(sys)) + +""" + has_alg_eqs(sys::AbstractSystem) + +For a system, returns true if it contain at least one algebraic equation (i.e. that does not contain any +differentials) in its *top-level system*. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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]) +osys21 = compose(osys2, [osys1]) + +has_alg_eqs(osys12) # returns `false`. +has_alg_eqs(osys21) # returns `true`. +``` +""" +has_alg_eqs(sys::AbstractSystem) = any(is_alg_equation, get_eqs(sys)) + +""" + has_diff_eqs(sys::AbstractSystem) + +Return `true` if a system contains at least one differential equation (i.e. an equation with a +differential term). Note that this does not consider subsystems, and only takes into account +equations in the top-level system. + +Example: +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +@parameters p 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]) +osys21 = compose(osys2, [osys1]) + +has_diff_eqs(osys12) # returns `true`. +has_diff_eqs(osys21) # returns `false`. +``` +""" +has_diff_eqs(sys::AbstractSystem) = any(is_diff_equation, get_eqs(sys)) diff --git a/src/systems/alias_elimination.jl b/src/systems/alias_elimination.jl index 3a2405e6bd..fb4fedc920 100644 --- a/src/systems/alias_elimination.jl +++ b/src/systems/alias_elimination.jl @@ -388,8 +388,10 @@ Use Kahn's algorithm to topologically sort observed equations. Example: ```julia -julia> @variables t x(t) y(t) z(t) k(t) -(t, x(t), y(t), z(t), k(t)) +julia> t = ModelingToolkit.t_nounits + +julia> @variables x(t) y(t) z(t) k(t) +(x(t), y(t), z(t), k(t)) julia> eqs = [ x ~ y + z diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 41389d39e3..3fe1f7f006 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -138,6 +138,14 @@ function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuo namespace_affects(affects(cb), s)) end +""" + continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} + +Returns a vector of all the `continuous_events` in an abstract system and its component subsystems. +The `SymbolicContinuousCallback`s in the returned vector are structs with two fields: `eqs` and +`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. +`eqs => affect`. +""" function continuous_events(sys::AbstractSystem) obs = get_continuous_events(sys) filter(!isempty, obs) @@ -234,6 +242,14 @@ SymbolicDiscreteCallbacks(cb::SymbolicDiscreteCallback) = [cb] SymbolicDiscreteCallbacks(cbs::Vector{<:SymbolicDiscreteCallback}) = cbs SymbolicDiscreteCallbacks(::Nothing) = SymbolicDiscreteCallback[] +""" + discrete_events(sys::AbstractSystem) :: Vector{SymbolicDiscreteCallback} + +Returns a vector of all the `discrete_events` in an abstract system and its component subsystems. +The `SymbolicDiscreteCallback`s in the returned vector are structs with two fields: `condition` and +`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. +`condition => affect`. +""" function discrete_events(sys::AbstractSystem) obs = get_discrete_events(sys) systems = get_systems(sys) @@ -327,7 +343,7 @@ Notes - `kwargs` are passed through to `Symbolics.build_function`. """ function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; - expression = Val{true}, kwargs...) + expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, kwargs...) u = map(x -> time_varying_as_func(value(x), sys), dvs) p = map.(x -> time_varying_as_func(value(x), sys), reorder_parameters(sys, ps)) t = get_iv(sys) @@ -337,8 +353,13 @@ function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; cmap = map(x -> x => getdefault(x), cs) condit = substitute(condit, cmap) end - build_function(condit, u, t, p...; expression, wrap_code = condition_header(sys), + expr = build_function( + condit, u, t, p...; expression = Val{true}, wrap_code = condition_header(sys), kwargs...) + if expression == Val{true} + return expr + end + return eval_or_rgf(expr; eval_expression, eval_module) end function compile_affect(cb::SymbolicContinuousCallback, args...; kwargs...) @@ -363,7 +384,8 @@ Notes - `kwargs` are passed through to `Symbolics.build_function`. """ function compile_affect(eqs::Vector{Equation}, sys, dvs, ps; outputidxs = nothing, - expression = Val{true}, checkvars = true, + expression = Val{true}, checkvars = true, eval_expression = false, + eval_module = @__MODULE__, postprocess_affect_expr! = nothing, kwargs...) if isempty(eqs) if expression == Val{true} @@ -372,6 +394,7 @@ function compile_affect(eqs::Vector{Equation}, sys, dvs, ps; outputidxs = nothin return (args...) -> () # We don't do anything in the callback, we're just after the event end else + eqs = flatten_equations(eqs) rhss = map(x -> x.rhs, eqs) outvar = :u if outputidxs === nothing @@ -415,9 +438,8 @@ function compile_affect(eqs::Vector{Equation}, sys, dvs, ps; outputidxs = nothin end t = get_iv(sys) integ = gensym(:MTKIntegrator) - getexpr = (postprocess_affect_expr! === nothing) ? expression : Val{true} pre = get_preprocess_constants(rhss) - rf_oop, rf_ip = build_function(rhss, u, p..., t; expression = getexpr, + rf_oop, rf_ip = build_function(rhss, u, p..., t; expression = Val{true}, wrap_code = add_integrator_header(sys, integ, outvar), outputidxs = update_inds, postprocess_fbody = pre, @@ -425,10 +447,11 @@ function compile_affect(eqs::Vector{Equation}, sys, dvs, ps; outputidxs = nothin # applied user-provided function to the generated expression if postprocess_affect_expr! !== nothing postprocess_affect_expr!(rf_ip, integ) - (expression == Val{false}) && - (return drop_expr(@RuntimeGeneratedFunction(rf_ip))) end - rf_ip + if expression == Val{false} + return eval_or_rgf(rf_ip; eval_expression, eval_module) + end + return rf_ip end end @@ -441,7 +464,7 @@ end function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknowns(sys), ps = full_parameters(sys); kwargs...) - eqs = map(cb -> cb.eqs, cbs) + eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) (isempty(eqs) || sum(num_eqs) == 0) && return nothing # fuse equations to create VectorContinuousCallback @@ -455,12 +478,8 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow rhss = map(x -> x.rhs, eqs) root_eq_vars = unique(collect(Iterators.flatten(map(ModelingToolkit.vars, rhss)))) - u = map(x -> time_varying_as_func(value(x), sys), dvs) - p = map.(x -> time_varying_as_func(value(x), sys), reorder_parameters(sys, ps)) - t = get_iv(sys) - pre = get_preprocess_constants(rhss) - rf_oop, rf_ip = build_function(rhss, u, p..., t; expression = Val{false}, - postprocess_fbody = pre, kwargs...) + rf_oop, rf_ip = generate_custom_function(sys, rhss, dvs, ps; expression = Val{false}, + kwargs...) affect_functions = map(cbs) do cb # Keep affect function separate eq_aff = affects(cb) @@ -471,16 +490,16 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow cond = function (u, t, integ) if DiffEqBase.isinplace(integ.sol.prob) tmp, = DiffEqBase.get_tmp_cache(integ) - rf_ip(tmp, u, parameter_values(integ)..., t) + rf_ip(tmp, u, parameter_values(integ), t) tmp[1] else - rf_oop(u, parameter_values(integ)..., t) + rf_oop(u, parameter_values(integ), t) end end ContinuousCallback(cond, affect_functions[]) else cond = function (out, u, t, integ) - rf_ip(out, u, parameter_values(integ)..., t) + rf_ip(out, u, parameter_values(integ), t) end # since there may be different number of conditions and affects, diff --git a/src/systems/clock_inference.jl b/src/systems/clock_inference.jl index c4c18d5bdb..dc1d612d73 100644 --- a/src/systems/clock_inference.jl +++ b/src/systems/clock_inference.jl @@ -21,6 +21,52 @@ function ClockInference(ts::TransformationState) ClockInference(ts, eq_domain, var_domain, inferred) end +struct NotInferredTimeDomain end +function error_sample_time(eq) + error("$eq\ncontains `SampleTime` but it is not an Inferred discrete equation.") +end +function substitute_sample_time(ci::ClockInference, ts::TearingState) + @unpack eq_domain = ci + eqs = copy(equations(ts)) + @assert length(eqs) == length(eq_domain) + for i in eachindex(eqs) + eq = eqs[i] + domain = eq_domain[i] + dt = sampletime(domain) + neweq = substitute_sample_time(eq, dt) + if neweq isa NotInferredTimeDomain + error_sample_time(eq) + end + eqs[i] = neweq + end + @set! ts.sys.eqs = eqs + @set! ci.ts = ts +end + +function substitute_sample_time(eq::Equation, dt) + substitute_sample_time(eq.lhs, dt) ~ substitute_sample_time(eq.rhs, dt) +end + +function substitute_sample_time(ex, dt) + iscall(ex) || return ex + op = operation(ex) + args = arguments(ex) + if op == SampleTime + dt === nothing && return NotInferredTimeDomain() + return dt + else + new_args = similar(args) + for (i, arg) in enumerate(args) + ex_arg = substitute_sample_time(arg, dt) + if ex_arg isa NotInferredTimeDomain + return ex_arg + end + new_args[i] = ex_arg + end + maketerm(typeof(ex), op, new_args, symtype(ex), metadata(ex)) + end +end + function infer_clocks!(ci::ClockInference) @unpack ts, eq_domain, var_domain, inferred = ci @unpack var_to_diff, graph = ts.structure @@ -66,6 +112,7 @@ function infer_clocks!(ci::ClockInference) end end + ci = substitute_sample_time(ci, ts) return ci end @@ -81,7 +128,7 @@ function resize_or_push!(v, val, idx) end function is_time_domain_conversion(v) - istree(v) && (o = operation(v)) isa Operator && + iscall(v) && (o = operation(v)) isa Operator && input_timedomain(o) != output_timedomain(o) end @@ -145,7 +192,7 @@ end function generate_discrete_affect( osys::AbstractODESystem, syss, inputs, continuous_id, id_to_clock; checkbounds = true, - eval_module = @__MODULE__, eval_expression = true) + eval_module = @__MODULE__, eval_expression = false) @static if VERSION < v"1.7" error("The `generate_discrete_affect` function requires at least Julia 1.7") end @@ -365,15 +412,17 @@ function generate_discrete_affect( push!(svs, sv) end if eval_expression + affects = map(a -> eval_module.eval(toexpr(LiteralExpr(a))), affect_funs) + inits = map(a -> eval_module.eval(toexpr(LiteralExpr(a))), init_funs) + else affects = map(affect_funs) do a - drop_expr(@RuntimeGeneratedFunction(eval_module, toexpr(LiteralExpr(a)))) + drop_expr(RuntimeGeneratedFunction( + eval_module, eval_module, toexpr(LiteralExpr(a)))) end inits = map(init_funs) do a - drop_expr(@RuntimeGeneratedFunction(eval_module, toexpr(LiteralExpr(a)))) + drop_expr(RuntimeGeneratedFunction( + eval_module, eval_module, toexpr(LiteralExpr(a)))) end - else - affects = map(a -> toexpr(LiteralExpr(a)), affect_funs) - inits = map(a -> toexpr(LiteralExpr(a)), init_funs) end defaults = Dict{Any, Any}(v => 0.0 for v in Iterators.flatten(inputs)) return affects, inits, clocks, svs, appended_parameters, defaults diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index 88df41d94f..227b4624bf 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -14,7 +14,7 @@ end function get_connection_type(s) s = unwrap(s) - if istree(s) && operation(s) === getindex + if iscall(s) && operation(s) === getindex s = arguments(s)[1] end getmetadata(s, VariableConnectType, Equality) @@ -82,10 +82,10 @@ function collect_instream!(set, eq::Equation) end function collect_instream!(set, expr, occurs = false) - istree(expr) || return occurs + iscall(expr) || return occurs op = operation(expr) op === instream && (push!(set, expr); occurs = true) - for a in SymbolicUtils.unsorted_arguments(expr) + for a in SymbolicUtils.arguments(expr) occurs |= collect_instream!(set, a, occurs) end return occurs @@ -129,7 +129,7 @@ function generate_isouter(sys::AbstractSystem) function isouter(sys)::Bool s = string(nameof(sys)) isconnector(sys) || error("$s is not a connector!") - idx = findfirst(isequal('₊'), s) + idx = findfirst(isequal(NAMESPACE_SEPARATOR), s) parent_name = Symbol(idx === nothing ? s : s[1:prevind(s, idx)]) parent_name in outer_connectors end @@ -163,11 +163,16 @@ end Base.nameof(l::ConnectionElement) = renamespace(nameof(l.sys), getname(l.v)) Base.isequal(l1::ConnectionElement, l2::ConnectionElement) = l1 == l2 function Base.:(==)(l1::ConnectionElement, l2::ConnectionElement) - nameof(l1.sys) == nameof(l2.sys) && isequal(l1.v, l2.v) && l1.isouter == l2.isouter + l1.isouter == l2.isouter && nameof(l1.sys) == nameof(l2.sys) && isequal(l1.v, l2.v) end const _debug_mode = Base.JLOptions().check_bounds == 1 +function Base.show(io::IO, c::ConnectionElement) + @unpack sys, v, isouter = c + print(io, nameof(sys), ".", v, "::", isouter ? "outer" : "inner") +end + function Base.hash(e::ConnectionElement, salt::UInt) if _debug_mode @assert e.h === _hash_impl(e.sys, e.v, e.isouter) @@ -187,7 +192,10 @@ end struct ConnectionSet set::Vector{ConnectionElement} # namespace.sys, var, isouter end +ConnectionSet() = ConnectionSet(ConnectionElement[]) Base.copy(c::ConnectionSet) = ConnectionSet(copy(c.set)) +Base.:(==)(a::ConnectionSet, b::ConnectionSet) = a.set == b.set +Base.sort(a::ConnectionSet) = ConnectionSet(sort(a.set, by = string)) function Base.show(io::IO, c::ConnectionSet) print(io, "<") @@ -373,51 +381,42 @@ function generate_connection_set!(connectionsets, domain_csets, end function Base.merge(csets::AbstractVector{<:ConnectionSet}, allouter = false) - csets, merged = partial_merge(csets, allouter) - while merged - csets, merged = partial_merge(csets) - end - csets -end - -function partial_merge(csets::AbstractVector{<:ConnectionSet}, allouter = false) - mcsets = ConnectionSet[] ele2idx = Dict{ConnectionElement, Int}() - cacheset = Set{ConnectionElement}() - merged = false - for (j, cset) in enumerate(csets) - if allouter - cset = ConnectionSet(map(withtrueouter, cset.set)) - end - idx = nothing - for e in cset.set - idx = get(ele2idx, e, nothing) - if idx !== nothing - merged = true - break + idx2ele = ConnectionElement[] + union_find = IntDisjointSets(0) + prev_id = Ref(-1) + for cset in csets, (j, s) in enumerate(cset.set) + v = allouter ? withtrueouter(s) : s + id = let ele2idx = ele2idx, idx2ele = idx2ele + get!(ele2idx, v) do + push!(idx2ele, v) + id = length(idx2ele) + id′ = push!(union_find) + @assert id == id′ + id end end - if idx === nothing - push!(mcsets, copy(cset)) - for e in cset.set - ele2idx[e] = length(mcsets) - end - else - for e in mcsets[idx].set - push!(cacheset, e) - end - for e in cset.set - push!(cacheset, e) - end - empty!(mcsets[idx].set) - for e in cacheset - ele2idx[e] = idx - push!(mcsets[idx].set, e) - end - empty!(cacheset) + # isequal might not be equal? lol + if v.sys.namespace !== nothing + idx2ele[id] = v + end + if j > 1 + union!(union_find, prev_id[], id) + end + prev_id[] = id + end + id2set = Dict{Int, Int}() + merged_set = ConnectionSet[] + for (id, ele) in enumerate(idx2ele) + rid = find_root!(union_find, id) + set_idx = get!(id2set, rid) do + set = ConnectionSet() + push!(merged_set, set) + length(merged_set) end + push!(merged_set[set_idx].set, ele) end - mcsets, merged + merged_set end function generate_connection_equations_and_stream_connections(csets::AbstractVector{ diff --git a/src/systems/dependency_graphs.jl b/src/systems/dependency_graphs.jl index 08755a57cb..c46cdca831 100644 --- a/src/systems/dependency_graphs.jl +++ b/src/systems/dependency_graphs.jl @@ -15,8 +15,9 @@ Example: ```julia using ModelingToolkit +using ModelingToolkit: t_nounits as t @parameters β γ κ η -@variables t S(t) I(t) R(t) +@variables S(t) I(t) R(t) rate₁ = β * S * I rate₂ = γ * I + t diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index a1fc4ee496..11abadad5f 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -202,7 +202,7 @@ end function isdelay(var, iv) iv === nothing && return false isvariable(var) || return false - if istree(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) + if iscall(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) args = arguments(var) length(args) == 1 || return false isequal(args[1], iv) || return true @@ -229,10 +229,11 @@ function delay_to_function(expr, iv, sts, ps, h) time = arguments(expr)[1] idx = sts[v] return term(getindex, h(Sym{Any}(:ˍ₋arg3), time), idx, type = Real) # BIG BIG HACK - elseif istree(expr) - return similarterm(expr, + elseif iscall(expr) + return maketerm(typeof(expr), operation(expr), - map(x -> delay_to_function(x, iv, sts, ps, h), arguments(expr))) + map(x -> delay_to_function(x, iv, sts, ps, h), arguments(expr)), + symtype(expr), metadata(expr)) else return expr end @@ -240,10 +241,9 @@ end function calculate_massmatrix(sys::AbstractODESystem; simplify = false) eqs = [eq for eq in equations(sys)] - dvs = unknowns(sys) M = zeros(length(eqs), length(eqs)) for (i, eq) in enumerate(eqs) - if istree(eq.lhs) && operation(eq.lhs) isa Differential + 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 @@ -313,7 +313,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, version = nothing, tgrad = false, jac = false, p = nothing, t = nothing, - eval_expression = true, + eval_expression = false, sparse = false, simplify = false, eval_module = @__MODULE__, steady_state = false, @@ -327,12 +327,11 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, 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{eval_expression}, + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - f_oop, f_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in f_gen) : - f_gen + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) + f(u, p, t) = f_oop(u, p, t) f(du, u, p, t) = f_iip(du, u, p, t) f(u, p::Tuple{Vararg{Number}}, t) = f_oop(u, p, t) @@ -352,12 +351,11 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, if tgrad tgrad_gen = generate_tgrad(sys, dvs, ps; simplify = simplify, - expression = Val{eval_expression}, + expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - tgrad_oop, tgrad_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in tgrad_gen) : - tgrad_gen + tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) + if p isa Tuple __tgrad(u, p, t) = tgrad_oop(u, p..., t) __tgrad(J, u, p, t) = tgrad_iip(J, u, p..., t) @@ -374,12 +372,11 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, if jac jac_gen = generate_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, - expression = Val{eval_expression}, + expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - jac_oop, jac_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in jac_gen) : - jac_gen + jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) + _jac(u, p, t) = jac_oop(u, p, t) _jac(J, u, p, t) = jac_iip(J, u, p, t) _jac(u, p::Tuple{Vararg{Number}}, t) = jac_oop(u, p, t) @@ -402,85 +399,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, ArrayInterface.restructure(u0 .* u0', M) end - obs = observed(sys) - observedfun = if steady_state - let sys = sys, dict = Dict(), ps = ps - function generated_observed(obsvar, args...) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar) - end - if args === () - let obs = obs, ps_T = typeof(ps) - (u, p, t = Inf) -> if p isa MTKParameters - obs(u, p..., t) - elseif ps_T <: Tuple - obs(u, p..., t) - else - obs(u, p, t) - end - end - else - if args[2] isa MTKParameters - if length(args) == 2 - u, p = args - obs(u, p..., Inf) - else - u, p, t = args - obs(u, p..., t) - end - elseif ps isa Tuple - if length(args) == 2 - u, p = args - obs(u, p..., Inf) - else - u, p, t = args - obs(u, p..., t) - end - else - if length(args) == 2 - u, p = args - obs(u, p, Inf) - else - u, p, t = args - obs(u, p, t) - end - end - end - end - end - else - let sys = sys, dict = Dict(), ps = ps - function generated_observed(obsvar, args...) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, - obsvar; - checkbounds = checkbounds, - ps) - end - if args === () - let obs = obs, ps_T = typeof(ps) - (u, p, t) -> if p isa MTKParameters - obs(u, p..., t) - elseif ps_T <: Tuple - obs(u, p..., t) - else - obs(u, p, t) - end - end - else - u, p, t = args - if p isa MTKParameters - u, p, t = args - obs(u, p..., t) - elseif ps isa Tuple # split parameters - obs(u, p..., t) - else - obs(args...) - end - end - end - end - end + observedfun = ObservedFunctionCache(sys; steady_state, eval_expression, eval_module) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) @@ -531,7 +450,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) ddvs = map(diff2term ∘ Differential(get_iv(sys)), dvs), version = nothing, p = nothing, jac = false, - eval_expression = true, + eval_expression = false, sparse = false, simplify = false, eval_module = @__MODULE__, checkbounds = false, @@ -542,12 +461,10 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(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{eval_expression}, + expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - f_oop, f_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in f_gen) : - f_gen + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(du, u, p, t) = f_oop(du, u, p, t) f(du, u, p::MTKParameters, t) = f_oop(du, u, p..., t) f(out, du, u, p, t) = f_iip(out, du, u, p, t) @@ -556,12 +473,11 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) if jac jac_gen = generate_dae_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, - expression = Val{eval_expression}, + expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - jac_oop, jac_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in jac_gen) : - jac_gen + jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) + _jac(du, u, p, ˍ₋gamma, t) = jac_oop(du, u, p, ˍ₋gamma, t) _jac(du, u, p::MTKParameters, ˍ₋gamma, t) = jac_oop(du, u, p..., ˍ₋gamma, t) @@ -571,24 +487,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) _jac = nothing end - obs = observed(sys) - observedfun = let sys = sys, dict = Dict() - function generated_observed(obsvar, args...) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar; checkbounds = checkbounds) - end - if args === () - let obs = obs - fun(u, p, t) = obs(u, p, t) - fun(u, p::MTKParameters, t) = obs(u, p..., t) - fun - end - else - u, p, t = args - p isa MTKParameters ? obs(u, p..., t) : obs(u, p, t) - end - end - end + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) @@ -619,6 +518,7 @@ end function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; + eval_expression = false, eval_module = @__MODULE__, checkbounds = false, kwargs...) where {iip} @@ -629,7 +529,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - f_oop, f_iip = (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in f_gen) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, h, p, t) = f_oop(u, h, p, t) f(u, h, p::MTKParameters, t) = f_oop(u, h, p..., t) f(du, u, h, p, t) = f_iip(du, u, h, p, t) @@ -644,6 +544,7 @@ end function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; + eval_expression = false, eval_module = @__MODULE__, checkbounds = false, kwargs...) where {iip} @@ -654,10 +555,10 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, kwargs...) - f_oop, f_iip = (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in f_gen) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, isdde = true, kwargs...) - g_oop, g_iip = (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in g_gen) + g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) f(u, h, p, t) = f_oop(u, h, p, t) f(u, h, p::MTKParameters, t) = f_oop(u, h, p..., t) f(du, u, h, p, t) = f_iip(du, u, h, p, t) @@ -751,6 +652,7 @@ function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), $_jac M = $_M ODEFunction{$iip}($fsym, + sys = $sys, jac = $jacsym, tgrad = $tgradsym, mass_matrix = M, @@ -789,7 +691,9 @@ function get_u0_p(sys, @warn "Observed variables cannot be assigned initial values. Initial values for $u0s_in_obs will be ignored." end end - defs = mergedefaults(defs, u0map, dvs) + obs = filter!(x -> !(x[1] isa Number), map(x -> x.rhs => x.lhs, observed(sys))) + observedmap = isempty(obs) ? Dict() : todict(obs) + defs = mergedefaults(defs, observedmap, u0map, dvs) for (k, v) in defs if Symbolics.isarraysymbolic(k) ks = scalarize(k) @@ -820,7 +724,17 @@ function get_u0( if parammap !== nothing defs = mergedefaults(defs, parammap, ps) end - defs = mergedefaults(defs, u0map, dvs) + + # Convert observed equations "lhs ~ rhs" into defaults. + # Use the order "lhs => rhs" by default, but flip it to "rhs => lhs" + # if "lhs" is known by other means (parameter, another default, ...) + # TODO: Is there a better way to determine which equations to flip? + obs = map(x -> x.lhs => x.rhs, observed(sys)) + obs = map(x -> x[1] in keys(defs) ? reverse(x) : x, obs) + obs = filter!(x -> !(x[1] isa Number), obs) # exclude e.g. "0 => x^2 + y^2 - 25" + obsmap = isempty(obs) ? Dict() : todict(obs) + + defs = mergedefaults(defs, obsmap, u0map, dvs) if symbolic_u0 u0 = varmap_to_vars( u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) @@ -837,7 +751,8 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; checkbounds = false, sparse = false, simplify = false, linenumbers = true, parallel = SerialForm(), - eval_expression = true, + eval_expression = false, + eval_module = @__MODULE__, use_union = true, tofloat = true, symbolic_u0 = false, @@ -846,6 +761,8 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; t = nothing, warn_initialize_determined = true, build_initializeprob = true, + initialization_eqs = [], + fully_determined = false, kwargs...) eqs = equations(sys) dvs = unknowns(sys) @@ -857,6 +774,7 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; varmap = u0map === nothing || isempty(u0map) || eltype(u0map) <: Number ? defaults(sys) : merge(defaults(sys), todict(u0map)) + varmap = canonicalize_varmap(varmap) varlist = collect(map(unwrap, dvs)) missingvars = setdiff(varlist, collect(keys(varmap))) @@ -875,37 +793,41 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; parammap = Dict(unwrap.(parameters(sys)) .=> parammap) end end - clockedparammap = Dict() - defs = ModelingToolkit.get_defaults(sys) - for v in ps - v = unwrap(v) - is_discrete_domain(v) || continue - op = operation(v) - if !isa(op, Symbolics.Operator) && parammap != SciMLBase.NullParameters() && - haskey(parammap, v) - error("Initial conditions for discrete variables must be for the past state of the unknown. Instead of providing the condition for $v, provide the condition for $(Shift(iv, -1)(v)).") + + if has_discrete_subsystems(sys) && get_discrete_subsystems(sys) !== nothing + clockedparammap = Dict() + defs = ModelingToolkit.get_defaults(sys) + for v in ps + v = unwrap(v) + is_discrete_domain(v) || continue + op = operation(v) + if !isa(op, Symbolics.Operator) && parammap != SciMLBase.NullParameters() && + haskey(parammap, v) + error("Initial conditions for discrete variables must be for the past state of the unknown. Instead of providing the condition for $v, provide the condition for $(Shift(iv, -1)(v)).") + end + shiftedv = StructuralTransformations.simplify_shifts(Shift(iv, -1)(v)) + if parammap != SciMLBase.NullParameters() && + (val = get(parammap, shiftedv, nothing)) !== nothing + clockedparammap[v] = val + elseif op isa Shift + root = arguments(v)[1] + haskey(defs, root) || error("Initial condition for $v not provided.") + clockedparammap[v] = defs[root] + end end - shiftedv = StructuralTransformations.simplify_shifts(Shift(iv, -1)(v)) - if parammap != SciMLBase.NullParameters() && - (val = get(parammap, shiftedv, nothing)) !== nothing - clockedparammap[v] = val - elseif op isa Shift - root = arguments(v)[1] - haskey(defs, root) || error("Initial condition for $v not provided.") - clockedparammap[v] = defs[root] + parammap = if parammap == SciMLBase.NullParameters() + clockedparammap + else + merge(parammap, clockedparammap) end end - parammap = if parammap == SciMLBase.NullParameters() - clockedparammap - else - merge(parammap, clockedparammap) - end # TODO: make it work with clocks # ModelingToolkit.get_tearing_state(sys) !== nothing => Requires structural_simplify first if sys isa ODESystem && build_initializeprob && - (implicit_dae || !isempty(missingvars)) && - all(isequal(Continuous()), ci.var_domain) && - ModelingToolkit.get_tearing_state(sys) !== nothing + (((implicit_dae || !isempty(missingvars)) && + all(isequal(Continuous()), ci.var_domain) && + ModelingToolkit.get_tearing_state(sys) !== nothing) || + !isempty(initialization_equations(sys))) && t !== nothing if eltype(u0map) <: Number u0map = unknowns(sys) .=> u0map end @@ -913,7 +835,8 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; u0map = Dict() end initializeprob = ModelingToolkit.InitializationProblem( - sys, t, u0map, parammap; guesses, warn_initialize_determined) + sys, t, u0map, parammap; guesses, warn_initialize_determined, + initialization_eqs, eval_expression, eval_module, fully_determined) initializeprobmap = getu(initializeprob, unknowns(sys)) zerovars = Dict(setdiff(unknowns(sys), keys(defaults(sys))) .=> 0.0) @@ -929,7 +852,12 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; if has_index_cache(sys) && get_index_cache(sys) !== nothing u0, defs = get_u0(sys, trueinit, parammap; symbolic_u0) check_eqs_u0(eqs, dvs, u0; kwargs...) - p = MTKParameters(sys, parammap, trueinit) + p = if parammap === nothing || + parammap == SciMLBase.NullParameters() && isempty(defs) + nothing + else + MTKParameters(sys, parammap, trueinit; eval_expression, eval_module) + end else u0, p, defs = get_u0_p(sys, trueinit, @@ -957,11 +885,11 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; ddvs = nothing end check_eqs_u0(eqs, dvs, u0; kwargs...) - f = constructor(sys, dvs, ps, u0; ddvs = ddvs, tgrad = tgrad, jac = jac, checkbounds = checkbounds, p = p, linenumbers = linenumbers, parallel = parallel, simplify = simplify, sparse = sparse, eval_expression = eval_expression, + eval_module = eval_module, initializeprob = initializeprob, initializeprobmap = initializeprobmap, kwargs...) @@ -1068,17 +996,20 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = 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 f, u0, p = process_DEProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, kwargs...) - cbs = process_events(sys; callback, kwargs...) + check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) inits = [] if has_discrete_subsystems(sys) && (dss = get_discrete_subsystems(sys)) !== nothing - affects, inits, clocks, svs = ModelingToolkit.generate_discrete_affect(sys, dss...) + affects, inits, clocks, svs = ModelingToolkit.generate_discrete_affect( + sys, dss...; eval_expression, eval_module) discrete_cbs = map(affects, clocks, svs) do affect, clock, sv if clock isa Clock PeriodicCallback(DiscreteSaveAffect(affect, sv), clock.dt; @@ -1163,9 +1094,9 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan kwargs...) end -function generate_history(sys::AbstractODESystem, u0; kwargs...) +function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) p = reorder_parameters(sys, full_parameters(sys)) - build_function(u0, p..., get_iv(sys); expression = Val{false}, kwargs...) + build_function(u0, p..., get_iv(sys); expression, kwargs...) end function DiffEqBase.DDEProblem(sys::AbstractODESystem, args...; kwargs...) @@ -1176,6 +1107,8 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], parammap = DiffEqBase.NullParameters(); callback = nothing, 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 `DDEProblem`") @@ -1183,14 +1116,16 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], f, u0, p = process_DEProblem(DDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, symbolic_u0 = true, - check_length, kwargs...) - h_oop, h_iip = generate_history(sys, u0) + check_length, eval_expression, eval_module, kwargs...) + h_gen = generate_history(sys, u0; expression = Val{true}) + h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) h(p, t) = h_oop(p, t) h(p::MTKParameters, t) = h_oop(p..., t) u0 = h(p, tspan[1]) - cbs = process_events(sys; callback, kwargs...) + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) if has_discrete_subsystems(sys) && (dss = get_discrete_subsystems(sys)) !== nothing - affects, clocks, svs = ModelingToolkit.generate_discrete_affect(sys, dss...) + affects, clocks, svs = ModelingToolkit.generate_discrete_affect( + sys, dss...; eval_expression, eval_module) discrete_cbs = map(affects, clocks, svs) do affect, clock, sv if clock isa Clock PeriodicCallback(DiscreteSaveAffect(affect, sv), clock.dt; @@ -1232,21 +1167,27 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], callback = nothing, check_length = true, sparsenoise = nothing, + 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 `SDDEProblem`") end f, u0, p = process_DEProblem(SDDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, + symbolic_u0 = true, eval_expression, eval_module, check_length, kwargs...) - h_oop, h_iip = generate_history(sys, u0) + h_gen = generate_history(sys, u0; expression = Val{true}) + h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) h(out, p, t) = h_iip(out, p, t) h(p, t) = h_oop(p, t) + h(p::MTKParameters, t) = h_oop(p..., t) + h(out, p::MTKParameters, t) = h_iip(out, p..., t) u0 = h(p, tspan[1]) - cbs = process_events(sys; callback, kwargs...) + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) if has_discrete_subsystems(sys) && (dss = get_discrete_subsystems(sys)) !== nothing - affects, clocks, svs = ModelingToolkit.generate_discrete_affect(sys, dss...) + affects, clocks, svs = ModelingToolkit.generate_discrete_affect( + sys, dss...; eval_expression, eval_module) discrete_cbs = map(affects, clocks, svs) do affect, clock, sv if clock isa Clock PeriodicCallback(DiscreteSaveAffect(affect, sv), clock.dt; @@ -1537,10 +1478,12 @@ InitializationProblem{iip}(sys::AbstractODESystem, u0map, tspan, 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 +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. """ @@ -1585,18 +1528,20 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, guesses = [], check_length = true, warn_initialize_determined = true, + initialization_eqs = [], + fully_determined = 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) + isys = get_initializesystem(sys; initialization_eqs) elseif isempty(u0map) && get_initializesystem(sys) === nothing - isys = structural_simplify(generate_initializesystem(sys); fully_determined = false) + isys = structural_simplify( + generate_initializesystem(sys; initialization_eqs); fully_determined) else isys = structural_simplify( - generate_initializesystem(sys; u0map); fully_determined = false) + generate_initializesystem(sys; u0map, initialization_eqs); fully_determined) end uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) @@ -1611,19 +1556,26 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, nunknown = length(unknowns(isys)) if warn_initialize_determined && neqs > nunknown - @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false." + @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. 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. To suppress this warning pass warn_initialize_determined = false." + @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" end - parammap isa DiffEqBase.NullParameters || isempty(parammap) ? - [get_iv(sys) => t] : - merge(todict(parammap), Dict(get_iv(sys) => t)) + parammap = parammap isa DiffEqBase.NullParameters || isempty(parammap) ? + [get_iv(sys) => t] : + merge(todict(parammap), Dict(get_iv(sys) => t)) + if isempty(u0map) + u0map = Dict() + end + if isempty(guesses) + guesses = Dict() + end + u0map = merge(todict(guesses), todict(u0map)) if neqs == nunknown - NonlinearProblem(isys, guesses, parammap) + NonlinearProblem(isys, u0map, parammap; kwargs...) else - NonlinearLeastSquaresProblem(isys, guesses, parammap) + NonlinearLeastSquaresProblem(isys, u0map, parammap; kwargs...) end end diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 61682c806e..e2be889c5e 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -16,7 +16,8 @@ Example: ```julia using ModelingToolkit, OrdinaryDiffEq, Test -@parameters t α β γ δ +@independent_variables t +@parameters α β γ δ @variables x(t) y(t) D = Differential(t) diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index d398778b33..b72a78add9 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -3,21 +3,36 @@ $(TYPEDSIGNATURES) Generate `ODESystem`, dependent variables, and parameters from an `ODEProblem`. """ -function modelingtoolkitize(prob::DiffEqBase.ODEProblem; kwargs...) +function modelingtoolkitize( + prob::DiffEqBase.ODEProblem; u_names = nothing, p_names = nothing, kwargs...) prob.f isa DiffEqBase.AbstractParameterizedFunction && return prob.f.sys - @parameters t - + t = t_nounits p = prob.p has_p = !(p isa Union{DiffEqBase.NullParameters, Nothing}) - _vars = define_vars(prob.u0, t) + if u_names !== nothing + varnames_length_check(prob.u0, u_names; is_unknowns = true) + _vars = [_defvar(name)(t) for name in u_names] + elseif SciMLBase.has_sys(prob.f) + varnames = getname.(variable_symbols(prob.f.sys)) + varidxs = variable_index.((prob.f.sys,), varnames) + invpermute!(varnames, varidxs) + _vars = [_defvar(name)(t) for name in varnames] + else + _vars = define_vars(prob.u0, t) + end vars = prob.u0 isa Number ? _vars : ArrayInterface.restructure(prob.u0, _vars) params = if has_p - _params = define_params(p) + if p_names === nothing && SciMLBase.has_sys(prob.f) + p_names = Dict(parameter_index(prob.f.sys, sym) => sym + for sym in parameter_symbols(prob.f.sys)) + end + _params = define_params(p, p_names) p isa Number ? _params[1] : - (p isa Tuple || p isa NamedTuple || p isa AbstractDict ? _params : + (p isa Tuple || p isa NamedTuple || p isa AbstractDict || p isa MTKParameters ? + _params : ArrayInterface.restructure(p, _params)) else [] @@ -25,7 +40,7 @@ function modelingtoolkitize(prob::DiffEqBase.ODEProblem; kwargs...) var_set = Set(vars) - D = Differential(t) + D = D_nounits mm = prob.f.mass_matrix if mm === I @@ -70,6 +85,8 @@ function modelingtoolkitize(prob::DiffEqBase.ODEProblem; kwargs...) default_p = if has_p if prob.p isa AbstractDict Dict(v => prob.p[k] for (k, v) in pairs(_params)) + elseif prob.p isa MTKParameters + Dict(params .=> reduce(vcat, prob.p)) else Dict(params .=> vec(collect(prob.p))) end @@ -125,44 +142,96 @@ function Base.showerror(io::IO, e::ModelingtoolkitizeParametersNotSupportedError println(io, e.type) end -function define_params(p) +function varnames_length_check(vars, names; is_unknowns = false) + if length(names) != length(vars) + throw(ArgumentError(""" + Number of $(is_unknowns ? "unknowns" : "parameters") ($(length(vars))) \ + does not match number of names ($(length(names))). + """)) + end +end + +function define_params(p, _ = nothing) throw(ModelingtoolkitizeParametersNotSupportedError(typeof(p))) end -function define_params(p::AbstractArray) - [toparam(variable(:α, i)) for i in eachindex(p)] +function define_params(p::AbstractArray, names = nothing) + if names === nothing + [toparam(variable(:α, i)) for i in eachindex(p)] + else + varnames_length_check(p, names) + [toparam(variable(names[i])) for i in eachindex(p)] + end end -function define_params(p::Number) - [toparam(variable(:α))] +function define_params(p::Number, names = nothing) + if names === nothing + [toparam(variable(:α))] + elseif names isa Union{AbstractArray, AbstractDict} + varnames_length_check(p, names) + [toparam(variable(names[i])) for i in eachindex(p)] + else + [toparam(variable(names))] + end end -function define_params(p::AbstractDict) - OrderedDict(k => toparam(variable(:α, i)) for (i, k) in zip(1:length(p), keys(p))) +function define_params(p::AbstractDict, names = nothing) + if names === nothing + OrderedDict(k => toparam(variable(:α, i)) for (i, k) in zip(1:length(p), keys(p))) + else + varnames_length_check(p, names) + OrderedDict(k => toparam(variable(names[k])) for k in keys(p)) + end end -function define_params(p::Union{SLArray, LArray}) - [toparam(variable(x)) for x in LabelledArrays.symnames(typeof(p))] +function define_params(p::Union{SLArray, LArray}, names = nothing) + if names === nothing + [toparam(variable(x)) for x in LabelledArrays.symnames(typeof(p))] + else + varnames_length_check(p, names) + [toparam(variable(names[i])) for i in eachindex(p)] + end end -function define_params(p::Tuple) - tuple((toparam(variable(:α, i)) for i in eachindex(p))...) +function define_params(p::Tuple, names = nothing) + if names === nothing + tuple((toparam(variable(:α, i)) for i in eachindex(p))...) + else + varnames_length_check(p, names) + tuple((toparam(variable(names[i])) for i in eachindex(p))...) + end end -function define_params(p::NamedTuple) - NamedTuple(x => toparam(variable(x)) for x in keys(p)) +function define_params(p::NamedTuple, names = nothing) + if names === nothing + NamedTuple(x => toparam(variable(x)) for x in keys(p)) + else + varnames_length_check(p, names) + NamedTuple(x => toparam(variable(names[x])) for x in keys(p)) + end end -function define_params(p::MTKParameters) - bufs = (p...,) - i = 1 - ps = [] - for buf in bufs - for _ in buf - push!(ps, toparam(variable(:α, i))) +function define_params(p::MTKParameters, names = nothing) + if names === nothing + bufs = (p...,) + i = 1 + ps = [] + for buf in bufs + for _ in buf + push!( + ps, + if names === nothing + toparam(variable(:α, i)) + else + toparam(variable(names[i])) + end + ) + end end + return identity.(ps) + else + return collect(values(names)) end - return identity.(ps) end """ @@ -173,7 +242,7 @@ Generate `SDESystem`, dependent variables, and parameters from an `SDEProblem`. function modelingtoolkitize(prob::DiffEqBase.SDEProblem; kwargs...) prob.f isa DiffEqBase.AbstractParameterizedFunction && return (prob.f.sys, prob.f.sys.unknowns, prob.f.sys.ps) - @parameters t + @independent_variables t p = prob.p has_p = !(p isa Union{DiffEqBase.NullParameters, Nothing}) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 34773524c9..baa82d9b6f 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -10,10 +10,10 @@ $(FIELDS) ```julia using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D @parameters σ ρ β -@variables t x(t) y(t) z(t) -D = Differential(t) +@variables x(t) y(t) z(t) eqs = [D(x) ~ σ*(y-x), D(y) ~ x*(ρ-z)-y, @@ -184,6 +184,7 @@ struct ODESystem <: AbstractODESystem discrete_subsystems = nothing, solved_unknowns = nothing, split_idxs = 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) @@ -247,6 +248,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; var_guesses = dvs′[hasaguess] .=> sysguesses[hasaguess] sysguesses = isempty(var_guesses) ? Dict() : todict(var_guesses) guesses = merge(sysguesses, todict(guesses)) + guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses)) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) @@ -314,7 +316,7 @@ function ODESystem(eqs, iv; kwargs...) end new_ps = OrderedSet() for p in ps - if istree(p) && operation(p) === getindex + 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)) @@ -354,11 +356,14 @@ function flatten(sys::ODESystem, noeqs = false) get_iv(sys), unknowns(sys), parameters(sys), + 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), + initialization_eqs = initialization_equations(sys), checks = false) end end @@ -374,10 +379,13 @@ i.e. there are no cycles. function build_explicit_observed_function(sys, ts; inputs = nothing, expression = false, + eval_expression = false, + eval_module = @__MODULE__, output_type = Array, checkbounds = true, drop_expr = drop_expr, ps = full_parameters(sys), + return_inplace = false, op = Operator, throw = true) if (isscalar = symbolic_type(ts) !== NotSymbolic()) @@ -400,16 +408,16 @@ function build_explicit_observed_function(sys, ts; sts = Set(unknowns(sys)) sts = union(sts, - Set(arguments(st)[1] for st in sts if istree(st) && operation(st) === getindex)) + Set(arguments(st)[1] for st in sts if iscall(st) && operation(st) === getindex)) observed_idx = Dict(x.lhs => i for (i, x) in enumerate(obs)) - param_set = Set(parameters(sys)) + param_set = Set(full_parameters(sys)) param_set = union(param_set, - Set(arguments(p)[1] for p in param_set if istree(p) && operation(p) === getindex)) - param_set_ns = Set(unknowns(sys, p) for p in parameters(sys)) + Set(arguments(p)[1] for p in param_set if iscall(p) && operation(p) === getindex)) + param_set_ns = Set(unknowns(sys, p) for p in full_parameters(sys)) param_set_ns = union(param_set_ns, Set(arguments(p)[1] - for p in param_set_ns if istree(p) && operation(p) === getindex)) + for p in param_set_ns if iscall(p) && operation(p) === getindex)) namespaced_to_obs = Dict(unknowns(sys, x.lhs) => x.lhs for x in obs) namespaced_to_sts = Dict(unknowns(sys, x) => x for x in unknowns(sys)) @@ -418,7 +426,10 @@ function build_explicit_observed_function(sys, ts; subs = Dict() maxidx = 0 for s in dep_vars - if s in param_set || s in param_set_ns + if s in param_set || s in param_set_ns || + iscall(s) && + operation(s) === getindex && + (arguments(s)[1] in param_set || arguments(s)[1] in param_set_ns) continue end idx = get(observed_idx, s, nothing) @@ -465,6 +476,9 @@ function build_explicit_observed_function(sys, ts; ps = DestructuredArgs.(ps, inbounds = !checkbounds) elseif has_index_cache(sys) && get_index_cache(sys) !== nothing ps = DestructuredArgs.(reorder_parameters(get_index_cache(sys), ps)) + if isempty(ps) && inputs !== nothing + ps = (:EMPTY,) + end else ps = (DestructuredArgs(ps, inbounds = !checkbounds),) end @@ -472,16 +486,35 @@ function build_explicit_observed_function(sys, ts; if inputs === nothing args = [dvs, ps..., ivs...] else + inputs = unwrap.(inputs) ipts = DestructuredArgs(inputs, inbounds = !checkbounds) args = [dvs, ipts, ps..., ivs...] end pre = get_postprocess_fbody(sys) - ex = Func(args, [], - pre(Let(obsexprs, - isscalar ? ts[1] : MakeArray(ts, output_type), - false))) |> wrap_array_vars(sys, ts)[1] |> toexpr - expression ? ex : drop_expr(@RuntimeGeneratedFunction(ex)) + # Need to keep old method of building the function since it uses `output_type`, + # which can't be provided to `build_function` + oop_fn = Func(args, [], + pre(Let(obsexprs, + isscalar ? ts[1] : MakeArray(ts, output_type), + false))) |> wrap_array_vars(sys, ts)[1] |> toexpr + oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) + + if !isscalar + iip_fn = build_function(ts, + args...; + postprocess_fbody = pre, + wrap_code = wrap_array_vars(sys, ts) .∘ wrap_assignments(isscalar, obsexprs), + expression = Val{true})[2] + if !expression + iip_fn = eval_or_rgf(iip_fn; eval_expression, eval_module) + end + end + if isscalar || !return_inplace + return oop_fn + else + return oop_fn, iip_fn + end end function _eq_unordered(a, b) @@ -514,7 +547,7 @@ function convert_system(::Type{<:ODESystem}, sys, t; name = nameof(sys)) sts = unknowns(sys) newsts = similar(sts, Any) for (i, s) in enumerate(sts) - if istree(s) + 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)`.")) @@ -523,7 +556,8 @@ function convert_system(::Type{<:ODESystem}, sys, t; name = nameof(sys)) newsts[i] = s continue end - ns = similarterm(s, operation(s), Any[t]; metadata = SymbolicUtils.metadata(s)) + ns = maketerm(typeof(s), operation(s), Any[t], + SymbolicUtils.symtype(s), SymbolicUtils.metadata(s)) newsts[i] = ns varmap[s] = ns else diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index b021a201fe..86237ac51c 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -10,10 +10,10 @@ $(FIELDS) ```julia using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D @parameters σ ρ β -@variables t x(t) y(t) z(t) -D = Differential(t) +@variables x(t) y(t) z(t) eqs = [D(x) ~ σ*(y-x), D(y) ~ x*(ρ-z)-y, @@ -137,9 +137,14 @@ struct SDESystem <: AbstractODESystem complete = false, index_cache = 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(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) end if checks == true || (checks & CheckUnits) > 0 @@ -231,6 +236,9 @@ function generate_diffusion_function(sys::SDESystem, dvs = unknowns(sys), if isdde eqs = delay_to_function(sys, eqs) end + if eqs isa AbstractMatrix && isdiag(eqs) + eqs = diag(eqs) + end u = map(x -> time_varying_as_func(value(x), sys), dvs) p = if has_index_cache(sys) && get_index_cache(sys) !== nothing reorder_parameters(get_index_cache(sys), ps) @@ -291,7 +299,7 @@ function stochastic_integral_transform(sys::SDESystem, correction_factor) end SDESystem(deqs, get_noiseeqs(sys), get_iv(sys), unknowns(sys), parameters(sys), - name = name, parameter_dependencies = get_parameter_dependencies(sys), checks = false) + name = name, parameter_dependencies = parameter_dependencies(sys), checks = false) end """ @@ -313,10 +321,10 @@ experiments. Springer Science & Business Media. ```julia using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D @parameters α β -@variables t x(t) y(t) z(t) -D = Differential(t) +@variables x(t) y(t) z(t) eqs = [D(x) ~ α*x] noiseeqs = [β*x] @@ -399,29 +407,28 @@ function Girsanov_transform(sys::SDESystem, u; θ0 = 1.0) # return modified SDE System SDESystem(deqs, noiseeqs, get_iv(sys), unknown_vars, parameters(sys); defaults = Dict(θ => θ0), observed = [weight ~ θ / θ0], - name = name, parameter_dependencies = get_parameter_dependencies(sys), + name = name, parameter_dependencies = parameter_dependencies(sys), checks = false) end -function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys), +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 = true, + jac = false, Wfact = false, eval_expression = false, + eval_module = @__MODULE__, checkbounds = false, - kwargs...) where {iip} + 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{eval_expression}, kwargs...) - f_oop, f_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in f_gen) : f_gen - g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{eval_expression}, + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, 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}, kwargs...) - g_oop, g_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in g_gen) : g_gen + g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) f(u, p, t) = f_oop(u, p, t) f(u, p::MTKParameters, t) = f_oop(u, p..., t) @@ -433,11 +440,10 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys), g(du, u, p::MTKParameters, t) = g_iip(du, u, p..., t) if tgrad - tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{eval_expression}, + tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, kwargs...) - tgrad_oop, tgrad_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in tgrad_gen) : - tgrad_gen + tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) + _tgrad(u, p, t) = tgrad_oop(u, p, t) _tgrad(u, p::MTKParameters, t) = tgrad_oop(u, p..., t) _tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) @@ -447,11 +453,10 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys), end if jac - jac_gen = generate_jacobian(sys, dvs, ps; expression = Val{eval_expression}, + jac_gen = generate_jacobian(sys, dvs, ps; expression = Val{true}, sparse = sparse, kwargs...) - jac_oop, jac_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in jac_gen) : - jac_gen + jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) + _jac(u, p, t) = jac_oop(u, p, t) _jac(u, p::MTKParameters, t) = jac_oop(u, p..., t) _jac(J, u, p, t) = jac_iip(J, u, p, t) @@ -463,12 +468,9 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys), if Wfact tmp_Wfact, tmp_Wfact_t = generate_factorized_W(sys, dvs, ps, true; expression = Val{true}, kwargs...) - Wfact_oop, Wfact_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in tmp_Wfact) : - tmp_Wfact - Wfact_oop_t, Wfact_iip_t = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in tmp_Wfact_t) : - tmp_Wfact_t + 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(u, p, dtgamma, t) = Wfact_oop(u, p, dtgamma, t) _Wfact(u, p::MTKParameters, dtgamma, t) = Wfact_oop(u, p..., dtgamma, t) _Wfact(W, u, p, dtgamma, t) = Wfact_iip(W, u, p, dtgamma, t) @@ -484,21 +486,9 @@ function DiffEqBase.SDEFunction{iip}(sys::SDESystem, dvs = unknowns(sys), M = calculate_massmatrix(sys) _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) - obs = observed(sys) - observedfun = let sys = sys, dict = Dict() - function generated_observed(obsvar, u, p, t) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar; checkbounds = checkbounds) - end - if p isa MTKParameters - obs(u, p..., t) - else - obs(u, p, t) - end - end - end + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) - SDEFunction{iip}(f, g, + SDEFunction{iip, specialize}(f, g, sys = sys, jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, @@ -523,6 +513,16 @@ 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), @@ -601,16 +601,18 @@ function SDEFunctionExpr(sys::SDESystem, args...; kwargs...) SDEFunctionExpr{true}(sys, args...; kwargs...) end -function DiffEqBase.SDEProblem{iip}(sys::SDESystem, u0map = [], tspan = get_tspan(sys), +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} + 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_DEProblem(SDEFunction{iip}, sys, u0map, parammap; check_length, + f, u0, p = process_DEProblem( + SDEFunction{iip, specialize}, sys, u0map, parammap; check_length, kwargs...) - cbs = process_events(sys; callback) + cbs = process_events(sys; callback, kwargs...) sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) noiseeqs = get_noiseeqs(sys) @@ -646,6 +648,21 @@ 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, diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 4f32632c22..79c48af134 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -101,6 +101,7 @@ struct DiscreteSystem <: AbstractTimeDependentSystem complete = false, index_cache = 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) end @@ -173,7 +174,7 @@ function DiscreteSystem(eqs, iv; kwargs...) for eq in eqs collect_vars!(allunknowns, ps, eq.lhs, iv; op = Shift) collect_vars!(allunknowns, ps, eq.rhs, iv; op = Shift) - if istree(eq.lhs) && operation(eq.lhs) isa 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 && @@ -183,7 +184,7 @@ function DiscreteSystem(eqs, iv; kwargs...) end new_ps = OrderedSet() for p in ps - if istree(p) && operation(p) === getindex + 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)) @@ -199,6 +200,22 @@ function DiscreteSystem(eqs, iv; kwargs...) collect(allunknowns), collect(new_ps); kwargs...) end +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), + name = nameof(sys), + checks = false) + end +end + function generate_function( sys::DiscreteSystem, dvs = unknowns(sys), ps = full_parameters(sys); kwargs...) generate_custom_function(sys, [eq.rhs for eq in equations(sys)], dvs, ps; kwargs...) @@ -206,9 +223,9 @@ end function process_DiscreteProblem(constructor, sys::DiscreteSystem, u0map, parammap; linenumbers = true, parallel = SerialForm(), - eval_expression = true, use_union = false, tofloat = !use_union, + eval_expression = false, eval_module = @__MODULE__, kwargs...) iv = get_iv(sys) eqs = equations(sys) @@ -233,7 +250,7 @@ function process_DiscreteProblem(constructor, sys::DiscreteSystem, u0map, paramm end if has_index_cache(sys) && get_index_cache(sys) !== nothing u0, defs = get_u0(sys, trueu0map, parammap) - p = MTKParameters(sys, parammap, trueu0map) + p = MTKParameters(sys, parammap, trueu0map; eval_expression, eval_module) else u0, p, defs = get_u0_p(sys, trueu0map, parammap; tofloat, use_union) end @@ -243,7 +260,8 @@ function process_DiscreteProblem(constructor, sys::DiscreteSystem, u0map, paramm f = constructor(sys, dvs, ps, u0; linenumbers = linenumbers, parallel = parallel, syms = Symbol.(dvs), paramsyms = Symbol.(ps), - eval_expression = eval_expression, kwargs...) + eval_expression = eval_expression, eval_module = eval_module, + kwargs...) return f, u0, p end @@ -255,7 +273,7 @@ function SciMLBase.DiscreteProblem( sys::DiscreteSystem, u0map = [], tspan = get_tspan(sys), parammap = SciMLBase.NullParameters(); eval_module = @__MODULE__, - eval_expression = true, + eval_expression = false, use_union = false, kwargs... ) @@ -292,18 +310,16 @@ function SciMLBase.DiscreteFunction{iip, specialize}( version = nothing, p = nothing, t = nothing, - eval_expression = true, + eval_expression = false, eval_module = @__MODULE__, analytic = nothing, 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{eval_expression}, + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, expression_module = eval_module, kwargs...) - f_oop, f_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) for ex in f_gen) : - f_gen + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, p, t) = f_oop(u, p, t) f(du, u, p, t) = f_iip(du, u, p, t) @@ -314,14 +330,7 @@ function SciMLBase.DiscreteFunction{iip, specialize}( f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) end - observedfun = let sys = sys, dict = Dict() - function generate_observed(obsvar, u, p, t) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar) - end - p isa MTKParameters ? obs(u, p..., t) : obs(u, p, t) - end - end + observedfun = ObservedFunctionCache(sys) DiscreteFunction{iip, specialize}(f; sys = sys, diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 5b0e87edff..75c8a7e235 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -1,5 +1,5 @@ struct BufferTemplate - type::DataType + type::Union{DataType, UnionAll} length::Int end @@ -8,16 +8,22 @@ function BufferTemplate(s::Type{<:Symbolics.Struct}, length::Int) BufferTemplate(T, length) end -const DEPENDENT_PORTION = :dependent -const NONNUMERIC_PORTION = :nonnumeric +struct Dependent <: SciMLStructures.AbstractPortion end +struct Nonnumeric <: SciMLStructures.AbstractPortion end +const DEPENDENT_PORTION = Dependent() +const NONNUMERIC_PORTION = Nonnumeric() struct ParameterIndex{P, I} portion::P idx::I + validate_size::Bool end +ParameterIndex(portion, idx) = ParameterIndex(portion, idx, false) + const ParamIndexMap = Dict{Union{Symbol, BasicSymbolic}, Tuple{Int, Int}} -const UnknownIndexMap = Dict{Union{Symbol, BasicSymbolic}, Union{Int, UnitRange{Int}}} +const UnknownIndexMap = Dict{ + Union{Symbol, BasicSymbolic}, Union{Int, UnitRange{Int}, AbstractArray{Int}}} struct IndexCache unknown_idx::UnknownIndexMap @@ -31,26 +37,53 @@ struct IndexCache constant_buffer_sizes::Vector{BufferTemplate} dependent_buffer_sizes::Vector{BufferTemplate} nonnumeric_buffer_sizes::Vector{BufferTemplate} + symbol_to_variable::Dict{Symbol, BasicSymbolic} end function IndexCache(sys::AbstractSystem) unks = solved_unknowns(sys) unk_idxs = UnknownIndexMap() + symbol_to_variable = Dict{Symbol, BasicSymbolic}() + let idx = 1 for sym in unks usym = unwrap(sym) sym_idx = if Symbolics.isarraysymbolic(sym) - idx:(idx + length(sym) - 1) + reshape(idx:(idx + length(sym) - 1), size(sym)) else idx end unk_idxs[usym] = sym_idx - - if hasname(sym) - unk_idxs[getname(usym)] = sym_idx + if hasname(sym) && (!iscall(sym) || operation(sym) !== getindex) + name = getname(usym) + unk_idxs[name] = sym_idx + symbol_to_variable[name] = sym end idx += length(sym) end + for sym in unks + usym = unwrap(sym) + iscall(sym) && operation(sym) === getindex || continue + arrsym = arguments(sym)[1] + all(haskey(unk_idxs, arrsym[i]) for i in eachindex(arrsym)) || continue + + idxs = [unk_idxs[arrsym[i]] for i in eachindex(arrsym)] + if idxs == idxs[begin]:idxs[end] + idxs = reshape(idxs[begin]:idxs[end], size(idxs)) + end + unk_idxs[arrsym] = idxs + if hasname(arrsym) + name = getname(arrsym) + unk_idxs[name] = idxs + symbol_to_variable[name] = arrsym + end + end + end + + for eq in observed(sys) + if symbolic_type(eq.lhs) != NotSymbolic() && hasname(eq.lhs) + symbol_to_variable[getname(eq.lhs)] = eq.lhs + end end disc_buffers = Dict{Any, Set{BasicSymbolic}}() @@ -61,7 +94,7 @@ function IndexCache(sys::AbstractSystem) function insert_by_type!(buffers::Dict{Any, Set{BasicSymbolic}}, sym) sym = unwrap(sym) - ctype = concrete_symtype(sym) + ctype = symtype(sym) buf = get!(buffers, ctype, Set{BasicSymbolic}()) push!(buf, sym) end @@ -88,8 +121,8 @@ function IndexCache(sys::AbstractSystem) end end - if has_parameter_dependencies(sys) && - (pdeps = get_parameter_dependencies(sys)) !== nothing + if has_parameter_dependencies(sys) + pdeps = parameter_dependencies(sys) for (sym, value) in pdeps sym = unwrap(sym) insert_by_type!(dependent_buffers, sym) @@ -98,7 +131,7 @@ function IndexCache(sys::AbstractSystem) for p in parameters(sys) p = unwrap(p) - ctype = concrete_symtype(p) + ctype = symtype(p) haskey(disc_buffers, ctype) && p in disc_buffers[ctype] && continue haskey(dependent_buffers, ctype) && p in dependent_buffers[ctype] && continue insert_by_type!( @@ -124,16 +157,17 @@ function IndexCache(sys::AbstractSystem) for (j, p) in enumerate(buf) idxs[p] = (i, j) idxs[default_toterm(p)] = (i, j) - if hasname(p) + if hasname(p) && (!iscall(p) || operation(p) !== getindex) idxs[getname(p)] = (i, j) + symbol_to_variable[getname(p)] = p idxs[getname(default_toterm(p))] = (i, j) + symbol_to_variable[getname(default_toterm(p))] = p end end push!(buffer_sizes, BufferTemplate(T, length(buf))) end return idxs, buffer_sizes end - disc_idxs, discrete_buffer_sizes = get_buffer_sizes_and_idxs(disc_buffers) tunable_idxs, tunable_buffer_sizes = get_buffer_sizes_and_idxs(tunable_buffers) const_idxs, const_buffer_sizes = get_buffer_sizes_and_idxs(constant_buffers) @@ -151,7 +185,8 @@ function IndexCache(sys::AbstractSystem) tunable_buffer_sizes, const_buffer_sizes, dependent_buffer_sizes, - nonnumeric_buffer_sizes + nonnumeric_buffer_sizes, + symbol_to_variable ) end @@ -172,16 +207,21 @@ function SymbolicIndexingInterface.is_parameter(ic::IndexCache, sym) end function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) + if sym isa Symbol + sym = ic.symbol_to_variable[sym] + end + validate_size = Symbolics.isarraysymbolic(sym) && + Symbolics.shape(sym) !== Symbolics.Unknown() return if (idx = check_index_map(ic.tunable_idx, sym)) !== nothing - ParameterIndex(SciMLStructures.Tunable(), idx) + ParameterIndex(SciMLStructures.Tunable(), idx, validate_size) elseif (idx = check_index_map(ic.discrete_idx, sym)) !== nothing - ParameterIndex(SciMLStructures.Discrete(), idx) + ParameterIndex(SciMLStructures.Discrete(), idx, validate_size) elseif (idx = check_index_map(ic.constant_idx, sym)) !== nothing - ParameterIndex(SciMLStructures.Constants(), idx) + ParameterIndex(SciMLStructures.Constants(), idx, validate_size) elseif (idx = check_index_map(ic.nonnumeric_idx, sym)) !== nothing - ParameterIndex(NONNUMERIC_PORTION, idx) + ParameterIndex(NONNUMERIC_PORTION, idx, validate_size) elseif (idx = check_index_map(ic.dependent_idx, sym)) !== nothing - ParameterIndex(DEPENDENT_PORTION, idx) + ParameterIndex(DEPENDENT_PORTION, idx, validate_size) else nothing end @@ -190,7 +230,7 @@ end function check_index_map(idxmap, sym) if (idx = get(idxmap, sym, nothing)) !== nothing return idx - elseif !isa(sym, Symbol) && (!istree(sym) || operation(sym) !== getindex) && + elseif !isa(sym, Symbol) && (!iscall(sym) || operation(sym) !== getindex) && hasname(sym) && (idx = get(idxmap, getname(sym), nothing)) !== nothing return idx end @@ -198,7 +238,7 @@ function check_index_map(idxmap, sym) isequal(sym, dsym) && return nothing if (idx = get(idxmap, dsym, nothing)) !== nothing idx - elseif !isa(dsym, Symbol) && (!istree(dsym) || operation(dsym) !== getindex) && + elseif !isa(dsym, Symbol) && (!iscall(dsym) || operation(dsym) !== getindex) && hasname(dsym) && (idx = get(idxmap, getname(dsym), nothing)) !== nothing idx else @@ -206,26 +246,6 @@ function check_index_map(idxmap, sym) end end -function ParameterIndex(ic::IndexCache, p, sub_idx = ()) - p = unwrap(p) - return if haskey(ic.tunable_idx, p) - ParameterIndex(SciMLStructures.Tunable(), (ic.tunable_idx[p]..., sub_idx...)) - elseif haskey(ic.discrete_idx, p) - ParameterIndex(SciMLStructures.Discrete(), (ic.discrete_idx[p]..., sub_idx...)) - elseif haskey(ic.constant_idx, p) - ParameterIndex(SciMLStructures.Constants(), (ic.constant_idx[p]..., sub_idx...)) - elseif haskey(ic.dependent_idx, p) - ParameterIndex(DEPENDENT_PORTION, (ic.dependent_idx[p]..., sub_idx...)) - elseif haskey(ic.nonnumeric_idx, p) - ParameterIndex(NONNUMERIC_PORTION, (ic.nonnumeric_idx[p]..., sub_idx...)) - elseif istree(p) && operation(p) === getindex - _p, sub_idx... = arguments(p) - ParameterIndex(ic, _p, sub_idx) - else - nothing - end -end - function discrete_linear_index(ic::IndexCache, idx::ParameterIndex) idx.portion isa SciMLStructures.Discrete || error("Discrete variable index expected") ind = sum(temp.length for temp in ic.tunable_buffer_sizes; init = 0) @@ -294,9 +314,3 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false) end return result end - -concrete_symtype(x::BasicSymbolic) = concrete_symtype(symtype(x)) -concrete_symtype(::Type{Real}) = Float64 -concrete_symtype(::Type{Integer}) = Int -concrete_symtype(::Type{A}) where {T, N, A <: Array{T, N}} = Array{concrete_symtype(T), N} -concrete_symtype(::Type{T}) where {T} = T diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 4feb7a1da7..cdd5f1b2f8 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -34,9 +34,10 @@ $(FIELDS) ```julia using ModelingToolkit, JumpProcesses +using ModelingToolkit: t_nounits as t @parameters β γ -@variables t S(t) I(t) R(t) +@variables S(t) I(t) R(t) rate₁ = β*S*I affect₁ = [S ~ S - 1, I ~ I + 1] rate₂ = γ*I @@ -118,6 +119,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem complete = false, index_cache = nothing; 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) end @@ -213,15 +215,17 @@ function generate_affect_function(js::JumpSystem, affect, outputidxs) expression = Val{true}, checkvars = false) end -function assemble_vrj(js, vrj, unknowntoid) - _rate = drop_expr(@RuntimeGeneratedFunction(generate_rate_function(js, vrj.rate))) +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(u, p, t) = _rate(u, p, t) rate(u, p::MTKParameters, t) = _rate(u, p..., t) outputvars = (value(affect.lhs) for affect in vrj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = drop_expr(@RuntimeGeneratedFunction(generate_affect_function(js, vrj.affect!, - outputidxs))) + affect = eval_or_rgf( + generate_affect_function(js, vrj.affect!, + outputidxs); eval_expression, eval_module) VariableRateJump(rate, affect) end @@ -240,15 +244,17 @@ function assemble_vrj_expr(js, vrj, unknowntoid) end end -function assemble_crj(js, crj, unknowntoid) - _rate = drop_expr(@RuntimeGeneratedFunction(generate_rate_function(js, crj.rate))) +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(u, p, t) = _rate(u, p, t) rate(u, p::MTKParameters, t) = _rate(u, p..., t) outputvars = (value(affect.lhs) for affect in crj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = drop_expr(@RuntimeGeneratedFunction(generate_affect_function(js, crj.affect!, - outputidxs))) + affect = eval_or_rgf( + generate_affect_function(js, crj.affect!, + outputidxs); eval_expression, eval_module) ConstantRateJump(rate, affect) end @@ -271,7 +277,7 @@ 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 !istree(spec) && _iszero(spec) + if !iscall(spec) && _iszero(spec) push!(rs, 0 => stoich) else push!(rs, unknowntoid[spec] => stoich) @@ -285,7 +291,7 @@ function numericnstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} ns = Vector{Pair{Int, W}}() for (wspec, stoich) in mtrs spec = value(wspec) - !istree(spec) && _iszero(spec) && + !iscall(spec) && _iszero(spec) && error("Net stoichiometry can not have a species labelled 0.") push!(ns, unknowntoid[spec] => stoich) end @@ -325,6 +331,8 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, parammap = DiffEqBase.NullParameters(); checkbounds = false, use_union = true, + eval_expression = false, + eval_module = @__MODULE__, kwargs...) if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") @@ -338,23 +346,14 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, 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) + p = MTKParameters(sys, parammap, u0map; eval_expression, eval_module) else p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) end f = DiffEqBase.DISCRETE_INPLACE_DEFAULT - # just taken from abstractodesystem.jl for ODEFunction def - obs = observed(sys) - observedfun = let sys = sys, dict = Dict() - function generated_observed(obsvar, u, p, t) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar; checkbounds = checkbounds) - end - p isa MTKParameters ? obs(u, p..., t) : obs(u, p, t) - end - end + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun) DiscreteProblem(df, u0, tspan, p; kwargs...) @@ -426,7 +425,7 @@ sol = solve(jprob, SSAStepper()) ``` """ function JumpProcesses.JumpProblem(js::JumpSystem, prob, aggregator; callback = nothing, - kwargs...) + 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 @@ -439,8 +438,10 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, aggregator; callback = 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) for j in eqs.x[2]] - vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid) for j in eqs.x[3]] + 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]] ((prob isa DiscreteProblem) && !isempty(vrjs)) && error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps") jset = JumpSet(Tuple(vrjs), Tuple(crjs), nothing, majs) @@ -459,7 +460,8 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, aggregator; callback = end # handle events, making sure to reset aggregators in the generated affect functions - cbs = process_events(js; callback, postprocess_affect_expr! = _reset_aggregator!) + 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, diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 602f06bc92..d79282ae27 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -50,7 +50,7 @@ function _model_macro(mod, name, expr, isconnector) ps, sps, vs, = [], [], [] c_evts = [] d_evts = [] - kwargs = Set() + kwargs = OrderedCollections.OrderedSet() where_types = Expr[] push!(exprs.args, :(variables = [])) @@ -92,7 +92,7 @@ function _model_macro(mod, name, expr, isconnector) iv = get(dict, :independent_variable, nothing) if iv === nothing - iv = dict[:independent_variable] = variable(:t) + iv = dict[:independent_variable] = get_t(mod, :t) end push!(exprs.args, :(push!(equations, $(eqs...)))) @@ -187,7 +187,7 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; var = generate_var!(dict, a, varclass; indices, type) update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, varclass, where_types) - (var, def) + return var, def, Dict() end Expr(:(::), a, type) => begin type = getfield(mod, type) @@ -199,15 +199,15 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; def, type) end Expr(:call, a, b) => begin - var = generate_var!(dict, a, b, varclass; indices, type) + var = generate_var!(dict, a, b, varclass, mod; indices, type) update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, varclass, where_types) - (var, def) + return var, def, Dict() end Expr(:(=), a, b) => begin Base.remove_linenums!(b) def, meta = parse_default(mod, b) - var, def = parse_variable_def!( + var, def, _ = parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; def, type) if dict[varclass] isa Vector dict[varclass][1][getname(var)][:default] = def @@ -225,12 +225,13 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end end end - var = set_var_metadata(var, meta) + var, metadata_with_exprs = set_var_metadata(var, meta) + return var, def, metadata_with_exprs end - (var, def) + return var, def, Dict() end Expr(:tuple, a, b) => begin - var, def = parse_variable_def!( + var, def, _ = parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; type) meta = parse_metadata(mod, b) if meta !== nothing @@ -244,9 +245,10 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end end end - var = set_var_metadata(var, meta) + var, metadata_with_exprs = set_var_metadata(var, meta) + return var, def, metadata_with_exprs end - (var, def) + return var, def, Dict() end Expr(:ref, a, b...) => begin indices = map(i -> UnitRange(i.args[2], i.args[end]), b) @@ -280,11 +282,10 @@ function generate_var!(dict, a, varclass; generate_var(a, varclass; indices, type) end -function generate_var!(dict, a, b, varclass; +function generate_var!(dict, a, b, varclass, mod; indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type = Real) - # (type isa Nothing && type = Real) - iv = generate_var(b, :variables) + iv = b == :t ? get_t(mod, b) : generate_var(b, :variables) prev_iv = get!(dict, :independent_variable) do iv end @@ -306,6 +307,20 @@ function generate_var!(dict, a, b, varclass; var end +# Use the `t` defined in the `mod`. When it is unavailable, generate a new `t` with a warning. +function get_t(mod, t) + try + get_var(mod, t) + catch e + if e isa UndefVarError + @warn("Could not find a predefined `t` in `$mod`; generating a new one within this model.\nConsider defining it or importing `t` (or `t_nounits`, `t_unitful` as `t`) from ModelingToolkit.") + variable(:t) + else + throw(e) + end + end +end + function parse_default(mod, a) a = Base.remove_linenums!(deepcopy(a)) MLStyle.@match a begin @@ -337,21 +352,33 @@ function parse_metadata(mod, a) end end +function _set_var_metadata!(metadata_with_exprs, a, m, v::Expr) + push!(metadata_with_exprs, m => v) + a +end +function _set_var_metadata!(metadata_with_exprs, a, m, v) + wrap(set_scalar_metadata(unwrap(a), m, v)) +end + function set_var_metadata(a, ms) + metadata_with_exprs = Dict{DataType, Expr}() for (m, v) in ms - a = wrap(set_scalar_metadata(unwrap(a), m, v)) + if m == VariableGuess && v isa Symbol + v = quote + $v + end + end + a = _set_var_metadata!(metadata_with_exprs, a, m, v) end - a + a, metadata_with_exprs end function get_var(mod::Module, b) if b isa Symbol - getproperty(mod, b) - elseif b isa Expr - Core.eval(mod, b) - else - b + isdefined(mod, b) && return getproperty(mod, b) + isdefined(@__MODULE__, b) && return getproperty(@__MODULE__, b) end + b end function parse_model!(exprs, comps, ext, eqs, icon, vs, ps, sps, c_evts, d_evts, @@ -393,8 +420,9 @@ function parse_constants!(exprs, dict, body, mod) Expr(:(=), Expr(:(::), a, type), Expr(:tuple, b, metadata)) || Expr(:(=), Expr(:(::), a, type), b) => begin type = getfield(mod, type) b = _type_check!(get_var(mod, b), a, type, :constants) - constant = first(@constants $a::type = b) - push!(exprs, :($a = $constant)) + push!(exprs, + :($(Symbolics._parse_vars( + :constants, type, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict(:value => b, :type => type) if @isdefined metadata for data in metadata.args @@ -403,16 +431,18 @@ function parse_constants!(exprs, dict, body, mod) end end Expr(:(=), a, Expr(:tuple, b, metadata)) => begin - constant = first(@constants $a = b) - push!(exprs, :($a = $constant)) + push!(exprs, + :($(Symbolics._parse_vars( + :constants, Real, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict{Symbol, Any}(:value => get_var(mod, b)) for data in metadata.args dict[:constants][a][data.args[1]] = data.args[2] end end Expr(:(=), a, b) => begin - constant = first(@constants $a = b) - push!(exprs, :($a = $constant)) + push!(exprs, + :($(Symbolics._parse_vars( + :constants, Real, [:($a = $b)], toconstant)))) dict[:constants][a] = Dict(:value => get_var(mod, b)) end _ => error("""Malformed constant definition `$arg`. Please use the following syntax: @@ -579,10 +609,26 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) - vv, def = parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types) + vv, def, metadata_with_exprs = parse_variable_def!( + dict, mod, arg, varclass, kwargs, where_types) name = getname(vv) - return vv isa Num ? name : :($name...), - :($name = $name === nothing ? $setdefault($vv, $def) : $setdefault($vv, $name)) + + varexpr = quote + $name = if $name === nothing + $setdefault($vv, $def) + else + $setdefault($vv, $name) + end + end + + metadata_expr = Expr(:block) + for (k, v) in metadata_with_exprs + push!(metadata_expr.args, + :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) + end + + push!(varexpr.args, metadata_expr) + return vv isa Num ? name : :($name...), varexpr end function handle_conditional_vars!( diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index e05456be6a..e9f70c09e9 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -8,6 +8,8 @@ function generate_initializesystem(sys::ODESystem; name = nameof(sys), guesses = Dict(), check_defguess = false, default_dd_value = 0.0, + algebraic_only = false, + initialization_eqs = [], kwargs...) sts, eqs = unknowns(sys), equations(sys) idxs_diff = isdiffeq.(eqs) @@ -20,6 +22,9 @@ function generate_initializesystem(sys::ODESystem; eqs_diff = eqs[idxs_diff] diffmap = Dict(getfield.(eqs_diff, :lhs) .=> getfield.(eqs_diff, :rhs)) + observed_diffmap = Dict(Differential(get_iv(sys)).(getfield.((observed(sys)), :lhs)) .=> + Differential(get_iv(sys)).(getfield.((observed(sys)), :rhs))) + full_diffmap = merge(diffmap, observed_diffmap) full_states = unique([sts; getfield.((observed(sys)), :lhs)]) set_full_states = Set(full_states) @@ -27,7 +32,7 @@ function generate_initializesystem(sys::ODESystem; schedule = getfield(sys, :schedule) if schedule !== nothing - guessmap = [x[2] => get(guesses, x[1], default_dd_value) + guessmap = [x[1] => get(guesses, x[1], default_dd_value) for x in schedule.dummy_sub] dd_guess = Dict(filter(x -> !isnothing(x[1]), guessmap)) if u0map === nothing || isempty(u0map) @@ -36,7 +41,8 @@ function generate_initializesystem(sys::ODESystem; filtered_u0 = Pair[] for x in u0map y = get(schedule.dummy_sub, x[1], x[1]) - y = get(diffmap, y, y) + y = ModelingToolkit.fixpoint_sub(y, full_diffmap) + if y isa Symbolics.Arr _y = collect(y) @@ -58,7 +64,7 @@ function generate_initializesystem(sys::ODESystem; end else dd_guess = Dict() - filtered_u0 = u0map + filtered_u0 = todict(u0map) end defs = merge(defaults(sys), filtered_u0) @@ -85,12 +91,17 @@ function generate_initializesystem(sys::ODESystem; end pars = [parameters(sys); get_iv(sys)] - nleqs = [eqs_ics; get_initialization_eqs(sys); observed(sys)] + nleqs = if algebraic_only + [eqs_ics; observed(sys)] + else + [eqs_ics; get_initialization_eqs(sys); initialization_eqs; observed(sys)] + end sys_nl = NonlinearSystem(nleqs, full_states, pars; defaults = merge(ModelingToolkit.defaults(sys), todict(u0), dd_guess), + parameter_dependencies = parameter_dependencies(sys), name, kwargs...) diff --git a/src/systems/nonlinear/modelingtoolkitize.jl b/src/systems/nonlinear/modelingtoolkitize.jl index 843e260406..2f12157884 100644 --- a/src/systems/nonlinear/modelingtoolkitize.jl +++ b/src/systems/nonlinear/modelingtoolkitize.jl @@ -3,40 +3,78 @@ $(TYPEDSIGNATURES) Generate `NonlinearSystem`, dependent variables, and parameters from an `NonlinearProblem`. """ -function modelingtoolkitize(prob::NonlinearProblem; kwargs...) +function modelingtoolkitize( + prob::Union{NonlinearProblem, NonlinearLeastSquaresProblem}; + u_names = nothing, p_names = nothing, kwargs...) p = prob.p has_p = !(p isa Union{DiffEqBase.NullParameters, Nothing}) - _vars = reshape([variable(:x, i) for i in eachindex(prob.u0)], size(prob.u0)) + if u_names !== nothing + varnames_length_check(prob.u0, u_names; is_unknowns = true) + _vars = [variable(name) for name in u_names] + elseif SciMLBase.has_sys(prob.f) + varnames = getname.(variable_symbols(prob.f.sys)) + varidxs = variable_index.((prob.f.sys,), varnames) + invpermute!(varnames, varidxs) + _vars = [variable(name) for name in varnames] + else + _vars = [variable(:x, i) for i in eachindex(prob.u0)] + end + _vars = reshape(_vars, size(prob.u0)) vars = prob.u0 isa Number ? _vars : ArrayInterface.restructure(prob.u0, _vars) params = if has_p - _params = define_params(p) + if p_names === nothing && SciMLBase.has_sys(prob.f) + p_names = Dict(parameter_index(prob.f.sys, sym) => sym + for sym in parameter_symbols(prob.f.sys)) + end + _params = define_params(p, p_names) p isa Number ? _params[1] : - (p isa Tuple || p isa NamedTuple ? _params : + (p isa Tuple || p isa NamedTuple || p isa AbstractDict || p isa MTKParameters ? + _params : ArrayInterface.restructure(p, _params)) else [] end if DiffEqBase.isinplace(prob) - rhs = ArrayInterface.restructure(prob.u0, similar(vars, Num)) - prob.f(rhs, vars, params) + if prob isa NonlinearLeastSquaresProblem + rhs = ArrayInterface.restructure( + prob.f.resid_prototype, similar(prob.f.resid_prototype, Num)) + prob.f(rhs, vars, params) + eqs = vcat([0.0 ~ rhs[i] for i in 1:length(prob.f.resid_prototype)]...) + else + rhs = ArrayInterface.restructure(prob.u0, similar(vars, Num)) + prob.f(rhs, vars, params) + eqs = vcat([0.0 ~ rhs[i] for i in 1:length(rhs)]...) + end + else rhs = prob.f(vars, params) + out_def = prob.f(prob.u0, prob.p) + eqs = vcat([0.0 ~ rhs[i] for i in 1:length(out_def)]...) end - out_def = prob.f(prob.u0, prob.p) - eqs = vcat([0.0 ~ rhs[i] for i in 1:length(out_def)]...) sts = vec(collect(vars)) - + _params = params + params = values(params) params = if params isa Number || (params isa Array && ndims(params) == 0) [params[1]] else vec(collect(params)) end default_u0 = Dict(sts .=> vec(collect(prob.u0))) - default_p = has_p ? Dict(params .=> vec(collect(prob.p))) : Dict() + default_p = if has_p + if prob.p isa AbstractDict + Dict(v => prob.p[k] for (k, v) in pairs(_params)) + elseif prob.p isa MTKParameters + Dict(params .=> reduce(vcat, prob.p)) + else + Dict(params .=> vec(collect(prob.p))) + end + else + Dict() + end de = NonlinearSystem(eqs, sts, params, defaults = merge(default_u0, default_p); diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index cf9e88c686..43d4bc8cc5 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -57,6 +57,11 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem """ connector_type::Any """ + A mapping from dependent parameters to expressions describing how they are calculated from + other parameters. + """ + parameter_dependencies::Union{Nothing, Dict} + """ Metadata for the system, to be used by downstream packages. """ metadata::Any @@ -87,7 +92,7 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem function NonlinearSystem(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, systems, - defaults, connector_type, metadata = nothing, + defaults, connector_type, parameter_dependencies = nothing, metadata = nothing, gui_metadata = nothing, tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, parent = nothing; checks::Union{ @@ -97,8 +102,8 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem check_units(u, eqs) end new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, systems, defaults, - connector_type, metadata, gui_metadata, tearing_state, substitutions, complete, - index_cache, parent) + connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, + substitutions, complete, index_cache, parent) end end @@ -113,6 +118,7 @@ function NonlinearSystem(eqs, unknowns, ps; 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 = nothing, metadata = nothing, gui_metadata = nothing) continuous_events === nothing || isempty(continuous_events) || @@ -148,9 +154,37 @@ function NonlinearSystem(eqs, unknowns, ps; process_variables!(var_to_name, defaults, ps) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) + parameter_dependencies, ps = process_parameter_dependencies( + parameter_dependencies, ps) NonlinearSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), eqs, unknowns, ps, var_to_name, observed, jac, name, systems, defaults, - connector_type, metadata, gui_metadata, checks = checks) + 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.lhs, nothing) + collect_vars!(allunknowns, ps, eq.rhs, 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 NonlinearSystem(eqs, collect(allunknowns), collect(new_ps); kwargs...) end function calculate_jacobian(sys::NonlinearSystem; sparse = false, simplify = false) @@ -159,8 +193,12 @@ function calculate_jacobian(sys::NonlinearSystem; sparse = false, simplify = fal return cache[1] end - rhs = [eq.rhs for eq in equations(sys)] + # 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 @@ -181,7 +219,8 @@ function generate_jacobian( end function calculate_hessian(sys::NonlinearSystem; sparse = false, simplify = false) - rhs = [eq.rhs for eq in equations(sys)] + 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)] @@ -243,15 +282,15 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s ps = full_parameters(sys), u0 = nothing; version = nothing, jac = false, - eval_expression = true, + eval_expression = false, + eval_module = @__MODULE__, 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 `NonlinearFunction`") end - f_gen = generate_function(sys, dvs, ps; expression = Val{eval_expression}, kwargs...) - f_oop, f_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in f_gen) : f_gen + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, p) = f_oop(u, p) f(u, p::MTKParameters) = f_oop(u, p...) f(du, u, p) = f_iip(du, u, p) @@ -260,10 +299,8 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s if jac jac_gen = generate_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, - expression = Val{eval_expression}, kwargs...) - jac_oop, jac_iip = eval_expression ? - (drop_expr(@RuntimeGeneratedFunction(ex)) for ex in jac_gen) : - jac_gen + expression = Val{true}, kwargs...) + jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac(u, p) = jac_oop(u, p) _jac(u, p::MTKParameters) = jac_oop(u, p...) _jac(J, u, p) = jac_iip(J, u, p) @@ -272,18 +309,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s _jac = nothing end - observedfun = let sys = sys, dict = Dict() - function generated_observed(obsvar, u, p) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar) - end - if p isa MTKParameters - obs(u, p...) - else - obs(u, p) - end - end - end + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) NonlinearFunction{iip}(f, sys = sys, @@ -353,18 +379,18 @@ function process_NonlinearProblem(constructor, sys::NonlinearSystem, u0map, para checkbounds = false, sparse = false, simplify = false, linenumbers = true, parallel = SerialForm(), - eval_expression = true, + eval_expression = false, + eval_module = @__MODULE__, use_union = false, tofloat = !use_union, kwargs...) eqs = equations(sys) dvs = unknowns(sys) - ps = parameters(sys) - + ps = full_parameters(sys) if has_index_cache(sys) && get_index_cache(sys) !== nothing u0, defs = get_u0(sys, u0map, parammap) check_eqs_u0(eqs, dvs, u0; kwargs...) - p = MTKParameters(sys, parammap, u0map) + p = MTKParameters(sys, parammap, u0map; eval_expression, eval_module) else u0, p, defs = get_u0_p(sys, u0map, parammap; tofloat, use_union) check_eqs_u0(eqs, dvs, u0; kwargs...) @@ -372,7 +398,8 @@ function process_NonlinearProblem(constructor, sys::NonlinearSystem, u0map, para f = constructor(sys, dvs, ps, u0; jac = jac, checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, simplify = simplify, - sparse = sparse, eval_expression = eval_expression, kwargs...) + sparse = sparse, eval_expression = eval_expression, eval_module = eval_module, + kwargs...) return f, u0, p end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 32055e7e7c..7fde00e2f4 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -226,12 +226,15 @@ 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) +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 diff --git a/src/systems/optimization/modelingtoolkitize.jl b/src/systems/optimization/modelingtoolkitize.jl index 9c419e0b24..5e419093ad 100644 --- a/src/systems/optimization/modelingtoolkitize.jl +++ b/src/systems/optimization/modelingtoolkitize.jl @@ -3,25 +3,70 @@ $(TYPEDSIGNATURES) Generate `OptimizationSystem`, dependent variables, and parameters from an `OptimizationProblem`. """ -function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; kwargs...) +function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; + u_names = nothing, p_names = nothing, kwargs...) num_cons = isnothing(prob.lcons) ? 0 : length(prob.lcons) if prob.p isa Tuple || prob.p isa NamedTuple p = [x for x in prob.p] else p = prob.p end - - vars = ArrayInterface.restructure(prob.u0, - [variable(:x, i) for i in eachindex(prob.u0)]) - params = if p isa DiffEqBase.NullParameters - [] - elseif p isa MTKParameters - [variable(:α, i) for i in eachindex(vcat(p...))] + has_p = !(p isa Union{DiffEqBase.NullParameters, Nothing}) + if u_names !== nothing + varnames_length_check(prob.u0, u_names; is_unknowns = true) + _vars = [variable(name) for name in u_names] + elseif SciMLBase.has_sys(prob.f) + varnames = getname.(variable_symbols(prob.f.sys)) + varidxs = variable_index.((prob.f.sys,), varnames) + invpermute!(varnames, varidxs) + _vars = [variable(name) for name in varnames] + if prob.f.sys isa OptimizationSystem + for (i, sym) in enumerate(variable_symbols(prob.f.sys)) + if hasbounds(sym) + _vars[i] = Symbolics.setmetadata( + _vars[i], VariableBounds, getbounds(sym)) + end + end + end + else + _vars = [variable(:x, i) for i in eachindex(prob.u0)] + end + _vars = reshape(_vars, size(prob.u0)) + vars = ArrayInterface.restructure(prob.u0, _vars) + params = if has_p + if p_names === nothing && SciMLBase.has_sys(prob.f) + p_names = Dict(parameter_index(prob.f.sys, sym) => sym + for sym in parameter_symbols(prob.f.sys)) + end + if p isa MTKParameters + old_to_new = Dict() + for sym in parameter_symbols(prob) + idx = parameter_index(prob, sym) + old_to_new[unwrap(sym)] = unwrap(p_names[idx]) + end + order = reorder_parameters(prob.f.sys, full_parameters(prob.f.sys)) + for arr in order + for i in eachindex(arr) + arr[i] = old_to_new[arr[i]] + end + end + _params = order + else + _params = define_params(p, p_names) + end + p isa Number ? _params[1] : + (p isa Tuple || p isa NamedTuple || p isa AbstractDict || p isa MTKParameters ? + _params : + ArrayInterface.restructure(p, _params)) else - ArrayInterface.restructure(p, [variable(:α, i) for i in eachindex(p)]) + [] end - eqs = prob.f(vars, params) + if p isa MTKParameters + eqs = prob.f(vars, params...) + else + eqs = prob.f(vars, params) + end if DiffEqBase.isinplace(prob) && !isnothing(prob.f.cons) lhs = Array{Num}(undef, num_cons) @@ -58,10 +103,32 @@ function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; kwargs...) else cons = [] end + params = values(params) + params = if params isa Number || (params isa Array && ndims(params) == 0) + [params[1]] + elseif p isa MTKParameters + reduce(vcat, params) + else + vec(collect(params)) + end - de = OptimizationSystem(eqs, vec(vars), vec(toparam.(params)); + sts = vec(collect(vars)) + default_u0 = Dict(sts .=> vec(collect(prob.u0))) + default_p = if has_p + if prob.p isa AbstractDict + Dict(v => prob.p[k] for (k, v) in pairs(_params)) + elseif prob.p isa MTKParameters + Dict(params .=> reduce(vcat, prob.p)) + else + Dict(params .=> vec(collect(prob.p))) + end + else + Dict() + end + de = OptimizationSystem(eqs, sts, params; name = gensym(:MTKizedOpt), constraints = cons, + defaults = merge(default_u0, default_p), kwargs...) de end diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index faa324f4fd..3a9f8aef74 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -26,7 +26,7 @@ struct OptimizationSystem <: AbstractOptimizationSystem """Objective function of the system.""" op::Any """Unknown variables.""" - unknowns::Vector + unknowns::Array """Parameters.""" ps::Vector """Array variables.""" @@ -240,6 +240,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_j = false, cons_h = false, cons_sparse = false, checkbounds = false, linenumbers = true, parallel = SerialForm(), + eval_expression = false, eval_module = @__MODULE__, use_union = false, kwargs...) where {iip} if !iscomplete(sys) @@ -280,7 +281,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, if parammap isa MTKParameters p = parammap elseif has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) + p = MTKParameters(sys, parammap, u0map; eval_expression, eval_module) else p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) end @@ -292,9 +293,12 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, ub = nothing end - f = let _f = generate_function( - sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{false}) + f = let _f = eval_or_rgf( + generate_function( + sys, checkbounds = checkbounds, linenumbers = linenumbers, + expression = Val{true}); + eval_expression, + eval_module) __f(u, p) = _f(u, p) __f(u, p::MTKParameters) = _f(u, p...) __f @@ -302,10 +306,13 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, obj_expr = subs_constants(objective(sys)) if grad - _grad = let (grad_oop, grad_iip) = generate_gradient( - sys, checkbounds = checkbounds, - linenumbers = linenumbers, - parallel = parallel, expression = Val{false}) + _grad = let (grad_oop, grad_iip) = eval_or_rgf.( + generate_gradient( + sys, checkbounds = checkbounds, + linenumbers = linenumbers, + parallel = parallel, expression = Val{true}); + 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...) @@ -317,10 +324,14 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, end if hess - _hess = let (hess_oop, hess_iip) = generate_hessian(sys, checkbounds = checkbounds, - linenumbers = linenumbers, - sparse = sparse, parallel = parallel, - expression = Val{false}) + _hess = let (hess_oop, hess_iip) = eval_or_rgf.( + generate_hessian( + sys, checkbounds = checkbounds, + linenumbers = linenumbers, + sparse = sparse, parallel = parallel, + expression = Val{true}); + 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...) @@ -337,40 +348,24 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, hess_prototype = nothing end - observedfun = let sys = sys, dict = Dict() - function generated_observed(obsvar, args...) - obs = get!(dict, value(obsvar)) do - build_explicit_observed_function(sys, obsvar) - end - if args === () - let obs = obs - _obs(u, p) = obs(u, p) - _obs(u, p::MTKParameters) = obs(u, p...) - _obs - end - else - u, p = args - if p isa MTKParameters - obs(u, p...) - else - obs(u, p) - end - end - end - end + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) if length(cstr) > 0 @named cons_sys = ConstraintsSystem(cstr, dvs, ps) cons_sys = complete(cons_sys) cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{false}) + expression = Val{true}) + cons = eval_or_rgf.(cons; eval_expression, eval_module) if cons_j - _cons_j = let (cons_jac_oop, cons_jac_iip) = generate_jacobian(cons_sys; - checkbounds = checkbounds, - linenumbers = linenumbers, - parallel = parallel, expression = Val{false}, - sparse = cons_sparse) + _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); + 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...) @@ -381,11 +376,14 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, _cons_j = nothing end if cons_h - _cons_h = let (cons_hess_oop, cons_hess_iip) = generate_hessian( - cons_sys, checkbounds = checkbounds, - linenumbers = linenumbers, - sparse = cons_sparse, parallel = parallel, - expression = Val{false}) + _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}); + 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...) @@ -478,6 +476,7 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, cons_j = false, cons_h = false, checkbounds = false, linenumbers = false, parallel = SerialForm(), + eval_expression = false, eval_module = @__MODULE__, use_union = false, kwargs...) where {iip} if !iscomplete(sys) @@ -516,7 +515,7 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, 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) + p = MTKParameters(sys, parammap, u0map; eval_expression, eval_module) else p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) end @@ -532,17 +531,23 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, f = generate_function(sys, checkbounds = checkbounds, linenumbers = linenumbers, expression = Val{true}) if grad - _grad = generate_gradient( - sys, checkbounds = checkbounds, linenumbers = linenumbers, - parallel = parallel, expression = Val{false})[idx] + _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 = generate_hessian(sys, checkbounds = checkbounds, linenumbers = linenumbers, - sparse = sparse, parallel = parallel, - expression = Val{false})[idx] + _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 @@ -566,14 +571,19 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, @named cons_sys = ConstraintsSystem(cstr, dvs, ps) cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{false}) + expression = Val{true}) + cons = eval_or_rgf(cons; eval_expression, eval_module) if cons_j - _cons_j = generate_jacobian(cons_sys; expression = Val{false}, sparse = sparse)[2] + _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 = generate_hessian(cons_sys; expression = Val{false}, sparse = sparse)[2] + _cons_h = eval_or_rgf( + generate_hessian(cons_sys; expression = Val{true}, sparse = sparse)[2]; + eval_expression, eval_module) else _cons_h = nothing end diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 7a37fac5c6..e491344fcd 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -1,5 +1,8 @@ symconvert(::Type{Symbolics.Struct{T}}, x) where {T} = convert(T, x) symconvert(::Type{T}, x) where {T} = convert(T, x) +symconvert(::Type{Real}, x::Integer) = convert(Float64, x) +symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) + struct MTKParameters{T, D, C, E, N, F, G} tunable::T discrete::D @@ -11,7 +14,8 @@ struct MTKParameters{T, D, C, E, N, F, G} end function MTKParameters( - sys::AbstractSystem, p, u0 = Dict(); tofloat = false, use_union = false) + sys::AbstractSystem, p, u0 = Dict(); tofloat = false, use_union = false, + eval_expression = false, eval_module = @__MODULE__) ic = if has_index_cache(sys) && get_index_cache(sys) !== nothing get_index_cache(sys) else @@ -28,9 +32,7 @@ function MTKParameters( p = Dict() end p = todict(p) - defs = Dict(default_toterm(unwrap(k)) => v - for (k, v) in defaults(sys) - if unwrap(k) in all_ps || default_toterm(unwrap(k)) in all_ps) + defs = Dict(default_toterm(unwrap(k)) => v for (k, v) in defaults(sys)) if eltype(u0) <: Pair u0 = todict(u0) elseif u0 isa AbstractArray && !isempty(u0) @@ -40,13 +42,57 @@ function MTKParameters( end defs = merge(defs, u0) defs = merge(Dict(eq.lhs => eq.rhs for eq in observed(sys)), defs) - p = merge(defs, p) - p = merge(Dict(unwrap(k) => v for (k, v) in p), - Dict(default_toterm(unwrap(k)) => v for (k, v) in p)) - p = Dict(k => fixpoint_sub(v, p) - for (k, v) in p if k in all_ps || default_toterm(k) in all_ps) + bigdefs = merge(defs, p) + p = Dict() + missing_params = Set() + pdeps = has_parameter_dependencies(sys) ? parameter_dependencies(sys) : nothing + + for sym in all_ps + ttsym = default_toterm(sym) + isarr = iscall(sym) && operation(sym) === getindex + arrparent = isarr ? arguments(sym)[1] : nothing + ttarrparent = isarr ? default_toterm(arrparent) : nothing + pname = hasname(sym) ? getname(sym) : nothing + ttpname = hasname(ttsym) ? getname(ttsym) : nothing + p[sym] = p[ttsym] = if haskey(bigdefs, sym) + bigdefs[sym] + elseif haskey(bigdefs, ttsym) + bigdefs[ttsym] + elseif haskey(bigdefs, pname) + isarr ? bigdefs[pname][arguments(sym)[2:end]...] : bigdefs[pname] + elseif haskey(bigdefs, ttpname) + isarr ? bigdefs[ttpname][arguments(sym)[2:end]...] : bigdefs[pname] + elseif isarr && haskey(bigdefs, arrparent) + bigdefs[arrparent][arguments(sym)[2:end]...] + elseif isarr && haskey(bigdefs, ttarrparent) + bigdefs[ttarrparent][arguments(sym)[2:end]...] + end + if get(p, sym, nothing) === nothing + push!(missing_params, sym) + continue + end + # We may encounter the `ttsym` version first, add it to `missing_params` + # then encounter the "normal" version of a parameter or vice versa + # Remove the old one in `missing_params` just in case + delete!(missing_params, sym) + delete!(missing_params, ttsym) + end + + if pdeps !== nothing + for (sym, expr) in pdeps + sym = unwrap(sym) + ttsym = default_toterm(sym) + delete!(missing_params, sym) + delete!(missing_params, ttsym) + p[sym] = p[ttsym] = expr + end + end + + isempty(missing_params) || throw(MissingParametersError(collect(missing_params))) + + p = Dict(unwrap(k) => fixpoint_sub(v, bigdefs) for (k, v) in p) for (sym, _) in p - if istree(sym) && operation(sym) === getindex && + if iscall(sym) && operation(sym) === getindex && first(arguments(sym)) in all_ps error("Scalarized parameter values ($sym) are not supported. Instead of `[p[1] => 1.0, p[2] => 2.0]` use `[p => [1.0, 2.0]]`") end @@ -89,29 +135,50 @@ function MTKParameters( for (sym, val) in p sym = unwrap(sym) - ctype = concrete_symtype(sym) - val = symconvert(ctype, fixpoint_sub(val, p)) + val = unwrap(val) + ctype = symtype(sym) + if symbolic_type(val) !== NotSymbolic() + error("Could not evaluate value of parameter $sym. Missing values for variables in expression $val.") + end + val = symconvert(ctype, val) done = set_value(sym, val) if !done && Symbolics.isarraysymbolic(sym) - done = all(set_value.(collect(sym), val)) - end - if !done - error("Symbol $sym does not have an index") + if Symbolics.shape(sym) === Symbolics.Unknown() + for i in eachindex(val) + set_value(sym[i], val[i]) + end + else + if size(sym) != size(val) + error("Got value of size $(size(val)) for parameter $sym of size $(size(sym))") + end + set_value.(collect(sym), val) + end end end + tunable_buffer = narrow_buffer_type.(tunable_buffer) + disc_buffer = narrow_buffer_type.(disc_buffer) + const_buffer = narrow_buffer_type.(const_buffer) + # Don't narrow nonnumeric types + nonnumeric_buffer = nonnumeric_buffer - if has_parameter_dependencies(sys) && - (pdeps = get_parameter_dependencies(sys)) !== nothing + if pdeps !== nothing pdeps = Dict(k => fixpoint_sub(v, pdeps) for (k, v) in pdeps) - dep_exprs = ArrayPartition((wrap.(v) for v in dep_buffer)...) + dep_exprs = ArrayPartition((Any[missing for _ in 1:length(v)] for v in dep_buffer)...) for (sym, val) in pdeps i, j = ic.dependent_idx[sym] - dep_exprs.x[i][j] = wrap(val) + dep_exprs.x[i][j] = unwrap(val) + end + dep_exprs = identity.(dep_exprs) + psyms = reorder_parameters(ic, full_parameters(sys)) + update_fn_exprs = build_function(dep_exprs, psyms..., expression = Val{true}) + + update_function_oop, update_function_iip = eval_or_rgf.( + update_fn_exprs; eval_expression, eval_module) + ap_dep_buffer = ArrayPartition(dep_buffer) + for i in eachindex(dep_exprs) + ap_dep_buffer[i] = fixpoint_sub(dep_exprs[i], p) end - p = reorder_parameters(ic, full_parameters(sys)) - oop, iip = build_function(dep_exprs, p...) - update_function_iip, update_function_oop = RuntimeGeneratedFunctions.@RuntimeGeneratedFunction(iip), - RuntimeGeneratedFunctions.@RuntimeGeneratedFunction(oop) + dep_buffer = narrow_buffer_type.(dep_buffer) else update_function_iip = update_function_oop = nothing end @@ -121,42 +188,74 @@ function MTKParameters( typeof(dep_buffer), typeof(nonnumeric_buffer), typeof(update_function_iip), typeof(update_function_oop)}(tunable_buffer, disc_buffer, const_buffer, dep_buffer, nonnumeric_buffer, update_function_iip, update_function_oop) - if mtkps.dependent_update_iip !== nothing - mtkps.dependent_update_iip(ArrayPartition(mtkps.dependent), mtkps...) - end return mtkps end -function buffer_to_arraypartition(buf) - return ArrayPartition(Tuple(eltype(v) <: AbstractArray ? buffer_to_arraypartition(v) : - v for v in buf)) +function narrow_buffer_type(buffer::AbstractArray) + type = Union{} + for x in buffer + type = promote_type(type, typeof(x)) + end + return convert.(type, buffer) end -function split_into_buffers(raw::AbstractArray, buf; recurse = true) - idx = 1 - function _helper(buf_v; recurse = true) - if eltype(buf_v) <: AbstractArray && recurse - return _helper.(buf_v; recurse = false) - else - res = reshape(raw[idx:(idx + length(buf_v) - 1)], size(buf_v)) - idx += length(buf_v) - return res - end +function narrow_buffer_type(buffer::AbstractArray{<:AbstractArray}) + buffer = narrow_buffer_type.(buffer) + type = Union{} + for x in buffer + type = promote_type(type, eltype(x)) end - return Tuple(_helper(buf_v; recurse) for buf_v in buf) + return broadcast.(convert, type, buffer) +end + +function buffer_to_arraypartition(buf) + return ArrayPartition(ntuple(i -> _buffer_to_arrp_helper(buf[i]), Val(length(buf)))) +end + +_buffer_to_arrp_helper(v::T) where {T} = _buffer_to_arrp_helper(eltype(T), v) +_buffer_to_arrp_helper(::Type{<:AbstractArray}, v) = buffer_to_arraypartition(v) +_buffer_to_arrp_helper(::Any, v) = v + +function _split_helper(buf_v::T, recurse, raw, idx) where {T} + _split_helper(eltype(T), buf_v, recurse, raw, idx) +end + +function _split_helper(::Type{<:AbstractArray}, buf_v, ::Val{true}, raw, idx) + map(b -> _split_helper(eltype(b), b, Val(false), raw, idx), buf_v) +end + +function _split_helper(::Type{<:AbstractArray}, buf_v, ::Val{false}, raw, idx) + _split_helper((), buf_v, (), raw, idx) +end + +function _split_helper(_, buf_v, _, raw, idx) + res = reshape(raw[idx[]:(idx[] + length(buf_v) - 1)], size(buf_v)) + idx[] += length(buf_v) + return res +end + +function split_into_buffers(raw::AbstractArray, buf, recurse = Val(true)) + idx = Ref(1) + ntuple(i -> _split_helper(buf[i], recurse, raw, idx), Val(length(buf))) +end + +function _update_tuple_helper(buf_v::T, raw, idx) where {T} + _update_tuple_helper(eltype(T), buf_v, raw, idx) +end + +function _update_tuple_helper(::Type{<:AbstractArray}, buf_v, raw, idx) + ntuple(i -> _update_tuple_helper(buf_v[i], raw, idx), length(buf_v)) +end + +function _update_tuple_helper(::Any, buf_v, raw, idx) + copyto!(buf_v, view(raw, idx[]:(idx[] + length(buf_v) - 1))) + idx[] += length(buf_v) + return nothing end function update_tuple_of_buffers(raw::AbstractArray, buf) - idx = 1 - function _helper(buf_v) - if eltype(buf_v) <: AbstractArray - _helper.(buf_v) - else - copyto!(buf_v, view(raw, idx:(idx + length(buf_v) - 1))) - idx += length(buf_v) - end - end - _helper.(buf) + idx = Ref(1) + ntuple(i -> _update_tuple_helper(buf[i], raw, idx), Val(length(buf))) end SciMLStructures.isscimlstructure(::MTKParameters) = true @@ -165,7 +264,8 @@ SciMLStructures.ismutablescimlstructure(::MTKParameters) = true for (Portion, field) in [(SciMLStructures.Tunable, :tunable) (SciMLStructures.Discrete, :discrete) - (SciMLStructures.Constants, :constant)] + (SciMLStructures.Constants, :constant) + (Nonnumeric, :nonnumeric)] @eval function SciMLStructures.canonicalize(::$Portion, p::MTKParameters) as_vector = buffer_to_arraypartition(p.$field) repack = let as_vector = as_vector, p = p @@ -186,16 +286,13 @@ for (Portion, field) in [(SciMLStructures.Tunable, :tunable) @set! p.$field = split_into_buffers(newvals, p.$field) if p.dependent_update_oop !== nothing raw = p.dependent_update_oop(p...) - @set! p.dependent = split_into_buffers(raw, p.dependent; recurse = false) + @set! p.dependent = split_into_buffers(raw, p.dependent, Val(false)) end p end @eval function SciMLStructures.replace!(::$Portion, p::MTKParameters, newvals) - src = split_into_buffers(newvals, p.$field) - for i in 1:length(p.$field) - (p.$field)[i] .= src[i] - end + update_tuple_of_buffers(newvals, p.$field) if p.dependent_update_iip !== nothing p.dependent_update_iip(ArrayPartition(p.dependent), p...) end @@ -240,22 +337,31 @@ end function SymbolicIndexingInterface.set_parameter!( p::MTKParameters, val, idx::ParameterIndex) - @unpack portion, idx = idx + @unpack portion, idx, validate_size = idx i, j, k... = idx if portion isa SciMLStructures.Tunable if isempty(k) + if validate_size && size(val) !== size(p.tunable[i][j]) + throw(InvalidParameterSizeException(size(p.tunable[i][j]), size(val))) + end p.tunable[i][j] = val else p.tunable[i][j][k...] = val end elseif portion isa SciMLStructures.Discrete if isempty(k) + if validate_size && size(val) !== size(p.discrete[i][j]) + throw(InvalidParameterSizeException(size(p.discrete[i][j]), size(val))) + end p.discrete[i][j] = val else p.discrete[i][j][k...] = val end elseif portion isa SciMLStructures.Constants if isempty(k) + if validate_size && size(val) !== size(p.constant[i][j]) + throw(InvalidParameterSizeException(size(p.constant[i][j]), size(val))) + end p.constant[i][j] = val else p.constant[i][j][k...] = val @@ -318,6 +424,125 @@ function _set_parameter_unchecked!( p.dependent_update_iip(ArrayPartition(p.dependent), p...) end +function narrow_buffer_type_and_fallback_undefs(oldbuf::Vector, newbuf::Vector) + type = Union{} + for i in eachindex(newbuf) + isassigned(newbuf, i) || continue + type = promote_type(type, typeof(newbuf[i])) + end + if type == Union{} + type = eltype(oldbuf) + end + for i in eachindex(newbuf) + isassigned(newbuf, i) && continue + newbuf[i] = convert(type, oldbuf[i]) + end + return convert(Vector{type}, newbuf) +end + +function validate_parameter_type(ic::IndexCache, p, index, val) + p = unwrap(p) + if p isa Symbol + p = get(ic.symbol_to_variable, p, nothing) + if p === nothing + @warn "No matching variable found for `Symbol` $p, skipping type validation." + return nothing + end + end + (; portion) = index + # Nonnumeric parameters have to match the type + if portion === NONNUMERIC_PORTION + stype = symtype(p) + val isa stype && return nothing + throw(ParameterTypeException(:validate_parameter_type, p, stype, val)) + end + stype = symtype(p) + # Array parameters need array values... + if stype <: AbstractArray && !isa(val, AbstractArray) + throw(ParameterTypeException(:validate_parameter_type, p, stype, val)) + end + # ... and must match sizes + if stype <: AbstractArray && Symbolics.shape(p) !== Symbolics.Unknown() && + size(val) != size(p) + throw(InvalidParameterSizeException(p, val)) + end + # Early exit + val isa stype && return nothing + if stype <: AbstractArray + # Arrays need handling when eltype is `Real` (accept any real array) + etype = eltype(stype) + if etype <: Real + etype = Real + end + # This is for duals and other complicated number types + etype = SciMLBase.parameterless_type(etype) + eltype(val) <: etype || throw(ParameterTypeException( + :validate_parameter_type, p, AbstractArray{etype}, val)) + else + # Real check + if stype <: Real + stype = Real + end + stype = SciMLBase.parameterless_type(stype) + val isa stype || + throw(ParameterTypeException(:validate_parameter_type, p, stype, val)) + end +end + +function indp_to_system(indp) + while hasmethod(symbolic_container, Tuple{typeof(indp)}) + indp = symbolic_container(indp) + end + return indp +end + +function SymbolicIndexingInterface.remake_buffer(indp, oldbuf::MTKParameters, vals::Dict) + newbuf = @set oldbuf.tunable = Tuple(Vector{Any}(undef, length(buf)) + for buf in oldbuf.tunable) + @set! newbuf.discrete = Tuple(Vector{Any}(undef, length(buf)) + for buf in newbuf.discrete) + @set! newbuf.constant = Tuple(Vector{Any}(undef, length(buf)) + for buf in newbuf.constant) + @set! newbuf.nonnumeric = Tuple(Vector{Any}(undef, length(buf)) + for buf in newbuf.nonnumeric) + + # If the parameter buffer is an `MTKParameters` object, `indp` must eventually drill + # down to an `AbstractSystem` using `symbolic_container`. We leverage this to get + # the index cache. + ic = get_index_cache(indp_to_system(indp)) + for (p, val) in vals + idx = parameter_index(indp, p) + validate_parameter_type(ic, p, idx, val) + _set_parameter_unchecked!( + newbuf, val, idx; update_dependent = false) + end + + @set! newbuf.tunable = narrow_buffer_type_and_fallback_undefs.( + oldbuf.tunable, newbuf.tunable) + @set! newbuf.discrete = narrow_buffer_type_and_fallback_undefs.( + oldbuf.discrete, newbuf.discrete) + @set! newbuf.constant = narrow_buffer_type_and_fallback_undefs.( + oldbuf.constant, newbuf.constant) + @set! newbuf.nonnumeric = narrow_buffer_type_and_fallback_undefs.( + oldbuf.nonnumeric, newbuf.nonnumeric) + if newbuf.dependent_update_oop !== nothing + @set! newbuf.dependent = narrow_buffer_type_and_fallback_undefs.( + oldbuf.dependent, + split_into_buffers( + newbuf.dependent_update_oop(newbuf...), oldbuf.dependent, Val(false))) + end + return newbuf +end + +function DiffEqBase.anyeltypedual( + p::MTKParameters, ::Type{Val{counter}} = Val{0}) where {counter} + DiffEqBase.anyeltypedual(p.tunable) +end +function DiffEqBase.anyeltypedual(p::Type{<:MTKParameters{T}}, + ::Type{Val{counter}} = Val{0}) where {counter} where {T} + DiffEqBase.__anyeltypedual(T) +end + _subarrays(v::AbstractVector) = isempty(v) ? () : (v,) _subarrays(v::ArrayPartition) = v.x _subarrays(v::Tuple) = v @@ -328,6 +553,7 @@ _num_subarrays(v::Tuple) = length(v) # getindex indexes the vectors, setindex! linearly indexes values # it's inconsistent, but we need it to be this way function Base.getindex(buf::MTKParameters, i) + i_orig = i if !isempty(buf.tunable) i <= _num_subarrays(buf.tunable) && return _subarrays(buf.tunable)[i] i -= _num_subarrays(buf.tunable) @@ -348,7 +574,7 @@ function Base.getindex(buf::MTKParameters, i) i <= _num_subarrays(buf.dependent) && return _subarrays(buf.dependent)[i] i -= _num_subarrays(buf.dependent) end - throw(BoundsError(buf, i)) + throw(BoundsError(buf, i_orig)) end function Base.setindex!(p::MTKParameters, val, i) function _helper(buf) @@ -432,9 +658,6 @@ function jacobian_wrt_vars(pf::F, p::MTKParameters, input_idxs, chunk::C) where for (i, val) in zip(input_idxs, p_small_inner) _set_parameter_unchecked!(p_big, val, i) end - # tunable, repack, _ = SciMLStructures.canonicalize(SciMLStructures.Tunable(), p_big) - # tunable[input_idxs] .= p_small_inner - # p_big = repack(tunable) return if pf isa SciMLBase.ParamJacobianWrapper buffer = Array{dualtype}(undef, size(pf.u)) pf(buffer, p_big) @@ -444,8 +667,6 @@ function jacobian_wrt_vars(pf::F, p::MTKParameters, input_idxs, chunk::C) where end end end - # tunable, _, _ = SciMLStructures.canonicalize(SciMLStructures.Tunable(), p) - # p_small = tunable[input_idxs] p_small = parameter_values.((p,), input_idxs) cfg = ForwardDiff.JacobianConfig(p_closure, p_small, chunk, tag) ForwardDiff.jacobian(p_closure, p_small, cfg, Val(false)) @@ -456,3 +677,29 @@ function as_duals(p::MTKParameters, dualtype) discrete = dualtype.(p.discrete) return MTKParameters{typeof(tunable), typeof(discrete)}(tunable, discrete) end + +const MISSING_PARAMETERS_MESSAGE = """ + Some parameters are missing from the variable map. + Please provide a value or default for the following variables: + """ + +struct MissingParametersError <: Exception + vars::Any +end + +function Base.showerror(io::IO, e::MissingParametersError) + println(io, MISSING_PARAMETERS_MESSAGE) + println(io, e.vars) +end + +function InvalidParameterSizeException(param, val) + DimensionMismatch("InvalidParameterSizeException: For parameter $(param) expected value of size $(size(param)). Received value $(val) of size $(size(val)).") +end + +function InvalidParameterSizeException(param::Tuple, val::Tuple) + DimensionMismatch("InvalidParameterSizeException: Expected value of size $(param). Received value of size $(val).") +end + +function ParameterTypeException(func, param, expected, val) + TypeError(func, "Parameter $param", expected, val) +end diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index 3735b8f6ea..7aa3f29191 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -11,8 +11,8 @@ $(FIELDS) ```julia using ModelingToolkit -@parameters x -@variables t u(..) +@parameters x t +@variables u(..) Dxx = Differential(x)^2 Dtt = Differential(t)^2 Dt = Differential(t) @@ -116,7 +116,8 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem p = ps isa SciMLBase.NullParameters ? [] : ps args = vcat(DestructuredArgs(p), args) ex = Func(args, [], eq.rhs) |> toexpr - eq.lhs => drop_expr(@RuntimeGeneratedFunction(eval_module, ex)) + eq.lhs => drop_expr(RuntimeGeneratedFunction( + eval_module, eval_module, ex)) end end end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 552f78f402..35aaf56a3f 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -7,16 +7,24 @@ end $(SIGNATURES) Structurally simplify algebraic equations in a system and compute the -topological sort of the observed equations. When `simplify=true`, the `simplify` -function will be applied during the tearing process. The kwargs -`allow_symbolic=false` and `allow_parameter=true` limit the coefficient -types during tearing. The kwarg `fully_determined=true` controls whether or not -an error will be thrown if the number of equations don't match the number of inputs, -outputs, and equations. +topological sort of the observed equations. -The optional argument `io` may take a tuple `(inputs, outputs)`. -This will convert all `inputs` to parameters and allow them to be unconnected, i.e., -simplification will allow models where `n_unknowns = n_equations - n_inputs`. +## Optional Arguments + +* `io` may take a tuple `(inputs, outputs)`. This will convert all `inputs` to parameters + and allow them to be unconnected, i.e., simplification will allow models where + `n_unknowns = n_equations - n_inputs`. + +## Keyword Arguments + +* `simplify = false`, When `simplify=true`, the symbolic `simplify` function will be + applied during the tearing process. +* `allow_symbolic = false`, allows symbolic coefficients during tearing. +* `allow_parameter = false`, allows parameter coefficients during tearing. +* `fully_determined = true`, controls whether or not an error will be thrown if the + number of equations don't match the number of inputs, outputs, and equations. +* `conservative=true`, limits tearing to only solve for trivial linear systems where + the coefficient has the absolute value of ``1``. """ function structural_simplify( sys::AbstractSystem, io = nothing; simplify = false, split = true, @@ -29,14 +37,19 @@ function structural_simplify( else newsys = newsys′ end - if newsys isa ODESystem - @set! newsys.parent = complete(sys; split) - elseif has_parent(newsys) + if newsys isa DiscreteSystem && + any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) + error(""" + Encountered algebraic equations when simplifying discrete system. This is \ + not yet supported. + """) + end + if newsys isa ODESystem || has_parent(newsys) @set! newsys.parent = complete(sys; split) end newsys = complete(newsys; split) if has_defaults(newsys) && (defs = get_defaults(newsys)) !== nothing - ks = collect(keys(defs)) + ks = collect(keys(defs)) # take copy to avoid mutating defs while iterating. for k in ks if Symbolics.isarraysymbolic(k) && Symbolics.shape(k) !== Symbolics.Unknown() for i in eachindex(k) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 54285a431f..ff26552c79 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -1,7 +1,7 @@ using DataStructures using Symbolics: linear_expansion, unwrap, Connection -using SymbolicUtils: istree, operation, arguments, Symbolic -using SymbolicUtils: quick_cancel, similarterm +using SymbolicUtils: iscall, operation, arguments, Symbolic +using SymbolicUtils: quick_cancel, maketerm using ..ModelingToolkit import ..ModelingToolkit: isdiffeq, var_from_nested_derivative, vars!, flatten, value, InvalidSystemException, isdifferential, _iszero, @@ -18,9 +18,8 @@ using SparseArrays function quick_cancel_expr(expr) Rewriters.Postwalk(quick_cancel, - similarterm = (x, f, args; kws...) -> similarterm(x, f, args, - SymbolicUtils.symtype(x); - metadata = SymbolicUtils.metadata(x), + similarterm = (x, f, args; kws...) -> maketerm(typeof(x), f, args, + SymbolicUtils.symtype(x), SymbolicUtils.metadata(x), kws...))(expr) end @@ -288,7 +287,7 @@ function TearingState(sys; quick_cancel = false, check = true) _var, _ = var_from_nested_derivative(v) any(isequal(_var), ivs) && continue if isparameter(_var) || - (istree(_var) && isparameter(operation(_var)) || isconstant(_var)) + (iscall(_var) && isparameter(operation(_var)) || isconstant(_var)) continue end v = scalarize(v) @@ -308,7 +307,7 @@ function TearingState(sys; quick_cancel = false, check = true) _var, _ = var_from_nested_derivative(var) any(isequal(_var), ivs) && continue if isparameter(_var) || - (istree(_var) && isparameter(operation(_var)) || isconstant(_var)) + (iscall(_var) && isparameter(operation(_var)) || isconstant(_var)) continue end varidx = addvar!(var) @@ -328,7 +327,7 @@ function TearingState(sys; quick_cancel = false, check = true) dvar = var idx = varidx - if istree(var) && operation(var) isa Symbolics.Operator && + if iscall(var) && operation(var) isa Symbolics.Operator && !isdifferential(var) && (it = input_timedomain(var)) !== nothing set_incidence = false var = only(arguments(var)) @@ -627,7 +626,7 @@ function structural_simplify!(state::TearingState, io = nothing; simplify = fals kwargs...) if state.sys isa ODESystem ci = ModelingToolkit.ClockInference(state) - ModelingToolkit.infer_clocks!(ci) + 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) @@ -636,6 +635,9 @@ function structural_simplify!(state::TearingState, io = nothing; simplify = fals 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 @@ -689,15 +691,18 @@ function _structural_simplify!(state::TearingState, io; simplify = false, ModelingToolkit.check_consistency(state, orig_inputs) end if fully_determined && dummy_derivative - sys = ModelingToolkit.dummy_derivative(sys, state; simplify, mm, check_consistency) + sys = ModelingToolkit.dummy_derivative( + sys, state; simplify, mm, check_consistency, kwargs...) elseif fully_determined var_eq_matching = pantelides!(state; finalize = false, kwargs...) sys = pantelides_reassemble(state, var_eq_matching) state = TearingState(sys) sys, mm = ModelingToolkit.alias_elimination!(state; kwargs...) - sys = ModelingToolkit.dummy_derivative(sys, state; simplify, mm, check_consistency) + sys = ModelingToolkit.dummy_derivative( + sys, state; simplify, mm, check_consistency, kwargs...) else - sys = ModelingToolkit.tearing(sys, state; simplify, mm, check_consistency) + sys = ModelingToolkit.tearing( + sys, state; simplify, mm, check_consistency, kwargs...) end fullunknowns = [map(eq -> eq.lhs, observed(sys)); unknowns(sys)] @set! sys.observed = ModelingToolkit.topsort_equations(observed(sys), fullunknowns) diff --git a/src/systems/unit_check.jl b/src/systems/unit_check.jl index 677d29895f..f053d9cf46 100644 --- a/src/systems/unit_check.jl +++ b/src/systems/unit_check.jl @@ -147,17 +147,17 @@ function get_unit(x::Symbolic) else pargs[2] isa Number ? base^pargs[2] : (1 * base)^pargs[2] end - elseif istree(x) + elseif iscall(x) op = operation(x) - if issym(op) || (istree(op) && istree(operation(op))) # Dependent variables, not function calls + if issym(op) || (iscall(op) && iscall(operation(op))) # Dependent variables, not function calls return screen_unit(getmetadata(x, VariableUnit, unitless)) # Like x(t) or x[i] - elseif istree(op) && !istree(operation(op)) + elseif iscall(op) && !iscall(operation(op)) gp = getmetadata(x, Symbolics.GetindexParent, nothing) # Like x[1](t) return screen_unit(getmetadata(gp, VariableUnit, unitless)) end # Actual function calls: args = arguments(x) return get_unit(op, args) - else # This function should only be reached by Terms, for which `istree` is true + else # This function should only be reached by Terms, for which `iscall` is true throw(ArgumentError("Unsupported value $x.")) end end diff --git a/src/systems/validation.jl b/src/systems/validation.jl index ea458d8b7e..84dd3b07e5 100644 --- a/src/systems/validation.jl +++ b/src/systems/validation.jl @@ -124,17 +124,17 @@ function get_unit(x::Symbolic) else pargs[2] isa Number ? base^pargs[2] : (1 * base)^pargs[2] end - elseif istree(x) + elseif iscall(x) op = operation(x) - if issym(op) || (istree(op) && istree(operation(op))) # Dependent variables, not function calls + if issym(op) || (iscall(op) && iscall(operation(op))) # Dependent variables, not function calls return screen_unit(getmetadata(x, VariableUnit, unitless)) # Like x(t) or x[i] - elseif istree(op) && !istree(operation(op)) + elseif iscall(op) && !iscall(operation(op)) gp = getmetadata(x, Symbolics.GetindexParent, nothing) # Like x[1](t) return screen_unit(getmetadata(gp, VariableUnit, unitless)) end # Actual function calls: args = arguments(x) return get_unit(op, args) - else # This function should only be reached by Terms, for which `istree` is true + else # This function should only be reached by Terms, for which `iscall` is true throw(ArgumentError("Unsupported value $x.")) end end diff --git a/src/utils.jl b/src/utils.jl index 97dea54d89..86a28dcae6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,3 +1,14 @@ +""" + union_nothing(x::Union{T1, Nothing}, y::Union{T2, Nothing}) where {T1, T2} + +Unite x and y gracefully when they could be nothing. If neither is nothing, x and y are united normally. If one is nothing, the other is returned unmodified. If both are nothing, nothing is returned. +""" +function union_nothing(x::Union{T1, Nothing}, y::Union{T2, Nothing}) where {T1, T2} + isnothing(x) && return y # y can be nothing or something + isnothing(y) && return x # x can be nothing or something + return union(x, y) # both x and y are something and can be united normally +end + get_iv(D::Differential) = D.x function make_operation(@nospecialize(op), args) @@ -16,19 +27,21 @@ function make_operation(@nospecialize(op), args) end function detime_dvs(op) - if !istree(op) + if !iscall(op) op elseif issym(operation(op)) Sym{Real}(nameof(operation(op))) else - similarterm(op, operation(op), detime_dvs.(arguments(op))) + maketerm(typeof(op), operation(op), detime_dvs.(arguments(op)), + symtype(op), metadata(op)) end end function retime_dvs(op, dvs, iv) issym(op) && return Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(op))(iv) - istree(op) ? - similarterm(op, operation(op), retime_dvs.(arguments(op), (dvs,), (iv,))) : + iscall(op) ? + maketerm(typeof(op), operation(op), retime_dvs.(arguments(op), (dvs,), (iv,)), + symtype(op), metadata(op)) : op end @@ -100,6 +113,13 @@ const CheckAll = 1 << 0 const CheckComponents = 1 << 1 const CheckUnits = 1 << 2 +function check_independent_variables(ivs) + for iv in ivs + isparameter(iv) || + @warn "Independent variable $iv should be defined with @independent_variables $iv." + end +end + function check_parameters(ps, iv) for p in ps isequal(iv, p) && @@ -186,7 +206,7 @@ end Get all the independent variables with respect to which differentials are taken. """ function collect_ivs_from_nested_operator!(ivs, x, target_op) - if !istree(x) + if !iscall(x) return end op = operation(unwrap(x)) @@ -202,9 +222,9 @@ function collect_ivs_from_nested_operator!(ivs, x, target_op) end function iv_from_nested_derivative(x, op = Differential) - if istree(x) && operation(x) == getindex + if iscall(x) && operation(x) == getindex iv_from_nested_derivative(arguments(x)[1], op) - elseif istree(x) + elseif iscall(x) operation(x) isa op ? iv_from_nested_derivative(arguments(x)[1], op) : arguments(x)[1] elseif issym(x) @@ -215,7 +235,7 @@ function iv_from_nested_derivative(x, op = Differential) end hasdefault(v) = hasmetadata(v, Symbolics.VariableDefaultValue) -getdefault(v) = value(getmetadata(v, Symbolics.VariableDefaultValue)) +getdefault(v) = value(Symbolics.getdefaultval(v)) function getdefaulttype(v) def = value(getmetadata(unwrap(v), Symbolics.VariableDefaultValue, nothing)) def === nothing ? Float64 : typeof(def) @@ -246,7 +266,7 @@ function collect_var_to_name!(vars, xs) hasname(xarr) || continue vars[Symbolics.getname(xarr)] = xarr else - if istree(x) && operation(x) === getindex + if iscall(x) && operation(x) === getindex x = arguments(x)[1] end x = unwrap(x) @@ -274,12 +294,12 @@ end Check if difference/derivative operation occurs in the R.H.S. of an equation """ function _check_operator_variables(eq, op::T, expr = eq.rhs) where {T} - istree(expr) || return nothing + iscall(expr) || return nothing if operation(expr) isa op throw_invalid_operator(expr, eq, op) end foreach(expr -> _check_operator_variables(eq, op, expr), - SymbolicUtils.unsorted_arguments(expr)) + SymbolicUtils.arguments(expr)) end """ Check if all the LHS are unique @@ -295,10 +315,10 @@ function check_operator_variables(eqs, op::T) where {T} if op === Differential is_tmp_fine = isdifferential(x) else - is_tmp_fine = istree(x) && !(operation(x) isa op) + is_tmp_fine = iscall(x) && !(operation(x) isa op) end else - nd = count(x -> istree(x) && !(operation(x) isa op), tmp) + nd = count(x -> iscall(x) && !(operation(x) isa op), tmp) is_tmp_fine = iszero(nd) end is_tmp_fine || @@ -312,7 +332,7 @@ function check_operator_variables(eqs, op::T) where {T} end end -isoperator(expr, op) = istree(expr) && operation(expr) isa op +isoperator(expr, op) = iscall(expr) && operation(expr) isa op isoperator(op) = expr -> isoperator(expr, op) isdifferential(expr) = isoperator(expr, Differential) @@ -336,14 +356,15 @@ Return a `Set` containing all variables in `x` that appear in Example: ``` -@variables t u(t) y(t) +t = ModelingToolkit.t_nounits +@variables u(t) y(t) D = Differential(t) v = ModelingToolkit.vars(D(y) ~ u) v == Set([D(y), u]) ``` """ function vars(exprs::Symbolic; op = Differential) - istree(exprs) ? vars([exprs]; op = op) : Set([exprs]) + iscall(exprs) ? vars([exprs]; op = op) : Set([exprs]) end vars(exprs::Num; op = Differential) = vars(unwrap(exprs); op) vars(exprs::Symbolics.Arr; op = Differential) = vars(unwrap(exprs); op) @@ -356,13 +377,13 @@ function vars!(vars, O; op = Differential) if isvariable(O) return push!(vars, O) end - !istree(O) && return vars + !iscall(O) && return vars operation(O) isa op && return push!(vars, O) if operation(O) === (getindex) arr = first(arguments(O)) - istree(arr) && operation(arr) isa op && return push!(vars, O) + iscall(arr) && operation(arr) isa op && return push!(vars, O) isvariable(arr) && return push!(vars, O) end @@ -408,7 +429,8 @@ collect_differential_variables(sys) = collect_operator_variables(sys, Differenti Return a `Set` with all applied operators in `x`, example: ``` -@variables t u(t) y(t) +@independent_variables t +@variables u(t) y(t) D = Differential(t) eq = D(y) ~ u ModelingToolkit.collect_applied_operators(eq, Differential) == Set([D(y)]) @@ -420,7 +442,7 @@ function collect_applied_operators(x, op) v = vars(x, op = op) filter(v) do x issym(x) && return false - istree(x) && return operation(x) isa op + iscall(x) && return operation(x) isa op false end end @@ -429,7 +451,7 @@ function find_derivatives!(vars, expr::Equation, f = identity) (find_derivatives!(vars, expr.lhs, f); find_derivatives!(vars, expr.rhs, f); vars) end function find_derivatives!(vars, expr, f) - !istree(O) && return vars + !iscall(O) && return vars operation(O) isa Differential && push!(vars, f(O)) for arg in arguments(O) vars!(vars, arg) @@ -442,7 +464,7 @@ function collect_vars!(unknowns, parameters, expr, iv; op = Differential) collect_var!(unknowns, parameters, expr, iv) else for var in vars(expr; op) - if istree(var) && operation(var) isa Differential + if iscall(var) && operation(var) isa Differential var, _ = var_from_nested_derivative(var) end collect_var!(unknowns, parameters, var, iv) @@ -453,7 +475,7 @@ end function collect_var!(unknowns, parameters, var, iv) isequal(var, iv) && return nothing - if isparameter(var) || (istree(var) && isparameter(operation(var))) + if isparameter(var) || (iscall(var) && isparameter(operation(var))) push!(parameters, var) elseif !isconstant(var) push!(unknowns, var) @@ -564,19 +586,22 @@ function empty_substitutions(sys) isnothing(subs) || isempty(subs.deps) end -function get_cmap(sys) +function get_cmap(sys, exprs = nothing) #Inject substitutions for constants => values cs = collect_constants([get_eqs(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 -function get_substitutions_and_solved_unknowns(sys; no_postprocess = false) - cmap, cs = get_cmap(sys) +function get_substitutions_and_solved_unknowns(sys, exprs = nothing; no_postprocess = false) + cmap, cs = get_cmap(sys, exprs) if empty_substitutions(sys) && isempty(cs) sol_states = Code.LazyState() pre = no_postprocess ? (ex -> ex) : get_postprocess_fbody(sys) @@ -612,6 +637,18 @@ function mergedefaults(defaults, varmap, vars) end end +function mergedefaults(defaults, observedmap, varmap, vars) + defs = if varmap isa Dict + merge(observedmap, defaults, varmap) + elseif eltype(varmap) <: Pair + merge(observedmap, defaults, Dict(varmap)) + elseif eltype(varmap) <: Number + merge(observedmap, defaults, Dict(zip(vars, varmap))) + else + merge(observedmap, defaults) + end +end + @noinline function throw_missingvars_in_sys(vars) throw(ArgumentError("$vars are either missing from the variable map or missing from the system's unknowns/parameters list.")) end @@ -626,42 +663,43 @@ function promote_to_concrete(vs; tofloat = true, use_union = true) vs = Any[vs...] end T = eltype(vs) - if Base.isconcretetype(T) && (!tofloat || T === float(T)) # nothing to do - return vs - else - sym_vs = filter(x -> SymbolicUtils.issym(x) || SymbolicUtils.istree(x), vs) - isempty(sym_vs) || throw_missingvars_in_sys(sym_vs) - - C = nothing - for v in vs - E = typeof(v) - if E <: Number - if tofloat - E = float(E) - end - end - if C === nothing - C = E - end - if use_union - C = Union{C, E} - else - @assert C==E "`promote_to_concrete` can't make type $E uniform with $C" - C = E - end - end - y = similar(vs, C) - for i in eachindex(vs) - if (vs[i] isa Number) & tofloat - y[i] = float(vs[i]) #needed because copyto! can't convert Int to Float automatically - else - y[i] = vs[i] + # return early if there is nothing to do + #Base.isconcretetype(T) && (!tofloat || T === float(T)) && return vs # TODO: disabled float(T) to restore missing errors in https://github.com/SciML/ModelingToolkit.jl/issues/2873 + Base.isconcretetype(T) && !tofloat && return vs + + sym_vs = filter(x -> SymbolicUtils.issym(x) || SymbolicUtils.iscall(x), vs) + isempty(sym_vs) || throw_missingvars_in_sys(sym_vs) + + C = nothing + for v in vs + E = typeof(v) + if E <: Number + if tofloat + E = float(E) end end + if C === nothing + C = E + end + if use_union + C = Union{C, E} + else + @assert C==E "`promote_to_concrete` can't make type $E uniform with $C" + C = E + end + end - return y + y = similar(vs, C) + for i in eachindex(vs) + if (vs[i] isa Number) & tofloat + y[i] = float(vs[i]) #needed because copyto! can't convert Int to Float automatically + else + y[i] = vs[i] + end end + + return y end struct BitDict <: AbstractDict{Int, Int} @@ -789,9 +827,9 @@ function jacobian_wrt_vars(pf::F, p, input_idxs, chunk::C) where {F, C} end function fold_constants(ex) - if istree(ex) - similarterm(ex, operation(ex), map(fold_constants, arguments(ex)), - symtype(ex); metadata = metadata(ex)) + if iscall(ex) + maketerm(typeof(ex), operation(ex), map(fold_constants, arguments(ex)), + symtype(ex), metadata(ex)) elseif issym(ex) && isconstant(ex) getdefault(ex) else @@ -808,3 +846,11 @@ function restrict_array_to_union(arr) end return Array{T, ndims(arr)}(arr) end + +function eval_or_rgf(expr::Expr; eval_expression = false, eval_module = @__MODULE__) + if eval_expression + return eval_module.eval(expr) + else + return drop_expr(RuntimeGeneratedFunction(eval_module, eval_module, expr)) + end +end diff --git a/src/variables.jl b/src/variables.jl index 3b3583cc5a..d9c5f71cab 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -47,6 +47,7 @@ function dump_variable_metadata(var) if desc == "" desc = nothing end + default = hasdefault(uvar) ? getdefault(uvar) : nothing guess = getguess(uvar) disturbance = isdisturbance(uvar) || nothing tunable = istunable(uvar, isparameter(uvar)) @@ -72,7 +73,8 @@ function dump_variable_metadata(var) disturbance, tunable, dist, - type + type, + default ) return NamedTuple(k => v for (k, v) in pairs(meta) if v !== nothing) @@ -102,7 +104,7 @@ isirreducible(x) = isvarkind(VariableIrreducible, x) state_priority(x) = convert(Float64, getmetadata(x, VariableStatePriority, 0.0))::Float64 function default_toterm(x) - if istree(x) && (op = operation(x)) isa Operator + if iscall(x) && (op = operation(x)) isa Operator if !(op isa Differential) if op isa Shift && op.steps < 0 return x @@ -197,7 +199,7 @@ function _varmap_to_vars(varmap::Dict, varlist; defaults = Dict(), check = false for var in varlist var = unwrap(var) val = unwrap(fixpoint_sub(var, varmap; operator = Symbolics.Operator)) - if symbolic_type(val) === NotSymbolic() + if !isequal(val, var) values[var] = val end end @@ -209,11 +211,14 @@ end function canonicalize_varmap(varmap; toterm = Symbolics.diff2term) new_varmap = Dict() for (k, v) in varmap - new_varmap[unwrap(k)] = unwrap(v) - new_varmap[toterm(unwrap(k))] = unwrap(v) + k = unwrap(k) + v = unwrap(v) + new_varmap[k] = v + new_varmap[toterm(k)] = v if Symbolics.isarraysymbolic(k) && Symbolics.shape(k) !== Symbolics.Unknown() for i in eachindex(k) new_varmap[k[i]] = v[i] + new_varmap[toterm(k[i])] = v[i] end end end @@ -229,7 +234,8 @@ ishistory(x) = ishistory(unwrap(x)) ishistory(x::Symbolic) = getmetadata(x, IsHistory, false) hist(x, t) = wrap(hist(unwrap(x), t)) function hist(x::Symbolic, t) - setmetadata(toparam(similarterm(x, operation(x), [unwrap(t)], metadata = metadata(x))), + setmetadata( + toparam(maketerm(typeof(x), operation(x), [unwrap(t)], symtype(x), metadata(x))), IsHistory, true) end @@ -453,7 +459,7 @@ end ## Guess ====================================================================== struct VariableGuess end Symbolics.option_to_metadata_type(::Val{:guess}) = VariableGuess -getguess(x::Num) = getguess(Symbolics.unwrap(x)) +getguess(x::Union{Num, Symbolics.Arr}) = getguess(Symbolics.unwrap(x)) """ getguess(x) @@ -466,8 +472,6 @@ Create variables with a guess like this ``` """ function getguess(x) - p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) Symbolics.getmetadata(x, VariableGuess, nothing) end diff --git a/test/abstractsystem.jl b/test/abstractsystem.jl index 85379bdad6..bd5b6fe542 100644 --- a/test/abstractsystem.jl +++ b/test/abstractsystem.jl @@ -2,7 +2,8 @@ using ModelingToolkit using Test MT = ModelingToolkit -@variables t x +@independent_variables t +@variables x struct MyNLS <: MT.AbstractSystem name::Any systems::Any diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index b174e37f3a..d9b59408e6 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -1,6 +1,7 @@ using ModelingToolkit, OrdinaryDiffEq, Test -@parameters t α β γ δ +@independent_variables t +@parameters α β γ δ @variables x(t) y(t) D = Differential(t) diff --git a/test/clock.jl b/test/clock.jl index b7964909d0..86967365ad 100644 --- a/test/clock.jl +++ b/test/clock.jl @@ -105,429 +105,428 @@ eqs = [yd ~ Sample(t, dt)(y) D(x) ~ -x + u y ~ x] @named sys = ODESystem(eqs, t) -ss = structural_simplify(sys); - -Tf = 1.0 -prob = ODEProblem(ss, [x => 0.1], (0.0, Tf), - [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) -# create integrator so callback is evaluated at t=0 and we can test correct param values -int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) -@test sort(vcat(int.p...)) == [0.1, 1.0, 2.1, 2.1, 2.1] # yd, kp, ud(k-1), ud, Hold(ud) -prob = ODEProblem(ss, [x => 0.1], (0.0, Tf), - [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) # recreate problem to empty saved values -sol = solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) - -ss_nosplit = structural_simplify(sys; split = false) -prob_nosplit = ODEProblem(ss_nosplit, [x => 0.1], (0.0, Tf), - [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) -int = init(prob_nosplit, Tsit5(); kwargshandle = KeywordArgSilent) -@test sort(int.p) == [0.1, 1.0, 2.1, 2.1, 2.1] # yd, kp, ud(k-1), ud, Hold(ud) -prob_nosplit = ODEProblem(ss_nosplit, [x => 0.1], (0.0, Tf), - [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) # recreate problem to empty saved values -sol_nosplit = solve(prob_nosplit, Tsit5(), kwargshandle = KeywordArgSilent) -# For all inputs in parameters, just initialize them to 0.0, and then set them -# in the callback. - -# kp is the only real parameter -function foo!(du, u, p, t) - x = u[1] - ud = p[2] - du[1] = -x + ud -end -function affect!(integrator, saved_values) - yd = integrator.u[1] - kp = integrator.p[1] - ud = integrator.p[2] - udd = integrator.p[3] - - integrator.p[2] = kp * yd + udd - integrator.p[3] = ud - - push!(saved_values.t, integrator.t) - push!(saved_values.saveval, [integrator.p[2], integrator.p[3]]) - - nothing -end -saved_values = SavedValues(Float64, Vector{Float64}) -cb = PeriodicCallback( - Base.Fix2(affect!, saved_values), 0.1; final_affect = true, initial_affect = true) -# kp ud -prob = ODEProblem(foo!, [0.1], (0.0, Tf), [1.0, 2.1, 2.0], callback = cb) -sol2 = solve(prob, Tsit5()) -@test sol.u == sol2.u -@test sol_nosplit.u == sol2.u -@test saved_values.t == sol.prob.kwargs[:disc_saved_values][1].t -@test saved_values.t == sol_nosplit.prob.kwargs[:disc_saved_values][1].t -@test saved_values.saveval == sol.prob.kwargs[:disc_saved_values][1].saveval -@test saved_values.saveval == sol_nosplit.prob.kwargs[:disc_saved_values][1].saveval - -@info "Testing multi-rate hybrid system" -dt = 0.1 -dt2 = 0.2 -@variables x(t) y(t) u(t) r(t) yd1(t) ud1(t) yd2(t) ud2(t) -@parameters kp - -eqs = [ - # controller (time discrete part `dt=0.1`) - yd1 ~ Sample(t, dt)(y) - ud1 ~ kp * (Sample(t, dt)(r) - yd1) - yd2 ~ Sample(t, dt2)(y) - ud2 ~ kp * (Sample(t, dt2)(r) - yd2) - - # plant (time continuous part) - u ~ Hold(ud1) + Hold(ud2) - D(x) ~ -x + u - y ~ x] -@named sys = ODESystem(eqs, t) -ci, varmap = infer_clocks(sys) - -d = Clock(t, dt) -d2 = Clock(t, dt2) -#@test get_eq_domain(eqs[1]) == d -#@test get_eq_domain(eqs[3]) == d2 - -@test varmap[yd1] == d -@test varmap[ud1] == d -@test varmap[yd2] == d2 -@test varmap[ud2] == d2 -@test varmap[r] == Continuous() -@test varmap[x] == Continuous() -@test varmap[y] == Continuous() -@test varmap[u] == Continuous() - -@info "test composed systems" - -dt = 0.5 -d = Clock(t, dt) -k = ShiftIndex(d) -timevec = 0:0.1:4 - -function plant(; name) - @variables x(t)=1 u(t)=0 y(t)=0 - eqs = [D(x) ~ -x + u - y ~ x] - ODESystem(eqs, t; name = name) -end - -function filt(; name) - @variables x(t)=0 u(t)=0 y(t)=0 - a = 1 / exp(dt) - eqs = [x ~ a * x(k - 1) + (1 - a) * u(k - 1) - y ~ x] - ODESystem(eqs, t, name = name) -end - -function controller(kp; name) - @variables y(t)=0 r(t)=0 ud(t)=0 yd(t)=0 - @parameters kp = kp - eqs = [yd ~ Sample(y) - ud ~ kp * (r - yd)] - ODESystem(eqs, t; name = name) -end - -@named f = filt() -@named c = controller(1) -@named p = plant() - -connections = [f.u ~ -1#(t >= 1) # step input - f.y ~ c.r # filtered reference to controller reference - Hold(c.ud) ~ p.u # controller output to plant input - p.y ~ c.y] - -@named cl = ODESystem(connections, t, systems = [f, c, p]) - -ci, varmap = infer_clocks(cl) - -@test varmap[f.x] == Clock(t, 0.5) -@test varmap[p.x] == Continuous() -@test varmap[p.y] == Continuous() -@test varmap[c.ud] == Clock(t, 0.5) -@test varmap[c.yd] == Clock(t, 0.5) -@test varmap[c.y] == Continuous() -@test varmap[f.y] == Clock(t, 0.5) -@test varmap[f.u] == Clock(t, 0.5) -@test varmap[p.u] == Continuous() -@test varmap[c.r] == Clock(t, 0.5) - -## Multiple clock rates -@info "Testing multi-rate hybrid system" -dt = 0.1 -dt2 = 0.2 -@variables x(t)=0 y(t)=0 u(t)=0 yd1(t)=0 ud1(t)=0 yd2(t)=0 ud2(t)=0 -@parameters kp=1 r=1 - -eqs = [ - # controller (time discrete part `dt=0.1`) - yd1 ~ Sample(t, dt)(y) - ud1 ~ kp * (r - yd1) - # controller (time discrete part `dt=0.2`) - yd2 ~ Sample(t, dt2)(y) - ud2 ~ kp * (r - yd2) - - # plant (time continuous part) - u ~ Hold(ud1) + Hold(ud2) - D(x) ~ -x + u - y ~ x] - -@named cl = ODESystem(eqs, t) - -d = Clock(t, dt) -d2 = Clock(t, dt2) - -ci, varmap = infer_clocks(cl) -@test varmap[yd1] == d -@test varmap[ud1] == d -@test varmap[yd2] == d2 -@test varmap[ud2] == d2 -@test varmap[x] == Continuous() -@test varmap[y] == Continuous() -@test varmap[u] == Continuous() +@test_throws ModelingToolkit.HybridSystemNotSupportedException ss=structural_simplify(sys); -ss = structural_simplify(cl) -ss_nosplit = structural_simplify(cl; split = false) - -if VERSION >= v"1.7" - prob = ODEProblem(ss, [x => 0.0], (0.0, 1.0), [kp => 1.0]) - prob_nosplit = ODEProblem(ss_nosplit, [x => 0.0], (0.0, 1.0), [kp => 1.0]) +@test_skip begin + Tf = 1.0 + prob = ODEProblem(ss, [x => 0.1], (0.0, Tf), + [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) + # create integrator so callback is evaluated at t=0 and we can test correct param values + int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) + @test sort(vcat(int.p...)) == [0.1, 1.0, 2.1, 2.1, 2.1] # yd, kp, ud(k-1), ud, Hold(ud) + prob = ODEProblem(ss, [x => 0.1], (0.0, Tf), + [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) # recreate problem to empty saved values sol = solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) - sol_nosplit = solve(prob_nosplit, Tsit5(), kwargshandle = KeywordArgSilent) - function foo!(dx, x, p, t) - kp, ud1, ud2 = p - dx[1] = -x[1] + ud1 + ud2 - end - - function affect1!(integrator) - kp = integrator.p[1] - y = integrator.u[1] - r = 1.0 - ud1 = kp * (r - y) - integrator.p[2] = ud1 - nothing + ss_nosplit = structural_simplify(sys; split = false) + prob_nosplit = ODEProblem(ss_nosplit, [x => 0.1], (0.0, Tf), + [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) + int = init(prob_nosplit, Tsit5(); kwargshandle = KeywordArgSilent) + @test sort(int.p) == [0.1, 1.0, 2.1, 2.1, 2.1] # yd, kp, ud(k-1), ud, Hold(ud) + prob_nosplit = ODEProblem(ss_nosplit, [x => 0.1], (0.0, Tf), + [kp => 1.0; ud(k - 1) => 2.1; ud(k - 2) => 2.0]) # recreate problem to empty saved values + sol_nosplit = solve(prob_nosplit, Tsit5(), kwargshandle = KeywordArgSilent) + # For all inputs in parameters, just initialize them to 0.0, and then set them + # in the callback. + + # kp is the only real parameter + function foo!(du, u, p, t) + x = u[1] + ud = p[2] + du[1] = -x + ud end - function affect2!(integrator) + function affect!(integrator, saved_values) + yd = integrator.u[1] kp = integrator.p[1] - y = integrator.u[1] - r = 1.0 - ud2 = kp * (r - y) - integrator.p[3] = ud2 - nothing - end - cb1 = PeriodicCallback(affect1!, dt; final_affect = true, initial_affect = true) - cb2 = PeriodicCallback(affect2!, dt2; final_affect = true, initial_affect = true) - cb = CallbackSet(cb1, cb2) - # kp ud1 ud2 - prob = ODEProblem(foo!, [0.0], (0.0, 1.0), [1.0, 1.0, 1.0], callback = cb) - sol2 = solve(prob, Tsit5()) - - @test sol.u≈sol2.u atol=1e-6 - @test sol_nosplit.u≈sol2.u atol=1e-6 -end + ud = integrator.p[2] + udd = integrator.p[3] -## -@info "Testing hybrid system with components" -using ModelingToolkitStandardLibrary.Blocks + integrator.p[2] = kp * yd + udd + integrator.p[3] = ud -dt = 0.05 -d = Clock(t, dt) -k = ShiftIndex(d) + push!(saved_values.t, integrator.t) + push!(saved_values.saveval, [integrator.p[2], integrator.p[3]]) -@mtkmodel DiscretePI begin - @components begin - input = RealInput() - output = RealOutput() - end - @parameters begin - kp = 1, [description = "Proportional gain"] - ki = 1, [description = "Integral gain"] - end - @variables begin - x(t) = 0, [description = "Integral state"] - u(t) - y(t) + nothing end - @equations begin - x(k) ~ x(k - 1) + ki * u(k) - output.u(k) ~ y(k) - input.u(k) ~ u(k) - y(k) ~ x(k - 1) + kp * u(k) + saved_values = SavedValues(Float64, Vector{Float64}) + cb = PeriodicCallback( + Base.Fix2(affect!, saved_values), 0.1; final_affect = true, initial_affect = true) + # kp ud + prob = ODEProblem(foo!, [0.1], (0.0, Tf), [1.0, 2.1, 2.0], callback = cb) + sol2 = solve(prob, Tsit5()) + @test sol.u == sol2.u + @test sol_nosplit.u == sol2.u + @test saved_values.t == sol.prob.kwargs[:disc_saved_values][1].t + @test saved_values.t == sol_nosplit.prob.kwargs[:disc_saved_values][1].t + @test saved_values.saveval == sol.prob.kwargs[:disc_saved_values][1].saveval + @test saved_values.saveval == sol_nosplit.prob.kwargs[:disc_saved_values][1].saveval + + @info "Testing multi-rate hybrid system" + dt = 0.1 + dt2 = 0.2 + @variables x(t) y(t) u(t) r(t) yd1(t) ud1(t) yd2(t) ud2(t) + @parameters kp + + eqs = [ + # controller (time discrete part `dt=0.1`) + yd1 ~ Sample(t, dt)(y) + ud1 ~ kp * (Sample(t, dt)(r) - yd1) + yd2 ~ Sample(t, dt2)(y) + ud2 ~ kp * (Sample(t, dt2)(r) - yd2) + + # plant (time continuous part) + u ~ Hold(ud1) + Hold(ud2) + D(x) ~ -x + u + y ~ x] + @named sys = ODESystem(eqs, t) + ci, varmap = infer_clocks(sys) + + d = Clock(t, dt) + d2 = Clock(t, dt2) + #@test get_eq_domain(eqs[1]) == d + #@test get_eq_domain(eqs[3]) == d2 + + @test varmap[yd1] == d + @test varmap[ud1] == d + @test varmap[yd2] == d2 + @test varmap[ud2] == d2 + @test varmap[r] == Continuous() + @test varmap[x] == Continuous() + @test varmap[y] == Continuous() + @test varmap[u] == Continuous() + + @info "test composed systems" + + dt = 0.5 + d = Clock(t, dt) + k = ShiftIndex(d) + timevec = 0:0.1:4 + + function plant(; name) + @variables x(t)=1 u(t)=0 y(t)=0 + eqs = [D(x) ~ -x + u + y ~ x] + ODESystem(eqs, t; name = name) end -end -@mtkmodel Sampler begin - @components begin - input = RealInput() - output = RealOutput() + function filt(; name) + @variables x(t)=0 u(t)=0 y(t)=0 + a = 1 / exp(dt) + eqs = [x ~ a * x(k - 1) + (1 - a) * u(k - 1) + y ~ x] + ODESystem(eqs, t, name = name) end - @equations begin - output.u ~ Sample(t, dt)(input.u) + + function controller(kp; name) + @variables y(t)=0 r(t)=0 ud(t)=0 yd(t)=0 + @parameters kp = kp + eqs = [yd ~ Sample(y) + ud ~ kp * (r - yd)] + ODESystem(eqs, t; name = name) end -end -@mtkmodel Holder begin - @components begin - input = RealInput() - output = RealOutput() + @named f = filt() + @named c = controller(1) + @named p = plant() + + connections = [f.u ~ -1#(t >= 1) # step input + f.y ~ c.r # filtered reference to controller reference + Hold(c.ud) ~ p.u # controller output to plant input + p.y ~ c.y] + + @named cl = ODESystem(connections, t, systems = [f, c, p]) + + ci, varmap = infer_clocks(cl) + + @test varmap[f.x] == Clock(t, 0.5) + @test varmap[p.x] == Continuous() + @test varmap[p.y] == Continuous() + @test varmap[c.ud] == Clock(t, 0.5) + @test varmap[c.yd] == Clock(t, 0.5) + @test varmap[c.y] == Continuous() + @test varmap[f.y] == Clock(t, 0.5) + @test varmap[f.u] == Clock(t, 0.5) + @test varmap[p.u] == Continuous() + @test varmap[c.r] == Clock(t, 0.5) + + ## Multiple clock rates + @info "Testing multi-rate hybrid system" + dt = 0.1 + dt2 = 0.2 + @variables x(t)=0 y(t)=0 u(t)=0 yd1(t)=0 ud1(t)=0 yd2(t)=0 ud2(t)=0 + @parameters kp=1 r=1 + + eqs = [ + # controller (time discrete part `dt=0.1`) + yd1 ~ Sample(t, dt)(y) + ud1 ~ kp * (r - yd1) + # controller (time discrete part `dt=0.2`) + yd2 ~ Sample(t, dt2)(y) + ud2 ~ kp * (r - yd2) + + # plant (time continuous part) + u ~ Hold(ud1) + Hold(ud2) + D(x) ~ -x + u + y ~ x] + + @named cl = ODESystem(eqs, t) + + d = Clock(t, dt) + d2 = Clock(t, dt2) + + ci, varmap = infer_clocks(cl) + @test varmap[yd1] == d + @test varmap[ud1] == d + @test varmap[yd2] == d2 + @test varmap[ud2] == d2 + @test varmap[x] == Continuous() + @test varmap[y] == Continuous() + @test varmap[u] == Continuous() + + ss = structural_simplify(cl) + ss_nosplit = structural_simplify(cl; split = false) + + if VERSION >= v"1.7" + prob = ODEProblem(ss, [x => 0.0], (0.0, 1.0), [kp => 1.0]) + prob_nosplit = ODEProblem(ss_nosplit, [x => 0.0], (0.0, 1.0), [kp => 1.0]) + sol = solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) + sol_nosplit = solve(prob_nosplit, Tsit5(), kwargshandle = KeywordArgSilent) + + function foo!(dx, x, p, t) + kp, ud1, ud2 = p + dx[1] = -x[1] + ud1 + ud2 + end + + function affect1!(integrator) + kp = integrator.p[1] + y = integrator.u[1] + r = 1.0 + ud1 = kp * (r - y) + integrator.p[2] = ud1 + nothing + end + function affect2!(integrator) + kp = integrator.p[1] + y = integrator.u[1] + r = 1.0 + ud2 = kp * (r - y) + integrator.p[3] = ud2 + nothing + end + cb1 = PeriodicCallback(affect1!, dt; final_affect = true, initial_affect = true) + cb2 = PeriodicCallback(affect2!, dt2; final_affect = true, initial_affect = true) + cb = CallbackSet(cb1, cb2) + # kp ud1 ud2 + prob = ODEProblem(foo!, [0.0], (0.0, 1.0), [1.0, 1.0, 1.0], callback = cb) + sol2 = solve(prob, Tsit5()) + + @test sol.u≈sol2.u atol=1e-6 + @test sol_nosplit.u≈sol2.u atol=1e-6 end - @equations begin - output.u ~ Hold(input.u) + + ## + @info "Testing hybrid system with components" + using ModelingToolkitStandardLibrary.Blocks + + dt = 0.05 + d = Clock(t, dt) + k = ShiftIndex() + + @mtkmodel DiscretePI begin + @components begin + input = RealInput() + output = RealOutput() + end + @parameters begin + kp = 1, [description = "Proportional gain"] + ki = 1, [description = "Integral gain"] + end + @variables begin + x(t) = 0, [description = "Integral state"] + u(t) + y(t) + end + @equations begin + x(k) ~ x(k - 1) + ki * u(k) * SampleTime() / dt + output.u(k) ~ y(k) + input.u(k) ~ u(k) + y(k) ~ x(k - 1) + kp * u(k) + end end -end -@mtkmodel ClosedLoop begin - @components begin - plant = FirstOrder(k = 0.3, T = 1) - sampler = Sampler() - holder = Holder() - controller = DiscretePI(kp = 2, ki = 2) - feedback = Feedback() - ref = Constant(k = 0.5) + @mtkmodel Sampler begin + @components begin + input = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ Sample(t, dt)(input.u) + end end - @equations begin - connect(ref.output, feedback.input1) - connect(feedback.output, controller.input) - connect(controller.output, holder.input) - connect(holder.output, plant.input) - connect(plant.output, sampler.input) - connect(sampler.output, feedback.input2) + + @mtkmodel ZeroOrderHold begin + @extend u, y = siso = Blocks.SISO() + @equations begin + y ~ Hold(u) + end end -end -## -@named model = ClosedLoop() -_model = complete(model) - -ci, varmap = infer_clocks(expand_connections(_model)) - -@test varmap[_model.plant.input.u] == Continuous() -@test varmap[_model.plant.u] == Continuous() -@test varmap[_model.plant.x] == Continuous() -@test varmap[_model.plant.y] == Continuous() -@test varmap[_model.plant.output.u] == Continuous() -@test varmap[_model.holder.output.u] == Continuous() -@test varmap[_model.sampler.input.u] == Continuous() -@test varmap[_model.controller.u] == d -@test varmap[_model.holder.input.u] == d -@test varmap[_model.controller.output.u] == d -@test varmap[_model.controller.y] == d -@test varmap[_model.feedback.input1.u] == d -@test varmap[_model.ref.output.u] == d -@test varmap[_model.controller.input.u] == d -@test varmap[_model.controller.x] == d -@test varmap[_model.sampler.output.u] == d -@test varmap[_model.feedback.output.u] == d -@test varmap[_model.feedback.input2.u] == d - -ssys = structural_simplify(model) - -Tf = 0.2 -timevec = 0:(d.dt):Tf - -import ControlSystemsBase as CS -import ControlSystemsBase: c2d, tf, feedback, lsim -# z = tf('z', d.dt) -# P = c2d(tf(0.3, [1, 1]), d.dt) -P = c2d(CS.ss([-1], [0.3], [1], 0), d.dt) -C = CS.ss([1], [2], [1], [2], d.dt) - -# Test the output of the continuous partition -G = feedback(P * C) -res = lsim(G, (x, t) -> [0.5], timevec) -y = res.y[:] - -# plant = FirstOrder(k = 0.3, T = 1) -# controller = DiscretePI(kp = 2, ki = 2) -# ref = Constant(k = 0.5) - -# ; model.controller.x(k-1) => 0.0 -prob = ODEProblem(ssys, - [model.plant.x => 0.0; model.controller.kp => 2.0; model.controller.ki => 2.0], - (0.0, Tf)) -int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) -@test int.ps[Hold(ssys.holder.input.u)] == 2 # constant output * kp issue https://github.com/SciML/ModelingToolkit.jl/issues/2356 -@test int.ps[ssys.controller.x] == 1 # c2d -@test int.ps[Sample(d)(ssys.sampler.input.u)] == 0 # disc state -sol = solve(prob, - Tsit5(), - kwargshandle = KeywordArgSilent, - abstol = 1e-8, - reltol = 1e-8) -@test_skip begin - # plot([y sol(timevec, idxs = model.plant.output.u)], m = :o, lab = ["CS" "MTK"]) + @mtkmodel ClosedLoop begin + @components begin + plant = FirstOrder(k = 0.3, T = 1) + sampler = Sampler() + holder = ZeroOrderHold() + controller = DiscretePI(kp = 2, ki = 2) + feedback = Feedback() + ref = Constant(k = 0.5) + end + @equations begin + connect(ref.output, feedback.input1) + connect(feedback.output, controller.input) + connect(controller.output, holder.input) + connect(holder.output, plant.input) + connect(plant.output, sampler.input) + connect(sampler.output, feedback.input2) + end + end ## - - @test sol(timevec, idxs = model.plant.output.u)≈y rtol=1e-8 # The output of the continuous partition is delayed exactly one sample - - # Test the output of the discrete partition - G = feedback(C, P) + @named model = ClosedLoop() + _model = complete(model) + + ci, varmap = infer_clocks(expand_connections(_model)) + + @test varmap[_model.plant.input.u] == Continuous() + @test varmap[_model.plant.u] == Continuous() + @test varmap[_model.plant.x] == Continuous() + @test varmap[_model.plant.y] == Continuous() + @test varmap[_model.plant.output.u] == Continuous() + @test varmap[_model.holder.output.u] == Continuous() + @test varmap[_model.sampler.input.u] == Continuous() + @test varmap[_model.controller.u] == d + @test varmap[_model.holder.input.u] == d + @test varmap[_model.controller.output.u] == d + @test varmap[_model.controller.y] == d + @test varmap[_model.feedback.input1.u] == d + @test varmap[_model.ref.output.u] == d + @test varmap[_model.controller.input.u] == d + @test varmap[_model.controller.x] == d + @test varmap[_model.sampler.output.u] == d + @test varmap[_model.feedback.output.u] == d + @test varmap[_model.feedback.input2.u] == d + + ssys = structural_simplify(model) + + Tf = 0.2 + timevec = 0:(d.dt):Tf + + import ControlSystemsBase as CS + import ControlSystemsBase: c2d, tf, feedback, lsim + # z = tf('z', d.dt) + # P = c2d(tf(0.3, [1, 1]), d.dt) + P = c2d(CS.ss([-1], [0.3], [1], 0), d.dt) + C = CS.ss([1], [2], [1], [2], d.dt) + + # Test the output of the continuous partition + G = feedback(P * C) res = lsim(G, (x, t) -> [0.5], timevec) y = res.y[:] - @test_broken sol(timevec .+ 1e-10, idxs = model.controller.output.u)≈y rtol=1e-8 # Broken due to discrete observed - # plot([y sol(timevec .+ 1e-12, idxs=model.controller.output.u)], lab=["CS" "MTK"]) - - # TODO: test the same system, but with the PI controller implemented as - # x(k) ~ x(k-1) + ki * u - # y ~ x(k-1) + kp * u - # Instead. This should be equivalent to the above, but gve me an error when I tried -end - -## Test continuous clock - -c = ModelingToolkit.SolverStepClock(t) -k = ShiftIndex(c) - -@mtkmodel CounterSys begin - @variables begin - count(t) = 0 - u(t) = 0 - ud(t) = 0 + # plant = FirstOrder(k = 0.3, T = 1) + # controller = DiscretePI(kp = 2, ki = 2) + # ref = Constant(k = 0.5) + + # ; model.controller.x(k-1) => 0.0 + prob = ODEProblem(ssys, + [model.plant.x => 0.0; model.controller.kp => 2.0; model.controller.ki => 2.0], + (0.0, Tf)) + int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) + @test_broken int.ps[Hold(ssys.holder.input.u)] == 2 # constant output * kp issue https://github.com/SciML/ModelingToolkit.jl/issues/2356 + @test int.ps[ssys.controller.x] == 1 # c2d + @test int.ps[Sample(d)(ssys.sampler.input.u)] == 0 # disc state + sol = solve(prob, + Tsit5(), + kwargshandle = KeywordArgSilent, + abstol = 1e-8, + reltol = 1e-8) + @test_skip begin + # plot([y sol(timevec, idxs = model.plant.output.u)], m = :o, lab = ["CS" "MTK"]) + + ## + + @test sol(timevec, idxs = model.plant.output.u)≈y rtol=1e-8 # The output of the continuous partition is delayed exactly one sample + + # Test the output of the discrete partition + G = feedback(C, P) + res = lsim(G, (x, t) -> [0.5], timevec) + y = res.y[:] + + @test_broken sol(timevec .+ 1e-10, idxs = model.controller.output.u)≈y rtol=1e-8 # Broken due to discrete observed + # plot([y sol(timevec .+ 1e-12, idxs=model.controller.output.u)], lab=["CS" "MTK"]) + + # TODO: test the same system, but with the PI controller implemented as + # x(k) ~ x(k-1) + ki * u + # y ~ x(k-1) + kp * u + # Instead. This should be equivalent to the above, but gve me an error when I tried end - @equations begin - ud ~ Sample(c)(u) - count ~ ud(k - 1) - end -end -@mtkmodel FirstOrderSys begin - @variables begin - x(t) = 0 - end - @equations begin - D(x) ~ -x + sin(t) + ## Test continuous clock + + c = ModelingToolkit.SolverStepClock(t) + k = ShiftIndex(c) + + @mtkmodel CounterSys begin + @variables begin + count(t) = 0 + u(t) = 0 + ud(t) = 0 + end + @equations begin + ud ~ Sample(c)(u) + count ~ ud(k - 1) + end end -end -@mtkmodel FirstOrderWithStepCounter begin - @components begin - counter = CounterSys() - fo = FirstOrderSys() + @mtkmodel FirstOrderSys begin + @variables begin + x(t) = 0 + end + @equations begin + D(x) ~ -x + sin(t) + end end - @equations begin - counter.u ~ fo.x + + @mtkmodel FirstOrderWithStepCounter begin + @components begin + counter = CounterSys() + firstorder = FirstOrderSys() + end + @equations begin + counter.u ~ firstorder.x + end end -end -@mtkbuild model = FirstOrderWithStepCounter() -prob = ODEProblem(model, [], (0.0, 10.0)) -sol = solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) - -@test sol.prob.kwargs[:disc_saved_values][1].t == sol.t[1:2:end] # Test that the discrete-tiem system executed at every step of the continuous solver. The solver saves each time step twice, one state value before discrete affect and one after. -@test_nowarn ModelingToolkit.build_explicit_observed_function( - model, model.counter.ud)(sol.u[1], prob.p..., sol.t[1]) - -@variables x(t)=1.0 y(t)=1.0 -eqs = [D(y) ~ Hold(x) - x ~ x(k - 1) + x(k - 2)] -@mtkbuild sys = ODESystem(eqs, t) -prob = ODEProblem(sys, [], (0.0, 10.0)) -int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) -@test int.ps[x] == 2.0 -@test int.ps[x(k - 1)] == 1.0 - -@test_throws ErrorException ODEProblem(sys, [], (0.0, 10.0), [x => 2.0]) -prob = ODEProblem(sys, [], (0.0, 10.0), [x(k - 1) => 2.0]) -int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) -@test int.ps[x] == 3.0 -@test int.ps[x(k - 1)] == 2.0 + @mtkbuild model = FirstOrderWithStepCounter() + prob = ODEProblem(model, [], (0.0, 10.0)) + sol = solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) + + @test sol.prob.kwargs[:disc_saved_values][1].t == sol.t[1:2:end] # Test that the discrete-time system executed at every step of the continuous solver. The solver saves each time step twice, one state value before discrete affect and one after. + @test_nowarn ModelingToolkit.build_explicit_observed_function( + model, model.counter.ud)(sol.u[1], prob.p..., sol.t[1]) + + @variables x(t)=1.0 y(t)=1.0 + eqs = [D(y) ~ Hold(x) + x ~ x(k - 1) + x(k - 2)] + @mtkbuild sys = ODESystem(eqs, t) + prob = ODEProblem(sys, [], (0.0, 10.0)) + int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) + @test int.ps[x] == 2.0 + @test int.ps[x(k - 1)] == 1.0 + + @test_throws ErrorException ODEProblem(sys, [], (0.0, 10.0), [x => 2.0]) + prob = ODEProblem(sys, [], (0.0, 10.0), [x(k - 1) => 2.0]) + int = init(prob, Tsit5(); kwargshandle = KeywordArgSilent) + @test int.ps[x] == 3.0 + @test int.ps[x(k - 1)] == 2.0 +end diff --git a/test/components.jl b/test/components.jl index c58ac7b3d0..037e089df1 100644 --- a/test/components.jl +++ b/test/components.jl @@ -101,6 +101,10 @@ let prob2 = ODEProblem(sys2, [source.p.i => 0.0], (0, 10.0), guesses = u0) sol2 = solve(prob2, Rosenbrock23()) @test sol2[source.p.i] ≈ sol2[rc_model2.source.p.i] ≈ -sol2[capacitor.i] + + prob3 = ODEProblem(sys2, [], (0, 10.0), guesses = u0) + sol3 = solve(prob2, Rosenbrock23()) + @test sol3[unknowns(rc_model2), end] ≈ sol2[unknowns(rc_model2), end] end # Outer/inner connections @@ -294,3 +298,25 @@ rc_eqs = [connect(capacitor.n, resistor.p) sys = structural_simplify(rc_model) prob = ODEProblem(sys, u0, (0, 10.0)) sol = solve(prob, Tsit5()) + +@testset "docstrings (#1155)" begin + """ + Hey there, Pin1! + """ + @connector function Pin1(; name) + @independent_variables t + sts = @variables v(t)=1.0 i(t)=1.0 + ODESystem(Equation[], t, sts, []; name = name) + end + @test string(Base.doc(Pin1)) == "Hey there, Pin1!\n" + + """ + Hey there, Pin2! + """ + @component function Pin2(; name) + @independent_variables t + sts = @variables v(t)=1.0 i(t)=1.0 + ODESystem(Equation[], t, sts, []; name = name) + end + @test string(Base.doc(Pin2)) == "Hey there, Pin2!\n" +end diff --git a/test/constants.jl b/test/constants.jl index 2427638703..bfdc83bafc 100644 --- a/test/constants.jl +++ b/test/constants.jl @@ -6,7 +6,8 @@ UMT = ModelingToolkit.UnitfulUnitCheck @constants a = 1 @test_throws MT.ArgumentError @constants b -@variables t x(t) w(t) +@independent_variables t +@variables x(t) w(t) D = Differential(t) eqs = [D(x) ~ a] @named sys = ODESystem(eqs, t) @@ -28,7 +29,8 @@ simp = structural_simplify(sys) @constants β=1 [unit = u"m/s"] UMT.get_unit(β) @test MT.isconstant(β) -@variables t [unit = u"s"] x(t) [unit = u"m"] +@independent_variables t [unit = u"s"] +@variables x(t) [unit = u"m"] D = Differential(t) eqs = [D(x) ~ β] @named sys = ODESystem(eqs, t) diff --git a/test/dde.jl b/test/dde.jl index 72db795362..3b303b8b7f 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -1,4 +1,6 @@ using ModelingToolkit, DelayDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D + p0 = 0.2; q0 = 0.3; v0 = 1; @@ -29,14 +31,13 @@ prob2 = DDEProblem(bc_model, u0, h2, tspan, constant_lags = lags) sol2 = solve(prob2, alg, reltol = 1e-7, abstol = 1e-10) @parameters p0=0.2 p1=0.2 q0=0.3 q1=0.3 v0=1 v1=1 d0=5 d1=1 d2=1 beta0=1 beta1=1 -@variables t x₀(t) x₁(t) x₂(..) +@variables x₀(t) x₁(t) x₂(..) tau = 1 -D = Differential(t) eqs = [D(x₀) ~ (v0 / (1 + beta0 * (x₂(t - tau)^2))) * (p0 - q0) * x₀ - d0 * x₀ D(x₁) ~ (v0 / (1 + beta0 * (x₂(t - tau)^2))) * (1 - p0 + q0) * x₀ + (v1 / (1 + beta1 * (x₂(t - tau)^2))) * (p1 - q1) * x₁ - d1 * x₁ D(x₂(t)) ~ (v1 / (1 + beta1 * (x₂(t - tau)^2))) * (1 - p1 + q1) * x₁ - d2 * x₂(t)] -@named sys = System(eqs) +@mtkbuild sys = System(eqs, t) prob = DDEProblem(sys, [x₀ => 1.0, x₁ => 1.0, x₂(t) => 1.0], tspan, @@ -70,21 +71,17 @@ prob = SDDEProblem(hayes_modelf, hayes_modelg, [1.0], h, tspan, pmul; constant_lags = (pmul[1],)); sol = solve(prob, RKMil()) -@variables t x(..) +@variables x(..) @parameters a=-4.0 b=-2.0 c=10.0 α=-1.3 β=-1.2 γ=1.1 -D = Differential(t) @brownian η τ = 1.0 eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] -@named sys = System(eqs) -sys = structural_simplify(sys) +@mtkbuild sys = System(eqs, t) @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] @test isequal(ModelingToolkit.get_noiseeqs(sys), [α * x(t) + γ;;]) prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); @test_nowarn sol_mtk = solve(prob_mtk, RKMil()) -@variables t -D = Differential(t) @parameters x(..) a function oscillator(; name, k = 1.0, τ = 0.01) @@ -93,7 +90,7 @@ function oscillator(; name, k = 1.0, τ = 0.01) eqs = [D(x(t)) ~ y, D(y) ~ -k * x(t - τ) + jcn, delx ~ x(t - τ)] - return System(eqs; name = name) + return System(eqs, t; name = name) end systems = @named begin diff --git a/test/direct.jl b/test/direct.jl index b7b18f14cc..70e1babe3f 100644 --- a/test/direct.jl +++ b/test/direct.jl @@ -41,7 +41,7 @@ for i in 1:3 @test eval(ModelingToolkit.toexpr.(eqs)[i]) == eval(simpexpr[i]) end -@parameters t σ ρ β +@parameters σ ρ β @variables x y z ∂ = ModelingToolkit.jacobian(eqs, [x, y, z]) for i in 1:3 @@ -59,7 +59,8 @@ reference_jac = sparse(ModelingToolkit.jacobian(du, [x, y, z])) findnz(reference_jac)[[1, 2]] let - @variables t x(t) y(t) z(t) + @independent_variables t + @variables x(t) y(t) z(t) @test ModelingToolkit.exprs_occur_in([x, y, z], x^2 * y) == [true, true, false] end @@ -196,7 +197,7 @@ test_worldage() let @register_symbolic foo(x) - @variables t + @independent_variables t D = Differential(t) @test isequal(expand_derivatives(D(foo(t))), D(foo(t))) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 3c2238bca3..f8ed0a911f 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -234,3 +234,40 @@ prob = DiscreteProblem(de, [], (0, 10)) prob = DiscreteProblem(de, [x(k - 1) => 2.0], (0, 10)) @test prob[x] == 3.0 @test prob[x(k - 1)] == 2.0 + +# Issue#2585 +getdata(buffer, t) = buffer[mod1(Int(t), length(buffer))] +@register_symbolic getdata(buffer::Vector, t) +k = ShiftIndex(t) +function SampledData(; name, buffer) + L = length(buffer) + pars = @parameters begin + buffer[1:L] = buffer + end + @variables output(t) time(t) + eqs = [time ~ time(k - 1) + 1 + output ~ getdata(buffer, time)] + return DiscreteSystem(eqs, t; name) +end +function System(; name, buffer) + @named y_sys = SampledData(; buffer = buffer) + pars = @parameters begin + α = 0.5, [description = "alpha"] + β = 0.5, [description = "beta"] + end + vars = @variables y(t)=0.0 y_shk(t)=0.0 + + eqs = [y_shk ~ y_sys.output + # y[t] = 0.5 * y[t - 1] + 0.5 * y[t + 1] + y_shk[t] + y(k - 1) ~ α * y(k - 2) + (β * y(k) + y_shk(k - 1))] + + DiscreteSystem(eqs, t, vars, pars; systems = [y_sys], name = name) +end + +@test_nowarn @mtkbuild sys = System(; buffer = ones(10)) + +# Ensure discrete systems with algebraic equations throw +@variables x(t) y(t) +k = ShiftIndex(t) +@named sys = DiscreteSystem([x ~ x^2 + y^2, y ~ x(k - 1) + y(k - 1)], t) +@test_throws ["algebraic equations", "not yet supported"] structural_simplify(sys) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 66cf46eb96..e37f07e809 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -148,19 +148,27 @@ sol = solve(prob, Rodas5P()) Sf, simplified_sys = Blocks.get_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) p = ModelingToolkit.MTKParameters(simplified_sys, op) -matrices1 = Sf(x, p, 0) -matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API -@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] -nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API -@test matrices2.A ≈ nsys.A +# If this somehow passes, mention it on +# https://github.com/SciML/ModelingToolkit.jl/issues/2786 +@test_broken begin + matrices1 = Sf(x, p, 0) + matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API + @test matrices1.f_x ≈ matrices2.A[1:7, 1:7] + nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API + @test matrices2.A ≈ nsys.A +end # Test the same thing for comp sensitivities Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) p = ModelingToolkit.MTKParameters(simplified_sys, op) -matrices1 = Sf(x, p, 0) -matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API -@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] -nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API -@test matrices2.A ≈ nsys.A +# If this somehow passes, mention it on +# https://github.com/SciML/ModelingToolkit.jl/issues/2786 +@test_broken begin + matrices1 = Sf(x, p, 0) + matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API + @test matrices1.f_x ≈ matrices2.A[1:7, 1:7] + nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API + @test matrices2.A ≈ nsys.A +end diff --git a/test/downstream/linearization_dd.jl b/test/downstream/linearization_dd.jl new file mode 100644 index 0000000000..11dc65f619 --- /dev/null +++ b/test/downstream/linearization_dd.jl @@ -0,0 +1,64 @@ +## Test that dummy_derivatives can be set to zero +# The call to Link(; m = 0.2, l = 10, I = 1, g = -9.807) hangs forever on Julia v1.6 +using LinearAlgebra +using ModelingToolkit +using ModelingToolkitStandardLibrary +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkitStandardLibrary.Mechanical.MultiBody2D +using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition +using Test + +using ControlSystemsMTK +using ControlSystemsMTK.ControlSystemsBase: sminreal, minreal, poles +connect = ModelingToolkit.connect + +@independent_variables t +D = Differential(t) + +@named link1 = Link(; m = 0.2, l = 10, I = 1, g = -9.807) +@named cart = TranslationalPosition.Mass(; m = 1, s = 0) +@named fixed = Fixed() +@named force = Force(use_support = false) + +eqs = [connect(link1.TX1, cart.flange) + connect(cart.flange, force.flange) + connect(link1.TY1, fixed.flange)] + +@named model = ODESystem(eqs, t, [], []; systems = [link1, cart, force, fixed]) +def = ModelingToolkit.defaults(model) +def[cart.s] = 10 +def[cart.v] = 0 +def[link1.A] = -pi / 2 +def[link1.dA] = 0 +lin_outputs = [cart.s, cart.v, link1.A, link1.dA] +lin_inputs = [force.f.u] + +@test_broken begin + @info "named_ss" + G = named_ss(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, + allow_input_derivatives = true, zero_dummy_der = true) + G = sminreal(G) + @info "minreal" + G = minreal(G) + @info "poles" + ps = poles(G) + + @test minimum(abs, ps) < 1e-6 + @test minimum(abs, complex(0, 1.3777260367206716) .- ps) < 1e-10 + + lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, + allow_input_derivatives = true, zero_dummy_der = true) + lsyss, sysss = ModelingToolkit.linearize_symbolic(model, lin_inputs, lin_outputs; + allow_input_derivatives = true) + + dummyder = setdiff(unknowns(sysss), unknowns(model)) + def = merge(ModelingToolkit.guesses(model), def, Dict(x => 0.0 for x in dummyder)) + def[link1.fy1] = -def[link1.g] * def[link1.m] + + @test substitute(lsyss.A, def) ≈ lsys.A + # We cannot pivot symbolically, so the part where a linear solve is required + # is not reliable. + @test substitute(lsyss.B, def)[1:6, 1] ≈ lsys.B[1:6, 1] + @test substitute(lsyss.C, def) ≈ lsys.C + @test substitute(lsyss.D, def) ≈ lsys.D +end diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index c1aa6f6724..c961b188eb 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -1,8 +1,9 @@ using ModelingToolkit, Test # r is an input, and y is an output. -@variables t x(t)=0 y(t)=0 u(t)=0 r(t)=0 -@variables t x(t)=0 y(t)=0 u(t)=0 r(t)=0 [input = true] +@independent_variables t +@variables x(t)=0 y(t)=0 u(t)=0 r(t)=0 +@variables x(t)=0 y(t)=0 u(t)=0 r(t)=0 [input = true] @parameters kp = 1 D = Differential(t) @@ -120,10 +121,14 @@ lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) lsyss, _ = ModelingToolkit.linearize_symbolic(pid, [reference.u, measurement.u], [ctr_output.u]) -@test substitute(lsyss.A, ModelingToolkit.defaults(pid)) == lsys.A -@test substitute(lsyss.B, ModelingToolkit.defaults(pid)) == lsys.B -@test substitute(lsyss.C, ModelingToolkit.defaults(pid)) == lsys.C -@test substitute(lsyss.D, ModelingToolkit.defaults(pid)) == lsys.D +@test substitute( + lsyss.A, ModelingToolkit.defaults_and_guesses(pid)) == lsys.A +@test substitute( + lsyss.B, ModelingToolkit.defaults_and_guesses(pid)) == lsys.B +@test substitute( + lsyss.C, ModelingToolkit.defaults_and_guesses(pid)) == lsys.C +@test substitute( + lsyss.D, ModelingToolkit.defaults_and_guesses(pid)) == lsys.D # Test with the reverse desired unknown order as well to verify that similarity transform and reoreder_unknowns really works lsys = ModelingToolkit.reorder_unknowns(lsys, unknowns(ssys), reverse(desired_order)) @@ -192,66 +197,112 @@ lsys, ssys = linearize(sat, [u], [y]; op = Dict(u => 2)) @test isempty(lsys.C) @test lsys.D[] == 0 -## Test that dummy_derivatives can be set to zero -if VERSION >= v"1.8" - # The call to Link(; m = 0.2, l = 10, I = 1, g = -9.807) hangs forever on Julia v1.6 - using LinearAlgebra - using ModelingToolkit - using ModelingToolkitStandardLibrary - using ModelingToolkitStandardLibrary.Blocks - using ModelingToolkitStandardLibrary.Mechanical.MultiBody2D - using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition - - using ControlSystemsMTK - using ControlSystemsMTK.ControlSystemsBase: sminreal, minreal, poles - connect = ModelingToolkit.connect - - @parameters t - D = Differential(t) +# Test case when unknowns in system do not have equations in initialization system +using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks: Add, Sine, PID, SecondOrder, Step, RealOutput +using ModelingToolkit: connect + +# Parameters +m1 = 1 +m2 = 1 +k = 1000 # Spring stiffness +c = 10 # Damping coefficient +@named inertia1 = Inertia(; J = m1) +@named inertia2 = Inertia(; J = m2) +@named spring = Spring(; c = k) +@named damper = Damper(; d = c) +@named torque = Torque() + +function SystemModel(u = nothing; name = :model) + 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)] + if u !== nothing + push!(eqs, connect(torque.tau, u.output)) + return ODESystem(eqs, t; + systems = [ + torque, + inertia1, + inertia2, + spring, + damper, + u + ], + name) + end + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) +end - @named link1 = Link(; m = 0.2, l = 10, I = 1, g = -9.807) - @named cart = TranslationalPosition.Mass(; m = 1, s = 0) - @named fixed = Fixed() - @named force = Force(use_support = false) - - eqs = [connect(link1.TX1, cart.flange) - connect(cart.flange, force.flange) - connect(link1.TY1, fixed.flange)] - - @named model = ODESystem(eqs, t, [], []; systems = [link1, cart, force, fixed]) - def = ModelingToolkit.defaults(model) - def[cart.s] = 10 - def[cart.v] = 0 - def[link1.A] = -pi / 2 - def[link1.dA] = 0 - lin_outputs = [cart.s, cart.v, link1.A, link1.dA] - lin_inputs = [force.f.u] - - @info "named_ss" - G = named_ss(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, - allow_input_derivatives = true, zero_dummy_der = true) - G = sminreal(G) - @info "minreal" - G = minreal(G) - @info "poles" - ps = poles(G) - - @test minimum(abs, ps) < 1e-6 - @test minimum(abs, complex(0, 1.3777260367206716) .- ps) < 1e-10 - - lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, - allow_input_derivatives = true, zero_dummy_der = true) - lsyss, sysss = ModelingToolkit.linearize_symbolic(model, lin_inputs, lin_outputs; - allow_input_derivatives = true) - - dummyder = setdiff(unknowns(sysss), unknowns(model)) - def = merge(def, Dict(x => 0.0 for x in dummyder)) - def[link1.fy1] = -def[link1.g] * def[link1.m] - - @test substitute(lsyss.A, def) ≈ lsys.A - # We cannot pivot symbolically, so the part where a linear solve is required - # is not reliable. - @test substitute(lsyss.B, def)[1:6, 1] ≈ lsys.B[1:6, 1] - @test substitute(lsyss.C, def) ≈ lsys.C - @test substitute(lsyss.D, def) ≈ lsys.D +@named r = Step(start_time = 0) +model = SystemModel() +@named pid = PID(k = 100, Ti = 0.5, Td = 1) +@named filt = SecondOrder(d = 0.9, w = 10) +@named sensor = AngleSensor() +@named er = Add(k2 = -1) + +connections = [connect(r.output, :r, filt.input) + connect(filt.output, er.input1) + connect(pid.ctr_output, :u, model.torque.tau) + connect(model.inertia2.flange_b, sensor.flange) + connect(sensor.phi, :y, er.input2) + connect(er.output, :e, pid.err_input)] + +closed_loop = ODESystem(connections, t, systems = [model, pid, filt, sensor, r, er], + name = :closed_loop, defaults = [ + model.inertia1.phi => 0.0, + model.inertia2.phi => 0.0, + model.inertia1.w => 0.0, + model.inertia2.w => 0.0, + filt.x => 0.0, + filt.xd => 0.0 + ]) + +@test_nowarn linearize(closed_loop, :r, :y) + +# https://discourse.julialang.org/t/mtk-change-in-linearize/115760/3 +@mtkmodel Tank_noi begin + # Model parameters + @parameters begin + ρ = 1, [description = "Liquid density"] + A = 5, [description = "Cross sectional tank area"] + K = 5, [description = "Effluent valve constant"] + h_ς = 3, [description = "Scaling level in valve model"] + end + # Model variables, with initial values needed + @variables begin + m(t) = 1.5 * ρ * A, [description = "Liquid mass"] + md_i(t), [description = "Influent mass flow rate"] + md_e(t), [description = "Effluent mass flow rate"] + V(t), [description = "Liquid volume"] + h(t), [description = "level"] + end + # Providing model equations + @equations begin + D(m) ~ md_i - md_e + m ~ ρ * V + V ~ A * h + md_e ~ K * sqrt(h / h_ς) + end end + +@named tank_noi = Tank_noi() +@unpack md_i, h, m = tank_noi +m_ss = 2.4000000003229878 +@test_nowarn linearize(tank_noi, [md_i], [h]; op = Dict(m => m_ss, md_i => 2)) + +# Test initialization +@variables x(t) y(t) u(t)=1.0 +@parameters p = 1.0 +eqs = [D(x) ~ p * u, x ~ y] +@named sys = ODESystem(eqs, t) + +matrices1, _ = linearize(sys, [u], []; op = Dict(x => 2.0)) +matrices2, _ = linearize(sys, [u], []; op = Dict(y => 2.0)) +@test matrices1 == matrices2 + +# Ensure parameter values passed as `Dict` are respected +linfun, _ = linearization_function(sys, [u], []; op = Dict(x => 2.0)) +matrices = linfun([1.0], Dict(p => 3.0), 1.0) +# this would be 1 if the parameter value isn't respected +@test matrices.f_u[] == 3.0 diff --git a/test/equation_type_accessors.jl b/test/equation_type_accessors.jl new file mode 100644 index 0000000000..f118784f44 --- /dev/null +++ b/test/equation_type_accessors.jl @@ -0,0 +1,182 @@ +# Fetch packages. +using ModelingToolkit +import ModelingToolkit: get_systems, namespace_equations +import ModelingToolkit: is_alg_equation, is_diff_equation +import ModelingToolkit: t_nounits as t, D_nounits as D, wrap, get_eqs + +# Creates equations. +@variables X(t) Y(t) Z(t) +@parameters a b c d +eq1 = X^Z - Z^(X + 1) ~ log(X - a + b) * Y +eq2 = X ~ Y^(X + 1) +eq3 = a + b + c + d ~ X * (Y + d * (Y + Z)) +eq4 = X ~ sqrt(a + Z) + t +eq5 = D(D(X)) ~ a^(2Y) + 3Z * t - 6 +eq6 = X * (Z - Z * (b + X)) ~ c^(X + D(Y)) +eq7 = sqrt(X + c) ~ 2 * (Y + log(a + D(Z))) +eq8 = -0.1 ~ D(Z) + X + +@test is_alg_equation(eq1) +@test is_alg_equation(eq2) +@test is_alg_equation(eq3) +@test is_alg_equation(eq4) +@test !is_alg_equation(eq5) +@test !is_alg_equation(eq6) +@test !is_alg_equation(eq7) +@test !is_alg_equation(eq8) + +@test !is_diff_equation(eq1) +@test !is_diff_equation(eq2) +@test !is_diff_equation(eq3) +@test !is_diff_equation(eq4) +@test is_diff_equation(eq5) +@test is_diff_equation(eq6) +@test is_diff_equation(eq7) +@test is_diff_equation(eq8) + +# Creates systems. +eqs1 = [X * Y + a ~ Z^3 - X * log(b + Y) + X ~ Z * Y * X + a + b + c * sin(X) + sin(Y) ~ d * (a + X * (b + Y * (c + Z)))] +eqs2 = [X + Y + c ~ b * X^(X + Z + a) + D(X) ~ a * Y + b * X + c * Z + D(Z) + Z * Y ~ X - log(Z)] +eqs3 = [D(X) ~ sqrt(X + b) + sqrt(Z + c) + 2Z * (Z + Y) ~ D(Y) * log(a) + D(Z) + c * X ~ b / (X + Y^d) + D(Z)] +@named osys1 = ODESystem(eqs1, t) +@named osys2 = ODESystem(eqs2, t) +@named osys3 = ODESystem(eqs3, t) + +# Test `has...` for non-composed systems. +@test has_alg_equations(osys1) +@test has_alg_equations(osys2) +@test !has_alg_equations(osys3) +@test has_alg_eqs(osys1) +@test has_alg_eqs(osys2) +@test !has_alg_eqs(osys3) +@test !has_diff_equations(osys1) +@test has_diff_equations(osys2) +@test has_diff_equations(osys3) +@test !has_diff_eqs(osys1) +@test has_diff_eqs(osys2) +@test has_diff_eqs(osys3) + +# Test getters for non-composed systems. +isequal(alg_equations(osys1), eqs1) +isequal(alg_equations(osys2), eqs2[1:1]) +isequal(alg_equations(osys3), []) +isequal(get_alg_eqs(osys1), eqs1) +isequal(get_alg_eqs(osys2), eqs2[1:1]) +isequal(get_alg_eqs(osys3), []) +isequal(diff_equations(osys1), []) +isequal(diff_equations(osys2), eqs2[2:3]) +isequal(diff_equations(osys3), eqs3) +isequal(get_diff_eqs(osys1), []) +isequal(get_diff_eqs(osys2), eqs2[2:3]) +isequal(get_diff_eqs(osys3), eqs3) + +# Creates composed systems. +osys1_1 = compose(osys1, [osys1]) +osys1_12 = compose(osys1, [osys1, osys2]) +osys1_12_1 = compose(osys1, [osys1, compose(osys2, [osys1])]) +osys3_2 = compose(osys3, [osys2]) +osys3_33 = compose(osys3, [osys3, osys3]) + +# Test `has...` for composed systems. +@test has_alg_equations(osys1_1) +@test !has_diff_equations(osys1_1) +@test has_alg_eqs(osys1_1) +@test !has_diff_eqs(osys1_1) +@test has_alg_equations(get_systems(osys1_1)[1]) +@test !has_diff_equations(get_systems(osys1_1)[1]) +@test has_alg_eqs(get_systems(osys1_1)[1]) +@test !has_diff_eqs(get_systems(osys1_1)[1]) + +@test has_alg_equations(osys1_12) +@test has_diff_equations(osys1_12) +@test has_alg_eqs(osys1_12) +@test !has_diff_eqs(osys1_12) +@test has_alg_equations(get_systems(osys1_12)[1]) +@test !has_diff_equations(get_systems(osys1_12)[1]) +@test has_alg_eqs(get_systems(osys1_12)[1]) +@test !has_diff_eqs(get_systems(osys1_12)[1]) +@test has_alg_equations(get_systems(osys1_12)[2]) +@test has_diff_equations(get_systems(osys1_12)[2]) +@test has_alg_eqs(get_systems(osys1_12)[2]) +@test has_diff_eqs(get_systems(osys1_12)[2]) + +@test has_alg_equations(osys1_12_1) +@test has_diff_equations(osys1_12_1) +@test has_alg_eqs(osys1_12_1) +@test !has_diff_eqs(osys1_12_1) +@test has_alg_equations(get_systems(osys1_12_1)[1]) +@test !has_diff_equations(get_systems(osys1_12_1)[1]) +@test has_alg_eqs(get_systems(osys1_12_1)[1]) +@test !has_diff_eqs(get_systems(osys1_12_1)[1]) +@test has_alg_equations(get_systems(osys1_12_1)[2]) +@test has_diff_equations(get_systems(osys1_12_1)[2]) +@test has_alg_eqs(get_systems(osys1_12_1)[2]) +@test has_diff_eqs(get_systems(osys1_12_1)[2]) +@test has_alg_equations(get_systems(get_systems(osys1_12_1)[2])[1]) +@test !has_diff_equations(get_systems(get_systems(osys1_12_1)[2])[1]) +@test has_alg_eqs(get_systems(get_systems(osys1_12_1)[2])[1]) +@test !has_diff_eqs(get_systems(get_systems(osys1_12_1)[2])[1]) + +@test has_alg_equations(osys3_2) +@test has_diff_equations(osys3_2) +@test !has_alg_eqs(osys3_2) +@test has_diff_eqs(osys3_2) +@test has_alg_equations(get_systems(osys3_2)[1]) +@test has_diff_equations(get_systems(osys3_2)[1]) +@test has_alg_eqs(get_systems(osys3_2)[1]) +@test has_diff_eqs(get_systems(osys3_2)[1]) + +@test !has_alg_equations(osys3_33) +@test has_diff_equations(osys3_33) +@test !has_alg_eqs(osys3_33) +@test has_diff_eqs(osys3_33) +@test !has_alg_equations(get_systems(osys3_33)[1]) +@test has_diff_equations(get_systems(osys3_33)[1]) +@test !has_alg_eqs(get_systems(osys3_33)[1]) +@test has_diff_eqs(get_systems(osys3_33)[1]) +@test !has_alg_equations(get_systems(osys3_33)[2]) +@test has_diff_equations(get_systems(osys3_33)[2]) +@test !has_alg_eqs(get_systems(osys3_33)[2]) +@test has_diff_eqs(get_systems(osys3_33)[2]) + +# Test getters for composed systems. +ns_eqs1 = namespace_equations(osys1) +ns_eqs2 = namespace_equations(osys2) +ns_eqs3 = namespace_equations(osys3) + +isequal(alg_equations(osys1_1), vcat(eqs1, ns_eqs1)) +isequal(diff_equations(osys1_1), []) +isequal(get_alg_eqs(osys1_1), eqs1) +isequal(get_diff_eqs(osys1_1), []) +isequal(alg_equations(get_systems(osys1_1)[1]), eqs1) +isequal(diff_equations(get_systems(osys1_1)[1]), []) +isequal(get_alg_eqs(get_systems(osys1_1)[1]), eqs1) +isequal(get_diff_eqs(get_systems(osys1_1)[1]), []) + +isequal(alg_equations(osys1_12), vcat(eqs1, ns_eqs1, filter(is_alg_equation, ns_eqs2))) +isequal(diff_equations(osys1_12), filter(is_diff_equation, ns_eqs2)) +isequal(get_alg_eqs(osys1_12), eqs1) +isequal(get_diff_eqs(osys1_12), []) +isequal(alg_equations(get_systems(osys1_12)[1]), eqs1) +isequal(diff_equations(get_systems(osys1_12)[1]), []) +isequal(get_alg_eqs(get_systems(osys1_12)[1]), eqs1) +isequal(get_diff_eqs(get_systems(osys1_12)[1]), []) +isequal(alg_equations(get_systems(osys1_12)[2]), eqs2[1:1]) +isequal(diff_equations(get_systems(osys1_12)[2]), eqs2[2:3]) +isequal(get_alg_eqs(get_systems(osys1_12)[2]), eqs2[1:1]) +isequal(get_diff_eqs(get_systems(osys1_12)[2]), eqs2[2:3]) + +isequal(alg_equations(osys3_2), vcat(filter(is_alg_equation, ns_eqs2))) +isequal(diff_equations(osys3_2), vcat(eqs3, filter(is_diff_equation, ns_eqs2))) +isequal(get_alg_eqs(osys3_2), []) +isequal(get_diff_eqs(osys3_2), eqs3) +isequal(alg_equations(get_systems(osys3_2)[1]), eqs2[1:1]) +isequal(diff_equations(get_systems(osys3_2)[1]), eqs2[2:3]) +isequal(get_alg_eqs(get_systems(osys3_2)[1]), eqs2[1:1]) +isequal(get_diff_eqs(get_systems(osys3_2)[1]), eqs2[2:3]) diff --git a/test/guess_propagation.jl b/test/guess_propagation.jl new file mode 100644 index 0000000000..738e930adc --- /dev/null +++ b/test/guess_propagation.jl @@ -0,0 +1,110 @@ +using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit: D, t_nounits as t +using Test + +# Standard case + +@variables x(t) [guess = 2] +@variables y(t) +eqs = [D(x) ~ 1 + x ~ y] +initialization_eqs = [1 ~ exp(1 + x)] + +@named sys = ODESystem(eqs, t; initialization_eqs) +sys = complete(structural_simplify(sys)) +tspan = (0.0, 0.2) +prob = ODEProblem(sys, [], tspan, []) + +@test prob.f.initializeprob[y] == 2.0 +@test prob.f.initializeprob[x] == 2.0 +sol = solve(prob.f.initializeprob; show_trace = Val(true)) + +# Guess via observed + +@variables x(t) +@variables y(t) [guess = 2] +eqs = [D(x) ~ 1 + x ~ y] +initialization_eqs = [1 ~ exp(1 + x)] + +@named sys = ODESystem(eqs, t; initialization_eqs) +sys = complete(structural_simplify(sys)) +tspan = (0.0, 0.2) +prob = ODEProblem(sys, [], tspan, []) + +@test prob.f.initializeprob[x] == 2.0 +@test prob.f.initializeprob[y] == 2.0 +sol = solve(prob.f.initializeprob; show_trace = Val(true)) + +# Guess via parameter + +@parameters a = -1.0 +@variables x(t) [guess = a] + +eqs = [D(x) ~ a] + +initialization_eqs = [1 ~ exp(1 + x)] + +@named sys = ODESystem(eqs, t; initialization_eqs) +sys = complete(structural_simplify(sys)) + +tspan = (0.0, 0.2) +prob = ODEProblem(sys, [], tspan, []) + +@test prob.f.initializeprob[x] == -1.0 +sol = solve(prob.f.initializeprob; show_trace = Val(true)) + +# Guess via observed parameter + +@parameters a = -1.0 +@variables x(t) +@variables y(t) [guess = a] + +eqs = [D(x) ~ a, + y ~ x] + +initialization_eqs = [1 ~ exp(1 + x)] + +@named sys = ODESystem(eqs, t; initialization_eqs) +sys = complete(structural_simplify(sys)) + +tspan = (0.0, 0.2) +prob = ODEProblem(sys, [], tspan, []) + +@test prob.f.initializeprob[x] == -1.0 +sol = solve(prob.f.initializeprob; show_trace = Val(true)) + +# Test parameters + defaults +# https://github.com/SciML/ModelingToolkit.jl/issues/2774 + +@parameters x0 +@variables x(t) +@variables y(t) = x +@mtkbuild sys = ODESystem([x ~ x0, D(y) ~ x], t) +prob = ODEProblem(sys, [], (0.0, 1.0), [x0 => 1.0]) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) +@variables y(t) = x0 +@mtkbuild sys = ODESystem([x ~ x0, D(y) ~ x], t) +prob = ODEProblem(sys, [], (0.0, 1.0), [x0 => 1.0]) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) +@variables y(t) = x0 +@mtkbuild sys = ODESystem([x ~ y, D(y) ~ x], t) +prob = ODEProblem(sys, [], (0.0, 1.0), [x0 => 1.0]) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) = x0 +@variables y(t) = x +@mtkbuild sys = ODESystem([x ~ y, D(y) ~ x], t) +prob = ODEProblem(sys, [], (0.0, 1.0), [x0 => 1.0]) +@test prob[x] == 1.0 +@test prob[y] == 1.0 diff --git a/test/hierarchical_initialization_eqs.jl b/test/hierarchical_initialization_eqs.jl new file mode 100644 index 0000000000..82dc3cb566 --- /dev/null +++ b/test/hierarchical_initialization_eqs.jl @@ -0,0 +1,147 @@ +using ModelingToolkit, OrdinaryDiffEq + +t = only(@parameters(t)) +D = Differential(t) +""" +A simple linear resistor model + +![Resistor](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTpJkiEyqh-BRx27pvVH0GLZ4MP_D1oriBwJhnZdgIq7m17z9VKUWaW9MeNQAz1rTML2ho&usqp=CAU) +""" +@component function Resistor(; name, R = 1.0) + systems = @named begin + p = Pin() + n = Pin() + end + vars = @variables begin + v(t), [guess = 0.0] + i(t), [guess = 0.0] + end + params = @parameters begin + R = R, [description = "Resistance of this Resistor"] + end + eqs = [v ~ p.v - n.v + i ~ p.i + p.i + n.i ~ 0 + # Ohm's Law + v ~ i * R] + return ODESystem(eqs, t, vars, params; systems, name) +end +@connector Pin begin + v(t) + i(t), [connect = Flow] +end +@component function ConstantVoltage(; name, V = 1.0) + systems = @named begin + p = Pin() + n = Pin() + end + vars = @variables begin + v(t), [guess = 0.0] + i(t), [guess = 0.0] + end + params = @parameters begin + V = 10 + end + eqs = [v ~ p.v - n.v + i ~ p.i + p.i + n.i ~ 0 + v ~ V] + return ODESystem(eqs, t, vars, params; systems, name) +end + +@component function Capacitor(; name, C = 1.0) + systems = @named begin + p = Pin() + n = Pin() + end + vars = @variables begin + v(t), [guess = 0.0] + i(t), [guess = 0.0] + end + params = @parameters begin + C = C + end + initialization_eqs = [ + v ~ 0 + ] + eqs = [v ~ p.v - n.v + i ~ p.i + p.i + n.i ~ 0 + C * D(v) ~ i] + return ODESystem(eqs, t, vars, params; systems, name, initialization_eqs) +end + +@component function Ground(; name) + systems = @named begin + g = Pin() + end + eqs = [ + g.v ~ 0 + ] + return ODESystem(eqs, t, [], []; systems, name) +end + +@component function Inductor(; name, L = 1.0) + systems = @named begin + p = Pin() + n = Pin() + end + vars = @variables begin + v(t), [guess = 0.0] + i(t), [guess = 0.0] + end + params = @parameters begin + (L = L) + end + eqs = [v ~ p.v - n.v + i ~ p.i + p.i + n.i ~ 0 + L * D(i) ~ v] + return ODESystem(eqs, t, vars, params; systems, name) +end + +""" +This is an RLC model. This should support markdown. That includes +HTML as well. +""" +@component function RLCModel(; name) + systems = @named begin + resistor = Resistor(R = 100) + capacitor = Capacitor(C = 0.001) + inductor = Inductor(L = 1) + source = ConstantVoltage(V = 30) + ground = Ground() + end + initialization_eqs = [ + inductor.i ~ 0 + ] + eqs = [connect(source.p, inductor.n) + connect(inductor.p, resistor.p, capacitor.p) + connect(resistor.n, ground.g, capacitor.n, source.n)] + return ODESystem(eqs, t, [], []; systems, name, initialization_eqs) +end +"""Run model RLCModel from 0 to 10""" +function simple() + @mtkbuild model = RLCModel() + u0 = [] + prob = ODEProblem(model, u0, (0.0, 10.0)) + sol = solve(prob) +end +@test SciMLBase.successful_retcode(simple()) + +@named model = RLCModel() +@test length(ModelingToolkit.get_initialization_eqs(model)) == 1 +syslist = ModelingToolkit.get_systems(model) +@test length(ModelingToolkit.get_initialization_eqs(syslist[1])) == 0 +@test length(ModelingToolkit.get_initialization_eqs(syslist[2])) == 1 +@test length(ModelingToolkit.get_initialization_eqs(syslist[3])) == 0 +@test length(ModelingToolkit.get_initialization_eqs(syslist[4])) == 0 +@test length(ModelingToolkit.get_initialization_eqs(syslist[5])) == 0 +@test length(ModelingToolkit.initialization_equations(model)) == 2 + +u0 = [] +prob = ODEProblem(structural_simplify(model), u0, (0.0, 10.0)) +sol = solve(prob, Rodas5P()) +@test length(sol[end]) == 2 +@test length(equations(prob.f.initializeprob.f.sys)) == 0 +@test length(unknowns(prob.f.initializeprob.f.sys)) == 0 diff --git a/test/index_cache.jl b/test/index_cache.jl new file mode 100644 index 0000000000..3bee1db381 --- /dev/null +++ b/test/index_cache.jl @@ -0,0 +1,45 @@ +using ModelingToolkit, SymbolicIndexingInterface +using ModelingToolkit: t_nounits as t + +# Ensure indexes of array symbolics are cached appropriately +@variables x(t)[1:2] +@named sys = ODESystem(Equation[], t, [x], []) +sys1 = complete(sys) +@named sys = ODESystem(Equation[], t, [x...], []) +sys2 = complete(sys) +for sys in [sys1, sys2] + for (sym, idx) in [(x, 1:2), (x[1], 1), (x[2], 2)] + @test is_variable(sys, sym) + @test variable_index(sys, sym) == idx + end +end + +@variables x(t)[1:2, 1:2] +@named sys = ODESystem(Equation[], t, [x], []) +sys1 = complete(sys) +@named sys = ODESystem(Equation[], t, [x...], []) +sys2 = complete(sys) +for sys in [sys1, sys2] + @test is_variable(sys, x) + @test variable_index(sys, x) == [1 3; 2 4] + for i in eachindex(x) + @test is_variable(sys, x[i]) + @test variable_index(sys, x[i]) == variable_index(sys, x)[i] + end +end + +# Ensure Symbol to symbolic map is correct +@parameters p1 p2[1:2] p3::String +@variables x(t) y(t)[1:2] z(t) + +@named sys = ODESystem(Equation[], t, [x, y, z], [p1, p2, p3]) +sys = complete(sys) + +ic = ModelingToolkit.get_index_cache(sys) + +@test isequal(ic.symbol_to_variable[:p1], p1) +@test isequal(ic.symbol_to_variable[:p2], p2) +@test isequal(ic.symbol_to_variable[:p3], p3) +@test isequal(ic.symbol_to_variable[:x], x) +@test isequal(ic.symbol_to_variable[:y], y) +@test isequal(ic.symbol_to_variable[:z], z) diff --git a/test/initial_values.jl b/test/initial_values.jl index dcbb57675d..3a7b48901d 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -60,3 +60,34 @@ u0 = [X1 => 1.0, X2 => 2.0] tspan = (0.0, 1.0) ps = [k1 => 1.0, k2 => 5.0] @test_nowarn oprob = ODEProblem(osys_m, u0, tspan, ps) + +# Make sure it doesn't error on array variables with unspecified size +@parameters p::Vector{Real} q[1:3] +varmap = Dict(p => ones(3), q => 2ones(3)) +cvarmap = ModelingToolkit.canonicalize_varmap(varmap) +target_varmap = Dict(p => ones(3), q => 2ones(3), q[1] => 2.0, q[2] => 2.0, q[3] => 2.0) +@test cvarmap == target_varmap + +# Initialization of ODEProblem with dummy derivatives of multidimensional arrays +# Issue#1283 +@variables z(t)[1:2, 1:2] +eqs = [D(D(z)) ~ ones(2, 2)] +@mtkbuild sys = ODESystem(eqs, t) +@test_nowarn ODEProblem(sys, [z => zeros(2, 2), D(z) => ones(2, 2)], (0.0, 10.0)) + +# Initialization with defaults involving parameters that are not part of the system +# Issue#2817 +@parameters A1 A2 B1 B2 +@variables x1(t) x2(t) +@mtkbuild sys = ODESystem( + [ + x1 ~ B1, + x2 ~ B2 + ], t; defaults = [ + A2 => 1 - A1, + B1 => A1, + B2 => A2 + ]) +prob = ODEProblem(sys, [], (0.0, 1.0), [A1 => 0.3]) +@test prob.ps[B1] == 0.3 +@test prob.ps[B2] == 0.7 diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 44a9423f24..ca41bcd072 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -17,12 +17,17 @@ sol = solve(initprob) @test SciMLBase.successful_retcode(sol) @test maximum(abs.(sol[conditions])) < 1e-14 +@test_throws ModelingToolkit.ExtraVariablesSystemException ModelingToolkit.InitializationProblem( + pend, 0.0, [], [g => 1]; + guesses = [ModelingToolkit.missing_variable_defaults(pend); x => 1; y => 0.2], + fully_determined = true) + initprob = ModelingToolkit.InitializationProblem(pend, 0.0, [x => 1, y => 0], [g => 1]; guesses = ModelingToolkit.missing_variable_defaults(pend)) @test initprob isa NonlinearProblem sol = solve(initprob) @test SciMLBase.successful_retcode(sol) -@test sol.u == [1.0, 0.0, 0.0, 0.0] +@test sol.u == [0.0, 0.0, 0.0, 0.0] @test maximum(abs.(sol[conditions])) < 1e-14 initprob = ModelingToolkit.InitializationProblem( @@ -31,6 +36,10 @@ initprob = ModelingToolkit.InitializationProblem( sol = solve(initprob) @test !SciMLBase.successful_retcode(sol) +@test_throws ModelingToolkit.ExtraVariablesSystemException ModelingToolkit.InitializationProblem( + pend, 0.0, [], [g => 1]; guesses = ModelingToolkit.missing_variable_defaults(pend), + fully_determined = true) + prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = ModelingToolkit.missing_variable_defaults(pend)) prob.f.initializeprob isa NonlinearProblem @@ -47,6 +56,11 @@ sol = solve(prob.f.initializeprob) sol = solve(prob, Rodas5P()) @test maximum(abs.(sol[conditions][1])) < 1e-14 +@test_throws ModelingToolkit.ExtraVariablesSystemException ODEProblem( + pend, [x => 1], (0.0, 1.5), [g => 1], + guesses = ModelingToolkit.missing_variable_defaults(pend), + fully_determined = true) + @connector Port begin p(t) dm(t) = 0, [connect = Flow] @@ -225,6 +239,9 @@ initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) @test SciMLBase.successful_retcode(initsol) @test maximum(abs.(initsol[conditions])) < 1e-14 +@test_throws ModelingToolkit.ExtraEquationsSystemException ModelingToolkit.InitializationProblem( + sys, 0.0, fully_determined = true) + allinit = unknowns(sys) .=> initsol[unknowns(sys)] prob = ODEProblem(sys, allinit, (0, 0.1)) sol = solve(prob, Rodas5P(), initializealg = BrownFullBasicInit()) @@ -232,7 +249,12 @@ sol = solve(prob, Rodas5P(), initializealg = BrownFullBasicInit()) @test sol.retcode == SciMLBase.ReturnCode.Unstable @test maximum(abs.(initsol[conditions][1])) < 1e-14 +prob = ODEProblem(sys, allinit, (0, 0.1)) prob = ODEProblem(sys, [], (0, 0.1), check = false) + +@test_throws ModelingToolkit.ExtraEquationsSystemException ODEProblem( + sys, [], (0, 0.1), fully_determined = true) + sol = solve(prob, Rodas5P()) # If initialized incorrectly, then it would be InitialFailure @test sol.retcode == SciMLBase.ReturnCode.Unstable @@ -430,3 +452,37 @@ sol = solve(prob, Tsit5()) # This should warn, but logging tests can't be marked as broken @test_logs prob = ODEProblem(simpsys, [], tspan, guesses = [x => 2.0]) + +# Late Binding initialization_eqs +# https://github.com/SciML/ModelingToolkit.jl/issues/2787 + +@parameters g +@variables x(t) y(t) [state_priority = 10] λ(t) +eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] +@mtkbuild pend = ODESystem(eqs, t) + +prob = ODEProblem(pend, [x => 1], (0.0, 1.5), [g => 1], + guesses = [λ => 0, y => 1], initialization_eqs = [y ~ 1]) + +unsimp = generate_initializesystem(pend; u0map = [x => 1], initialization_eqs = [y ~ 1]) +sys = structural_simplify(unsimp; fully_determined = false) +@test length(equations(sys)) == 3 + +# Extend two systems with initialization equations and guesses +# https://github.com/SciML/ModelingToolkit.jl/issues/2845 +@variables x(t) y(t) +@named sysx = ODESystem([D(x) ~ 0], t; initialization_eqs = [x ~ 1]) +@named sysy = ODESystem([D(y) ~ 0], t; initialization_eqs = [y^2 ~ 2], guesses = [y => 1]) +sys = extend(sysx, sysy) +@test length(equations(generate_initializesystem(sys))) == 2 +@test length(ModelingToolkit.guesses(sys)) == 1 + +# https://github.com/SciML/ModelingToolkit.jl/issues/2873 +@testset "Error on missing defaults" begin + @variables x(t) y(t) + @named sys = ODESystem([x^2 + y^2 ~ 25, D(x) ~ 1], t) + ssys = structural_simplify(sys) + @test_throws ArgumentError ODEProblem(ssys, [x => 3], (0, 1), []) # y should have a guess +end diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 2aff6d44c0..778e02db0a 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -20,9 +20,9 @@ end @named sys = ODESystem([D(x) ~ -x + u], t) # both u and x are unbound @named sys1 = ODESystem([D(x) ~ -x + v[1] + v[2]], t) # both v and x are unbound @named sys2 = ODESystem([D(x) ~ -sys.x], t, systems = [sys]) # this binds sys.x in the context of sys2, sys2.x is still unbound -@named sys21 = ODESystem([D(x) ~ -sys.x], t, systems = [sys1]) # this binds sys.x in the context of sys2, sys2.x is still unbound +@named sys21 = ODESystem([D(x) ~ -sys1.x], t, systems = [sys1]) # this binds sys.x in the context of sys2, sys2.x is still unbound @named sys3 = ODESystem([D(x) ~ -sys.x + sys.u], t, systems = [sys]) # This binds both sys.x and sys.u -@named sys31 = ODESystem([D(x) ~ -sys.x + sys1.v[1]], t, systems = [sys1]) # This binds both sys.x and sys1.v[1] +@named sys31 = ODESystem([D(x) ~ -sys1.x + sys1.v[1]], t, systems = [sys1]) # This binds both sys.x and sys1.v[1] @named sys4 = ODESystem([D(x) ~ -sys.x, u ~ sys.u], t, systems = [sys]) # This binds both sys.x and sys3.u, this system is one layer deeper than the previous. u is directly forwarded to sys.u, and in this case sys.u is bound while u is not @@ -43,7 +43,7 @@ end @test is_bound(sys2, sys.x) @test !is_bound(sys2, sys.u) @test !is_bound(sys2, sys2.sys.u) -@test is_bound(sys21, sys.x) +@test is_bound(sys21, sys1.x) @test !is_bound(sys21, sys1.v[1]) @test !is_bound(sys21, sys1.v[2]) @test is_bound(sys31, sys1.v[1]) @@ -144,7 +144,9 @@ if VERSION >= v"1.8" # :opaque_closure not supported before drop_expr = identity) x = randn(size(A, 1)) u = randn(size(B, 2)) - p = getindex.(Ref(ModelingToolkit.defaults(ssys)), parameters(ssys)) + p = getindex.( + Ref(ModelingToolkit.defaults_and_guesses(ssys)), + parameters(ssys)) y1 = obsf(x, u, p, 0) y2 = C * x + D * u @test y1[] ≈ y2[] @@ -376,3 +378,16 @@ matrices, ssys = linearize(augmented_sys, # P = ss(A,B,C,0) # G = ss(matrices...) # @test sminreal(G[1, 3]) ≈ sminreal(P[1,1])*dist + +@testset "Observed functions with inputs" begin + @variables x(t)=0 u(t)=0 [input = true] + eqs = [ + D(x) ~ -x + u + ] + + @named sys = ODESystem(eqs, t) + (; io_sys,) = ModelingToolkit.generate_control_function(sys, simplify = true) + obsfn = ModelingToolkit.build_explicit_observed_function( + io_sys, [x + u * t]; inputs = [u]) + @test obsfn([1.0], [2.0], nothing, 3.0) == [7.0] +end diff --git a/test/inputoutput.jl b/test/inputoutput.jl index fde9c68516..2a81a0e315 100644 --- a/test/inputoutput.jl +++ b/test/inputoutput.jl @@ -1,6 +1,7 @@ using ModelingToolkit, OrdinaryDiffEq, Symbolics, Test -@parameters t σ ρ β +@independent_variables t +@parameters σ ρ β @variables x(t) y(t) z(t) F(t) u(t) D = Differential(t) diff --git a/test/latexify/20.tex b/test/latexify/20.tex index 9de213aafd..b96d9fdc2d 100644 --- a/test/latexify/20.tex +++ b/test/latexify/20.tex @@ -1,5 +1,5 @@ \begin{align} -\frac{\mathrm{d}}{\mathrm{d}t} u(t)_1 =& p_3 \left( - u(t)_1 + u(t)_2 \right) \\ -0 =& - u(t)_2 + \frac{1}{10} \left( p_1 - u(t)_1 \right) p_2 p_3 u(t)_1 \\ -\frac{\mathrm{d}}{\mathrm{d}t} u(t)_3 =& u(t)_2^{\frac{2}{3}} u(t)_1 - p_3 u(t)_3 +\frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} =& p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ +0 =& - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ +\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} =& u\left( t \right)_{2}^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} \end{align} diff --git a/test/latexify/30.tex b/test/latexify/30.tex index b83feeba72..767b8e54f2 100644 --- a/test/latexify/30.tex +++ b/test/latexify/30.tex @@ -1,5 +1,5 @@ \begin{align} -\frac{\mathrm{d}}{\mathrm{d}t} u(t)_1 =& p_3 \left( - u(t)_1 + u(t)_2 \right) \\ -\frac{\mathrm{d}}{\mathrm{d}t} u(t)_2 =& - u(t)_2 + \frac{1}{10} \left( p_1 - u(t)_1 \right) p_2 p_3 u(t)_1 \\ -\frac{\mathrm{d}}{\mathrm{d}t} u(t)_3 =& u(t)_2^{\frac{2}{3}} u(t)_1 - p_3 u(t)_3 +\frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} =& p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ +\frac{\mathrm{d} u\left( t \right)_{2}}{\mathrm{d}t} =& - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ +\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} =& u\left( t \right)_{2}^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} \end{align} diff --git a/test/linearity.jl b/test/linearity.jl index 0293110c7f..aed9a256d2 100644 --- a/test/linearity.jl +++ b/test/linearity.jl @@ -3,7 +3,8 @@ using DiffEqBase using Test # Define some variables -@parameters t σ ρ β +@independent_variables t +@parameters σ ρ β @variables x(t) y(t) z(t) D = Differential(t) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 411a5e398f..7ca5618786 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -1,8 +1,7 @@ using ModelingToolkit, Test using ModelingToolkit: get_connector_type, get_defaults, get_gui_metadata, - get_systems, get_ps, getdefault, getname, scalarize, symtype, - VariableDescription, - RegularConnector + get_systems, get_ps, getdefault, getname, readable_code, + scalarize, symtype, VariableDescription, RegularConnector using URIs: URI using Distributions using DynamicQuantities, OrdinaryDiffEq @@ -47,7 +46,7 @@ end output = RealOutput() end @parameters begin - k, [description = "Constant output value of block"] + k, [description = "Constant output value of block", unit = u"V"] end @equations begin output.u ~ k @@ -184,10 +183,15 @@ resistor = getproperty(rc, :resistor; namespace = false) @testset "Constants" begin @mtkmodel PiModel begin @constants begin - _p::Irrational = π, [description = "Value of Pi."] + _p::Irrational = π, [description = "Value of Pi.", unit = u"V"] end @parameters begin p = _p, [description = "Assign constant `_p` value."] + e, [unit = u"V"] + end + @equations begin + # This validates units; indirectly verifies that metadata was correctly passed. + e ~ _p end end @@ -259,7 +263,7 @@ end @test getdefault(model.cval) == 1 @test isequal(getdefault(model.c), model.cval + model.jval) @test getdefault(model.d) == 2 - @test_throws KeyError getdefault(model.e) + @test_throws ErrorException getdefault(model.e) @test getdefault(model.f) == 3 @test getdefault(model.i) == 4 @test all(getdefault.(scalarize(model.b2)) .== [1, 3]) @@ -371,8 +375,8 @@ end @testset "Metadata in variables" begin metadata = Dict(:description => "Variable to test metadata in the Model.structure", - :input => true, :bounds => (-1, 1), :connection_type => :Flow, - :tunable => false, :disturbance => true, :dist => Normal(1, 1)) + :input => true, :bounds => :((-1, 1)), :connection_type => :Flow, + :tunable => false, :disturbance => true, :dist => :(Normal(1, 1))) @connector MockMeta begin m(t), @@ -426,6 +430,7 @@ end @test A.structure[:components] == [[:cc, :C]] end +using ModelingToolkit: D_nounits @testset "Event handling in MTKModel" begin @mtkmodel M begin @variables begin @@ -434,9 +439,9 @@ end z(t) end @equations begin - x ~ -D(x) - D(y) ~ 0 - D(z) ~ 0 + x ~ -D_nounits(x) + D_nounits(y) ~ 0 + D_nounits(z) ~ 0 end @continuous_events begin [x ~ 1.5] => [x ~ 5, y ~ 1] @@ -467,7 +472,7 @@ using ModelingToolkit: getdefault, scalarize @named model_with_component_array = ModelWithComponentArray() - @test ModelWithComponentArray.structure[:parameters][:r][:unit] == u"Ω" + @test eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == eval(u"Ω") @test lastindex(parameters(model_with_component_array)) == 3 # Test the constant `k`. Manually k's value should be kept in sync here @@ -763,3 +768,52 @@ end @testset "Parent module of Models" begin @test parentmodule(MyMockModule.Ground) == MyMockModule end + +@testset "Guesses with expression" begin + @mtkmodel GuessModel begin + @variables begin + k(t) + l(t) = 10, [guess = k, unit = u"A"] + i(t), [guess = k, unit = u"A"] + j(t), [guess = k + l / i] + end + end + + @named guess_model = GuessModel() + + j_guess = getguess(guess_model.j) + @test typeof(j_guess) == Num + @test readable_code(j_guess) == "l(t) / i(t) + k(t)" + + i_guess = getguess(guess_model.i) + @test typeof(i_guess) == Num + @test readable_code(i_guess) == "k(t)" + + l_guess = getguess(guess_model.l) + @test typeof(l_guess) == Num + @test readable_code(l_guess) == "k(t)" +end + +@testset "Argument order" begin + @mtkmodel OrderModel begin + @structural_parameters begin + b = 1 # reverse alphabetical order to test that the order is preserved + a = b + end + @parameters begin + c = a + d = b + end + end + @named ordermodel = OrderModel() + ordermodel = complete(ordermodel) + defs = ModelingToolkit.defaults(ordermodel) + @test defs[ordermodel.c] == 1 + @test defs[ordermodel.d] == 1 + + @test_nowarn @named ordermodel = OrderModel(a = 2) + ordermodel = complete(ordermodel) + defs = ModelingToolkit.defaults(ordermodel) + @test defs[ordermodel.c] == 2 + @test defs[ordermodel.d] == 1 +end diff --git a/test/modelingtoolkitize.jl b/test/modelingtoolkitize.jl index ec582e4145..ac32f874ed 100644 --- a/test/modelingtoolkitize.jl +++ b/test/modelingtoolkitize.jl @@ -1,5 +1,8 @@ using OrdinaryDiffEq, ModelingToolkit, DataStructures, Test using Optimization, RecursiveArrayTools, OptimizationOptimJL +using LabelledArrays, SymbolicIndexingInterface +using ModelingToolkit: t_nounits as t, D_nounits as D +using SciMLBase: parameterless_type N = 32 const xyd_brusselator = range(0, stop = 1, length = N) @@ -252,7 +255,6 @@ u0 = @LArray [9998.0, 1.0, 1.0, 1.0] (:S, :I, :R, :C) problem = ODEProblem(SIR!, u0, tspan, p) sys = complete(modelingtoolkitize(problem)) -@parameters t @test all(isequal.(parameters(sys), getproperty.(@variables(β, η, ω, φ, σ, μ), :val))) @test all(isequal.(Symbol.(unknowns(sys)), Symbol.(@variables(S(t), I(t), R(t), C(t))))) @@ -291,9 +293,8 @@ sys = modelingtoolkitize(prob) @test [ModelingToolkit.defaults(sys)[s] for s in parameters(sys)] == [10, 20] @test ModelingToolkit.has_tspan(sys) -@parameters t sig=10 rho=28.0 beta=8 / 3 +@parameters sig=10 rho=28.0 beta=8 / 3 @variables x(t)=100 y(t)=1.0 z(t)=1 -D = Differential(t) eqs = [D(x) ~ sig * (y - x), D(y) ~ x * (rho - z) - y, @@ -307,3 +308,182 @@ noiseeqs = [0.1 * x, prob = SDEProblem(complete(sys)) sys = modelingtoolkitize(prob) @test ModelingToolkit.has_tspan(sys) + +@testset "Explicit variable names" begin + function fn(du, u, p::NamedTuple, t) + du[1] = u[1] + p.a * u[2] + du[2] = u[2] + p.b * u[1] + end + function fn(du, u, p::AbstractDict, t) + du[1] = u[1] + p[:a] * u[2] + du[2] = u[2] + p[:b] * u[1] + end + function fn(du, u, p, t) + du[1] = u[1] + p[1] * u[2] + du[2] = u[2] + p[2] * u[1] + end + function fn(du, u, p::Real, t) + du[1] = u[1] + p * u[2] + du[2] = u[2] + p * u[1] + end + function nl_fn(u, p::NamedTuple) + [u[1] + p.a * u[2], u[2] + p.b * u[1]] + end + function nl_fn(u, p::AbstractDict) + [u[1] + p[:a] * u[2], u[2] + p[:b] * u[1]] + end + function nl_fn(u, p) + [u[1] + p[1] * u[2], u[2] + p[2] * u[1]] + end + function nl_fn(u, p::Real) + [u[1] + p * u[2], u[2] + p * u[1]] + end + params = (a = 1, b = 1) + odeprob = ODEProblem(fn, [1 1], (0, 1), params) + nlprob = NonlinearProblem(nl_fn, [1, 1], params) + optprob = OptimizationProblem(nl_fn, [1, 1], params) + + @testset "$(parameterless_type(prob))" for prob in [optprob] + sys = modelingtoolkitize(prob, u_names = [:a, :b]) + @test is_variable(sys, sys.a) + @test is_variable(sys, sys.b) + @test is_variable(sys, :a) + @test is_variable(sys, :b) + + @test_throws ["unknowns", "2", "does not match", "names", "3"] modelingtoolkitize( + prob, u_names = [:a, :b, :c]) + for (pvals, pnames) in [ + ([1, 2], [:p, :q]), + ((1, 2), [:p, :q]), + ([1, 2], Dict(1 => :p, 2 => :q)), + ((1, 2), Dict(1 => :p, 2 => :q)), + (1.0, :p), + (1.0, [:p]), + (1.0, Dict(1 => :p)), + (Dict(:a => 2, :b => 4), Dict(:a => :p, :b => :q)), + (LVector(a = 1, b = 2), [:p, :q]), + (SLVector(a = 1, b = 2), [:p, :q]), + (LVector(a = 1, b = 2), Dict(1 => :p, 2 => :q)), + (SLVector(a = 1, b = 2), Dict(1 => :p, 2 => :q)), + ((a = 1, b = 2), (a = :p, b = :q)), + ((a = 1, b = 2), Dict(:a => :p, :b => :q)) + ] + if pvals isa NamedTuple && prob isa OptimizationProblem + continue + end + sys = modelingtoolkitize( + remake(prob, p = pvals, interpret_symbolicmap = false), p_names = pnames) + if pnames isa Symbol + @test is_parameter(sys, pnames) + continue + end + for p in values(pnames) + @test is_parameter(sys, p) + end + end + + for (pvals, pnames) in [ + ([1, 2], [:p, :q, :r]), + ((1, 2), [:p, :q, :r]), + ([1, 2], Dict(1 => :p, 2 => :q, 3 => :r)), + ((1, 2), Dict(1 => :p, 2 => :q, 3 => :r)), + (1.0, [:p, :q]), + (1.0, Dict(1 => :p, 2 => :q)), + (Dict(:a => 2, :b => 4), Dict(:a => :p, :b => :q, :c => :r)), + (LVector(a = 1, b = 2), [:p, :q, :r]), + (SLVector(a = 1, b = 2), [:p, :q, :r]), + (LVector(a = 1, b = 2), Dict(1 => :p, 2 => :q, 3 => :r)), + (SLVector(a = 1, b = 2), Dict(1 => :p, 2 => :q, 3 => :r)), + ((a = 1, b = 2), (a = :p, b = :q, c = :r)), + ((a = 1, b = 2), Dict(:a => :p, :b => :q, :c => :r)) + ] + newprob = remake(prob, p = pvals, interpret_symbolicmap = false) + @test_throws [ + "parameters", "$(length(pvals))", "does not match", "$(length(pnames))"] modelingtoolkitize( + newprob, p_names = pnames) + end + + sc = SymbolCache([:a, :b], [:p, :q]) + sci_f = parameterless_type(prob.f)(prob.f.f, sys = sc) + newprob = remake(prob, f = sci_f, p = [1, 2]) + sys = modelingtoolkitize(newprob) + @test is_variable(sys, sys.a) + @test is_variable(sys, sys.b) + @test is_variable(sys, :a) + @test is_variable(sys, :b) + @test is_parameter(sys, sys.p) + @test is_parameter(sys, sys.q) + @test is_parameter(sys, :p) + @test is_parameter(sys, :q) + end + + @testset "From MTK model" begin + @testset "ODE" begin + @variables x(t)=1.0 y(t)=2.0 + @parameters p=3.0 q=4.0 + @mtkbuild sys = ODESystem([D(x) ~ p * y, D(y) ~ q * x], t) + prob1 = ODEProblem(sys, [], (0.0, 5.0)) + newsys = complete(modelingtoolkitize(prob1)) + @test is_variable(newsys, newsys.x) + @test is_variable(newsys, newsys.y) + @test is_parameter(newsys, newsys.p) + @test is_parameter(newsys, newsys.q) + prob2 = ODEProblem(newsys, [], (0.0, 5.0)) + + sol1 = solve(prob1, Tsit5()) + sol2 = solve(prob2, Tsit5()) + @test sol1 ≈ sol2 + end + @testset "Nonlinear" begin + @variables x=1.0 y=2.0 + @parameters p=3.0 q=4.0 + @mtkbuild nlsys = NonlinearSystem([0 ~ p * y^2 + x, 0 ~ x + exp(x) * q]) + prob1 = NonlinearProblem(nlsys, []) + newsys = complete(modelingtoolkitize(prob1)) + @test is_variable(newsys, newsys.x) + @test is_variable(newsys, newsys.y) + @test is_parameter(newsys, newsys.p) + @test is_parameter(newsys, newsys.q) + prob2 = NonlinearProblem(newsys, []) + + sol1 = solve(prob1) + sol2 = solve(prob2) + @test sol1 ≈ sol2 + end + @testset "Optimization" begin + @variables begin + x = 1.0, [bounds = (-2.0, 10.0)] + y = 2.0, [bounds = (-1.0, 10.0)] + end + @parameters p=3.0 q=4.0 + loss = (p - x)^2 + q * (y - x^2)^2 + @mtkbuild optsys = OptimizationSystem(loss, [x, y], [p, q]) + prob1 = OptimizationProblem(optsys, [], grad = true, hess = true) + newsys = complete(modelingtoolkitize(prob1)) + @test is_variable(newsys, newsys.x) + @test is_variable(newsys, newsys.y) + @test is_parameter(newsys, newsys.p) + @test is_parameter(newsys, newsys.q) + prob2 = OptimizationProblem(newsys, [], grad = true, hess = true) + + sol1 = solve(prob1, GradientDescent()) + sol2 = solve(prob2, GradientDescent()) + + @test sol1 ≈ sol2 + end + end +end + +## NonlinearLeastSquaresProblem + +function nlls!(du, u, p) + du[1] = 2u[1] - 2 + du[2] = u[1] - 4u[2] + du[3] = 0 +end +u0 = [0.0, 0.0] +prob = NonlinearLeastSquaresProblem( + NonlinearFunction(nlls!, resid_prototype = zeros(3)), u0) +sys = modelingtoolkitize(prob) +@test length(equations(sys)) == 3 +@test length(equations(structural_simplify(sys; fully_determined = false))) == 0 diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index d669b4cdac..d5e16bb071 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -2,6 +2,9 @@ using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D, MTKParameters using SymbolicIndexingInterface using SciMLStructures: SciMLStructures, canonicalize, Tunable, Discrete, Constants +using OrdinaryDiffEq +using ForwardDiff +using JET @parameters a b c d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String @named sys = ODESystem( @@ -20,9 +23,7 @@ ps = MTKParameters(sys, ivs) ivs[a] = 1.0 ps = MTKParameters(sys, ivs) -@test_broken getp(sys, g) # SII bug for (p, val) in ivs - isequal(p, g) && continue # broken if isequal(p, c) val = 3ivs[a] end @@ -64,9 +65,228 @@ setp(sys, e)(ps, 5ones(3)) # with an array setp(sys, f[2, 2])(ps, 42) # with a sub-index @test getp(sys, f[2, 2])(ps) == 42 -# SII bug -@test_broken setp(sys, g)(ps, ones(100)) # with non-fixed-length array -@test_broken getp(sys, g)(ps) == ones(100) +setp(sys, g)(ps, ones(100)) # with non-fixed-length array +@test getp(sys, g)(ps) == ones(100) setp(sys, h)(ps, "bar") # with a non-numeric @test getp(sys, h)(ps) == "bar" + +newps = remake_buffer(sys, + ps, + Dict(a => 1.0f0, b => 5.0f0, c => 2.0, d => 0x5, e => [0.4, 0.5, 0.6], + f => 3ones(UInt, 3, 3), g => ones(Float32, 4), h => "bar")) + +for fname in (:tunable, :discrete, :constant, :dependent) + # ensure same number of sub-buffers + @test length(getfield(ps, fname)) == length(getfield(newps, fname)) +end +@test ps.dependent_update_iip === newps.dependent_update_iip +@test ps.dependent_update_oop === newps.dependent_update_oop + +@test getp(sys, a)(newps) isa Float32 +@test getp(sys, b)(newps) == 2.0f0 # ensure dependent update still happened, despite explicit value +@test getp(sys, c)(newps) isa Float64 +@test getp(sys, d)(newps) isa UInt8 +@test getp(sys, f)(newps) isa Matrix{UInt} +@test getp(sys, g)(newps) isa Vector{Float32} + +ps = MTKParameters(sys, ivs) +function loss(value, sys, ps) + @test value isa ForwardDiff.Dual + vals = merge(Dict(parameters(sys) .=> getp(sys, parameters(sys))(ps)), Dict(a => value)) + ps = remake_buffer(sys, ps, vals) + getp(sys, a)(ps) + getp(sys, b)(ps) +end + +@test ForwardDiff.derivative(x -> loss(x, sys, ps), 1.5) == 3.0 + +# Issue#2615 +@parameters p::Vector{Float64} +@variables X(t) +eq = D(X) ~ p[1] - p[2] * X +@mtkbuild osys = ODESystem([eq], t) + +u0 = [X => 1.0] +ps = [p => [2.0, 0.1]] +p = MTKParameters(osys, ps, u0) +@test p.tunable[1] == [2.0, 0.1] + +# Ensure partial update promotes the buffer +@parameters p q r +@named sys = ODESystem(Equation[], t, [], [p, q, r]) +sys = complete(sys) +ps = MTKParameters(sys, [p => 1.0, q => 2.0, r => 3.0]) +newps = remake_buffer(sys, ps, Dict(p => 1.0f0)) +@test newps.tunable[1] isa Vector{Float32} +@test newps.tunable[1] == [1.0f0, 2.0f0, 3.0f0] + +# Issue#2624 +@parameters p d +@variables X(t) +eqs = [D(X) ~ p - d * X] +@mtkbuild sys = ODESystem(eqs, t) + +u0 = [X => 1.0] +tspan = (0.0, 100.0) +ps = [p => 1.0] # Value for `d` is missing + +@test_throws ModelingToolkit.MissingParametersError ODEProblem(sys, u0, tspan, ps) +@test_nowarn ODEProblem(sys, u0, tspan, [ps..., d => 1.0]) + +# JET tests + +# scalar parameters only +function level1() + @parameters p1=0.5 [tunable = true] p2=1 [tunable = true] p3=3 [tunable = false] p4=3 [tunable = true] y0=1 + @variables x(t)=2 y(t)=y0 + D = Differential(t) + + eqs = [D(x) ~ p1 * x - p2 * x * y + D(y) ~ -p3 * y + p4 * x * y] + + sys = structural_simplify(complete(ODESystem( + eqs, t, tspan = (0, 3.0), name = :sys, parameter_dependencies = [y0 => 2p4]))) + prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys) +end + +# scalar and vector parameters +function level2() + @parameters p1=0.5 [tunable = true] (p23[1:2]=[1, 3.0]) [tunable = true] p4=3 [tunable = false] y0=1 + @variables x(t)=2 y(t)=y0 + D = Differential(t) + + eqs = [D(x) ~ p1 * x - p23[1] * x * y + D(y) ~ -p23[2] * y + p4 * x * y] + + sys = structural_simplify(complete(ODESystem( + eqs, t, tspan = (0, 3.0), name = :sys, parameter_dependencies = [y0 => 2p4]))) + prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys) +end + +# scalar and vector parameters with different scalar types +function level3() + @parameters p1=0.5 [tunable = true] (p23[1:2]=[1, 3.0]) [tunable = true] p4::Int=3 [tunable = true] y0::Int=1 + @variables x(t)=2 y(t)=y0 + D = Differential(t) + + eqs = [D(x) ~ p1 * x - p23[1] * x * y + D(y) ~ -p23[2] * y + p4 * x * y] + + sys = structural_simplify(complete(ODESystem( + eqs, t, tspan = (0, 3.0), name = :sys, parameter_dependencies = [y0 => 2p4]))) + prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys) +end + +@testset "level$i" for (i, prob) in enumerate([level1(), level2(), level3()]) + ps = prob.p + @testset "Type stability of $portion" for portion in [ + Tunable(), Discrete(), Constants()] + @test_call canonicalize(portion, ps) + # @inferred canonicalize(portion, ps) + broken = (i ∈ [2, 3] && portion == Tunable()) + + # broken because the size of a vector of vectors can't be determined at compile time + @test_opt broken=broken target_modules=(ModelingToolkit,) canonicalize( + portion, ps) + + buffer, repack, alias = canonicalize(portion, ps) + + @test_call SciMLStructures.replace(portion, ps, ones(length(buffer))) + @inferred SciMLStructures.replace(portion, ps, ones(length(buffer))) + @test_opt target_modules=(ModelingToolkit,) SciMLStructures.replace( + portion, ps, ones(length(buffer))) + + @test_call target_modules=(ModelingToolkit,) SciMLStructures.replace!( + portion, ps, ones(length(buffer))) + @inferred SciMLStructures.replace!(portion, ps, ones(length(buffer))) + @test_opt target_modules=(ModelingToolkit,) SciMLStructures.replace!( + portion, ps, ones(length(buffer))) + end +end + +# Issue#2642 +@parameters α β γ δ +@variables x(t) y(t) +eqs = [D(x) ~ (α - β * y) * x + D(y) ~ (δ * x - γ) * y] +@mtkbuild odesys = ODESystem(eqs, t) +odeprob = ODEProblem( + odesys, [x => 1.0, y => 1.0], (0.0, 10.0), [α => 1.5, β => 1.0, γ => 3.0, δ => 1.0]) +tunables, _... = canonicalize(Tunable(), odeprob.p) +@test tunables isa AbstractVector{Float64} + +function loss(x) + ps = odeprob.p + newps = SciMLStructures.replace(Tunable(), ps, x) + newprob = remake(odeprob, p = newps) + sol = solve(newprob, Tsit5()) + return sum(sol) +end + +@test_nowarn ForwardDiff.gradient(loss, collect(tunables)) + +# Ensure dependent parameters are `Tuple{...}` and not `ArrayPartition` when using +# `remake_buffer`. +@parameters p1 p2 p3[1:2] p4[1:2] +@named sys = ODESystem( + Equation[], t, [], [p1, p2, p3, p4]; parameter_dependencies = [p2 => 2p1, p4 => 3p3]) +sys = complete(sys) +ps = MTKParameters(sys, [p1 => 1.0, p3 => [2.0, 3.0]]) +@test ps[parameter_index(sys, p2)] == 2.0 +@test ps[parameter_index(sys, p4)] == [6.0, 9.0] + +newps = remake_buffer( + sys, ps, Dict(p1 => ForwardDiff.Dual(2.0), p3 => ForwardDiff.Dual.([3.0, 4.0]))) + +VDual = Vector{<:ForwardDiff.Dual} +VVDual = Vector{<:Vector{<:ForwardDiff.Dual}} +@test newps.dependent isa Union{Tuple{VDual, VVDual}, Tuple{VVDual, VDual}} + +@testset "Parameter type validation" begin + struct Foo{T} + x::T + end + + @parameters a b::Int c::Vector{Float64} d[1:2, 1:2]::Int e::Foo{Int} f::Foo + @named sys = ODESystem(Equation[], t, [], [a, b, c, d, e, f]) + sys = complete(sys) + ps = MTKParameters(sys, + Dict(a => 1.0, b => 2, c => 3ones(2), + d => 3ones(Int, 2, 2), e => Foo(1), f => Foo("a"))) + @test_nowarn setp(sys, c)(ps, ones(4)) # so this is fixed when SII is fixed + @test_throws DimensionMismatch set_parameter!( + ps, 4ones(Int, 3, 2), parameter_index(sys, d)) + @test_throws DimensionMismatch set_parameter!( + ps, 4ones(Int, 4), parameter_index(sys, d)) # size has to match, not just length + @test_nowarn setp(sys, f)(ps, Foo(:a)) # can change non-concrete type + + # Same flexibility is afforded to `b::Int` to allow for ForwardDiff + for sym in [a, b] + @test_nowarn remake_buffer(sys, ps, Dict(sym => 1)) + newps = @test_nowarn remake_buffer(sys, ps, Dict(sym => 1.0f0)) # Can change type if it's numeric + @test getp(sys, sym)(newps) isa Float32 + newps = @test_nowarn remake_buffer(sys, ps, Dict(sym => ForwardDiff.Dual(1.0))) + @test getp(sys, sym)(newps) isa ForwardDiff.Dual + @test_throws TypeError remake_buffer(sys, ps, Dict(sym => :a)) # still has to be numeric + end + + newps = @test_nowarn remake_buffer(sys, ps, Dict(c => view(1.0:4.0, 2:4))) # can change type of array + @test getp(sys, c)(newps) == 2.0:4.0 + @test parameter_values(newps, parameter_index(sys, c)) ≈ [2.0, 3.0, 4.0] + @test_throws TypeError remake_buffer(sys, ps, Dict(c => [:a, :b, :c])) # can't arbitrarily change eltype + @test_throws TypeError remake_buffer(sys, ps, Dict(c => :a)) # can't arbitrarily change type + + newps = @test_nowarn remake_buffer(sys, ps, Dict(d => ForwardDiff.Dual.(ones(2, 2)))) # can change eltype + @test_throws TypeError remake_buffer(sys, ps, Dict(d => [:a :b; :c :d])) # eltype still has to be numeric + @test getp(sys, d)(newps) isa Matrix{<:ForwardDiff.Dual} + + @test_throws TypeError remake_buffer(sys, ps, Dict(e => Foo(2.0))) # need exact same type for nonnumeric + @test_nowarn remake_buffer(sys, ps, Dict(f => Foo(:a))) +end + +@testset "Error on missing parameter defaults" begin + @parameters a b c + @named sys = ODESystem(Equation[], t, [], [a, b]; defaults = Dict(b => 2c)) + sys = complete(sys) + @test_throws ["Could not evaluate", "b", "Missing", "2c"] MTKParameters(sys, [a => 1.0]) +end diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 6fdd099c78..841a026d0f 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -9,7 +9,7 @@ using ModelingToolkit: get_default_or_guess, MTKParameters canonequal(a, b) = isequal(simplify(a), simplify(b)) # Define some variables -@parameters t σ ρ β +@parameters σ ρ β @constants h = 1 @variables x y z @@ -115,7 +115,7 @@ lorenz2 = lorenz(:lorenz2) # system promotion using OrdinaryDiffEq -@variables t +@independent_variables t D = Differential(t) @named subsys = convert_system(ODESystem, lorenz1, t) @named sys = ODESystem([D(subsys.x) ~ subsys.x + subsys.x], t, systems = [subsys]) @@ -126,7 +126,7 @@ sol = solve(prob, FBDF(), reltol = 1e-7, abstol = 1e-7) @test sol[subsys.x] + sol[subsys.y] - sol[subsys.z]≈sol[subsys.u] atol=1e-7 @test_throws ArgumentError convert_system(ODESystem, sys, t) -@parameters t σ ρ β +@parameters σ ρ β @variables x y z # Define a nonlinear system @@ -178,8 +178,9 @@ end end # observed variable handling -@variables t x(t) RHS(t) +@independent_variables t @parameters τ +@variables x(t) RHS(t) @named fol = NonlinearSystem([0 ~ (1 - x * h) / τ], [x], [τ]; observed = [RHS ~ (1 - x) / τ]) @test isequal(RHS, @nonamespace fol.RHS) @@ -188,7 +189,7 @@ RHS2 = RHS @test isequal(RHS, RHS2) # issue #1358 -@variables t +@independent_variables t @variables v1(t) v2(t) i1(t) i2(t) eq = [v1 ~ sin(2pi * t * h) v1 - v2 ~ i1 @@ -257,3 +258,63 @@ end sol = solve(prob) @test_nowarn sol[unknowns(ns)] end + +# Issue#2625 +@parameters p d +@variables X(t) +alg_eqs = [0 ~ p - d * X] + +sys = @test_nowarn NonlinearSystem(alg_eqs; name = :name) +@test isequal(only(unknowns(sys)), X) +@test all(isequal.(parameters(sys), [p, d])) + +# Over-determined sys +@variables u1 u2 +@parameters u3 u4 +eqs = [u3 ~ u1 + u2, u4 ~ 2 * (u1 + u2), u3 + u4 ~ 3 * (u1 + u2)] +@named ns = NonlinearSystem(eqs, [u1, u2], [u3, u4]) +sys = structural_simplify(ns; fully_determined = false) +@test length(unknowns(sys)) == 1 + +# Conservative +@variables X(t) +alg_eqs = [1 ~ 2X] +@named ns = NonlinearSystem(alg_eqs) +sys = structural_simplify(ns) +@test length(equations(sys)) == 0 +sys = structural_simplify(ns; conservative = true) +@test length(equations(sys)) == 1 + +# https://github.com/SciML/ModelingToolkit.jl/issues/2858 +@testset "Jacobian/Hessian with observed equations that depend on unknowns" begin + @variables x y z + @parameters σ ρ β + eqs = [0 ~ σ * (y - x) + 0 ~ x * (ρ - z) - y + 0 ~ x * y - β * z] + guesses = [x => 1.0, y => 0.0, z => 0.0] + ps = [σ => 10.0, ρ => 26.0, β => 8 / 3] + @mtkbuild ns = NonlinearSystem(eqs) + + @test isequal(calculate_jacobian(ns), [(-1 - z + ρ)*σ -x*σ + 2x*(-z + ρ) -β-(x^2)]) + # solve without analytical jacobian + prob = NonlinearProblem(ns, guesses, ps) + sol = solve(prob, NewtonRaphson()) + @test sol.retcode == ReturnCode.Success + + # solve with analytical jacobian + prob = NonlinearProblem(ns, guesses, ps, jac = true) + sol = solve(prob, NewtonRaphson()) + @test sol.retcode == ReturnCode.Success + + # system that contains a chain of observed variables when simplified + @variables x y z + eqs = [0 ~ x^2 + 2z + y, z ~ y, y ~ x] # analytical solution x = y = z = 0 or -3 + @mtkbuild ns = NonlinearSystem(eqs) # solve for y with observed chain z -> x -> y + @test isequal(expand.(calculate_jacobian(ns)), [3 // 2 + y;;]) + @test isequal(calculate_hessian(ns), [[1;;]]) + prob = NonlinearProblem(ns, unknowns(ns) .=> -4.0) # give guess < -3 to reach -3 + sol = solve(prob, NewtonRaphson()) + @test sol[x] ≈ sol[y] ≈ sol[z] ≈ -3 +end diff --git a/test/odesystem.jl b/test/odesystem.jl index f45987cdc5..0f675c49e7 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -6,7 +6,7 @@ using DiffEqBase, SparseArrays using StaticArrays using Test using SymbolicUtils: issym - +using ForwardDiff using ModelingToolkit: value using ModelingToolkit: t_nounits as t, D_nounits as D @@ -58,6 +58,9 @@ for f in [ ODEFunction(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true), eval(ODEFunctionExpr(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true)) ] + # system + @test f.sys === de + # iip du = zeros(3) u = collect(1:3) @@ -969,7 +972,8 @@ vars_sub2 = @variables s2(t) @named partial_sub = ODESystem(Equation[], t, vars_sub2, []) @named sub = extend(partial_sub, sub) -new_sys2 = complete(substitute(sys2, Dict(:sub => sub))) +# no warnings for systems without events +new_sys2 = @test_nowarn complete(substitute(sys2, Dict(:sub => sub))) Set(unknowns(new_sys2)) == Set([new_sys2.x1, new_sys2.sys1.x1, new_sys2.sys1.sub.s1, new_sys2.sys1.sub.s2, new_sys2.sub.s1, new_sys2.sub.s2]) @@ -995,3 +999,254 @@ let # Issue https://github.com/SciML/ModelingToolkit.jl/issues/2322 sol = solve(prob, Rodas4()) @test sol(1)[]≈0.6065307685451087 rtol=1e-4 end + +# Issue#2599 +@variables x(t) y(t) +eqs = [D(x) ~ x * t, y ~ 2x] +@mtkbuild sys = ODESystem(eqs, t; continuous_events = [[y ~ 3] => [x ~ 2]]) +prob = ODEProblem(sys, [x => 1.0], (0.0, 10.0)) +@test_nowarn solve(prob, Tsit5()) + +# Issue#2383 +@variables x(t)[1:3] +@parameters p[1:3, 1:3] +eqs = [ + D(x) ~ p * x +] +@mtkbuild sys = ODESystem(eqs, t; continuous_events = [[norm(x) ~ 3.0] => [x ~ ones(3)]]) +# array affect equations used to not work +prob1 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) +sol1 = @test_nowarn solve(prob1, Tsit5()) + +# array condition equations also used to not work +@mtkbuild sys = ODESystem( + eqs, t; continuous_events = [[x ~ sqrt(3) * ones(3)] => [x ~ ones(3)]]) +# array affect equations used to not work +prob2 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) +sol2 = @test_nowarn solve(prob2, Tsit5()) + +@test sol1 ≈ sol2 + +# Requires fix in symbolics for `linear_expansion(p * x, D(y))` +@test_skip begin + @variables x(t)[1:3] y(t) + @parameters p[1:3, 1:3] + @test_nowarn @mtkbuild sys = ODESystem([D(x) ~ p * x, D(y) ~ x' * p * x], t) + @test_nowarn ODEProblem(sys, [x => ones(3), y => 2], (0.0, 10.0), [p => ones(3, 3)]) +end + +@parameters g L +@variables q₁(t) q₂(t) λ(t) θ(t) + +eqs = [D(D(q₁)) ~ -λ * q₁, + D(D(q₂)) ~ -λ * q₂ - g, + q₁ ~ L * sin(θ), + q₂ ~ L * cos(θ)] + +@named pend = ODESystem(eqs, t) +@test_nowarn generate_initializesystem( + pend, u0map = [q₁ => 1.0, q₂ => 0.0], guesses = [λ => 1]) + +# https://github.com/SciML/ModelingToolkit.jl/issues/2618 +@parameters σ ρ β +@variables x(t) y(t) z(t) + +eqs = [D(D(x)) ~ σ * (y - x), + D(y) ~ x * (ρ - z) - y, + D(z) ~ x * y - β * z] + +@mtkbuild sys = ODESystem(eqs, t) + +u0 = [D(x) => 2.0, + x => 1.0, + y => 0.0, + z => 0.0] + +p = [σ => 28.0, + ρ => 10.0, + β => 8 / 3] + +prob = SteadyStateProblem(sys, u0, p) +@test prob isa SteadyStateProblem +prob = SteadyStateProblem(ODEProblem(sys, u0, (0.0, 10.0), p)) +@test prob isa SteadyStateProblem + +# Issue#2344 +using ModelingToolkitStandardLibrary.Blocks + +function FML2(; name) + @parameters begin + k2[1:1] = [1.0] + end + systems = @named begin + constant = Constant(k = k2[1]) + end + @variables begin + x(t) = 0 + end + eqs = [ + D(x) ~ constant.output.u + k2[1] + ] + ODESystem(eqs, t; systems, name) +end + +@mtkbuild model = FML2() + +@test isequal(ModelingToolkit.defaults(model)[model.constant.k], model.k2[1]) +@test_nowarn ODEProblem(model, [], (0.0, 10.0)) + +# Issue#2477 +function RealExpression(; name, y) + vars = @variables begin + u(t) + end + eqns = [ + u ~ y + ] + sys = ODESystem(eqns, t, vars, []; name) +end + +function RealExpressionSystem(; name) + vars = @variables begin + x(t) + z(t)[1:1] + end # doing a collect on z doesn't work either. + @named e1 = RealExpression(y = x) # This works perfectly. + @named e2 = RealExpression(y = z[1]) # This bugs. However, `full_equations(e2)` works as expected. + systems = [e1, e2] + ODESystem(Equation[], t, Iterators.flatten(vars), []; systems, name) +end + +@named sys = RealExpressionSystem() +sys = complete(sys) +@test Set(equations(sys)) == Set([sys.e1.u ~ sys.x, sys.e2.u ~ sys.z[1]]) +tearing_state = TearingState(expand_connections(sys)) +ts_vars = tearing_state.fullvars +orig_vars = unknowns(sys) +@test isempty(setdiff(ts_vars, orig_vars)) + +# Guesses in hierarchical systems +@variables x(t) y(t) +@named sys = ODESystem(Equation[], t, [x], []; guesses = [x => 1.0]) +@named outer = ODESystem( + [D(y) ~ sys.x + t, 0 ~ t + y - sys.x * y], t, [y], []; systems = [sys]) +@test ModelingToolkit.guesses(outer)[sys.x] == 1.0 +outer = structural_simplify(outer) +@test ModelingToolkit.get_guesses(outer)[sys.x] == 1.0 +prob = ODEProblem(outer, [outer.y => 2.0], (0.0, 10.0)) +int = init(prob, Rodas4()) +@test int[outer.sys.x] == 1.0 + +# Ensure indexes of array symbolics are cached appropriately +@variables x(t)[1:2] +@named sys = ODESystem(Equation[], t, [x], []) +sys1 = complete(sys) +@named sys = ODESystem(Equation[], t, [x...], []) +sys2 = complete(sys) +for sys in [sys1, sys2] + for (sym, idx) in [(x, 1:2), (x[1], 1), (x[2], 2)] + @test is_variable(sys, sym) + @test variable_index(sys, sym) == idx + end +end + +@variables x(t)[1:2, 1:2] +@named sys = ODESystem(Equation[], t, [x], []) +sys1 = complete(sys) +@named sys = ODESystem(Equation[], t, [x...], []) +sys2 = complete(sys) +for sys in [sys1, sys2] + @test is_variable(sys, x) + @test variable_index(sys, x) == [1 3; 2 4] + for i in eachindex(x) + @test is_variable(sys, x[i]) + @test variable_index(sys, x[i]) == variable_index(sys, x)[i] + end +end + +# Namespacing of array variables +@variables x(t)[1:2] +@named sys = ODESystem(Equation[], t) +@test getname(unknowns(sys, x)) == :sys₊x +@test size(unknowns(sys, x)) == size(x) + +# Issue#2667 +@testset "ForwardDiff through ODEProblem constructor" begin + @parameters P + @variables x(t) + sys = structural_simplify(ODESystem([D(x) ~ P], t, [x], [P]; name = :sys)) + + function x_at_1(P) + prob = ODEProblem(sys, [x => 0.0], (0.0, 1.0), [sys.P => P]) + return solve(prob, Tsit5())(1.0) + end + + @test_nowarn ForwardDiff.derivative(P -> x_at_1(P), 1.0) +end + +@testset "Inplace observed functions" begin + @parameters P + @variables x(t) + sys = structural_simplify(ODESystem([D(x) ~ P], t, [x], [P]; name = :sys)) + obsfn = ModelingToolkit.build_explicit_observed_function( + sys, [x + 1, x + P, x + t], return_inplace = true)[2] + ps = ModelingToolkit.MTKParameters(sys, [P => 2.0]) + buffer = zeros(3) + @test_nowarn obsfn(buffer, [1.0], ps..., 3.0) + @test buffer ≈ [2.0, 3.0, 4.0] +end + +# https://github.com/SciML/ModelingToolkit.jl/issues/2818 +@testset "Custom independent variable" begin + @independent_variables x + @variables y(x) + @test_nowarn @named sys = ODESystem([y ~ 0], x) + + @variables x y(x) + @test_logs (:warn,) @named sys = ODESystem([y ~ 0], x) + + @parameters T + D = Differential(T) + @variables x(T) + eqs = [D(x) ~ 0.0] + initialization_eqs = [x ~ T] + guesses = [x => 0.0] + @named sys2 = ODESystem(eqs, T; initialization_eqs, guesses) + prob2 = ODEProblem(structural_simplify(sys2), [], (1.0, 2.0), []) + sol2 = solve(prob2) + @test all(sol2[x] .== 1.0) +end + +# https://github.com/SciML/ModelingToolkit.jl/issues/2502 +@testset "Extend systems with a field that can be nothing" begin + A = Dict(:a => 1) + B = Dict(:b => 2) + @named A1 = ODESystem(Equation[], t, [], []) + @named B1 = ODESystem(Equation[], t, [], []) + @named A2 = ODESystem(Equation[], t, [], []; metadata = A) + @named B2 = ODESystem(Equation[], t, [], []; metadata = B) + @test ModelingToolkit.get_metadata(extend(A1, B1)) == nothing + @test ModelingToolkit.get_metadata(extend(A1, B2)) == B + @test ModelingToolkit.get_metadata(extend(A2, B1)) == A + @test Set(ModelingToolkit.get_metadata(extend(A2, B2))) == Set(A ∪ B) +end + +# https://github.com/SciML/ModelingToolkit.jl/issues/2859 +@testset "Initialization with defaults from observed equations (edge case)" begin + @variables x(t) y(t) z(t) + eqs = [D(x) ~ 0, y ~ x, D(z) ~ 0] + defaults = [x => 1, z => y] + @named sys = ODESystem(eqs, t; defaults) + ssys = structural_simplify(sys) + prob = ODEProblem(ssys, [], (0.0, 1.0), []) + @test prob[x] == prob[y] == prob[z] == 1.0 + + @parameters y0 + @variables x(t) y(t) z(t) + eqs = [D(x) ~ 0, y ~ y0 / x, D(z) ~ y] + defaults = [y0 => 1, x => 1, z => y] + @named sys = ODESystem(eqs, t; defaults) + ssys = structural_simplify(sys) + prob = ODEProblem(ssys, [], (0.0, 1.0), []) + @test prob[x] == prob[y] == prob[z] == 1.0 +end diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index 5a059fe02f..598f9c3f27 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -180,7 +180,7 @@ end end @testset "time dependent var" begin - @parameters t + @independent_variables t @variables x(t) y @parameters a b loss = (a - x)^2 + b * (y - x^2)^2 diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index d55043383a..ef446e9630 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -7,6 +7,7 @@ using JumpProcesses using StableRNGs using SciMLStructures: canonicalize, Tunable, replace, replace! using SymbolicIndexingInterface +using NonlinearSolve @testset "ODESystem with callbacks" begin @parameters p1=1.0 p2=1.0 @@ -48,6 +49,110 @@ using SymbolicIndexingInterface @test integ.ps[p2] == 10.0 end +@testset "vector parameter deps" begin + @parameters p1[1:2]=[1.0, 2.0] p2[1:2]=[0.0, 0.0] + @variables x(t) = 0 + + @named sys = ODESystem( + [D(x) ~ sum(p1) * t + sum(p2)], + t; + parameter_dependencies = [p2 => 2p1] + ) + prob = ODEProblem(complete(sys)) + setp1! = setp(prob, p1) + get_p1 = getp(prob, p1) + get_p2 = getp(prob, p2) + setp1!(prob, [1.5, 2.5]) + + @test get_p1(prob) == [1.5, 2.5] + @test get_p2(prob) == [3.0, 5.0] +end + +@testset "extend" begin + @parameters p1=1.0 p2=1.0 + @variables x(t) = 0 + + @mtkbuild sys1 = ODESystem( + [D(x) ~ p1 * t + p2], + t + ) + @named sys2 = ODESystem( + [], + t; + parameter_dependencies = [p2 => 2p1] + ) + sys = extend(sys2, sys1) + @test isequal(only(parameters(sys)), p1) + @test Set(full_parameters(sys)) == Set([p1, p2]) + prob = ODEProblem(complete(sys)) + get_dep = getu(prob, 2p2) + @test get_dep(prob) == 4 +end + +@testset "getu with parameter deps" begin + @parameters p1=1.0 p2=1.0 + @variables x(t) = 0 + + @named sys = ODESystem( + [D(x) ~ p1 * t + p2], + t; + parameter_dependencies = [p2 => 2p1] + ) + prob = ODEProblem(complete(sys)) + get_dep = getu(prob, 2p2) + @test get_dep(prob) == 4 +end + +@testset "getu with vector parameter deps" begin + @parameters p1[1:2]=[1.0, 2.0] p2[1:2]=[0.0, 0.0] + @variables x(t) = 0 + + @named sys = ODESystem( + [D(x) ~ sum(p1) * t + sum(p2)], + t; + parameter_dependencies = [p2 => 2p1] + ) + prob = ODEProblem(complete(sys)) + get_dep = getu(prob, 2p1) + @test get_dep(prob) == [2.0, 4.0] +end + +@testset "composing systems with parameter deps" begin + @parameters p1=1.0 p2=2.0 + @variables x(t) = 0 + + @mtkbuild sys1 = ODESystem( + [D(x) ~ p1 * t + p2], + t + ) + @named sys2 = ODESystem( + [D(x) ~ p1 * t - p2], + t; + parameter_dependencies = [p2 => 2p1] + ) + sys = complete(ODESystem([], t, systems = [sys1, sys2], name = :sys)) + + prob = ODEProblem(sys) + v1 = sys.sys2.p2 + v2 = 2 * v1 + @test is_parameter(prob, v1) + @test is_observed(prob, v2) + get_v1 = getu(prob, v1) + get_v2 = getu(prob, v2) + @test get_v1(prob) == 2 + @test get_v2(prob) == 4 + + setp1! = setp(prob, sys2.p1) + setp1!(prob, 2.5) + @test prob.ps[sys2.p2] == 5.0 + + new_prob = remake(prob, p = [sys2.p1 => 1.5]) + + @test !isempty(ModelingToolkit.parameter_dependencies(sys2)) + @test new_prob.ps[sys2.p1] == 1.5 + @test new_prob.ps[sys2.p2] == 3.0 +end + @testset "Clock system" begin dt = 0.1 @variables x(t) y(t) u(t) yd(t) ud(t) r(t) z(t) @@ -62,28 +167,34 @@ end D(x) ~ -x + u y ~ x z(k) ~ z(k - 2) + yd(k - 2)] - @mtkbuild sys = ODESystem(eqs, t; parameter_dependencies = [kq => 2kp]) - - Tf = 1.0 - prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), - [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; yd(k - 2) => 2.0]) - @test_nowarn solve(prob, Tsit5(); kwargshandle = KeywordArgSilent) - - @mtkbuild sys = ODESystem(eqs, t; parameter_dependencies = [kq => 2kp], - discrete_events = [[0.5] => [kp ~ 2.0]]) - prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), - [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; yd(k - 2) => 2.0]) - @test prob.ps[kp] == 1.0 - @test prob.ps[kq] == 2.0 - @test_nowarn solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) - prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), - [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; yd(k - 2) => 2.0]) - integ = init(prob, Tsit5(), kwargshandle = KeywordArgSilent) - @test integ.ps[kp] == 1.0 - @test integ.ps[kq] == 2.0 - step!(integ, 0.6) - @test integ.ps[kp] == 2.0 - @test integ.ps[kq] == 4.0 + @test_throws ModelingToolkit.HybridSystemNotSupportedException @mtkbuild sys = ODESystem( + eqs, t; parameter_dependencies = [kq => 2kp]) + + @test_skip begin + Tf = 1.0 + prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), + [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; + yd(k - 2) => 2.0]) + @test_nowarn solve(prob, Tsit5(); kwargshandle = KeywordArgSilent) + + @mtkbuild sys = ODESystem(eqs, t; parameter_dependencies = [kq => 2kp], + discrete_events = [[0.5] => [kp ~ 2.0]]) + prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), + [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; + yd(k - 2) => 2.0]) + @test prob.ps[kp] == 1.0 + @test prob.ps[kq] == 2.0 + @test_nowarn solve(prob, Tsit5(), kwargshandle = KeywordArgSilent) + prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), + [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; + yd(k - 2) => 2.0]) + integ = init(prob, Tsit5(), kwargshandle = KeywordArgSilent) + @test integ.ps[kp] == 1.0 + @test integ.ps[kq] == 2.0 + step!(integ, 0.6) + @test integ.ps[kp] == 2.0 + @test integ.ps[kq] == 4.0 + end end @testset "SDESystem" begin @@ -162,6 +273,22 @@ end @test integ.ps[β] == 0.0002 end +@testset "NonlinearSystem" begin + @parameters p1=1.0 p2=1.0 + @variables x(t) + eqs = [0 ~ p1 * x * exp(x) + p2] + @mtkbuild sys = NonlinearSystem(eqs; parameter_dependencies = [p2 => 2p1]) + @test isequal(only(parameters(sys)), p1) + @test Set(full_parameters(sys)) == Set([p1, p2]) + prob = NonlinearProblem(sys, [x => 1.0]) + @test prob.ps[p1] == 1.0 + @test prob.ps[p2] == 2.0 + @test_nowarn solve(prob, NewtonRaphson()) + prob = NonlinearProblem(sys, [x => 1.0], [p1 => 2.0]) + @test prob.ps[p1] == 2.0 + @test prob.ps[p2] == 4.0 +end + @testset "SciMLStructures interface" begin @parameters p1=1.0 p2=1.0 @variables x(t) diff --git a/test/precompile_test.jl b/test/precompile_test.jl index 206459f500..6e31317de2 100644 --- a/test/precompile_test.jl +++ b/test/precompile_test.jl @@ -30,3 +30,11 @@ end @test parentmodule(typeof(ODEPrecompileTest.f_noeval_good.f.f_oop).parameters[2]) == ODEPrecompileTest @test ODEPrecompileTest.f_noeval_good(u, p, 0.1) == [4, 0, -16] + +ODEPrecompileTest.f_eval_bad(u, p, 0.1) + +@test parentmodule(typeof(ODEPrecompileTest.f_eval_good.f.f_iip)) == + ODEPrecompileTest +@test parentmodule(typeof(ODEPrecompileTest.f_eval_good.f.f_oop)) == + ODEPrecompileTest +@test ODEPrecompileTest.f_eval_good(u, p, 0.1) == [4, 0, -16] diff --git a/test/precompile_test/ModelParsingPrecompile.jl b/test/precompile_test/ModelParsingPrecompile.jl index 744e3c2954..87177d519f 100644 --- a/test/precompile_test/ModelParsingPrecompile.jl +++ b/test/precompile_test/ModelParsingPrecompile.jl @@ -1,6 +1,7 @@ module ModelParsingPrecompile using ModelingToolkit, Unitful +using ModelingToolkit: t @mtkmodel ModelWithComponentArray begin @constants begin diff --git a/test/precompile_test/ODEPrecompileTest.jl b/test/precompile_test/ODEPrecompileTest.jl index 3e25fa21e2..81187c4075 100644 --- a/test/precompile_test/ODEPrecompileTest.jl +++ b/test/precompile_test/ODEPrecompileTest.jl @@ -3,7 +3,8 @@ using ModelingToolkit function system(; kwargs...) # Define some variables - @parameters t σ ρ β + @independent_variables t + @parameters σ ρ β @variables x(t) y(t) z(t) D = Differential(t) @@ -27,4 +28,12 @@ const f_noeval_bad = system(; eval_expression = false) using RuntimeGeneratedFunctions RuntimeGeneratedFunctions.init(@__MODULE__) const f_noeval_good = system(; eval_expression = false, eval_module = @__MODULE__) + +# Eval the expression but into MTK's module, which means it won't be properly cached by +# the package image +const f_eval_bad = system(; eval_expression = true, eval_module = @__MODULE__) + +# Change the module the eval'd function is eval'd into to be the containing module, +# which should make it be in the package image +const f_eval_good = system(; eval_expression = true, eval_module = @__MODULE__) end diff --git a/test/runtests.jl b/test/runtests.jl index e029a4bdcc..263b91f1e9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,6 +24,7 @@ end @safetestset "Parsing Test" include("variable_parsing.jl") @safetestset "Simplify Test" include("simplify.jl") @safetestset "Direct Usage Test" include("direct.jl") + @safetestset "IndexCache Test" include("index_cache.jl") @safetestset "System Linearity Test" include("linearity.jl") @safetestset "Input Output Test" include("input_output_handling.jl") @safetestset "Clock Test" include("clock.jl") @@ -34,8 +35,11 @@ end @safetestset "Mass Matrix Test" include("mass_matrix.jl") @safetestset "SteadyStateSystem Test" include("steadystatesystems.jl") @safetestset "SDESystem Test" include("sdesystem.jl") + @safetestset "DDESystem Test" include("dde.jl") @safetestset "NonlinearSystem Test" include("nonlinearsystem.jl") @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") @safetestset "PDE Construction Test" include("pde.jl") @safetestset "JumpSystem Test" include("jumpsystem.jl") @safetestset "Constraints Test" include("constraints.jl") @@ -68,6 +72,7 @@ end @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") @safetestset "Initial Values Test" include("initial_values.jl") @safetestset "Discrete System" include("discrete_system.jl") + @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") end end @@ -90,6 +95,7 @@ end if GROUP == "All" || GROUP == "Downstream" activate_downstream_env() @safetestset "Linearization Tests" include("downstream/linearize.jl") + @safetestset "Linearization Dummy Derivative Tests" include("downstream/linearization_dd.jl") @safetestset "Inverse Models Test" include("downstream/inversemodel.jl") end @@ -98,5 +104,3 @@ end @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") end end - -@safetestset "Model Parsing Test" include("model_parsing.jl") diff --git a/test/sdesystem.jl b/test/sdesystem.jl index b18ab648e7..3d4fb6d311 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -458,13 +458,16 @@ fdif!(du, u0, p, t) # issue #819 @testset "Combined system name collisions" begin - @variables t + @independent_variables t + D = Differential(t) eqs_short = [D(x) ~ σ * (y - x), D(y) ~ x * (ρ - z) - y ] - sys1 = SDESystem(eqs_short, noiseeqs, t, [x, y, z], [σ, ρ, β], name = :sys1) - sys2 = SDESystem(eqs_short, noiseeqs, t, [x, y, z], [σ, ρ, β], name = :sys1) - @test_throws ArgumentError SDESystem([sys2.y ~ sys1.z], [], t, [], [], + noise_eqs = [y - x + x - y] + sys1 = SDESystem(eqs_short, noise_eqs, t, [x, y, z], [σ, ρ, β], name = :sys1) + sys2 = SDESystem(eqs_short, noise_eqs, t, [x, y, z], [σ, ρ, β], name = :sys1) + @test_throws ArgumentError SDESystem([sys2.y ~ sys1.z], [sys2.y], t, [], [], systems = [sys1, sys2], name = :foo) end @@ -614,3 +617,43 @@ sys2 = complete(sys2) prob = SDEProblem(sys1, sts .=> [1.0, 0.0, 0.0], (0.0, 100.0), ps .=> (10.0, 26.0)) solve(prob, LambaEulerHeun(), seed = 1) + +# Test ill-formed due to more equations than states in noise equations + +@independent_variables t +@parameters p d +@variables X(t) +eqs = [D(X) ~ p - d * X] +noise_eqs = [sqrt(p), -sqrt(d * X)] +@test_throws ArgumentError SDESystem(eqs, noise_eqs, t, [X], [p, d]; name = :ssys) + +noise_eqs = reshape([sqrt(p), -sqrt(d * X)], 1, 2) +ssys = SDESystem(eqs, noise_eqs, t, [X], [p, d]; name = :ssys) + +# SDEProblem construction with StaticArrays +# Issue#2814 +@parameters p d +@variables x(tt) +@brownian a +eqs = [D(x) ~ p - d * x + a * sqrt(p)] +@mtkbuild sys = System(eqs, tt) +u0 = @SVector[x => 10.0] +tspan = (0.0, 10.0) +ps = @SVector[p => 5.0, d => 0.5] +sprob = SDEProblem(sys, u0, tspan, ps) +@test !isinplace(sprob) +@test !isinplace(sprob.f) +@test_nowarn solve(sprob, ImplicitEM()) + +# Ensure diagonal noise generates vector noise function +@variables y(tt) +@brownian b +eqs = [D(x) ~ p - d * x + a * sqrt(p) + D(y) ~ p - d * y + b * sqrt(d)] +@mtkbuild sys = System(eqs, tt) +u0 = @SVector[x => 10.0, y => 20.0] +tspan = (0.0, 10.0) +ps = @SVector[p => 5.0, d => 0.5] +sprob = SDEProblem(sys, u0, tspan, ps) +@test sprob.f.g(sprob.u0, sprob.p, sprob.tspan[1]) isa SVector{2, Float64} +@test_nowarn solve(sprob, ImplicitEM()) diff --git a/test/serialization.jl b/test/serialization.jl index 94577b433a..5e09055a92 100644 --- a/test/serialization.jl +++ b/test/serialization.jl @@ -55,7 +55,10 @@ for var in all_obs push!(obs_exps, ex) end # observedfun expression for ODEFunctionExpr -observedfun_exp = :(function (var, u0, p, t) +observedfun_exp = :(function obs(var, u0, p, t) + if var isa AbstractArray + return obs.(var, (u0,), (p,), (t,)) + end name = ModelingToolkit.getname(var) $(obs_exps...) end) diff --git a/test/simplify.jl b/test/simplify.jl index 48bfafc731..4252e3262e 100644 --- a/test/simplify.jl +++ b/test/simplify.jl @@ -2,7 +2,7 @@ using ModelingToolkit using ModelingToolkit: value using Test -@parameters t +@independent_variables t @variables x(t) y(t) z(t) null_op = 0 * t @@ -37,7 +37,8 @@ d3 = Differential(x)(d2) # 699 using SymbolicUtils: substitute -@parameters t a(t) b(t) +@independent_variables t +@parameters a(t) b(t) # back and forth substitution does not work for parameters with dependencies term = value(a) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index 8bf09a480b..dea6af0b11 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -98,7 +98,7 @@ end # e5 [ 1 1 | 1 ] let state = TearingState(sys) - torn_matching = tearing(state) + torn_matching, = tearing(state) S = StructuralTransformations.reordered_matrix(sys, torn_matching) @test S == [1 0 0 0 1 1 1 0 0 0 diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index b15769e630..c9b4946d30 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -458,7 +458,7 @@ let ∂ₜ = D eqs = [∂ₜ(A) ~ -k * A] - @named ssys = SDESystem(eqs, Equation[], t, [A], [k, t1, t2], + @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) u0 = [A => 1.0] p = [k => 0.0, t1 => 1.0, t2 => 2.0] @@ -468,7 +468,7 @@ let cond1a = (t == t1) affect1a = [A ~ A + 1, B ~ A] cb1a = cond1a => affect1a - @named ssys1 = SDESystem(eqs, Equation[], t, [A, B], [k, t1, t2], + @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) u0′ = [A => 1.0, B => 0.0] sol = testsol( @@ -478,11 +478,11 @@ let # same as above - but with set-time event syntax cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once cb2‵ = [2.0] => affect2 - @named ssys‵ = SDESystem(eqs, Equation[], t, [A], [k], discrete_events = [cb1‵, cb2‵]) + @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) testsol(ssys‵, u0, p, tspan; paramtotest = k) # mixing discrete affects - @named ssys3 = SDESystem(eqs, Equation[], t, [A], [k, t1, t2], + @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) @@ -492,16 +492,16 @@ let nothing end cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named ssys4 = SDESystem(eqs, Equation[], t, [A], [k, t1], + @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) # mixing with symbolic condition in the func affect cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named ssys5 = SDESystem(eqs, Equation[], t, [A], [k, t1, t2], + @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - @named ssys6 = SDESystem(eqs, Equation[], t, [A], [k, t1, t2], + @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) @@ -509,7 +509,7 @@ let cond3 = A ~ 0.1 affect3 = [k ~ 0.0] cb3 = cond3 => affect3 - @named ssys7 = SDESystem(eqs, Equation[], t, [A], [k, t1, t2], + @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], continuous_events = [cb3]) sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 6ae3430c3e..7fd57c0474 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -85,3 +85,32 @@ analytic_function = (ps, t, x) -> -ps[1] * x * (x - 1) * sin(x) * exp(-2 * ps[1] @test isequal(pdesys.ps, [h]) @test isequal(parameter_symbols(pdesys), [h]) @test isequal(parameters(pdesys), [h]) + +# Issue#2767 +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using SymbolicIndexingInterface + +@parameters p1[1:2]=[1.0, 2.0] p2[1:2]=[0.0, 0.0] +@variables x(t) = 0 + +@named sys = ODESystem( + [D(x) ~ sum(p1) * t + sum(p2)], + t; +) +prob = ODEProblem(complete(sys)) +get_dep = @test_nowarn getu(prob, 2p1) +@test get_dep(prob) == [2.0, 4.0] + +@testset "Observed functions with variables as `Symbol`s" begin + @variables x(t) y(t) z(t)[1:2] + @parameters p1 p2[1:2, 1:2] + @mtkbuild sys = ODESystem([D(x) ~ x * t + p1, y ~ 2x, D(z) ~ p2 * z], t) + prob = ODEProblem( + sys, [x => 1.0, z => ones(2)], (0.0, 1.0), [p1 => 2.0, p2 => ones(2, 2)]) + @test getu(prob, x)(prob) == getu(prob, :x)(prob) + @test getu(prob, [x, y])(prob) == getu(prob, [:x, :y])(prob) + @test getu(prob, z)(prob) == getu(prob, :z)(prob) + @test getu(prob, p1)(prob) == getu(prob, :p1)(prob) + @test getu(prob, p2)(prob) == getu(prob, :p2)(prob) +end diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index cdaeae8b93..22832fed98 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -16,6 +16,24 @@ using ModelingToolkit @test hasguess(y) === true @test ModelingToolkit.dump_variable_metadata(y).guess == 0 +# Default +@variables y = 0 +@test ModelingToolkit.getdefault(y) === 0 +@test ModelingToolkit.hasdefault(y) === true +@test ModelingToolkit.dump_variable_metadata(y).default == 0 + +# Issue#2653 +@variables y[1:3] [guess = ones(3)] +@test getguess(y) == ones(3) +@test hasguess(y) === true +@test ModelingToolkit.dump_variable_metadata(y).guess == ones(3) + +for i in 1:3 + @test getguess(y[i]) == 1.0 + @test hasguess(y[i]) === true + @test ModelingToolkit.dump_variable_metadata(y[i]).guess == 1.0 +end + @variables y @test hasguess(y) === false @test !haskey(ModelingToolkit.dump_variable_metadata(y), :guess) @@ -55,7 +73,7 @@ d = FakeNormal() @test !haskey(ModelingToolkit.dump_variable_metadata(y), :dist) ## System interface -@parameters t +@independent_variables t Dₜ = Differential(t) @variables x(t)=0 [bounds = (-10, 10)] u(t)=0 [input = true] y(t)=0 [output = true] @parameters T [bounds = (0, Inf)] @@ -106,9 +124,21 @@ sp = Set(p) @test !hasdescription(u) @test !haskey(ModelingToolkit.dump_variable_metadata(u), :desc) -@parameters t +@independent_variables t @variables u(t) [description = "A short description of u"] @parameters p [description = "A description of p"] @named sys = ODESystem([u ~ p], t) @test_nowarn show(stdout, "text/plain", sys) + +# Defaults overridden by system, parameter dependencies +@variables x(t) = 1.0 +@parameters p=2.0 q +@named sys = ODESystem(Equation[], t, [x], [p]; defaults = Dict(x => 2.0, p => 3.0), + parameter_dependencies = [q => 2p]) +x_meta = ModelingToolkit.dump_unknowns(sys)[] +@test x_meta.default == 2.0 +params_meta = ModelingToolkit.dump_parameters(sys) +params_meta = Dict([ModelingToolkit.getname(meta.var) => meta for meta in params_meta]) +@test params_meta[:p].default == 3.0 +@test isequal(params_meta[:q].dependency, 2p) diff --git a/test/units.jl b/test/units.jl index e170540270..033a64c0e3 100644 --- a/test/units.jl +++ b/test/units.jl @@ -2,8 +2,9 @@ using ModelingToolkit, OrdinaryDiffEq, JumpProcesses, Unitful using Test MT = ModelingToolkit UMT = ModelingToolkit.UnitfulUnitCheck +@independent_variables t [unit = u"ms"] @parameters τ [unit = u"ms"] γ -@variables t [unit = u"ms"] E(t) [unit = u"kJ"] P(t) [unit = u"MW"] +@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] D = Differential(t) #This is how equivalent works: @@ -94,8 +95,9 @@ bad_length_eqs = [connect(op, lp)] @test_throws MT.ValidationError ODESystem(bad_eqs, t, [], []; name = :sys) # Array variables -@variables t [unit = u"s"] x(t)[1:3] [unit = u"m"] +@independent_variables t [unit = u"s"] @parameters v[1:3]=[1, 2, 3] [unit = u"m/s"] +@variables x(t)[1:3] [unit = u"m"] D = Differential(t) eqs = D.(x) .~ v ODESystem(eqs, t, name = :sys) @@ -109,8 +111,9 @@ eqs = [ @named nls = NonlinearSystem(eqs, [x], [a]) # SDE test w/ noise vector +@independent_variables t [unit = u"ms"] @parameters τ [unit = u"ms"] Q [unit = u"MW"] -@variables t [unit = u"ms"] E(t) [unit = u"kJ"] P(t) [unit = u"MW"] +@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] D = Differential(t) eqs = [D(E) ~ P - E / τ P ~ Q] @@ -130,8 +133,9 @@ noiseeqs = [0.1u"MW" 0.1u"MW" @test !UMT.validate(eqs, noiseeqs) # Non-trivial simplifications -@variables t [unit = u"s"] V(t) [unit = u"m"^3] L(t) [unit = u"m"] +@independent_variables t [unit = u"s"] @parameters v [unit = u"m/s"] r [unit = u"m"^3 / u"s"] +@variables V(t) [unit = u"m"^3] L(t) [unit = u"m"] D = Differential(t) eqs = [D(L) ~ v, V ~ L^3] diff --git a/test/variable_parsing.jl b/test/variable_parsing.jl index 14c6ca543e..1930b3273d 100644 --- a/test/variable_parsing.jl +++ b/test/variable_parsing.jl @@ -4,7 +4,7 @@ using Test using ModelingToolkit: value, Flow using SymbolicUtils: FnType -@parameters t +@independent_variables t @variables x(t) y(t) # test multi-arg @variables z(t) # test single-arg @@ -60,7 +60,7 @@ end # @test isequal(s1, collect(s)) # @test isequal(σ1, σ) -#@parameters t +#@independent_variables t #@variables x[1:2](t) #x1 = Num[Variable{FnType{Tuple{Any}, Real}}(:x, 1)(t.val), # Variable{FnType{Tuple{Any}, Real}}(:x, 2)(t.val)] @@ -104,6 +104,7 @@ end y = 2, [connect = Flow] end +@test_throws ErrorException ModelingToolkit.getdefault(x) @test !hasmetadata(x, VariableDefaultValue) @test getmetadata(x, VariableConnectType) == Flow @test getmetadata(x, VariableUnit) == u @@ -111,11 +112,13 @@ end @test getmetadata(y, VariableConnectType) == Flow a = rename(value(x), :a) -@test !hasmetadata(x, VariableDefaultValue) -@test getmetadata(x, VariableConnectType) == Flow -@test getmetadata(x, VariableUnit) == u +@test_throws ErrorException ModelingToolkit.getdefault(a) +@test !hasmetadata(a, VariableDefaultValue) +@test getmetadata(a, VariableConnectType) == Flow +@test getmetadata(a, VariableUnit) == u -@variables t x(t)=1 [connect = Flow, unit = u] +@independent_variables t +@variables x(t)=1 [connect = Flow, unit = u] @test getmetadata(x, VariableDefaultValue) == 1 @test getmetadata(x, VariableConnectType) == Flow diff --git a/test/variable_scope.jl b/test/variable_scope.jl index b80c004a2b..8c6e358c23 100644 --- a/test/variable_scope.jl +++ b/test/variable_scope.jl @@ -3,7 +3,7 @@ using ModelingToolkit: SymScope using Symbolics: arguments, value using Test -@parameters t +@independent_variables t @variables a b(t) c d e(t) b = ParentScope(b) @@ -52,7 +52,8 @@ end @test renamed([:foo :bar :baz], c) == Symbol("foo₊c") @test renamed([:foo :bar :baz], d) == :d -@parameters t a b c d e f +@independent_variables t +@parameters a b c d e f p = [a ParentScope(b) ParentScope(ParentScope(c)) @@ -73,3 +74,13 @@ ps = ModelingToolkit.getname.(parameters(level3)) @test isequal(ps[4], :level2₊level0₊d) @test isequal(ps[5], :level1₊level0₊e) @test isequal(ps[6], :f) + +# Issue@2252 +# Tests from PR#2354 +@parameters xx[1:2] +arr_p = [ParentScope(xx[1]), xx[2]] +arr0 = ODESystem(Equation[], t, [], arr_p; name = :arr0) +arr1 = ODESystem(Equation[], t, [], []; name = :arr1) ∘ arr0 +arr_ps = ModelingToolkit.getname.(parameters(arr1)) +@test isequal(arr_ps[1], Symbol("xx")) +@test isequal(arr_ps[2], Symbol("arr0₊xx")) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index 551614038b..8f3178f453 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -18,7 +18,8 @@ new = (((1 / β - 1) + δ) / γ)^(1 / (γ - 1)) # Continuous using ModelingToolkit: isdifferential, vars, collect_differential_variables, collect_ivs -@variables t u(t) y(t) +@independent_variables t +@variables u(t) y(t) D = Differential(t) eq = D(y) ~ u v = vars(eq)