diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 78ac0d17c0..552c507534 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -5,6 +5,7 @@ on: branches: - master - v10 + - 'as/su-v4' tags: '*' pull_request: @@ -24,7 +25,7 @@ jobs: version: 'lts' - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Install dependencies - run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ -e 'using Pkg; Pkg.develop([PackageSpec(path=pwd()), PackageSpec(path=joinpath(pwd(), "lib", "ModelingToolkitBase"))]); Pkg.instantiate()' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index 1a577c5811..b339c8b3c9 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -61,6 +61,7 @@ jobs: using Pkg try # force it to use this PR's version of the package + Pkg.develop(PackageSpec(path="./lib/ModelingToolkitBase")) # resolver may fail with main deps Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps Pkg.update() Pkg.test(coverage=true) # resolver may fail with test time deps diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index 0d3052b969..612ffe6a62 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -11,4 +11,4 @@ on: jobs: format-check: name: "Format Check" - uses: "SciML/.github/.github/workflows/format-suggestions-on-pr.yml@v1" + uses: "SciML/.github/.github/workflows/format-check.yml@v1" diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 67c6806708..8cca6f125c 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -5,7 +5,7 @@ on: branches: - master - 'release-' - - v10 + - as/su-v4 paths-ignore: - 'docs/**' push: @@ -31,22 +31,80 @@ jobs: - "1" - "lts" - "pre" - group: - - InterfaceI - - InterfaceII - - Initialization - - SymbolicIndexingInterface - - Extended - - Extensions - - Downstream - - RegressionI - - FMI - uses: "SciML/.github/.github/workflows/tests.yml@master" - with: - julia-version: "${{ matrix.version }}" - group: "${{ matrix.group }}" - # Disable cache for self-hosted runners since they persist between runs - # Set USE_SELF_HOSTED repository variable to 'true' when using self-hosted runners - self-hosted: ${{ vars.USE_SELF_HOSTED == 'true' }} - cache: ${{ vars.USE_SELF_HOSTED != 'true' }} - secrets: "inherit" + pkggroup: + - ModelingToolkit/InterfaceI + - ModelingToolkit/InterfaceII + - ModelingToolkit/Initialization + - ModelingToolkit/SymbolicIndexingInterface + - ModelingToolkit/Extensions + - ModelingToolkit/Downstream + - ModelingToolkit/FMI + - ModelingToolkitBase/InterfaceI + - ModelingToolkitBase/InterfaceII + - ModelingToolkitBase/Initialization + - ModelingToolkitBase/SymbolicIndexingInterface + - ModelingToolkitBase/Extended + - ModelingToolkitBase/RegressionI + - ModelingToolkitBase/Extensions + - SciCompDSL/All + runs-on: ${{ vars.USE_SELF_HOSTED == 'true' && 'self-hosted' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v5 + - name: "Setup Julia ${{ matrix.version }}" + uses: julia-actions/setup-julia@v2 + with: + version: "${{ matrix.version }}" + arch: "${{ runner.arch }}" + - uses: julia-actions/cache@v2 + if: ${{ vars.USE_SELF_HOSTED != 'true' }} + with: + token: "${{ secrets.GITHUB_TOKEN }}" + - name: "Test ${{ matrix.pkggroup }}" + env: + PKGGROUP: ${{ matrix.pkggroup }} + JULIA_PKG_PRECOMPILE_AUTO: 0 + shell: julia --color=yes --check-bounds=yes --depwarn=yes {0} + run: | + using Pkg + const PKGGROUP = ENV["PKGGROUP"] + const PKG = split(PKGGROUP, "/")[1] + const GROUP = split(PKGGROUP, "/")[2] + ENV["GROUP"] = GROUP + + if PKG == "ModelingToolkitBase" + @info "Testing ModelingToolkitBase" + Pkg.activate("lib/ModelingToolkitBase") + @info "`dev`ing SciCompDSL" + Pkg.develop(; path = "lib/SciCompDSL") + @info "Running tests" GROUP + Pkg.test() + elseif PKG == "ModelingToolkit" + @info "Testing ModelingToolkit" + Pkg.activate(".") + @info "`dev`ing ModelingToolkitBase" + Pkg.develop(; path = "lib/ModelingToolkitBase") + @info "`dev`ing SciCompDSL" + Pkg.develop(; path = "lib/SciCompDSL") + @info "Running tests" GROUP + Pkg.test() + elseif PKG == "SciCompDSL" + @info "Testing SciCompDSL" + Pkg.activate("lib/SciCompDSL") + @info "`dev`ing ModelingToolkitBase" + Pkg.develop(; path = "lib/ModelingToolkitBase") + @info "`dev`ing ModelingToolkit" + Pkg.develop(; path = ".") + @info "Running tests" GROUP + Pkg.test() + else + error("Invalid package $PKG") + end + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: "src,ext,lib/ModelingToolkitBase/src,lib/ModelingToolkitBase/ext" + - name: "Report Coverage with Codecov" + uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: "${{ secrets.CODECOV_TOKEN }}" + fail_ci_if_error: true diff --git a/LICENSE.md b/LICENSE.md index 947cd9843e..f25fcc1542 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -32,29 +32,3 @@ The ModelingToolkit.jl package is licensed under the MIT "Expat" License: > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE > > SOFTWARE. - -The code in `src/structural_transformation/bipartite_tearing/modia_tearing.jl`, -which is from the [Modia.jl](https://github.com/ModiaSim/Modia.jl) project, is -licensed as follows: - -MIT License - -Copyright (c) 2017-2018 ModiaSim developers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Project.toml b/Project.toml index de809aaf53..f354804ae1 100644 --- a/Project.toml +++ b/Project.toml @@ -1,142 +1,92 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "10.30.0" +version = "11.0.0" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" -AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" -ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" +BipartiteGraphs = "caf10ac8-0290-4205-88aa-f15908547e8d" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" -ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" -Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" -ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" -DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" -DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503" -DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" -Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf" -DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" -EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56" -ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" -FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" -FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" -ImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" -JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" -Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839" +ModelingToolkitTearing = "6bb917b9-1269-42b9-9f7c-b0dca72083ab" Moshi = "2e0e35c7-a2e4-4343-998d-7ef72827ed2d" -NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" SCCNonlinearSolve = "9dfe8606-65a1-4bb3-9748-cb89d1561431" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" -SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StateSelection = "64909d44-ed92-46a8-bbd9-f047dfbdc84b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [weakdeps] -BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" -CasADi = "c49709b8-5c63-11e9-2fb2-69db5844192f" -DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" -InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" -LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" -Pyomo = "0e8e1daf-01b5-4eba-a626-3897743a3816" + +[sources] +ModelingToolkitBase = {subdir = "lib/ModelingToolkitBase"} +ModelingToolkitStandardLibrary = {rev = "as/mtk-v11", url = "https://github.com/SciML/ModelingToolkitStandardLibrary.jl"} +ModelingToolkitTearing = {rev = "main", url = "https://github.com/JuliaComputing/StateSelection.jl/", subdir = "lib/ModelingToolkitTearing"} +Optimization = {rev = "as/symbolics-v7", url = "https://github.com/AayushSabharwal/Optimization.jl"} +OptimizationBase = { url = "https://github.com/AayushSabharwal/Optimization.jl", rev = "as/symbolics-v7", subdir = "lib/OptimizationBase" } +OptimizationMOI = {rev = "as/symbolics-v7", subdir = "lib/OptimizationMOI", url = "https://github.com/AayushSabharwal/Optimization.jl"} +SciCompDSL = {subdir = "lib/SciCompDSL"} +StateSelection = {rev = "main", url = "https://github.com/JuliaComputing/StateSelection.jl/"} [extensions] -MTKBifurcationKitExt = "BifurcationKit" -MTKCasADiDynamicOptExt = "CasADi" -MTKDeepDiffsExt = "DeepDiffs" MTKFMIExt = "FMI" -MTKInfiniteOptExt = "InfiniteOpt" -MTKLabelledArraysExt = "LabelledArrays" -MTKPyomoDynamicOptExt = "Pyomo" [compat] ADTypes = "1.14.0" -AbstractTrees = "0.3, 0.4" -ArrayInterface = "6, 7" -BifurcationKit = "0.4, 0.5" -BlockArrays = "1.1" +BipartiteGraphs = "0.1.2" +BlockArrays = "1.9.3" BoundaryValueDiffEqAscher = "1.6.0" BoundaryValueDiffEqMIRK = "1.7.0" -CasADi = "1.0.7" -ChainRulesCore = "1" Combinatorics = "1" CommonSolve = "0.2.4" -Compat = "3.42, 4" -ConstructionBase = "1" DataInterpolations = "7, 8" DataStructures = "0.17, 0.18, 0.19" -DeepDiffs = "1" DelayDiffEq = "5.61" DiffEqBase = "6.189.1" -DiffEqCallbacks = "2.16, 3, 4" -DiffEqNoiseProcess = "5" -DiffRules = "0.1, 1.0" DifferentiationInterface = "0.6.47, 0.7" -Distributed = "1" -Distributions = "0.23, 0.24, 0.25" DocStringExtensions = "0.7, 0.8, 0.9" -DomainSets = "0.6, 0.7" -DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" -EnumX = "1.0.4" -ExprTools = "0.1.10" FMI = "0.14" FillArrays = "1.13.0" FindFirstFunctions = "1" ForwardDiff = "0.10.3, 1" -FunctionWrappers = "1.1" -FunctionWrappersWrappers = "0.1" Graphs = "1.5.2" -ImplicitDiscreteSolve = "0.1.2, 1" -InfiniteOpt = "0.6" InteractiveUtils = "1" -JuliaFormatter = "1.0.47, 2" -JumpProcesses = "9.19" -LabelledArrays = "1.3" -Latexify = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16" Libdl = "1" LinearAlgebra = "1" LinearSolve = "3.19.2" Logging = "1" -MLStyle = "0.4.17" +ModelingToolkitBase = "1" ModelingToolkitStandardLibrary = "2.20" Moshi = "0.3" -NaNMath = "0.3, 1" NonlinearSolve = "4.3" OffsetArrays = "1" OrderedCollections = "1" @@ -146,44 +96,44 @@ OrdinaryDiffEqDefault = "1.2" OrdinaryDiffEqNonlinearSolve = "1.5.0" PreallocationTools = "0.4.27" PrecompileTools = "1" -Pyomo = "0.1.0" REPL = "1" -RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.4.0" SciMLBase = "2.125.0" SciMLPublic = "1.0.0" -SciMLStructures = "1.7" Serialization = "1" Setfield = "0.7, 0.8, 1" SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" -SpecialFunctions = "1, 2" StaticArrays = "1.9.14" StochasticDelayDiffEq = "1.11" StochasticDiffEq = "6.82.0" SymbolicIndexingInterface = "0.3.39" -SymbolicUtils = "3.30.0" -Symbolics = "6.40" -URIs = "1" +SymbolicUtils = "4.5.1" +Symbolics = "7" UnPack = "0.1, 1.0" -Unitful = "1.1" julia = "1.9" [extras] AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" BoundaryValueDiffEqAscher = "7227322d-7511-4e07-9247-ad6ff830280e" BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" -DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" +DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" +DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503" +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" 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" +JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" @@ -199,8 +149,12 @@ OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciCompDSL = "91a8cdf1-4ca6-467b-a780-87fda3fff15e" +SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f" @@ -208,6 +162,8 @@ StochasticDelayDiffEq = "29a0d76e-afc8-11e9-03a4-eda52ae4b960" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestEnv = "1e6cf692-eddd-4d53-88a5-2d735e33781b" +URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve", "Latexify", "Distributed", "DiffEqNoiseProcess", "DynamicQuantities", "DiffEqCallbacks", "URIs", "JumpProcesses", "RecursiveArrayTools", "SciMLStructures", "SpecialFunctions", "SciCompDSL"] diff --git a/docs/Project.toml b/docs/Project.toml index c61a42c7d6..0ba55263f8 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -32,7 +32,6 @@ StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] Attractors = "1.24" @@ -65,4 +64,3 @@ StochasticDiffEq = "6" SymbolicIndexingInterface = "0.3.1" SymbolicUtils = "3, 4" Symbolics = "6" -Unitful = "1.12" diff --git a/docs/src/API/model_building.md b/docs/src/API/model_building.md index 7a8389fd2f..6ad624298e 100644 --- a/docs/src/API/model_building.md +++ b/docs/src/API/model_building.md @@ -13,8 +13,6 @@ ModelingToolkit.t_nounits ModelingToolkit.D_nounits ModelingToolkit.t ModelingToolkit.D -ModelingToolkit.t_unitful -ModelingToolkit.D_unitful ``` Users are recommended to use the appropriate common definition in their models. The required diff --git a/docs/src/basics/FAQ.md b/docs/src/basics/FAQ.md index 2511c00675..7b712395b3 100644 --- a/docs/src/basics/FAQ.md +++ b/docs/src/basics/FAQ.md @@ -192,9 +192,7 @@ p, replace, alias = SciMLStructures.canonicalize(Tunable(), prob.p) # changes to the array will be reflected in parameter values ``` -See the [basic example on optimizing](https://docs.sciml.ai/ModelingToolkit/dev/examples/remake/#Optimizing-through-an-ODE-solve-and-re-creating-MTK-Problems) for combining these steps to optimizing parameters and use ForwardDiff.jl as the backend for Automatic Differentiation. - -# ERROR: ArgumentError: SymbolicUtils.BasicSymbolic{Real}[xˍt(t)] are missing from the variable map. +# ERROR: ArgumentError: `[xˍt(t)]` are missing from the variable map. This error can come up after running `mtkcompile` 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. diff --git a/docs/src/basics/Validation.md b/docs/src/basics/Validation.md index 3f36a06e5e..651cf3f477 100644 --- a/docs/src/basics/Validation.md +++ b/docs/src/basics/Validation.md @@ -155,7 +155,7 @@ future when `ModelingToolkit` is extended to support eliminating `DynamicQuantit ## Other Restrictions -`Unitful` provides non-scalar units such as `dBm`, `°C`, etc. At this time, `ModelingToolkit` only supports scalar quantities. Additionally, angular degrees (`°`) are not supported because trigonometric functions will treat plain numerical values as radians, which would lead systems validated using degrees to behave erroneously when being solved. +`DynamicQuantities` provides non-scalar units such as `°C`, etc. At this time, `ModelingToolkit` only supports scalar quantities. Additionally, angular degrees (`°`) are not supported because trigonometric functions will treat plain numerical values as radians, which would lead systems validated using degrees to behave erroneously when being solved. ## Troubleshooting & Gotchas @@ -169,7 +169,7 @@ Parameter and initial condition values are supplied to problem constructors as p ```julia function remove_units(p::Dict) - Dict(k => Unitful.ustrip(ModelingToolkit.get_unit(k), v) for (k, v) in p) + Dict(k => DynamicQuantities.ustrip(ModelingToolkit.get_unit(k), v) for (k, v) in p) end add_units(p::Dict) = Dict(k => v * ModelingToolkit.get_unit(k) for (k, v) in p) ``` diff --git a/docs/src/internals.md b/docs/src/internals.md index 5b04714e8f..db0f3219c4 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -14,3 +14,9 @@ These components work together to enable ModelingToolkit's symbolic manipulation !!! warning The functions and types documented in this section are internal implementation details. Users should not rely on these APIs as they may change or be removed without deprecation warnings. + +## Misc + +- Bindings, initial conditions and guesses are stored as `AtomicArrayDict`. This is a custom wrapper which only + supports symbolic keys, and disallows keys which are indexed array variables. +- Keys of parameter bindings cannot be present in `get_ps(sys)`. diff --git a/ext/MTKDeepDiffsExt.jl b/ext/MTKDeepDiffsExt.jl deleted file mode 100644 index a24a638d32..0000000000 --- a/ext/MTKDeepDiffsExt.jl +++ /dev/null @@ -1,190 +0,0 @@ -module MTKDeepDiffsExt - -using DeepDiffs, ModelingToolkit -using ModelingToolkit.BipartiteGraphs: Label, - BipartiteAdjacencyList, unassigned, - HighlightInt -using ModelingToolkit: SystemStructure, - MatchedSystemStructure, - SystemStructurePrintMatrix - -""" -A utility struct for displaying the difference between two HighlightInts. - -# Example -```julia -using ModelingToolkit, DeepDiffs - -old_i = HighlightInt(1, :default, true) -new_i = HighlightInt(2, :default, false) -diff = HighlightIntDiff(new_i, old_i) - -show(diff) -``` -""" -struct HighlightIntDiff - new::HighlightInt - old::HighlightInt -end - -function Base.show(io::IO, d::HighlightIntDiff) - p_color = d.new.highlight - (d.new.match && !d.old.match) && (p_color = :light_green) - (!d.new.match && d.old.match) && (p_color = :light_red) - - (d.new.match || d.old.match) && printstyled(io, "(", color = p_color) - if d.new.i != d.old.i - Base.show(io, HighlightInt(d.old.i, :light_red, d.old.match)) - print(io, " ") - Base.show(io, HighlightInt(d.new.i, :light_green, d.new.match)) - else - Base.show(io, HighlightInt(d.new.i, d.new.highlight, false)) - end - (d.new.match || d.old.match) && printstyled(io, ")", color = p_color) -end - -""" -A utility struct for displaying the difference between two -BipartiteAdjacencyList's. - -# Example -```julia -using ModelingToolkit, DeepDiffs - -old = BipartiteAdjacencyList(...) -new = BipartiteAdjacencyList(...) -diff = BipartiteAdjacencyListDiff(new, old) - -show(diff) -``` -""" -struct BipartiteAdjacencyListDiff - new::BipartiteAdjacencyList - old::BipartiteAdjacencyList -end - -function Base.show(io::IO, l::BipartiteAdjacencyListDiff) - print(io, - LabelDiff(Label(l.new.match === true ? "∫ " : ""), - Label(l.old.match === true ? "∫ " : ""))) - (l.new.match !== true && l.old.match !== true) && print(io, " ") - - new_nonempty = isnothing(l.new.u) ? nothing : !isempty(l.new.u) - old_nonempty = isnothing(l.old.u) ? nothing : !isempty(l.old.u) - if new_nonempty === true && old_nonempty === true - if (!isempty(setdiff(l.new.highlight_u, l.new.u)) || - !isempty(setdiff(l.old.highlight_u, l.old.u))) - throw(ArgumentError("The provided `highlight_u` must be a sub-graph of `u`.")) - end - - new_items = Dict(i => HighlightInt(i, :nothing, i === l.new.match) for i in l.new.u) - old_items = Dict(i => HighlightInt(i, :nothing, i === l.old.match) for i in l.old.u) - - highlighted = union(map(intersect(l.new.u, l.old.u)) do i - HighlightIntDiff(new_items[i], old_items[i]) - end, - map(setdiff(l.new.u, l.old.u)) do i - HighlightInt(new_items[i].i, :light_green, - new_items[i].match) - end, - map(setdiff(l.old.u, l.new.u)) do i - HighlightInt(old_items[i].i, :light_red, - old_items[i].match) - end) - print(IOContext(io, :typeinfo => typeof(highlighted)), highlighted) - elseif new_nonempty === true - printstyled( - io, map(l.new.u) do i - HighlightInt(i, :nothing, i === l.new.match) - end, color = :light_green) - elseif old_nonempty === true - printstyled( - io, map(l.old.u) do i - HighlightInt(i, :nothing, i === l.old.match) - end, color = :light_red) - elseif old_nonempty !== nothing || new_nonempty !== nothing - print(io, - LabelDiff(Label(new_nonempty === false ? "∅" : "", :light_black), - Label(old_nonempty === false ? "∅" : "", :light_black))) - else - printstyled(io, '⋅', color = :light_black) - end -end - -""" -A utility struct for displaying the difference between two Labels -in git-style red/green highlighting. - -# Example -```julia -using ModelingToolkit, DeepDiffs - -old = Label("before") -new = Label("after") -diff = LabelDiff(new, old) - -show(diff) -``` -""" -struct LabelDiff - new::Label - old::Label -end -function Base.show(io::IO, l::LabelDiff) - if l.new != l.old - printstyled(io, l.old.s, color = :light_red) - length(l.new.s) != 0 && length(l.old.s) != 0 && print(io, " ") - printstyled(io, l.new.s, color = :light_green) - else - print(io, l.new) - end -end - -""" -A utility struct for displaying the difference between two -(Matched)SystemStructure's in git-style red/green highlighting. - -# Example -```julia -using ModelingToolkit, DeepDiffs - -old = SystemStructurePrintMatrix(...) -new = SystemStructurePrintMatrix(...) -diff = SystemStructureDiffPrintMatrix(new, old) - -show(diff) -``` -""" -struct SystemStructureDiffPrintMatrix <: - AbstractMatrix{Union{LabelDiff, BipartiteAdjacencyListDiff}} - new::SystemStructurePrintMatrix - old::SystemStructurePrintMatrix -end - -function Base.size(ssdpm::SystemStructureDiffPrintMatrix) - max.(Base.size(ssdpm.new), Base.size(ssdpm.old)) -end - -function Base.getindex(ssdpm::SystemStructureDiffPrintMatrix, i::Integer, j::Integer) - checkbounds(ssdpm, i, j) - if i > 1 && (j == 4 || j == 9) - old = new = BipartiteAdjacencyList(nothing, nothing, unassigned) - (i <= size(ssdpm.new, 1)) && (new = ssdpm.new[i, j]) - (i <= size(ssdpm.old, 1)) && (old = ssdpm.old[i, j]) - BipartiteAdjacencyListDiff(new, old) - else - old = new = Label("") - (i <= size(ssdpm.new, 1)) && (new = ssdpm.new[i, j]) - (i <= size(ssdpm.old, 1)) && (old = ssdpm.old[i, j]) - LabelDiff(new, old) - end -end - -function DeepDiffs.deepdiff(old::Union{MatchedSystemStructure, SystemStructure}, - new::Union{MatchedSystemStructure, SystemStructure}) - new_sspm = SystemStructurePrintMatrix(new) - old_sspm = SystemStructurePrintMatrix(old) - Base.print_matrix(stdout, SystemStructureDiffPrintMatrix(new_sspm, old_sspm)) -end - -end # module diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 87ed6662d4..eb4b500c4f 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -137,7 +137,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # CS FMUs do their own independent integration in a periodic callback, so their # unknowns are discrete variables in the `ODESystem`. A default of `missing` allows # them to be solved for during initialization. - @parameters __mtk_internal_u(t)[1:length(diffvars)]=missing [guess = diffvars] + @discretes __mtk_internal_u(t)[1:length(diffvars)]=missing [guess = diffvars] push!(observed, __mtk_internal_u ~ copy(diffvars)) end @@ -178,7 +178,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, if isempty(outputs) __mtk_internal_o = Float64[] else - @parameters __mtk_internal_o(t)[1:length(outputs)]=missing [guess = zeros(length(outputs))] + @discretes __mtk_internal_o(t)[1:length(outputs)]=missing [guess = zeros(length(outputs))] push!(observed, __mtk_internal_o ~ outputs) end end @@ -289,7 +289,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, end eqs = [observed; diffeqs] - return System(eqs, t, states, params; parameter_dependencies, defaults = defs, + bindings = [eq.lhs => eq.rhs for eq in parameter_dependencies] + return System(eqs, t, states, params; bindings, initial_conditions = defs, discrete_events = [instance_management_callback], name, initialization_eqs) end diff --git a/ext/MTKLabelledArraysExt.jl b/ext/MTKLabelledArraysExt.jl deleted file mode 100644 index c10400b109..0000000000 --- a/ext/MTKLabelledArraysExt.jl +++ /dev/null @@ -1,18 +0,0 @@ -module MTKLabelledArraysExt - -using ModelingToolkit, LabelledArrays -using ModelingToolkit: _defvar, toparam, variable, varnames_length_check -function ModelingToolkit.define_vars(u::Union{SLArray, LArray}, t) - [ModelingToolkit._defvar(x)(t) for x in LabelledArrays.symnames(typeof(u))] -end - -function ModelingToolkit.define_params(p::Union{SLArray, LArray}, t, 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 - -end diff --git a/lib/ModelingToolkitBase/LICENSE.md b/lib/ModelingToolkitBase/LICENSE.md new file mode 100644 index 0000000000..a4bd9d9fd8 --- /dev/null +++ b/lib/ModelingToolkitBase/LICENSE.md @@ -0,0 +1,22 @@ +The ModelingToolkitBase.jl package is licensed under the MIT "Expat" License: + +Copyright (c) 2018-22: Yingbo Ma, Christopher Rackauckas, Julia Computing, and +contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/ModelingToolkitBase/Project.toml b/lib/ModelingToolkitBase/Project.toml new file mode 100644 index 0000000000..3a8e7e821a --- /dev/null +++ b/lib/ModelingToolkitBase/Project.toml @@ -0,0 +1,216 @@ +name = "ModelingToolkitBase" +uuid = "7771a370-6774-4173-bd38-47e70ca0b839" +authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] +version = "1.0.0" + +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" +BipartiteGraphs = "caf10ac8-0290-4205-88aa-f15908547e8d" +BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" +DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf" +EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" +FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +ImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Moshi = "2e0e35c7-a2e4-4343-998d-7ef72827ed2d" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ReadOnlyDicts = "795d4caa-f5a7-4580-b5d8-c01d53451803" +RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SciMLPublic = "431bcebd-1456-4ced-9d72-93c2757fff0b" +SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" +SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" +SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed" + +[weakdeps] +BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" +CasADi = "c49709b8-5c63-11e9-2fb2-69db5844192f" +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503" +DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +Pyomo = "0e8e1daf-01b5-4eba-a626-3897743a3816" + +[sources] +ModelingToolkitStandardLibrary = {rev = "as/mtk-v11", url = "https://github.com/SciML/ModelingToolkitStandardLibrary.jl"} +Optimization = {rev = "as/symbolics-v7", url = "https://github.com/AayushSabharwal/Optimization.jl"} +OptimizationBase = {rev = "as/symbolics-v7", subdir = "lib/OptimizationBase", url = "https://github.com/AayushSabharwal/Optimization.jl"} +OptimizationMOI = {rev = "as/symbolics-v7", subdir = "lib/OptimizationMOI", url = "https://github.com/AayushSabharwal/Optimization.jl"} + +[extensions] +MTKBifurcationKitExt = "BifurcationKit" +MTKCasADiDynamicOptExt = "CasADi" +MTKChainRulesCoreExt = "ChainRulesCore" +MTKDiffEqNoiseProcessExt = "DiffEqNoiseProcess" +MTKDynamicQuantitiesExt = "DynamicQuantities" +MTKInfiniteOptExt = "InfiniteOpt" +MTKJuliaFormatterExt = "JuliaFormatter" +MTKLabelledArraysExt = "LabelledArrays" +MTKLatexifyExt = "Latexify" +MTKPyomoDynamicOptExt = "Pyomo" + +[compat] +ADTypes = "1.14.0" +AbstractTrees = "0.3, 0.4" +ArrayInterface = "6, 7" +BifurcationKit = "0.4, 0.5" +BipartiteGraphs = "0.1.0" +BlockArrays = "1.1" +BoundaryValueDiffEqAscher = "1.6.0" +BoundaryValueDiffEqMIRK = "1.7.0" +CasADi = "1.0.7" +ChainRulesCore = "1" +Combinatorics = "1" +CommonSolve = "0.2.4" +Compat = "3.42, 4" +ConstructionBase = "1" +DataInterpolations = "8.7" +DataStructures = "0.17, 0.18, 0.19" +DelayDiffEq = "5.61" +DiffEqBase = "6.189.1" +DiffEqCallbacks = "2.16, 3, 4" +DiffEqNoiseProcess = "5" +DiffRules = "0.1, 1.0" +DifferentiationInterface = "0.6.47, 0.7" +DocStringExtensions = "0.7, 0.8, 0.9" +DomainSets = "0.6, 0.7" +DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" +EnumX = "1.0.4" +ExprTools = "0.1.10" +FillArrays = "1.13.0" +FindFirstFunctions = "1" +ForwardDiff = "0.10.3, 1" +FunctionWrappers = "1.1" +FunctionWrappersWrappers = "0.1" +Graphs = "1.5.2" +ImplicitDiscreteSolve = "0.1.2, 1" +InfiniteOpt = "0.6" +InteractiveUtils = "1" +JuliaFormatter = "1.0.47, 2" +JumpProcesses = "9.19" +LabelledArrays = "1.3" +Latexify = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16" +Libdl = "1" +LinearAlgebra = "1" +LinearSolve = "3.19.2" +Logging = "1" +ModelingToolkitStandardLibrary = "2.20" +Moshi = "0.3" +NaNMath = "0.3, 1" +NonlinearSolve = "4.3" +OffsetArrays = "1" +OrderedCollections = "1" +OrdinaryDiffEq = "6.82.0" +OrdinaryDiffEqCore = "1.34.0" +OrdinaryDiffEqDefault = "1.2" +OrdinaryDiffEqNonlinearSolve = "1.5.0" +PreallocationTools = "0.4.27" +PrecompileTools = "1" +Pyomo = "0.1.0" +REPL = "1" +Random = "1" +ReadOnlyDicts = "1.0.0" +RecursiveArrayTools = "3.26" +Reexport = "0.2, 1" +RuntimeGeneratedFunctions = "0.5.9" +SciMLBase = "2.125.0" +SciMLPublic = "1.0.0" +SciMLStructures = "1.7" +Serialization = "1" +Setfield = "0.7, 0.8, 1" +SimpleNonlinearSolve = "0.1.0, 1, 2" +SparseArrays = "1" +SpecialFunctions = "1, 2" +StaticArrays = "1.9.14" +StochasticDelayDiffEq = "1.11" +StochasticDiffEq = "6.82.0" +SymbolicIndexingInterface = "0.3.39" +SymbolicUtils = "4.6" +Symbolics = "7.1.1" +UnPack = "0.1, 1.0" +julia = "1.9" + +[extras] +AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +BoundaryValueDiffEqAscher = "7227322d-7511-4e07-9247-ad6ff830280e" +BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" +ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" +DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" +DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" +DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503" +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +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" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" +NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" +OptimizationBase = "bca83a33-5cc9-4baa-983d-23429ab6bcbb" +OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" +OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" +OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f" +StochasticDelayDiffEq = "29a0d76e-afc8-11e9-03a4-eda52ae4b960" +StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" +Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve", "Latexify", "Distributed", "DiffEqNoiseProcess", "DynamicQuantities"] diff --git a/ext/MTKBifurcationKitExt.jl b/lib/ModelingToolkitBase/ext/MTKBifurcationKitExt.jl similarity index 92% rename from ext/MTKBifurcationKitExt.jl rename to lib/ModelingToolkitBase/ext/MTKBifurcationKitExt.jl index 6a85fe66cd..e75b1af380 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/lib/ModelingToolkitBase/ext/MTKBifurcationKitExt.jl @@ -3,7 +3,7 @@ module MTKBifurcationKitExt ### Preparations ### # Imports -using ModelingToolkit, Setfield +using ModelingToolkitBase, Setfield import BifurcationKit using SymbolicIndexingInterface: is_time_dependent @@ -92,7 +92,7 @@ function BifurcationKit.BifurcationProblem(nsys::System, record_from_solution = BifurcationKit.record_sol_default, jac = true, kwargs...) - if !ModelingToolkit.iscomplete(nsys) + if !ModelingToolkitBase.iscomplete(nsys) error("A completed `System` is required. Call `complete` or `structural_simplify` on the system before creating a `BifurcationProblem`") end if is_time_dependent(nsys) @@ -113,12 +113,12 @@ function BifurcationKit.BifurcationProblem(nsys::System, J = jac ? ofun.jac : nothing # Converts the input state guess. - u0_bif = ModelingToolkit.to_varmap(u0_bif, unknowns(nsys)) - u0_buf = merge(ModelingToolkit.get_defaults(nsys), u0_bif) - u0_bif_vals = ModelingToolkit.varmap_to_vars(u0_bif, unknowns(nsys)) - ps = ModelingToolkit.to_varmap(ps, parameters(nsys)) - ps = merge(ModelingToolkit.get_defaults(nsys), ps) - p_vals = ModelingToolkit.varmap_to_vars(ps, parameters(nsys)) + u0_bif = ModelingToolkitBase.to_varmap(u0_bif, unknowns(nsys)) + u0_buf = merge(ModelingToolkitBase.get_initial_conditions(nsys), u0_bif) + u0_bif_vals = ModelingToolkitBase.varmap_to_vars(u0_bif, unknowns(nsys)) + ps = ModelingToolkitBase.to_varmap(ps, parameters(nsys)) + ps = merge(ModelingToolkitBase.get_initial_conditions(nsys), ps) + p_vals = ModelingToolkitBase.varmap_to_vars(ps, parameters(nsys)) # Computes bifurcation parameter and the plotting function. bif_idx = findfirst(isequal(bif_par), parameters(nsys)) diff --git a/ext/MTKCasADiDynamicOptExt.jl b/lib/ModelingToolkitBase/ext/MTKCasADiDynamicOptExt.jl similarity index 94% rename from ext/MTKCasADiDynamicOptExt.jl rename to lib/ModelingToolkitBase/ext/MTKCasADiDynamicOptExt.jl index f61cec4274..a391389799 100644 --- a/ext/MTKCasADiDynamicOptExt.jl +++ b/lib/ModelingToolkitBase/ext/MTKCasADiDynamicOptExt.jl @@ -1,10 +1,11 @@ module MTKCasADiDynamicOptExt -using ModelingToolkit +using ModelingToolkitBase using CasADi using DiffEqBase using UnPack using NaNMath -const MTK = ModelingToolkit +using Symbolics: SymbolicT +const MTK = ModelingToolkitBase for ff in [acos, log1p, acosh, log2, asin, tan, atanh, cos, log, sin, log10, sqrt] f = nameof(ff) @@ -114,11 +115,11 @@ end function MTK.add_constraint!(m::CasADiModel, expr) if expr isa Equation - subject_to!(m.model, expr.lhs - expr.rhs == 0) + subject_to!(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) == 0) elseif expr.relational_op === Symbolics.geq - subject_to!(m.model, expr.lhs - expr.rhs ≥ 0) + subject_to!(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) ≥ 0) else - subject_to!(m.model, expr.lhs - expr.rhs ≤ 0) + subject_to!(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) ≤ 0) end end @@ -133,10 +134,11 @@ end function MTK.lowered_var(m::CasADiModel, uv, i, t) X = getfield(m, uv) - t isa Union{Num, Symbolics.Symbolic} ? X.u[i, :] : X(t)[i] + t isa Union{Num, SymbolicT} ? X.u[i, :] : X(t)[i] end function MTK.lowered_integral(model::CasADiModel, expr, lo, hi) + expr = SymbolicUtils.unwrap_const(expr) total = MX(0) dt = model.U.t[2] - model.U.t[1] for (i, t) in enumerate(model.U.t) diff --git a/src/adjoints.jl b/lib/ModelingToolkitBase/ext/MTKChainRulesCoreExt.jl similarity index 85% rename from src/adjoints.jl rename to lib/ModelingToolkitBase/ext/MTKChainRulesCoreExt.jl index 98266de938..23e9996847 100644 --- a/src/adjoints.jl +++ b/lib/ModelingToolkitBase/ext/MTKChainRulesCoreExt.jl @@ -1,3 +1,14 @@ +module MTKChainRulesCoreExt + +import ChainRulesCore +import ChainRulesCore: Tangent, ZeroTangent, NoTangent, zero_tangent, unthunk +using ModelingToolkitBase: MTKParameters, NONNUMERIC_PORTION, AbstractSystem +import ModelingToolkitBase +import ModelingToolkitBase as MTK +import SciMLStructures +import SymbolicIndexingInterface: remake_buffer +import SciMLBase: AbstractNonlinearProblem, remake + function ChainRulesCore.rrule(::Type{MTKParameters}, tunables, args...) function mtp_pullback(dt) dt = unthunk(dt) @@ -104,3 +115,11 @@ function ChainRulesCore.rrule( end ChainRulesCore.@non_differentiable Base.getproperty(sys::AbstractSystem, x::Symbol) + +function ModelingToolkitBase.update_initializeprob!(initprob::AbstractNonlinearProblem, prob) + pgetter = ChainRulesCore.@ignore_derivatives MTK.get_scimlfn(prob).initialization_data.metadata.oop_reconstruct_u0_p.pgetter + p = pgetter(prob, initprob) + return remake(initprob; p) +end + +end diff --git a/lib/ModelingToolkitBase/ext/MTKDiffEqNoiseProcessExt.jl b/lib/ModelingToolkitBase/ext/MTKDiffEqNoiseProcessExt.jl new file mode 100644 index 0000000000..e83e8e3848 --- /dev/null +++ b/lib/ModelingToolkitBase/ext/MTKDiffEqNoiseProcessExt.jl @@ -0,0 +1,10 @@ +module MTKDiffEqNoiseProcessExt + +using DiffEqNoiseProcess: WienerProcess +import ModelingToolkitBase + +function ModelingToolkitBase.__default_wiener_process(::Nothing) + return WienerProcess(0.0, 0.0, 0.0) +end + +end diff --git a/src/systems/unit_check.jl b/lib/ModelingToolkitBase/ext/MTKDynamicQuantitiesExt.jl similarity index 73% rename from src/systems/unit_check.jl rename to lib/ModelingToolkitBase/ext/MTKDynamicQuantitiesExt.jl index 1a6e00c33f..f13eb9dd75 100644 --- a/src/systems/unit_check.jl +++ b/lib/ModelingToolkitBase/ext/MTKDynamicQuantitiesExt.jl @@ -1,34 +1,52 @@ +module MTKDynamicQuantitiesExt + +import DynamicQuantities +const DQ = DynamicQuantities + +using ModelingToolkitBase, Symbolics, SymbolicUtils, JumpProcesses +using ModelingToolkitBase: get_unit, check_units, __get_unit_type, validate, VariableUnit, + JumpType, setdefault, ValidationError +using SymbolicUtils: isconst, issym, isadd, ismul, ispow, unwrap +using Symbolics: SymbolicT, value +import SciMLBase +import ModelingToolkitBase as MTK +import SymbolicUtils as SU + +function __init__() + MTK.t = let + only(@independent_variables t [unit = DQ.u"s"]) + end + SymbolicUtils.hashcons(unwrap(MTK.t), true) + MTK.D = Differential(MTK.t) +end #For dispatching get_unit const Conditional = Union{typeof(ifelse)} const Comparison = Union{typeof.([==, !=, ≠, <, <=, ≤, >, >=, ≥])...} -struct ValidationError <: Exception - message::String -end - -check_units(::Nothing, _...) = true +MTK.check_units(::Nothing, _...) = true function __get_literal_unit(x) if x isa Pair x = x[1] end - if !(x isa Union{Num, Symbolic}) + if !(x isa Union{Num, SymbolicT}) return nothing end v = value(x) u = getmetadata(v, VariableUnit, nothing) u isa DQ.AbstractQuantity ? screen_unit(u) : u end + function __get_scalar_unit_type(v) u = __get_literal_unit(v) if u isa DQ.AbstractQuantity return Val(:DynamicQuantities) - elseif u isa Unitful.Unitlike - return Val(:Unitful) end return nothing end -function __get_unit_type(vs′...) + +function MTK.__get_unit_type(v1, v2, v3) + vs′ = (v1, v2, v3) for vs in vs′ if vs isa AbstractVector for v in vs @@ -44,6 +62,22 @@ function __get_unit_type(vs′...) return nothing end +MTK.get_unit(x::DQ.AbstractQuantity) = screen_unit(x) +const unitless = DQ.Quantity(1.0) +get_literal_unit(x) = screen_unit(something(__get_literal_unit(x), unitless)) + +""" +Find the unit of a symbolic item. +""" +MTK.get_unit(x::Real) = unitless +MTK.get_unit(x::AbstractArray) = map(get_unit, x) +MTK.get_unit(x::Num) = get_unit(unwrap(x)) +MTK.get_unit(x::Symbolics.Arr) = get_unit(unwrap(x)) +MTK.get_unit(op::Differential, args) = get_unit(args[1]) / get_unit(op.x) ^ op.order +MTK.get_unit(op::typeof(getindex), args) = get_unit(args[1]) +MTK.get_unit(x::SciMLBase.NullParameters) = unitless +MTK.get_unit(op::typeof(instream), args) = get_unit(args[1]) + function screen_unit(result) if result isa DQ.AbstractQuantity d = DQ.dimension(result) @@ -59,24 +93,7 @@ function screen_unit(result) end end -const unitless = DQ.Quantity(1.0) -get_literal_unit(x) = screen_unit(something(__get_literal_unit(x), unitless)) - -""" -Find the unit of a symbolic item. -""" -get_unit(x::Real) = unitless -get_unit(x::DQ.AbstractQuantity) = screen_unit(x) -get_unit(x::AbstractArray) = map(get_unit, x) -get_unit(x::Num) = get_unit(unwrap(x)) -get_unit(x::Symbolics.Arr) = get_unit(unwrap(x)) -get_unit(op::Differential, args) = get_unit(args[1]) / get_unit(op.x) -get_unit(op::Difference, args) = get_unit(args[1]) / get_unit(op.t) -get_unit(op::typeof(getindex), args) = get_unit(args[1]) -get_unit(x::SciMLBase.NullParameters) = unitless -get_unit(op::typeof(instream), args) = get_unit(args[1]) - -function get_unit(op, args) # Fallback +function MTK.get_unit(op, args) # Fallback result = oneunit(op(get_unit.(args)...)) try get_unit(result) @@ -85,14 +102,14 @@ function get_unit(op, args) # Fallback end end -function get_unit(::Union{typeof(+), typeof(-)}, args) +function MTK.get_unit(::Union{typeof(+), typeof(-)}, args) u = get_unit(args[1]) if all(i -> get_unit(args[i]) == u, 2:length(args)) return u end end -function get_unit(op::Integral, args) +function MTK.get_unit(op::Integral, args) unit = 1 if op.domain.variables isa Vector for u in op.domain.variables @@ -105,7 +122,7 @@ function get_unit(op::Integral, args) end equivalent(x, y) = isequal(x, y) -function get_unit(op::Conditional, args) +function MTK.get_unit(op::Conditional, args) terms = get_unit.(args) terms[1] == unitless || throw(ValidationError(", in $op, [$(terms[1])] is not dimensionless.")) @@ -114,7 +131,7 @@ function get_unit(op::Conditional, args) return terms[2] end -function get_unit(op::typeof(Symbolics._mapreduce), args) +function MTK.get_unit(op::typeof(mapreduce), args) if args[2] == + get_unit(args[3]) else @@ -122,15 +139,17 @@ function get_unit(op::typeof(Symbolics._mapreduce), args) end end -function get_unit(op::Comparison, args) +function MTK.get_unit(op::Comparison, args) terms = get_unit.(args) equivalent(terms[1], terms[2]) || throw(ValidationError(", in comparison $op, units [$(terms[1])] and [$(terms[2])] do not match.")) return unitless end -function get_unit(x::Symbolic) - if (u = __get_literal_unit(x)) !== nothing +function MTK.get_unit(x::SymbolicT) + if isconst(x) + return get_unit(value(x)) + elseif (u = __get_literal_unit(x)) !== nothing screen_unit(u) elseif issym(x) get_literal_unit(x) @@ -150,14 +169,14 @@ function get_unit(x::Symbolic) if base == unitless unitless else - pargs[2] isa Number ? base^pargs[2] : (1 * base)^pargs[2] + isconst(pargs[2]) ? base^unwrap_const(pargs[2]) : (1 * base)^pargs[2] end elseif iscall(x) op = operation(x) 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 iscall(op) && !iscall(operation(op)) - gp = getmetadata(x, Symbolics.GetindexParent, nothing) # Like x[1](t) + elseif iscall(op) && operation(op) === getindex + gp = arguments(op)[1] return screen_unit(getmetadata(gp, VariableUnit, unitless)) end # Actual function calls: args = arguments(x) @@ -196,7 +215,7 @@ function _validate(terms::Vector, labels::Vector{String}; info::String = "") equnit = safe_get_unit(term, info * label) if equnit === nothing valid = false - elseif !isequal(term, 0) + elseif !SU._iszero(term) if first_unit === nothing first_unit = equnit first_label = label @@ -213,20 +232,9 @@ function _validate(terms::Vector, labels::Vector{String}; info::String = "") valid end -function _validate(ap::AnalysisPoint; info::String = "") - is_valid = false - if (ap.outputs == nothing) - is_valid = true - else - conn_eq = connect(ap.input, ap.outputs...) - is_valid = _validate(conn_eq.rhs, info=info) - end - return is_valid -end - function _validate(conn::Connection; info::String = "") valid = true - syss = get_systems(conn) + syss = MTK.get_systems(conn) sys = first(syss) st = unknowns(sys) for i in 2:length(syss) @@ -259,15 +267,15 @@ function _validate(conn::Connection; info::String = "") valid end -function validate(jump::Union{VariableRateJump, - ConstantRateJump}, t::Symbolic; +function MTK.validate(jump::Union{VariableRateJump, + ConstantRateJump}, t::SymbolicT; info::String = "") newinfo = replace(info, "eq." => "jump") _validate([jump.rate, 1 / t], ["rate", "1/t"], info = newinfo) && # Assuming the rate is per time units validate(jump.affect!, info = newinfo) end -function validate(jump::MassActionJump, t::Symbolic; info::String = "") +function MTK.validate(jump::MassActionJump, t::SymbolicT; info::String = "") left_symbols = [x[1] for x in jump.reactant_stoch] #vector of pairs of symbol,int -> vector symbols net_symbols = [x[1] for x in jump.net_stoch] all_symbols = vcat(left_symbols, net_symbols) @@ -278,7 +286,7 @@ function validate(jump::MassActionJump, t::Symbolic; info::String = "") ["scaled_rates", "1/(t*reactants^$n))"]; info) end -function validate(jumps::Vector{JumpType}, t::Symbolic) +function MTK.validate(jumps::Vector{JumpType}, t::SymbolicT) labels = ["in Mass Action Jumps,", "in Constant Rate Jumps,", "in Variable Rate Jumps,"] majs = filter(x -> x isa MassActionJump, jumps) crjs = filter(x -> x isa ConstantRateJump, jumps) @@ -287,18 +295,22 @@ function validate(jumps::Vector{JumpType}, t::Symbolic) all([validate(js, t; info) for (js, info) in zip(splitjumps, labels)]) end -function validate(eq::Union{Inequality, Equation}; info::String = "") - if typeof(eq.lhs) <: Union{Connection, AnalysisPoint} - _validate(eq.rhs; info) +function MTK.validate(eq::Union{Inequality, Equation}; info::String = "") + if isconst(eq.lhs) && value(eq.lhs) isa Union{Connection, AnalysisPoint} + tmp = value(eq.rhs)::Union{Connection, AnalysisPoint} + if tmp isa AnalysisPoint + tmp = value(MTK.to_connection(tmp).rhs)::Connection + end + _validate(tmp; info) else _validate([eq.lhs, eq.rhs], ["left", "right"]; info) end end -function validate(eq::Equation, - term::Union{Symbolic, DQ.AbstractQuantity, Num}; info::String = "") +function MTK.validate(eq::Equation, + term::Union{SymbolicT, DQ.AbstractQuantity, Num}; info::String = "") _validate([eq.lhs, eq.rhs, term], ["left", "right", "noise"]; info) end -function validate(eq::Equation, terms::Vector; info::String = "") +function MTK.validate(eq::Equation, terms::Vector; info::String = "") _validate(vcat([eq.lhs, eq.rhs], terms), vcat(["left", "right"], "noise #" .* string.(1:length(terms))); info) end @@ -306,26 +318,28 @@ end """ Returns true iff units of equations are valid. """ -function validate(eqs::Vector; info::String = "") +function MTK.validate(eqs::Vector; info::String = "") all([validate(eqs[idx], info = info * " in eq. #$idx") for idx in 1:length(eqs)]) end -function validate(eqs::Vector, noise::Vector; info::String = "") +function MTK.validate(eqs::Vector, noise::Vector; info::String = "") all([validate(eqs[idx], noise[idx], info = info * " in eq. #$idx") for idx in 1:length(eqs)]) end -function validate(eqs::Vector, noise::Matrix; info::String = "") +function MTK.validate(eqs::Vector, noise::Matrix; info::String = "") all([validate(eqs[idx], noise[idx, :], info = info * " in eq. #$idx") for idx in 1:length(eqs)]) end -function validate(eqs::Vector, term::Symbolic; info::String = "") +function MTK.validate(eqs::Vector, term::SymbolicT; info::String = "") all([validate(eqs[idx], term, info = info * " in eq. #$idx") for idx in 1:length(eqs)]) end -validate(term::Symbolics.SymbolicUtils.Symbolic) = safe_get_unit(term, "") !== nothing +MTK.validate(term::SymbolicT) = safe_get_unit(term, "") !== nothing """ Throws error if units of equations are invalid. """ -function check_units(::Val{:DynamicQuantities}, eqs...) +function MTK.check_units(::Val{:DynamicQuantities}, eqs...) validate(eqs...) || throw(ValidationError("Some equations had invalid units. See warnings for details.")) end + +end diff --git a/ext/MTKInfiniteOptExt.jl b/lib/ModelingToolkitBase/ext/MTKInfiniteOptExt.jl similarity index 92% rename from ext/MTKInfiniteOptExt.jl rename to lib/ModelingToolkitBase/ext/MTKInfiniteOptExt.jl index bcfb7ce597..570ee3f215 100644 --- a/ext/MTKInfiniteOptExt.jl +++ b/lib/ModelingToolkitBase/ext/MTKInfiniteOptExt.jl @@ -1,5 +1,6 @@ module MTKInfiniteOptExt -using ModelingToolkit +using ModelingToolkitBase +import Symbolics: SymbolicT using InfiniteOpt using DiffEqBase using SciMLStructures @@ -8,7 +9,7 @@ using StaticArrays using UnPack import SymbolicUtils import NaNMath -const MTK = ModelingToolkit +const MTK = ModelingToolkitBase struct InfiniteOptModel model::InfiniteModel @@ -75,14 +76,14 @@ end function MTK.add_constraint!(m::InfiniteOptModel, expr::Union{Equation, Inequality}) if expr isa Equation - @constraint(m.model, expr.lhs - expr.rhs == 0) + @constraint(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) == 0) elseif expr.relational_op === Symbolics.geq - @constraint(m.model, expr.lhs - expr.rhs ≥ 0) + @constraint(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) ≥ 0) else - @constraint(m.model, expr.lhs - expr.rhs ≤ 0) + @constraint(m.model, SymbolicUtils.unwrap_const(expr.lhs) - SymbolicUtils.unwrap_const(expr.rhs) ≤ 0) end end -MTK.set_objective!(m::InfiniteOptModel, expr) = @objective(m.model, Min, expr) +MTK.set_objective!(m::InfiniteOptModel, expr) = @objective(m.model, Min, SymbolicUtils.unwrap_const(expr)) function MTK.JuMPDynamicOptProblem(sys::System, op, tspan; dt = nothing, @@ -108,7 +109,7 @@ function MTK.InfiniteOptDynamicOptProblem(sys::System, op, tspan; end function MTK.lowered_integral(model::InfiniteOptModel, expr, lo, hi) - model.tₛ * InfiniteOpt.∫(expr, model.model[:t], lo, hi) + model.tₛ * InfiniteOpt.∫(SymbolicUtils.unwrap_const(expr), model.model[:t], lo, hi) end MTK.lowered_derivative(model::InfiniteOptModel, i) = ∂(model.U[i], model.model[:t]) @@ -130,7 +131,7 @@ end function MTK.lowered_var(m::InfiniteOptModel, uv, i, t) X = getfield(m, uv) - t isa Union{Num, Symbolics.Symbolic} ? X[i] : X[i](t) + t isa Union{Num, SymbolicT} ? X[i] : X[i](t) end function add_solve_constraints!(prob::JuMPDynamicOptProblem, tableau) @@ -254,7 +255,7 @@ function MTK.successful_solve(model::InfiniteModel) tstatus = termination_status(model) pstatus = primal_status(model) !has_values(model) && - error("Model not solvable; please report this to github.com/SciML/ModelingToolkit.jl with a MWE.") + error("Model not solvable; please report this to github.com/SciML/ModelingToolkitBase.jl with a MWE.") pstatus === FEASIBLE_POINT && (tstatus === OPTIMAL || tstatus === LOCALLY_SOLVED || tstatus === ALMOST_OPTIMAL || @@ -270,13 +271,13 @@ for ff in [acos, log1p, acosh, log2, asin, tan, atanh, cos, log, sin, log10, sqr end # JuMP variables and Symbolics variables never compare equal. When tracing through dynamics, a function argument can be either a JuMP variable or A Symbolics variable, it can never be both. -function Base.isequal(::SymbolicUtils.Symbolic, +function Base.isequal(::SymbolicT, ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}) false end function Base.isequal( ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, - ::SymbolicUtils.Symbolic) + ::SymbolicT) false end end diff --git a/lib/ModelingToolkitBase/ext/MTKJuliaFormatterExt.jl b/lib/ModelingToolkitBase/ext/MTKJuliaFormatterExt.jl new file mode 100644 index 0000000000..329acd2538 --- /dev/null +++ b/lib/ModelingToolkitBase/ext/MTKJuliaFormatterExt.jl @@ -0,0 +1,12 @@ +module MTKJuliaFormatterExt + +import ModelingToolkitBase: readable_code, _readable_code, rec_remove_macro_linenums! +import JuliaFormatter + +function readable_code(expr::Expr) + expr = Base.remove_linenums!(_readable_code(expr)) + rec_remove_macro_linenums!(expr) + JuliaFormatter.format_text(string(expr), JuliaFormatter.SciMLStyle()) +end + +end diff --git a/lib/ModelingToolkitBase/ext/MTKLabelledArraysExt.jl b/lib/ModelingToolkitBase/ext/MTKLabelledArraysExt.jl new file mode 100644 index 0000000000..002f725b32 --- /dev/null +++ b/lib/ModelingToolkitBase/ext/MTKLabelledArraysExt.jl @@ -0,0 +1,18 @@ +module MTKLabelledArraysExt + +using ModelingToolkitBase, LabelledArrays +using ModelingToolkitBase: _defvar, toparam, variable, varnames_length_check +function ModelingToolkitBase.define_vars(u::Union{SLArray, LArray}, t) + [ModelingToolkitBase._defvar(x)(t) for x in LabelledArrays.symnames(typeof(u))] +end + +function ModelingToolkitBase.define_params(p::Union{SLArray, LArray}, t, 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 + +end diff --git a/lib/ModelingToolkitBase/ext/MTKLatexifyExt.jl b/lib/ModelingToolkitBase/ext/MTKLatexifyExt.jl new file mode 100644 index 0000000000..043d7ec433 --- /dev/null +++ b/lib/ModelingToolkitBase/ext/MTKLatexifyExt.jl @@ -0,0 +1,38 @@ +module MTKLatexifyExt + +using ModelingToolkitBase +import SymbolicIndexingInterface: getname +using Latexify + +@latexrecipe function f(c::ModelingToolkitBase.Connection) + index --> :subscript + fn = eltype(c.systems) <: ModelingToolkitBase.AbstractSystem ? nameof : getname + return Expr(:call, :connect, map(fn, c.systems)...) +end + +function Base.show(io::IO, ::MIME"text/latex", ap::ModelingToolkitBase.Connection) + print(io, latexify(ap)) +end + +@latexrecipe function f(sys::ModelingToolkitBase.AbstractSystem) + return latexify(equations(sys)) +end + +function Base.show(io::IO, ::MIME"text/latex", x::ModelingToolkitBase.AbstractSystem) + print(io, "\$\$ " * latexify(x) * " \$\$") +end + +@latexrecipe function f(ap::ModelingToolkitBase.AnalysisPoint) + index --> :subscript + snakecase --> true + ap.input === nothing && return 0 + outs = Expr(:vect) + append!(outs.args, ModelingToolkitBase.ap_var.(ap.outputs)) + return Expr(:call, :AnalysisPoint, ModelingToolkitBase.ap_var(ap.input), ap.name, outs) +end + +function Base.show(io::IO, ::MIME"text/latex", ap::ModelingToolkitBase.AnalysisPoint) + print(io, latexify(ap)) +end + +end diff --git a/ext/MTKPyomoDynamicOptExt.jl b/lib/ModelingToolkitBase/ext/MTKPyomoDynamicOptExt.jl similarity index 96% rename from ext/MTKPyomoDynamicOptExt.jl rename to lib/ModelingToolkitBase/ext/MTKPyomoDynamicOptExt.jl index a2a05d297a..97bdf53fef 100644 --- a/ext/MTKPyomoDynamicOptExt.jl +++ b/lib/ModelingToolkitBase/ext/MTKPyomoDynamicOptExt.jl @@ -1,11 +1,12 @@ module MTKPyomoDynamicOptExt -using ModelingToolkit +using ModelingToolkitBase using Pyomo using DiffEqBase using UnPack using NaNMath using Setfield -const MTK = ModelingToolkit +import SymbolicUtils as SU +const MTK = ModelingToolkitBase const SPECIAL_FUNCTIONS_DICT = Dict([acos => Pyomo.py_acos, acosh => Pyomo.py_acosh, @@ -56,7 +57,7 @@ struct PyomoDynamicOptProblem{uType, tType, isinplace, P, F, K} <: end end -function pysym_getproperty(s::Union{Num, Symbolics.Symbolic}, name::Symbol) +function pysym_getproperty(s::Union{Num, SymbolicT}, name::Symbol) Symbolics.wrap(SymbolicUtils.term( _getproperty, Symbolics.unwrap(s), Val{name}(), type = Symbolics.Struct{PyomoVar})) end @@ -122,7 +123,7 @@ function MTK.add_constraint!(pmodel::PyomoDynamicOptModel, cons; n_idxs = 1) Symbolics.unwrap(expr), SPECIAL_FUNCTIONS_DICT, fold = false) cons_sym = Symbol("cons", hash(cons)) - if occursin(Symbolics.unwrap(t_sym), expr) + if SU.query(isequal(Symbolics.unwrap(t_sym)), expr) f = eval(Symbolics.build_function(expr, model_sym, t_sym)) setproperty!(model, cons_sym, pyomo.Constraint(model.t, rule = Pyomo.pyfunc(f))) else @@ -134,7 +135,7 @@ end function MTK.set_objective!(pmodel::PyomoDynamicOptModel, expr) @unpack model, model_sym, t_sym, dummy_sym = pmodel expr = Symbolics.substitute(expr, SPECIAL_FUNCTIONS_DICT, fold = false) - if occursin(Symbolics.unwrap(t_sym), expr) + if SU.query(isequal(Symbolics.unwrap(t_sym)), expr) f = eval(Symbolics.build_function(expr, model_sym, t_sym)) model.obj = pyomo.Objective(model.t, rule = Pyomo.pyfunc(f)) else @@ -175,7 +176,7 @@ end function MTK.lowered_var(m::PyomoDynamicOptModel, uv, i, t) X = Symbolics.value(pysym_getproperty(m.model_sym, uv)) - var = t isa Union{Num, Symbolics.Symbolic} ? X[i, m.t_sym] : X[i, t] + var = t isa Union{Num, SymbolicT} ? X[i, m.t_sym] : X[i, t] Symbolics.unwrap(var) end diff --git a/lib/ModelingToolkitBase/src/ModelingToolkitBase.jl b/lib/ModelingToolkitBase/src/ModelingToolkitBase.jl new file mode 100644 index 0000000000..2ccd665935 --- /dev/null +++ b/lib/ModelingToolkitBase/src/ModelingToolkitBase.jl @@ -0,0 +1,358 @@ +""" +$(DocStringExtensions.README) +""" +module ModelingToolkitBase +using PrecompileTools, Reexport +@recompile_invalidations begin + using StaticArrays + using Symbolics + using ImplicitDiscreteSolve + using JumpProcesses + # ONLY here for the invalidations + import REPL +end + +import SymbolicUtils +import SymbolicUtils as SU +import SymbolicUtils: iscall, arguments, operation, maketerm, promote_symtype, + isadd, ismul, ispow, issym, FnType, isconst, BSImpl, + @rule, Rewriters, substitute, metadata, BasicSymbolic +using SymbolicUtils.Code +import SymbolicUtils.Code: toexpr +import SymbolicUtils.Rewriters: Chain, Postwalk, Prewalk, Fixpoint +using DocStringExtensions +using SpecialFunctions, NaNMath +@recompile_invalidations begin + using DiffEqCallbacks + using DiffEqBase, SciMLBase, ForwardDiff +end +using Graphs +import ExprTools: splitdef, combinedef +import OrderedCollections + +using SymbolicIndexingInterface +using LinearAlgebra, SparseArrays +using InteractiveUtils +using DataStructures +@static if pkgversion(DataStructures) >= v"0.19" + import DataStructures: IntDisjointSet +else + import DataStructures: IntDisjointSets + const IntDisjointSet = IntDisjointSets +end +using Base.Threads +using ArrayInterface +using Setfield, ConstructionBase +import Libdl +using DocStringExtensions +using Base: RefValue +using Combinatorics +import FunctionWrappersWrappers +import FunctionWrappers: FunctionWrapper +using SciMLStructures +using Compat +using AbstractTrees +using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain, + PeriodicClock, Clock, SolverStepClock, ContinuousClock, OverrideInit, + NoInit +import Moshi +using Moshi.Data: @data +using Reexport +using RecursiveArrayTools +import Graphs: SimpleDiGraph, add_edge!, incidence_matrix +import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!, + undef_blocks, blocks +using OffsetArrays: Origin +import CommonSolve +import EnumX +import ReadOnlyDicts: ReadOnlyDict + +using RuntimeGeneratedFunctions +using RuntimeGeneratedFunctions: drop_expr + +using Symbolics: degree, VartypeT, SymbolicT +using Symbolics: parse_vars, value, @derivatives, get_variables, + exprs_occur_in, symbolic_linear_solve, unwrap, wrap, + VariableSource, getname, variable, + NAMESPACE_SEPARATOR, setdefaultval, Arr, + hasnode, fixpoint_sub, CallAndWrap, SArgsT, SSym, STerm +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, gradient, linear_expansion, + jacobian, hessian, derivative, sparsejacobian, sparsehessian, + scalarize, hasderiv + +import DiffEqBase: @add_kwonly +export independent_variables, unknowns, observables, parameters, bound_parameters, + continuous_events, discrete_events +@reexport using Symbolics +@reexport using UnPack +RuntimeGeneratedFunctions.init(@__MODULE__) + +import DifferentiationInterface as DI +using ADTypes: AutoForwardDiff +import SciMLPublic: @public +import PreallocationTools +import PreallocationTools: DiffCache +import FillArrays +using BipartiteGraphs +import Random: AbstractRNG + +export @derivatives + +for fun in [:toexpr] + @eval begin + function $fun(eq::Equation; kw...) + Expr(:call, :(==), $fun(eq.lhs; kw...), $fun(eq.rhs; kw...)) + end + + function $fun(ineq::Inequality; kw...) + if ineq.relational_op == Symbolics.leq + Expr(:call, :(<=), $fun(ineq.lhs; kw...), $fun(ineq.rhs; kw...)) + else + Expr(:call, :(>=), $fun(ineq.lhs; kw...), $fun(ineq.rhs; kw...)) + end + end + + $fun(eqs::AbstractArray; kw...) = map(eq -> $fun(eq; kw...), eqs) + $fun(x::Integer; kw...) = x + $fun(x::AbstractFloat; kw...) = x + end +end + +const INTERNAL_FIELD_WARNING = """ +This field is internal API. It may be removed or changed without notice in a non-breaking \ +release. Usage of this field is not advised. +""" + +const INTERNAL_ARGS_WARNING = """ +The following arguments are internal API. They may be removed or changed without notice \ +in a non-breaking release. Usage of these arguments is not advised. +""" + +""" +$(TYPEDEF) + +Abstract supertype of all system types. Any custom system types must subtype this. +""" +abstract type AbstractSystem end +# Solely so that `ODESystem` can be deprecated and still act as a valid type. +# See `deprecations.jl`. +abstract type IntermediateDeprecationSystem <: AbstractSystem end + +function independent_variable end + +# this has to be included early to deal with dependency issues +function complete end + +complete(m::Matching, args...; kw...) = BipartiteGraphs.complete(m, args...; kw...) +complete(g::BipartiteGraph, args...; kw...) = BipartiteGraphs.complete(g, args...; kw...) + +export EvalAt +include("variables.jl") +include("parameters.jl") +include("discretes.jl") +include("independent_variables.jl") +include("constants.jl") +include("derivative_dict.jl") +include("atomic_array_dict.jl") +include("parameter_bindings_graph.jl") + +const SymmapT = AtomicArrayDict{SymbolicT, Dict{SymbolicT, SymbolicT}} +const ROSymmapT = ReadOnlyDict{SymbolicT, SymbolicT, SymmapT} +struct CommonSentinel end +const COMMON_SENTINEL = SU.Const{VartypeT}(CommonSentinel()) +const COMMON_NOTHING = SU.Const{VartypeT}(nothing) +const COMMON_MISSING = SU.Const{VartypeT}(missing) +const COMMON_TRUE = SU.Const{VartypeT}(true) +const COMMON_FALSE = SU.Const{VartypeT}(false) +const COMMON_INF = SU.Const{VartypeT}(Inf) + +include("utils.jl") + +include("systems/index_cache.jl") +include("systems/parameter_buffer.jl") +include("systems/abstractsystem.jl") +include("systems/connectiongraph.jl") +include("systems/connectors.jl") +include("systems/imperative_affect.jl") +include("systems/callbacks.jl") +include("systems/system.jl") +include("systems/analysis_points.jl") +include("systems/codegen_utils.jl") +include("problems/docs.jl") +include("systems/codegen.jl") +include("systems/problem_utils.jl") + +include("problems/compatibility.jl") +include("problems/odeproblem.jl") +include("problems/ddeproblem.jl") +include("problems/daeproblem.jl") +include("problems/sdeproblem.jl") +include("problems/sddeproblem.jl") +include("problems/nonlinearproblem.jl") +include("problems/intervalnonlinearproblem.jl") +include("problems/implicitdiscreteproblem.jl") +include("problems/discreteproblem.jl") +include("problems/optimizationproblem.jl") +include("problems/jumpproblem.jl") +include("problems/initializationproblem.jl") +include("problems/bvproblem.jl") +include("problems/linearproblem.jl") + +include("modelingtoolkitize/common.jl") +include("modelingtoolkitize/odeproblem.jl") +include("modelingtoolkitize/sdeproblem.jl") +include("modelingtoolkitize/optimizationproblem.jl") +include("modelingtoolkitize/nonlinearproblem.jl") + +include("systems/nonlinear/homotopy_continuation.jl") +include("systems/nonlinear/initializesystem.jl") +include("systems/diffeqs/basic_transformations.jl") + +include("systems/pde/pdesystem.jl") + + +include("systems/unit_check.jl") +include("systems/dependency_graphs.jl") +include("discretedomain.jl") +include("systems/systems.jl") + +include("debugging.jl") + +include("inputoutput.jl") + +include("deprecations.jl") + +const t_nounits = let + only(@independent_variables t) +end +const D_nounits = Differential(t_nounits) + +export ODEFunction, convert_system_indepvar, + System, OptimizationSystem, JumpSystem, SDESystem, NonlinearSystem, ODESystem +export SDEFunction +export DiscreteProblem, DiscreteFunction +export ImplicitDiscreteProblem, ImplicitDiscreteFunction +export ODEProblem, SDEProblem +export NonlinearFunction +export NonlinearProblem +export IntervalNonlinearFunction +export IntervalNonlinearProblem +export OptimizationProblem, constraints +export SteadyStateProblem +export JumpProblem +export flatten +export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream, + instream +export @component, @mtkcompile, @mtkbuild +export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance, + istunable, getdist, hasdist, + tunable_parameters, isirreducible, getdescription, hasdescription, + hasunit, getunit, hasconnect, getconnect, + hasmisc, getmisc, state_priority, + subset_tunables +export liouville_transform, change_independent_variable, + add_accumulations, noise_to_brownians, Girsanov_transform, change_of_variables, + fractional_to_ordinary, linear_fractional_to_ordinary +export respecialize +export PDESystem +export Differential, expand_derivatives, @derivatives +export Equation +export Term +export SymScope, LocalScope, ParentScope, GlobalScope +export independent_variable, equations, observed, full_equations, jumps, cost, + brownians +export initialization_equations, guesses, bindings, initial_conditions, hierarchy +export mtkcompile, expand_connections, structural_simplify +export solve +export Pre + +export calculate_jacobian, generate_jacobian, generate_rhs, generate_custom_function, + generate_W, calculate_hessian +export calculate_control_jacobian, generate_control_jacobian +export calculate_tgrad, generate_tgrad +export generate_cost, calculate_cost_gradient, generate_cost_gradient +export calculate_cost_hessian, generate_cost_hessian +export calculate_massmatrix, generate_diffusion_function +export stochastic_integral_transform + +export BipartiteGraph, equation_dependencies, variable_dependencies +export eqeq_dependencies, varvar_dependencies +export asgraph, asdigraph + +export toexpr, get_variables +export simplify, substitute +export build_function +export modelingtoolkitize +export generate_initializesystem, Initial, isinitial, InitializationProblem + +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, @brownians, @brownian, + @discretes +export @named, @nonamespace, @namespace, extend, compose, complete, toggle_namespacing +export debug_system + +#export ContinuousClock, Discrete, sampletime, input_timedomain, output_timedomain +#export has_discrete_domain, has_continuous_domain +#export is_discrete_domain, is_continuous_domain, is_hybrid_domain +export Shift, ShiftIndex +export Sample, Hold, SampleTime +export Clock, SolverStepClock, TimeDomain + +export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables + +export HomotopyContinuationProblem + +export AnalysisPoint, open_loop + +include("systems/optimal_control_interface.jl") + +using SciMLBase: AbstractDynamicOptProblem +export AbstractDynamicOptProblem, JuMPDynamicOptProblem, InfiniteOptDynamicOptProblem, + CasADiDynamicOptProblem, PyomoDynamicOptProblem +export AbstractCollocation, JuMPCollocation, InfiniteOptCollocation, + CasADiCollocation, PyomoCollocation +export DynamicOptSolution + +const set_scalar_metadata = setmetadata + +@public apply_to_variables, equations_toplevel, unknowns_toplevel, parameters_toplevel +@public continuous_events_toplevel, discrete_events_toplevel, assertions, is_alg_equation +@public is_diff_equation, Equality +@public inputs, outputs, bound_inputs, unbound_inputs, bound_outputs +@public unbound_outputs, is_bound +@public AbstractSystem, CheckAll, CheckNone, CheckComponents, CheckUnits +@public t, D, t_nounits, D_nounits +@public SymbolicContinuousCallback, SymbolicDiscreteCallback +@public VariableType, MTKVariableTypeCtx, VariableBounds, VariableConnectType +@public VariableDescription, VariableInput, VariableIrreducible, VariableMisc +@public VariableOutput, VariableStatePriority, VariableUnit, collect_scoped_vars! +@public collect_var_to_name!, collect_vars!, eqtype_supports_collect_vars, hasdefault +@public getdefault, setdefault, iscomplete, isparameter, modified_unknowns! +@public renamespace, namespace_equations + +for prop in [SYS_PROPS; [:continuous_events, :discrete_events]] + getter = Symbol(:get_, prop) + hasfn = Symbol(:has_, prop) + @eval @public $getter, $hasfn +end + +function __init__() + SU.hashcons(unwrap(t_nounits), true) + SU.hashcons(COMMON_NOTHING, true) + SU.hashcons(COMMON_MISSING, true) + SU.hashcons(COMMON_TRUE, true) + SU.hashcons(COMMON_FALSE, true) + SU.hashcons(COMMON_SENTINEL, true) + SU.hashcons(COMMON_INF, true) +end + +include("precompile.jl") +end # module diff --git a/lib/ModelingToolkitBase/src/atomic_array_dict.jl b/lib/ModelingToolkitBase/src/atomic_array_dict.jl new file mode 100644 index 0000000000..19301fd306 --- /dev/null +++ b/lib/ModelingToolkitBase/src/atomic_array_dict.jl @@ -0,0 +1,209 @@ +""" + $(TYPEDEF) + +Wrapper over an `AbstractDict{SymbolicT, V} where {V}` which disallows keys that are +indexed array symbolics. Specifically, if `@variables x[1:4]` exists, then `x` can be +a key but `x[1]` cannot. +""" +struct AtomicArrayDict{V, D <: AbstractDict{SymbolicT, V}} <: AbstractDict{SymbolicT, V} + dict::D + + function AtomicArrayDict(dict::AbstractDict{SymbolicT, V}) where {V} + for k in keys(dict) + validate_atomic_array_key(k) + end + new{V, typeof(dict)}(dict) + end +end + +AtomicArrayDict{V, D}(dict::AtomicArrayDict{V, D}) where {V, D} = copy(dict) +AtomicArrayDict{V, D}() where {V, D} = AtomicArrayDict(D()) +AtomicArrayDict() = AtomicArrayDict(Dict{SymbolicT, SymbolicT}()) +AtomicArrayDict(args::Pair...) = AtomicArrayDict(Dict(args...)) +AtomicArrayDict{V, D}(args::Pair...) where {V, D} = AtomicArrayDict(Dict(args...)) +AtomicArrayDict{V}(args...) where {V} = AtomicArrayDict(Dict{SymbolicT, V}(args...)) +AtomicArrayDict{V, D}(args...) where {V, D} = AtomicArrayDict(D(args...)) + +struct IndexedArrayKeyError <: Exception + k::SymbolicT +end + +function Base.showerror(io::IO, err::IndexedArrayKeyError) + print(io, """ + `AtomicArrayDict` treats symbolic arrays as atomic. It does not allow keys to be \ + indexed array symbolics. Got key $(err.k). + """) +end + +function validate_atomic_array_key(k::SymbolicT) + split_indexed_var(k)[2] && throw(IndexedArrayKeyError(k)) +end + +Base.copy(dd::AtomicArrayDict) = AtomicArrayDict(copy(dd.dict)) +function Base.empty(dd::AtomicArrayDict, ::Type{K}, ::Type{V}) where {K, V} + AtomicArrayDict(empty(dd.dict, K, V)) +end + +Base.get(def::Base.Callable, dd::AtomicArrayDict, k) = def() +Base.get(def::Base.Callable, dd::AtomicArrayDict, k::SymbolicT) = get(def, dd.dict, k) +function Base.get(f::Base.Callable, dd::AtomicArrayDict, k::Union{Num, Arr, CallAndWrap}) + return get(f, dd, unwrap(k)) +end +Base.get(dd::AtomicArrayDict, k, default) = get(Returns(default), dd, k) + +Base.haskey(dd::AtomicArrayDict, k) = haskey(dd.dict, k) + +Base.getindex(dd::AtomicArrayDict, k) = dd.dict[k] + +function Base.setindex!(dd::AtomicArrayDict, v, k) + k = unwrap(k) + validate_atomic_array_key(unwrap(k)) + setindex!(dd.dict, v, k) +end + +Base.isempty(dd::AtomicArrayDict) = isempty(dd.dict) +Base.length(dd::AtomicArrayDict) = length(dd.dict) +Base.iterate(dd::AtomicArrayDict, args...) = Base.iterate(dd.dict, args...) +Base.sizehint!(dd::AtomicArrayDict, n; kw...) = sizehint!(dd.dict, n; kw...) +Base.empty!(dd::AtomicArrayDict) = empty!(dd.dict) + +Base.delete!(dd::AtomicArrayDict, k) = delete!(dd.dict, k) + +""" + $TYPEDSIGNATURES + +Convert the symbolic mapping `dict` to an `AtomicArrayDict`. If `dict` contains keys which +are elements of a symbolic array, the returned mappng will have a key for the array, and +a value which is a symbolic array where entries specified in `dict` are present and `default` +otherwise. +""" +function as_atomic_dict_with_defaults(dict::AbstractDict{SymbolicT, SymbolicT}, default::SymbolicT) + dd = AtomicArrayDict(empty(dict)) + indexed_array_vals = empty(dict, SymbolicT, Array{SymbolicT}) + for (k, v) in dict + arr, isarr = split_indexed_var(k) + if isarr + buffer = get!(() -> fill(default, size(arr)), indexed_array_vals, arr) + si = get_stable_index(k) + buffer[si] = v + else + dd[k] = v + end + end + for (k, v) in indexed_array_vals + if all(SU.isconst, v) + dd[k] = BSImpl.Const{VartypeT}(unwrap_const.(v)) + else + dd[k] = BSImpl.Const{VartypeT}(v) + end + end + return dd +end + +""" + $TYPEDSIGNATURES + +Modify an atomic array mapping `dd` to map `k` to `v`. If `k` is an indexed array symbolic, +update the array to have value `v` at the corresponding index. If the array is not a key, +create the key and set all other entries to `default`. +""" +function write_possibly_indexed_array!(dd::AtomicArrayDict{SymbolicT}, k::SymbolicT, v::SymbolicT, default::SymbolicT) + arr, isarr = split_indexed_var(k) + if isarr + buffer::Array{SymbolicT} = if haskey(dd, arr) + collect(dd[arr]) + else + fill(default, size(arr)) + end + idx = get_stable_index(k) + buffer[idx] = v + if all(SU.isconst, buffer) + dd[arr] = BSImpl.Const{VartypeT}(unwrap_const.(buffer)) + else + dd[arr] = BSImpl.Const{VartypeT}(buffer) + end + else + dd[k] = v + end + return dd +end + +""" + $TYPEDSIGNATURES + +Check if `dd` has the key `k`. If `k` is indexed, check if `dd` has the array as a key. +""" +function has_possibly_indexed_key(dd::AtomicArrayDict, k::SymbolicT) + arr, _ = split_indexed_var(k) + return haskey(dd, arr) +end + +""" + $TYPEDSIGNATURES + +Equivalent to `get(dd, k, default)`. If `k` is an indexed array, then return +`dd[arr][idxs...]` for the corresponding array `arr` and indices, or `default` +if `arr` does not exist. +""" +function get_possibly_indexed(dd::AtomicArrayDict, k::SymbolicT, default) + arr, isarr = split_indexed_var(k) + res = get(dd, arr, default) + isarr || return res + res === default && return default + idx = get_stable_index(k) + return res[idx] +end + +struct AtomicArraySet{D <: AbstractDict{SymbolicT, Nothing}} <: AbstractSet{SymbolicT} + dd::AtomicArrayDict{Nothing, D} + + function AtomicArraySet{D}(dd::AtomicArrayDict{Nothing, D}) where {D} + new{D}(dd) + end +end + +AtomicArraySet() = AtomicArraySet{Dict{SymbolicT, Nothing}}() +AtomicArraySet{D}() where {D} = AtomicArraySet{D}(D()) +AtomicArraySet{D}(x::D) where {D} = AtomicArraySet{D}(AtomicArrayDict(x)) + +Base.isempty(x::AtomicArraySet) = isempty(x.dd) +Base.length(x::AtomicArraySet) = length(x.dd) +Base.sizehint!(x::AtomicArraySet, n::Integer) = (sizehint!(x.dd, n); x) +Base.in(item, x::AtomicArraySet) = haskey(x.dd, item) +Base.push!(x::AtomicArraySet, item) = (x.dd[item] = nothing; x) +Base.delete!(x::AtomicArraySet, item) = (delete!(x.dd, item); x) +Base.empty(::AtomicArraySet{D}) where {D} = AtomicArraySet{D}() +Base.copy(x::AtomicArraySet{D}) where {D} = AtomicArraySet{D}(copy(x.dd)) +Base.iterate(x::AtomicArraySet, args...) = iterate(keys(x.dd), args...) + +function Base.filter!(f::F, x::AtomicArraySet) where {F} + filter!(f ∘ first, x.dd) + return x +end + +""" + $TYPEDSIGNATURES + +Add `item` to `x`. If `item` is an indexed array, add the array instead. +""" +function push_as_atomic_array!(x::AtomicArraySet, item::SymbolicT) + push!(x, split_indexed_var(item)[1]) +end + +""" + $METHODLIST + +Convert an array of possibly scalarized variables into an `AtomicArraySet`. +""" +as_atomic_array_set(vars::Vector{SymbolicT}) = as_atomic_array_set(Dict{SymbolicT, Nothing}, vars) +function as_atomic_array_set(::Type{D}, vars::Vector{SymbolicT}) where {D} + set = AtomicArraySet{D}() + for v in vars + push_as_atomic_array!(set, v) + end + return set +end + +function contains_possibly_indexed_element(x::AtomicArraySet, k::SymbolicT) + has_possibly_indexed_key(x.dd, k) +end diff --git a/src/constants.jl b/lib/ModelingToolkitBase/src/constants.jl similarity index 81% rename from src/constants.jl rename to lib/ModelingToolkitBase/src/constants.jl index 4113287ad4..fed010a2ee 100644 --- a/src/constants.jl +++ b/lib/ModelingToolkitBase/src/constants.jl @@ -3,7 +3,7 @@ Test whether `x` is a constant-type Sym. """ function isconstant(x) x = unwrap(x) - x isa Symbolic && !getmetadata(x, VariableTunable, true) + x isa SymbolicT && !getmetadata(x, VariableTunable, true) end """ @@ -26,8 +26,8 @@ Define one or more constants. See also [`@independent_variables`](@ref), [`@parameters`](@ref) and [`@variables`](@ref). """ macro constants(xs...) - Symbolics._parse_vars(:constants, + Symbolics.parse_vars(:constants, Real, xs, - toconstant) |> esc + toconstant) end diff --git a/src/debugging.jl b/lib/ModelingToolkitBase/src/debugging.jl similarity index 100% rename from src/debugging.jl rename to lib/ModelingToolkitBase/src/debugging.jl diff --git a/src/deprecations.jl b/lib/ModelingToolkitBase/src/deprecations.jl similarity index 100% rename from src/deprecations.jl rename to lib/ModelingToolkitBase/src/deprecations.jl diff --git a/lib/ModelingToolkitBase/src/derivative_dict.jl b/lib/ModelingToolkitBase/src/derivative_dict.jl new file mode 100644 index 0000000000..5747ea83b5 --- /dev/null +++ b/lib/ModelingToolkitBase/src/derivative_dict.jl @@ -0,0 +1,62 @@ +""" + $(TYPEDEF) + +Wrapper over an `AbstractDict{K, SymbolicT} where {K}` which matches higher order +derivatives if the first-order derivative is present in the wrapped dictionary. +Specifically, if `Differential(t, 1)(x)` is present in the wrapped dictionary with value +`v`, then `Differential(t, n::Int)(x)` maps to `Differential(t, n - 1)(v)`. This only +affects `get`, `getindex` and `haskey`. All other methods fall back to the wrapped +dictionary. +""" +struct DerivativeDict{K, D <: AbstractDict{K, SymbolicT}} <: AbstractDict{K, SymbolicT} + dict::D +end + +DerivativeDict() = DerivativeDict(Dict{SymbolicT, SymbolicT}()) +DerivativeDict(args::Pair...) = DerivativeDict(Dict(args...)) +DerivativeDict{K}(args...) where {K} = DerivativeDict(Dict{K, SymbolicT}(args...)) + +Base.copy(dd::DerivativeDict) = DerivativeDict(copy(dd.dict)) +function Base.empty(dd::DerivativeDict, ::Type{K}, ::Type{V}) where {K, V} + DerivativeDict(empty(dd.dict, K, V)) +end + +struct __DDSentinel end +const DD_SENTINEL = __DDSentinel() + +function Base.get(def::Base.Callable, dd::DerivativeDict, k::SymbolicT) + res = get(dd.dict, k, DD_SENTINEL) + res === DD_SENTINEL || return res + Moshi.Match.@match k begin + BSImpl.Term(; f, args) && if f isa Differential && f.order::Int > 1 end => begin + order = f.order::Int + res = get(dd.dict, Differential(f.x, 1)(args[1]), DD_SENTINEL) + res === DD_SENTINEL && return def() + res = res::SymbolicT + return Differential(f.x, order - 1)(res) + end + _ => return def() + end +end +function Base.get(f::Base.Callable, dd::DerivativeDict, k::Union{Num, Arr, CallAndWrap}) + return get(f, dd, unwrap(k)) +end +Base.get(f::Base.Callable, dd::DerivativeDict, k) = get(f, dd.dict, k) + +Base.get(dd::DerivativeDict, k, default) = get(Returns(default), dd, k) + +Base.haskey(dd::DerivativeDict, k) = get(Returns(DD_SENTINEL), dd, k) !== DD_SENTINEL + +function Base.getindex(dd::DerivativeDict, k) + res = get(Returns(DD_SENTINEL), dd, k) + res === DD_SENTINEL && throw(KeyError(k)) + return res::SymbolicT +end + +Base.setindex!(dd::DerivativeDict, v, k) = setindex!(dd.dict, unwrap(v), unwrap(k)) + +Base.isempty(dd::DerivativeDict) = isempty(dd.dict) +Base.length(dd::DerivativeDict) = length(dd.dict) +Base.iterate(dd::DerivativeDict, args...) = Base.iterate(dd.dict, args...) +Base.sizehint!(dd::DerivativeDict, n; kw...) = sizehint!(dd.dict, n; kw...) +Base.empty!(dd::DerivativeDict) = empty!(dd.dict) diff --git a/lib/ModelingToolkitBase/src/discretedomain.jl b/lib/ModelingToolkitBase/src/discretedomain.jl new file mode 100644 index 0000000000..6531596340 --- /dev/null +++ b/lib/ModelingToolkitBase/src/discretedomain.jl @@ -0,0 +1,312 @@ +using Symbolics: Operator, Num, Term, value, recursive_hasoperator + +# Shift + +""" +$(TYPEDEF) + +Represents a shift operator. + +# Fields +$(FIELDS) + +# Examples + +```jldoctest +julia> using Symbolics + +julia> Δ = Shift(t) +(::Shift) (generic function with 2 methods) +``` +""" +struct Shift <: Operator + """Fixed Shift""" + t::Union{Nothing, SymbolicT} + steps::Int + Shift(t, steps = 1) = new(value(t), steps) +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::Equation, allow_zero = false) + D(x.lhs, allow_zero) ~ D(x.rhs, allow_zero) +end +function (D::Shift)(x, allow_zero = false) + !allow_zero && D.steps == 0 && return x + term(D, x; type = symtype(x), shape = SU.shape(x)) +end +function (D::Shift)(x::Union{Num, Symbolics.Arr}, allow_zero = false) + !allow_zero && D.steps == 0 && return x + vt = value(x) + if iscall(vt) + op = operation(vt) + if op isa Shift + if D.t === nothing || isequal(D.t, op.t) + arg = arguments(vt)[1] + newsteps = D.steps + op.steps + return wrap(newsteps == 0 ? arg : Shift(D.t, newsteps)(arg)) + end + end + end + wrap(D(vt, allow_zero)) +end +SymbolicUtils.promote_symtype(::Shift, ::Type{T}) where {T} = T +SymbolicUtils.promote_shape(::Shift, @nospecialize(x::SU.ShapeT)) = x + +Base.show(io::IO, D::Shift) = print(io, "Shift(", D.t, ", ", D.steps, ")") + +Base.:(==)(D1::Shift, D2::Shift) = isequal(D1.t, D2.t) && isequal(D1.steps, D2.steps) +Base.hash(D::Shift, u::UInt) = hash(D.steps, hash(D.t, xor(u, 0x055640d6d952f101))) + +Base.:^(D::Shift, n::Integer) = Shift(D.t, D.steps * n) +Base.literal_pow(f::typeof(^), D::Shift, ::Val{n}) where {n} = Shift(D.t, D.steps * n) + +function validate_operator(op::Shift, args, iv; context = nothing) + isequal(op.t, iv) || throw(OperatorIndepvarMismatchError(op, iv, context)) + op.steps <= 0 || error(""" + Only non-positive shifts are allowed. Found shift of $(op.steps) in $context. + """) +end + +hasshift(eq::Equation) = hasshift(eq.lhs) || hasshift(eq.rhs) + +""" + hasshift(O) + +Returns true if the expression or equation `O` contains [`Shift`](@ref) terms. +""" +hasshift(O) = recursive_hasoperator(Shift, O) + +# ShiftIndex + +struct IntegerSequence end + +""" + ShiftIndex + +The `ShiftIndex` operator allows you to index a signal and obtain a shifted discrete-time signal. If the signal is continuous-time, the signal is sampled before shifting. + +# Examples + +``` +julia> t = ModelingToolkitBase.t_nounits; + +julia> @variables x(t); + +julia> k = ShiftIndex(t, 0.1); + +julia> x(k) # no shift +x(t) + +julia> x(k+1) # shift +Shift(1)(x(t)) +``` +""" +struct ShiftIndex + clock::Any + steps::Int + ShiftIndex(clock, steps::Int = 0) = new(clock, steps) + ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps) + ShiftIndex(::Num, steps::Int) = new(IntegerSequence(), steps) +end + +function (xn::Num)(k::ShiftIndex) + @unpack clock, steps = k + x = unwrap(xn) + # Verify that the independent variables of k and x match and that the expression doesn't have multiple variables + vars = Set{SymbolicT}() + SU.search_variables!(vars, x) + if length(vars) != 1 + error("Cannot shift a multivariate expression $x. Either create a new unknown and shift this, or shift the individual variables in the expression.") + end + var = only(vars) + if operation(var) === getindex + var = arguments(var)[1] + end + if !iscall(var) + throw(ArgumentError("Cannot shift time-independent variable $var")) + end + if length(arguments(var)) != 1 + error("Cannot shift an expression with multiple independent variables $x.") + end + t = only(arguments(var)) + + # d, _ = propagate_time_domain(xn) + # if d != clock # this is only required if the variable has another clock + # xn = Sample(t, clock)(xn) + # end + # QUESTION: should we return a variable with time domain set to k.clock? + xn = setmetadata(xn, VariableTimeDomain, k.clock) + if steps == 0 + return xn # x(k) needs no shift operator if the step of k is 0 + end + Shift(t, steps)(xn) # a shift of k steps +end + +function (xn::Symbolics.Arr)(k::ShiftIndex) + @unpack clock, steps = k + x = unwrap(xn) + # Verify that the independent variables of k and x match and that the expression doesn't have multiple variables + vars = Set{SymbolicT}() + SU.search_variables!(vars, x) + if length(vars) != 1 + error("Cannot shift a multivariate expression $x. Either create a new unknown and shift this, or shift the individual variables in the expression.") + end + var = only(vars) + if !iscall(var) + throw(ArgumentError("Cannot shift time-independent variable $var")) + end + if length(arguments(var)) != 1 + error("Cannot shift an expression with multiple independent variables $x.") + end + t = only(arguments(var)) + + # d, _ = propagate_time_domain(xn) + # if d != clock # this is only required if the variable has another clock + # xn = Sample(t, clock)(xn) + # end + # QUESTION: should we return a variable with time domain set to k.clock? + xn = wrap(setmetadata(unwrap(xn), VariableTimeDomain, k.clock)) + if steps == 0 + return xn # x(k) needs no shift operator if the step of k is 0 + end + Shift(t, steps)(xn) # a shift of k steps +end + +Base.:+(k::ShiftIndex, i::Int) = ShiftIndex(k.clock, k.steps + i) +Base.:-(k::ShiftIndex, i::Int) = k + (-i) + +# SampleTime + +""" + function SampleTime() + +`SampleTime()` can be used in the equations of a hybrid system to represent time sampled +at the inferred clock for that equation. +""" +struct SampleTime <: Operator + SampleTime() = SymbolicUtils.term(SampleTime, type = Real) +end +SymbolicUtils.promote_symtype(::Type{SampleTime}, ::Type{T}) where {T} = Real +SymbolicUtils.promote_shape(::Type{SampleTime}, @nospecialize(x::SU.ShapeT)) = x +Base.nameof(::SampleTime) = :SampleTime +SymbolicUtils.isbinop(::SampleTime) = false + +function validate_operator(op::SampleTime, args, iv; context = nothing) end + +# Sample + +""" +$(TYPEDEF) + +Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal. + +# Constructors +`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete())` +`Sample(dt::Real)` + +`Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`. + +# Fields +$(FIELDS) + +# Examples + +```jldoctest +julia> using Symbolics + +julia> t = ModelingToolkit.t_nounits + +julia> Δ = Sample(0.01) +(::Sample) (generic function with 2 methods) +``` +""" +struct Sample <: Operator + clock::Any +end + +is_transparent_operator(::Type{Sample}) = true + +Sample(x::Num) = Sample()(unwrap(x)) +Sample(arg::Real) = Sample(Clock(arg)) +(D::Sample)(x) = STerm(D, SArgsT((x,)); type = symtype(x), shape = SU.shape(x)) +(D::Sample)(x::Num) = Num(D(value(x))) +SymbolicUtils.promote_symtype(::Sample, ::Type{T}) where {T} = T +SymbolicUtils.promote_shape(::Sample, @nospecialize(x::SU.ShapeT)) = x +Base.nameof(::Sample) = :Sample +SymbolicUtils.isbinop(::Sample) = false + +Base.show(io::IO, D::Sample) = print(io, "Sample(", D.clock, ")") + +Base.:(==)(D1::Sample, D2::Sample) = isequal(D1.clock, D2.clock) +Base.hash(D::Sample, u::UInt) = hash(D.clock, xor(u, 0x055640d6d952f101)) + +function validate_operator(op::Sample, args, iv; context = nothing) + arg = unwrap(only(args)) + if !is_variable_floatingpoint(arg) + throw(ContinuousOperatorDiscreteArgumentError(op, arg, context)) + end + if isparameter(arg) + throw(ArgumentError(""" + Expected argument of $op to be an unknown, found $arg which is a parameter. + """)) + end +end + +""" + hassample(O) + +Returns true if the expression or equation `O` contains [`Sample`](@ref) terms. +""" +hassample(O) = recursive_hasoperator(Sample, unwrap(O)) + +# Hold + +""" +$(TYPEDEF) + +Represents a hold operator. A continuous-time signal is produced by holding a discrete-time signal `x` with zero-order hold. + +``` +cont_x = Hold()(disc_x) +``` +""" +struct Hold <: Operator end + +(D::Hold)(x) = STerm(D, SArgsT((x,)); type = symtype(x), shape = SU.shape(x)) +(D::Hold)(x::Number) = x +(D::Hold)(x::Num) = Num(D(value(x))) +SymbolicUtils.promote_symtype(::Hold, ::Type{T}) where {T} = T +SymbolicUtils.promote_shape(::Hold, @nospecialize(x::SU.ShapeT)) = x +Base.nameof(::Hold) = :Hold +SymbolicUtils.isbinop(::Hold) = false + +Hold(x) = Hold()(x) + +function validate_operator(op::Hold, args, iv; context = nothing) + # TODO: maybe validate `VariableTimeDomain`? + return nothing +end + +""" + hashold(O) + +Returns true if the expression or equation `O` contains [`Hold`](@ref) terms. +""" +hashold(O) = recursive_hasoperator(Hold, unwrap(O)) + +function ZeroCrossing(expr; name = gensym(), up = true, down = true, kwargs...) + return SymbolicContinuousCallback( + [expr ~ 0], up ? ImperativeAffect(Returns(nothing)) : nothing; + affect_neg = down ? ImperativeAffect(Returns(nothing)) : nothing, + kwargs..., zero_crossing_id = name) +end + +function SciMLBase.Clocks.EventClock(cb::SymbolicContinuousCallback) + return SciMLBase.Clocks.EventClock(cb.zero_crossing_id) +end + +distribute_shift_into_operator(::Sample) = false +distribute_shift_into_operator(::Hold) = false diff --git a/lib/ModelingToolkitBase/src/discretes.jl b/lib/ModelingToolkitBase/src/discretes.jl new file mode 100644 index 0000000000..8f939f45cd --- /dev/null +++ b/lib/ModelingToolkitBase/src/discretes.jl @@ -0,0 +1,28 @@ +function todiscrete_validate(s::SymbolicT) + if !iscall(s) + error(""" + `@discretes` cannot create time-independent variables. Encountered $s. Use \ + `@parameters` for this purpose. + """) + end + toparam(s) +end +function todiscrete_validate(s::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + typeof(s)(todiscrete_validate(unwrap(s))) +end + +""" +$(SIGNATURES) + +Define one or more discrete variables, for use in events of continuous systems. All +symbolics declare with this macro must be dependent variables. + +See also [`@independent_variables`](@ref), [`@variables`](@ref) and [`@constants`](@ref). +""" +macro discretes(xs...) + Symbolics.parse_vars(:discretes, + Real, + xs, + todiscrete_validate) +end + diff --git a/src/independent_variables.jl b/lib/ModelingToolkitBase/src/independent_variables.jl similarity index 66% rename from src/independent_variables.jl rename to lib/ModelingToolkitBase/src/independent_variables.jl index d1f2ab4210..fce2d93873 100644 --- a/src/independent_variables.jl +++ b/lib/ModelingToolkitBase/src/independent_variables.jl @@ -7,12 +7,12 @@ Define one or more independent variables. For example: @variables x(t) """ macro independent_variables(ts...) - Symbolics._parse_vars(:independent_variables, + Symbolics.parse_vars(:independent_variables, Real, ts, - toiv) |> esc + toiv) end -toiv(s::Symbolic) = GlobalScope(setmetadata(s, MTKVariableTypeCtx, PARAMETER)) +toiv(s::SymbolicT) = GlobalScope(setmetadata(s, MTKVariableTypeCtx, PARAMETER)) toiv(s::Symbolics.Arr) = wrap(toiv(value(s))) toiv(s::Num) = Num(toiv(value(s))) diff --git a/src/inputoutput.jl b/lib/ModelingToolkitBase/src/inputoutput.jl similarity index 56% rename from src/inputoutput.jl rename to lib/ModelingToolkitBase/src/inputoutput.jl index 3807e912d6..08b0a78d45 100644 --- a/src/inputoutput.jl +++ b/lib/ModelingToolkitBase/src/inputoutput.jl @@ -49,17 +49,38 @@ See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@re """ unbound_outputs(sys) = filter(x -> !is_bound(sys, x), outputs(sys)) -""" - is_bound(sys, u) +function _is_atomic_inside_operator(ex::SymbolicT) + SU.default_is_atomic(ex) && Moshi.Match.@match ex begin + BSImpl.Term(; f) && if f isa Operator end => false + _ => true + end +end -Determine whether input/output variable `u` is "bound" within the system, i.e., if it's to be considered internal to `sys`. -A variable/signal is considered bound if it appears in an equation together with variables from other subsystems. -The typical usecase for this function is to determine whether the input to an IO component is connected to another component, -or if it remains an external input that the user has to supply before simulating the system. +struct IsBoundValidator + eqs_vars::Vector{Set{SymbolicT}} + obs_vars::Vector{Set{SymbolicT}} + stack::OrderedSet{SymbolicT} +end -See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref) -""" -function is_bound(sys, u, stack = []) +function IsBoundValidator(sys::System) + eqs_vars = Set{SymbolicT}[] + for eq in equations(sys) + vars = Set{SymbolicT}() + SU.search_variables!(vars, eq.rhs; is_atomic = _is_atomic_inside_operator) + SU.search_variables!(vars, eq.lhs; is_atomic = _is_atomic_inside_operator) + push!(eqs_vars, vars) + end + obs_vars = Set{SymbolicT}[] + for eq in observed(sys) + vars = Set{SymbolicT}() + SU.search_variables!(vars, eq.rhs; is_atomic = _is_atomic_inside_operator) + SU.search_variables!(vars, eq.lhs; is_atomic = _is_atomic_inside_operator) + push!(obs_vars, vars) + end + return IsBoundValidator(eqs_vars, obs_vars, OrderedSet{SymbolicT}()) +end + +function (ibv::IsBoundValidator)(u::SymbolicT) #= For observed quantities, we check if a variable is connected to something that is bound to something further out. In the following scenario @@ -71,35 +92,42 @@ function is_bound(sys, u, stack = []) When asking is_bound(sys₊y(t)), we know that we are looking through observed equations and can thus ask if var is bound, if it is, then sys₊y(t) is also bound. This can lead to an infinite recursion, so we maintain a stack of variables we have previously asked about to be able to break cycles =# - u ∈ Set(stack) && return false # Cycle detected - eqs = equations(sys) - eqs = filter(eq -> has_var(eq, u), eqs) # Only look at equations that contain u - # isout = isoutput(u) - for eq in eqs - vars = [get_variables(eq.rhs); get_variables(eq.lhs)] + u in ibv.stack && return false # Cycle detected + for vars in ibv.eqs_vars + u in vars || continue for var in vars var === u && continue - if !same_or_inner_namespace(u, var) - return true - end + same_or_inner_namespace(u, var) || return true end end - # Look through observed equations as well - oeqs = observed(sys) - oeqs = filter(eq -> has_var(eq, u), oeqs) # Only look at equations that contain u - for eq in oeqs - vars = [get_variables(eq.rhs); get_variables(eq.lhs)] + for vars in ibv.obs_vars + u in vars || continue for var in vars var === u && continue - if !same_or_inner_namespace(u, var) - return true - end - if is_bound(sys, var, [stack; u]) && !inner_namespace(u, var) # The variable we are comparing to can not come from an inner namespace, binding only counts outwards - return true - end + same_or_inner_namespace(u, var) || return true + push!(ibv.stack, u) + isbound = ibv(var) + pop!(ibv.stack) + # The variable we are comparing to can not come from an inner namespace, + # binding only counts outwards + isbound && !inner_namespace(u, var) && return true end end - false + return false +end + +""" + is_bound(sys, u) + +Determine whether input/output variable `u` is "bound" within the system, i.e., if it's to be considered internal to `sys`. +A variable/signal is considered bound if it appears in an equation together with variables from other subsystems. +The typical usecase for this function is to determine whether the input to an IO component is connected to another component, +or if it remains an external input that the user has to supply before simulating the system. + +See also [`bound_inputs`](@ref), [`unbound_inputs`](@ref), [`bound_outputs`](@ref), [`unbound_outputs`](@ref) +""" +function is_bound(sys, u) + return IsBoundValidator(sys)(unwrap(u)) end """ @@ -185,7 +213,7 @@ The return values also include the chosen state-realization (the remaining unkno # Example ```julia -using ModelingToolkit: generate_control_function, varmap_to_vars, defaults +using ModelingToolkitBase: generate_control_function, varmap_to_vars, defaults f, x_sym, ps = generate_control_function(sys, expression=Val{false}, simplify=false) p = varmap_to_vars(defaults(sys), ps) x = varmap_to_vars(defaults(sys), x_sym) @@ -232,9 +260,9 @@ function generate_control_function(sys::AbstractSystem, inputs = unbound_inputs( if !isempty(all_disturbances) inputs = [inputs; all_disturbances] end - + inputs = vec(unwrap_vars(inputs)) dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) + ps::Vector{SymbolicT} = parameters(sys; initial_parameters = true) ps = setdiff(ps, inputs) # Remove unknown disturbances from inputs (we don't want them as actual inputs to the dynamics) @@ -283,179 +311,3 @@ function generate_control_function(sys::AbstractSystem, inputs = unbound_inputs( ps = setdiff(parameters(sys), inputs, all_disturbances) (; f = (f, f), dvs, ps, io_sys = sys) end - -""" -Turn input variables into parameters of the system. -""" -function inputs_to_parameters!(state::TransformationState, inputsyms) - check_bound = inputsyms === nothing - @unpack structure, fullvars, sys = state - @unpack var_to_diff, graph, solvable_graph = structure - @assert solvable_graph === nothing - - inputs = BitSet() - var_reidx = zeros(Int, length(fullvars)) - ninputs = 0 - nvar = 0 - new_parameters = [] - input_to_parameters = Dict() - new_fullvars = [] - for (i, v) in enumerate(fullvars) - if isinput(v) && !(check_bound && is_bound(sys, v)) - if var_to_diff[i] !== nothing - error("Input $(fullvars[i]) is differentiated!") - end - push!(inputs, i) - ninputs += 1 - var_reidx[i] = -1 - p = toparam(v) - push!(new_parameters, p) - input_to_parameters[v] = p - else - nvar += 1 - var_reidx[i] = nvar - push!(new_fullvars, v) - end - end - if ninputs == 0 - @set! sys.inputs = OrderedSet{BasicSymbolic}() - @set! sys.outputs = OrderedSet{BasicSymbolic}(filter(isoutput, fullvars)) - state.sys = sys - return state - end - - nvars = ndsts(graph) - ninputs - new_graph = BipartiteGraph(nsrcs(graph), nvars, Val(false)) - - for ie in 1:nsrcs(graph) - for iv in 𝑠neighbors(graph, ie) - iv = var_reidx[iv] - iv > 0 || continue - add_edge!(new_graph, ie, iv) - end - end - - new_var_to_diff = DiffGraph(nvars, true) - for (i, v) in enumerate(var_to_diff) - new_i = var_reidx[i] - (new_i < 1 || v === nothing) && continue - new_v = var_reidx[v] - @assert new_v > 0 - new_var_to_diff[new_i] = new_v - end - @set! structure.var_to_diff = complete(new_var_to_diff) - @set! structure.graph = complete(new_graph) - - @set! sys.eqs = isempty(input_to_parameters) ? equations(sys) : - fast_substitute(equations(sys), input_to_parameters) - @set! sys.unknowns = setdiff(unknowns(sys), keys(input_to_parameters)) - ps = parameters(sys) - - @set! sys.ps = [ps; new_parameters] - @set! sys.inputs = OrderedSet{BasicSymbolic}(new_parameters) - @set! sys.outputs = OrderedSet{BasicSymbolic}(filter(isoutput, fullvars)) - @set! state.sys = sys - @set! state.fullvars = Vector{BasicSymbolic}(new_fullvars) - @set! state.structure = structure - return state -end - -""" - DisturbanceModel{M} - -The structure represents a model of a disturbance, along with the input variable that is affected by the disturbance. See [`add_input_disturbance`](@ref) for additional details and an example. - -# Fields: - - - `input`: The variable affected by the disturbance. - - `model::M`: A model of the disturbance. This is typically a `System`, but type that implements [`ModelingToolkit.get_disturbance_system`](@ref)`(dist::DisturbanceModel) -> ::System` is supported. -""" -struct DisturbanceModel{M} - input::Any - model::M - name::Symbol -end -DisturbanceModel(input, model; name) = DisturbanceModel(input, model, name) - -# Point of overloading for libraries, e.g., to be able to support disturbance models from ControlSystemsBase -function get_disturbance_system(dist::DisturbanceModel{System}) - dist.model -end - -""" - (f_oop, f_ip), augmented_sys, dvs, p = add_input_disturbance(sys, dist::DisturbanceModel, inputs = Any[]) - -Add a model of an unmeasured disturbance to `sys`. The disturbance model is an instance of [`DisturbanceModel`](@ref). - -The generated dynamics functions `(f_oop, f_ip)` will preserve any state and dynamics associated with disturbance inputs, but the disturbance inputs themselves will not be included as inputs to the generated function. The use case for this is to generate dynamics for state observers that estimate the influence of unmeasured disturbances, and thus require state variables for the disturbance model, but without disturbance inputs since the disturbances are not available for measurement. - -`dvs` will be the states of the simplified augmented system, consisting of the states of `sys` as well as the states of the disturbance model. - -For MIMO systems, all inputs to the system has to be specified in the argument `inputs` - -# Example - -The example below builds a double-mass model and adds an integrating disturbance to the input - -```julia -using ModelingToolkit -using ModelingToolkitStandardLibrary -using ModelingToolkitStandardLibrary.Mechanical.Rotational -using ModelingToolkitStandardLibrary.Blocks -t = ModelingToolkitStandardLibrary.Blocks.t - -# 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(; use_support = false) - -eqs = [connect(torque.flange, inertia1.flange_a) - connect(inertia1.flange_b, spring.flange_a, damper.flange_a) - connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] -model = System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name = :model) -model = complete(model) -model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] - -# Disturbance model -@named dmodel = Blocks.StateSpace([0.0], [1.0], [1.0], [0.0]) # An integrating disturbance -@named dist = ModelingToolkit.DisturbanceModel(model.torque.tau.u, dmodel) -(f_oop, f_ip), augmented_sys, dvs, p = ModelingToolkit.add_input_disturbance(model, dist) -``` - -`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 = Any[]; kwargs...) - t = get_iv(sys) - @variables d(t)=0 [disturbance = true] - @variables u(t)=0 [input = true] # New system input - dsys = get_disturbance_system(dist) - - if isempty(inputs) - all_inputs = [u] - else - i = findfirst(isequal(dist.input), inputs) - if i === nothing - throw(ArgumentError("Input $(dist.input) indicated in the disturbance model was not found among inputs specified to add_input_disturbance")) - end - all_inputs = convert(Vector{Any}, copy(inputs)) - all_inputs[i] = u # The input where the disturbance acts is no longer an input, the new input is u - end - - eqs = [dsys.input.u[1] ~ d - dist.input ~ u + dsys.output.u[1]] - augmented_sys = System(eqs, t, systems = [dsys], name = gensym(:outer)) - augmented_sys = extend(augmented_sys, sys) - ssys = mtkcompile(augmented_sys, inputs = all_inputs, disturbance_inputs = [d]) - - f, dvs, p, io_sys = generate_control_function(ssys, all_inputs, - [d]; kwargs...) - f, augmented_sys, dvs, p, io_sys -end diff --git a/src/modelingtoolkitize/common.jl b/lib/ModelingToolkitBase/src/modelingtoolkitize/common.jl similarity index 99% rename from src/modelingtoolkitize/common.jl rename to lib/ModelingToolkitBase/src/modelingtoolkitize/common.jl index ffddca2f4c..e9f5951743 100644 --- a/src/modelingtoolkitize/common.jl +++ b/lib/ModelingToolkitBase/src/modelingtoolkitize/common.jl @@ -20,14 +20,14 @@ Define a subscripted time-dependent variable with name `x` and subscript `i`. Eq to `@variables \$name(..)`. `T` is the desired symtype of the variable when called with the independent variable. """ -_defvaridx(x, i; T = Real) = variable(x, i, T = SymbolicUtils.FnType{Tuple, T}) +_defvaridx(x, i; T = Real) = variable(x, i, T = SymbolicUtils.FnType{Tuple, T, Nothing}) """ $(TYPEDSIGNATURES) Define a time-dependent variable with name `x`. Equivalent to `@variables \$x(..)`. `T` is the desired symtype of the variable when called with the independent variable. """ -_defvar(x; T = Real) = variable(x, T = SymbolicUtils.FnType{Tuple, T}) +_defvar(x; T = Real) = variable(x, T = SymbolicUtils.FnType{Tuple, T, Nothing}) """ $(TYPEDSIGNATURES) diff --git a/src/modelingtoolkitize/nonlinearproblem.jl b/lib/ModelingToolkitBase/src/modelingtoolkitize/nonlinearproblem.jl similarity index 89% rename from src/modelingtoolkitize/nonlinearproblem.jl rename to lib/ModelingToolkitBase/src/modelingtoolkitize/nonlinearproblem.jl index 92425be373..b8c100a525 100644 --- a/src/modelingtoolkitize/nonlinearproblem.jl +++ b/lib/ModelingToolkitBase/src/modelingtoolkitize/nonlinearproblem.jl @@ -2,7 +2,7 @@ $(TYPEDSIGNATURES) Convert a `NonlinearProblem` or `NonlinearLeastSquaresProblem` to a -`ModelingToolkit.System`. +`ModelingToolkitBase.System`. # Keyword arguments @@ -28,6 +28,8 @@ function modelingtoolkitize( rhs = trace_rhs(prob, vars, params, nothing; prototype = prob.f.resid_prototype) eqs = vcat([0 ~ rhs[i] for i in eachindex(rhs)]...) + filter!(eq -> !SU._iszero(eq.rhs), eqs) + sts = vec(collect(vars)) # turn `params` into a list of symbolic variables as opposed to @@ -35,14 +37,14 @@ function modelingtoolkitize( _params = params params = to_paramvec(params) - defaults = defaults_from_u0_p(prob, vars, _params, params) + initial_conditions = defaults_from_u0_p(prob, vars, _params, params) # In case initials crept in, specifically from when we constructed parameters # using prob.f.sys filter!(x -> !iscall(x) || !(operation(x) isa Initial), params) - filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), defaults) + filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), initial_conditions) return System(eqs, sts, params; - defaults, + initial_conditions, name = gensym(:MTKizedNonlin), kwargs...) end diff --git a/src/modelingtoolkitize/odeproblem.jl b/lib/ModelingToolkitBase/src/modelingtoolkitize/odeproblem.jl similarity index 91% rename from src/modelingtoolkitize/odeproblem.jl rename to lib/ModelingToolkitBase/src/modelingtoolkitize/odeproblem.jl index 3bc74d8887..6905ce2bdb 100644 --- a/src/modelingtoolkitize/odeproblem.jl +++ b/lib/ModelingToolkitBase/src/modelingtoolkitize/odeproblem.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Convert an `ODEProblem` to a `ModelingToolkit.System`. +Convert an `ODEProblem` to a `ModelingToolkitBase.System`. # Keyword arguments @@ -40,14 +40,14 @@ function modelingtoolkitize(prob::ODEProblem; u_names = nothing, p_names = nothi _params = params params = to_paramvec(params) - defaults = defaults_from_u0_p(prob, vars, _params, params) + initial_conditions = defaults_from_u0_p(prob, vars, _params, params) # In case initials crept in, specifically from when we constructed parameters # using prob.f.sys filter!(x -> !iscall(x) || !(operation(x) isa Initial), params) - filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), defaults) + filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), initial_conditions) sys = System(eqs, t, sts, params; - defaults, + initial_conditions, name = gensym(:MTKizedODE), kwargs...) diff --git a/src/modelingtoolkitize/optimizationproblem.jl b/lib/ModelingToolkitBase/src/modelingtoolkitize/optimizationproblem.jl similarity index 94% rename from src/modelingtoolkitize/optimizationproblem.jl rename to lib/ModelingToolkitBase/src/modelingtoolkitize/optimizationproblem.jl index 26d557152a..e2380db5a9 100644 --- a/src/modelingtoolkitize/optimizationproblem.jl +++ b/lib/ModelingToolkitBase/src/modelingtoolkitize/optimizationproblem.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Convert an `OptimizationProblem` to a `ModelingToolkit.System`. +Convert an `OptimizationProblem` to a `ModelingToolkitBase.System`. # Keyword arguments @@ -80,15 +80,15 @@ function modelingtoolkitize( _params = params params = to_paramvec(params) - defaults = defaults_from_u0_p(prob, vars, _params, params) + initial_conditions = defaults_from_u0_p(prob, vars, _params, params) # In case initials crept in, specifically from when we constructed parameters # using prob.f.sys filter!(x -> !iscall(x) || !(operation(x) isa Initial), params) - filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), defaults) + filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), initial_conditions) sts = vec(collect(vars)) sys = OptimizationSystem(objective, sts, params; - defaults, + initial_conditions, constraints = cons, name = gensym(:MTKizedOpt), kwargs...) diff --git a/src/modelingtoolkitize/sdeproblem.jl b/lib/ModelingToolkitBase/src/modelingtoolkitize/sdeproblem.jl similarity index 94% rename from src/modelingtoolkitize/sdeproblem.jl rename to lib/ModelingToolkitBase/src/modelingtoolkitize/sdeproblem.jl index 0f63a35fc1..47c69fae2b 100644 --- a/src/modelingtoolkitize/sdeproblem.jl +++ b/lib/ModelingToolkitBase/src/modelingtoolkitize/sdeproblem.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Convert an `SDEProblem` to a `ModelingToolkit.System`. +Convert an `SDEProblem` to a `ModelingToolkitBase.System`. # Keyword arguments @@ -48,7 +48,7 @@ function modelingtoolkitize( end end - @set! sys.noise_eqs = neqs + @set! sys.noise_eqs = unwrap_vars(neqs) return sys end diff --git a/lib/ModelingToolkitBase/src/parameter_bindings_graph.jl b/lib/ModelingToolkitBase/src/parameter_bindings_graph.jl new file mode 100644 index 0000000000..348b36217f --- /dev/null +++ b/lib/ModelingToolkitBase/src/parameter_bindings_graph.jl @@ -0,0 +1,147 @@ +""" + $TYPEDEF + +A struct which stores dependency information about the bound parameters in a system. + +# Fields + +$TYPEDFIELDS +""" +struct ParameterBindingsGraph + """ + An ordered set of bound parameters, in topologically sorted order. + """ + bound_ps::AtomicArraySet{OrderedDict{SymbolicT, Nothing}} + """ + Since indexing `OrderedSet` is deprecated, this maps an index to the corresponding + bound parameter. Literally just `collect(bound_ps)`. + """ + bound_ps_order::Vector{SymbolicT} + """ + Mapping from bound parameters to their index in `bound_ps`. + """ + bound_par_idx::AtomicArrayDict{Int, Dict{SymbolicT, Int}} + """ + Dependency graph for bindings. Edges flow from a bound parameter to the + other bound parameters it depends on. + """ + dependency_graph::SimpleDiGraph{Int} +end + +function ParameterBindingsGraph(sys::AbstractSystem) + if !isempty(get_systems(sys)) + throw(ArgumentError("`ParameterBindingsGraph` can only be created from a flattened system.")) + end + all_ps = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + for p in get_ps(sys) + push_as_atomic_array!(all_ps, p) + end + # If the system already has a pbgraph, it was previously `complete`d. Those bound parameters + # will not be in `ps`, but need to be considered here. + old_pbgraph = get_parameter_bindings_graph(sys) + if old_pbgraph isa ParameterBindingsGraph + union!(all_ps, old_pbgraph.bound_ps) + end + # This may be called on a non-completed system, or a `split=false` system, or one with + # callbacks modified. The only fool-proof way to find discretes is to filter out ones + # from callbacks. + all_discretes = get_all_discretes(sys) + + static_ps = setdiff!(all_ps, all_discretes) + binds = bindings(sys) + bound_ps = intersect!(static_ps, keys(binds)) + filter!(!(Base.Fix2(===, COMMON_MISSING) ∘ Base.Fix1(getindex, binds)), bound_ps) + + bound_par_idx = AtomicArrayDict{Int}() + for (i, p) in enumerate(bound_ps) + bound_par_idx[p] = i + end + + dependency_graph = SimpleDiGraph{Int}(length(bound_ps)) + varsbuf = Set{SymbolicT}() + for (i, p) in enumerate(bound_ps) + val = binds[p] + empty!(varsbuf) + Symbolics.get_variables!(varsbuf, val, bound_ps) + + for dep in varsbuf + add_edge!(dependency_graph, i, bound_par_idx[dep]) + end + end + + possible_cycles = simplecycles_iter(dependency_graph, 1) + if !isempty(possible_cycles) + throw(CyclicBindingsError(collect(bound_ps)[possible_cycles[1]])) + end + + toporder = topological_sort(dependency_graph) + reverse!(toporder) + bound_ps_order = collect(bound_ps) + _bound_ps = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + _bound_par_idx = AtomicArrayDict{Int}() + _dep_graph = SimpleDiGraph{Int}(length(bound_ps)) + + for (ii, i) in enumerate(toporder) + push!(_bound_ps, bound_ps_order[i]) + _bound_par_idx[bound_ps_order[i]] = ii + end + + for e in edges(dependency_graph) + add_edge!(_dep_graph, _bound_par_idx[bound_ps_order[src(e)]], _bound_par_idx[bound_ps_order[dst(e)]]) + end + bound_ps = _bound_ps + bound_par_idx = _bound_par_idx + dependency_graph = _dep_graph + + return ParameterBindingsGraph(bound_ps, collect(bound_ps), bound_par_idx, dependency_graph) +end + +""" + $TYPEDSIGNATURES + +Find the bound parameters used by `expr`. +""" +function bound_parameters_used_by!(buffer::OrderedSet{SymbolicT}, sys::AbstractSystem, expr; + bgraph::ParameterBindingsGraph = get_parameter_bindings_graph(sys)) + # No point searching if we've already included all of them. + if issubset(bgraph.bound_ps, buffer) + return buffer + end + + vars = OrderedSet{SymbolicT}() + Symbolics.get_variables!(vars, expr, bgraph.bound_ps) + idxs = Int[] + for v in vars + push!(idxs, bgraph.bound_par_idx[v]) + end + for i in BFSIterator(bgraph.dependency_graph, idxs) + push!(buffer, bgraph.bound_ps_order[i]) + end + + return buffer +end + +""" + $TYPEDSIGNATURES + +Topologically sort the bound parameters `bound_ps`. +""" +function sort_bound_parameters!(bound_ps::OrderedSet{SymbolicT}, sys::AbstractSystem; + bgraph::ParameterBindingsGraph = get_parameter_bindings_graph(sys)) + sort!(bound_ps; by = Base.Fix1(getindex, bgraph.bound_par_idx)) + return bound_ps +end + +struct CyclicBindingsError <: Exception + cycle_ps::Vector{SymbolicT} +end + +function Base.showerror(io::IO, err::CyclicBindingsError) + println(io, """ + The bindings for parameters were found to have at least one cycle involving the \ + follow parameters: + """) + for p in err.cycle_ps + println(io, p) + end +end diff --git a/src/parameters.jl b/lib/ModelingToolkitBase/src/parameters.jl similarity index 55% rename from src/parameters.jl rename to lib/ModelingToolkitBase/src/parameters.jl index d8ff1bf1be..4f14dd4dbd 100644 --- a/src/parameters.jl +++ b/lib/ModelingToolkitBase/src/parameters.jl @@ -15,48 +15,29 @@ The symbolic metadata key for storing the `VariableType`. """ struct MTKVariableTypeCtx end -getvariabletype(x, def = VARIABLE) = getmetadata(unwrap(x), MTKVariableTypeCtx, def) +getvariabletype(x, def = VARIABLE) = safe_getmetadata(MTKVariableTypeCtx, unwrap(x), def)::Union{typeof(def), VariableType} """ $TYPEDEF Check if the variable contains the metadata identifying it as a parameter. """ -function isparameter(x) - x = unwrap(x) - - if x isa Symbolic && (varT = getvariabletype(x, nothing)) !== nothing - return varT === PARAMETER - #TODO: Delete this branch - elseif x isa Symbolic && Symbolics.getparent(x, false) !== false - p = Symbolics.getparent(x) - isparameter(p) || - (hasmetadata(p, Symbolics.VariableSource) && - getmetadata(p, Symbolics.VariableSource)[1] == :parameters) - elseif iscall(x) && operation(x) isa Symbolic - varT === PARAMETER || isparameter(operation(x)) - elseif iscall(x) && operation(x) == (getindex) - isparameter(arguments(x)[1]) - elseif x isa Symbolic - varT === PARAMETER - else - false - end +isparameter(x::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) = isparameter(unwrap(x)) +function isparameter(x::SymbolicT) + varT = getvariabletype(x, nothing) + return varT === PARAMETER end +isparameter(x) = false function iscalledparameter(x) x = unwrap(x) - return isparameter(getmetadata(x, CallWithParent, nothing)) + return SymbolicUtils.is_called_function_symbolic(x) && isparameter(operation(x)) end function getcalledparameter(x) x = unwrap(x) - # `parent` is a `CallWithMetadata` with the correct metadata, - # but no namespacing. `operation(x)` has the correct namespacing, - # but is not a `CallWithMetadata` and doesn't have any metadata. - # This approach combines both. - parent = getmetadata(x, CallWithParent) - return CallWithMetadata(operation(x), metadata(parent)) + @assert iscalledparameter(x) + return operation(x) end """ @@ -80,21 +61,35 @@ toparam(s::Num) = wrap(toparam(value(s))) Maps the variable to an unknown. """ -tovar(s::Symbolic) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) +tovar(s::SymbolicT) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) tovar(s::Union{Num, Symbolics.Arr}) = wrap(tovar(unwrap(s))) +function toparam_validate(s::SymbolicT) + if iscall(s) + error(""" + `@parameters` cannot create time-dependent parameters. Encountered $s. Use \ + `@discretes` for this purpose. + """) + end + toparam(s) +end +function toparam_validate(s::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + typeof(s)(toparam_validate(unwrap(s))) +end + """ $(SIGNATURES) -Define one or more known parameters. +Define one or more known parameters. A parameter is a non-time-dependent quantity +in the model. See also [`@independent_variables`](@ref), [`@variables`](@ref) and [`@constants`](@ref). """ macro parameters(xs...) - Symbolics._parse_vars(:parameters, + Symbolics.parse_vars(:parameters, Real, xs, - toparam) |> esc + toparam_validate) end function find_types(array) @@ -114,40 +109,6 @@ function find_types(array) return by.(array) end -function split_parameters_by_type(ps) - if ps === SciMLBase.NullParameters() - return Float64[], [] #use Float64 to avoid Any type warning - else - by = let set = Dict{Any, Int}(), counter = Ref(0) - x -> begin - get!(set, typeof(x)) do - counter[] += 1 - end - end - end - idxs = by.(ps) - split_idxs = [Int[]] - for (i, idx) in enumerate(idxs) - if idx > length(split_idxs) - push!(split_idxs, Int[]) - end - push!(split_idxs[idx], i) - end - tighten_types = x -> identity.(x) - split_ps = tighten_types.(Base.Fix1(getindex, ps).(split_idxs)) - - if ps isa StaticArray - parrs = map(x -> SArray{Tuple{size(x)...}}(x), split_ps) - split_ps = SArray{Tuple{size(parrs)...}}(parrs) - end - if length(split_ps) == 1 #Tuple not needed, only 1 type - return split_ps[1], split_idxs - else - return (split_ps...,), split_idxs - end - end -end - """ $(TYPEDSIGNATURES) diff --git a/lib/ModelingToolkitBase/src/precompile.jl b/lib/ModelingToolkitBase/src/precompile.jl new file mode 100644 index 0000000000..26cc55a574 --- /dev/null +++ b/lib/ModelingToolkitBase/src/precompile.jl @@ -0,0 +1,105 @@ +PrecompileTools.@compile_workload begin + fold1 = Val{false}() + using SymbolicUtils + using SymbolicUtils: shape + using Symbolics + @syms x y f(t) q[1:5] + SymbolicUtils.Sym{SymReal}(:a; type = Real, shape = SymbolicUtils.ShapeVecT()) + x + y + x * y + x / y + x ^ y + x ^ 5 + 6 ^ x + x - y + -y + 2y + z = 2 + dict = SymbolicUtils.ACDict{VartypeT}() + dict[x] = 1 + dict[y] = 1 + type::typeof(DataType) = rand() < 0.5 ? Real : Float64 + nt = (; type, shape, unsafe = true) + Base.pairs(nt) + BSImpl.AddMul{VartypeT}(1, dict, SymbolicUtils.AddMulVariant.MUL; type, shape = SymbolicUtils.ShapeVecT(), unsafe = true) + *(y, z) + *(z, y) + SymbolicUtils.symtype(y) + f(x) + (5x / 5) + expand((x + y) ^ 2) + simplify(x ^ (1//2) + (sin(x) ^ 2 + cos(x) ^ 2) + 2(x + y) - x - y) + ex = x + 2y + sin(x) + rules1 = Dict(x => y) + rules2 = Dict(x => 1) + Dx = Differential(x) + Differential(y)(ex) + uex = unwrap(ex) + Symbolics.executediff(Dx, uex) + # Running `fold = Val(true)` invalidates the precompiled statements + # for `fold = Val(false)` and itself doesn't precompile anyway. + # substitute(ex, rules1) + substitute(ex, rules1; fold = fold1) + substitute(ex, rules2; fold = fold1) + @variables foo + f(foo) + @variables x y f(::Real) q[1:5] + x + y + x * y + x / y + x ^ y + x ^ 5 + # 6 ^ x + x - y + -y + 2y + symtype(y) + z = 2 + *(y, z) + *(z, y) + f(x) + (5x / 5) + [x, y] + [x, f, f] + promote_type(Int, Num) + promote_type(Real, Num) + promote_type(Float64, Num) + # expand((x + y) ^ 2) + # simplify(x ^ (1//2) + (sin(x) ^ 2 + cos(x) ^ 2) + 2(x + y) - x - y) + ex = x + 2y + sin(x) + rules1 = Dict(x => y) + # rules2 = Dict(x => 1) + # Running `fold = Val(true)` invalidates the precompiled statements + # for `fold = Val(false)` and itself doesn't precompile anyway. + # substitute(ex, rules1) + substitute(ex, rules1; fold = fold1) + Symbolics.linear_expansion(ex, y) + # substitute(ex, rules2; fold = fold1) + # substitute(ex, rules2) + # substitute(ex, rules1; fold = fold2) + # substitute(ex, rules2; fold = fold2) + q[1] + q'q + using ModelingToolkitBase + @variables x(ModelingToolkitBase.t_nounits) y(ModelingToolkitBase.t_nounits) + isequal(ModelingToolkitBase.D_nounits.x, ModelingToolkitBase.t_nounits) + ics = Dict{SymbolicT, SymbolicT}() + ics[x] = 2.3 + sys = System([ModelingToolkitBase.D_nounits(x) ~ x * y, y ~ 2x], ModelingToolkitBase.t_nounits, [x, y], Num[]; initial_conditions = ics, guesses = ics, name = :sys) + complete(sys) + @static if @isdefined(ModelingToolkit) + TearingState(sys) + end + mtkcompile(sys) + @syms p[1:2] + ndims(p) + size(p) + axes(p) + length(p) + v = [p] + isempty(v) + # mtkcompile(sys) +end + +precompile(Tuple{typeof(SymbolicUtils.isequal_somescalar), Float64, Float64}) +precompile(Tuple{typeof(Base.:(var"==")), ModelingToolkitBase.Initial, ModelingToolkitBase.Initial}) diff --git a/src/problems/bvproblem.jl b/lib/ModelingToolkitBase/src/problems/bvproblem.jl similarity index 100% rename from src/problems/bvproblem.jl rename to lib/ModelingToolkitBase/src/problems/bvproblem.jl diff --git a/src/problems/compatibility.jl b/lib/ModelingToolkitBase/src/problems/compatibility.jl similarity index 98% rename from src/problems/compatibility.jl rename to lib/ModelingToolkitBase/src/problems/compatibility.jl index 9d5abf926e..8dc009b8e7 100644 --- a/src/problems/compatibility.jl +++ b/lib/ModelingToolkitBase/src/problems/compatibility.jl @@ -50,7 +50,7 @@ function check_not_dde(sys::System) end function check_no_cost(sys::System, T) - cost = ModelingToolkit.cost(sys) + cost = ModelingToolkitBase.cost(sys) if !_iszero(cost) throw(SystemCompatibilityError(""" `$T` will not optimize solutions of systems that have associated cost \ @@ -60,7 +60,7 @@ function check_no_cost(sys::System, T) end function check_has_cost(sys::System, T) - cost = ModelingToolkit.cost(sys) + cost = ModelingToolkitBase.cost(sys) if _iszero(cost) throw(SystemCompatibilityError(""" A system without cost cannot be used to construct a `$T`. diff --git a/src/problems/daeproblem.jl b/lib/ModelingToolkitBase/src/problems/daeproblem.jl similarity index 100% rename from src/problems/daeproblem.jl rename to lib/ModelingToolkitBase/src/problems/daeproblem.jl diff --git a/src/problems/ddeproblem.jl b/lib/ModelingToolkitBase/src/problems/ddeproblem.jl similarity index 100% rename from src/problems/ddeproblem.jl rename to lib/ModelingToolkitBase/src/problems/ddeproblem.jl diff --git a/src/problems/discreteproblem.jl b/lib/ModelingToolkitBase/src/problems/discreteproblem.jl similarity index 100% rename from src/problems/discreteproblem.jl rename to lib/ModelingToolkitBase/src/problems/discreteproblem.jl diff --git a/lib/ModelingToolkitBase/src/problems/docs.jl b/lib/ModelingToolkitBase/src/problems/docs.jl new file mode 100644 index 0000000000..7398139834 --- /dev/null +++ b/lib/ModelingToolkitBase/src/problems/docs.jl @@ -0,0 +1,443 @@ +const U0_P_DOCS = """ +The order of unknowns is determined by `unknowns(sys)`. If the system is split +[`is_split`](@ref) create an [`MTKParameters`](@ref) object. Otherwise, a parameter vector. +Initial values provided in terms of other variables will be symbolically evaluated. +The type of `op` will be used to determine the type of the containers. For example, if +given as an `SArray` of key-value pairs, `u0` will be an appropriately sized `SVector` +and the parameter object will be an `MTKParameters` object with `SArray`s inside. +""" + +const EVAL_EXPR_MOD_KWARGS = """ +- `eval_expression`: Whether to compile any functions via `eval` or + `RuntimeGeneratedFunctions`. +- `eval_module`: If `eval_expression == true`, the module to `eval` into. Otherwise, the + module in which to generate the `RuntimeGeneratedFunction`. +""" + +const INITIALIZEPROB_KWARGS = """ +- `guesses`: The guesses for variables in the system, used as initial values for the + initialization problem. +- `warn_initialize_determined`: Warn if the initialization system is under/over-determined. +- `initialization_eqs`: Extra equations to use in the initialization problem. +- `fully_determined`: Override whether the initialization system is fully determined. +- `use_scc`: Whether to use `SCCNonlinearProblem` for initialization if the system is fully + determined. +""" + +const PROBLEM_KWARGS = """ +$EVAL_EXPR_MOD_KWARGS +$INITIALIZEPROB_KWARGS +- `check_initialization_units`: Enable or disable unit checks when constructing the + initialization problem. +- `tofloat`: Passed to [`varmap_to_vars`](@ref) when building the parameter vector of + a non-split system. +- `u0_eltype`: The `eltype` of the `u0` vector. If `nothing`, finds the promoted floating point + type from `op`. +- `u0_constructor`: A function to apply to the `u0` value returned from + [`varmap_to_vars`](@ref). + to construct the final `u0` value. +- `p_constructor`: A function to apply to each array buffer created when constructing the + parameter object. +- `warn_cyclic_dependency`: Whether to emit a warning listing out cycles in initial + conditions provided for unknowns and parameters. +- `circular_dependency_max_cycle_length`: Maximum length of cycle to check for. Only + applicable if `warn_cyclic_dependency == true`. +- `circular_dependency_max_cycles`: Maximum number of cycles to check for. Only applicable + if `warn_cyclic_dependency == true`. +- `substitution_limit`: The number times to substitute initial conditions into each other + to attempt to arrive at a numeric value. +- `missing_guess_value`: An instance of [`MissingGuessValue`](@ref) which indicates what + happens when the initialization problem is missing guess values for variables. +""" + +const TIME_DEPENDENT_PROBLEM_KWARGS = """ +- `callback`: An extra callback or `CallbackSet` to add to the problem, in addition to the + ones defined symbolically in the system. +""" + +const PROBLEM_INTERNALS_HEADER = """ +# Extended docs + +The following API is internal and may change or be removed without notice. Its usage is +highly discouraged. +""" + +const INTERNAL_INITIALIZEPROB_KWARGS = """ +- `time_dependent_init`: Whether to build a time-dependent initialization for the problem. A + time-dependent initialization solves for a consistent `u0`, whereas a time-independent one + only runs parameter initialization. +- `algebraic_only`: Whether to build the initialization problem using only algebraic equations. +- `allow_incomplete`: Whether to allow incomplete initialization problems. +""" + +const PROBLEM_INTERNAL_KWARGS = """ +- `build_initializeprob`: If `false`, avoids building the initialization problem. +- `check_length`: Whether to check the number of equations along with number of unknowns and + length of `u0` vector for consistency. If `false`, do not check with equations. This is + forwarded to `check_eqs_u0`. +$INTERNAL_INITIALIZEPROB_KWARGS +""" + +function problem_ctors(prob, istd) + if istd + """ + SciMLBase.$prob(sys::System, op, tspan::NTuple{2}; kwargs...) + SciMLBase.$prob{iip}(sys::System, op, tspan::NTuple{2}; kwargs...) + SciMLBase.$prob{iip, specialize}(sys::System, op, tspan::NTuple{2}; kwargs...) + """ + else + """ + SciMLBase.$prob(sys::System, op; kwargs...) + SciMLBase.$prob{iip}(sys::System, op; kwargs...) + SciMLBase.$prob{iip, specialize}(sys::System, op; kwargs...) + """ + end +end + +function prob_fun_common_kwargs(T, istd) + return """ + - `check_compatibility`: Whether to check if the given system `sys` contains all the + information necessary to create a `$T` and no more. If disabled, assumes that `sys` + at least contains the necessary information. + - `expression`: `Val{true}` to return an `Expr` that constructs the corresponding + problem instead of the problem itself. `Val{false}` otherwise. + $(istd ? " Constructing the expression does not support callbacks" : "") + """ +end + +function problem_docstring(prob, func, istd; init = true, extra_body = "", + extra_kwargs = "", extra_kwargs_desc = "") + if func isa DataType + func = "`$func`" + end + return """ + $(problem_ctors(prob, istd)) + + Build a `$prob` given a system `sys` and operating point `op` + $(istd ? " and timespan `tspan`" : ""). `iip` is a boolean indicating whether the + problem should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` subtype + indicating the level of specialization of the $func. The operating point should be an + iterable collection of key-value pairs mapping variables/parameters in the system to the + (initial) values they should take in `$prob`. Any values not provided will fallback to + the corresponding default (if present). + + $(init ? istd ? TIME_DEPENDENT_INIT : TIME_INDEPENDENT_INIT : "") + + $extra_body + + # Keyword arguments + + $PROBLEM_KWARGS + $(istd ? TIME_DEPENDENT_PROBLEM_KWARGS : "") + $(prob_fun_common_kwargs(prob, istd)) + $(extra_kwargs) + All other keyword arguments are forwarded to the $func constructor. + $(extra_kwargs_desc) + + $PROBLEM_INTERNALS_HEADER + + $PROBLEM_INTERNAL_KWARGS + """ +end + +const TIME_DEPENDENT_INIT = """ +ModelingToolkitBase will build an initialization problem where all initial values for +unknowns or observables of `sys` (either explicitly provided or in defaults) will +be constraints. To remove an initial condition in the defaults (without providing +a replacement) give the corresponding variable a value of `nothing` in the operating +point. The initialization problem will also run parameter initialization. See the +[Initialization](@ref initialization) documentation for more information. +""" + +const TIME_INDEPENDENT_INIT = """ +ModelingToolkitBase will build an initialization problem that will run parameter +initialization. Since it does not solve for initial values of unknowns, observed +equations will not be initialization constraints. If an initialization equation +of the system must involve the initial value of an unknown `x`, it must be used as +`Initial(x)` in the equation. For example, an equation to be used to solve for parameter +`p` in terms of unknowns `x` and `y` must be provided as `Initial(x) + Initial(y) ~ p` +instead of `x + y ~ p`. See the [Initialization](@ref initialization) documentation +for more information. +""" + +const BV_EXTRA_BODY = """ +Boundary value conditions are supplied to Systems in the form of a list of constraints. +These equations should specify values that state variables should take at specific points, +as in `x(0.5) ~ 1`). More general constraints that should hold over the entire solution, +such as `x(t)^2 + y(t)^2`, should be specified as one of the equations used to build the +`System`. + +If a `System` without `constraints` is specified, it will be treated as an initial value problem. + +```julia + @parameters g t_c = 0.5 + @variables x(..) y(t) λ(t) + eqs = [D(D(x(t))) ~ λ * x(t) + D(D(y)) ~ λ * y - g + x(t)^2 + y^2 ~ 1] + cstr = [x(0.5) ~ 1] + @mtkcompile pend = System(eqs, t; constraints = cstrs) + + tspan = (0.0, 1.5) + u0map = [x(t) => 0.6, y => 0.8] + parammap = [g => 1] + guesses = [λ => 1] + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) +``` + +If the `System` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting +`BVProblem` must be solved using BVDAE solvers, such as Ascher. +""" + +for (mod, prob, func, istd, kws) in [ + (SciMLBase, :ODEProblem, ODEFunction, true, (;)), + (SciMLBase, :SteadyStateProblem, ODEFunction, false, (;)), + (SciMLBase, :BVProblem, ODEFunction, true, + (; init = false, extra_body = BV_EXTRA_BODY)), + (SciMLBase, :DAEProblem, DAEFunction, true, (;)), + (SciMLBase, :DDEProblem, DDEFunction, true, (;)), + (SciMLBase, :SDEProblem, SDEFunction, true, (;)), + (SciMLBase, :SDDEProblem, SDDEFunction, true, (;)), + (JumpProcesses, :JumpProblem, "inner SciMLFunction", true, (; init = false)), + (SciMLBase, :DiscreteProblem, DiscreteFunction, true, (;)), + (SciMLBase, :ImplicitDiscreteProblem, ImplicitDiscreteFunction, true, (;)), + (SciMLBase, :NonlinearProblem, NonlinearFunction, false, (;)), + (SciMLBase, :NonlinearLeastSquaresProblem, NonlinearFunction, false, (;)), + (SciMLBase, :OptimizationProblem, OptimizationFunction, false, (; init = false)), +] + kwexpr = Expr(:parameters) + for (k, v) in pairs(kws) + push!(kwexpr.args, Expr(:kw, k, v)) + end + @eval @doc problem_docstring($kwexpr, $mod.$prob, $func, $istd) $mod.$prob +end + +function function_docstring( + func, istd, optionals; extra_body = "", extra_kwargs = "", extra_kwargs_desc = "") + return """ + $func(sys::System; kwargs...) + $func{iip}(sys::System; kwargs...) + $func{iip, specialize}(sys::System; kwargs...) + + Create a `$func` from the given `sys`. `iip` is a boolean indicating whether the + function should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` + subtype indicating the level of specialization of the $func. + + $(extra_body) + + Beyond the arguments listed below, this constructor accepts all keyword arguments + supported by the DifferentialEquations.jl `solve` function. For a complete list + and detailed descriptions, see the [DifferentialEquations.jl solve documentation](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/). + + # Keyword arguments + + - `u0`: The `u0` vector for the corresponding problem, if available. Can be obtained + using [`ModelingToolkitBase.get_u0`](@ref). + - `p`: The parameter object for the corresponding problem, if available. Can be obtained + using [`ModelingToolkitBase.get_p`](@ref). + $(istd ? TIME_DEPENDENT_FUNCTION_KWARGS : "") + $EVAL_EXPR_MOD_KWARGS + - `checkbounds`: Whether to enable bounds checking in the generated code. + - `simplify`: Whether to `simplify` any symbolically computed jacobians/hessians/etc. + - `cse`: Whether to enable Common Subexpression Elimination (CSE) on the generated code. + This typically improves performance of the generated code but reduces readability. + - `sparse`: Whether to generate jacobian/hessian/etc. functions that return/operate on + sparse matrices. Also controls whether the mass matrix is sparse, wherever applicable. + $(prob_fun_common_kwargs(func, istd)) + $(process_optional_function_kwargs(optionals)) + $(extra_kwargs) + - `kwargs...`: Additional keyword arguments passed to the solver + + All other keyword arguments are forwarded to the `$func` struct constructor. + $(extra_kwargs_desc) + """ +end + +const TIME_DEPENDENT_FUNCTION_KWARGS = """ +- `t`: The initial time for the corresponding problem, if available. +""" + +const JAC_KWARGS = """ +- `jac`: Whether to symbolically compute and generate code for the jacobian function. +""" + +const TGRAD_KWARGS = """ +- `tgrad`: Whether to symbolically compute and generate code for the `tgrad` function. +""" + +const SPARSITY_KWARGS = """ +- `sparsity`: Whether to provide symbolically compute and provide sparsity patterns for the + jacobian/hessian/etc. +""" + +const RESID_PROTOTYPE_KWARGS = """ +- `resid_prototype`: The prototype of the residual function `f` for a problem involving a + nonlinear solve where the residual and `u0` have different sizes. +""" + +const GRAD_KWARGS = """ +- `grad`: Whether the symbolically compute and generate code for the gradient of the cost + function with respect to unknowns. +""" + +const HESS_KWARGS = """ +- `hess`: Whether to symbolically compute and generate code for the hessian function. +""" + +const CONSH_KWARGS = """ +- `cons_h`: Whether to symbolically compute and generate code for the hessian function of + constraints. Since the constraint function is vector-valued, the hessian is a vector + of hessian matrices. +""" + +const CONSJ_KWARGS = """ +- `cons_j`: Whether to symbolically compute and generate code for the jacobian function of + constraints. +""" + +const CONSSPARSE_KWARGS = """ +- `cons_sparse`: Identical to the `sparse` keyword, but specifically for jacobian/hessian + functions of the constraints. +""" + +const INPUTFN_KWARGS = """ +- `inputs`: The variables in the input vector. The system must have been simplified using + `mtkcompile` with these variables passed as `inputs`. +- `disturbance_inputs`: The disturbance input variables. The system must have been + simplified using `mtkcompile` with these variables passed as `disturbance_inputs`. +""" + +const CONTROLJAC_KWARGS = """ +- `controljac`: Whether to symbolically compute and generate code for the jacobian of + the ODE with respect to the inputs. +""" + +const OPTIONAL_FN_KWARGS_DICT = Dict( + :jac => JAC_KWARGS, + :tgrad => TGRAD_KWARGS, + :sparsity => SPARSITY_KWARGS, + :resid_prototype => RESID_PROTOTYPE_KWARGS, + :grad => GRAD_KWARGS, + :hess => HESS_KWARGS, + :cons_h => CONSH_KWARGS, + :cons_j => CONSJ_KWARGS, + :cons_sparse => CONSSPARSE_KWARGS, + :inputfn => INPUTFN_KWARGS, + :controljac => CONTROLJAC_KWARGS +) + +const SPARSITY_OPTIONALS = Set([:jac, :hess, :cons_h, :cons_j, :controljac]) + +const CONS_SPARSITY_OPTIONALS = Set([:cons_h, :cons_j]) + +function process_optional_function_kwargs(choices::Vector{Symbol}) + if !isdisjoint(choices, SPARSITY_OPTIONALS) + push!(choices, :sparsity) + end + if !isdisjoint(choices, CONS_SPARSITY_OPTIONALS) + push!(choices, :cons_sparse) + end + join(map(Base.Fix1(getindex, OPTIONAL_FN_KWARGS_DICT), choices), "\n") +end + +for (mod, func, istd, optionals, kws) in [ + (SciMLBase, :ODEFunction, true, [:jac, :tgrad], (;)), + (SciMLBase, :ODEInputFunction, true, [:inputfn, :jac, :tgrad, :controljac], (;)), + (SciMLBase, :DAEFunction, true, [:jac, :tgrad], (;)), + (SciMLBase, :DDEFunction, true, Symbol[], (;)), + (SciMLBase, :SDEFunction, true, [:jac, :tgrad], (;)), + (SciMLBase, :SDDEFunction, true, Symbol[], (;)), + (SciMLBase, :DiscreteFunction, true, Symbol[], (;)), + (SciMLBase, :ImplicitDiscreteFunction, true, Symbol[], (;)), + (SciMLBase, :NonlinearFunction, false, [:resid_prototype, :jac], (;)), + (SciMLBase, :IntervalNonlinearFunction, false, Symbol[], (;)), + (SciMLBase, :OptimizationFunction, false, [:jac, :grad, :hess, :cons_h, :cons_j], (;)), +] + kwexpr = Expr(:parameters) + for (k, v) in pairs(kws) + push!(kwexpr.args, Expr(:kw, k, v)) + end + @eval @doc function_docstring($kwexpr, $mod.$func, $istd, $optionals) $mod.$func +end + +@doc """ + SciMLBase.HomotopyNonlinearFunction(sys::System; kwargs...) + SciMLBase.HomotopyNonlinearFunction{iip}(sys::System; kwargs...) + SciMLBase.HomotopyNonlinearFunction{iip, specialize}(sys::System; kwargs...) + +Create a `HomotopyNonlinearFunction` from the given `sys`. `iip` is a boolean indicating +whether the function should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` +subtype indicating the level of specialization of the $func. + +# Keyword arguments + +- `u0`: The `u0` vector for the corresponding problem, if available. Can be obtained + using [`ModelingToolkitBase.get_u0`](@ref). +- `p`: The parameter object for the corresponding problem, if available. Can be obtained + using [`ModelingToolkitBase.get_p`](@ref). +$EVAL_EXPR_MOD_KWARGS +- `checkbounds`: Whether to enable bounds checking in the generated code. +- `simplify`: Whether to `simplify` any symbolically computed jacobians/hessians/etc. +- `cse`: Whether to enable Common Subexpression Elimination (CSE) on the generated code. + This typically improves performance of the generated code but reduces readability. +- `fraction_cancel_fn`: The function to use to simplify fractions in the polynomial + expression. A more powerful function can increase processing time but be able to + eliminate more rational functions, thus improving solve time. Should be a function that + takes a symbolic expression containing zero or more fraction expressions and returns the + simplified expression. While this defaults to `SymbolicUtils.simplify_fractions`, a viable + alternative is `SymbolicUtils.quick_cancel` + +All keyword arguments are forwarded to the wrapped `NonlinearFunction` constructor. +""" SciMLBase.HomotopyNonlinearFunction + +@doc """ + SciMLBase.IntervalNonlinearProblem(sys::System, uspan::NTuple{2}, parammap = SciMLBase.NullParameters(); kwargs...) + +Create an `IntervalNonlinearProblem` from the given `sys`. This is only valid for a system +of nonlinear equations with a single equation and unknown. `uspan` is the interval in which +the root is to be found, and `parammap` is an iterable collection of key-value pairs +providing values for the parameters in the system. + +$TIME_INDEPENDENT_INIT + +# Keyword arguments + +$PROBLEM_KWARGS +$(prob_fun_common_kwargs(IntervalNonlinearProblem, false)) + +All other keyword arguments are forwarded to the `IntervalNonlinearFunction` constructor. + +$PROBLEM_INTERNALS_HEADER + +$PROBLEM_INTERNAL_KWARGS +""" SciMLBase.IntervalNonlinearProblem + +@doc """ + SciMLBase.LinearProblem(sys::System, op; kwargs...) + SciMLBase.LinearProblem{iip}(sys::System, op; kwargs...) + +Build a `LinearProblem` given a system `sys` and operating point `op`. `iip` is a boolean +indicating whether the problem should be in-place. The operating point should be an +iterable collection of key-value pairs mapping variables/parameters in the system to the +(initial) values they should take in `LinearProblem`. Any values not provided will +fallback to the corresponding default (if present). + +Note that since `u0` is optional for `LinearProblem`, values of unknowns do not need to be +specified in `op` to create a `LinearProblem`. In such a case, `prob.u0` will be `nothing` +and attempting to symbolically index the problem with an unknown, observable, or expression +depending on unknowns/observables will error. + +Updating the parameters automatically updates the `A` and `b` arrays. + +# Keyword arguments + +$PROBLEM_KWARGS +$(prob_fun_common_kwargs(LinearProblem, false)) + +All other keyword arguments are forwarded to the $func constructor. + +$PROBLEM_INTERNALS_HEADER + +$PROBLEM_INTERNAL_KWARGS +""" SciMLBase.LinearProblem diff --git a/src/problems/implicitdiscreteproblem.jl b/lib/ModelingToolkitBase/src/problems/implicitdiscreteproblem.jl similarity index 100% rename from src/problems/implicitdiscreteproblem.jl rename to lib/ModelingToolkitBase/src/problems/implicitdiscreteproblem.jl diff --git a/src/problems/initializationproblem.jl b/lib/ModelingToolkitBase/src/problems/initializationproblem.jl similarity index 62% rename from src/problems/initializationproblem.jl rename to lib/ModelingToolkitBase/src/problems/initializationproblem.jl index 6960811bbd..a11b281112 100644 --- a/src/problems/initializationproblem.jl +++ b/lib/ModelingToolkitBase/src/problems/initializationproblem.jl @@ -20,13 +20,13 @@ All other keyword arguments are forwarded to the wrapped nonlinear problem const @fallback_iip_specialize function InitializationProblem{iip, specialize}( sys::AbstractSystem, t, op = Dict(); + fast_path = false, guesses = [], check_length = true, warn_initialize_determined = true, initialization_eqs = [], fully_determined = nothing, check_units = true, - use_scc = true, allow_incomplete = false, algebraic_only = false, time_dependent_init = is_time_dependent(sys), @@ -34,24 +34,25 @@ All other keyword arguments are forwarded to the wrapped nonlinear problem const if !iscomplete(sys) error("A completed system is required. Call `complete` or `mtkcompile` on the system before creating an `ODEProblem`") end + if !fast_path + op = build_operating_point(sys, op) + end has_u0_ics = false - op = copy(anydict(op)) for k in keys(op) - has_u0_ics |= is_variable(sys, k) || isdifferential(k) || - symbolic_type(k) == ArraySymbolic() && - is_sized_array_symbolic(k) && is_variable(sys, unwrap(first(wrap(k)))) + has_u0_ics |= is_variable(sys, k) || isdifferential(k) end if !has_u0_ics && get_initializesystem(sys) !== nothing isys = get_initializesystem(sys; initialization_eqs, check_units) simplify_system = false elseif !has_u0_ics && get_initializesystem(sys) === nothing isys = generate_initializesystem( - sys; initialization_eqs, check_units, op, guesses, algebraic_only) + sys; initialization_eqs, check_units, op, guesses, algebraic_only, + fast_path) simplify_system = true else isys = generate_initializesystem( sys; op, initialization_eqs, check_units, time_dependent_init, - guesses, algebraic_only) + guesses, algebraic_only, fast_path) simplify_system = true end @@ -62,12 +63,15 @@ All other keyword arguments are forwarded to the wrapped nonlinear problem const idx === nothing || deleteat!(get_ps(isys), idx) end + if !is_split(sys) + @set! isys.ps = mapreduce(collect, vcat, get_ps(isys)) + end if simplify_system isys = mtkcompile(isys; fully_determined, split = is_split(sys)) end ts = get_tearing_state(isys) - unassigned_vars = StructuralTransformations.singular_check(ts) + unassigned_vars = singular_check(ts) if warn_initialize_determined && !isempty(unassigned_vars) errmsg = """ The initialization system is structurally singular. Guess values may \ @@ -79,65 +83,83 @@ All other keyword arguments are forwarded to the wrapped nonlinear problem const @warn errmsg end - uninit = setdiff(unknowns(sys), unknowns(isys), observables(isys)) + uninit = as_atomic_array_set(unknowns(sys)) + setdiff!(uninit, as_atomic_array_set(unknowns(isys))) + setdiff!(uninit, as_atomic_array_set(observables(isys))) - # TODO: throw on uninitialized arrays - filter!(x -> !(x isa Symbolics.Arr), uninit) if time_dependent_init && !isempty(uninit) allow_incomplete || throw(IncompleteInitializationError(uninit, sys)) # for incomplete initialization, we will add the missing variables as parameters. # they will be updated by `update_initializeprob!` and `initializeprobmap` will # use them to construct the new `u0`. - newparams = map(toparam, uninit) - append!(get_ps(isys), newparams) + new_ps = copy(get_ps(isys)) + append!(new_ps, uninit) + @set! isys.ps = new_ps isys = complete(isys) end + if t !== nothing + op = copy(op) + op[get_iv(sys)] = t + end + filter!(!Base.Fix2(===, COMMON_MISSING) ∘ last, op) + TProb = get_initialization_problem_type(sys, isys; warn_initialize_determined, + kwargs...) + TProb{iip}(isys, op; kwargs..., build_initializeprob = false, is_initializeprob = true) +end + +function overdetermined_initialization_message(neqs::Integer, nunknown::Integer, extra::AbstractString) + """ + Initialization system is overdetermined. $neqs equations for $nunknown unknowns. \ + Initialization will default to using least squares. $(extra) + + To suppress this warning, pass `warn_initialize_determined = false`. To turn this \ + warning into an error, pass `fully_determined = true`. + """ +end + +function underdetermined_initialization_message(neqs::Integer, nunknown::Integer, extra::AbstractString) + """ + Initialization system is underdetermined. $neqs equations for $nunknown unknowns. \ + Initialization will default to using least squares. $(extra) + + To suppress this warning, pass `warn_initialize_determined = false`. To turn this \ + warning into an error, pass `fully_determined = true`. + """ +end + +""" + $TYPEDSIGNATURES + +Get the type of the initialization problem (Nonlinear problem) to use, given the system +`sys`, initialization system `isys` and arbitrary keyword arguments. +""" +function get_initialization_problem_type(sys::AbstractSystem, isys::AbstractSystem; + warn_initialize_determined = true, kwargs...) neqs = length(equations(isys)) nunknown = length(unknowns(isys)) - if use_scc - scc_message = "`SCCNonlinearProblem` can only be used for initialization of fully determined systems and hence will not be used here. " - else - scc_message = "" - end - if warn_initialize_determined && neqs > nunknown - @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + @warn overdetermined_initialization_message(neqs, nunknown, "") end if warn_initialize_determined && neqs < nunknown - @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + @warn underdetermined_initialization_message(neqs, nunknown, "") end - if t !== nothing - op[get_iv(sys)] = t - end - filter!(kvp -> kvp[2] !== missing, op) - - if isempty(guesses) - guesses = Dict() - end - - filter_missing_values!(op) - op = merge(ModelingToolkit.guesses(sys), todict(guesses), op) - - TProb = if neqs == nunknown && isempty(unassigned_vars) - if use_scc && neqs > 0 - if is_split(isys) - SCCNonlinearProblem - else - @warn "`SCCNonlinearProblem` can only be used with `split = true` systems. Simplify your `ODESystem` with `split = true` or pass `use_scc = false` to disable this warning" - NonlinearProblem - end - else - NonlinearProblem - end + if neqs == nunknown + NonlinearProblem else NonlinearLeastSquaresProblem end - TProb{iip}(isys, op; kwargs..., build_initializeprob = false, is_initializeprob = true) end +""" + $TYPEDSIGNATURES + +Return a list of possibly singular variables, given `get_tearing_state(sys)`. +""" +singular_check(::Nothing) = SymbolicT[] + const INCOMPLETE_INITIALIZATION_MESSAGE = """ Initialization incomplete. Not all of the state variables of the DAE system can be determined by the initialization. Missing @@ -151,5 +173,5 @@ end function Base.showerror(io::IO, e::IncompleteInitializationError) println(io, INCOMPLETE_INITIALIZATION_MESSAGE) - println(io, underscore_to_D(e.uninit, e.sys)) + println(io, underscore_to_D(collect(e.uninit), e.sys)) end diff --git a/src/problems/intervalnonlinearproblem.jl b/lib/ModelingToolkitBase/src/problems/intervalnonlinearproblem.jl similarity index 100% rename from src/problems/intervalnonlinearproblem.jl rename to lib/ModelingToolkitBase/src/problems/intervalnonlinearproblem.jl diff --git a/src/problems/jumpproblem.jl b/lib/ModelingToolkitBase/src/problems/jumpproblem.jl similarity index 90% rename from src/problems/jumpproblem.jl rename to lib/ModelingToolkitBase/src/problems/jumpproblem.jl index 32aa25182f..03b8fd2aa6 100644 --- a/src/problems/jumpproblem.jl +++ b/lib/ModelingToolkitBase/src/problems/jumpproblem.jl @@ -172,7 +172,7 @@ end ##### MTK dispatches for Symbolic jumps ##### eqtype_supports_collect_vars(j::MassActionJump) = true -function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, +function collect_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, j::MassActionJump, iv::Union{SymbolicT, Nothing}; depth = 0, op = Differential) collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) for field in (j.reactant_stoch, j.net_stoch) @@ -184,8 +184,8 @@ function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, end eqtype_supports_collect_vars(j::Union{ConstantRateJump, VariableRateJump}) = true -function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump, VariableRateJump}, - iv; depth = 0, op = Differential) +function collect_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, j::Union{ConstantRateJump, VariableRateJump}, + iv::Union{SymbolicT, Nothing}; depth = 0, op = Differential) collect_vars!(unknowns, parameters, j.rate, iv; depth, op) for eq in j.affect! (eq isa Equation) && collect_vars!(unknowns, parameters, eq, iv; depth, op) @@ -194,17 +194,19 @@ function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump, Variable end ### Functions to determine which unknowns a jump depends on -function get_variables!(dep, jump::Union{ConstantRateJump, VariableRateJump}, variables) - jr = value(jump.rate) - (jr isa Symbolic) && get_variables!(dep, jr, variables) +function SU.search_variables!(dep, jump::Union{ConstantRateJump, VariableRateJump}; kw...) + jr = unwrap(jump.rate) + (jr isa SymbolicT) && SU.search_variables!(dep, jr; kw...) dep end -function get_variables!(dep, jump::MassActionJump, variables) - sr = value(jump.scaled_rates) - (sr isa Symbolic) && get_variables!(dep, sr, variables) +function SU.search_variables!(dep, jump::MassActionJump; is_atomic = SU.default_is_atomic, kw...) + sr = unwrap(jump.scaled_rates) + (sr isa SymbolicT) && SU.search_variables!(dep, sr; is_atomic, kw...) for varasop in jump.reactant_stoch - any(isequal(varasop[1]), variables) && push!(dep, varasop[1]) + var = unwrap(varasop[1]) + var isa SymbolicT || continue + is_atomic(var) && push!(dep, var) end dep end diff --git a/src/problems/linearproblem.jl b/lib/ModelingToolkitBase/src/problems/linearproblem.jl similarity index 100% rename from src/problems/linearproblem.jl rename to lib/ModelingToolkitBase/src/problems/linearproblem.jl diff --git a/src/problems/nonlinearproblem.jl b/lib/ModelingToolkitBase/src/problems/nonlinearproblem.jl similarity index 100% rename from src/problems/nonlinearproblem.jl rename to lib/ModelingToolkitBase/src/problems/nonlinearproblem.jl diff --git a/lib/ModelingToolkitBase/src/problems/odeproblem.jl b/lib/ModelingToolkitBase/src/problems/odeproblem.jl new file mode 100644 index 0000000000..822c30ce49 --- /dev/null +++ b/lib/ModelingToolkitBase/src/problems/odeproblem.jl @@ -0,0 +1,122 @@ +@fallback_iip_specialize function SciMLBase.ODEFunction{iip, spec}( + sys::System; u0 = nothing, p = nothing, tgrad = false, jac = false, + t = nothing, eval_expression = false, eval_module = @__MODULE__, sparse = false, + steady_state = false, checkbounds = false, sparsity = false, analytic = nothing, + simplify = false, cse = true, initialization_data = nothing, expression = Val{false}, + check_compatibility = true, nlstep = false, nlstep_compile = true, nlstep_scc = false, + kwargs...) where {iip, spec} + check_complete(sys, ODEFunction) + check_compatibility && check_compatible_system(ODEFunction, sys) + + f = generate_rhs(sys; expression, wrap_gfw = Val{true}, + eval_expression, eval_module, checkbounds = checkbounds, cse, + kwargs...) + + if spec === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") + end + if expression == Val{true} + f = :($(SciMLBase.wrapfun_iip)($f, ($u0, $u0, $p, $t))) + else + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + end + + if tgrad + _tgrad = generate_tgrad( + sys; expression, wrap_gfw = Val{true}, + simplify, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _tgrad = nothing + end + + if jac + _jac = generate_jacobian( + sys; expression, wrap_gfw = Val{true}, + simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) + else + _jac = nothing + end + + M = calculate_massmatrix(sys) + _M = concrete_massmatrix(M; sparse, u0) + + if nlstep + ode_nlstep = generate_ODENLStepData(sys, u0, p, M, nlstep_compile, nlstep_scc) + else + ode_nlstep = nothing + end + + observedfun = ObservedFunctionCache( + sys; expression, steady_state, eval_expression, eval_module, checkbounds, cse) + + _W_sparsity = W_sparsity(sys) + W_prototype = calculate_W_prototype(_W_sparsity; u0, sparse) + + args = (; f) + kwargs = (; + sys = sys, + jac = _jac, + tgrad = _tgrad, + mass_matrix = _M, + jac_prototype = W_prototype, + observed = observedfun, + sparsity = sparsity ? _W_sparsity : nothing, + analytic = analytic, + initialization_data, + nlstep_data = ode_nlstep) + + maybe_codegen_scimlfn(expression, ODEFunction{iip, spec}, args; kwargs...) +end + +@fallback_iip_specialize function SciMLBase.ODEProblem{iip, spec}( + sys::System, op, tspan; + callback = nothing, check_length = true, eval_expression = false, + expression = Val{false}, eval_module = @__MODULE__, check_compatibility = true, + kwargs...) where {iip, spec} + check_complete(sys, ODEProblem) + check_compatibility && check_compatible_system(ODEProblem, sys) + + f, u0, + p = process_SciMLProblem(ODEFunction{iip, spec}, sys, op; + t = tspan !== nothing ? tspan[1] : tspan, check_length, eval_expression, + eval_module, expression, check_compatibility, kwargs...) + + kwargs = process_kwargs( + sys; expression, callback, eval_expression, eval_module, op, kwargs...) + + ptype = getmetadata(sys, ProblemTypeCtx, StandardODEProblem()) + args = (; f, u0, tspan, p, ptype) + maybe_codegen_scimlproblem(expression, ODEProblem{iip}, args; kwargs...) +end + +@fallback_iip_specialize function DiffEqBase.SteadyStateProblem{iip, spec}( + sys::System, op; check_length = true, check_compatibility = true, + expression = Val{false}, kwargs...) where {iip, spec} + check_complete(sys, SteadyStateProblem) + check_compatibility && check_compatible_system(SteadyStateProblem, sys) + + f, u0, + p = process_SciMLProblem(ODEFunction{iip}, sys, op; + steady_state = true, check_length, check_compatibility, expression, + time_dependent_init = false, kwargs...) + + kwargs = process_kwargs(sys; expression, kwargs...) + args = (; f, u0, p) + + maybe_codegen_scimlproblem(expression, SteadyStateProblem{iip}, args; kwargs...) +end + +function check_compatible_system( + T::Union{Type{ODEFunction}, Type{ODEProblem}, Type{DAEFunction}, + Type{DAEProblem}, Type{SteadyStateProblem}}, + sys::System) + check_time_dependent(sys, T) + check_not_dde(sys) + check_no_cost(sys, T) + check_no_constraints(sys, T) + check_no_jumps(sys, T) + check_no_noise(sys, T) + check_is_continuous(sys, T) +end diff --git a/src/problems/optimizationproblem.jl b/lib/ModelingToolkitBase/src/problems/optimizationproblem.jl similarity index 94% rename from src/problems/optimizationproblem.jl rename to lib/ModelingToolkitBase/src/problems/optimizationproblem.jl index 97882092e5..2fa7170949 100644 --- a/src/problems/optimizationproblem.jl +++ b/lib/ModelingToolkitBase/src/problems/optimizationproblem.jl @@ -116,14 +116,12 @@ function SciMLBase.OptimizationProblem{iip}( throw(ArgumentError("Expected both `ub` to be of the same length as the vector of optimization variables")) end - ps = parameters(sys) - defs = defaults(sys) - op = to_varmap(op, dvs) - lbmap = merge(op, AnyDict(dvs .=> lb)) - _, _ = build_operating_point!(sys, lbmap, Dict(), Dict(), defs, dvs, ps) + op = build_operating_point(sys, op) + lbmap = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(dvs .=> lb), COMMON_NOTHING) + left_merge!(lbmap, op) lb = varmap_to_vars(lbmap, dvs; tofloat = false) - ubmap = merge(op, AnyDict(dvs .=> ub)) - _, _ = build_operating_point!(sys, ubmap, Dict(), Dict(), defs, dvs, ps) + ubmap = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(dvs .=> ub), COMMON_NOTHING) + left_merge!(ubmap, op) ub = varmap_to_vars(ubmap, dvs; tofloat = false) if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) diff --git a/src/problems/sddeproblem.jl b/lib/ModelingToolkitBase/src/problems/sddeproblem.jl similarity index 100% rename from src/problems/sddeproblem.jl rename to lib/ModelingToolkitBase/src/problems/sddeproblem.jl diff --git a/src/problems/sdeproblem.jl b/lib/ModelingToolkitBase/src/problems/sdeproblem.jl similarity index 94% rename from src/problems/sdeproblem.jl rename to lib/ModelingToolkitBase/src/problems/sdeproblem.jl index f4bdd806f5..111b556848 100644 --- a/src/problems/sdeproblem.jl +++ b/lib/ModelingToolkitBase/src/problems/sdeproblem.jl @@ -119,7 +119,7 @@ function calculate_noise_and_rate_prototype(sys::System, u0; sparsenoise = false elseif size(noiseeqs, 2) == 1 # scalar noise noise_rate_prototype = nothing - noise = WienerProcess(0.0, 0.0, 0.0) + noise = __default_wiener_process() elseif sparsenoise I, J, V = findnz(SparseArrays.sparse(noiseeqs)) noise_rate_prototype = SparseArrays.sparse(I, J, zero(eltype(u0))) @@ -130,3 +130,12 @@ function calculate_noise_and_rate_prototype(sys::System, u0; sparsenoise = false end return noise, noise_rate_prototype end + +__default_wiener_process() = __default_wiener_process(nothing) + +function __default_wiener_process(_) + error(""" + Generating code for this problem requires loading DiffEqNoiseProcess.jl. Please run + `import DiffEqNoiseProcess` to proceed. + """) +end diff --git a/src/systems/abstractsystem.jl b/lib/ModelingToolkitBase/src/systems/abstractsystem.jl similarity index 75% rename from src/systems/abstractsystem.jl rename to lib/ModelingToolkitBase/src/systems/abstractsystem.jl index 7f10dba8c3..880108ac58 100644 --- a/src/systems/abstractsystem.jl +++ b/lib/ModelingToolkitBase/src/systems/abstractsystem.jl @@ -69,7 +69,7 @@ function wrap_assignments(isscalar, assignments; let_block = false) end end -const MTKPARAMETERS_ARG = Sym{Vector{Vector}}(:___mtkparameters___) +const MTKPARAMETERS_ARG = SSym(:___mtkparameters___; type = Vector{Vector{Any}}, shape = SymbolicUtils.Unknown(1)) """ $(TYPEDSIGNATURES) @@ -90,15 +90,15 @@ $(TYPEDSIGNATURES) Get the independent variable(s) of the system `sys`. -See also [`@independent_variables`](@ref) and [`ModelingToolkit.get_iv`](@ref). +See also [`@independent_variables`](@ref) and [`ModelingToolkitBase.get_iv`](@ref). """ function independent_variables(sys::AbstractSystem) if isdefined(sys, :iv) && getfield(sys, :iv) !== nothing - return [getfield(sys, :iv)] + return SymbolicT[getfield(sys, :iv)] elseif isdefined(sys, :ivs) - return getfield(sys, :ivs) + return unwrap.(getfield(sys, :ivs))::Vector{SymbolicT} else - return [] + return SymbolicT[] end end @@ -170,17 +170,20 @@ function SymbolicIndexingInterface.variable_symbols(sys::AbstractSystem) return unknowns(sys) end -function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym) - sym = unwrap(sym) +function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + is_parameter(sys, unwrap(sym)) +end + +function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::Int) + !is_split(sys) && sym in 1:length(parameter_symbols(sys)) +end + +function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::SymbolicT) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - return sym isa ParameterIndex || is_parameter(ic, sym) || - iscall(sym) && - operation(sym) === getindex && + return is_parameter(ic, sym) || + iscall(sym) && operation(sym) === getindex && is_parameter(ic, first(arguments(sym))) end - if unwrap(sym) isa Int - return unwrap(sym) in 1:length(parameter_symbols(sys)) - end return any(isequal(sym), parameter_symbols(sys)) || hasname(sym) && !(iscall(sym) && operation(sym) == getindex) && is_parameter(sys, getname(sym)) @@ -191,7 +194,7 @@ function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::Symbol return is_parameter(ic, sym) end - named_parameters = [getname(x) + named_parameters = Symbol[getname(x) for x in parameter_symbols(sys) if hasname(x) && !(iscall(x) && operation(x) == getindex)] return any(isequal(sym), named_parameters) || @@ -200,6 +203,9 @@ function SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym::Symbol Symbol.(nameof(sys), NAMESPACE_SEPARATOR_SYMBOL, named_parameters)) == 1 end +SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, ::ParameterIndex) = is_split(sys) +SymbolicIndexingInterface.is_parameter(sys::AbstractSystem, sym) = false + function SymbolicIndexingInterface.parameter_index(sys::AbstractSystem, sym) sym = unwrap(sym) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing @@ -285,20 +291,6 @@ function has_observed_with_lhs(sys::AbstractSystem, sym) end end -""" - $(TYPEDSIGNATURES) - -Check if the system `sys` contains a parameter dependency equation with LHS `sym`. -""" -function has_parameter_dependency_with_lhs(sys, sym) - has_parameter_dependencies(sys) || return false - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - return haskey(ic.dependent_pars_to_timeseries, unwrap(sym)) - else - return any(isequal(sym), [eq.lhs for eq in get_parameter_dependencies(sys)]) - end -end - function _all_ts_idxs!(ts_idxs, ::NotSymbolic, sys, sym) if is_variable(sys, sym) || is_independent_variable(sys, sym) push!(ts_idxs, ContinuousTimeseries()) @@ -312,7 +304,8 @@ for traitT in [ ArraySymbolic ] @eval function _all_ts_idxs!(ts_idxs, ::$traitT, sys, sym) - allsyms = vars(sym; op = Symbolics.Operator) + allsyms = Set{SymbolicT}() + SU.search_variables!(allsyms, sym; is_atomic = OperatorIsAtomic{Symbolics.Operator}()) for s in allsyms s = unwrap(s) if is_variable(sys, s) || is_independent_variable(sys, s) @@ -420,10 +413,11 @@ function SymbolicIndexingInterface.observed( end function SymbolicIndexingInterface.default_values(sys::AbstractSystem) - return merge( + return recursive_unwrap(merge( Dict(eq.lhs => eq.rhs for eq in observed(sys)), - defaults(sys) - ) + bindings(sys), + initial_conditions(sys), + )) end SymbolicIndexingInterface.is_markovian(sys::AbstractSystem) = !is_dde(sys) @@ -438,7 +432,7 @@ end function SymbolicIndexingInterface.all_symbols(sys::AbstractSystem) syms = all_variable_symbols(sys) - for other in (full_parameters(sys), independent_variable_symbols(sys)) + for other in (parameters(sys; initial_parameters = true), collect(bound_parameters(sys)), independent_variable_symbols(sys)) isempty(other) || (syms = vcat(syms, other)) end return syms @@ -486,12 +480,13 @@ The `Initial` operator. Used by initialization to store constant constraints on of a system. See the documentation section on initialization for more information. """ struct Initial <: Symbolics.Operator end -is_timevarying_operator(::Type{Initial}) = false Initial(x) = Initial()(x) -SymbolicUtils.promote_symtype(::Type{Initial}, T) = T +SymbolicUtils.promote_symtype(::Initial, ::Type{T}) where {T} = T +SymbolicUtils.promote_shape(::Initial, @nospecialize(x::SU.ShapeT)) = x SymbolicUtils.isbinop(::Initial) = false Base.nameof(::Initial) = :Initial Base.show(io::IO, x::Initial) = print(io, "Initial") +distribute_shift_into_operator(::Initial) = false function (f::Initial)(x) # wrap output if wrapped input @@ -507,16 +502,16 @@ function (f::Initial)(x) end # don't double wrap iscall(x) && operation(x) isa Initial && return x - result = if symbolic_type(x) == ArraySymbolic() - # create an array for `Initial(array)` - Symbolics.array_term(f, x) - elseif iscall(x) && operation(x) == getindex + sh = SU.shape(x) + result = if SU.is_array_shape(sh) + term(f, x; type = symtype(x), shape = sh) + elseif iscall(x) && operation(x) === getindex # instead of `Initial(x[1])` create `Initial(x)[1]` # which allows parameter indexing to handle this case automatically. arr = arguments(x)[1] - term(getindex, f(arr), arguments(x)[2:end]...) + f(arr)[arguments(x)[2:end]...] else - term(f, x) + term(f, x; type = symtype(x), shape = sh) end # the result should be a parameter result = toparam(result) @@ -526,15 +521,6 @@ function (f::Initial)(x) return result end -# This is required so `fast_substitute` works -function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) - val = Initial()(args...) - if symbolic_type(val) == NotSymbolic() - return val - end - return metadata(val, meta) -end - supports_initialization(sys::AbstractSystem) = true function add_initialization_parameters(sys::AbstractSystem; split = true) @@ -542,47 +528,57 @@ function add_initialization_parameters(sys::AbstractSystem; split = true) supports_initialization(sys) || return sys is_initializesystem(sys) && return sys - all_initialvars = Set{BasicSymbolic}() + all_initialvars = Set{SymbolicT}() # time-independent systems don't initialize unknowns # but may initialize parameters using guesses for unknowns eqs = equations(sys) - if !(eqs isa Vector{Equation}) - eqs = Equation[x for x in eqs if x isa Equation] - end obs, eqs = unhack_observed(observed(sys), eqs) - for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) - x = unwrap(x) - if iscall(x) && operation(x) == getindex && split - push!(all_initialvars, arguments(x)[1]) - else + for x in unknowns(sys) + if !split push!(all_initialvars, x) + continue + end + arr, _ = split_indexed_var(x) + push!(all_initialvars, arr) + end + for eq in obs + x = eq.lhs + if !split + push!(all_initialvars, x) + continue end + arr, _ = split_indexed_var(x) + push!(all_initialvars, arr) end # add derivatives of all variables for steady-state initial conditions if is_time_dependent(sys) && !is_discrete_system(sys) - D = Differential(get_iv(sys)) - union!(all_initialvars, [D(v) for v in all_initialvars if iscall(v)]) - end - for eq in get_parameter_dependencies(sys) - is_variable_floatingpoint(eq.lhs) || continue - push!(all_initialvars, eq.lhs) - end - all_initialvars = collect(all_initialvars) - initials = map(Initial(), all_initialvars) - @set! sys.ps = unique!([get_ps(sys); initials]) - defs = copy(get_defaults(sys)) - for ivar in initials - if symbolic_type(ivar) == ScalarSymbolic() - defs[ivar] = false - else - defs[ivar] = collect(ivar) - for scal_ivar in defs[ivar] - defs[scal_ivar] = false - end + D = Differential(get_iv(sys)::SymbolicT) + for v in collect(all_initialvars) + iscall(v) && push!(all_initialvars, D(v)) + end + end + + # Do this after the previous block to avoid adding derivatives of discretes + for x in get_all_discretes(sys) + if !split + push!(all_initialvars, x) + continue end + is_variable_floatingpoint(x) || continue + arr, _ = split_indexed_var(x) + push!(all_initialvars, arr) + end + + for (k, v) in bindings(sys) + v === COMMON_MISSING && push!(all_initialvars, k) + end + + initials = collect(all_initialvars) + for (i, v) in enumerate(initials) + initials[i] = Initial()(v) end - @set! sys.defaults = defs + @set! sys.ps = unique!([filter(!isinitial, get_ps(sys)); initials]) return sys end @@ -601,9 +597,9 @@ end Find [`GlobalScope`](@ref)d variables in `sys` and add them to the unknowns/parameters. """ function discover_globalscoped(sys::AbstractSystem) - newunknowns = OrderedSet() - newparams = OrderedSet() - iv = has_iv(sys) ? get_iv(sys) : nothing + newunknowns = OrderedSet{SymbolicT}() + newparams = OrderedSet{SymbolicT}() + iv::Union{SymbolicT, Nothing} = has_iv(sys) ? get_iv(sys) : nothing collect_scoped_vars!(newunknowns, newparams, sys, iv; depth = -1) setdiff!(newunknowns, observables(sys)) @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) @@ -626,79 +622,179 @@ This namespacing functionality can also be toggled independently of `complete` using [`toggle_namespacing`](@ref). """ function complete( - sys::AbstractSystem; split = true, flatten = true, add_initial_parameters = true) + sys::T; split = true, flatten = true, add_initial_parameters = true) where {T <: AbstractSystem} sys = discover_globalscoped(sys) if flatten - eqs = equations(sys) - if eqs isa AbstractArray && eltype(eqs) <: Equation - newsys = expand_connections(sys) - else - newsys = sys - end - newsys = ModelingToolkit.flatten(newsys) + newsys = expand_connections(sys) + newsys = ModelingToolkitBase.flatten(newsys) + newsys = discrete_unknowns_to_parameters(newsys) + @set! newsys.parameter_bindings_graph = ParameterBindingsGraph(newsys) + newsys = remove_bound_parameters_from_ps(newsys) + check_no_bound_initial_conditions(newsys) if has_parent(newsys) && get_parent(sys) === nothing - @set! newsys.parent = complete(sys; split = false, flatten = false) + @set! newsys.parent = complete(sys; split = false, flatten = false)::T end sys = newsys - sys = process_parameter_equations(sys) + check_no_parameter_equations(sys) if add_initial_parameters - sys = add_initialization_parameters(sys; split) + sys = add_initialization_parameters(sys; split)::T end + cb_alg_eqs = Equation[alg_equations(sys); observed(sys)] if has_continuous_events(sys) && is_time_dependent(sys) - @set! sys.continuous_events = complete.( - get_continuous_events(sys); iv = get_iv(sys), - alg_eqs = [alg_equations(sys); observed(sys)]) + cevts = SymbolicContinuousCallback[] + for ev in get_continuous_events(sys) + ev = complete(ev; iv = get_iv(sys)::SymbolicT, alg_eqs = cb_alg_eqs) + push!(cevts, ev) + end + @set! sys.continuous_events = cevts end if has_discrete_events(sys) && is_time_dependent(sys) - @set! sys.discrete_events = complete.( - get_discrete_events(sys); iv = get_iv(sys), - alg_eqs = [alg_equations(sys); observed(sys)]) + devts = SymbolicDiscreteCallback[] + for ev in get_discrete_events(sys) + ev = complete(ev; iv = get_iv(sys)::SymbolicT, alg_eqs = cb_alg_eqs) + push!(devts, ev) + end + @set! sys.discrete_events = devts end + else + # reduce the potential for outdated information + @set! sys.parameter_bindings_graph = nothing end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) # Ideally we'd do `get_ps` but if `flatten = false` # we don't get all of them. So we call `parameters`. all_ps = parameters(sys; initial_parameters = true) + all_ps_set = Set{SymbolicT}(all_ps) # inputs have to be maintained in a specific order input_vars = inputs(sys) if !isempty(all_ps) # reorder parameters by portions - ps_split = reorder_parameters(sys, all_ps) + ps_split = Vector{Vector{SymbolicT}}(reorder_parameters(sys, all_ps)) # if there are tunables, they will all be in `ps_split[1]` # and the arrays will have been scalarized - ordered_ps = eltype(all_ps)[] + ordered_ps = SymbolicT[] + offset = 0 # if there are no tunables, vcat them if !isempty(get_index_cache(sys).tunable_idx) - unflatten_parameters!(ordered_ps, ps_split[1], all_ps) - ps_split = Base.tail(ps_split) + unflatten_parameters!(ordered_ps, ps_split[offset + 1], all_ps_set) + offset += 1 end # unflatten initial parameters if !isempty(get_index_cache(sys).initials_idx) - unflatten_parameters!(ordered_ps, ps_split[1], all_ps) - ps_split = Base.tail(ps_split) + unflatten_parameters!(ordered_ps, ps_split[offset + 1], all_ps_set) + offset += 1 + end + for i in (offset+1):length(ps_split) + append!(ordered_ps, ps_split[i]::Vector{SymbolicT}) end - ordered_ps = vcat( - ordered_ps, reduce(vcat, ps_split; init = eltype(ordered_ps)[])) if isscheduled(sys) # ensure inputs are sorted - input_idxs = findfirst.(isequal.(input_vars), (ordered_ps,)) - @assert all(!isnothing, input_idxs) - @assert issorted(input_idxs) + last_idx = 0 + for p in input_vars + p, _ = split_indexed_var(p) + idx = findfirst(isequal(p), ordered_ps)::Int + @assert last_idx <= idx + last_idx = idx + end end @set! sys.ps = ordered_ps end elseif has_index_cache(sys) @set! sys.index_cache = nothing end - if isdefined(sys, :initializesystem) && get_initializesystem(sys) !== nothing - @set! sys.initializesystem = complete(get_initializesystem(sys); split) + if has_initializesystem(sys) + isys = get_initializesystem(sys) + if isys isa T + @set! sys.initializesystem = complete(isys::T; split) + end end sys = toggle_namespacing(sys, false; safe = true) isdefined(sys, :complete) ? (@set! sys.complete = true) : sys end +""" + $TYPEDSIGNATURES + +Find all discretes from symbolic event affects. +""" +function get_all_discretes(sys::AbstractSystem) + all_discretes = AtomicArraySet() + is_time_dependent(sys) || return all_discretes + for cb::SymbolicContinuousCallback in continuous_events(sys) + for aff in (cb.affect, cb.affect_neg, cb.initialize, cb.finalize) + aff === nothing && continue + discs = discretes(aff) + for v in discs + push_as_atomic_array!(all_discretes, v) + end + end + end + for cb::SymbolicDiscreteCallback in discrete_events(sys) + for aff in (cb.affect, cb.initialize, cb.finalize) + aff === nothing && continue + discs = discretes(aff) + for v in discs + push_as_atomic_array!(all_discretes, v) + end + end + end + + return all_discretes +end + +""" + $TYPEDSIGNATURES + +Identical to `get_all_discretes`, except it uses the `IndexCache` as a fast path if +possible. +""" +function get_all_discretes_fast(sys::AbstractSystem) + is_split(sys) || return get_all_discretes(sys) + ic::IndexCache = get_index_cache(sys) + all_discretes = AtomicArraySet() + for k in keys(ic.discrete_idx) + push_as_atomic_array!(all_discretes, k) + end + return all_discretes +end + +""" + $TYPEDSIGNATURES + +Find discrete variables in `unknowns(sys)` and turn them into parameters. +""" +function discrete_unknowns_to_parameters(sys::AbstractSystem) + all_discretes = get_all_discretes(sys) + + all_dvs = AtomicArraySet() + for v in unknowns(sys) + push_as_atomic_array!(all_dvs, v) + end + + intersect!(all_discretes, all_dvs) + + new_dvs = SymbolicT[] + for v in unknowns(sys) + split_indexed_var(v)[1] in all_discretes && continue + push!(new_dvs, v) + end + + @set! sys.unknowns = new_dvs + @set! sys.ps = [get_ps(sys); collect(all_discretes)] + + return sys +end + +function remove_bound_parameters_from_ps(sys::AbstractSystem) + bgraph::ParameterBindingsGraph = get_parameter_bindings_graph(sys) + ps = OrderedSet{SymbolicT}(get_ps(sys)) + filterer = !in(bgraph.bound_ps) ∘ first ∘ split_indexed_var + filter!(filterer, ps) + @set! sys.ps = collect(ps) +end + """ $(TYPEDSIGNATURES) @@ -722,26 +818,28 @@ parameters in the system `all_ps`, unscalarize the elements in `params` and appe to `buffer` in the same order as they are present in `params`. Effectively, if `params = [p[1], p[2], p[3], q]` then this is equivalent to `push!(buffer, p, q)`. """ -function unflatten_parameters!(buffer, params, all_ps) +function unflatten_parameters!(buffer::Vector{SymbolicT}, params::Vector{SymbolicT}, all_ps::Set{SymbolicT}) i = 1 # go through all the tunables while i <= length(params) sym = params[i] # if the sym is not a scalarized array symbolic OR it was already scalarized, # just push it as-is - if !iscall(sym) || operation(sym) != getindex || - any(isequal(sym), all_ps) + arrsym, isarr = split_indexed_var(sym) + if !isarr || !symbolic_has_known_size(arrsym) push!(buffer, sym) i += 1 continue end + # the next `length(sym)` symbols should be scalarized versions of the same # array symbolic - if !allequal(first(arguments(x)) - for x in view(params, i:(i + length(sym) - 1))) - error("This should not be possible. Please open an issue in ModelingToolkit.jl with an MWE and stacktrace.") + for j in (i+1):(i+length(sym)-1) + p = params[j] + if !(iscall(p) && operation(p) === getindex && isequal(arguments(p)[1], arrsym)) + error("This should not be possible. Please open an issue in ModelingToolkitBase.jl with an MWE and stacktrace.") + end end - arrsym = first(arguments(sym)) push!(buffer, arrsym) i += length(arrsym) end @@ -759,7 +857,8 @@ const SYS_PROPS = [:eqs :name :description :var_to_name - :defaults + :bindings + :initial_conditions :guesses :observed :systems @@ -778,7 +877,6 @@ const SYS_PROPS = [:eqs :gui_metadata :is_initializesystem :is_discrete - :parameter_dependencies :assertions :ignored_connections :parent @@ -787,6 +885,7 @@ const SYS_PROPS = [:eqs :inputs :outputs :index_cache + :parameter_bindings_graph :isscheduled :costs :consolidate] @@ -900,9 +999,9 @@ end Base.getproperty(sys::AbstractSystem, name::Symbol) Access the subsystem, variable or analysis point of `sys` named `name`. To check if `sys` -will namespace the returned value, use `ModelingToolkit.does_namespacing(sys)`. +will namespace the returned value, use `ModelingToolkitBase.does_namespacing(sys)`. -See also: [`ModelingToolkit.does_namespacing`](@ref). +See also: [`ModelingToolkitBase.does_namespacing`](@ref). """ function Base.getproperty( sys::AbstractSystem, name::Symbol; namespace = does_namespacing(sys)) @@ -958,8 +1057,12 @@ function getvar(sys::AbstractSystem, name::Symbol; namespace = does_namespacing( if has_eqs(sys) for eq in get_eqs(sys) eq isa Equation || continue - if eq.lhs isa AnalysisPoint && nameof(eq.rhs) == name - return namespace ? renamespace(sys, eq.rhs) : eq.rhs + lhs = value(eq.lhs) + rhs = value(eq.rhs) + if value(lhs) isa AnalysisPoint + rhs = rhs::AnalysisPoint + nameof(rhs) == name || continue + return namespace ? renamespace(sys, rhs) : rhs end end end @@ -974,7 +1077,7 @@ function Base.setproperty!(sys::AbstractSystem, prop::Symbol, val) easily using packages such as Setfield.jl. If you are looking for the old behavior of updating the default of a variable via \ - `setproperty!`, this should now be done by mutating `ModelingToolkit.get_defaults(sys)`. + `setproperty!`, this should now be done by mutating `ModelingToolkitBase.get_initial_conditions(sys)`. """) end @@ -1019,7 +1122,7 @@ struct LocalScope <: SymScope end Apply `LocalScope` to `sym`. """ -function LocalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) +function LocalScope(sym::Union{Num, SymbolicT, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) === getindex args = arguments(sym) @@ -1051,7 +1154,7 @@ end Apply `ParentScope` to `sym`, with `parent` being `LocalScope`. """ -function ParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) +function ParentScope(sym::Union{Num, SymbolicT, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) === getindex args = arguments(sym) @@ -1081,7 +1184,7 @@ struct GlobalScope <: SymScope end Apply `GlobalScope` to `sym`. """ -function GlobalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) +function GlobalScope(sym::Union{Num, SymbolicT, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) == getindex args = arguments(sym) @@ -1094,60 +1197,68 @@ function GlobalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) end end +const AllScopes = Union{LocalScope, ParentScope, GlobalScope} + renamespace(sys, eq::Equation) = namespace_equation(eq, sys) renamespace(names::AbstractVector, x) = foldr(renamespace, names, init = x) +renamespace(sys, tgt::AbstractSystem) = rename(tgt, renamespace(sys, nameof(tgt))) +renamespace(sys, tgt::Symbol) = Symbol(getname(sys), NAMESPACE_SEPARATOR_SYMBOL, tgt) +renamespace(sys, x::Num) = Num(renamespace(sys, unwrap(x))) +renamespace(sys, x::Arr{T, N}) where {T, N} = Arr{T, N}(renamespace(sys, unwrap(x))) +renamespace(sys, x::CallAndWrap{T}) where {T} = CallAndWrap{T}(renamespace(sys, unwrap(x))) + """ $(TYPEDSIGNATURES) Namespace `x` with the name of `sys`. """ -function renamespace(sys, x) - sys === nothing && return x - x = unwrap(x) - if x isa Symbolic - T = typeof(x) - if iscall(x) && operation(x) isa Operator - return maketerm(typeof(x), operation(x), - Any[renamespace(sys, only(arguments(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]), - metadata(x))::T - end - let scope = getmetadata(x, SymScope, LocalScope()) +function renamespace(sys, x::SymbolicT) + isequal(x, SU.idxs_for_arrayop(VartypeT)) && return x + Moshi.Match.@match x begin + BSImpl.Sym(; name) => let scope = getmetadata(x, SymScope, LocalScope())::AllScopes if scope isa LocalScope - rename(x, renamespace(getname(sys), getname(x)))::T + return rename(x, renamespace(getname(sys), name))::SymbolicT elseif scope isa ParentScope - setmetadata(x, SymScope, scope.parent)::T - else # GlobalScope - x::T + return setmetadata(x, SymScope, scope.parent)::SymbolicT + elseif scope isa GlobalScope + return x end + error() + end + BSImpl.Term(; f, args, shape, type, metadata) => begin + if f === getindex + newargs = copy(parent(args)) + newargs[1] = renamespace(sys, args[1]) + return BSImpl.Term{VartypeT}(getindex, newargs; type, shape, metadata) + elseif f isa SymbolicT + let scope = getmetadata(x, SymScope, LocalScope())::Union{LocalScope, ParentScope, GlobalScope} + if scope isa LocalScope + return rename(x, renamespace(getname(sys), getname(x)))::SymbolicT + elseif scope isa ParentScope + return setmetadata(x, SymScope, scope.parent)::SymbolicT + elseif scope isa GlobalScope + return x + end + error() + end + elseif f isa Operator + newargs = copy(parent(args)) + for (i, arg) in enumerate(args) + newargs[i] = renamespace(sys, arg) + end + return BSImpl.Term{VartypeT}(f, newargs; type, shape, metadata) + end + error() end - elseif x isa AbstractSystem - rename(x, renamespace(sys, nameof(x))) - else - Symbol(getname(sys), NAMESPACE_SEPARATOR_SYMBOL, x) end end namespace_variables(sys::AbstractSystem) = unknowns(sys, unknowns(sys)) namespace_parameters(sys::AbstractSystem) = parameters(sys, parameters(sys)) -function namespace_defaults(sys) - defs = defaults(sys) - Dict((isparameter(k) ? parameters(sys, k) : unknowns(sys, k)) => namespace_expr(v, 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 +namespace_guesses(sys::AbstractSystem) = namespace_expr(guesses(sys), sys) """ $(TYPEDSIGNATURES) @@ -1156,8 +1267,14 @@ Return `equations(sys)`, namespaced by the name of `sys`. """ function namespace_equations(sys::AbstractSystem, ivs = independent_variables(sys)) eqs = equations(sys) - isempty(eqs) && return Equation[] - map(eq -> namespace_equation(eq, sys; ivs), eqs) + isempty(eqs) && return eqs + if eqs === get_eqs(sys) + eqs = copy(eqs) + end + for i in eachindex(eqs) + eqs[i] = namespace_equation(eqs[i], sys; ivs) + end + return eqs end function namespace_initialization_equations( @@ -1204,11 +1321,26 @@ function namespace_jump(j::MassActionJump, sys) end function namespace_jumps(sys::AbstractSystem) - return [namespace_jump(j, sys) for j in get_jumps(sys)] + js = jumps(sys) + isempty(js) && return js + if js === get_jumps(sys) + js = copy(js) + end + for i in eachindex(js) + js[i] = namespace_jump(js[i], sys) + end + return js end function namespace_brownians(sys::AbstractSystem) - return [renamespace(sys, b) for b in brownians(sys)] + bs = brownians(sys) + if bs === get_brownians(sys) + bs = copy(bs) + end + for i in eachindex(bs) + bs[i] = renamespace(sys, bs[i]) + end + return bs end function namespace_assignment(eq::Assignment, sys) @@ -1228,74 +1360,92 @@ function is_array_of_symbolics(x::SparseMatrixCSC) return is_array_of_symbolics(nonzeros(x)) end -function namespace_expr( - O, sys, n = (sys === nothing ? nothing : nameof(sys)); - ivs = sys === nothing ? nothing : independent_variables(sys)) - sys === nothing && return O - O = unwrap(O) - # Exceptions for arrays of symbolic and Ref of a symbolic, the latter - # of which shows up in broadcasts - if symbolic_type(O) == NotSymbolic() && !(O isa AbstractArray) && !(O isa Ref) - return O - end - if any(isequal(O), ivs) - return 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)) - end - if isvariable(O) - # Use renamespace so the scope is correct, and make sure to use the - # metadata from the rescoped variable - rescoped = renamespace(n, O) - maketerm(typeof(rescoped), operation(rescoped), renamed, - metadata(rescoped)) - elseif Symbolics.isarraysymbolic(O) - # promote_symtype doesn't work for array symbolics - maketerm(typeof(O), operation(O), renamed, metadata(O)) - else - maketerm(typeof(O), operation(O), renamed, metadata(O)) +function namespace_expr(O, sys::AbstractSystem, n::Symbol = nameof(sys); kw...) + return O +end +function namespace_expr(O::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}, sys::AbstractSystem, n::Symbol = nameof(sys); kw...) + typeof(O)(namespace_expr(unwrap(O), sys, n; kw...)) +end +function namespace_expr(O::AbstractArray, sys::AbstractSystem, n::Symbol = nameof(sys); ivs = independent_variables(sys)) + is_array_of_symbolics(O) || return O + O = copy(O) + for i in eachindex(O) + O[i] = namespace_expr(O[i], sys, n; ivs) + end + return O +end +function namespace_expr(O::AbstractDict, sys::AbstractSystem, n::Symbol = nameof(sys); kw...) + O2 = empty(O) + for (k, v) in O + O2[namespace_expr(k, sys, n; kw...)] = namespace_expr(v, sys, n; kw...) + end + return O2 +end +function namespace_expr(O::SymbolicT, sys::AbstractSystem, n::Symbol = nameof(sys); ivs = independent_variables(sys)) + any(isequal(O), ivs) && return O + isvar = isvariable(O) + Moshi.Match.@match O begin + BSImpl.Const(;) => return O + BSImpl.Sym(;) => return isvar ? renamespace(n, O) : O + BSImpl.Term(; f, args, metadata, type, shape) => begin + newargs = copy(parent(args)) + for i in eachindex(args) + newargs[i] = namespace_expr(newargs[i], sys, n; ivs) + end + if isvar + rescoped = renamespace(n, O) + f = Moshi.Data.variant_getfield(rescoped, BSImpl.Term{VartypeT}, :f) + meta = Moshi.Data.variant_getfield(rescoped, BSImpl.Term{VartypeT}, :metadata) + elseif f isa SymbolicT + f = renamespace(n, f) + meta = metadata + else + meta = metadata + end + return BSImpl.Term{VartypeT}(f, newargs; type, shape, metadata = meta) + end + BSImpl.AddMul(; coeff, dict, variant, type, shape, metadata) => begin + newdict = copy(dict) + empty!(newdict) + for (k, v) in dict + newdict[namespace_expr(k, sys, n; ivs)] = v + end + return BSImpl.AddMul{VartypeT}(coeff, newdict, variant; type, shape, metadata) end - elseif isvariable(O) - renamespace(n, O) - elseif O isa AbstractArray && is_array_of_symbolics(O) - let sys = sys, n = n - map(o -> namespace_expr(o, sys, n; ivs), O) + BSImpl.Div(; num, den, type, shape, metadata) => begin + num = namespace_expr(num, sys, n; ivs) + den = namespace_expr(den, sys, n; ivs) + return BSImpl.Div{VartypeT}(num, den, false; type, shape, metadata) + end + BSImpl.ArrayOp(; output_idx, expr, term, ranges, reduce, type, shape, metadata) => begin + if term isa SymbolicT + term = namespace_expr(term, sys, n; ivs) + end + expr = namespace_expr(expr, sys, n; ivs) + return BSImpl.ArrayOp{VartypeT}(output_idx, expr, reduce, term, ranges; type, shape, metadata) end - else - O end end -_nonum(@nospecialize x) = x isa Num ? x.val : x - """ $(TYPEDSIGNATURES) Get the unknown variables of the system `sys` and its subsystems. These must be explicitly solved for, unlike `observables(sys)`. -See also [`ModelingToolkit.get_unknowns`](@ref). +See also [`ModelingToolkitBase.get_unknowns`](@ref). """ function unknowns(sys::AbstractSystem) sts = get_unknowns(sys) systems = get_systems(sys) - nonunique_unknowns = if isempty(systems) - sts - else - system_unknowns = reduce(vcat, namespace_variables.(systems)) - isempty(sts) ? system_unknowns : [sts; system_unknowns] + if isempty(systems) + return sts end - isempty(nonunique_unknowns) && return nonunique_unknowns - # `Vector{Any}` is incompatible with the `SymbolicIndexingInterface`, which uses - # `elsymtype = symbolic_type(eltype(_arg))` - # which inappropriately returns `NotSymbolic()` - if nonunique_unknowns isa Vector{Any} - nonunique_unknowns = _nonum.(nonunique_unknowns) + result = copy(sts) + for subsys in systems + append!(result, namespace_variables(subsys)) end - @assert typeof(nonunique_unknowns) !== Vector{Any} - unique(nonunique_unknowns) + return result end """ @@ -1315,39 +1465,32 @@ $(TYPEDSIGNATURES) Get the parameters of the system `sys` and its subsystems. -See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). +See also [`@parameters`](@ref) and [`ModelingToolkitBase.get_ps`](@ref). """ function parameters(sys::AbstractSystem; initial_parameters = false) ps = get_ps(sys) - if ps == SciMLBase.NullParameters() - return [] + if ps === SciMLBase.NullParameters() + return SymbolicT[] end if eltype(ps) <: Pair - ps = first.(ps) + ps = Vector{SymbolicT}(unwrap.(first.(ps))) end systems = get_systems(sys) - result = unique(isempty(systems) ? ps : - [ps; reduce(vcat, namespace_parameters.(systems))]) + result = OrderedSet{SymbolicT}(ps) + for subsys in systems + union!(result, namespace_parameters(subsys)) + end + result = collect(result) if !initial_parameters && !is_initializesystem(sys) filter!(result) do sym return !(isoperator(sym, Initial) || - iscall(sym) && operation(sym) == getindex && + iscall(sym) && operation(sym) === getindex && isoperator(arguments(sym)[1], Initial)) end end return result end -function dependent_parameters(sys::AbstractSystem) - if !iscomplete(sys) - throw(ArgumentError(""" - `dependent_parameters` requires that the system is marked as complete. Call - `complete` or `mtkcompile` on the system. - """)) - end - return map(eq -> eq.lhs, parameter_dependencies(sys)) -end - """ parameters_toplevel(sys::AbstractSystem) @@ -1361,33 +1504,14 @@ function parameters_toplevel(sys::AbstractSystem) end """ - $(TYPEDSIGNATURES) + $TYPEDSIGNATURES -Get the parameter dependencies of the system `sys` and its subsystems. Requires that the -system is `complete`d. +Return the bound parameters of a system. Currently requires that the system is +marked complete. """ -function parameter_dependencies(sys::AbstractSystem) - if !iscomplete(sys) - throw(ArgumentError(""" - `parameter_dependencies` requires that the system is marked as complete. Call \ - `complete` or `mtkcompile` on the system. - """)) - end - if !has_parameter_dependencies(sys) - return Equation[] - end - get_parameter_dependencies(sys) -end - -""" - $(TYPEDSIGNATURES) - -Return all of the parameters of the system, including hidden initial parameters and ones -eliminated via `parameter_dependencies`. -""" -function full_parameters(sys::AbstractSystem) - dep_ps = [eq.lhs for eq in get_parameter_dependencies(sys)] - vcat(parameters(sys; initial_parameters = true), dep_ps) +function bound_parameters(sys::AbstractSystem) + iscomplete(sys) || error("`bound_parameters` requires a completed system.") + (get_parameter_bindings_graph(sys)::ParameterBindingsGraph).bound_ps end """ @@ -1434,7 +1558,7 @@ $(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). +See also [`initialization_equations`](@ref) and [`ModelingToolkitBase.get_guesses`](@ref). """ function guesses(sys::AbstractSystem) guess = get_guesses(sys) @@ -1455,15 +1579,20 @@ $(TYPEDSIGNATURES) Get the observed equations of the system `sys` and its subsystems. These can be expressed in terms of `unknowns(sys)`, and do not have to be explicitly solved for. -See also [`observables`](@ref) and [`ModelingToolkit.get_observed()`](@ref). +See also [`observables`](@ref) and [`ModelingToolkitBase.get_observed()`](@ref). """ function observed(sys::AbstractSystem) obs = get_observed(sys) systems = get_systems(sys) - [obs; - reduce(vcat, - (map(o -> namespace_equation(o, s), observed(s)) for s in systems), - init = Equation[])] + isempty(systems) && return obs + obs = copy(obs) + for subsys in systems + _obs = observed(subsys) + for eq in _obs + push!(obs, namespace_equation(eq, subsys)) + end + end + return obs end """ @@ -1480,34 +1609,44 @@ function observables(sys::AbstractSystem) end """ -$(TYPEDSIGNATURES) + $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. +Get the bindings of a system `sys` and its subsystems. +""" +function bindings(sys::AbstractSystem) + systems = get_systems(sys) + binds = get_bindings(sys) + isempty(systems) && return binds + binds = copy(parent(binds)) + for s in systems + no_override_merge!(binds, namespace_expr(parent(bindings(s)), s)) + end + return ROSymmapT(binds) +end + +""" + $TYPEDSIGNATURES -See also [`initialization_equations`](@ref) and [`ModelingToolkit.get_defaults`](@ref). +Get the initial conditions of a system `sys` and its subsystems. """ -function defaults(sys::AbstractSystem) +function initial_conditions(sys::AbstractSystem) systems = get_systems(sys) - defs = get_defaults(sys) - # `mapfoldr` is really important!!! We should prefer the base model for - # defaults, because people write: - # - # `compose(System(...; defaults=defs), ...)` - # - # Thus, right associativity is required and crucial for correctness. - isempty(systems) ? defs : mapfoldr(namespace_defaults, merge, systems; init = defs) + ics = get_initial_conditions(sys) + isempty(systems) && return ics + ics = copy(ics) + for s in systems + left_merge!(ics, namespace_expr(initial_conditions(s), s)) + end + return ics end -function defaults_and_guesses(sys::AbstractSystem) - merge(guesses(sys), defaults(sys)) +function initial_conditions_and_guesses(sys::AbstractSystem) + merge(guesses(sys), initial_conditions(sys)) end unknowns(sys::Union{AbstractSystem, Nothing}, v) = namespace_expr(v, sys) -for vType in [Symbolics.Arr, Symbolics.Symbolic{<:AbstractArray}] - @eval unknowns(sys::AbstractSystem, v::$vType) = namespace_expr(v, sys) - @eval parameters(sys::AbstractSystem, v::$vType) = toparam(unknowns(sys, v)) -end +unknowns(sys::AbstractSystem, v::Symbolics.Arr) = namespace_expr(v, sys) +parameters(sys::AbstractSystem, v::Symbolics.Arr) = toparam(unknowns(sys, v)) parameters(sys::Union{AbstractSystem, Nothing}, v) = toparam(unknowns(sys, v)) for f in [:unknowns, :parameters] @eval function $f(sys::AbstractSystem, vs::AbstractArray) @@ -1524,20 +1663,17 @@ 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). +See also [`full_equations`](@ref) and [`ModelingToolkitBase.get_eqs`](@ref). """ function equations(sys::AbstractSystem) eqs = get_eqs(sys) systems = get_systems(sys) - if isempty(systems) - return eqs - else - eqs = Equation[eqs; - reduce(vcat, - namespace_equations.(get_systems(sys)); - init = Equation[])] - return eqs + isempty(systems) && return eqs + eqs = copy(eqs) + for subsys in systems + append!(eqs, namespace_equations(subsys)) end + return eqs end """ @@ -1563,7 +1699,7 @@ Recursively substitute `dict` into `expr`. Use `Symbolics.simplify` on the expre if `simplify == true`. """ function substitute_and_simplify(expr, dict::AbstractDict, simplify::Bool) - expr = Symbolics.fixpoint_sub(expr, dict; operator = ModelingToolkit.Initial) + expr = Symbolics.fixpoint_sub(expr, dict; operator = Union{ModelingToolkitBase.Initial, Pre}) simplify ? Symbolics.simplify(expr) : expr end @@ -1585,7 +1721,7 @@ $(TYPEDSIGNATURES) Like `equations(sys)`, but also substitutes the observed equations eliminated from the equations during `mtkcompile`. These equations matches generated numerical code. -See also [`equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). +See also [`equations`](@ref) and [`ModelingToolkitBase.get_eqs`](@ref). """ function full_equations(sys::AbstractSystem; simplify = false) empty_substitutions(sys) && return equations(sys) @@ -1617,10 +1753,12 @@ all the subsystems of `sys` (appropriately namespaced). function jumps(sys::AbstractSystem) js = get_jumps(sys) systems = get_systems(sys) - if isempty(systems) - return js + isempty(systems) && return js + js = copy(js) + for subsys in systems + append!(js, namespace_jumps(subsys)) end - return [js; reduce(vcat, namespace_jumps.(systems); init = [])] + return js end """ @@ -1635,7 +1773,11 @@ function brownians(sys::AbstractSystem) if isempty(systems) return bs end - return [bs; reduce(vcat, namespace_brownians.(systems); init = [])] + bs = copy(bs) + for subsys in systems + append!(bs, namespace_brownians(subsys)) + end + return bs end """ @@ -1649,10 +1791,13 @@ function cost(sys::AbstractSystem) consolidate = get_consolidate(sys) systems = get_systems(sys) if isempty(systems) - return consolidate(cs, Float64[]) + return consolidate(cs, Float64[])::SymbolicT end - subcosts = [namespace_expr(cost(subsys), subsys) for subsys in systems] - return consolidate(cs, subcosts) + subcosts = SymbolicT[] + for subsys in systems + push!(subcosts, namespace_expr(cost(subsys), subsys)) + end + return consolidate(cs, subcosts)::SymbolicT end namespace_constraint(eq::Equation, sys) = namespace_equation(eq, sys) @@ -1669,8 +1814,14 @@ end function namespace_constraints(sys) cstrs = constraints(sys) - isempty(cstrs) && return Vector{Union{Equation, Inequality}}(undef, 0) - map(cstr -> namespace_constraint(cstr, sys), cstrs) + isempty(cstrs) && return cstrs + if cstrs === get_constraints(sys) + cstrs = copy(cstrs) + end + for i in eachindex(cstrs) + cstrs[i] = namespace_constraint(cstrs[i], sys) + end + return cstrs end """ @@ -1681,7 +1832,12 @@ Get all constraints in the system `sys` and all of its subsystems, appropriately function constraints(sys::AbstractSystem) cs = get_constraints(sys) systems = get_systems(sys) - isempty(systems) ? cs : [cs; reduce(vcat, namespace_constraints.(systems))] + isempty(systems) && return cs + cs = copy(cs) + for subsys in systems + append!(cs, namespace_constraints(subsys)) + end + return cs end """ @@ -1689,7 +1845,7 @@ $(TYPEDSIGNATURES) Get the initialization equations of the system `sys` and its subsystems. -See also [`guesses`](@ref), [`defaults`](@ref) and [`ModelingToolkit.get_initialization_eqs`](@ref). +See also [`guesses`](@ref), [`initial_conditions`](@ref) and [`ModelingToolkitBase.get_initialization_eqs`](@ref). """ function initialization_equations(sys::AbstractSystem) eqs = get_initialization_eqs(sys) @@ -1900,7 +2056,7 @@ end ### ### System I/O ### -function toexpr(sys::AbstractSystem) +function SymbolicUtils.Code.toexpr(sys::AbstractSystem) sys = flatten(sys) expr = Expr(:block) stmt = expr.args @@ -1918,6 +2074,9 @@ function toexpr(sys::AbstractSystem) push_vars!(stmt, stsname, Symbol("@variables"), sts) psname = gensym(:ps) ps = parameters(sys) + if iscomplete(sys) + ps = [ps; collect(bound_parameters(sys))] + end push_vars!(stmt, psname, Symbol("@parameters"), ps) obs = observed(sys) obsvars = [o.lhs for o in obs] @@ -1930,11 +2089,14 @@ function toexpr(sys::AbstractSystem) end eqs_name = push_eqs!(stmt, full_equations(sys), var2name) - filtered_defs = filter( - kvp -> !(iscall(kvp[1]) && operation(kvp[1]) isa Initial), defaults(sys)) + filtered_bindings = filter( + kvp -> !(iscall(kvp[1]) && operation(kvp[1]) isa Initial), parent(copy(bindings(sys)))) + filtered_initial_conditions = filter( + kvp -> !(iscall(kvp[1]) && operation(kvp[1]) isa Initial), initial_conditions(sys)) filtered_guesses = filter( kvp -> !(iscall(kvp[1]) && operation(kvp[1]) isa Initial), guesses(sys)) - defs_name = push_defaults!(stmt, filtered_defs, var2name) + bindings_name = push_defaults!(stmt, filtered_bindings, var2name; name = :bindings) + initial_conditions_name = push_defaults!(stmt, filtered_initial_conditions, var2name; name = :initial_conditions) guesses_name = push_defaults!(stmt, filtered_guesses, var2name; name = :guesses) obs_name = push_eqs!(stmt, obs, var2name) @@ -1946,8 +2108,8 @@ function toexpr(sys::AbstractSystem) push!(stmt, :($ivname = (@variables $(getname(iv)))[1])) end push!(stmt, - :($System($eqs_name, $ivname, $stsname, $psname; defaults = $defs_name, - guesses = $guesses_name, observed = $obs_name, + :($System($eqs_name, $ivname, $stsname, $psname; bindings = $bindings_name, + initial_conditions = $initial_conditions_name, guesses = $guesses_name, observed = $obs_name, name = $name, checks = false))) expr = :(let @@ -1988,6 +2150,7 @@ Base.show(io::IO, sys::AbstractSystem; kws...) = show(io, MIME"text/plain"(), sy function Base.show( io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = true, bold = true) + Symbolics.warn_load_latexify() limit = get(io, :limit, false) # if output should be limited, rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list @@ -2041,7 +2204,7 @@ function Base.show( printstyled(io, "\n$header ($nvars):"; bold) hint && print(io, " see $(nameof(varfunc))($name)") nrows = min(nvars, limit ? rows : nvars) - defs = has_defaults(sys) ? defaults(sys) : nothing + defs = has_bindings(sys) ? bindings(sys) : nothing for i in 1:nrows s = vars[i] print(io, "\n ", s) @@ -2065,11 +2228,6 @@ function Base.show( limited && printstyled(io, "\n ⋮") # too many variables to print end - # Print parameter dependencies - npdeps = has_parameter_dependencies(sys) ? length(get_parameter_dependencies(sys)) : 0 - npdeps > 0 && printstyled(io, "\nParameter dependencies ($npdeps):"; bold) - npdeps > 0 && hint && print(io, " see parameter_dependencies($name)") - # Print observed nobs = has_observed(sys) ? length(observed(sys)) : 0 nobs > 0 && printstyled(io, "\nObserved ($nobs):"; bold) @@ -2089,16 +2247,18 @@ varname_fix!(s) = return function varname_fix!(expr::Expr) for arg in expr.args - MLStyle.@match arg begin - ::Symbol => continue - Expr(:kw, a...) || Expr(:kw, a) => varname_sanitization!(arg) - Expr(:parameters, a...) => begin - for _arg in arg.args - varname_sanitization!(_arg) - end + arg isa Symbol && continue + if Meta.isexpr(arg, :kw) + varname_sanitization!(arg) + continue + end + if Meta.isexpr(arg, :parameters) + for _arg in arg.args + varname_sanitization!(_arg) end - _ => @debug "skipping variable sanitization of $arg" + continue end + @debug "skipping variable sanitization of $arg" end end @@ -2233,7 +2393,7 @@ component. Examples: ```julia-repl -julia> using ModelingToolkit +julia> using ModelingToolkitBase julia> foo(i; name) = (; i, name) foo (generic function with 1 method) @@ -2261,7 +2421,7 @@ end function default_to_parentscope(v) uv = unwrap(v) - uv isa Symbolic || return v + uv isa SymbolicT || return v apply_to_variables(v) do sym ParentScope(sym) end @@ -2349,7 +2509,7 @@ Mark a system constructor function as building a component. For example, end ``` -ModelingToolkit systems are either components or connectors. Components define dynamics of +ModelingToolkitBase systems are either components or connectors. Components define dynamics of the model. Connectors are used to connect components together. See the [Model building reference](@ref model_building_api) section of the documentation for more information. @@ -2378,7 +2538,7 @@ sys = mtkcompile(sys) """ macro mtkcompile(exprs...) expr = exprs[1] - named_expr = ModelingToolkit.named_expr(expr) + named_expr = ModelingToolkitBase.named_expr(expr) name = named_expr.args[1] kwargs = Base.tail(exprs) kwargs = map(kwargs) do ex @@ -2420,8 +2580,8 @@ Additionally, all assertions in the system are optionally logged when they fail. A new parameter is also added to the system which controls whether the message associated with each assertion will be logged when the assertion fails. This parameter defaults to `true` and can be toggled by symbolic indexing with -`ModelingToolkit.ASSERTION_LOG_VARIABLE`. For example, -`prob.ps[ModelingToolkit.ASSERTION_LOG_VARIABLE] = false` will disable logging. +`ModelingToolkitBase.ASSERTION_LOG_VARIABLE`. For example, +`prob.ps[ModelingToolkitBase.ASSERTION_LOG_VARIABLE] = false` will disable logging. """ function debug_system( sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...) @@ -2435,7 +2595,7 @@ function debug_system( eqs = debug_sub.(equations(sys), Ref(functions); kw...) @set! sys.eqs = eqs @set! sys.ps = unique!([get_ps(sys); ASSERTION_LOG_VARIABLE]) - @set! sys.defaults = merge(get_defaults(sys), Dict(ASSERTION_LOG_VARIABLE => true)) + @set! sys.initial_conditions = merge(get_initial_conditions(sys), Dict(ASSERTION_LOG_VARIABLE => true)) end if has_observed(sys) @set! sys.observed = debug_sub.(observed(sys), Ref(functions); kw...) @@ -2446,14 +2606,6 @@ function debug_system( return sys end -@latexrecipe function f(sys::AbstractSystem) - return latexify(equations(sys)) -end - -function Base.show(io::IO, ::MIME"text/latex", x::AbstractSystem) - print(io, "\$\$ " * latexify(x) * " \$\$") -end - struct InvalidSystemException <: Exception msg::String end @@ -2489,7 +2641,7 @@ function Base.showerror(io::IO, e::HybridSystemNotSupportedException) end function AbstractTrees.children(sys::AbstractSystem) - ModelingToolkit.get_systems(sys) + ModelingToolkitBase.get_systems(sys) end function AbstractTrees.printnode( io::IO, sys::AbstractSystem; describe = false, bold = false) @@ -2510,11 +2662,11 @@ function hierarchy(sys::AbstractSystem; describe = false, bold = describe, kwarg print_tree(sys; printnode_kw = (describe = describe, bold = bold), kwargs...) end -function Base.IteratorEltype(::Type{<:TreeIterator{ModelingToolkit.AbstractSystem}}) +function Base.IteratorEltype(::Type{<:TreeIterator{ModelingToolkitBase.AbstractSystem}}) Base.HasEltype() end -function Base.eltype(::Type{<:TreeIterator{ModelingToolkit.AbstractSystem}}) - ModelingToolkit.AbstractSystem +function Base.eltype(::Type{<:TreeIterator{ModelingToolkitBase.AbstractSystem}}) + ModelingToolkitBase.AbstractSystem end function check_array_equations_unknowns(eqs, dvs) @@ -2548,7 +2700,7 @@ end $(TYPEDSIGNATURES) Extend `basesys` with `sys`. This can be thought of as the `merge` operation on systems. -Values in `sys` take priority over duplicates in `basesys` (for example, defaults). +Values in `sys` take priority over duplicates in `basesys` (for example, initial conditions). By default, the resulting system inherits `sys`'s name and description. @@ -2576,11 +2728,11 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; 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(get_parameter_dependencies(basesys), get_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` + ics = merge(get_initial_conditions(basesys), get_initial_conditions(sys)) # prefer `sys` + binds = merge(get_bindings(basesys), get_bindings(sys)) # prefer `sys` meta = MetadataT() for kvp in get_metadata(basesys) kvp[1] == MutableCacheKey && continue @@ -2593,7 +2745,8 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; syss = union(get_systems(basesys), get_systems(sys)) args = length(ivs) == 0 ? (eqs, sts, ps) : (eqs, ivs[1], sts, ps) kwargs = (observed = obs, continuous_events = cevs, - discrete_events = devs, defaults = defs, systems = syss, metadata = meta, + discrete_events = devs, bindings = binds, initial_conditions = ics, systems = syss, + metadata = meta, name = name, description = description, gui_metadata = gui_metadata) # collect fields specific to some system types @@ -2607,7 +2760,6 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; end newsys = T(args...; kwargs...) - @set! newsys.parameter_dependencies = dep_ps return newsys end @@ -2661,8 +2813,8 @@ function compose(sys::AbstractSystem, systems::AbstractArray; name = nameof(sys) if has_is_dde(sys) @set! sys.is_dde = _check_if_dde(equations(sys), get_iv(sys), get_systems(sys)) end - newunknowns = OrderedSet() - newparams = OrderedSet() + newunknowns = OrderedSet{SymbolicT}() + newparams = OrderedSet{SymbolicT}() iv = has_iv(sys) ? get_iv(sys) : nothing for ssys in systems collect_scoped_vars!(newunknowns, newparams, ssys, iv) @@ -2702,48 +2854,19 @@ See also: [`compose`](@ref). """ Base.:(∘)(sys1::AbstractSystem, sys2::AbstractSystem) = compose(sys1, sys2) -function UnPack.unpack(sys::ModelingToolkit.AbstractSystem, ::Val{p}) where {p} +function UnPack.unpack(sys::ModelingToolkitBase.AbstractSystem, ::Val{p}) where {p} getproperty(sys, p; namespace = false) 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. -""" -function missing_variable_defaults( - sys::AbstractSystem, default = 0.0; subset = unknowns(sys)) - varmap = get_defaults(sys) - varmap = Dict(Symbolics.diff2term(value(k)) => value(varmap[k]) for k in keys(varmap)) - missingvars = setdiff(subset, keys(varmap)) - ds = Pair[] - - n = length(missingvars) - - if default isa Vector - @assert length(default)==n "`default` size ($(length(default))) should match the number of missing variables: $n" - end - - for (i, missingvar) in enumerate(missingvars) - if default isa Vector - push!(ds, missingvar => default[i]) - else - push!(ds, missingvar => default) - end - end - - return ds -end - -keytype(::Type{<:Pair{T, V}}) where {T, V} = T +_keytype(::Type{<:Pair{T, V}}) where {T, V} = T +_keytype(::Type{T}) where {T} = keytype(T) function Symbolics.substitute(sys::AbstractSystem, rules::Union{Vector{<:Pair}, Dict}) - if has_continuous_domain(sys) && get_continuous_events(sys) !== nothing && - !isempty(get_continuous_events(sys)) || + if 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 + if _keytype(eltype(rules)) <: Symbol dict = todict(rules) systems = get_systems(sys) # post-walk to avoid infinite recursion @@ -2752,85 +2875,32 @@ function Symbolics.substitute(sys::AbstractSystem, rules::Union{Vector{<:Pair}, elseif sys isa System rules = todict(map(r -> Symbolics.unwrap(r[1]) => Symbolics.unwrap(r[2]), collect(rules))) - newsys = @set sys.eqs = fast_substitute(get_eqs(sys), rules) + newsys = @set sys.eqs = substitute(get_eqs(sys), rules) @set! newsys.unknowns = map(get_unknowns(sys)) do var get(rules, var, var) end @set! newsys.ps = map(get_ps(sys)) do var get(rules, var, var) end - @set! newsys.parameter_dependencies = fast_substitute( - get_parameter_dependencies(sys), rules) - @set! newsys.defaults = Dict(fast_substitute(k, rules) => fast_substitute(v, rules) - for (k, v) in get_defaults(sys)) - @set! newsys.guesses = Dict(fast_substitute(k, rules) => fast_substitute(v, rules) + @set! newsys.bindings = Dict(substitute(k, rules) => substitute(v, rules) + for (k, v) in get_bindings(sys)) + @set! newsys.initial_conditions = Dict(substitute(k, rules) => substitute(v, rules) + for (k, v) in get_initial_conditions(sys)) + @set! newsys.guesses = Dict(substitute(k, rules) => substitute(v, rules) for (k, v) in get_guesses(sys)) - @set! newsys.noise_eqs = fast_substitute(get_noise_eqs(sys), rules) - @set! newsys.costs = Vector{Union{Real, BasicSymbolic}}(fast_substitute( + @set! newsys.noise_eqs = substitute(get_noise_eqs(sys), rules) + @set! newsys.costs = Vector{Union{Real, BasicSymbolic}}(substitute( get_costs(sys), rules)) - @set! newsys.observed = fast_substitute(get_observed(sys), rules) - @set! newsys.initialization_eqs = fast_substitute( + @set! newsys.observed = substitute(get_observed(sys), rules) + @set! newsys.initialization_eqs = substitute( get_initialization_eqs(sys), rules) - @set! newsys.constraints = fast_substitute(get_constraints(sys), rules) + @set! newsys.constraints = substitute(get_constraints(sys), rules) @set! newsys.systems = map(s -> substitute(s, rules), get_systems(sys)) else error("substituting symbols is not supported for $(typeof(sys))") end end -""" - $(TYPEDSIGNATURES) - -Find equations of `sys` involving only parameters and separate them out into the -`parameter_dependencies` field. Relative ordering of equations is maintained. -Parameter-only equations are validated to be explicit and sorted topologically. All such -explicitly determined parameters are removed from the parameters of `sys`. Return the new -system. -""" -function process_parameter_equations(sys::AbstractSystem) - if !isempty(get_systems(sys)) - throw(ArgumentError("Expected flattened system")) - end - varsbuf = Set() - pareq_idxs = Int[] - eqs = equations(sys) - for (i, eq) in enumerate(eqs) - empty!(varsbuf) - vars!(varsbuf, eq; op = Union{Differential, Initial, Pre, Hold, Sample}) - # singular equations - isempty(varsbuf) && continue - if all(varsbuf) do sym - is_parameter(sys, sym) || - symbolic_type(sym) == ArraySymbolic() && - is_sized_array_symbolic(sym) && - all(Base.Fix1(is_parameter, sys), collect(sym)) || - iscall(sym) && - operation(sym) === getindex && is_parameter(sys, arguments(sym)[1]) - end - # Everything in `varsbuf` is a parameter, so this is a cheap `is_parameter` - # check. - if !(eq.lhs in varsbuf) - throw(ArgumentError(""" - LHS of parameter dependency equation must be a single parameter. Found \ - $(eq.lhs). - """)) - end - push!(pareq_idxs, i) - end - end - - pareqs = [get_parameter_dependencies(sys); eqs[pareq_idxs]] - explicitpars = [eq.lhs for eq in pareqs] - pareqs = topsort_equations(pareqs, explicitpars) - - eqs = eqs[setdiff(eachindex(eqs), pareq_idxs)] - - @set! sys.eqs = eqs - @set! sys.parameter_dependencies = pareqs - @set! sys.ps = setdiff(get_ps(sys), explicitpars) - return sys -end - """ dump_parameters(sys::AbstractSystem) @@ -2838,38 +2908,30 @@ Return an array of `NamedTuple`s containing the metadata associated with each pa `sys`. Also includes the default value of the parameter, if provided. ```@example -using ModelingToolkit +using ModelingToolkitBase using DynamicQuantities -using ModelingToolkit: t, D +using ModelingToolkitBase: t, D @parameters p = 1.0, [description = "My parameter", tunable = false] q = 2.0, [description = "Other parameter"] @variables x(t) = 3.0 [unit = u"m"] @named sys = System(Equation[], t, [x], [p, q]) -ModelingToolkit.dump_parameters(sys) +ModelingToolkitBase.dump_parameters(sys) ``` -See also: [`ModelingToolkit.dump_variable_metadata`](@ref), [`ModelingToolkit.dump_unknowns`](@ref) +See also: [`ModelingToolkitBase.dump_variable_metadata`](@ref), [`ModelingToolkitBase.dump_unknowns`](@ref) """ function dump_parameters(sys::AbstractSystem) - defs = defaults(sys) - pdeps = get_parameter_dependencies(sys) - metas = map(dump_variable_metadata.(parameters(sys))) do meta - if haskey(defs, meta.var) - meta = merge(meta, (; default = defs[meta.var])) + ics = initial_conditions(sys) + binds = bindings(sys) + return map(dump_variable_metadata.(parameters(sys))) do meta + if haskey(ics, meta.var) + meta = merge(meta, (; initial_condition = ics[meta.var])) + end + if haskey(binds, meta.var) + meta = merge(meta, (; binding = binds[meta.var])) end meta end - pdep_metas = map(pdeps) do eq - sym = eq.lhs - val = eq.rhs - meta = dump_variable_metadata(sym) - defs[eq.lhs] = eq.rhs - meta = merge(meta, - (; dependency = val, - default = symbolic_evaluate(val, defs))) - return meta - end - return vcat(metas, pdep_metas) end """ @@ -2879,24 +2941,28 @@ Return an array of `NamedTuple`s containing the metadata associated with each un `sys`. Also includes the default value of the unknown, if provided. ```@example -using ModelingToolkit +using ModelingToolkitBase using DynamicQuantities -using ModelingToolkit: t, D +using ModelingToolkitBase: t, D @parameters p = 1.0, [description = "My parameter", tunable = false] q = 2.0, [description = "Other parameter"] @variables x(t) = 3.0 [unit = u"m"] @named sys = System(Equation[], t, [x], [p, q]) -ModelingToolkit.dump_unknowns(sys) +ModelingToolkitBase.dump_unknowns(sys) ``` -See also: [`ModelingToolkit.dump_variable_metadata`](@ref), [`ModelingToolkit.dump_parameters`](@ref) +See also: [`ModelingToolkitBase.dump_variable_metadata`](@ref), [`ModelingToolkitBase.dump_parameters`](@ref) """ function dump_unknowns(sys::AbstractSystem) - defs = add_toterms(defaults(sys)) + binds = add_toterms(bindings(sys)) + ics = add_toterms(initial_conditions(sys)) gs = add_toterms(guesses(sys)) map(dump_variable_metadata.(unknowns(sys))) do meta - if haskey(defs, meta.var) - meta = merge(meta, (; default = defs[meta.var])) + if haskey(binds, meta.var) + meta = merge(meta, (; binding = binds[meta.var])) + end + if haskey(ics, meta.var) + meta = merge(meta, (; initial_condition = ics[meta.var])) end if haskey(gs, meta.var) meta = merge(meta, (; guess = gs[meta.var])) @@ -3006,8 +3072,8 @@ differential term. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3034,8 +3100,8 @@ any differentials. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3057,8 +3123,8 @@ differentials). Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3077,8 +3143,8 @@ For a system, returns a vector of all its differential equations (i.e. that does Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3098,8 +3164,8 @@ differentials). Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3120,8 +3186,8 @@ For a system, returns true if it contain at least one differential equation (i.e Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3143,8 +3209,8 @@ differentials) in its *top-level system*. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3168,8 +3234,8 @@ in its *top-level system*. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3193,8 +3259,8 @@ differentials) in its *top-level system*. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3219,8 +3285,8 @@ equations in the top-level system. Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters p d @variables X(t) eq1 = D(X) ~ p - d*X @@ -3235,188 +3301,3 @@ has_diff_eqs(osys21) # returns `false`. ``` """ has_diff_eqs(sys::AbstractSystem) = any(is_diff_equation, get_eqs(sys)) - -""" - $(TYPEDSIGNATURES) - -Validate the rules for replacement of subcomponents as defined in `substitute_component`. -""" -function validate_replacement_rule( - rule::Pair{T, T}; namespace = []) where {T <: AbstractSystem} - lhs, rhs = rule - - iscomplete(lhs) && throw(ArgumentError("LHS of replacement rule cannot be completed.")) - iscomplete(rhs) && throw(ArgumentError("RHS of replacement rule cannot be completed.")) - - rhs_h = namespace_hierarchy(nameof(rhs)) - if length(rhs_h) != 1 - throw(ArgumentError("RHS of replacement rule must not be namespaced.")) - end - rhs_h[1] == namespace_hierarchy(nameof(lhs))[end] || - throw(ArgumentError("LHS and RHS must have the same name.")) - - if !isequal(get_iv(lhs), get_iv(rhs)) - throw(ArgumentError("LHS and RHS of replacement rule must have the same independent variable.")) - end - - lhs_u = get_unknowns(lhs) - rhs_u = Dict(get_unknowns(rhs) .=> nothing) - for u in lhs_u - if !haskey(rhs_u, u) - if isempty(namespace) - throw(ArgumentError("RHS of replacement rule does not contain unknown $u.")) - else - throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain unknown $u.")) - end - end - ru = getkey(rhs_u, u, nothing) - name = join([namespace; nameof(lhs); (hasname(u) ? getname(u) : Symbol(u))], - NAMESPACE_SEPARATOR) - l_connect = something(getconnect(u), Equality) - r_connect = something(getconnect(ru), Equality) - if l_connect != r_connect - throw(ArgumentError("Variable $(name) should have connection metadata $(l_connect),")) - end - - l_input = isinput(u) - r_input = isinput(ru) - if l_input != r_input - throw(ArgumentError("Variable $name has differing causality. Marked as `input = $l_input` in LHS and `input = $r_input` in RHS.")) - end - l_output = isoutput(u) - r_output = isoutput(ru) - if l_output != r_output - throw(ArgumentError("Variable $name has differing causality. Marked as `output = $l_output` in LHS and `output = $r_output` in RHS.")) - end - end - - lhs_p = get_ps(lhs) - rhs_p = Set(get_ps(rhs)) - for p in lhs_p - if !(p in rhs_p) - if isempty(namespace) - throw(ArgumentError("RHS of replacement rule does not contain parameter $p")) - else - throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain parameter $p.")) - end - end - end - - lhs_s = get_systems(lhs) - rhs_s = Dict(nameof(s) => s for s in get_systems(rhs)) - - for s in lhs_s - if haskey(rhs_s, nameof(s)) - rs = rhs_s[nameof(s)] - if isconnector(s) - name = join([namespace; nameof(lhs); nameof(s)], NAMESPACE_SEPARATOR) - if !isconnector(rs) - throw(ArgumentError("Subsystem $name of RHS is not a connector.")) - end - if (lct = get_connector_type(s)) !== (rct = get_connector_type(rs)) - throw(ArgumentError("Subsystem $name of RHS has connection type $rct but LHS has $lct.")) - end - end - validate_replacement_rule(s => rs; namespace = [namespace; nameof(rhs)]) - continue - end - name1 = join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR) - throw(ArgumentError("$name1 of replacement rule does not contain subsystem $(nameof(s)).")) - end -end - -""" - $(TYPEDSIGNATURES) - -Chain `getproperty` calls on `root` in the order given in `hierarchy`. - -# Keyword Arguments - -- `skip_namespace_first`: Whether to avoid namespacing in the first `getproperty` call. -""" -function recursive_getproperty( - root::AbstractSystem, hierarchy::Vector{Symbol}; skip_namespace_first = true) - cur = root - for (i, name) in enumerate(hierarchy) - cur = getproperty(cur, name; namespace = i > 1 || !skip_namespace_first) - end - return unwrap(cur) -end - -""" - $(TYPEDSIGNATURES) - -Recursively descend through `sys`, finding all connection equations and re-creating them -using the names of the involved variables/systems and finding the required variables/ -systems in the hierarchy. -""" -function recreate_connections(sys::AbstractSystem) - eqs = map(get_eqs(sys)) do eq - eq.lhs isa Union{Connection, AnalysisPoint} || return eq - if eq.lhs isa Connection - oldargs = get_systems(eq.rhs) - else - ap::AnalysisPoint = eq.rhs - oldargs = [ap.input; ap.outputs] - end - newargs = map(get_systems(eq.rhs)) do arg - rewrap_nameof = arg isa SymbolicWithNameof - if rewrap_nameof - arg = arg.var - end - name = arg isa AbstractSystem ? nameof(arg) : getname(arg) - hierarchy = namespace_hierarchy(name) - newarg = recursive_getproperty(sys, hierarchy) - if rewrap_nameof - newarg = SymbolicWithNameof(newarg) - end - return newarg - end - if eq.lhs isa Connection - return eq.lhs ~ Connection(newargs) - else - return eq.lhs ~ AnalysisPoint(newargs[1], eq.rhs.name, newargs[2:end]) - end - end - @set! sys.eqs = eqs - @set! sys.systems = map(recreate_connections, get_systems(sys)) - return sys -end - -""" - $(TYPEDSIGNATURES) - -Given a hierarchical system `sys` and a rule `lhs => rhs`, replace the subsystem `lhs` in -`sys` by `rhs`. The `lhs` must be the namespaced version of a subsystem of `sys` (e.g. -obtained via `sys.inner.component`). The `rhs` must be valid as per the following -conditions: - -1. `rhs` must not be namespaced. -2. The name of `rhs` must be the same as the unnamespaced name of `lhs`. -3. Neither one of `lhs` or `rhs` can be marked as complete. -4. Both `lhs` and `rhs` must share the same independent variable. -5. `rhs` must contain at least all of the unknowns and parameters present in - `lhs`. -6. Corresponding unknowns in `rhs` must share the same connection and causality - (input/output) metadata as their counterparts in `lhs`. -7. For each subsystem of `lhs`, there must be an identically named subsystem of `rhs`. - These two corresponding subsystems must satisfy conditions 3, 4, 5, 6, 7. If the - subsystem of `lhs` is a connector, the corresponding subsystem of `rhs` must also - be a connector of the same type. - -`sys` also cannot be marked as complete. -""" -function substitute_component(sys::T, rule::Pair{T, T}) where {T <: AbstractSystem} - iscomplete(sys) && - throw(ArgumentError("Cannot replace subsystems of completed systems")) - - validate_replacement_rule(rule) - - lhs, rhs = rule - hierarchy = namespace_hierarchy(nameof(lhs)) - - newsys, _ = modify_nested_subsystem(sys, hierarchy) do inner - return rhs, () - end - return recreate_connections(newsys) -end diff --git a/lib/ModelingToolkitBase/src/systems/analysis_points.jl b/lib/ModelingToolkitBase/src/systems/analysis_points.jl new file mode 100644 index 0000000000..9eb342996c --- /dev/null +++ b/lib/ModelingToolkitBase/src/systems/analysis_points.jl @@ -0,0 +1,888 @@ +""" + $(TYPEDEF) + AnalysisPoint(input, name::Symbol, outputs::Vector) + +Create an AnalysisPoint for linear analysis. Analysis points can be created by calling + +``` +connect(out, :ap_name, in...) +``` + +Where `out` is the output being connected to the inputs `in...`. All involved +connectors (input and outputs) are required to either have an unknown named +`u` or a single unknown, all of which should have the same size. + +See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref), [`open_loop`](@ref) + +# Fields + +$(TYPEDFIELDS) + +# Example + +```julia +using ModelingToolkitBase +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkitBase: t_nounits as t + +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) +t = ModelingToolkitBase.get_iv(P) + +eqs = [connect(P.output, C.input) + connect(C.output, :plant_input, P.input)] +sys = System(eqs, t, systems = [P, C], name = :feedback_system) + +matrices_S, _ = get_sensitivity(sys, :plant_input) # Compute the matrices of a state-space representation of the (input) sensitivity function. +matrices_T, _ = get_comp_sensitivity(sys, :plant_input) +``` + +Continued linear analysis and design can be performed using ControlSystemsBase.jl. +Create `ControlSystemsBase.StateSpace` objects using + +```julia +using ControlSystemsBase, Plots +S = ss(matrices_S...) +T = ss(matrices_T...) +bodeplot([S, T], lab = ["S" "T"]) +``` + +The sensitivity functions obtained this way should be equivalent to the ones obtained with the code below + +```julia +using ControlSystemsBase +P = tf(1.0, [1, 1]) +C = 1 # Negative feedback assumed in ControlSystems +S = sensitivity(P, C) # or feedback(1, P*C) +T = comp_sensitivity(P, C) # or feedback(P*C) +``` +""" +struct AnalysisPoint + """ + The input to the connection. In the context of ModelingToolkitStandardLibrary.jl, + this is a `RealOutput` connector. + """ + input::Any + """ + The name of the analysis point. + """ + name::Symbol + """ + The outputs of the connection. In the context of ModelingToolkitStandardLibrary.jl, + these are all `RealInput` connectors. + """ + outputs::Union{Nothing, Vector{System}, Vector{SymbolicT}} + + function AnalysisPoint(input, name::Symbol, outputs; verbose = true) + # input to analysis point should be an output variable + if verbose && input !== nothing + var = ap_var(input) + isoutput(var) || ap_warning(1, name, true) + end + # outputs of analysis points should be input variables + if verbose && outputs !== nothing + for (i, output) in enumerate(outputs) + var = ap_var(output) + isinput(var) || ap_warning(2 + i, name, false) + end + end + + return new(input, name, outputs) + end +end + +function ap_warning(arg::Int, name::Symbol, should_be_output) + causality = should_be_output ? "output" : "input" + @warn """ + The $(arg)-th argument to analysis point $(name) was not a $causality. This is supported in \ + order to handle inverse models, but may not be what you intended. + + If you are building a forward mode (causal), you may want to swap this argument with \ + one on the opposite side of the name of the analysis point provided to `connect`. \ + Learn more about the causality of analysis points in the docstring for `AnalysisPoint`. \ + Silence this message using `connect(out, :name, in...; warn = false)`. + """ +end + +AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing) +""" + $(TYPEDSIGNATURES) + +Create an `AnalysisPoint` with the given name, with no input or outputs specified. +""" +AnalysisPoint(name::Symbol) = AnalysisPoint(nothing, name, nothing) + +Base.nameof(ap::AnalysisPoint) = ap.name + +Base.show(io::IO, ap::AnalysisPoint) = show(io, MIME"text/plain"(), ap) +function Base.show(io::IO, ::MIME"text/plain", ap::AnalysisPoint) + Symbolics.warn_load_latexify() + if ap.input === nothing + print(io, "0") + return + end + if get(io, :compact, false) + print(io, + "AnalysisPoint($(ap_var(ap.input)), $(ap_var.(ap.outputs)); name=$(ap.name))") + else + print(io, "AnalysisPoint(") + printstyled(io, ap.name, color = :cyan) + if ap.input !== nothing && ap.outputs !== nothing + print(io, " from ") + printstyled(io, ap_var(ap.input), color = :green) + print(io, " to ") + if length(ap.outputs) == 1 + printstyled(io, ap_var(ap.outputs[1]), color = :blue) + else + printstyled(io, "[", join(ap_var.(ap.outputs), ", "), "]", color = :blue) + end + end + print(io, ")") + end +end + +Symbolics.hide_lhs(::AnalysisPoint) = true + +""" + $(TYPEDSIGNATURES) + +Convert an `AnalysisPoint` to a standard connection. +""" +function to_connection(ap::AnalysisPoint) + if ap.input isa System + vs = System[ap.input] + append!(vs, ap.outputs::Vector{System}) + return Connection() ~ Connection(vs) + elseif ap.input isa SymbolicT + vs = SymbolicT[ap.input] + append!(vs, ap.outputs::Vector{SymbolicT}) + return Connection() ~ Connection(vs) + else + error("Unreachable!") + end +end + +""" + $(TYPEDSIGNATURES) + +Namespace an `AnalysisPoint` by namespacing the involved systems and the name of the point. +""" +function renamespace(sys, ap::AnalysisPoint) + return AnalysisPoint( + ap.input === nothing ? nothing : renamespace(sys, ap.input), + renamespace(sys, ap.name), + ap.outputs === nothing ? nothing : map(Base.Fix1(renamespace, sys), ap.outputs) + ) +end + +# create analysis points via `connect` +function connect(in, ap::AnalysisPoint, outs...; verbose = true) + return AnalysisPoint() ~ AnalysisPoint(unwrap(in), ap.name, collect(unwrap.(outs)); verbose) +end + +""" + connect(output_connector, ap_name::Symbol, input_connector; verbose = true) + connect(output_connector, ap::AnalysisPoint, input_connector; verbose = true) + +Connect `output_connector` and `input_connector` with an [`AnalysisPoint`](@ref) inbetween. +The incoming connection `output_connector` is expected to be an output connector (for +example, `ModelingToolkitStandardLibrary.Blocks.RealOutput`), and vice versa. + +*PLEASE NOTE*: The connection is assumed to be *causal*, meaning that + +```julia +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) +connect(C.output, :plant_input, P.input) +``` + +is correct, whereas + +```julia +connect(P.input, :plant_input, C.output) +``` + +typically is not (unless the model is an inverse model). + +# Arguments + +- `output_connector`: An output connector +- `input_connector`: An input connector +- `ap`: An explicitly created [`AnalysisPoint`](@ref) +- `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be + created automatically. + +# Keyword arguments + +- `verbose`: Warn if an input is connected to an output (reverse causality). Silence this + warning if you are analyzing an inverse model. +""" +function connect(in::AbstractSystem, name::Symbol, out, outs...; verbose = true) + return AnalysisPoint() ~ AnalysisPoint(in, name, System[out; collect(outs)]; verbose) +end + +function connect( + in::ConnectableSymbolicT, name::Symbol, out::ConnectableSymbolicT, + outs::ConnectableSymbolicT...; verbose = true) + allvars = SymbolicT[] + push!(allvars, unwrap(in)) + push!(allvars, unwrap(out)) + for var in outs + push!(allvars, unwrap(var)) + end + validate_causal_variables_connection(allvars) + return AnalysisPoint() ~ AnalysisPoint( + allvars[1], name, allvars[2:end]; verbose) +end + +""" + $(TYPEDSIGNATURES) + +Return all the namespaces in `name`. Namespaces should be separated by `.` or +`$NAMESPACE_SEPARATOR`. +""" +function namespace_hierarchy(name::Symbol) + map( + Symbol, split(string(name), ('.', NAMESPACE_SEPARATOR))) +end + +""" + $(TYPEDSIGNATURES) + +Remove all `AnalysisPoint`s in `sys` and any of its subsystems, replacing them by equivalent connections. +""" +function remove_analysis_points(sys::AbstractSystem) + eqs = Equation[] + for eq in get_eqs(sys) + if unwrap_const(eq.lhs) isa AnalysisPoint + push!(eqs, to_connection(unwrap_const(eq.rhs)::AnalysisPoint)) + else + push!(eqs, eq) + end + end + @set! sys.eqs = eqs + @set! sys.systems = map(remove_analysis_points, get_systems(sys)) + + return sys +end + +""" + $(TYPEDSIGNATURES) + +Given a system involved in an `AnalysisPoint`, get the variable to be used in the +connection. This is the variable named `u` if present, and otherwise the only +variable in the system. If the system does not have a variable named `u` and +contains multiple variables, throw an error. +""" +function ap_var(sys::AbstractSystem) + if hasproperty(sys, :u) + return sys.u + end + x = unknowns(sys) + length(x) == 1 && return renamespace(sys, x[1]) + error("Could not determine the analysis-point variable in system $(nameof(sys)). To use an analysis point, apply it to a connection between causal blocks which have a variable named `u` or a single unknown of the same size.") +end + +""" + $(TYPEDSIGNATURES) + +For an `AnalysisPoint` involving causal variables. Simply return the variable. +""" +function ap_var(var::ConnectableSymbolicT) + return var +end + +""" + $(TYPEDEF) + +The supertype of all transformations that can be applied to an `AnalysisPoint`. All +concrete subtypes must implement `apply_transformation`. +""" +abstract type AnalysisPointTransformation end + +""" + apply_transformation(tf::AnalysisPointTransformation, sys::AbstractSystem) + +Apply the given analysis point transformation `tf` to the system `sys`. Throw an error if +any analysis points referred to in `tf` are not present in `sys`. Return a tuple +containing the modified system as the first element, and a tuple of the additional +variables added by the transformation as the second element. +""" +function apply_transformation end + +""" + $(TYPEDSIGNATURES) + +Given a namespaced subsystem `target` of root system `root`, return a modified copy of +`root` with `target` modified according to `fn` alongside any extra variables added +by `fn`. + +`fn` is a function which takes the instance of `target` present in the hierarchy of +`root`, and returns a 2-tuple consisting of the modified version of `target` and a tuple +of the extra variables added. +""" +function modify_nested_subsystem(fn, root::AbstractSystem, target::AbstractSystem) + modify_nested_subsystem( + fn, root, nameof(target)) +end +""" + $(TYPEDSIGNATURES) + +Apply the modification to the system containing the namespaced analysis point `target`. +""" +function modify_nested_subsystem(fn, root::AbstractSystem, target::AnalysisPoint) + modify_nested_subsystem( + fn, root, @view namespace_hierarchy(nameof(target))[1:(end - 1)]) +end +""" + $(TYPEDSIGNATURES) + +Apply the modification to the nested subsystem of `root` whose namespaced name matches +the provided name `target`. The namespace separator in `target` should be `.` or +`$NAMESPACE_SEPARATOR`. The `target` may include `nameof(root)` as the first namespace. +""" +function modify_nested_subsystem(fn, root::AbstractSystem, target::Symbol) + modify_nested_subsystem( + fn, root, namespace_hierarchy(target)) +end + +""" + $(TYPEDSIGNATURES) + +Apply the modification to the nested subsystem of `root` where the name of the subsystem at +each level in the hierarchy is given by elements of `hierarchy`. For example, if +`hierarchy = [:a, :b, :c]`, the system being searched for will be `root.a.b.c`. Note that +the hierarchy may include the name of the root system, in which the first element will be +ignored. For example, `hierarchy = [:root, :a, :b, :c]` also searches for `root.a.b.c`. +An empty `hierarchy` will apply the modification to `root`. +""" +function modify_nested_subsystem( + fn, root::AbstractSystem, hierarchy::AbstractVector{Symbol}) + # no hierarchy, so just apply to the root + if isempty(hierarchy) + return fn(root) + end + # ignore the name of the root + if nameof(root) != hierarchy[1] + error(""" + Invalid analysis point name `$(join(hierarchy, NAMESPACE_SEPARATOR))`. The name + must include the name of the root system `$(nameof(root))`. This typically happens + when using an analysis point obtained by calling `getproperty` on a system marked + as `complete` to linearize a system that is not marked as `complete`. + """) + end + hierarchy = @view hierarchy[2:end] + + # recursive helper function which does the searching and modification + function _helper(sys::AbstractSystem, i::Int) + if i > length(hierarchy) + # we reached past the end, so everything matched and + # `sys` is the system to modify. + sys, vars = fn(sys) + else + # find the subsystem with the given name and error otherwise + cur = hierarchy[i] + idx = findfirst(subsys -> nameof(subsys) == cur, get_systems(sys)) + idx === nothing && + error("System $(join([nameof(root); hierarchy[1:i-1]], '.')) does not have a subsystem named $cur.") + + # recurse into new subsystem + newsys, vars = _helper(get_systems(sys)[idx], i + 1) + # update this system with modified subsystem + @set! sys.systems[idx] = newsys + end + # only namespace variables from inner systems + if i != 1 + vars = ntuple(Val(length(vars))) do i + renamespace(sys, vars[i]) + end + end + return sys, vars + end + + return _helper(root, 1) +end + +""" + $(TYPEDSIGNATURES) + +Given a system `sys` and analysis point `ap`, return the index in `get_eqs(sys)` +containing an equation which has as it's RHS an analysis point with name `nameof(ap)`. +""" +function analysis_point_index(sys::AbstractSystem, ap::AnalysisPoint) + analysis_point_index( + sys, nameof(ap)) +end +""" + $(TYPEDSIGNATURES) + +Search for the analysis point with the given `name` in `get_eqs(sys)`. +""" +function analysis_point_index(sys::AbstractSystem, name::Symbol) + name = namespace_hierarchy(name)[end] + findfirst(get_eqs(sys)) do eq + value(eq.lhs) isa AnalysisPoint && nameof(value(eq.rhs)::AnalysisPoint) == name + end +end + +""" + $(TYPEDSIGNATURES) + +Create a new variable of the same `symtype` and size as `var`, using `name` as the base +name for the new variable. `iv` denotes the independent variable of the system. Prefix +`d_` to the name of the new variable if `perturb == true`. Return the new symbolic +variable and the appropriate zero value for it. +""" +function get_analysis_variable(var, name, iv; perturb = true) + var = unwrap(var) + if perturb + name = Symbol(:d_, name) + end + if symbolic_type(var) == ArraySymbolic() + T = eltype(symtype(var)) + pvar = unwrap(only(@variables $name(iv)[SU.shape(var)...]::T)) + default = zeros(eltype(symtype(var)), size(var)) + else + T = symtype(var) + pvar = unwrap(only(@variables $name(iv)::T)) + default = zero(T) + end + return pvar, default +end + +function with_analysis_point_ignored(sys::AbstractSystem, ap::AnalysisPoint) + has_ignored_connections(sys) || return sys + ignored = get_ignored_connections(sys) + if ignored === nothing + ignored = Connection[] + else + ignored = copy(ignored) + end + if ap.outputs === nothing + error("Empty analysis point") + end + + push!(ignored, Connection([unwrap(ap.input); unwrap.(ap.outputs)])) + + return @set sys.ignored_connections = ignored +end + +#### PRIMITIVE TRANSFORMATIONS + +const DOC_WILL_REMOVE_AP = """ + Note that this transformation will remove `ap`, causing any subsequent transformations \ + referring to it to fail.\ + """ + +const DOC_ADDED_VARIABLE = """ + The added variable(s) will have a default of zero, of the appropriate type and size.\ + """ + +""" + $(TYPEDEF) + +A transformation which breaks the connection referred to by `ap`. If `add_input == true`, +it will add a new input variable which connects to the outputs of the analysis point. +`apply_transformation` returns the new input variable (if added) as the auxiliary +information. The new input variable will have the name `Symbol(:d_, nameof(ap))`. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE + +## Fields + +$(TYPEDFIELDS) +""" +struct Break <: AnalysisPointTransformation + """ + The analysis point to break. + """ + ap::AnalysisPoint + """ + Whether to add a new input variable connected to all the outputs of `ap`. + """ + add_input::Bool + """ + Whether the initial condition of the added input variable should be the input of `ap`. + Only applicable if `add_input == true`. + """ + default_outputs_to_input::Bool + """ + Whether the added input is a parameter. Only applicable if `add_input == true`. + """ + added_input_is_param::Bool +end + +""" + $(TYPEDSIGNATURES) + +`Break` the given analysis point `ap`. +""" +function Break(ap::AnalysisPoint, add_input::Bool = false, default_outputs_to_input = false) + Break(ap, add_input, default_outputs_to_input, false) +end + +function apply_transformation(tf::Break, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do breaksys + # get analysis point + ap_idx = analysis_point_index(breaksys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + breaksys_eqs = copy(get_eqs(breaksys)) + @set! breaksys.eqs = breaksys_eqs + + ap = value(breaksys_eqs[ap_idx].rhs)::AnalysisPoint + deleteat!(breaksys_eqs, ap_idx) + + breaksys = with_analysis_point_ignored(breaksys, ap) + + tf.add_input || return breaksys, () + + ap_ivar = ap_var(ap.input) + new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) + for outsys in ap.outputs + push!(breaksys_eqs, ap_var(outsys) ~ new_var) + end + defs = copy(get_initial_conditions(breaksys)) + defs[new_var] = if tf.default_outputs_to_input + ap_ivar + else + new_def + end + @set! breaksys.initial_conditions = defs + if tf.added_input_is_param + ps = copy(get_ps(breaksys)) + push!(ps, new_var) + @set! breaksys.ps = ps + else + unks = copy(get_unknowns(breaksys)) + push!(unks, new_var) + @set! breaksys.unknowns = unks + end + + return breaksys, (new_var,) + end +end + +""" + $(TYPEDEF) + +A transformation which returns the variable corresponding to the input of the analysis +point. Does not modify the system. + +## Fields + +$(TYPEDFIELDS) +""" +struct GetInput <: AnalysisPointTransformation + """ + The analysis point to get the input of. + """ + ap::AnalysisPoint +end + +function apply_transformation(tf::GetInput, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get the analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + # get the analysis point + ap_sys_eqs = get_eqs(ap_sys) + ap = value(ap_sys_eqs[ap_idx].rhs)::AnalysisPoint + + # input variable + ap_ivar = ap_var(ap.input) + return ap_sys, (ap_ivar,) + end +end + +""" + $(TYPEDEF) + +A transformation that creates a new input variable which is added to the input of +the analysis point before connecting to the outputs. The new variable will have the name +`Symbol(:d_, nameof(ap))`. + +If `with_output == true`, also creates an additional new variable which has the value +provided to the outputs after the above modification. This new variable has the same name +as the analysis point and will be the second variable in the tuple of new variables returned +from `apply_transformation`. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE + +## Fields + +$(TYPEDFIELDS) +""" +struct PerturbOutput <: AnalysisPointTransformation + """ + The analysis point to modify + """ + ap::AnalysisPoint + """ + Whether to add an additional output variable. + """ + with_output::Bool +end + +""" + $(TYPEDSIGNATURES) + +Add an input without an additional output variable. +""" +PerturbOutput(ap::AnalysisPoint) = PerturbOutput(ap, false) + +function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + # modified equations + ap_sys_eqs = copy(get_eqs(ap_sys)) + @set! ap_sys.eqs = ap_sys_eqs + ap = value(ap_sys_eqs[ap_idx].rhs)::AnalysisPoint + # remove analysis point + deleteat!(ap_sys_eqs, ap_idx) + ap_sys = with_analysis_point_ignored(ap_sys, ap) + + # add equations involving new variable + ap_ivar = ap_var(ap.input) + new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) + for outsys in ap.outputs + push!(ap_sys_eqs, ap_var(outsys) ~ ap_ivar + wrap(new_var)) + end + # add variable + unks = copy(get_unknowns(ap_sys)) + push!(unks, new_var) + @set! ap_sys.unknowns = unks + # add default + defs = copy(get_initial_conditions(ap_sys)) + defs[new_var] = new_def + @set! ap_sys.initial_conditions = defs + + tf.with_output || return ap_sys, (new_var,) + + # add output variable, equation, default + out_var, + out_def = get_analysis_variable( + ap_ivar, nameof(ap), get_iv(sys); perturb = false) + push!(ap_sys_eqs, out_var ~ ap_ivar + wrap(new_var)) + push!(unks, out_var) + + return ap_sys, (new_var, out_var) + end +end + +""" + $(TYPEDEF) + +A transformation which adds a variable named `name` to the system containing the analysis +point `ap`. $DOC_ADDED_VARIABLE + +# Fields + +$(TYPEDFIELDS) +""" +struct AddVariable <: AnalysisPointTransformation + """ + The analysis point in the system to modify, and whose input should be used as the + template for the new variable. + """ + ap::AnalysisPoint + """ + The name of the added variable. + """ + name::Symbol +end + +""" + $(TYPEDSIGNATURES) + +Add a new variable to the system containing analysis point `ap` with the same name as the +analysis point. +""" +AddVariable(ap::AnalysisPoint) = AddVariable(ap, nameof(ap)) + +function apply_transformation(tf::AddVariable, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + ap_sys_eqs = get_eqs(ap_sys) + ap = value(ap_sys_eqs[ap_idx].rhs)::AnalysisPoint + + # add equations involving new variable + ap_ivar = ap_var(ap.input) + new_var, + new_def = get_analysis_variable( + ap_ivar, tf.name, get_iv(sys); perturb = false) + # add variable + unks = copy(get_unknowns(ap_sys)) + push!(unks, new_var) + @set! ap_sys.unknowns = unks + return ap_sys, (new_var,) + end +end + +#### DERIVED TRANSFORMATIONS + +""" + $(TYPEDSIGNATURES) + +A transformation enable calculating the sensitivity function about the analysis point `ap`. +The returned added variables are `(du, u)` where `du` is the perturbation added to the +input, and `u` is the output after perturbation. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE +""" +SensitivityTransform(ap::AnalysisPoint) = PerturbOutput(ap, true) + +""" + $(TYPEDEF) + +A transformation to enable calculating the complementary sensitivity function about the +analysis point `ap`. The returned added variables are `(du, u)` where `du` is the +perturbation added to the outputs and `u` is the input to the analysis point. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE + +# Fields + +$(TYPEDFIELDS) +""" +struct ComplementarySensitivityTransform <: AnalysisPointTransformation + """ + The analysis point to modify. + """ + ap::AnalysisPoint +end + +function apply_transformation(cst::ComplementarySensitivityTransform, sys::AbstractSystem) + sys, (u,) = apply_transformation(GetInput(cst.ap), sys) + sys, + (du,) = apply_transformation( + AddVariable( + cst.ap, Symbol(namespace_hierarchy(nameof(cst.ap))[end], :_comp_sens_du)), + sys) + sys, (_du,) = apply_transformation(PerturbOutput(cst.ap), sys) + + # `PerturbOutput` adds the equation `input + _du ~ output` + # but comp sensitivity wants `output + du ~ input`. Thus, `du ~ -_du`. + eqs = copy(get_eqs(sys)) + @set! sys.eqs = eqs + push!(eqs, du ~ -wrap(_du)) + + defs = copy(get_initial_conditions(sys)) + @set! sys.initial_conditions = defs + defs[du] = -wrap(_du) + return sys, (du, u) +end + +""" + $(TYPEDEF) + +A transformation to enable calculating the loop transfer function about the analysis point +`ap`. The returned added variables are `(du, u)` where `du` feeds into the outputs of `ap` +and `u` is the input of `ap`. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE + +# Fields + +$(TYPEDFIELDS) +""" +struct LoopTransferTransform <: AnalysisPointTransformation + """ + The analysis point to modify. + """ + ap::AnalysisPoint +end + +function apply_transformation(tf::LoopTransferTransform, sys::AbstractSystem) + sys, (u,) = apply_transformation(GetInput(tf.ap), sys) + sys, (du,) = apply_transformation(Break(tf.ap, true), sys) + return sys, (du, u) +end + +""" + $(TYPEDSIGNATURES) + +A utility function to get the "canonical" form of a list of analysis points. Always returns +a list of values. Any value that cannot be turned into an `AnalysisPoint` (i.e. isn't +already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. `Symbol` names of +`AnalysisPoint`s are namespaced with `sys`. +""" +canonicalize_ap(sys::AbstractSystem, ap::Symbol) = [AnalysisPoint(renamespace(sys, ap))] +function canonicalize_ap(sys::AbstractSystem, ap::AnalysisPoint) + if does_namespacing(sys) + return [ap] + else + return [renamespace(sys, ap)] + end +end +canonicalize_ap(sys::AbstractSystem, ap) = [ap] +function canonicalize_ap(sys::AbstractSystem, aps::Vector) + mapreduce(Base.Fix1(canonicalize_ap, sys), vcat, aps; init = []) +end + +""" + $(TYPEDSIGNATURES) + +Apply `LoopTransferTransform` to the analysis point `ap` and return the +result of `apply_transformation`. + +# Keyword Arguments + +- `system_modifier`: a function which takes the modified system and returns a new system + with any required further modifications performed. +""" +function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = identity) + ap = only(canonicalize_ap(sys, ap)) + tf = LoopTransferTransform(ap) + sys, vars = apply_transformation(tf, sys) + return system_modifier(sys), vars +end + +""" + generate_control_function(sys::ModelingToolkitBase.AbstractSystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) + +When called with analysis points as input arguments, we assume that all analysis points corresponds to connections that should be opened (broken). The use case for this is to get rid of input signal blocks, such as `Step` or `Sine`, since these are useful for simulation but are not needed when using the plant model in a controller or state estimator. +""" +function generate_control_function( + sys::ModelingToolkitBase.AbstractSystem, input_ap_name::Union{ + Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, + dist_ap_name::Union{ + Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}} = nothing; + system_modifier = identity, + kwargs...) + input_ap_name = canonicalize_ap(sys, input_ap_name) + u = [] + for input_ap in input_ap_name + sys, (du, _) = open_loop(sys, input_ap) + push!(u, du) + end + if dist_ap_name === nothing + return ModelingToolkitBase.generate_control_function(system_modifier(sys), u; kwargs...) + end + + dist_ap_name = canonicalize_ap(sys, dist_ap_name) + d = [] + for dist_ap in dist_ap_name + sys, (du, _) = open_loop(sys, dist_ap) + push!(d, du) + end + + ModelingToolkitBase.generate_control_function(system_modifier(sys), u, d; kwargs...) +end diff --git a/src/systems/callbacks.jl b/lib/ModelingToolkitBase/src/systems/callbacks.jl similarity index 88% rename from src/systems/callbacks.jl rename to lib/ModelingToolkitBase/src/systems/callbacks.jl index c6ce34123e..68a3e18256 100644 --- a/src/systems/callbacks.jl +++ b/lib/ModelingToolkitBase/src/systems/callbacks.jl @@ -7,15 +7,19 @@ end struct SymbolicAffect affect::Vector{Equation} alg_eqs::Vector{Equation} - discrete_parameters::Vector{Any} + discrete_parameters::Vector{SymbolicT} end function SymbolicAffect(affect::Vector{Equation}; alg_eqs = Equation[], - discrete_parameters = Any[], kwargs...) - if !(discrete_parameters isa AbstractVector) - discrete_parameters = Any[discrete_parameters] - elseif !(discrete_parameters isa Vector{Any}) - discrete_parameters = Vector{Any}(discrete_parameters) + discrete_parameters = SymbolicT[], kwargs...) + if symbolic_type(discrete_parameters) !== NotSymbolic() + discrete_parameters = SymbolicT[unwrap(discrete_parameters)] + elseif !(discrete_parameters isa Vector{SymbolicT}) + _discs = SymbolicT[] + for p in discrete_parameters + push!(_discs, unwrap(p)) + end + discrete_parameters = _discs end SymbolicAffect(affect, alg_eqs, discrete_parameters) end @@ -25,34 +29,33 @@ function SymbolicAffect(affect::SymbolicAffect; kwargs...) end SymbolicAffect(affect; kwargs...) = make_affect(affect; kwargs...) -function Symbolics.fast_substitute(aff::SymbolicAffect, rules) - substituter = Base.Fix2(fast_substitute, rules) - SymbolicAffect(map(substituter, aff.affect), map(substituter, aff.alg_eqs), - map(substituter, aff.discrete_parameters)) +function (s::SymbolicUtils.Substituter)(aff::SymbolicAffect) + SymbolicAffect(s(aff.affect), s(aff.alg_eqs), s(aff.discrete_parameters)) end +discretes(affect::SymbolicAffect) = affect.discrete_parameters + struct AffectSystem """The internal implicit discrete system whose equations are solved to obtain values after the affect.""" system::AbstractSystem """Unknowns of the parent ODESystem whose values are modified or accessed by the affect.""" - unknowns::Vector + unknowns::Vector{SymbolicT} """Parameters of the parent ODESystem whose values are accessed by the affect.""" - parameters::Vector + parameters::Vector{SymbolicT} """Parameters of the parent ODESystem whose values are modified by the affect.""" - discretes::Vector + discretes::Vector{SymbolicT} end -function Symbolics.fast_substitute(aff::AffectSystem, rules) - substituter = Base.Fix2(fast_substitute, rules) +function (s::SymbolicUtils.Substituter)(aff::AffectSystem) sys = aff.system - @set! sys.eqs = map(substituter, get_eqs(sys)) - @set! sys.parameter_dependencies = map(substituter, get_parameter_dependencies(sys)) - @set! sys.defaults = Dict([k => substituter(v) for (k, v) in defaults(sys)]) - @set! sys.guesses = Dict([k => substituter(v) for (k, v) in guesses(sys)]) - @set! sys.unknowns = map(substituter, get_unknowns(sys)) - @set! sys.ps = map(substituter, get_ps(sys)) - AffectSystem(sys, map(substituter, aff.unknowns), - map(substituter, aff.parameters), map(substituter, aff.discretes)) + @set! sys.eqs = s(get_eqs(sys)) + @set! sys.parameter_dependencies = (get_parameter_dependencies(sys)) + @set! sys.defaults = Dict([k => s(v) for (k, v) in defaults(sys)]) + @set! sys.guesses = Dict([k => s(v) for (k, v) in guesses(sys)]) + @set! sys.unknowns = s(get_unknowns(sys)) + @set! sys.ps = s(get_ps(sys)) + AffectSystem(sys, s(aff.unknowns), s(aff.parameters), s(aff.discretes)) + end function AffectSystem(spec::SymbolicAffect; iv = nothing, alg_eqs = Equation[], kwargs...) @@ -60,7 +63,11 @@ function AffectSystem(spec::SymbolicAffect; iv = nothing, alg_eqs = Equation[], discrete_parameters = spec.discrete_parameters, kwargs...) end -function AffectSystem(affect::Vector{Equation}; discrete_parameters = Any[], +@noinline function warn_algebraic_equation(eq::Equation) + @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x). Errors may be thrown if there is no `Pre` and the algebraic equation is unsatisfiable, such as X ~ X + 1." +end + +function AffectSystem(affect::Vector{Equation}; discrete_parameters = SymbolicT[], iv = nothing, alg_eqs::Vector{Equation} = Equation[], warn_no_algebraic = true, kwargs...) isempty(affect) && return nothing if isnothing(iv) @@ -68,26 +75,24 @@ function AffectSystem(affect::Vector{Equation}; discrete_parameters = Any[], @warn "No independent variable specified. Defaulting to t_nounits." end - discrete_parameters isa AbstractVector || (discrete_parameters = [discrete_parameters]) - discrete_parameters = unwrap.(discrete_parameters) + discrete_parameters = SymbolicAffect(affect; alg_eqs, discrete_parameters).discrete_parameters for p in discrete_parameters - occursin(unwrap(iv), unwrap(p)) || + SU.query(isequal(unwrap(iv)), unwrap(p)) || error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") end - dvs = OrderedSet() - params = OrderedSet() - _varsbuf = Set() + dvs = OrderedSet{SymbolicT}() + params = OrderedSet{SymbolicT}() + _varsbuf = Set{SymbolicT}() for eq in affect - if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic() || - symbolic_type(eq.lhs) === NotSymbolic()) - @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x). Errors may be thrown if there is no `Pre` and the algebraic equation is unsatisfiable, such as X ~ X + 1." + if !haspre(eq) && !(isconst(eq.lhs) && isconst(eq.rhs)) + @invokelatest warn_algebraic_equation(eq) end collect_vars!(dvs, params, eq, iv; op = Pre) empty!(_varsbuf) - vars!(_varsbuf, eq; op = Pre) - filter!(x -> iscall(x) && operation(x) isa Pre, _varsbuf) + SU.search_variables!(_varsbuf, eq; is_atomic = OperatorIsAtomic{Pre}()) + filter!(x -> iscall(x) && operation(x) === Pre(), _varsbuf) union!(params, _varsbuf) diffvs = collect_applied_operators(eq, Differential) union!(dvs, diffvs) @@ -95,34 +100,47 @@ function AffectSystem(affect::Vector{Equation}; discrete_parameters = Any[], for eq in alg_eqs collect_vars!(dvs, params, eq, iv) end - pre_params = filter(haspre ∘ value, params) - discrete_parameters = gather_array_params(OrderedSet(discrete_parameters)) - sys_params = collect(setdiff(params, union(discrete_parameters, pre_params))) - discrete_parameters = collect(discrete_parameters) + pre_params = filter(haspre, params) + sys_params = SymbolicT[] + disc_ps_set = Set{SymbolicT}(discrete_parameters) + disc_ps_set = gather_array_params(disc_ps_set) + discrete_parameters = collect(disc_ps_set) + for p in params + p in disc_ps_set && continue + p in pre_params && continue + push!(sys_params, p) + end discretes = map(tovar, discrete_parameters) dvs = collect(dvs) _dvs = map(default_toterm, dvs) - rev_map = Dict(zip(discrete_parameters, discretes)) - subs = merge(rev_map, Dict(zip(dvs, _dvs))) - affect = Symbolics.fast_substitute(affect, subs) - alg_eqs = Symbolics.fast_substitute(alg_eqs, subs) + rev_map = Dict{SymbolicT, SymbolicT}(zip(discrete_parameters, discretes)) + subs = merge(rev_map, Dict{SymbolicT, SymbolicT}(zip(dvs, _dvs))) + affect = substitute(affect, subs) + alg_eqs = substitute(alg_eqs, subs) @named affectsys = System( vcat(affect, alg_eqs), iv, collect(union(_dvs, discretes)), collect(union(pre_params, sys_params)); is_discrete = true) - affectsys = mtkcompile(affectsys; fully_determined = nothing) + # This `@invokelatest` should not be necessary, but it works around the inference bug + # in https://github.com/JuliaLang/julia/issues/59943. Remove it at your own risk, the + # bug took weeks to reduce to an MWE. + affectsys = @invokelatest mtkcompile(affectsys; fully_determined = nothing) # get accessed parameters p from Pre(p) in the callback parameters - accessed_params = Vector{Any}(filter(isparameter, map(unPre, collect(pre_params)))) + accessed_params = Vector{SymbolicT}(filter(isparameter, map(unPre, collect(pre_params)))) union!(accessed_params, sys_params) # add scalarized unknowns to the map. - _dvs = reduce(vcat, map(scalarize, _dvs), init = Any[]) - - AffectSystem(affectsys, collect(_dvs), collect(accessed_params), - collect(discrete_parameters)) + _obs, _ = unhack_observed(observed(affectsys), equations(affectsys)) + _dvs = vcat(unknowns(affectsys), map(eq -> eq.lhs, _obs)) + _dvs = reduce(vcat, map(safe_vec ∘ scalarize, _dvs), init = SymbolicT[]) + _discs = reduce(vcat, map(safe_vec ∘ scalarize, discretes); init = SymbolicT[]) + setdiff!(_dvs, _discs) + AffectSystem(affectsys, _dvs, accessed_params, discrete_parameters) end +safe_vec(@nospecialize(x)) = x isa SymbolicT ? [x] : vec(x::Array{SymbolicT}) + system(a::AffectSystem) = a.system discretes(a::AffectSystem) = a.discretes unknowns(a::AffectSystem) = a.unknowns @@ -149,11 +167,10 @@ function Base.hash(a::AffectSystem, s::UInt) hash(discretes(a), s) end -function vars!(vars, aff::AffectSystem; op = Differential) - for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) - vars!(vars, var) - end - vars +function SU.search_variables!(vars, aff::AffectSystem; kwargs...) + SU.search_variables!(vars, unknowns(aff); kwargs...) + SU.search_variables!(vars, parameters(aff); kwargs...) + SU.search_variables!(vars, discretes(aff); kwargs...) end """ @@ -164,19 +181,20 @@ before the callback is triggered. """ struct Pre <: Symbolics.Operator end Pre(x) = Pre()(x) -is_timevarying_operator(::Type{Pre}) = false SymbolicUtils.promote_symtype(::Type{Pre}, T) = T SymbolicUtils.isbinop(::Pre) = false Base.nameof(::Pre) = :Pre Base.show(io::IO, x::Pre) = print(io, "Pre") unPre(x::Num) = unPre(unwrap(x)) unPre(x::Symbolics.Arr) = unPre(unwrap(x)) -unPre(x::Symbolic) = (iscall(x) && operation(x) isa Pre) ? only(arguments(x)) : x +unPre(x::SymbolicT) = (iscall(x) && operation(x) isa Pre) ? only(arguments(x)) : x +distribute_shift_into_operator(::Pre) = false function (p::Pre)(x) iw = Symbolics.iswrapped(x) x = unwrap(x) # non-symbolic values don't change + SU.isconst(x) && return x if symbolic_type(x) == NotSymbolic() return x end @@ -186,16 +204,13 @@ function (p::Pre)(x) end # don't double wrap iscall(x) && operation(x) isa Pre && return x - result = if symbolic_type(x) == ArraySymbolic() - # create an array for `Pre(array)` - Symbolics.array_term(p, x) - elseif iscall(x) && operation(x) == getindex + result = if iscall(x) && operation(x) === getindex # instead of `Pre(x[1])` create `Pre(x)[1]` # which allows parameter indexing to handle this case automatically. arr = arguments(x)[1] - term(getindex, p(arr), arguments(x)[2:end]...) + p(arr)[arguments(x)[2:end]...] else - term(p, x) + term(p, x; type = symtype(x), shape = SU.shape(x)) end # the result should be a parameter result = toparam(result) @@ -393,21 +408,17 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::AbstractCallback) end end -function vars!(vars, cb::AbstractCallback; op = Differential) - if symbolic_type(conditions(cb)) == NotSymbolic - if conditions(cb) isa AbstractArray - for eq in conditions(cb) - vars!(vars, eq; op) - end - end - else - vars!(vars, conditions(cb); op) +function SU.search_variables!(vars, cb::AbstractCallback; kwargs...) + if symbolic_type(conditions(cb)) isa NotSymbolic + SU.search_variables!(vars, conditions(cb); kwargs...) end - for aff in (affects(cb), initialize_affects(cb), finalize_affects(cb)) - isnothing(aff) || vars!(vars, aff; op) - end - !is_discrete(cb) && vars!(vars, affect_negs(cb); op) - return vars + affs = affects(cb) + affs === nothing || SU.search_variables!(vars, affs; kwargs...) + affs = initialize_affects(cb) + affs === nothing || SU.search_variables!(vars, affs; kwargs...) + affs = finalize_affects(cb) + affs === nothing || SU.search_variables!(vars, affs; kwargs...) + is_discrete(cb) || SU.search_variables!(vars, affect_negs(cb); kwargs...) end ################################ @@ -422,14 +433,14 @@ A callback that triggers at the first timestep that the conditions are satisfied The condition can be one of: - Δt::Real - periodic events with period Δt - ts::Vector{Real} - events trigger at these preset times given by `ts` -- eqs::Vector{Symbolic} - events trigger when the condition evaluates to true +- eqs::Vector{SymbolicT} - events trigger when the condition evaluates to true Arguments: - iv: The independent variable of the system. This must be specified if the independent variable appears in one of the equations explicitly, as in x ~ t + 1. - alg_eqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Union{Number, Vector{<:Number}, Symbolic{Bool}} + conditions::Union{Number, Vector{<:Number}, SymbolicT} affect::Union{Affect, SymbolicAffect, Nothing} initialize::Union{Affect, SymbolicAffect, Nothing} finalize::Union{Affect, SymbolicAffect, Nothing} @@ -437,12 +448,14 @@ struct SymbolicDiscreteCallback <: AbstractCallback end function SymbolicDiscreteCallback( - condition::Union{Symbolic{Bool}, Number, Vector{<:Number}}, affect = nothing; + condition::Union{SymbolicT, Number, Vector{<:Number}}, affect = nothing; initialize = nothing, finalize = nothing, reinitializealg = nothing, kwargs...) # Manual error check (to prevent events like `[X < 5.0] => [X ~ Pre(X) + 10.0]` from being created). (condition isa Vector) && (eltype(condition) <: Num) && error("Vectors of symbolic conditions are not allowed for `SymbolicDiscreteCallback`.") + @assert !(condition isa SymbolicT && symtype(condition) != Bool) + c = is_timed_condition(condition) ? condition : value(scalarize(condition)) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) if isnothing(reinitializealg) @@ -574,6 +587,9 @@ conditions(cb::AbstractCallback) = cb.conditions function conditions(cbs::Vector{<:AbstractCallback}) reduce(vcat, conditions(cb) for cb in cbs; init = []) end +function conditions(cbs::Vector{SymbolicContinuousCallback}) + mapreduce(conditions, vcat, cbs; init = Equation[]) +end equations(cb::AbstractCallback) = conditions(cb) equations(cb::Vector{<:AbstractCallback}) = conditions(cb) @@ -876,7 +892,7 @@ function default_operating_point(affsys::AffectSystem) T = symtype(p) if T <: Number op[p] = false - elseif T <: Array{<:Real} && is_sized_array_symbolic(p) + elseif T <: Array{<:Real} && symbolic_has_known_size(p) op[p] = zeros(size(p)) end end @@ -902,7 +918,7 @@ function compile_equational_affect( obseqs, eqs = unhack_observed(observed(affsys), equations(affsys)) if isempty(equations(affsys)) - update_eqs = Symbolics.fast_substitute( + update_eqs = substitute( obseqs, Dict([p => unPre(p) for p in parameters(affsys)])) rhss = map(x -> x.rhs, update_eqs) lhss = map(x -> x.lhs, update_eqs) diff --git a/lib/ModelingToolkitBase/src/systems/codegen.jl b/lib/ModelingToolkitBase/src/systems/codegen.jl new file mode 100644 index 0000000000..ba911863f2 --- /dev/null +++ b/lib/ModelingToolkitBase/src/systems/codegen.jl @@ -0,0 +1,1339 @@ +const GENERATE_X_KWARGS = """ +- `expression`: `Val{true}` if this should return an `Expr` (or tuple of `Expr`s) of the + generated code. `Val{false}` otherwise. +- `wrap_gfw`: `Val{true}` if the returned functions should be wrapped in a callable + struct to make them callable using the expected syntax. The callable struct itself is + internal API. If `expression == Val{true}`, the returned expression will construct the + callable struct. If this function returns a tuple of functions/expressions, both will + be identical if `wrap_gfw == Val{true}`. +$EVAL_EXPR_MOD_KWARGS +""" + +const EXPERIMENTAL_WARNING = """ +!!! warn + + This API is experimental and may change in a future non-breaking release. +""" + +""" + $(TYPEDSIGNATURES) + +Generate the RHS function for the [`equations`](@ref) of a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `implicit_dae`: Whether the generated function should be in the implicit form. Applicable + only for ODEs/DAEs or discrete systems. Instead of `f(u, p, t)` (`f(du, u, p, t)` for the + in-place form) the function is `f(du, u, p, t)` (respectively `f(resid, du, u, p, t)`). +- `override_discrete`: Whether to assume the system is discrete regardless of + `is_discrete_system(sys)`. +- `scalar`: Whether to generate a single-out-of-place function that returns a scalar for + the only equation in the system. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_rhs(sys::System; implicit_dae = false, + scalar = false, expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, override_discrete = false, + kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + eqs = equations(sys) + obs = observed(sys) + u = dvs + p = reorder_parameters(sys, ps) + t = get_iv(sys) + ddvs = nothing + extra_assignments = Assignment[] + + # used for DAEProblem and ImplicitDiscreteProblem + if implicit_dae + if override_discrete || is_discrete_system(sys) + # ImplicitDiscrete case + D = Shift(t, 1) + rhss = map(eqs) do eq + # Algebraic equations get shifted forward 1, to match with differential + # equations + _iszero(eq.lhs) ? distribute_shift(D(eq.rhs)) : (eq.rhs - eq.lhs) + end + # Handle observables in algebraic equations, since they are shifted + shifted_obs = Equation[distribute_shift(D(eq)) for eq in obs] + obsidxs = observed_equations_used_by(sys, rhss; obs = shifted_obs) + ddvs = map(D, dvs) + + append!(extra_assignments, + [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) + for i in obsidxs]) + else + D = Differential(t) + ddvs = map(D, dvs) + rhss = [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] + end + else + if !override_discrete && !is_discrete_system(sys) + check_operator_variables(eqs, Differential) + check_lhs(eqs, Differential, Set(dvs)) + end + rhss = [eq.rhs for eq in eqs] + end + + if !isempty(assertions(sys)) + rhss[end] += unwrap(get_assertions_expr(sys)) + end + + # TODO: add an optional check on the ordering of observed equations + if scalar + rhss = only(rhss) + u = only(u) + end + + args = (u, p...) + p_start = 2 + if t !== nothing + args = (args..., t) + end + if implicit_dae + args = (ddvs, args...) + p_start += 1 + end + + res = build_function_wrapper(sys, rhss, args...; p_start, extra_assignments, + expression = Val{true}, expression_module = eval_module, kwargs...) + nargs = length(args) - length(p) + 1 + if is_dde(sys) + p_start += 1 + nargs += 1 + end + return maybe_compile_function( + expression, wrap_gfw, (p_start, nargs, is_split(sys)), + res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Generate the diffusion function for the noise equations of a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_diffusion_function(sys::System; expression = Val{true}, + wrap_gfw = Val{false}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + eqs = get_noise_eqs(sys) + if ndims(eqs) == 2 && size(eqs, 2) == 1 + # scalar noise + eqs = vec(eqs) + end + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) + if expression == Val{true} + return res + end + f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) + p_start = 2 + nargs = 3 + if is_dde(sys) + p_start += 1 + nargs += 1 + end + return maybe_compile_function( + expression, wrap_gfw, (p_start, nargs, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Calculate the gradient of the equations of `sys` with respect to the independent variable. +`simplify` is forwarded to `Symbolics.expand_derivatives`. +""" +function calculate_tgrad(sys::System; simplify = false) + # We need to remove explicit time dependence on the unknown because when we + # have `u(t) * t` we want to have the tgrad to be `u(t)` instead of `u'(t) * + # t + u(t)`. + rhs = [detime_dvs(eq.rhs) for eq in full_equations(sys)] + iv = get_iv(sys) + xs = unknowns(sys) + rule = Dict(map((x, xt) -> xt => x, detime_dvs.(xs), xs)) + rhs = substitute.(rhs, Ref(rule)) + tgrad = [expand_derivatives(Differential(iv)(r), simplify) for r in rhs] + reverse_rule = Dict(map((x, xt) -> x => xt, detime_dvs.(xs), xs)) + tgrad = Num.(substitute.(tgrad, Ref(reverse_rule))) + return tgrad +end + +""" + $(TYPEDSIGNATURES) + +Calculate the jacobian of the equations of `sys`. + +# Keyword arguments + +- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. +- `dvs`: The variables with respect to which the jacobian should be computed. +""" +function calculate_jacobian(sys::System; + sparse = false, simplify = false, dvs = unknowns(sys)) + obs = Dict(eq.lhs => eq.rhs for eq in observed(sys)) + rhs = map(eq -> fixpoint_sub(eq.rhs - eq.lhs, obs), equations(sys)) + + if sparse + jac = sparsejacobian(rhs, dvs; simplify) + if get_iv(sys) !== nothing + # Add nonzeros of W as non-structural zeros of the Jacobian + # (to ensure equal results for oop and iip Jacobian) + JIs, JJs, JVs = findnz(jac) + WIs, WJs, _ = findnz(W_sparsity(sys)) + append!(JIs, WIs) # explicitly put all W's indices also in J, + append!(JJs, WJs) # even if it duplicates some indices + append!(JVs, zeros(eltype(JVs), length(WIs))) # add zero + jac = SparseArrays.sparse(JIs, JJs, JVs) # values at duplicate indices are summed; not overwritten + end + else + jac = jacobian(rhs, dvs; simplify) + end + + return jac +end + +""" + $(TYPEDSIGNATURES) + +Generate the jacobian function for the equations of a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). +- `checkbounds`: Whether to check correctness of indices at runtime if `sparse`. + Also forwarded to `build_function_wrapper`. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_jacobian(sys::System; + simplify = false, sparse = false, eval_expression = false, + eval_module = @__MODULE__, expression = Val{true}, wrap_gfw = Val{false}, + checkbounds = false, kwargs...) + dvs = unknowns(sys) + jac = calculate_jacobian(sys; simplify, sparse, dvs) + p = reorder_parameters(sys) + t = get_iv(sys) + if t !== nothing && sparse && checkbounds + wrap_code = assert_jac_length_header(sys) # checking sparse J indices at runtime is expensive for large systems + else + wrap_code = (identity, identity) + end + args = (dvs, p...) + nargs = 2 + if is_time_dependent(sys) + args = (args..., t) + nargs = 3 + end + res = build_function_wrapper(sys, jac, args...; wrap_code, expression = Val{true}, + expression_module = eval_module, checkbounds, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, nargs, is_split(sys)), res; eval_expression, eval_module) +end + +function assert_jac_length_header(sys) + W = W_sparsity(sys) + identity, + function add_header(expr) + Func(expr.args, [], expr.body, + [:(@assert $(SymbolicUtils.Code.toexpr(term(findnz, expr.args[1])))[1:2] == + $(findnz(W)[1:2]))]) + end +end + +""" + $(TYPEDSIGNATURES) + +Generate the tgrad function for the equations of a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`: Forwarded to [`calculate_tgrad`](@ref). + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_tgrad( + sys::System; + simplify = false, eval_expression = false, eval_module = @__MODULE__, + expression = Val{true}, wrap_gfw = Val{false}, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + tgrad = calculate_tgrad(sys, simplify = simplify) + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, tgrad, + dvs, + p..., + get_iv(sys); + expression = Val{true}, + expression_module = eval_module, + kwargs...) + + return maybe_compile_function( + expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Return an array of symbolic hessians corresponding to the equations of the system. + +# Keyword Arguments + +- `sparse`: Controls whether the symbolic hessians are sparse matrices +- `simplify`: Forwarded to `Symbolics.hessian` +""" +function calculate_hessian(sys::System; simplify = false, sparse = false) + rhs = [eq.rhs - eq.lhs for eq in full_equations(sys)] + dvs = unknowns(sys) + if sparse + hess = map(rhs) do expr + Symbolics.sparsehessian(expr, dvs; simplify)::AbstractSparseArray + end + else + hess = [Symbolics.hessian(expr, dvs; simplify) for expr in rhs] + end + + return hess +end + +""" + $(TYPEDSIGNATURES) + +Return the sparsity pattern of the hessian of the equations of `sys`. +""" +function Symbolics.hessian_sparsity(sys::System) + hess = calculate_hessian(sys; sparse = true) + return similar.(hess, Float64) +end + +const W_GAMMA = only(@variables ˍ₋gamma) + +""" + $(TYPEDSIGNATURES) + +Generate the `W = γ * M + J` function for the equations of a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). +- `checkbounds`: Whether to check correctness of indices at runtime if `sparse`. + Also forwarded to `build_function_wrapper`. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_W(sys::System; + simplify = false, sparse = false, expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, checkbounds = false, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + M = calculate_massmatrix(sys; simplify) + if sparse + M = SparseArrays.sparse(M) + end + J = calculate_jacobian(sys; simplify, sparse, dvs) + W = W_GAMMA * M + J + t = get_iv(sys) + if t !== nothing && sparse && checkbounds + wrap_code = assert_jac_length_header(sys) + else + wrap_code = (identity, identity) + end + + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, W, dvs, p..., W_GAMMA, t; wrap_code, + p_end = 1 + length(p), checkbounds, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, 4, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Generate the DAE jacobian `γ * J′ + J` function for the equations of a [`System`](@ref). +`J′` is the jacobian of the equations with respect to the `du` vector, and `J` is the +standard jacobian. + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_dae_jacobian(sys::System; simplify = false, sparse = false, + expression = Val{true}, wrap_gfw = Val{false}, eval_expression = false, + eval_module = @__MODULE__, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + jac_u = calculate_jacobian(sys; simplify = simplify, sparse = sparse) + t = get_iv(sys) + derivatives = Differential(t).(unknowns(sys)) + jac_du = calculate_jacobian(sys; simplify = simplify, sparse = sparse, + dvs = derivatives) + dvs = unknowns(sys) + jac = W_GAMMA * jac_du + jac_u + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, jac, derivatives, dvs, p..., W_GAMMA, t; + p_start = 3, p_end = 2 + length(p), kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (3, 5, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Generate the history function for a [`System`](@ref), given a symbolic representation of +the `u0` vector prior to the initial time. + +# Keyword Arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_history(sys::System, u0; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + p = reorder_parameters(sys) + res = build_function_wrapper(sys, u0, p..., get_iv(sys); expression = Val{true}, + expression_module = eval_module, p_start = 1, p_end = length(p), + similarto = typeof(u0), wrap_delays = false, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (1, 2, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Calculate the mass matrix of `sys`. `simplify` controls whether `Symbolics.simplify` is +applied to the symbolic mass matrix. Returns a `Diagonal` or `LinearAlgebra.I` wherever +possible. +""" +function calculate_massmatrix(sys::System; simplify = false) + eqs = [eq for eq in equations(sys)] + M = zeros(length(eqs), length(eqs)) + for (i, eq) in enumerate(eqs) + if iscall(eq.lhs) && operation(eq.lhs) isa Differential + st = var_from_nested_derivative(eq.lhs)[1] + j = variable_index(sys, st) + M[i, j] = 1 + else + _iszero(eq.lhs) || + error("Only semi-explicit constant mass matrices are currently supported. Faulty equation: $eq.") + end + end + M = simplify ? Symbolics.simplify.(M) : M + if isdiag(M) + M = Diagonal(M) + end + # M should only contain concrete numbers + M == I ? I : M +end + +""" + $(TYPEDSIGNATURES) + +Return a modified version of mass matrix `M` which is of a similar type to `u0`. `sparse` +controls whether the mass matrix should be a sparse matrix. +""" +function concrete_massmatrix(M; sparse = false, u0 = nothing) + if sparse && !(u0 === nothing || M === I) + SparseArrays.sparse(M) + elseif u0 === nothing || M === I + M + elseif M isa Diagonal + Diagonal(ArrayInterface.restructure(u0, diag(M))) + else + ArrayInterface.restructure(u0 .* u0', M) + end +end + +""" + $TYPEDSIGNATURES + +Obtain the jacobian sparsity pattern from a torn system. Returns `nothing` by default. +""" +torn_system_jacobian_sparsity(sys::AbstractSystem) = nothing + +""" + $(TYPEDSIGNATURES) + +Return the sparsity pattern of the jacobian of `sys` as a matrix. +""" +function jacobian_sparsity(sys::System) + sparsity = torn_system_jacobian_sparsity(sys) + sparsity === nothing || return sparsity + + Symbolics.jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in unknowns(sys)]) +end + +""" + $(TYPEDSIGNATURES) + +Return the sparsity pattern of the DAE jacobian of `sys` as a matrix. + +See also: [`generate_dae_jacobian`](@ref). +""" +function jacobian_dae_sparsity(sys::System) + J1 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in unknowns(sys)]) + derivatives = Differential(get_iv(sys)).(unknowns(sys)) + J2 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], + [dv for dv in derivatives]) + J1 + J2 +end + +""" + $(TYPEDSIGNATURES) + +Return the sparsity pattern of the `W` matrix of `sys`. + +See also: [`generate_W`](@ref). +""" +function W_sparsity(sys::System) + jac_sparsity = jacobian_sparsity(sys) + (n, n) = size(jac_sparsity) + M = calculate_massmatrix(sys) + M_sparsity = M isa UniformScaling ? sparse(I(n)) : + SparseMatrixCSC{Bool, Int64}((!iszero).(M)) + jac_sparsity .| M_sparsity +end + +""" + $(TYPEDSIGNATURES) + +Return the matrix to use as the jacobian prototype given the W-sparsity matrix of the +system. This is not the same as the jacobian sparsity pattern. + +# Keyword arguments + +- `u0`: The `u0` vector for the problem. +- `sparse`: The prototype is `nothing` for non-sparse matrices. +""" +function calculate_W_prototype(W_sparsity; u0 = nothing, sparse = false) + sparse || return nothing + uElType = u0 === nothing ? Float64 : eltype(u0) + return similar(W_sparsity, uElType) +end + +function isautonomous(sys::System) + tgrad = calculate_tgrad(sys; simplify = true) + all(iszero, tgrad) +end + +function get_bv_solution_symbol(ns) + only(@variables BV_SOLUTION(..)[1:ns]) +end + +function get_constraint_unknown_subs!(subs::Dict, cons::Vector, stidxmap::Dict, iv, sol) + vs = SU.search_variables(cons) + for v in vs + iscall(v) || continue + op = operation(v) + args = arguments(v) + issym(op) && length(args) == 1 || continue + newv = op(iv) + haskey(stidxmap, newv) || continue + subs[v] = sol(args[1])[stidxmap[newv]] + end +end + +""" + $(TYPEDSIGNATURES) + +Generate the boundary condition function for a [`System`](@ref) given the state vector `u0`, +the indexes of `u0` to consider as hard constraints `u0_idxs` and the initial time `t0`. + +# Keyword Arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_boundary_conditions(sys::System, u0, u0_idxs, t0; expression = Val{true}, + wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, + kwargs...) + iv = get_iv(sys) + sts = unknowns(sys) + ps = parameters(sys) + np = length(ps) + ns = length(sts) + stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) + pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) + + # sol = get_bv_solution_symbol(ns) + + cons = [con.lhs - con.rhs for con in constraints(sys)] + # conssubs = Dict() + # get_constraint_unknown_subs!(conssubs, cons, stidxmap, iv, sol) + # cons = map(x -> substitute(x, conssubs), cons) + + init_conds = Any[] + for i in u0_idxs + expr = BVP_SOLUTION(t0)[i] - u0[i] + push!(init_conds, expr) + end + + exprs = vcat(init_conds, cons) + _p = reorder_parameters(sys, ps) + + res = build_function_wrapper(sys, exprs, _p..., iv; output_type = Array, + p_start = 1, histfn = (p, t) -> BVP_SOLUTION(t), + histfn_symbolic = BVP_SOLUTION, wrap_delays = true, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Generate the cost function for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_cost(sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + + if is_time_dependent(sys) + wrap_delays = true + p_start = 1 + p_end = length(ps) + args = (ps..., get_iv(sys)) + nargs = 3 + else + wrap_delays = false + p_start = 2 + p_end = length(ps) + 1 + args = (dvs, ps...) + nargs = 2 + end + res = build_function_wrapper( + sys, obj, args...; expression = Val{true}, p_start, p_end, wrap_delays, + histfn = (p, t) -> BVP_SOLUTION(t), histfn_symbolic = BVP_SOLUTION, kwargs...) + if expression == Val{true} + return res + end + f_oop = eval_or_rgf(res; eval_expression, eval_module) + return maybe_compile_function( + expression, wrap_gfw, (2, nargs, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Calculate the gradient of the consolidated cost of `sys` with respect to the unknowns. +`simplify` is forwarded to `Symbolics.gradient`. +""" +function calculate_cost_gradient(sys::System; simplify = false) + obj = cost(sys) + dvs = unknowns(sys) + return Symbolics.gradient(obj, dvs; simplify) +end + +""" + $(TYPEDSIGNATURES) + +Generate the gradient of the cost function with respect to unknowns for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`: Forwarded to [`calculate_cost_gradient`](@ref). + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_cost_gradient( + sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, simplify = false, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + exprs = calculate_cost_gradient(sys; simplify) + res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Calculate the hessian of the consolidated cost of `sys` with respect to the unknowns. +`simplify` is forwarded to `Symbolics.hessian`. `sparse` controls whether a sparse +matrix is returned. +""" +function calculate_cost_hessian(sys::System; sparse = false, simplify = false) + obj = cost(sys) + dvs = unknowns(sys) + if sparse + return Symbolics.sparsehessian(obj, dvs; simplify)::AbstractSparseArray + else + return Symbolics.hessian(obj, dvs; simplify) + end +end + +""" + $(TYPEDSIGNATURES) + +Return the sparsity pattern for the hessian of the cost function of `sys`. +""" +function cost_hessian_sparsity(sys::System) + return similar(calculate_cost_hessian(sys; sparse = true), Float64) +end + +""" + $(TYPEDSIGNATURES) + +Generate the hessian of the cost function for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_cost_hessian`](@ref). +- `return_sparsity`: Whether to also return the sparsity pattern of the hessian as the + second return value. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_cost_hessian( + sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, simplify = false, + sparse = false, return_sparsity = false, kwargs...) + obj = cost(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + sparsity = nothing + exprs = calculate_cost_hessian(sys; sparse, simplify) + if sparse + sparsity = similar(exprs, Float64) + end + res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) + fn = maybe_compile_function( + expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) + + return return_sparsity ? (fn, sparsity) : fn +end + +function canonical_constraints(sys::System) + return map(constraints(sys)) do cstr + Symbolics.canonical_form(cstr).lhs + end +end + +""" + $(TYPEDSIGNATURES) + +Generate the constraint function for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_cons(sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, kwargs...) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + res = build_function_wrapper(sys, cons, dvs, ps...; expression = Val{true}, kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Return the jacobian of the constraints of `sys` with respect to unknowns. + +# Keyword arguments + +- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. +- `return_sparsity`: Whether to also return the sparsity pattern of the jacobian. +""" +function calculate_constraint_jacobian(sys::System; simplify = false, sparse = false, + return_sparsity = false) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + sparsity = nothing + if sparse + jac = Symbolics.sparsejacobian(cons, dvs; simplify)::AbstractSparseArray + sparsity = similar(jac, Float64) + else + jac = Symbolics.jacobian(cons, dvs; simplify) + end + return return_sparsity ? (jac, sparsity) : jac +end + +""" + $(TYPEDSIGNATURES) + +Generate the jacobian of the constraint function for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_constraint_jacobian`](@ref). +- `return_sparsity`: Whether to also return the sparsity pattern of the jacobian as the + second return value. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_constraint_jacobian( + sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, return_sparsity = false, + simplify = false, sparse = false, kwargs...) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + jac, + sparsity = calculate_constraint_jacobian( + sys; simplify, sparse, return_sparsity = true) + res = build_function_wrapper(sys, jac, dvs, ps...; expression = Val{true}, kwargs...) + fn = maybe_compile_function( + expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) + return return_sparsity ? (fn, sparsity) : fn +end + +""" + $(TYPEDSIGNATURES) + +Return the hessian of the constraints of `sys` with respect to unknowns. + +# Keyword arguments + +- `simplify`, `sparse`: Forwarded to `Symbolics.hessian`. +- `return_sparsity`: Whether to also return the sparsity pattern of the hessian. +""" +function calculate_constraint_hessian( + sys::System; simplify = false, sparse = false, return_sparsity = false) + cons = canonical_constraints(sys) + dvs = unknowns(sys) + sparsity = nothing + if sparse + hess = map(cons) do cstr + Symbolics.sparsehessian(cstr, dvs; simplify)::AbstractSparseArray + end + sparsity = similar.(hess, Float64) + else + hess = [Symbolics.hessian(cstr, dvs; simplify) for cstr in cons] + end + return return_sparsity ? (hess, sparsity) : hess +end + +""" + $(TYPEDSIGNATURES) + +Generate the hessian of the constraint function for a [`System`](@ref). + +# Keyword Arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_constraint_hessian`](@ref). +- `return_sparsity`: Whether to also return the sparsity pattern of the hessian as the + second return value. + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_constraint_hessian( + sys::System; expression = Val{true}, wrap_gfw = Val{false}, + eval_expression = false, eval_module = @__MODULE__, return_sparsity = false, + simplify = false, sparse = false, kwargs...) + dvs = unknowns(sys) + ps = reorder_parameters(sys) + hess, + sparsity = calculate_constraint_hessian( + sys; simplify, sparse, return_sparsity = true) + res = build_function_wrapper(sys, hess, dvs, ps...; expression = Val{true}, kwargs...) + fn = maybe_compile_function( + expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) + return return_sparsity ? (fn, sparsity) : fn +end + +""" + $(TYPEDSIGNATURES) + +Calculate the jacobian of the equations of `sys` with respect to the inputs. + +# Keyword arguments + +- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. +""" +function calculate_control_jacobian(sys::AbstractSystem; + sparse = false, simplify = false) + rhs = [eq.rhs for eq in full_equations(sys)] + ctrls = unbound_inputs(sys) + + if sparse + jac = sparsejacobian(rhs, ctrls, simplify = simplify) + else + jac = jacobian(rhs, ctrls, simplify = simplify) + end + + return jac +end + +""" + $(TYPEDSIGNATURES) + +Generate the jacobian function of the equations of `sys` with respect to the inputs. + +# Keyword arguments + +$GENERATE_X_KWARGS +- `simplify`, `sparse`: Forwarded to [`calculate_constraint_hessian`](@ref). + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_control_jacobian(sys::AbstractSystem; + expression = Val{true}, wrap_gfw = Val{false}, eval_expression = false, + eval_module = @__MODULE__, simplify = false, sparse = false, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + jac = calculate_control_jacobian(sys; simplify = simplify, sparse = sparse) + p = reorder_parameters(sys, ps) + res = build_function_wrapper(sys, jac, dvs, p..., get_iv(sys); kwargs...) + return maybe_compile_function( + expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) +end + +function generate_rate_function(js::System, rate) + p = reorder_parameters(js) + build_function_wrapper(js, rate, unknowns(js), p..., + get_iv(js), + expression = Val{true}) +end + +function generate_affect_function(js::System, affect; kwargs...) + compile_equational_affect(affect, js; checkvars = false, kwargs...) +end + +function assemble_vrj( + js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) + rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) + rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) + outputvars = (value(affect.lhs) for affect in vrj.affect!) + outputidxs = [unknowntoid[var] for var in outputvars] + affect = generate_affect_function(js, vrj.affect!; eval_expression, eval_module) + VariableRateJump(rate, affect; save_positions = vrj.save_positions) +end + +function assemble_crj( + js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) + rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) + rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) + outputvars = (value(affect.lhs) for affect in crj.affect!) + outputidxs = [unknowntoid[var] for var in outputvars] + affect = generate_affect_function(js, crj.affect!; eval_expression, eval_module) + ConstantRateJump(rate, affect) +end + +# assemble a numeric MassActionJump from a MT symbolics MassActionJumps +function assemble_maj(majv::Vector{U}, unknowntoid, pmapper) where {U <: MassActionJump} + rs = [numericrstoich(maj.reactant_stoch, unknowntoid) for maj in majv] + ns = [numericnstoich(maj.net_stoch, unknowntoid) for maj in majv] + MassActionJump(rs, ns; param_mapper = pmapper, nocopy = true) +end + +function numericrstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} + rs = Vector{Pair{Int, W}}() + for (wspec, stoich) in mtrs + spec = value(wspec) + if !iscall(spec) && _iszero(spec) + push!(rs, 0 => stoich) + else + push!(rs, unknowntoid[spec] => stoich) + end + end + sort!(rs) + rs +end + +function numericnstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} + ns = Vector{Pair{Int, W}}() + for (wspec, stoich) in mtrs + spec = value(wspec) + !iscall(spec) && _iszero(spec) && + error("Net stoichiometry can not have a species labelled 0.") + push!(ns, unknowntoid[spec] => stoich) + end + sort!(ns) +end + +function is_atomic_inside_indexed(x::SymbolicT) + SU.default_is_atomic(x) && isequal(split_indexed_var(x)[1], x) +end + +struct CheckInvalidAndTrackNamespaced + simplevars::AtomicArraySet{Dict{SymbolicT, Nothing}} + dervars::Set{SymbolicT} + ns_map::Dict{SymbolicT, SymbolicT} + namespace_subs::Dict{SymbolicT, SymbolicT} + present_dervars::Set{SymbolicT} + iv::Union{SymbolicT, Nothing} + throw::Bool +end + +function (pred::CheckInvalidAndTrackNamespaced)(x::SymbolicT) + isatomic1 = SU.default_is_atomic(x) + isatomic1 || return false + + newx = get(pred.ns_map, x, nothing) + if newx !== nothing + pred.namespace_subs[x] = newx + x = newx + end + arrx, _ = split_indexed_var(x) + iv = pred.iv + if iv isa SymbolicT + Moshi.Match.@match arrx begin + BSImpl.Term(f, args) && if f isa SymbolicT && iscall(args[1]) end => begin + arrx = f(iv) + end + _ => nothing + end + end + arrx in pred.simplevars && return true + if x in pred.dervars + push!(pred.present_dervars, x) + return true + end + if pred.throw + Base.throw(ArgumentError("Symbol $x is not present in the system.")) + end + return false +end + +""" + build_explicit_observed_function(sys, ts; kwargs...) -> Function(s) + +Generates a function that computes the observed value(s) `ts` in the system `sys`, while making the assumption that there are no cycles in the equations. + +## Arguments +- `sys`: The system for which to generate the function +- `ts`: The symbolic observed values whose value should be computed + +## Keywords +- `return_inplace = false`: If true and the observed value is a vector, then return both the in place and out of place methods. +- `expression = false`: Generates a Julia `Expr`` computing the observed value if `expression` is true +- `eval_expression = false`: If true and `expression = false`, evaluates the returned function in the module `eval_module` +- `output_type = Array` the type of the array generated by a out-of-place vector-valued function +- `param_only = false` if true, only allow the generated function to access system parameters +- `inputs = nothing` additinoal symbolic variables that should be provided to the generated function +- `disturbance_inputs = nothing` symbolic variables representing unknown disturbance inputs (removed from parameters, not added as function arguments) +- `known_disturbance_inputs = nothing` symbolic variables representing known disturbance inputs (removed from parameters, added as function arguments) +- `checkbounds = true` checks bounds if true when destructuring parameters +- `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. +- `mkarray`: only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. +- `cse = true`: Whether to use Common Subexpression Elimination (CSE) to generate a more efficient function. +- `wrap_delays = is_dde(sys)`: Whether to add an argument for the history function and use + it to calculate all delayed variables. + +## Returns + +The return value will be either: +* a single function `f_oop` if the input is a scalar or if the input is a Vector but `return_inplace` is false +* the out of place and in-place functions `(f_ip, f_oop)` if `return_inplace` is true and the input is a `Vector` + +The function(s) `f_oop` (and potentially `f_ip`) will be: +* `RuntimeGeneratedFunction`s by default, +* A Julia `Expr` if `expression` is true, +* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true and `expression` is false. + +The signatures will be of the form `g(...)` with arguments: + +- `output` for in-place functions +- `unknowns` if `param_only` is `false` +- `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` +- `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted +- `t` if the system is time-dependent; for example systems of nonlinear equations will not have `t` +- `known_disturbance_inputs` if provided; these are disturbance inputs that are known and provided as arguments + +For example, a function `g(op, unknowns, p..., inputs, t, known_disturbances)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, +an array of inputs `inputs` is given, `known_disturbance_inputs` is provided, and `param_only` is false for a time-dependent system. +""" +function build_explicit_observed_function(sys, ts; + inputs = nothing, + disturbance_inputs = nothing, + known_disturbance_inputs = nothing, + disturbance_argument = false, + expression = false, + eval_expression = false, + eval_module = @__MODULE__, + output_type = Array, + checkbounds = true, + ps = parameters(sys; initial_parameters = true), + return_inplace = false, + param_only = false, + throw = true, + cse = true, + mkarray = nothing, + wrap_delays = is_dde(sys) && !param_only) + if inputs === nothing + inputs = () + else + inputs = vec(unwrap_vars(inputs)) + end + if disturbance_inputs === nothing + disturbance_inputs = () + else + disturbance_inputs = vec(unwrap_vars(disturbance_inputs)) + end + if known_disturbance_inputs === nothing + known_disturbance_inputs = () + else + known_disturbance_inputs = vec(unwrap_vars(known_disturbance_inputs)) + end + ps::Vector{SymbolicT} = vec(unwrap_vars(ps)) + # TODO: cleanup + is_tuple = ts isa Tuple + if is_tuple + ts = collect(ts) + output_type = Tuple + end + + ts = unwrap(ts) + if !(ts isa Union{SymbolicT, Symbol}) + ts = unwrap_vars(ts) + end + + allsyms = all_symbols(sys) + if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray + ts = map(x -> symbol_to_symbolic(sys, x; allsyms), ts) + else + ts = symbol_to_symbolic(sys, ts; allsyms) + end + + namespace_subs = Dict{SymbolicT, SymbolicT}() + ns_map = Dict{SymbolicT, SymbolicT}(renamespace(sys, eq.lhs) => eq.lhs for eq in observed(sys)) + for sym in unknowns(sys) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] + end + end + for sym in parameters(sys; initial_parameters = true) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] + end + end + allsyms = as_atomic_array_set(unknowns(sys)) + foreach(Base.Fix1(push_as_atomic_array!, allsyms), observables(sys)) + foreach(Base.Fix1(push_as_atomic_array!, allsyms), parameters(sys; initial_parameters = true)) + foreach(Base.Fix1(push_as_atomic_array!, allsyms), bound_parameters(sys)) + union!(allsyms, independent_variables(sys)) + dervars = Set{SymbolicT}() + dervals = Dict{SymbolicT, SymbolicT}() + if isscheduled(sys) + sched::Schedule = get_schedule(sys) + for (k, v) in sched.dummy_sub + ttk = default_toterm(k) + push!(dervars, ttk) + dervals[ttk] = v + end + else + for eq in equations(sys) + isdiffeq(eq) || continue + ttk = default_toterm(eq.lhs) + push!(dervars, ttk) + dervals[ttk] = eq.rhs + end + end + pred = CheckInvalidAndTrackNamespaced(allsyms, dervars, ns_map, namespace_subs, + Set{SymbolicT}(), get_iv(sys), throw) + iv = has_iv(sys) ? get_iv(sys) : nothing + if ts isa SymbolicT + SU.query(pred, ts) + else + for x in ts + SU.query(pred, x) + end + end + extra_assignments = Assignment[] + for var in pred.present_dervars + push!(extra_assignments, var ← dervals[var]) + end + ts = substitute(ts, namespace_subs) + + obsfilter = if param_only + if is_split(sys) + let ic = get_index_cache(sys) + eq -> !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) + end + else + Returns(false) + end + else + Returns(true) + end + dvs = if param_only + () + else + (unknowns(sys),) + end + if inputs isa Vector{SymbolicT} + ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list + inputs = (inputs,) + end + # Handle backward compatibility for disturbance_argument + if disturbance_argument + Base.depwarn("The `disturbance_argument` keyword argument is deprecated. Use `known_disturbance_inputs` instead. " * + "For `disturbance_argument=true`, pass `known_disturbance_inputs=disturbance_inputs, disturbance_inputs=nothing`. " * + "For `disturbance_argument=false`, use `disturbance_inputs` as before.", + :build_explicit_observed_function) + if known_disturbance_inputs !== nothing + error("Cannot specify both `disturbance_argument=true` and `known_disturbance_inputs`") + end + known_disturbance_inputs = disturbance_inputs + disturbance_inputs = nothing + end + + # Remove disturbance inputs from parameters (both known and unknown) + if disturbance_inputs isa Vector{SymbolicT} + # Disturbance inputs may or may not be included as inputs, depending on disturbance_argument + ps = setdiff(ps, disturbance_inputs) + end + if known_disturbance_inputs isa Vector{SymbolicT} + ps = setdiff(ps, known_disturbance_inputs) + known_disturbance_inputs = (known_disturbance_inputs,) + end + rps::ReorderedParametersT = reorder_parameters(sys, ps) + iv = if is_time_dependent(sys) + (get_iv(sys),) + else + () + end + args = (dvs..., inputs..., rps..., iv..., known_disturbance_inputs...) + p_start = length(dvs) + length(inputs) + 1 + p_end = length(dvs) + length(inputs) + length(rps) + fns = build_function_wrapper( + sys, ts, args...; p_start, p_end, filter_observed = obsfilter, + output_type, mkarray, try_namespaced = true, expression = Val{true}, cse, + wrap_delays, extra_assignments) + if fns isa Tuple + if expression + return return_inplace ? fns : fns[1] + end + oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + p_start + wrap_delays, length(args) - length(rps) + 1 + wrap_delays, is_split(sys))}( + oop, iip) + return return_inplace ? (f, f) : f + else + if expression + return fns + end + f = eval_or_rgf(fns; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + p_start + wrap_delays, length(args) - length(rps) + 1 + wrap_delays, is_split(sys))}( + f, nothing) + return f + end +end + +""" + $(TYPEDSIGNATURES) + +Return matrix `A` and vector `b` such that the system `sys` can be represented as +`A * x = b` where `x` is `unknowns(sys)`. Errors if the system is not affine. + +# Keyword arguments + +- `sparse`: return a sparse `A`. +""" +function calculate_A_b(sys::System; sparse = false) + rhss = [eq.rhs for eq in full_equations(sys)] + dvs = unknowns(sys) + + A = Matrix{Any}(undef, length(rhss), length(dvs)) + b = Vector{Any}(undef, length(rhss)) + for (i, rhs) in enumerate(rhss) + # mtkcompile makes this `0 ~ rhs` which typically ends up giving + # unknowns negative coefficients. If given the equations `A * x ~ b` + # it will simplify to `0 ~ b - A * x`. Thus this negation usually leads + # to more comprehensible user API. + resid = -rhs + for (j, var) in enumerate(dvs) + p, q, islinear = Symbolics.linear_expansion(resid, var) + if !islinear + throw(ArgumentError("System is not linear. Equation $(0 ~ rhs) is not linear in unknown $var.")) + end + A[i, j] = p + resid = q + end + # negate beucause `resid` is the residual on the LHS + b[i] = -resid + end + + @assert all(Base.Fix1(isassigned, A), eachindex(A)) + @assert all(Base.Fix1(isassigned, A), eachindex(b)) + + if sparse + A = SparseArrays.sparse(A) + end + return A, b +end + +""" + $(TYPEDSIGNATURES) + +Given a system `sys` and the `A` from [`calculate_A_b`](@ref) generate the function that +updates `A` given the parameter object. + +# Keyword arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_update_A(sys::System, A::AbstractMatrix; expression = Val{true}, + wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, cachesyms = (), kwargs...) + ps = reorder_parameters(sys) + + res = build_function_wrapper( + sys, A, ps..., cachesyms...; p_start = 1, expression = Val{true}, + similarto = typeof(A), kwargs...) + return maybe_compile_function(expression, wrap_gfw, (1, 1, is_split(sys)), res; + eval_expression, eval_module) +end + +""" + $(TYPEDSIGNATURES) + +Given a system `sys` and the `b` from [`calculate_A_b`](@ref) generate the function that +updates `b` given the parameter object. + +# Keyword arguments + +$GENERATE_X_KWARGS + +All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). +""" +function generate_update_b(sys::System, b::AbstractVector; expression = Val{true}, + wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, cachesyms = (), kwargs...) + ps = reorder_parameters(sys) + + res = build_function_wrapper( + sys, b, ps..., cachesyms...; p_start = 1, expression = Val{true}, + similarto = typeof(b), kwargs...) + return maybe_compile_function(expression, wrap_gfw, (1, 1, is_split(sys)), res; + eval_expression, eval_module) +end diff --git a/src/systems/codegen_utils.jl b/lib/ModelingToolkitBase/src/systems/codegen_utils.jl similarity index 87% rename from src/systems/codegen_utils.jl rename to lib/ModelingToolkitBase/src/systems/codegen_utils.jl index d594a3902a..320d0be769 100644 --- a/src/systems/codegen_utils.jl +++ b/lib/ModelingToolkitBase/src/systems/codegen_utils.jl @@ -92,7 +92,7 @@ function array_variable_assignments(args...; argument_name = generated_argument_ else elems = map(idxs) do idx i, j = idx - term(getindex, argument_name(i), j) + term(getindex, argument_name(i), j; type = Real, shape = SU.ShapeVecT()) end # use `MakeArray` syntax and generate a stack-allocated array expr = term(SymbolicUtils.Code.create_array, SArray, nothing, @@ -115,12 +115,12 @@ variable. """ function isdelay(var, iv) iv === nothing && return false - if iscall(var) && ModelingToolkit.isoperator(var, Differential) + if iscall(var) && ModelingToolkitBase.isoperator(var, Differential) return isdelay(arguments(var)[1], iv) end isvariable(var) || return false isparameter(var) && return false - if iscall(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) + if iscall(var) && !ModelingToolkitBase.isoperator(var, Symbolics.Operator) args = arguments(var) length(args) == 1 || return false arg = args[1] @@ -135,8 +135,8 @@ end """ The argument of generated functions corresponding to the history function. """ -const DDE_HISTORY_FUN = Sym{Symbolics.FnType{Tuple{Any, <:Real}, Vector{Real}}}(:___history___) -const BVP_SOLUTION = Sym{Symbolics.FnType{Tuple{<:Real}, Vector{Real}}}(:__sol__) +const DDE_HISTORY_FUN = SSym(:___history___; type = SU.FnType{Tuple{Any, <:Real}, Vector{Real}, Nothing}, shape = SU.Unknown(1)) +const BVP_SOLUTION = SSym(:__sol__; type = Symbolics.FnType{Tuple{<:Real}, Vector{Real}, Nothing}, shape = SU.Unknown(1)) """ $(TYPEDSIGNATURES) @@ -184,6 +184,13 @@ function delay_to_function(expr, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG) end end +function __search_dervars_recurse(x::SymbolicT) + iscall(x) && Moshi.Match.@match x begin + BSImpl.Term(; f) && if f isa Operator end => false + _ => true + end +end + """ $(TYPEDSIGNATURES) @@ -250,25 +257,62 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_start += 1 p_end += 1 end - pdeps = get_parameter_dependencies(sys) + dervars = Dict{SymbolicT, SymbolicT}() + dervars_in_expr = Set{SymbolicT}() + if isscheduled(sys) + sched::Schedule = get_schedule(sys) + for (k, v) in sched.dummy_sub + ttk = default_toterm(k) + isequal(ttk, v) && continue + dervars[default_toterm(k)] = v + end + else + for eq in equations(sys) + isdiffeq(eq) || continue + ttk = default_toterm(eq.lhs) + isequal(ttk, eq.rhs) && continue + dervars[ttk] = eq.rhs + end + end + Symbolics.get_variables!(dervars_in_expr, expr, keys(dervars); recurse = __search_dervars_recurse) # only get the necessary observed equations, avoiding extra computation if add_observed && !isempty(obs) - obsidxs = observed_equations_used_by(sys, expr; obs) + obsidxs = BitSet(observed_equations_used_by(sys, expr; obs)) else - obsidxs = Int[] + obsidxs = BitSet() end # similarly for parameter dependency equations - pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) + reqd_bound_pars = OrderedSet{SymbolicT}() + bgraph::ParameterBindingsGraph = if iscomplete(sys) + get_parameter_bindings_graph(sys) + else + ParameterBindingsGraph(sys) + end + + bound_parameters_used_by!(reqd_bound_pars, sys, expr; bgraph) for i in obsidxs - union!(pdepidxs, observed_equations_used_by(sys, obs[i].rhs; obs = pdeps)) + bound_parameters_used_by!(reqd_bound_pars, sys, obs[i].rhs; bgraph) end + for dervar in dervars_in_expr + bound_parameters_used_by!(reqd_bound_pars, sys, dervars[dervar]; bgraph) + union!(obsidxs, observed_equations_used_by(sys, dervars[dervar]; obs)) + end + sort_bound_parameters!(reqd_bound_pars, sys; bgraph) # assignments for reconstructing scalarized array symbolics assignments = array_variable_assignments(args...) - - for eq in Iterators.flatten((pdeps[pdepidxs], obs[obsidxs])) + binds = bindings(sys) + for p in reqd_bound_pars + push!(assignments, p ← binds[p]) + end + obsidxs = collect(obsidxs) + for eq in obs[obsidxs] push!(assignments, eq.lhs ← eq.rhs) end + + for dervar in dervars_in_expr + push!(assignments, dervar ← dervars[dervar]) + end append!(assignments, extra_assignments) args = ntuple(Val(length(args))) do i diff --git a/src/systems/connectiongraph.jl b/lib/ModelingToolkitBase/src/systems/connectiongraph.jl similarity index 74% rename from src/systems/connectiongraph.jl rename to lib/ModelingToolkitBase/src/systems/connectiongraph.jl index 27e542c2ab..592ec2106e 100644 --- a/src/systems/connectiongraph.jl +++ b/lib/ModelingToolkitBase/src/systems/connectiongraph.jl @@ -45,8 +45,8 @@ Create a `ConnectionVertex` given use for this connection. """ function ConnectionVertex( - namespace::Vector{Symbol}, var::Union{BasicSymbolic, AbstractSystem}, isouter::Bool) - if var isa BasicSymbolic + namespace::Vector{Symbol}, var::Union{SymbolicT, AbstractSystem}, isouter::Bool) + if var isa SymbolicT name = getname(var) else name = nameof(var) @@ -95,7 +95,7 @@ function Base.:(==)(a::ConnectionVertex, b::ConnectionVertex) a.type == b.type || return false if a.hash != b.hash error(""" - This should never happen. Please open an issue in ModelingToolkit.jl with an MWE. + This should never happen. Please open an issue in ModelingToolkitBase.jl with an MWE. """) end return true @@ -108,105 +108,21 @@ function Base.show(io::IO, vert::ConnectionVertex) print(io, vert.name[end], "::", vert.isouter ? "outer" : "inner") end -""" - $(TYPEDEF) - -A hypergraph used to represent the connection sets in a system. Vertices of this graph are -of type `ConnectionVertex`. The connected components of a connection graph are the merged -connection sets. - -## Fields - -$(TYPEDFIELDS) -""" -struct HyperGraph{V} - """ - Mapping from vertices to their integer ID. - """ - labels::Dict{V, Int} - """ - Reverse mapping from integer ID to vertices. - """ - invmap::Vector{V} - """ - Core data structure for storing the hypergraph. Each hyperedge is a source vertex and - has bipartite edges to the connection vertices it is incident on. - """ - graph::BipartiteGraph{Int, Nothing} -end - const ConnectionGraph = HyperGraph{ConnectionVertex} -""" - $(TYPEDSIGNATURES) - -Create an empty `ConnectionGraph`. -""" -function HyperGraph{V}() where {V} - graph = BipartiteGraph(0, 0, Val(true)) - return HyperGraph{V}(Dict{V, Int}(), V[], graph) -end - -function Base.show(io::IO, graph::ConnectionGraph) - printstyled(io, get(io, :cgraph_name, "ConnectionGraph"); color = :blue, bold = true) - println(io, " with ", length(graph.labels), - " vertices and ", nsrcs(graph.graph), " hyperedges") - compact = get(io, :compact, false) - for edge_i in 𝑠vertices(graph.graph) - if compact && edge_i > 5 - println(io, "⋮") - break - end - edge_idxs = 𝑠neighbors(graph.graph, edge_i) - type = graph.invmap[edge_idxs[1]].type - if type <: Union{InputVar, OutputVar} - type = "Causal" - elseif type == Equality - # otherwise it prints `ModelingToolkit.Equality` - type = "Equality" - end - printstyled(io, " ", type; bold = true, color = :yellow) - print(io, "<") - for vi in @view(edge_idxs[1:(end - 1)]) - print(io, graph.invmap[vi], ", ") - end - println(io, graph.invmap[edge_idxs[end]], ">") +function BipartiteGraphs.print_hyperedge_hint(io::IO, ::Type{ConnectionVertex}, graph::HyperGraph{ConnectionVertex}, edge_i::Int) + edge_idxs = 𝑠neighbors(graph.graph, edge_i) + type = graph.invmap[edge_idxs[1]].type + if type <: Union{InputVar, OutputVar} + type = "Causal" + elseif type == Equality + # otherwise it prints `ModelingToolkitBase.Equality` + type = "Equality" end + printstyled(io, type; bold = true, color = :yellow) end -""" - $(TYPEDSIGNATURES) - -Add the given vertex to the connection graph. Return the integer ID of the added vertex. -No-op if the vertex already exists. -""" -function Graphs.add_vertex!(graph::HyperGraph{V}, dst::V) where {V} - j = get(graph.labels, dst, 0) - iszero(j) || return j - j = Graphs.add_vertex!(graph.graph, DST) - push!(graph.invmap, dst) - @assert length(graph.invmap) == j - graph.labels[dst] = j - return j -end - -const HyperGraphEdge{V} = Union{Vector{V}, Tuple{Vararg{V}}, Set{V}} -const ConnectionGraphEdge = HyperGraphEdge{ConnectionVertex} - -""" - $(TYPEDSIGNATURES) - -Add the given hyperedge to the connection graph. Adds all vertices in the given edge if -they do not exist. Returns the integer ID of the added edge. -""" -function Graphs.add_edge!(graph::HyperGraph{V}, src::HyperGraphEdge{V}) where {V} - i = Graphs.add_vertex!(graph.graph, SRC) - for vert in src - j = Graphs.add_vertex!(graph, vert) - Graphs.add_edge!(graph.graph, i, j) - end - return i -end +const ConnectionGraphEdge = BipartiteGraphs.HyperGraphEdge{ConnectionVertex} """ $(TYPEDEF) @@ -450,36 +366,7 @@ end Return the merged connection sets in `graph` as a `Vector{Vector{ConnectionVertex}}`. These are equivalent to the connected components of `graph`. """ -function connectionsets(graph::HyperGraph{V}) where {V} - bigraph = graph.graph - invmap = graph.invmap - - # union all of the hyperedges - disjoint_sets = IntDisjointSet(length(invmap)) - for edge_i in 𝑠vertices(bigraph) - hyperedge = 𝑠neighbors(bigraph, edge_i) - isempty(hyperedge) && continue - root, rest = Iterators.peel(hyperedge) - for vert in rest - union!(disjoint_sets, root, vert) - end - end - - # maps the root of a vertex in `disjoint_sets` to the index of the corresponding set - # in `vertex_sets` - root_to_set = Dict{Int, Int}() - vertex_sets = Vector{V}[] - for (vert_i, vert) in enumerate(invmap) - root = find_root!(disjoint_sets, vert_i) - set_i = get!(root_to_set, root) do - push!(vertex_sets, V[]) - return length(vertex_sets) - end - push!(vertex_sets[set_i], vert) - end - - return vertex_sets -end +@inline connectionsets(graph::HyperGraph{V}) where {V} = Graphs.connected_components(graph) """ $(TYPEDSIGNATURES) diff --git a/src/systems/connectors.jl b/lib/ModelingToolkitBase/src/systems/connectors.jl similarity index 67% rename from src/systems/connectors.jl rename to lib/ModelingToolkitBase/src/systems/connectors.jl index a4156990ab..2a199b338f 100644 --- a/src/systems/connectors.jl +++ b/lib/ModelingToolkitBase/src/systems/connectors.jl @@ -23,19 +23,30 @@ Connect multiple connectors created via `@connector`. All connected connectors must be unique. """ function connect(sys1::AbstractSystem, sys2::AbstractSystem, syss::AbstractSystem...) - syss = (sys1, sys2, syss...) - length(unique(nameof, syss)) == length(syss) || error("connect takes distinct systems!") + _syss = System[] + push!(_syss, sys1) + push!(_syss, sys2) + for sys in syss + push!(_syss, sys) + end + syss = _syss + sysnames = Symbol[] + for sys in syss + push!(sysnames, nameof(sys)) + end + allunique(sysnames) || error("connect takes distinct systems!") Equation(Connection(), Connection(syss)) # the RHS are connected systems end const _debug_mode = Base.JLOptions().check_bounds == 1 function Base.show(io::IO, c::Connection) + Symbolics.warn_load_latexify() print(io, "connect(") if c.systems isa AbstractArray || c.systems isa Tuple n = length(c.systems) for (i, s) in enumerate(c.systems) - str = join(split(string(nameof(s)), NAMESPACE_SEPARATOR), '.') + str = join(split(string(Symbol(s)), NAMESPACE_SEPARATOR), '.') print(io, str) i != n && print(io, ", ") end @@ -43,15 +54,6 @@ function Base.show(io::IO, c::Connection) print(io, ")") end -@latexrecipe function f(c::Connection) - index --> :subscript - return Expr(:call, :connect, map(nameof, c.systems)...) -end - -function Base.show(io::IO, ::MIME"text/latex", ap::Connection) - print(io, latexify(ap)) -end - isconnection(_) = false isconnection(_::Connection) = true @@ -61,8 +63,18 @@ isconnection(_::Connection) = true Adds a domain only connection equation, through and across state equations are not generated. """ function domain_connect(sys1::AbstractSystem, sys2::AbstractSystem, syss::AbstractSystem...) - syss = (sys1, sys2, syss...) - length(unique(nameof, syss)) == length(syss) || error("connect takes distinct systems!") + _syss = System[] + push!(_syss, sys1) + push!(_syss, sys2) + for sys in syss + push!(_syss, sys) + end + syss = _syss + sysnames = Symbol[] + for sys in syss + push!(sysnames, nameof(sys)) + end + allunique(sysnames) || error("connect takes distinct systems!") Equation(Connection(:domain), Connection(syss)) # the RHS are connected systems end @@ -72,12 +84,8 @@ end Get the connection type of symbolic variable `s` from the `VariableConnectType` metadata. Defaults to `Equality` if not present. """ -function get_connection_type(s::Symbolic) - s = unwrap(s) - if iscall(s) && operation(s) === getindex - s = arguments(s)[1] - end - getmetadata(s, VariableConnectType, Equality) +function get_connection_type(s::SymbolicT) + safe_getmetadata(VariableConnectType, s, Equality)::DataType end """ @@ -95,7 +103,8 @@ Mark a system constructor function as building a connector. For example, end ``` -Since connectors only declare variables, the equivalent shorthand syntax can also be used: +Since connectors only declare variables, if `SciCompDSL.jl` is loaded the equivalent +shorthand syntax can also be used: ```julia @connector Pin begin @@ -104,7 +113,7 @@ Since connectors only declare variables, the equivalent shorthand syntax can als end ``` -ModelingToolkit systems are either components or connectors. Components define dynamics of +ModelingToolkitBase systems are either components or connectors. Components define dynamics of the model. Connectors are used to connect components together. See the [Model building reference](@ref model_building_api) section of the documentation for more information. @@ -115,6 +124,14 @@ macro connector(expr) esc(component_post_processing(expr, true)) end +function __mtkmodel_connector(_...) + error("To use this `@connector` syntax, please import `SciCompDSL.jl`.") +end + +macro connector(name, body) + esc(__mtkmodel_connector(__module__, name, body)) +end + abstract type AbstractConnectorType end struct StreamConnector <: AbstractConnectorType end struct RegularConnector <: AbstractConnectorType end @@ -171,28 +188,18 @@ get_systems(c::Connection) = c.systems Refer to the [Connection semantics](@ref connect_semantics) section of the docs for more information. """ -instream(a) = term(instream, unwrap(a), type = symtype(a)) -SymbolicUtils.promote_symtype(::typeof(instream), _) = Real - -isconnector(s::AbstractSystem) = has_connector_type(s) && get_connector_type(s) !== nothing - -""" - $(TYPEDEF) - -Utility struct which wraps a symbolic variable used in a `Connection` to enable `Base.show` -to work. -""" -struct SymbolicWithNameof - var::Any +function instream(a::SymbolicT) + BSImpl.Term{VartypeT}(instream, SArgsT((unwrap(a),)); type = symtype(a), shape = SU.shape(a)) end +instream(a::Num) = Num(instream(unwrap(a))) +instream(a::Symbolics.Arr{T, N}) where {T, N} = Symbolics.Arr{T, N}(instream(unwrap(a))) +SymbolicUtils.promote_symtype(::typeof(instream), ::Type{T}) where {T} = T -function Base.nameof(x::SymbolicWithNameof) - return Symbol(x.var) -end +isconnector(s::AbstractSystem) = has_connector_type(s) && get_connector_type(s) !== nothing is_causal_variable_connection(c) = false function is_causal_variable_connection(c::Connection) - all(x -> x isa SymbolicWithNameof, get_systems(c)) + get_systems(c) isa Vector{SymbolicT} end const ConnectableSymbolicT = Union{BasicSymbolic, Num, Symbolics.Arr} @@ -214,24 +221,36 @@ end Perform validation for a connect statement involving causal variables. """ -function validate_causal_variables_connection(allvars) - var1 = allvars[1] - var2 = allvars[2] - vars = Base.tail(Base.tail(allvars)) +function validate_causal_variables_connection(allvars::Vector{SymbolicT}) for var in allvars vtype = getvariabletype(var) vtype === VARIABLE || throw(ArgumentError("Expected $var to be of kind `$VARIABLE`. Got `$vtype`.")) end - if length(unique(allvars)) !== length(allvars) + if !allunique(allvars) throw(ArgumentError("Expected all connection variables to be unique. Got variables $allvars which contains duplicate entries.")) end - allsizes = map(size, allvars) - if !allequal(allsizes) - throw(ArgumentError("Expected all connection variables to have the same size. Got variables $allvars with sizes $allsizes respectively.")) + sh1 = SU.shape(allvars[1])::SU.ShapeVecT + sz1 = SU.SmallV{Int}() + for x in sh1 + push!(sz1, length(x)) end - non_causal_variables = filter(allvars) do var - !isinput(var) && !isoutput(var) + sz2 = SU.SmallV{Int}() + for v in allvars + sh = SU.shape(v)::SU.ShapeVecT + empty!(sz2) + for x in sh + push!(sz2, length(x)) + end + if !isequal(sz1, sz2) + throw(ArgumentError("Expected all connection variables to have the same size. Got variables $(allvars[1]) and $v with sizes $sz1 and $sz2 respectively.")) + + end + end + non_causal_variables = SymbolicT[] + for x in allvars + (isinput(x) || isoutput(x)) && continue + push!(non_causal_variables, x) end isempty(non_causal_variables) || throw(NonCausalVariableError(non_causal_variables)) end @@ -250,9 +269,14 @@ var1 ~ var3 """ function connect(var1::ConnectableSymbolicT, var2::ConnectableSymbolicT, vars::ConnectableSymbolicT...) - allvars = (var1, var2, vars...) + allvars = SymbolicT[] + push!(allvars, unwrap(var1)) + push!(allvars, unwrap(var2)) + for var in vars + push!(allvars, unwrap(var)) + end validate_causal_variables_connection(allvars) - return Equation(Connection(), Connection(map(SymbolicWithNameof, unwrap.(allvars)))) + return Equation(Connection(), Connection(allvars)) end """ @@ -302,6 +326,27 @@ mydiv(num, den) = end @register_symbolic mydiv(n, d) +struct IsOuter + outer_connectors::Set{Symbol} +end + +function (io::IsOuter)(name::Symbol) + name in io.outer_connectors +end + +function (io::IsOuter)(sys) + nm = nameof(sys) + isconnector(sys) || error("$nm is not a connector!") + s = string(nm) + idx = findfirst(NAMESPACE_SEPARATOR, s) + parent_name = if idx === nothing + nm + else + Symbol(@view(s[1:prevind(s, idx)])) + end + return io(parent_name) +end + """ $(TYPEDSIGNATURES) @@ -309,23 +354,12 @@ Return a function which checks whether the connector (system) passed to it is an connector of `sys`. The function can also be given the name of a system as a `Symbol`. """ function generate_isouter(sys::AbstractSystem) - outer_connectors = Symbol[] + outer_connectors = Set{Symbol}() for s in get_systems(sys) n = nameof(s) isconnector(s) && push!(outer_connectors, n) end - let outer_connectors = outer_connectors - function isouter(sys)::Bool - s = string(nameof(sys)) - isconnector(sys) || error("$s is not a connector!") - idx = findfirst(isequal(NAMESPACE_SEPARATOR), s) - parent_name = Symbol(idx === nothing ? s : s[1:prevind(s, idx)]) - isouter(parent_name) - end - function isouter(name::Symbol)::Bool - return name in outer_connectors - end - end + return IsOuter(outer_connectors) end @noinline function connection_error(ss) @@ -336,14 +370,24 @@ abstract type IsFrame end "Return true if the system is a 3D multibody frame, otherwise return false." function isframe(sys) - getmetadata(sys, IsFrame, false) + getmetadata(sys, IsFrame, false)::Bool end abstract type FrameOrientation end +struct RotationMatrix + R::Matrix{SymbolicT} + w::Vector{SymbolicT} + + function RotationMatrix(R::AbstractMatrix, w::AbstractVector) + new(unwrap_vars(R), unwrap_vars(w)) + end + +end + "Return orientation object of a multibody frame." function ori(sys) - getmetadata(sys, FrameOrientation, nothing) + getmetadata(sys, FrameOrientation, nothing)::Union{RotationMatrix, Nothing} end """ @@ -373,13 +417,13 @@ index_from_type(::Type{OutputVar{I}}) where {I} = I Chain `getproperty` calls on sys in the order given by `names` and return the unwrapped result. """ -function iterative_getproperty(sys::AbstractSystem, names::AbstractVector{Symbol}) +function iterative_getproperty(sys::AbstractSystem, names::Vector{Symbol}) # we don't want to namespace the first time - result = toggle_namespacing(sys, false) + result::Union{SymbolicT, System} = toggle_namespacing(sys, false) for name in names - result = getproperty(result, name) + result = getvar(result, name)::Union{SymbolicT, System} end - return unwrap(result) + return result end """ @@ -389,10 +433,13 @@ Return the variable/subsystem of `sys` referred to by vertex `vert`. """ function variable_from_vertex(sys::AbstractSystem, vert::ConnectionVertex) value = iterative_getproperty(sys, vert.name) - value isa AbstractSystem && return value + value isa System && return value + value = value::SymbolicT vert.type <: Union{InputVar, OutputVar} || return value + vert.type === InputVar{CartesianIndex()} && return value + vert.type === OutputVar{CartesianIndex()} && return value # index possibly array causal variable - unwrap(wrap(value)[index_from_type(vert.type)]) + value[index_from_type(vert.type)]::SymbolicT end """ @@ -408,7 +455,7 @@ function returned from [`generate_isouter`](@ref) for the system referred to by `namespace` must not contain the name of the root system. """ function generate_connectionsets!(connection_state::AbstractConnectionState, - namespace::Vector{Symbol}, connected, isouter) + namespace::Vector{Symbol}, connected, isouter::IsOuter) initial_len = length(namespace) _generate_connectionsets!(connection_state, namespace, connected, isouter) # Enforce postcondition as a sanity check that the namespacing is implemented correctly @@ -416,53 +463,47 @@ function generate_connectionsets!(connection_state::AbstractConnectionState, return nothing end -function _generate_connectionsets!(connection_state::AbstractConnectionState, - namespace::Vector{Symbol}, - connected_vars::Union{ - AbstractVector{SymbolicWithNameof}, Tuple{Vararg{SymbolicWithNameof}}}, - isouter) - # unwrap the `SymbolicWithNameof` into the contained symbolic variables. - connected_vars = map(x -> x.var, connected_vars) - _generate_connectionsets!(connection_state, namespace, connected_vars, isouter) +@noinline function throw_both_input_output(var::SymbolicT, connected_vars::Vector{SymbolicT}) + names = join(string.(connected_vars), ", ") + throw(ArgumentError(""" + Variable $var in connection `connect($names)` is both input and output. + """)) +end +@noinline function throw_not_input_output(var::SymbolicT, connected_vars::Vector{SymbolicT}) + names = join(string.(connected_vars), ", ") + throw(ArgumentError(""" + Variable $var in connection `connect($names)` is neither input nor output. + """)) end -function _generate_connectionsets!(connection_state::AbstractConnectionState, - namespace::Vector{Symbol}, - connected_vars::Union{ - AbstractVector{<:BasicSymbolic}, Tuple{Vararg{BasicSymbolic}}}, - isouter) - # NOTE: variable connections don't populate the domain network - - # wrap to be able to call `eachindex` on a non-array variable - representative = wrap(first(connected_vars)) +function _generate_connectionsets_with_idxs!(connection_state::AbstractConnectionState, + namespace::Vector{Symbol}, connected_vars::Vector{SymbolicT}, isouter::IsOuter, + idxs::CartesianIndices{N, NTuple{N, UnitRange{Int}}}) where {N} # all of them have the same size, but may have different axes/shape # so we iterate over `eachindex(eachindex(..))` since that is identical for all - for sz_i in eachindex(eachindex(representative)) - hyperedge = map(connected_vars) do var - var = unwrap(var) + for sz_i in eachindex(idxs) + hyperedge = ConnectionVertex[] + for var in connected_vars var_ns = namespace_hierarchy(getname(var)) - i = eachindex(wrap(var))[sz_i] - + if N === 0 + i = sz_i + else + i = (eachindex(var)::CartesianIndices{N, NTuple{N, UnitRange{Int}}})[sz_i]::CartesianIndex{N} + end is_input = isinput(var) is_output = isoutput(var) if is_input && is_output - names = join(string.(connected_vars), ", ") - throw(ArgumentError(""" - Variable $var in connection `connect($names)` is both input and output. - """)) + throw_both_input_output(var, connected_vars) elseif is_input type = InputVar{i} elseif is_output type = OutputVar{i} else - names = join(string.(connected_vars), ", ") - throw(ArgumentError(""" - Variable $var in connection `connect($names)` is neither input nor output. - """)) + throw_not_input_output(var, connected_vars) end - - return ConnectionVertex( + vert = ConnectionVertex( [namespace; var_ns], length(var_ns) == 1 || isouter(var_ns[1]), type) + push!(hyperedge, vert) end add_connection_edge!(connection_state, hyperedge) @@ -480,10 +521,36 @@ end function _generate_connectionsets!(connection_state::AbstractConnectionState, namespace::Vector{Symbol}, - systems::Union{AbstractVector{<:AbstractSystem}, Tuple{Vararg{AbstractSystem}}}, - isouter) + connected_vars::Vector{SymbolicT}, + isouter::IsOuter) + # NOTE: variable connections don't populate the domain network + + representative = first(connected_vars) + idxs = eachindex(representative) + # Manual dispatch for common cases + if idxs isa CartesianIndices{0, Tuple{}} + _generate_connectionsets_with_idxs!(connection_state, namespace, connected_vars, + isouter, idxs) + elseif idxs isa CartesianIndices{1, Tuple{UnitRange{Int}}} + _generate_connectionsets_with_idxs!(connection_state, namespace, connected_vars, + isouter, idxs) + elseif idxs isa CartesianIndices{2, NTuple{2, UnitRange{Int}}} + _generate_connectionsets_with_idxs!(connection_state, namespace, connected_vars, + isouter, idxs) + else + # Dynamic dispatch + _generate_connectionsets_with_idxs!(connection_state, namespace, connected_vars, + isouter, idxs) + end +end + +function _generate_connectionsets!(connection_state::AbstractConnectionState, + namespace::Vector{Symbol}, + systems::Vector{T}, + isouter::IsOuter) where {T <: AbstractSystem} + systems = systems::Vector{System} regular_systems = System[] - domain_system = nothing + domain_system::Union{Nothing, System} = nothing for s in systems if is_domain_connector(s) if domain_system === nothing @@ -518,7 +585,7 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, push!(domain_hyperedge, domain_vertex) push!(hyperedge, dv_vertex) - for (i, sys) in enumerate(systems) + for sys in systems sts = unknowns(sys) sys_is_outer = isouter(sys) @@ -526,6 +593,7 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, # are properly namespaced sysname = nameof(sys) sys_ns = namespace_hierarchy(sysname) + N = length(namespace) append!(namespace, sys_ns) for v in sts vtype = get_connection_type(v) @@ -539,7 +607,7 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, push!(domain_hyperedge, sys_vertex) end # remember to remove the added namespace! - foreach(_ -> pop!(namespace), sys_ns) + resize!(namespace, N) end @assert length(hyperedge) > 1 @assert length(domain_hyperedge) == length(hyperedge) @@ -553,10 +621,10 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, # Add 9 orientation variables if connection is between multibody frames if isframe(sys1) # Multibody O = ori(sys1) - orientation_vars = Symbolics.unwrap.(collect(vec(O.R))) - sys1_dvs = [sys1_dvs; orientation_vars] + orientation_vars = vec(O.R) + sys1_dvs = SymbolicT[sys1_dvs; orientation_vars] end - sys1_dvs_set = Set(sys1_dvs) + sys1_dvs_set = Set{SymbolicT}(sys1_dvs) num_unknowns = length(sys1_dvs) # We first build sets of all vertices that are connected together @@ -567,8 +635,8 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, # Add 9 orientation variables if connection is between multibody frames if isframe(sys) # Multibody O = ori(sys) - orientation_vars = Symbolics.unwrap.(vec(O.R)) - unknown_vars = [unknown_vars; orientation_vars] + orientation_vars = vec(O.R) + unknown_vars = SymbolicT[unknown_vars; orientation_vars] end # Error if any subsequent systems do not have the same number of unknowns # or have unknowns not in the others. @@ -580,6 +648,7 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, # are properly namespaced sysname = nameof(sys) sys_ns = namespace_hierarchy(sysname) + N = length(namespace) append!(namespace, sys_ns) sys_is_outer = isouter(sys) for (j, v) in enumerate(unknown_vars) @@ -588,7 +657,7 @@ function _generate_connectionsets!(connection_state::AbstractConnectionState, domain_vertex = ConnectionVertex(namespace) push!(domain_hyperedge, domain_vertex) # remember to remove the added namespace! - foreach(_ -> pop!(namespace), sys_ns) + resize!(namespace, N) end for var_set in var_sets # all connected variables should have the same type @@ -630,35 +699,39 @@ can be pushed, unmodified. Connection equations update the given `state`. The eq present at the path in the hierarchical system given by `namespace`. `isouter` is the function returned from `generate_isouter`. """ -function handle_maybe_connect_equation!(eqs, state::AbstractConnectionState, - eq::Equation, namespace::Vector{Symbol}, isouter) - lhs = eq.lhs - rhs = eq.rhs +function handle_maybe_connect_equation!(eqs::Vector{Equation}, state::AbstractConnectionState, + eq::Equation, namespace::Vector{Symbol}, isouter::IsOuter) + lhs = value(eq.lhs) + rhs = value(eq.rhs) if !(lhs isa Connection) # split connections and equations - if eq.lhs isa AbstractArray || eq.rhs isa AbstractArray - append!(eqs, Symbolics.scalarize(eq)) - else - push!(eqs, eq) - end + push!(eqs, eq) return end + lhs = lhs::Connection + rhs = rhs::Connection + handle_maybe_connect_equation!(state, lhs, rhs, namespace, isouter) +end +function handle_maybe_connect_equation!(state::AbstractConnectionState, + lhs::Connection, rhs::Connection, namespace::Vector{Symbol}, isouter::IsOuter) if get_systems(lhs) === :domain # This is a domain connection, so we only update the domain connection graph - hyperedge = map(get_systems(rhs)) do sys - sys isa AbstractSystem || error("Domain connections can only connect systems!") + syss = get_systems(rhs)::Vector{System} + hyperedge = ConnectionVertex[] + for sys in syss sysname = nameof(sys) sys_ns = namespace_hierarchy(sysname) + N = length(namespace) append!(namespace, sys_ns) vertex = ConnectionVertex(namespace) - foreach(_ -> pop!(namespace), sys_ns) - return vertex + resize!(namespace, N) + push!(hyperedge, vertex) end add_domain_connection_edge!(state, hyperedge) else - connected_systems = get_systems(rhs) + connected_systems = get_systems(rhs)::Union{Vector{System}, Vector{SymbolicT}} generate_connectionsets!(state, namespace, connected_systems, isouter) end return nothing @@ -712,12 +785,13 @@ function _generate_connection_set!(connection_state::ConnectionState, end # go through the removed connections and update the negative graph - for conn in something(get_ignored_connections(sys), ()) - eq = Equation(Connection(), conn) - # there won't be any standard equations, so we can pass `nothing` instead of - # `eqs`. - handle_maybe_connect_equation!( - nothing, negative_connection_state, eq, namespace, isouter) + ignored = get_ignored_connections(sys) + if ignored isa Vector{Connection} + for conn in ignored + # there won't be any standard equations, so we can pass `nothing` instead of + # `eqs`. + handle_maybe_connect_equation!(negative_connection_state, Connection(), conn, namespace, isouter) + end end # all connectors are eventually inside connectors, and all flow variables @@ -732,17 +806,35 @@ function _generate_connection_set!(connection_state::ConnectionState, end pop!(namespace) end - - # recurse down the hierarchy - @set! sys.systems = map(subsys) do s - generate_connection_set!(connection_state, negative_connection_state, s, namespace) + new_systems = System[] + for s in subsys + news = generate_connection_set!(connection_state, negative_connection_state, s, namespace) + push!(new_systems, news) end + # recurse down the hierarchy + @set! sys.systems = new_systems @set! sys.eqs = eqs # Remember to pop the name at the end! does_namespacing(sys) && pop!(namespace) return sys end +function _flow_equations_from_idxs!(sys::AbstractSystem, eqs::Vector{Equation}, cset::Vector{ConnectionVertex}, len::Int) + add_buffer = SymbolicT[] + # each variable can have different axes, but they all have the same size + for sz_i in 1:len + empty!(add_buffer) + for cvert in cset + v = variable_from_vertex(sys, cvert)::SymbolicT + vidxs = SU.stable_eachindex(v) + v = v[vidxs[sz_i]] + push!(add_buffer, cvert.isouter ? -v : v) + end + rhs = SU.add_worker(VartypeT, add_buffer) + push!(eqs, Symbolics.COMMON_ZERO ~ rhs) + end +end + """ $(TYPEDSIGNATURES) @@ -756,7 +848,7 @@ function generate_connection_equations_and_stream_connections( for cset in csets cvert = cset[1] - var = variable_from_vertex(sys, cvert)::BasicSymbolic + var = variable_from_vertex(sys, cvert)::SymbolicT vtype = cvert.type if vtype <: Union{InputVar, OutputVar} length(cset) > 1 || continue @@ -782,10 +874,10 @@ function generate_connection_equations_and_stream_connections( end end root_vert = something(inner_output, outer_input) - root_var = variable_from_vertex(sys, root_vert) + root_var = variable_from_vertex(sys, root_vert)::SymbolicT for cvert in cset isequal(cvert, root_vert) && continue - push!(eqs, variable_from_vertex(sys, cvert) ~ root_var) + push!(eqs, variable_from_vertex(sys, cvert)::SymbolicT ~ root_var) end elseif vtype === Stream push!(stream_connections, cset) @@ -793,48 +885,39 @@ function generate_connection_equations_and_stream_connections( # arrays have to be broadcasted to be added/subtracted/negated which leads # to bad-looking equations. Just generate scalar equations instead since # mtkcompile will scalarize anyway. - representative = variable_from_vertex(sys, cset[1]) - # each variable can have different axes, but they all have the same size - for sz_i in eachindex(eachindex(wrap(representative))) - rhs = 0 - for cvert in cset - # all of this wrapping/unwrapping is necessary because the relevant - # methods are defined on `Arr/Num` and not `BasicSymbolic`. - v = variable_from_vertex(sys, cvert)::BasicSymbolic - idxs = eachindex(wrap(v)) - v = unwrap(wrap(v)[idxs[sz_i]]) - rhs += cvert.isouter ? unwrap(-wrap(v)) : v - end - push!(eqs, 0 ~ rhs) - end + representative = variable_from_vertex(sys, cset[1])::SymbolicT + _flow_equations_from_idxs!(sys, eqs, cset, length(representative)::Int) else # Equality - vars = map(Base.Fix1(variable_from_vertex, sys), cset) - outer_input = inner_output = nothing + vars = SymbolicT[] + for cvar in cset + push!(vars, variable_from_vertex(sys, cvar)::SymbolicT) + end + outer_input = inner_output = 0 all_io = true # attempt to interpret the equality as a causal connectionset if # possible - for (cvert, vert) in zip(cset, vars) + for (i, vert) in enumerate(vars) is_i = isinput(vert) is_o = isoutput(vert) all_io &= is_i || is_o all_io || break if cvert.isouter && is_i && outer_input === nothing - outer_input = cvert + outer_input = i elseif !cvert.isouter && is_o && inner_output === nothing - inner_output = cvert + inner_output = i end end # this doesn't necessarily mean this is a well-structured causal connection, # but it is sufficient and we're generating equalities anyway. - if all_io && xor(outer_input !== nothing, inner_output !== nothing) - root_vert = something(inner_output, outer_input) - root_var = variable_from_vertex(sys, root_vert) - for (cvert, var) in zip(cset, vars) - isequal(cvert, root_vert) && continue + if all_io && xor(!iszero(outer_input), !iszero(inner_output)) + root_vert_i = iszero(outer_input) ? inner_output : outer_input + root_var = vars[root_vert_i] + for (i, var) in enumerate(vars) + i == root_vert_i && continue push!(eqs, var ~ root_var) end else - base = variable_from_vertex(sys, cset[1]) + base = vars[1] for i in 2:length(cset) v = vars[i] push!(eqs, base ~ v) @@ -848,19 +931,23 @@ end """ $(TYPEDSIGNATURES) -Generate the defaults for parameters in the domain sets given by `domain_csets`. +Generate the bindings for parameters in the domain sets given by `domain_csets`. """ -function domain_defaults( +function get_domain_bindings( sys::AbstractSystem, domain_csets::Vector{Vector{ConnectionVertex}}) - defs = Dict() + binds = SymmapT() for cset in domain_csets - systems = map(Base.Fix1(variable_from_vertex, sys), cset) - @assert all(x -> x isa AbstractSystem, systems) + systems = System[] + for cvar in cset + push!(systems, variable_from_vertex(sys, cvar)::System) + end idx = findfirst(is_domain_connector, systems) idx === nothing && continue + idx = idx::Int domain_sys = systems[idx] # note that these will not be namespaced with `domain_sys`. - domain_defs = defaults(domain_sys) + domain_binds = bindings(domain_sys) + domain_ics = initial_conditions(domain_sys) for (j, csys) in enumerate(systems) j == idx && continue if is_domain_connector(csys) @@ -869,13 +956,15 @@ function domain_defaults( """)) end for par in parameters(csys) - defval = get(domain_defs, par, nothing) + defval = @something(get(domain_binds, par, nothing), + get(domain_ics, par, nothing), Some(nothing)) defval === nothing && continue - defs[parameters(csys, par)] = parameters(domain_sys, par) + binds[renamespace(csys, par)] = renamespace(domain_sys, par) end end end - return defs + binds = no_override_merge!(binds, bindings(sys)) + return binds end """ @@ -902,12 +991,12 @@ function expand_connections(sys::AbstractSystem; tol = 1e-10) eqs[i], instream_subs; maxiters = max(length(instream_subs), 10)) end end - # get the defaults for domain networks - d_defs = domain_defaults(sys, domain_csets) + # set the bindingss for domain networks + newbinds = get_domain_bindings(sys, domain_csets) # build the new system sys = flatten(sys, true) @set! sys.eqs = eqs - @set! sys.defaults = merge(get_defaults(sys), d_defs) + @set sys.bindings = newbinds end """ @@ -917,16 +1006,24 @@ Given a connection vertex `cvert` referring to a variable in a connector in `sys the flow variable in that connector. """ function get_flowvar(sys::AbstractSystem, cvert::ConnectionVertex) - parent_names = @view cvert.name[1:(end - 1)] - parent_sys = iterative_getproperty(sys, parent_names) + tmp = pop!(cvert.name) + parent_sys = iterative_getproperty(sys, cvert.name)::System + push!(cvert.name, tmp) for var in unknowns(parent_sys) type = get_connection_type(var) - type == Flow || continue - return unwrap(unknowns(parent_sys, var)) + type === Flow || continue + return renamespace(parent_sys, var) end throw(ArgumentError("There is no flow variable in system `$(nameof(parent_sys))`")) end +function instream_is_atomic(ex::SymbolicT) + Moshi.Match.@match ex begin + BSImpl.Term(; f) && if f === instream end => true + _ => false + end +end + """ $(TYPEDSIGNATURES) @@ -939,43 +1036,54 @@ function expand_instream(csets::Vector{Vector{ConnectionVertex}}, sys::AbstractS tol = 1e-8) eqs = equations(sys) # collect all `instream` terms in the equations - instream_exprs = Set{BasicSymbolic}() + instream_exprs = Set{SymbolicT}() for eq in eqs - collect_instream!(instream_exprs, eq) + SU.search_variables!(instream_exprs, eq; is_atomic = instream_is_atomic) end # specifically substitute `instream(x[i]) => instream(x)[i]` - instream_subs = Dict{BasicSymbolic, BasicSymbolic}() + instream_subs = Dict{SymbolicT, SymbolicT}() for expr in instream_exprs - stream_var = only(arguments(expr)) - iscall(stream_var) && operation(stream_var) === getindex || continue - args = arguments(stream_var) - new_expr = Symbolics.array_term( - instream, args[1]; size = size(args[1]), ndims = ndims(args[1]))[args[2:end]...] - instream_subs[expr] = new_expr + exargs = Moshi.Data.variant_getfield(expr, BSImpl.Term{VartypeT}, :args) + stream_var = only(exargs) + Moshi.Match.@match stream_var begin + BSImpl.Term(; f, args, type, shape) && if f === getindex end => begin + newargs = copy(parent(args)) + arg = newargs[1] + sharg = SU.shape(arg) + starg = SU.symtype(arg) + newargs[1] = BSImpl.Term{VartypeT}(instream, SArgsT((arg,)); type = starg, shape = sharg) + new_expr = BSImpl.Term{VartypeT}(getindex, newargs; type, shape) + instream_subs[expr] = new_expr + end + _ => nothing + end end # for all the newly added `instream(x)[i]`, add `instream(x)` to `instream_exprs` # also remove all `instream(x[i])` for (k, v) in instream_subs - push!(instream_exprs, arguments(v)[1]) + push!(instream_exprs, Moshi.Match.@match v begin + BSImpl.Term(; args) => args[1] + end) delete!(instream_exprs, k) end # This is an implementation of the modelica spec # https://specification.modelica.org/maint/3.6/stream-connectors.html additional_eqs = Equation[] + add_buffer = SymbolicT[] for cset in csets n_outer = count(cvert -> cvert.isouter, cset) n_inner = length(cset) - n_outer if n_inner == 1 && n_outer == 0 cvert = only(cset) - stream_var = variable_from_vertex(sys, cvert)::BasicSymbolic + stream_var = variable_from_vertex(sys, cvert)::SymbolicT instream_subs[instream(stream_var)] = stream_var elseif n_inner == 2 && n_outer == 0 cvert1, cvert2 = cset - stream_var1 = variable_from_vertex(sys, cvert1)::BasicSymbolic - stream_var2 = variable_from_vertex(sys, cvert2)::BasicSymbolic + stream_var1 = variable_from_vertex(sys, cvert1)::SymbolicT + stream_var2 = variable_from_vertex(sys, cvert2)::SymbolicT instream_subs[instream(stream_var1)] = stream_var2 instream_subs[instream(stream_var2)] = stream_var1 elseif n_inner == 1 && n_outer == 1 @@ -983,14 +1091,14 @@ function expand_instream(csets::Vector{Vector{ConnectionVertex}}, sys::AbstractS if cvert_inner.isouter cvert_inner, cvert_outer = cvert_outer, cvert_inner end - streamvar_inner = variable_from_vertex(sys, cvert_inner)::BasicSymbolic - streamvar_outer = variable_from_vertex(sys, cvert_outer)::BasicSymbolic + streamvar_inner = variable_from_vertex(sys, cvert_inner)::SymbolicT + streamvar_outer = variable_from_vertex(sys, cvert_outer)::SymbolicT instream_subs[instream(streamvar_inner)] = instream(streamvar_outer) push!(additional_eqs, (streamvar_outer ~ streamvar_inner)) elseif n_inner == 0 && n_outer == 2 cvert1, cvert2 = cset - stream_var1 = variable_from_vertex(sys, cvert1)::BasicSymbolic - stream_var2 = variable_from_vertex(sys, cvert2)::BasicSymbolic + stream_var1 = variable_from_vertex(sys, cvert1)::SymbolicT + stream_var2 = variable_from_vertex(sys, cvert2)::SymbolicT push!(additional_eqs, (stream_var1 ~ instream(stream_var2)), (stream_var2 ~ instream(stream_var1))) else @@ -998,56 +1106,71 @@ function expand_instream(csets::Vector{Vector{ConnectionVertex}}, sys::AbstractS # implementation of stream connectors in the Modelica spec v3.6 section 15.2. # https://specification.modelica.org/maint/3.6/stream-connectors.html#instream-and-connection-equations # We could implement the "if" case using variable bounds? It would be nice to - # move that metadata to the system (storing it similar to `defaults`). - outer_cverts = filter(cvert -> cvert.isouter, cset) - inner_cverts = filter(cvert -> !cvert.isouter, cset) - - outer_streamvars = map(Base.Fix1(variable_from_vertex, sys), outer_cverts) - inner_streamvars = map(Base.Fix1(variable_from_vertex, sys), inner_cverts) - - outer_flowvars = map(Base.Fix1(get_flowvar, sys), outer_cverts) - inner_flowvars = map(Base.Fix1(get_flowvar, sys), inner_cverts) + # move that metadata to the system (storing it similar to `initial_conditions`). + outer_cverts = ConnectionVertex[] + inner_cverts = ConnectionVertex[] + outer_streamvars = SymbolicT[] + inner_streamvars = SymbolicT[] + outer_flowvars = SymbolicT[] + inner_flowvars = SymbolicT[] + for cvert in cset + svar = variable_from_vertex(sys, cvert)::SymbolicT + fvar = get_flowvar(sys, cvert)::SymbolicT + push!(cvert.isouter ? outer_cverts : inner_cverts, cvert) + push!(cvert.isouter ? outer_streamvars : inner_streamvars, svar) + push!(cvert.isouter ? outer_flowvars : inner_flowvars, fvar) + end - mask = trues(length(inner_cverts)) for inner_i in eachindex(inner_cverts) - # mask out the current variable - mask[inner_i] = false svar = inner_streamvars[inner_i] - instream_subs[instream(svar)] = term( - instream_rt, Val(n_inner - 1), Val(n_outer), inner_flowvars[mask]..., - inner_streamvars[mask]..., outer_flowvars..., outer_streamvars...) - # make sure to reset the mask - mask[inner_i] = true + args = SArgsT() + push!(args, SU.Const{VartypeT}(Val(n_inner - 1))) + push!(args, SU.Const{VartypeT}(Val(n_outer))) + for i in eachindex(inner_cverts) + i == inner_i && continue + push!(args, inner_flowvars[i]) + end + for i in eachindex(inner_cverts) + i == inner_i && continue + push!(args, inner_streamvars[i]) + end + append!(args, outer_flowvars) + append!(args, outer_streamvars) + expr = BSImpl.Term{VartypeT}(instream_rt, args; + type = Real, shape = SU.ShapeVecT()) + instream_subs[instream(svar)] = expr end for q in 1:n_outer - sq = mapreduce(+, inner_flowvars) do fvar - max(-fvar, 0) + empty!(add_buffer) + for fvar in inner_flowvars + push!(add_buffer, max(-fvar, 0)) end - sq += mapreduce(+, enumerate(outer_flowvars)) do (outer_i, fvar) - outer_i == q && return 0 - max(fvar, 0) + for (i, fvar) in enumerate(outer_flowvars) + i == q && continue + push!(add_buffer, max(fvar, 0)) end - # sanity check to make sure it isn't going to codegen a `mapreduce` - @assert operation(sq) == (+) + sq = SU.add_worker(VartypeT, add_buffer) - num = mapreduce(+, inner_flowvars, inner_streamvars) do fvar, svar - positivemax(-fvar, sq; tol) * svar + empty!(add_buffer) + for (fvar, svar) in zip(inner_flowvars, inner_streamvars) + push!(add_buffer, positivemax(-fvar, sq; tol) * svar) end - num += mapreduce( - +, enumerate(outer_flowvars), outer_streamvars) do (outer_i, fvar), svar - outer_i == q && return 0 - positivemax(fvar, sq; tol) * instream(svar) + for (i, (fvar, svar)) in enumerate(zip(outer_flowvars, outer_streamvars)) + i == q && continue + push!(add_buffer, positivemax(fvar, sq; tol) * instream(svar)) end - @assert operation(num) == (+) + num = SU.add_worker(VartypeT, add_buffer) - den = mapreduce(+, inner_flowvars) do fvar - positivemax(-fvar, sq; tol) + empty!(add_buffer) + for fvar in inner_flowvars + push!(add_buffer, positivemax(-fvar, sq; tol)) end - den += mapreduce(+, enumerate(outer_flowvars)) do (outer_i, fvar) - outer_i == q && return 0 - positivemax(fvar, sq; tol) + for (i, fvar) in enumerate(outer_flowvars) + i == q && continue + push!(add_buffer, positivemax(fvar, sq; tol)) end + den = SU.add_worker(VartypeT, add_buffer) push!(additional_eqs, (outer_streamvars[q] ~ num / den)) end @@ -1118,4 +1241,4 @@ function instream_rt(ins::Val{inner_n}, outs::Val{outer_n}, for k in 1:M and ck.m_flow.max > 0 =# end -SymbolicUtils.promote_symtype(::typeof(instream_rt), ::Vararg) = Real +SymbolicUtils.promote_symtype(::typeof(instream_rt), _...) = Real diff --git a/src/systems/dependency_graphs.jl b/lib/ModelingToolkitBase/src/systems/dependency_graphs.jl similarity index 97% rename from src/systems/dependency_graphs.jl rename to lib/ModelingToolkitBase/src/systems/dependency_graphs.jl index 344526add0..c6bbb2e1f1 100644 --- a/src/systems/dependency_graphs.jl +++ b/lib/ModelingToolkitBase/src/systems/dependency_graphs.jl @@ -14,8 +14,8 @@ Notes: Example: ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t @parameters β γ κ η @variables S(t) I(t) R(t) @@ -23,8 +23,8 @@ rate₁ = β * S * I rate₂ = γ * I + t affect₁ = [S ~ S - 1, I ~ I + 1] affect₂ = [I ~ I - 1, R ~ R + 1] -j₁ = ModelingToolkit.ConstantRateJump(rate₁, affect₁) -j₂ = ModelingToolkit.VariableRateJump(rate₂, affect₂) +j₁ = ModelingToolkitBase.ConstantRateJump(rate₁, affect₁) +j₂ = ModelingToolkitBase.VariableRateJump(rate₂, affect₂) # create a JumpSystem using these jumps @named jumpsys = JumpSystem([j₁, j₂], t, [S, I, R], [β, γ]) diff --git a/src/systems/diffeqs/basic_transformations.jl b/lib/ModelingToolkitBase/src/systems/diffeqs/basic_transformations.jl similarity index 87% rename from src/systems/diffeqs/basic_transformations.jl rename to lib/ModelingToolkitBase/src/systems/diffeqs/basic_transformations.jl index 200e0d3d56..72fa922425 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/lib/ModelingToolkitBase/src/systems/diffeqs/basic_transformations.jl @@ -14,7 +14,7 @@ the final value of `trJ` is the probability of ``u(t)``. Example: ```julia -using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkitBase, OrdinaryDiffEq @independent_variables t @parameters α β γ δ @@ -62,7 +62,7 @@ Generates the set of ODEs after change of variables. Example: ```julia -using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkitBase, OrdinaryDiffEq, Test # Change of variables: z = log(x) # (this implies that x = exp(z) is automatically non-negative) @@ -76,7 +76,7 @@ eqs = [D(x) ~ α*x] tspan = (0., 1.) def = [x => 1.0, α => -0.5] -@mtkcompile sys = System(eqs, t;defaults=def) +@mtkcompile sys = System(eqs, t; initial_conditions = def) prob = ODEProblem(sys, [], tspan) sol = solve(prob, Tsit5()) @@ -142,7 +142,7 @@ function change_of_variables( for (new_var, ex, first, second) in zip(new_vars, dfdt, ∂f∂x, ∂2f∂x2) for (eqs, neq) in zip(old_eqs, neqs) - if occursin(value(eqs.lhs), value(ex)) + if SU.query(isequal(value(eqs.lhs)), value(ex)) ex = substitute(ex, eqs.lhs => eqs.rhs) if isSDE for (noise, B) in zip(neq, brownvars) @@ -159,24 +159,24 @@ function change_of_variables( push!(new_eqs, Differential(t)(new_var) ~ ex) end - defs = get_defaults(sys) - new_defs = Dict() + ics = get_initial_conditions(sys) + new_ics = SymmapT() for f_sub in forward_subs - ex = substitute(first(f_sub), defs) + ex = substitute(first(f_sub), ics) if !ismissing(t0) ex = substitute(ex, t => t0) end - new_defs[last(f_sub)] = ex + write_possibly_indexed_array!(new_ics, unwrap(last(f_sub)), unwrap(ex), COMMON_NOTHING) end for para in parameters(sys) - if haskey(defs, para) - new_defs[para] = defs[para] + if haskey(ics, para) + new_ics[para] = ics[para] end end @named new_sys = System( vcat(new_eqs, first.(backward_subs) .~ last.(backward_subs)), t; - defaults = new_defs, + initial_conditions = new_ics, observed = observed(sys) ) if simplify @@ -260,7 +260,7 @@ function fractional_to_ordinary( push!(def, new_z=>zeros(N-M)) else new_z = only(@variables $new_z(iv)[1:N-M, 1:m]) - new_eq = D(new_z) ~ -γs*new_z + hcat(fill(sub_eq, N-M, 1), collect(new_z[:, 1:m-1]*diagm(1:m-1))) + new_eq = D(new_z) ~ -γs*new_z + BS{VartypeT}[fill(sub_eq, N-M, 1);; collect(new_z[:, 1:m-1]*diagm(1:m-1))] rhs = coeff*sum(cs[i]*new_z[i, m] for i in 1:N-M) for (index, value) in enumerate(initial) rhs += value * iv^(index - 1) / gamma(index) @@ -274,7 +274,7 @@ function fractional_to_ordinary( for index in range(M, N-1; step=1) new_z = Symbol(:ʐ, :_, i) i += 1 - new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_z = ModelingToolkitBase.unwrap(only(@variables $new_z(iv))) new_eq = D(new_z) ~ sub_eq - γ_i(index)*new_z push!(new_eqs, new_eq) push!(def, new_z=>0) @@ -292,7 +292,7 @@ function fractional_to_ordinary( base = sub_eq for k in range(1, m; step=1) new_z = Symbol(:ʐ, :_, index-M, :_, k) - new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_z = ModelingToolkitBase.unwrap(only(@variables $new_z(iv))) new_eq = D(new_z) ~ base - γ*new_z base = k * new_z push!(new_eqs, new_eq) @@ -312,7 +312,7 @@ function fractional_to_ordinary( append!(all_def, def) end append!(all_eqs, additional_eqs) - @named sys = System(all_eqs, iv; defaults=all_def) + @named sys = System(all_eqs, iv; initial_conditions=all_def) return mtkcompile(sys) end @@ -341,7 +341,7 @@ function linear_fractional_to_ordinary( initials = 0, symbol = :x, iv = only(@independent_variables t), matrix=false ) previous = Symbol(symbol, :_, 0) - previous = ModelingToolkit.unwrap(only(@variables $previous(iv))) + previous = ModelingToolkitBase.unwrap(only(@variables $previous(iv))) @variables x_0(iv) D = Differential(iv) i = 0 @@ -384,7 +384,7 @@ function linear_fractional_to_ordinary( for index in range(M, N-1; step=1) new_z = Symbol(:ʐ, :_, i) i += 1 - new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_z = ModelingToolkitBase.unwrap(only(@variables $new_z(iv))) new_eq = D(new_z) ~ sub_eq - γ_i(index)*new_z push!(new_eqs, new_eq) push!(def, new_z=>0) @@ -396,7 +396,7 @@ function linear_fractional_to_ordinary( for i in range(1, ceil(Int, degrees[1]); step=1) new_x = Symbol(symbol, :_, i) - new_x = ModelingToolkit.unwrap(only(@variables $new_x(iv))) + new_x = ModelingToolkitBase.unwrap(only(@variables $new_x(iv))) push!(all_eqs, D(previous) ~ new_x) push!(all_def, previous => initials[i]) previous = new_x @@ -406,7 +406,7 @@ function linear_fractional_to_ordinary( for (degree, coeff) in zip(degrees, coeffs) rounded = ceil(Int, degree) new_x = Symbol(symbol, :_, rounded) - new_x = ModelingToolkit.unwrap(only(@variables $new_x(iv))) + new_x = ModelingToolkitBase.unwrap(only(@variables $new_x(iv))) if isinteger(degree) new_rhs += coeff * new_x else @@ -417,14 +417,14 @@ function linear_fractional_to_ordinary( end end push!(all_eqs, 0 ~ new_rhs) - @named sys = System(all_eqs, iv; defaults=all_def) + @named sys = System(all_eqs, iv; initial_conditions=all_def) return mtkcompile(sys) end """ change_independent_variable( sys::System, iv, eqs = []; - add_old_diff = false, simplify = true, fold = false + add_old_diff = false, simplify = true, fold = Val(false) ) Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``). @@ -470,7 +470,7 @@ julia> M = change_independent_variable(M, x); julia> M = mtkcompile(M; allow_symbolic = true); julia> unknowns(M) -3-element Vector{SymbolicUtils.BasicSymbolic{Real}}: +3-element Vector{Symbolics.SymbolicsT}: xˍt(x) y(x) yˍx(x) @@ -478,7 +478,7 @@ julia> unknowns(M) """ function change_independent_variable( sys::System, iv, eqs = []; - add_old_diff = false, simplify = true, fold = false + add_old_diff = false, simplify = true, fold = Val(false) ) iv2_of_iv1 = unwrap(iv) # e.g. u(t) iv1 = get_iv(sys) # e.g. t @@ -511,8 +511,8 @@ function change_independent_variable( div2_of_iv1 = GlobalScope(diff2term_with_unit(D1(iv2_of_iv1), iv1)) end - div2_of_iv2 = substitute(div2_of_iv1, iv1 => iv2) # e.g. uˍt(u) - div2_of_iv2_of_iv1 = substitute(div2_of_iv2, iv2 => iv2_of_iv1) # e.g. uˍt(u(t)) + div2_of_iv2 = substitute(div2_of_iv1, iv1 => iv2; filterer = Returns(true)) # e.g. uˍt(u) + div2_of_iv2_of_iv1 = substitute(div2_of_iv2, iv2 => iv2_of_iv1; filterer = Returns(true)) # e.g. uˍt(u(t)) # If requested, add a differential equation for the old independent variable as a function of the old one if add_old_diff @@ -538,22 +538,29 @@ function change_independent_variable( # e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u) function transform(ex::T) where {T} # 1) Replace the argument of every function; e.g. f(t) -> f(u(t)) - for var in vars(ex; op = Nothing) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable) + rules = DerivativeDict() + for var in SU.search_variables(ex; is_atomic = OperatorIsAtomic{Nothing}()) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable) if is_function_of(var, iv1) && !isequal(var, iv2_of_iv1) # of the form f(t)? but prevent e.g. u(t) -> u(u(t)) var_of_iv1 = var # e.g. f(t) - var_of_iv2_of_iv1 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t)) - ex = substitute(ex, var_of_iv1 => var_of_iv2_of_iv1; fold) + var_of_iv2_of_iv1 = substitute(var_of_iv1, iv1 => iv2_of_iv1; filterer = Returns(true)) # e.g. f(u(t)) + rules[var_of_iv1] = var_of_iv2_of_iv1 + # ex = substitute(ex, var_of_iv1 => var_of_iv2_of_iv1; fold, filterer = Returns(true)) end end + ex = substitute(ex, rules; fold, filterer = Returns(true)) + empty!(rules) # 2) Repeatedly expand chain rule until nothing changes anymore orgex = nothing while !isequal(ex, orgex) orgex = ex # save original ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt - ex = substitute(ex, D1(iv2_of_iv1) => div2_of_iv2_of_iv1; fold) # e.g. du(t)/dt -> uˍt(u(t)) + # e.g. du(t)/dt -> uˍt(u(t)) + rules[D1(iv2_of_iv1)] = div2_of_iv2_of_iv1 + ex = substitute(ex, rules; fold, filterer = Returns(true)) + empty!(rules) end # 3) Set new independent variable - ex = substitute(ex, iv2_of_iv1 => iv2; fold) # set e.g. u(t) -> u everywhere + ex = substitute(ex, iv2_of_iv1 => iv2; fold, filterer = Returns(true)) # set e.g. u(t) -> u everywhere ex = substitute(ex, iv1 => iv1_of_iv2; fold) # set e.g. t -> t(u) everywhere return ex::T end @@ -580,9 +587,10 @@ function change_independent_variable( ps = filter(!isinitial, ps) # remove Initial(...) # TODO: shouldn't have to touch this observed = map(transform, get_observed(sys)) initialization_eqs = map(transform, get_initialization_eqs(sys)) - parameter_dependencies = map(transform, get_parameter_dependencies(sys)) - defaults = Dict(transform(var) => transform(val) - for (var, val) in get_defaults(sys)) + bindings = Dict(transform(var) => transform(val) + for (var, val) in get_bindings(sys)) + initial_conditions = Dict(transform(var) => transform(val) + for (var, val) in get_initial_conditions(sys)) guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) connector_type = get_connector_type(sys) assertions = Dict(transform(ass) => msg for (ass, msg) in get_assertions(sys)) @@ -591,13 +599,12 @@ function change_independent_variable( wasflat = isempty(systems) sys = typeof(sys)( # recreate system with transformed fields eqs, iv2, unknowns, ps; observed, initialization_eqs, - defaults, guesses, connector_type, + bindings, initial_conditions, guesses, connector_type, assertions, name = nameof(sys), description = description(sys) ) sys = compose(sys, systems) # rebuild hierarchical system if wascomplete sys = complete(sys; split = wassplit, flatten = wasflat) # complete output if input was complete - @set! sys.parameter_dependencies = parameter_dependencies end return sys end @@ -677,8 +684,8 @@ experiments. Springer Science & Business Media. # Example ```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters α β @variables x(t) y(t) z(t) @@ -692,7 +699,7 @@ noiseeqs = [β*x] u = x θ0 = 0.1 g(x) = x[1]^2 -demod = ModelingToolkit.Girsanov_transform(de, u; θ0=0.1) +demod = ModelingToolkitBase.Girsanov_transform(de, u; θ0=0.1) u0modmap = [ x => x0 @@ -768,9 +775,9 @@ function Girsanov_transform(sys::System, u; θ0 = 1.0) # return modified SDE System @set! sys.eqs = deqs - @set! sys.noise_eqs = noiseeqs + @set! sys.noise_eqs = unwrap.(noiseeqs) @set! sys.unknowns = unknown_vars - get_defaults(sys)[θ] = θ0 + get_initial_conditions(sys)[θ] = θ0 obs = observed(sys) @set! sys.observed = [weight ~ θ / θ0; obs] if get_parent(sys) !== nothing @@ -814,7 +821,7 @@ function add_accumulations(sys::System, vars::Vector{<:Pair}) D = Differential(get_iv(sys)) @set! sys.eqs = [eqs; Equation[D(a) ~ v[2] for (a, v) in zip(avars, vars)]] @set! sys.unknowns = [get_unknowns(sys); avars] - @set! sys.defaults = merge(get_defaults(sys), Dict(a => 0.0 for a in avars)) + @set! sys.initial_conditions = merge(get_initial_conditions(sys), Dict(a => 0.0 for a in avars)) return sys end @@ -836,33 +843,43 @@ function noise_to_brownians(sys::System; names::Union{Symbol, Vector{Symbol}} = if neqs === nothing throw(ArgumentError("Expected a system with `noise_eqs`.")) end + neqs = neqs::Union{Vector{SymbolicT}, Matrix{SymbolicT}} if !isempty(get_systems(sys)) throw(ArgumentError("The system must be flattened.")) end # vector means diagonal noise - nbrownians = ndims(neqs) == 1 ? length(neqs) : size(neqs, 2) - if names isa Symbol - names = [Symbol(names, :_, i) for i in 1:nbrownians] + nbrownians = if neqs isa Vector{SymbolicT} + length(neqs) + elseif neqs isa Matrix{SymbolicT} + size(neqs, 2) end - if length(names) != nbrownians + if names isa Symbol + _names = Symbol[] + for i in 1:nbrownians + push!(_names, Symbol(names, :_, i)) + end + names = _names + elseif names isa Vector{Symbol} && length(names) != nbrownians throw(ArgumentError(""" The system has $nbrownians brownian variables. Received $(length(names)) names \ for the brownian variables. Provide $nbrownians names or a single `Symbol` to use \ an array variable of the appropriately length. """)) end - brownvars = map(names) do name - unwrap(only(@brownians $name)) + names = names::Vector{Symbol} + brownvars = SymbolicT[] + for name in names + push!(brownvars, unwrap(only(@brownians $name))) end - - terms = if ndims(neqs) == 1 + terms = if neqs isa Vector{SymbolicT} neqs .* brownvars - else + elseif neqs isa Matrix{SymbolicT} neqs * brownvars end - eqs = map(get_eqs(sys), terms) do eq, term - eq.lhs ~ eq.rhs + term + eqs = Equation[] + for (eq, term) in zip(get_eqs(sys), terms) + push!(eqs, eq.lhs ~ eq.rhs + term) end @set! sys.eqs = eqs @@ -911,7 +928,7 @@ function convert_system_indepvar(sys::System, t; name = nameof(sys)) newsts[i] = ns varmap[s] = ns else - ns = variable(getname(s); T = FnType)(t) + ns = variable(getname(s); T = FnType{Tuple, Real, Nothing})(t) newsts[i] = ns varmap[s] = ns end @@ -922,7 +939,8 @@ function convert_system_indepvar(sys::System, t; name = nameof(sys)) sub.x[iv] = t # otherwise the Differentials aren't fixed end neweqs = map(sub, equations(sys)) - defs = Dict(sub(k) => sub(v) for (k, v) in defaults(sys)) + binds = Dict(sub(k) => sub(v) for (k, v) in bindings(sys)) + ics = Dict(sub(k) => sub(v) for (k, v) in initial_conditions(sys)) neqs = get_noise_eqs(sys) if neqs !== nothing neqs = map(sub, neqs) @@ -932,7 +950,8 @@ function convert_system_indepvar(sys::System, t; name = nameof(sys)) @set! sys.eqs = neweqs @set! sys.iv = t @set! sys.unknowns = newsts - @set! sys.defaults = defs + @set! sys.bindings = binds + @set! sys.initial_conditions = ics @set! sys.name = name @set! sys.noise_eqs = neqs @set! sys.constraints = cstrs @@ -984,7 +1003,7 @@ function respecialize(sys::AbstractSystem, mapping; all = false) new_ps = copy(get_ps(sys)) @set! sys.ps = new_ps - extras = [] + extras = Union{SymbolicT, Pair}[] if all for x in filter(!is_variable_numeric, get_ps(sys)) if any(y -> isequal(x, y) || y isa Pair && isequal(x, y[1]), mapping) || @@ -997,10 +1016,10 @@ function respecialize(sys::AbstractSystem, mapping; all = false) end ps_to_specialize = Iterators.flatten((extras, mapping)) - defs = copy(defaults(sys)) - @set! sys.defaults = defs + defs = copy(initial_conditions(sys)) + @set! sys.initial_conditions = defs final_defs = copy(defs) - evaluate_varmap!(final_defs, ps_to_specialize) + evaluate_varmap!(final_defs, Iterators.map(x -> x isa Pair ? x[1] : x, ps_to_specialize)) subrules = Dict() @@ -1009,13 +1028,13 @@ function respecialize(sys::AbstractSystem, mapping; all = false) k, v = element else k = element - v = get(final_defs, k, nothing) + v = value(get(final_defs, k, nothing)) @assert v !== nothing """ Parameter $k needs an associated value to be respecialized. """ @assert symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) """ Parameter $k needs an associated value to be respecialized. Found symbolic \ - default $v. + initial value $v. """ end @@ -1037,21 +1056,21 @@ function respecialize(sys::AbstractSystem, mapping; all = false) """ if iscall(k) - op = operation(k)::BasicSymbolic + op = operation(k)::SymbolicT @assert !iscall(op) - op = SymbolicUtils.Sym{SymbolicUtils.FnType{Tuple{Any}, T}}(nameof(op)) + op = SU.Sym{VartypeT}(nameof(op); type = SU.FnType{Tuple, T, Nothing}, shape = SU.shape(k)) args = arguments(k) new_p = op(args...) else - new_p = SymbolicUtils.Sym{T}(getname(k)) + new_p = SSym(getname(k); type = T, shape = SU.shape(v)) end get_ps(sys)[idx] = new_p - defaults(sys)[new_p] = v + initial_conditions(sys)[new_p] = v subrules[unwrap(k)] = unwrap(new_p) end - substituter = Base.Fix2(fast_substitute, subrules) + substituter = Base.Fix2(substitute, subrules) @set! sys.eqs = map(substituter, get_eqs(sys)) @set! sys.observed = map(substituter, get_observed(sys)) @set! sys.initialization_eqs = map(substituter, get_initialization_eqs(sys)) @@ -1059,8 +1078,8 @@ function respecialize(sys::AbstractSystem, mapping; all = false) @set! sys.noise_eqs = map(substituter, get_noise_eqs(sys)) end @set! sys.assertions = Dict([substituter(k) => v for (k, v) in assertions(sys)]) - @set! sys.parameter_dependencies = map(substituter, get_parameter_dependencies(sys)) - @set! sys.defaults = Dict([substituter(k) => substituter(v) for (k, v) in defaults(sys)]) + @set! sys.bindings = Dict([substituter(k) => substituter(v) for (k, v) in bindings(sys)]) + @set! sys.initial_conditions = Dict([substituter(k) => substituter(v) for (k, v) in initial_conditions(sys)]) @set! sys.guesses = Dict([k => substituter(v) for (k, v) in guesses(sys)]) @set! sys.continuous_events = map(get_continuous_events(sys)) do cev SymbolicContinuousCallback( diff --git a/src/systems/imperative_affect.jl b/lib/ModelingToolkitBase/src/systems/imperative_affect.jl similarity index 91% rename from src/systems/imperative_affect.jl rename to lib/ModelingToolkitBase/src/systems/imperative_affect.jl index 1c43022f4b..c686cd20e8 100644 --- a/src/systems/imperative_affect.jl +++ b/lib/ModelingToolkitBase/src/systems/imperative_affect.jl @@ -67,10 +67,10 @@ function ImperativeAffect(; f, kwargs...) ImperativeAffect(f; kwargs...) end -function Symbolics.fast_substitute(aff::ImperativeAffect, rules) - substituter = Base.Fix2(fast_substitute, rules) - ImperativeAffect(aff.f, map(substituter, aff.obs), aff.obs_syms, - map(substituter, aff.modified), aff.mod_syms, aff.ctx, aff.skip_checks) +function (s::SymbolicUtils.Substituter)(aff::ImperativeAffect) + ImperativeAffect(aff.f, s(aff.obs), aff.obs_syms, + s(aff.modified), aff.mod_syms, aff.ctx, aff.skip_checks) + end function Base.show(io::IO, mfa::ImperativeAffect) @@ -85,10 +85,16 @@ context(a::ImperativeAffect) = a.ctx observed(a::ImperativeAffect) = a.obs observed_syms(a::ImperativeAffect) = a.obs_syms function discretes(a::ImperativeAffect) - Iterators.filter(ModelingToolkit.isparameter, - Iterators.flatten(Iterators.map( - x -> symbolic_type(x) == NotSymbolic() && x isa AbstractArray ? x : [x], - a.modified))) + discs = SymbolicT[] + for val in a.modified + val = unwrap(val) + if val isa SymbolicT + isparameter(val) && push!(discs, val) + elseif val isa AbstractArray + append!(discs, filter(isparameter, map(unwrap, val))) + end + end + return discs end modified(a::ImperativeAffect) = a.modified modified_syms(a::ImperativeAffect) = a.mod_syms @@ -132,7 +138,7 @@ function namespace_affect(affect::ImperativeAffect, s) end function invalid_variables(sys, expr) - filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init = [])) + setdiff!(SU.search_variables(expr), all_symbols(sys)) end function unassignable_variables(sys, expr) @@ -140,7 +146,7 @@ function unassignable_variables(sys, expr) vcat, Symbolics.scalarize.(vcat( unknowns(sys), parameters(sys; initial_parameters = true))); init = []) - written = reduce(vcat, Symbolics.scalarize.(vars(expr)); init = []) + written = reduce(vcat, Symbolics.scalarize.(collect(SU.search_variables(expr))); init = []) return filter( x -> !any(isequal(x), assignable_syms), written) end @@ -279,19 +285,10 @@ end scalarize_affects(affects::ImperativeAffect) = affects -function vars!(vars, aff::ImperativeAffect; op = Differential) +function SU.search_variables!(vars, aff::ImperativeAffect; kwargs...) for var in Iterators.flatten((observed(aff), modified(aff))) if symbolic_type(var) == NotSymbolic() - if var isa AbstractArray - for v in var - v = unwrap(v) - vars!(vars, v) - end - end - else - var = unwrap(var) - vars!(vars, var) + SU.search_variables!(vars, v; kwargs...) end end - return vars end diff --git a/src/systems/index_cache.jl b/lib/ModelingToolkitBase/src/systems/index_cache.jl similarity index 64% rename from src/systems/index_cache.jl rename to lib/ModelingToolkitBase/src/systems/index_cache.jl index 0482e07c5f..253e81ebda 100644 --- a/src/systems/index_cache.jl +++ b/lib/ModelingToolkitBase/src/systems/index_cache.jl @@ -5,11 +5,6 @@ struct BufferTemplate length::Int end -function BufferTemplate(s::Type{<:Symbolics.Struct}, length::Int) - T = Symbolics.juliatype(s) - BufferTemplate(T, length) -end - struct Nonnumeric <: SciMLStructures.AbstractPortion end const NONNUMERIC_PORTION = Nonnumeric() @@ -33,16 +28,15 @@ struct DiscreteIndex idx_in_clock::Int end -const ParamIndexMap = Dict{BasicSymbolic, Tuple{Int, Int}} -const NonnumericMap = Dict{ - Union{BasicSymbolic, Symbolics.CallWithMetadata}, Tuple{Int, Int}} -const UnknownIndexMap = Dict{ - BasicSymbolic, Union{Int, UnitRange{Int}, AbstractArray{Int}}} -const TunableIndexMap = Dict{BasicSymbolic, - Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}}} +const MaybeUnknownArrayIndexT = Union{Int, UnitRange{Int}, AbstractArray{Int}} +const MaybeArrayIndexT = Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}} +const ParamIndexMap = Dict{SymbolicT, Tuple{Int, Int}} +const NonnumericMap = Dict{SymbolicT, Tuple{Int, Int}} +const UnknownIndexMap = Dict{SymbolicT, MaybeUnknownArrayIndexT} +const TunableIndexMap = Dict{SymbolicT, MaybeArrayIndexT} const TimeseriesSetType = Set{Union{ContinuousTimeseries, Int}} -const SymbolicParam = Union{BasicSymbolic, CallWithMetadata} +const SymbolicParam = SymbolicT struct IndexCache unknown_idx::UnknownIndexMap @@ -54,9 +48,8 @@ struct IndexCache initials_idx::TunableIndexMap constant_idx::ParamIndexMap nonnumeric_idx::NonnumericMap - observed_syms_to_timeseries::Dict{BasicSymbolic, TimeseriesSetType} - dependent_pars_to_timeseries::Dict{ - Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType} + observed_syms_to_timeseries::Dict{SymbolicT, TimeseriesSetType} + dependent_pars_to_timeseries::Dict{SymbolicT, TimeseriesSetType} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate initials_buffer_size::BufferTemplate @@ -82,26 +75,36 @@ function IndexCache(sys::AbstractSystem) let idx = 1 for sym in unks - usym = unwrap(sym) - rsym = renamespace(sys, usym) - sym_idx = if Symbolics.isarraysymbolic(sym) + rsym = renamespace(sys, sym) + sym_idx::MaybeUnknownArrayIndexT = if Symbolics.isarraysymbolic(sym) reshape(idx:(idx + length(sym) - 1), size(sym)) else idx end - unk_idxs[usym] = sym_idx + unk_idxs[sym] = sym_idx unk_idxs[rsym] = sym_idx idx += length(sym) end + found_array_syms = Set{SymbolicT}() 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)] + arrsym in found_array_syms && continue + idxs = Int[] + valid_arrsym = true + for i in eachindex(arrsym) + idxsym = arrsym[i] + idx = get(unk_idxs, idxsym, nothing)::Union{Int, Nothing} + valid_arrsym = idx !== nothing + valid_arrsym || break + push!(idxs, idx::Int) + end + push!(found_array_syms, arrsym) + valid_arrsym || break if idxs == idxs[begin]:idxs[end] - idxs = reshape(idxs[begin]:idxs[end], size(idxs)) + idxs = reshape(idxs[begin]:idxs[end], size(arrsym))::AbstractArray{Int} + else + idxs = reshape(idxs, size(arrsym))::AbstractArray{Int} end rsym = renamespace(sys, arrsym) unk_idxs[arrsym] = idxs @@ -109,68 +112,24 @@ function IndexCache(sys::AbstractSystem) end end - tunable_pars = BasicSymbolic[] - initial_pars = BasicSymbolic[] - constant_buffers = Dict{Any, Set{BasicSymbolic}}() - nonnumeric_buffers = Dict{Any, Set{SymbolicParam}}() - - function insert_by_type!(buffers::Dict{Any, S}, sym, ctype) where {S} - sym = unwrap(sym) - buf = get!(buffers, ctype, S()) - push!(buf, sym) - end - function insert_by_type!(buffers::Vector{BasicSymbolic}, sym, ctype) - sym = unwrap(sym) - push!(buffers, sym) - end - - disc_param_callbacks = Dict{SymbolicParam, Set{Int}}() - events = vcat(continuous_events(sys), discrete_events(sys)) - for (i, event) in enumerate(events) - discs = Set{SymbolicParam}() - affs = affects(event) - if !(affs isa AbstractArray) - affs = [affs] - end - for affect in affs - if affect isa AffectSystem || affect isa ImperativeAffect - union!(discs, unwrap.(discretes(affect))) - elseif isnothing(affect) - continue - else - error("Unhandled affect type $(typeof(affect))") - end - end - - for sym in discs - if !is_parameter(sys, sym) - if iscall(sym) && operation(sym) === getindex && - is_parameter(sys, arguments(sym)[1]) - sym = arguments(sym)[1] - else - error("Expected discrete variable $sym in callback to be a parameter") - end - end - - # Only `foo(t)`-esque parameters can be saved - if iscall(sym) && length(arguments(sym)) == 1 && - isequal(only(arguments(sym)), get_iv(sys)) - clocks = get!(() -> Set{Int}(), disc_param_callbacks, sym) - push!(clocks, i) - elseif is_variable_floatingpoint(sym) - insert_by_type!(constant_buffers, sym, symtype(sym)) - else - stype = symtype(sym) - if stype <: FnType - stype = fntype_to_function_type(stype) - end - insert_by_type!(nonnumeric_buffers, sym, stype) - end - end - end - clock_partitions = unique(collect(values(disc_param_callbacks))) - disc_symtypes = unique(symtype.(keys(disc_param_callbacks))) - disc_symtype_idx = Dict(disc_symtypes .=> eachindex(disc_symtypes)) + tunable_pars = SymbolicT[] + initial_pars = SymbolicT[] + constant_buffers = Dict{TypeT, Set{SymbolicT}}() + nonnumeric_buffers = Dict{TypeT, Set{SymbolicT}}() + + disc_param_callbacks = Dict{SymbolicParam, BitSet}() + cevs = continuous_events(sys) + devs = discrete_events(sys) + events = Union{SymbolicContinuousCallback, SymbolicDiscreteCallback}[cevs; devs] + parse_callbacks_for_discretes!(sys, cevs, disc_param_callbacks, constant_buffers, nonnumeric_buffers, 0) + parse_callbacks_for_discretes!(sys, devs, disc_param_callbacks, constant_buffers, nonnumeric_buffers, length(cevs)) + clock_partitions = unique(collect(values(disc_param_callbacks)))::Vector{BitSet} + disc_symtypes = Set{TypeT}() + for x in keys(disc_param_callbacks) + push!(disc_symtypes, symtype(x)) + end + disc_symtypes = collect(disc_symtypes)::Vector{TypeT} + disc_symtype_idx = Dict{TypeT, Int}(zip(disc_symtypes, eachindex(disc_symtypes))) disc_syms_by_symtype = [SymbolicParam[] for _ in disc_symtypes] for sym in keys(disc_param_callbacks) push!(disc_syms_by_symtype[disc_symtype_idx[symtype(sym)]], sym) @@ -178,13 +137,12 @@ function IndexCache(sys::AbstractSystem) disc_syms_by_symtype_by_partition = [Vector{SymbolicParam}[] for _ in disc_symtypes] for (i, buffer) in enumerate(disc_syms_by_symtype) for partition in clock_partitions - push!(disc_syms_by_symtype_by_partition[i], - [sym for sym in buffer if disc_param_callbacks[sym] == partition]) + push!(disc_syms_by_symtype_by_partition[i], filter(==(partition) ∘ Base.Fix1(getindex, disc_param_callbacks), buffer)) end end disc_idxs = Dict{SymbolicParam, DiscreteIndex}() callback_to_clocks = Dict{ - Union{SymbolicContinuousCallback, SymbolicDiscreteCallback}, Set{Int}}() + Union{SymbolicContinuousCallback, SymbolicDiscreteCallback}, BitSet}() for (typei, disc_syms_by_partition) in enumerate(disc_syms_by_symtype_by_partition) symi = 0 for (parti, disc_syms) in enumerate(disc_syms_by_partition) @@ -197,9 +155,7 @@ function IndexCache(sys::AbstractSystem) symi += 1 clocki += 1 ttsym = default_toterm(sym) - rsym = renamespace(sys, sym) - rttsym = renamespace(sys, ttsym) - for cursym in (sym, ttsym, rsym, rttsym) + for cursym in (sym, ttsym) disc_idxs[cursym] = DiscreteIndex(typei, symi, parti, clocki) end end @@ -212,26 +168,24 @@ function IndexCache(sys::AbstractSystem) disc_buffer_templates = Vector{BufferTemplate}[] for (symtype, disc_syms_by_partition) in zip( disc_symtypes, disc_syms_by_symtype_by_partition) - push!(disc_buffer_templates, - [BufferTemplate(symtype, length(buf)) for buf in disc_syms_by_partition]) + push!(disc_buffer_templates, map(Base.Fix1(BufferTemplate, symtype) ∘ length, disc_syms_by_partition)) end for p in parameters(sys; initial_parameters = true) - p = unwrap(p) ctype = symtype(p) if ctype <: FnType - ctype = fntype_to_function_type(ctype) + ctype = fntype_to_function_type(ctype)::TypeT end haskey(disc_idxs, p) && continue haskey(constant_buffers, ctype) && p in constant_buffers[ctype] && continue haskey(nonnumeric_buffers, ctype) && p in nonnumeric_buffers[ctype] && continue insert_by_type!( if ctype <: Real || ctype <: AbstractArray{<:Real} - if istunable(p, true) && Symbolics.shape(p) != Symbolics.Unknown() && + if istunable(p, true) && symbolic_has_known_size(p) && (ctype == Real || ctype <: AbstractFloat || ctype <: AbstractArray{Real} || ctype <: AbstractArray{<:AbstractFloat}) - if iscall(p) && operation(p) isa Initial + if iscall(p) && operation(p) === Initial() initial_pars else tunable_pars @@ -247,33 +201,10 @@ function IndexCache(sys::AbstractSystem) ) end - function get_buffer_sizes_and_idxs(T, buffers::Dict) - idxs = T() - buffer_sizes = BufferTemplate[] - for (i, (T, buf)) in enumerate(buffers) - for (j, p) in enumerate(buf) - ttp = default_toterm(p) - rp = renamespace(sys, p) - rttp = renamespace(sys, ttp) - idxs[p] = (i, j) - idxs[ttp] = (i, j) - idxs[rp] = (i, j) - idxs[rttp] = (i, j) - end - if T <: Symbolics.FnType - T = Any - end - push!(buffer_sizes, BufferTemplate(T, length(buf))) - end - return idxs, buffer_sizes - end - const_idxs, - const_buffer_sizes = get_buffer_sizes_and_idxs( - ParamIndexMap, constant_buffers) + const_buffer_sizes = get_buffer_sizes_and_idxs(ParamIndexMap, sys, constant_buffers) nonnumeric_idxs, - nonnumeric_buffer_sizes = get_buffer_sizes_and_idxs( - NonnumericMap, nonnumeric_buffers) + nonnumeric_buffer_sizes = get_buffer_sizes_and_idxs(NonnumericMap, sys, nonnumeric_buffers) tunable_idxs = TunableIndexMap() tunable_buffer_size = 0 @@ -282,7 +213,8 @@ function IndexCache(sys::AbstractSystem) empty!(initial_pars) end for p in tunable_pars - idx = if size(p) == () + sh = SU.shape(p) + idx = if !SU.is_array_shape(sh) tunable_buffer_size + 1 else reshape( @@ -296,11 +228,38 @@ function IndexCache(sys::AbstractSystem) symbol_to_variable[getname(default_toterm(p))] = p end end + found_array_syms = Set{SymbolicT}() + for p in tunable_pars + arrsym, isarr = split_indexed_var(p) + isarr || continue + arrsym in found_array_syms && continue + symbolic_has_known_size(arrsym) || continue + idxs = Int[] + valid_arrsym = true + for i in eachindex(arrsym) + idxsym = arrsym[i] + idx = get(tunable_idxs, idxsym, nothing)::Union{Int, Nothing} + valid_arrsym = idx !== nothing + valid_arrsym || break + push!(idxs, idx::Int) + end + push!(found_array_syms, arrsym) + valid_arrsym || break + if idxs == idxs[begin]:idxs[end] + idxs = reshape(idxs[begin]:idxs[end], size(arrsym))::AbstractArray{Int} + else + idxs = reshape(idxs, size(arrsym))::AbstractArray{Int} + end + rsym = renamespace(sys, arrsym) + tunable_idxs[arrsym] = idxs + tunable_idxs[rsym] = idxs + end initials_idxs = TunableIndexMap() initials_buffer_size = 0 for p in initial_pars - idx = if size(p) == () + sh = SU.shape(p) + idx = if !SU.is_array_shape(sh) initials_buffer_size + 1 else reshape( @@ -318,55 +277,42 @@ function IndexCache(sys::AbstractSystem) for k in collect(keys(tunable_idxs)) v = tunable_idxs[k] v isa AbstractArray || continue - for (kk, vv) in zip(collect(k), v) + v = v::Union{UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}} + iter = vec(collect(k)::Array{SymbolicT})::Vector{SymbolicT} + for (kk::SymbolicT, vv) in zip(iter, v) tunable_idxs[kk] = vv end end for k in collect(keys(initials_idxs)) v = initials_idxs[k] v isa AbstractArray || continue - for (kk, vv) in zip(collect(k), v) + v = v::Union{UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}} + iter = vec(collect(k)::Array{SymbolicT})::Vector{SymbolicT} + for (kk, vv) in zip(iter, v) initials_idxs[kk] = vv end end - dependent_pars_to_timeseries = Dict{ - Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType}() - - for eq in get_parameter_dependencies(sys) - sym = eq.lhs - vs = vars(eq.rhs) - timeseries = TimeseriesSetType() - if is_time_dependent(sys) - for v in vs - if (idx = get(disc_idxs, v, nothing)) !== nothing - push!(timeseries, idx.clock_idx) - end - end - end - ttsym = default_toterm(sym) - rsym = renamespace(sys, sym) - rttsym = renamespace(sys, ttsym) - for s in (sym, ttsym, rsym, rttsym) - dependent_pars_to_timeseries[s] = timeseries - if hasname(s) && (!iscall(s) || operation(s) != getindex) - symbol_to_variable[getname(s)] = sym - end - end - end + dependent_pars_to_timeseries = Dict{SymbolicT, TimeseriesSetType}() + vs = Set{SymbolicT}() - observed_syms_to_timeseries = Dict{BasicSymbolic, TimeseriesSetType}() + observed_syms_to_timeseries = Dict{SymbolicT, TimeseriesSetType}() for eq in observed(sys) if symbolic_type(eq.lhs) != NotSymbolic() sym = eq.lhs - vs = vars(eq.rhs; op = Nothing) + empty!(vs) + SU.search_variables!(vs, eq.rhs) timeseries = TimeseriesSetType() if is_time_dependent(sys) for v in vs if (idx = get(disc_idxs, v, nothing)) !== nothing push!(timeseries, idx.clock_idx) - elseif iscall(v) && operation(v) === getindex && - (idx = get(disc_idxs, arguments(v)[1], nothing)) !== nothing + elseif Moshi.Match.@match v begin + BSImpl.Term(; f, args) => begin + f === getindex && (idx = get(disc_idxs, args[1], nothing)) !== nothing + end + _ => false + end push!(timeseries, idx.clock_idx) elseif haskey(observed_syms_to_timeseries, v) union!(timeseries, observed_syms_to_timeseries[v]) @@ -387,12 +333,16 @@ function IndexCache(sys::AbstractSystem) end end - for sym in Iterators.flatten((keys(unk_idxs), keys(disc_idxs), keys(tunable_idxs), - keys(const_idxs), keys(nonnumeric_idxs), - keys(observed_syms_to_timeseries), independent_variable_symbols(sys))) - if hasname(sym) && (!iscall(sym) || operation(sym) !== getindex) - symbol_to_variable[getname(sym)] = sym - end + populate_symbol_to_var!(symbol_to_variable, keys(unk_idxs)) + populate_symbol_to_var!(symbol_to_variable, keys(disc_idxs)) + populate_symbol_to_var!(symbol_to_variable, keys(tunable_idxs)) + populate_symbol_to_var!(symbol_to_variable, keys(const_idxs)) + populate_symbol_to_var!(symbol_to_variable, keys(nonnumeric_idxs)) + populate_symbol_to_var!(symbol_to_variable, independent_variable_symbols(sys)) + populate_symbol_to_var!(symbol_to_variable, observables(sys)) + pbg = get_parameter_bindings_graph(sys) + if pbg isa ParameterBindingsGraph + populate_symbol_to_var!(symbol_to_variable, pbg.bound_ps) end return IndexCache( @@ -414,36 +364,123 @@ function IndexCache(sys::AbstractSystem) ) end +function populate_symbol_to_var!(symbol_to_variable::Dict{Symbol, SymbolicT}, vars) + for sym::SymbolicT in vars + if hasname(sym) && (!iscall(sym) || operation(sym) !== getindex) + symbol_to_variable[getname(sym)] = sym + end + end +end + +""" + $TYPEDSIGNATURES + +Utility function for the `IndexCache` constructor. +""" +function insert_by_type!(buffers::Dict{TypeT, Set{SymbolicT}}, sym::SymbolicT, ctype::TypeT) + buf = get!(Set{SymbolicT}, buffers, ctype) + push!(buf, sym) +end +function insert_by_type!(buffers::Vector{SymbolicT}, sym::SymbolicT, ::TypeT) + push!(buffers, sym) +end + +function parse_callbacks_for_discretes!(sys::AbstractSystem, events::Vector, disc_param_callbacks::Dict{SymbolicT, BitSet}, constant_buffers::Dict{TypeT, Set{SymbolicT}}, nonnumeric_buffers::Dict{TypeT, Set{SymbolicT}}, offset::Int) + for (i, event) in enumerate(events) + discs = Set{SymbolicParam}() + affect = event.affect::Union{AffectSystem, ImperativeAffect, Nothing} + if affect isa AffectSystem || affect isa ImperativeAffect + union!(discs, discretes(affect)) + elseif affect === nothing + continue + end + + for sym in discs + if !is_parameter(sys, sym) + if iscall(sym) && operation(sym) === getindex && + is_parameter(sys, arguments(sym)[1]) + sym = arguments(sym)[1] + else + error("Expected discrete variable $sym in callback to be a parameter") + end + end + + # Only `foo(t)`-esque parameters can be saved + if iscall(sym) && length(arguments(sym)) == 1 && + isequal(only(arguments(sym)), get_iv(sys)) + clocks = get!(BitSet, disc_param_callbacks, sym) + push!(clocks, i + offset) + elseif is_variable_floatingpoint(sym) + insert_by_type!(constant_buffers, sym, symtype(sym)) + else + stype = symtype(sym) + if stype <: FnType + stype = fntype_to_function_type(stype)::TypeT + end + insert_by_type!(nonnumeric_buffers, sym, stype) + end + end + end +end + +function get_buffer_sizes_and_idxs(::Type{BufT}, sys::AbstractSystem, buffers::Dict) where {BufT} + idxs = BufT() + buffer_sizes = BufferTemplate[] + for (i, (T, buf)) in enumerate(buffers) + for (j, p) in enumerate(buf) + ttp = default_toterm(p) + rp = renamespace(sys, p) + rttp = renamespace(sys, ttp) + idxs[p] = (i, j) + idxs[ttp] = (i, j) + idxs[rp] = (i, j) + idxs[rttp] = (i, j) + end + if T <: Symbolics.FnType + T = Any + end + push!(buffer_sizes, BufferTemplate(T, length(buf))) + end + return idxs, buffer_sizes +end + function SymbolicIndexingInterface.is_variable(ic::IndexCache, sym) variable_index(ic, sym) !== nothing end -function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return nothing - end +function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + variable_index(ic, unwrap(sym)) +end +function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym::Symbol) + sym = get(ic.symbol_to_variable, sym, nothing) + sym === nothing && return nothing + variable_index(ic, sym) +end +function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym::SymbolicT) idx = check_index_map(ic.unknown_idx, sym) idx === nothing || return idx iscall(sym) && operation(sym) == getindex || return nothing args = arguments(sym) idx = variable_index(ic, args[1]) idx === nothing && return nothing - return idx[args[2:end]...] + return idx[unwrap_const.(args[2:end])...] end +SymbolicIndexingInterface.variable_index(ic::IndexCache, sym) = false function SymbolicIndexingInterface.is_parameter(ic::IndexCache, sym) parameter_index(ic, sym) !== nothing end -function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return nothing - end - sym = unwrap(sym) - validate_size = Symbolics.isarraysymbolic(sym) && symtype(sym) <: AbstractArray && - Symbolics.shape(sym) !== Symbolics.Unknown() +function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + parameter_index(ic, unwrap(sym)) +end +function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym::Symbol) + sym = get(ic.symbol_to_variable, sym, nothing) + sym === nothing && return nothing + parameter_index(ic, sym) +end +function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym::SymbolicT) + validate_size = Symbolics.isarraysymbolic(sym) && symbolic_has_known_size(sym) return if (idx = check_index_map(ic.tunable_idx, sym)) !== nothing ParameterIndex(SciMLStructures.Tunable(), idx, validate_size) elseif (idx = check_index_map(ic.initials_idx, sym)) !== nothing @@ -461,10 +498,10 @@ function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) pidx === nothing && return nothing if pidx.portion == SciMLStructures.Tunable() ParameterIndex(pidx.portion, - Origin(first.(axes((args[1]))))(reshape(pidx.idx, size(args[1])))[args[2:end]...], + Origin(first.(axes((args[1]))))(reshape(pidx.idx, size(args[1])))[value.(args[2:end])...], pidx.validate_size) else - ParameterIndex(pidx.portion, (pidx.idx..., args[2:end]...), pidx.validate_size) + ParameterIndex(pidx.portion, (pidx.idx..., value.(args[2:end])...), pidx.validate_size) end end end @@ -473,12 +510,15 @@ function SymbolicIndexingInterface.is_timeseries_parameter(ic::IndexCache, sym) timeseries_parameter_index(ic, sym) !== nothing end +function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sym::Union{Num, Symbolics.Arr, Symbolics.CallAndWrap}) + timeseries_parameter_index(ic, unwrap(sym)) +end +function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sym::Symbol) + sym = get(ic.symbol_to_variable, sym, nothing) + sym === nothing && return nothing + timeseries_parameter_index(ic, sym) +end function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return nothing - end - sym = unwrap(sym) idx = check_index_map(ic.discrete_idx, sym) idx === nothing || return ParameterTimeseriesIndex(idx.clock_idx, (idx.buffer_idx, idx.idx_in_clock)) @@ -487,80 +527,84 @@ function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sy idx = timeseries_parameter_index(ic, args[1]) idx === nothing && return nothing return ParameterTimeseriesIndex( - idx.timeseries_idx, (idx.parameter_idx..., args[2:end]...)) + idx.timeseries_idx, (idx.parameter_idx..., value.(args[2:end])...)) end -function check_index_map(idxmap, sym) - if (idx = get(idxmap, sym, nothing)) !== nothing - return idx - elseif !isa(sym, Symbol) && (!iscall(sym) || operation(sym) !== getindex) && - hasname(sym) && (idx = get(idxmap, getname(sym), nothing)) !== nothing - return idx - end +function check_index_map(idxmap::Dict{SymbolicT, V}, sym::SymbolicT)::Union{V, Nothing} where {V} + idx = get(idxmap, sym, nothing) + idx === nothing || return idx dsym = default_toterm(sym) isequal(sym, dsym) && return nothing - if (idx = get(idxmap, dsym, nothing)) !== nothing - idx - elseif !isa(dsym, Symbol) && (!iscall(dsym) || operation(dsym) !== getindex) && - hasname(dsym) && (idx = get(idxmap, getname(dsym), nothing)) !== nothing - idx - else - nothing - end + idx = get(idxmap, dsym, nothing) + idx === nothing || return idx + return nothing end +const ReorderedParametersT = Vector{Union{Vector{SymbolicT}, Vector{Vector{SymbolicT}}}} + function reorder_parameters( sys::AbstractSystem, ps = parameters(sys; initial_parameters = true); kwargs...) if has_index_cache(sys) && get_index_cache(sys) !== nothing - reorder_parameters(get_index_cache(sys), ps; kwargs...) + return reorder_parameters(get_index_cache(sys)::IndexCache, ps; kwargs...) elseif ps isa Tuple - ps + return ReorderedParametersT(collect(ps)) else - (ps,) + return eltype(ReorderedParametersT)[ps] end end -function reorder_parameters(ic::IndexCache, ps; drop_missing = false, flatten = true) - isempty(ps) && return () - param_buf = if ic.tunable_buffer_size.length == 0 - () - else - (BasicSymbolic[unwrap(variable(:DEF)) - for _ in 1:(ic.tunable_buffer_size.length)],) +const COMMON_DEFAULT_VAR = unwrap(only(@variables __DEF__)) + +function reorder_parameters(ic::IndexCache, ps::Vector{SymbolicT}; drop_missing = false, flatten = true) + result = ReorderedParametersT() + isempty(ps) && return result + param_buf = fill(COMMON_DEFAULT_VAR, ic.tunable_buffer_size.length) + if !isempty(param_buf) || !flatten + push!(result, param_buf) + end + initials_buf = fill(COMMON_DEFAULT_VAR, ic.initials_buffer_size.length) + if !isempty(initials_buf) || !flatten + push!(result, initials_buf) + end + + disc_buf = Vector{SymbolicT}[] + for bufszs in ic.discrete_buffer_sizes + push!(disc_buf, fill(COMMON_DEFAULT_VAR, sum(x -> x.length, bufszs))) + end + const_buf = Vector{SymbolicT}[] + for bufsz in ic.constant_buffer_sizes + push!(const_buf, fill(COMMON_DEFAULT_VAR, bufsz.length)) end - initials_buf = if ic.initials_buffer_size.length == 0 - () + nonnumeric_buf = Vector{SymbolicT}[] + for bufsz in ic.nonnumeric_buffer_sizes + push!(nonnumeric_buf, fill(COMMON_DEFAULT_VAR, bufsz.length)) + end + if flatten + append!(result, disc_buf) + append!(result, const_buf) + append!(result, nonnumeric_buf) else - (BasicSymbolic[unwrap(variable(:DEF)) - for _ in 1:(ic.initials_buffer_size.length)],) - end - - disc_buf = Tuple(BasicSymbolic[unwrap(variable(:DEF)) - for _ in 1:(sum(x -> x.length, temp))] - for temp in ic.discrete_buffer_sizes) - const_buf = Tuple(BasicSymbolic[unwrap(variable(:DEF)) for _ in 1:(temp.length)] - for temp in ic.constant_buffer_sizes) - nonnumeric_buf = Tuple(Union{BasicSymbolic, CallWithMetadata}[unwrap(variable(:DEF)) - for _ in 1:(temp.length)] - for temp in ic.nonnumeric_buffer_sizes) + push!(result, disc_buf) + push!(result, const_buf) + push!(result, nonnumeric_buf) + end for p in ps - p = unwrap(p) if haskey(ic.discrete_idx, p) idx = ic.discrete_idx[p] disc_buf[idx.buffer_idx][idx.idx_in_buffer] = p elseif haskey(ic.tunable_idx, p) i = ic.tunable_idx[p] if i isa Int - param_buf[1][i] = unwrap(p) + param_buf[i] = p else - param_buf[1][i] = unwrap.(collect(p)) + param_buf[i] = collect(p) end elseif haskey(ic.initials_idx, p) i = ic.initials_idx[p] if i isa Int - initials_buf[1][i] = unwrap(p) + initials_buf[i] = p else - initials_buf[1][i] = unwrap.(collect(p)) + initials_buf[i] = collect(p) end elseif haskey(ic.constant_idx, p) i, j = ic.constant_idx[p] @@ -573,37 +617,20 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false, flatten = end end - param_buf = broadcast.(unwrap, param_buf) - initials_buf = broadcast.(unwrap, initials_buf) - disc_buf = broadcast.(unwrap, disc_buf) - const_buf = broadcast.(unwrap, const_buf) - nonnumeric_buf = broadcast.(unwrap, nonnumeric_buf) - if drop_missing - filterer = !isequal(unwrap(variable(:DEF))) - param_buf = filter.(filterer, param_buf) - initials_buf = filter.(filterer, initials_buf) - disc_buf = filter.(filterer, disc_buf) - const_buf = filter.(filterer, const_buf) - nonnumeric_buf = filter.(filterer, nonnumeric_buf) - end - - if flatten - result = ( - param_buf..., initials_buf..., disc_buf..., const_buf..., nonnumeric_buf...) - if all(isempty, result) - return () - end - return result - else - if isempty(param_buf) - param_buf = ((),) - end - if isempty(initials_buf) - initials_buf = ((),) + filterer = !isequal(COMMON_DEFAULT_VAR) + for inner in result + if inner isa Vector{SymbolicT} + filter!(filterer, inner) + elseif inner isa Vector{Vector{SymbolicT}} + for buf in inner + filter!(filterer, buf) + end + end end - return (param_buf..., initials_buf..., disc_buf, const_buf, nonnumeric_buf) end + + return result end # Given a parameter index, find the index of the buffer it is in when diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/lib/ModelingToolkitBase/src/systems/nonlinear/homotopy_continuation.jl similarity index 97% rename from src/systems/nonlinear/homotopy_continuation.jl rename to lib/ModelingToolkitBase/src/systems/nonlinear/homotopy_continuation.jl index 96c00411ad..c19f2e32ba 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/lib/ModelingToolkitBase/src/systems/nonlinear/homotopy_continuation.jl @@ -1,5 +1,5 @@ function contains_variable(x, wrt) - any(y -> occursin(y, x), wrt) + any(y -> SU.query(isequal(y), x), wrt) end """ @@ -41,7 +41,7 @@ function display_reason(reason::NonPolynomialReason.T, sym) `*, /, +, -, ^`. """ else - error("This should never happen. Please open an issue in ModelingToolkit.jl.") + error("This should never happen. Please open an issue in ModelingToolkitBase.jl.") end end @@ -104,7 +104,7 @@ struct NemoNotLoaded <: PolynomialTransformationError end function Base.showerror(io::IO, err::NemoNotLoaded) println(io, - "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") + "ModelingToolkitBase may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") end struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError @@ -270,7 +270,7 @@ function PolynomialTransformation(sys::System) transformation_err = nothing for t in all_non_poly_terms # if the term involves multiple unknowns, we can't invert it - dvs_in_term = map(x -> occursin(x, t), dvs) + dvs_in_term = map(x -> SU.query(isequal(x), t), dvs) if count(dvs_in_term) > 1 transformation_err = MultivarTerm(t, dvs[dvs_in_term]) is_poly = false @@ -299,7 +299,7 @@ function PolynomialTransformation(sys::System) invterm = Symbolics.substitute.(invterm, (Dict(),)) # RootsOf implies Symbolics couldn't solve the inner polynomial because # `Nemo` wasn't loaded. - if any(x -> iscall(x) && operation(x) == Symbolics.RootsOf, invterm) + if any(x -> iscall(x) && operation(x) === Symbolics.RootsOf, invterm) transformation_err = NemoNotLoaded() is_poly = false break @@ -369,7 +369,7 @@ function transform_system(sys::System, transformation::PolynomialTransformation; t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) # the substituted variable occurs outside the substituted term poly_and_nonpoly = map(dvs) do x - all(!isequal(x), new_dvs) && occursin(x, t) + all(!isequal(x), new_dvs) && SU.query(isequal(x), t) end if any(poly_and_nonpoly) return NotPolynomialError( @@ -452,7 +452,7 @@ function handle_rational_polynomials(x, wrt; fraction_cancel_fn = simplify_fract den *= d end else - error("Unhandled operation in `handle_rational_polynomials`. This should never happen. Please open an issue in ModelingToolkit.jl with an MWE.") + error("Unhandled operation in `handle_rational_polynomials`. This should never happen. Please open an issue in ModelingToolkitBase.jl with an MWE.") end if fraction_cancel_fn !== nothing diff --git a/lib/ModelingToolkitBase/src/systems/nonlinear/initializesystem.jl b/lib/ModelingToolkitBase/src/systems/nonlinear/initializesystem.jl new file mode 100644 index 0000000000..436912f72b --- /dev/null +++ b/lib/ModelingToolkitBase/src/systems/nonlinear/initializesystem.jl @@ -0,0 +1,751 @@ +""" + $(TYPEDSIGNATURES) + +Generate the initialization system for `sys`. The initialization system is a system of +nonlinear equations that solve for the full set of initial conditions of `sys` given +specified constraints. + +The initialization system can be of two types: time-dependent and time-independent. +Time-dependent initialization systems solve for the initial values of unknowns as well as +the values of solvable parameters of the system. Time-independent initialization systems +only solve for solvable parameters of the system. + +# Keyword arguments + +- `time_dependent_init`: Whether to create an initialization system for a time-dependent + system. A time-dependent initialization requires a time-dependent `sys`, but a time- + independent initialization can be created regardless. +- `op`: The operating point of user-specified initial conditions of variables in `sys`. +- `initialization_eqs`: Additional initialization equations to use apart from those in + `initialization_equations(sys)`. +- `guesses`: Additional guesses to use apart from those in `guesses(sys)`. +- `default_dd_guess`: Default guess for dummy derivative variables in time-dependent + initialization. +- `algebraic_only`: If `false`, does not use initialization equations (provided via the + keyword or part of the system) to construct initialization. +- `check_defguess`: Whether to error when a variable does not have a default or guess + despite ModelingToolkitBase expecting it to. +- `name`: The name of the initialization system. + +All other keyword arguments are forwarded to the [`System`](@ref) constructor. +""" +function generate_initializesystem( + sys::AbstractSystem; time_dependent_init = is_time_dependent(sys), kwargs...) + if time_dependent_init + generate_initializesystem_timevarying(sys; kwargs...) + else + generate_initializesystem_timeindependent(sys; kwargs...) + end +end + +""" +$(TYPEDSIGNATURES) + +Generate `System` of nonlinear equations which initializes a problem from specified initial conditions of a time-dependent `AbstractSystem`. +""" +function generate_initializesystem_timevarying(sys::AbstractSystem; + op = SymmapT(), + initialization_eqs = Equation[], + guesses = SymmapT(), + default_dd_guess = Bool(0), + fast_path = false, + algebraic_only = false, + check_units = true, check_defguess = false, + name = nameof(sys), kwargs...) + eqs = equations(sys) + trueobs, eqs = unhack_observed(observed(sys), eqs) + # remove any observed equations that directly or indirectly contain + # delayed unknowns + isempty(trueobs) || filter_delay_equations_variables!(sys, trueobs) + + # Firstly, all variables and observables are initialization unknowns + init_vars_set = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + add_trivial_initsys_vars!(init_vars_set, unknowns(sys), trueobs) + + eqs_ics = Equation[] + + inps = copy(get_inputs(sys)) + ps = parameters(sys; initial_parameters = true) + init_ps = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + + for v in get_all_discretes_fast(sys) + push!(is_variable_floatingpoint(v) ? init_vars_set : init_ps, v) + end + for v in inps + Moshi.Match.@match v begin + BSImpl.Term(; f, args) => begin + if f === getindex + push!(init_ps, args[1]) + elseif f isa SymbolicT + push!(init_ps, v) + else + error("Unexpected input $v.") + end + end + # Intentionally no fallback case. All inputs are originally variables. + end + end + push!(init_ps, get_iv(sys)::SymbolicT) + initsys_sort_system_parameters!(init_vars_set, init_ps, ps) + + guesses = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(guesses), COMMON_NOTHING) + left_merge!(guesses, ModelingToolkitBase.guesses(sys)) + + # Anything with a binding of `missing` is solvable. + binds = bindings(sys) + newbinds = SymmapT() + # All bound parameters are solvable. The corresponding equation comes from the binding + for v in bound_parameters(sys) + push!(is_variable_floatingpoint(v) ? init_vars_set : init_ps, v) + end + initsys_sort_system_bindings!(init_vars_set, init_ps, eqs_ics, binds, newbinds, guesses) + + derivative_rules = DerivativeDict() + dd_guess_sym = BSImpl.Const{VartypeT}(default_dd_guess) + banned_derivatives = Set{SymbolicT}() + if has_schedule(sys) && (schedule = get_schedule(sys); schedule isa Schedule) + for (k, v) in schedule.dummy_sub + ttk = default_toterm(k) + if !has_possibly_indexed_key(guesses, k) && !has_possibly_indexed_key(guesses, ttk) + write_possibly_indexed_array!(guesses, ttk, dd_guess_sym, COMMON_NOTHING) + end + # For DDEs, the derivatives can have delayed terms + if _has_delays(sys, v, banned_derivatives) + push!(banned_derivatives, ttk) + continue + end + push_as_atomic_array!(init_vars_set, ttk) + isequal(ttk, v) || push!(eqs_ics, ttk ~ v) + derivative_rules[k] = ttk + end + merge!(derivative_rules, as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(derivative_rules), COMMON_NOTHING)) + end + for eq in eqs + if _has_delays(sys, eq.rhs, banned_derivatives) + isdiffeq(eq) && push!(banned_derivatives, default_toterm(eq.lhs)) + continue + end + if isdiffeq(eq) + get!(derivative_rules, eq.lhs) do + k = eq.lhs + ttk = default_toterm(eq.lhs) + if !has_possibly_indexed_key(guesses, k) && !has_possibly_indexed_key(guesses, ttk) + write_possibly_indexed_array!(guesses, ttk, dd_guess_sym, COMMON_NOTHING) + end + push_as_atomic_array!(init_vars_set, ttk) + isequal(ttk, eq.rhs) || push!(eqs_ics, ttk ~ eq.rhs) + ttk + end + else + push!(eqs_ics, eq) + end + end + D = Differential(get_iv(sys)) + for eq in trueobs + # Observed derivatives aren't added the same way as dummy_sub/diffeqs because + # doing so would require all observed equations to be symbolically differentiable. + get!(derivative_rules, D(eq.lhs), D(eq.rhs)) + # Add as guesses + if !has_possibly_indexed_key(guesses, eq.lhs) + write_possibly_indexed_array!(guesses, eq.lhs, eq.rhs, COMMON_NOTHING) + end + end + op::SymmapT = if fast_path + op + else + build_operating_point(sys, op) + end + timevaring_initsys_process_op!(init_vars_set, init_ps, eqs_ics, op, derivative_rules, guesses) + + # process explicitly provided initialization equations + if !algebraic_only + initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] + for eq in initialization_eqs + eq = fixpoint_sub(eq, derivative_rules; maxiters = get_maxiters(derivative_rules)) # expand dummy derivatives + push!(eqs_ics, eq) + end + end + # TODO + # 8) use observed equations for guesses of observed variables if not provided + # guessed = Set(keys(defs)) # x(t), D(x(t)), ... + # guessed = union(guessed, Set(default_toterm.(guessed))) # x(t), D(x(t)), xˍt(t), ... + # for eq in trueobs + # if !(eq.lhs in guessed) + # defs[eq.lhs] = eq.rhs + # #push!(guessed, eq.lhs) # should not encounter eq.lhs twice, so don't need to track it + # end + # end + + append!(eqs_ics, trueobs) + + vars = collect(init_vars_set) + pars = collect(init_ps) + System(Vector{Equation}(eqs_ics), + vars, + pars; + bindings = newbinds, + initial_conditions = guesses, + checks = check_units, + name, + is_initializesystem = true, + discover_from_metadata = false, + kwargs...) +end + +get_maxiters(subrules::AbstractDict) = max(3, min(1000, length(subrules))) + +""" +$(TYPEDSIGNATURES) + +Generate `System` of nonlinear equations which initializes a problem from specified initial conditions of a time-independent `AbstractSystem`. +""" +function generate_initializesystem_timeindependent(sys::AbstractSystem; + op = Dict(), + initialization_eqs = [], + guesses = Dict(), + algebraic_only = false, + check_units = true, check_defguess = false, + fast_path = false, + name = nameof(sys), kwargs...) + eqs = equations(sys) + trueobs, eqs = unhack_observed(observed(sys), eqs) + # remove any observed equations that directly or indirectly contain + # delayed unknowns + isempty(trueobs) || filter_delay_equations_variables!(sys, trueobs) + + og_dvs = as_atomic_array_set(unknowns(sys)) + union!(og_dvs, as_atomic_array_set(observables(sys))) + + init_vars_set = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + + eqs_ics = Equation[] + + ps = parameters(sys; initial_parameters = true) + init_ps = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + initsys_sort_system_parameters!(init_vars_set, init_ps, ps) + + guesses = SymmapT(guesses) + left_merge!(guesses, ModelingToolkitBase.guesses(sys)) + + # Anything with a binding of `missing` is solvable. + binds = bindings(sys) + newbinds = SymmapT() + # All bound parameters are solvable. The corresponding equation comes from the binding + for v in bound_parameters(sys) + # Edge case for `NonlinearSystem(::ODE)` where `t` is bound to `Inf` + binds[v] === COMMON_INF && continue + push!(is_variable_floatingpoint(v) ? init_vars_set : init_ps, v) + end + # Anything with a binding of `missing` is solvable. + for (k, v) in binds + if v === COMMON_MISSING + push!(init_vars_set, k) + delete!(init_ps, k) + continue + end + # Edge case for `NonlinearSystem(::ODE)` where `t` is bound to `Inf` + v === COMMON_INF && continue + k in og_dvs && continue + if is_variable_floatingpoint(k) + push!(eqs_ics, k ~ v) + get!(guesses, k, v) + else + newbinds[k] = v + end + end + + op::SymmapT = if fast_path + op + else + build_operating_point(sys, op) + end + + valid_initial_parameters = AtomicArraySet{OrderedDict{SymbolicT, Nothing}}() + for (k, v) in op + if is_variable(sys, k) || has_observed_with_lhs(sys, k) || + Moshi.Match.@match k begin + BSImpl.Term(; f, args) && if f isa Differential end => is_variable(sys, args[1]) + _ => false + end + + isconst(v) && push!(valid_initial_parameters, Initial(k)) + continue + end + + if v === COMMON_MISSING + push!(init_vars_set, k) + delete!(init_ps, k) + continue + end + + # No need to process any non-solvables + if k in init_ps + continue + end + + if isconst(v) + push!(eqs_ics, k ~ Initial(k)) + op[Initial(k)] = v + else + push!(eqs_ics, k ~ v) + end + end + + for k in valid_initial_parameters + op[k] = Moshi.Data.variant_getfield(k, BSImpl.Term, :args)[1] + end + + # process explicitly provided initialization equations + if !algebraic_only + initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] + end + + # only include initialization equations where all the involved `Initial` + # parameters are valid. + vs = Set{SymbolicT}() + allpars = as_atomic_array_set(parameters(sys; initial_parameters = true)) + union!(allpars, bound_parameters(sys)) + initialization_eqs = filter(initialization_eqs) do eq + empty!(vs) + SU.search_variables!(vs, eq; is_atomic = OperatorIsAtomic{Initial}()) + # error if non-parameters are present in the initialization equations + non_params = filter(!Base.Fix1(contains_possibly_indexed_element, allpars), vs) + if !isempty(non_params) + throw(UnknownsInTimeIndependentInitializationError(eq, non_params)) + end + filter!(x -> iscall(x) && isinitial(x), vs) + return issubset(vs, valid_initial_parameters) + invalid_initials = setdiff(vs, valid_initial_parameters) + return isempty(invalid_initials) + end + + append!(eqs_ics, initialization_eqs) + + vars = collect(init_vars_set) + pars = collect(init_ps) + System(Vector{Equation}(eqs_ics), + vars, + pars; + initial_conditions = guesses, + checks = check_units, + name, + is_initializesystem = true, + discover_from_metadata = false, + kwargs...) +end + +function add_trivial_initsys_vars!(init_vars_set::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, dvs::Vector{SymbolicT}, trueobs::Vector{Equation}) + for v in dvs + push!(init_vars_set, split_indexed_var(v)[1]) + end + for eq in trueobs + push!(init_vars_set, split_indexed_var(eq.lhs)[1]) + end +end + +function initsys_sort_system_parameters!(init_vars_set::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + init_ps::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + ps::Vector{SymbolicT}) + for v in ps + arr, _ = split_indexed_var(v) + arr in init_vars_set && continue + push!(init_ps, arr) + end +end + +function initsys_sort_system_bindings!(init_vars_set::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + init_ps::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + eqs_ics::Vector{Equation}, binds::ROSymmapT, + newbinds::SymmapT, guesses::SymmapT) + # Anything with a binding of `missing` is solvable. + for (k, v) in binds + if v === COMMON_MISSING + push!(init_vars_set, k) + delete!(init_ps, k) + continue + end + if is_variable_floatingpoint(k) + push!(eqs_ics, k ~ v) + get!(guesses, k, v) + else + newbinds[k] = v + end + end +end + +function timevaring_initsys_process_op!(init_vars_set::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + init_ps::AtomicArraySet{OrderedDict{SymbolicT, Nothing}}, + eqs_ics::Vector{Equation}, op::SymmapT, + derivative_rules::DerivativeDict, guesses::SymmapT) + for (k, v) in op + # Late binding `missing` also makes the key solvable + if v === COMMON_MISSING + push!(init_vars_set, k) + delete!(init_ps, k) + continue + end + # No need to process any non-solvables + if k in init_ps + continue + end + + # At this point, not only is `k` solvable but it should also have + # `Initial(k)` defined if required. + ik = Initial(k) + @assert ik in init_ps + subk = fixpoint_sub(k, derivative_rules; maxiters = get_maxiters(derivative_rules)) + # FIXME: DAEs can have initial conditions that require reducing the system + # to index zero. If `isdifferential(y)`, an initial condition was given for the + # derivative of an algebraic variable, so ignore it. Otherwise, the initialization + # system gets a `D(y) ~ ...` equation and errors. This is the same behavior as v9. + if isdifferential(subk) + continue + end + shk = SU.shape(k) + if SU.isconst(v) + # The operating point already has `Initial(x) => x`. This same operating point + # will be passed to the `NonlinearProblem` constructor, and guesses will not take + # priority over it. So instead of adding `Initial(x) => v` as a guess, add `x => v`. + op[ik] = k + left_merge!(guesses, AtomicArrayDict(k => v)) + if !SU.is_array_shape(shk) + push!(eqs_ics, subk ~ ik) + continue + end + for i in SU.stable_eachindex(k) + v[i] === COMMON_NOTHING && continue + push!(eqs_ics, subk[i] ~ ik[i]) + end + continue + end + if Symbolics.isarraysymbolic(k) + for idx in SU.stable_eachindex(k) + vv = v[idx] + # `as_atomic_dict_with_defaults` is used to build `op`, which in + # `build_operating_point` will put `COMMON_NOTHING` for missing + # entries. Ignore them. + vv === COMMON_NOTHING && continue + kk = k[idx] + subkk = subk[idx] + ikk = Initial(kk) + if SU.isconst(vv) + push!(eqs_ics, subkk ~ ikk) + else + write_possibly_indexed_array!(guesses, kk, vv, COMMON_FALSE) + vv = fixpoint_sub(vv, derivative_rules; maxiters = get_maxiters(derivative_rules)) + push!(eqs_ics, subkk ~ vv) + end + end + else + v = fixpoint_sub(v, derivative_rules; maxiters = get_maxiters(derivative_rules)) + isequal(subk, v) || push!(eqs_ics, subk ~ v) + end + end +end + +""" + $(TYPEDSIGNATURES) + +Given `sys` and a list of observed equations `trueobs`, remove all the equations that +directly or indirectly contain a delayed unknown of `sys`. +""" +function filter_delay_equations_variables!(sys::AbstractSystem, trueobs::Vector{Equation}) + is_time_dependent(sys) || return trueobs + banned_vars = Set{SymbolicT}() + idxs_to_remove = Int[] + for (i, eq) in enumerate(trueobs) + _has_delays(sys, eq.rhs, banned_vars) || continue + push!(idxs_to_remove, i) + push!(banned_vars, eq.lhs) + end + return deleteat!(trueobs, idxs_to_remove) +end + +""" + $(TYPEDSIGNATURES) + +Check if the expression `ex` contains a delayed unknown of `sys` or a term in +`banned`. +""" +function _has_delays(sys::AbstractSystem, ex, banned) + ex = unwrap(ex) + ex in banned && return true + if symbolic_type(ex) == NotSymbolic() + if is_array_of_symbolics(ex) + return any(x -> _has_delays(sys, x, banned), ex) + end + return false + end + iscall(ex) || return false + op = operation(ex) + args = arguments(ex) + if iscalledparameter(ex) + return any(x -> _has_delays(sys, x, banned), args) + end + if issym(op) && length(args) == 1 && is_variable(sys, op(get_iv(sys))) && + iscall(args[1]) && get_iv(sys) in SU.search_variables(args[1]) + return true + end + return any(x -> _has_delays(sys, x, banned), args) +end + +function SciMLBase.remake_initialization_data( + sys::AbstractSystem, odefn, u0, t0, p, newu0, newp) + if u0 === missing && p === missing + return odefn.initialization_data + end + + oldinitdata = odefn.initialization_data + + # We _always_ build initialization now. So if we didn't build it before, don't do + # it now + oldinitdata === nothing && return nothing + meta = oldinitdata.metadata + meta isa InitializationMetadata || return oldinitdata + + if !(eltype(u0) <: Pair) && !(eltype(p) <: Pair) + oldinitprob = oldinitdata.initializeprob + oldinitprob === nothing && return nothing + + reconstruct_fn = meta.oop_reconstruct_u0_p + # the history function doesn't matter because `reconstruct_fn` is only going to + # update the values of parameters, which aren't time dependent. The reason it + # is called is because `Initial` parameters are calculated from the corresponding + # state values. + history_fn = is_time_dependent(sys) && !is_markovian(sys) ? Returns(newu0) : nothing + new_initu0, + new_initp = reconstruct_fn( + ProblemState(; u = newu0, p = newp, t = t0, h = history_fn), oldinitprob) + if oldinitprob.f.resid_prototype === nothing + newf = oldinitprob.f + else + newf = remake(oldinitprob.f; + resid_prototype = calculate_resid_prototype( + length(oldinitprob.f.resid_prototype), new_initu0, new_initp)) + end + initprob = remake(oldinitprob; f = newf, u0 = new_initu0, p = new_initp) + return @set oldinitdata.initializeprob = initprob + end + + dvs = unknowns(sys) + ps = parameters(sys) + if eltype(u0) <: Pair + if u0 isa Array + u0 = Dict(u0) + end + if keytype(u0) === Any || keytype(u0) <: Symbol + u0 = anydict(u0) + symbols_to_symbolics!(sys, u0) + end + else + u0 = to_varmap(u0, dvs) + symbols_to_symbolics!(sys, u0) + end + u0map = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(u0), COMMON_NOTHING) + if eltype(p) <: Pair + if p isa Array + p = Dict(p) + end + if keytype(p) === Any || keytype(p) <: Symbol + p = anydict(p) + symbols_to_symbolics!(sys, p) + end + else + p = to_varmap(p, ps) + symbols_to_symbolics!(sys, p) + end + pmap = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(p), COMMON_NOTHING) + op = merge!(u0map, pmap) + guesses = SymmapT() + use_scc = true + initialization_eqs = Equation[] + + left_merge!(op, meta.op) + filter!(Base.Fix2(!==, COMMON_NOTHING) ∘ last, op) + merge!(guesses, meta.guesses) + use_scc = meta.use_scc + initialization_eqs = meta.additional_initialization_eqs + time_dependent_init = meta.time_dependent_init + + if t0 === nothing && is_time_dependent(sys) + t0 = 0.0 + end + + floatT = float_type_from_varmap(op) + u0_constructor = get_u0_constructor(identity, typeof(newu0), floatT, false) + p_constructor = get_p_constructor(identity, typeof(newu0), floatT) + kws = maybe_build_initialization_problem( + sys, SciMLBase.isinplace(odefn), op, t0, guesses; + time_dependent_init, use_scc, initialization_eqs, floatT, fast_path = true, + u0_constructor, p_constructor, allow_incomplete = true, check_units = false) + + odefn = remake(odefn; kws...) + return SciMLBase.remake_initialization_data(sys, odefn, newu0, t0, newp, newu0, newp) +end + +promote_type_with_nothing(::Type{T}, ::Nothing) where {T} = T +promote_type_with_nothing(::Type{T}, ::StaticVector{0}) where {T} = T +function promote_type_with_nothing(::Type{T}, ::AbstractArray{T2}) where {T, T2} + promote_type(T, T2) +end +function promote_type_with_nothing(::Type{T}, p::MTKParameters) where {T} + promote_type_with_nothing(promote_type_with_nothing(T, p.tunable), p.initials) +end + +promote_with_nothing(::Type, ::Nothing) = nothing +promote_with_nothing(::Type, x::StaticVector{0}) = x +promote_with_nothing(::Type{T}, x::AbstractArray{T}) where {T} = x +function promote_with_nothing(::Type{T}, x::AbstractArray{T2}) where {T, T2} + if ArrayInterface.ismutable(x) + y = similar(x, T) + copyto!(y, x) + return y + else + yT = similar_type(x, T) + return yT(x) + end +end +function promote_with_nothing(::Type{T}, p::MTKParameters) where {T} + tunables = promote_with_nothing(T, p.tunable) + p = SciMLStructures.replace(SciMLStructures.Tunable(), p, tunables) + initials = promote_with_nothing(T, p.initials) + p = SciMLStructures.replace(SciMLStructures.Initials(), p, initials) + return p +end + +function promote_u0_p(u0, p, t0) + T = Union{} + T = promote_type_with_nothing(T, u0) + T = promote_type_with_nothing(T, p) + + u0 = promote_with_nothing(T, u0) + p = promote_with_nothing(T, p) + return u0, p +end + +function SciMLBase.late_binding_update_u0_p( + prob, sys::AbstractSystem, u0, p, t0, newu0, newp) + supports_initialization(sys) || return newu0, newp + prob isa IntervalNonlinearProblem && return newu0, newp + prob isa LinearProblem && return newu0, newp + + initdata = prob.f.initialization_data + meta = initdata === nothing ? nothing : initdata.metadata + + newu0, newp = promote_u0_p(newu0, newp, t0) + + # non-symbolic u0 updates initials... + if eltype(u0) <: Pair + syms = [] + vals = [] + allsyms = all_symbols(sys) + for (k, v) in u0 + v === nothing && continue + (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue + if k isa Symbol + k2 = symbol_to_symbolic(sys, k; allsyms) + # if it is returned as-is, there is no match so skip it + k2 === k && continue + k = k2 + end + is_parameter(sys, Initial(k)) || continue + push!(syms, Initial(k)) + push!(vals, v) + end + newp = setp_oop(sys, syms)(newp, vals) + else + allsyms = nothing + # if `p` is not provided or is symbolic + p === missing || eltype(p) <: Pair || return newu0, newp + (newu0 === nothing || isempty(newu0)) && return newu0, newp + initdata === nothing && return newu0, newp + meta = initdata.metadata + meta isa InitializationMetadata || return newu0, newp + newp = p === missing ? copy(newp) : newp + + if length(newu0) != length(prob.u0) + throw(ArgumentError("Expected `newu0` to be of same length as unknowns ($(length(prob.u0))). Got $(typeof(newu0)) of length $(length(newu0))")) + end + newp = meta.set_initial_unknowns!(newp, newu0) + end + + if eltype(p) <: Pair + syms = [] + vals = [] + if allsyms === nothing + allsyms = all_symbols(sys) + end + for (k, v) in p + v === nothing && continue + (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue + if k isa Symbol + k2 = symbol_to_symbolic(sys, k; allsyms) + # if it is returned as-is, there is no match so skip it + k2 === k && continue + k = k2 + end + is_parameter(sys, Initial(k)) || continue + push!(syms, Initial(k)) + push!(vals, v) + end + newp = setp_oop(sys, syms)(newp, vals) + end + + return newu0, newp +end + +function DiffEqBase.get_updated_symbolic_problem( + sys::AbstractSystem, prob; u0 = state_values(prob), + p = parameter_values(prob), kw...) + supports_initialization(sys) || return prob + initdata = prob.f.initialization_data + initdata isa SciMLBase.OverrideInitData || return prob + meta = initdata.metadata + meta isa InitializationMetadata || return prob + meta.get_updated_u0 === nothing && return prob + + u0 === nothing && return remake(prob; p) + + t0 = is_time_dependent(prob) ? current_time(prob) : nothing + + if p isa MTKParameters + buffer = p.initials + else + buffer = p + end + + u0 = DiffEqBase.promote_u0(u0, buffer, t0) + + if ArrayInterface.ismutable(u0) + T = typeof(u0) + else + T = StaticArrays.similar_type(u0) + end + + return remake(prob; u0 = T(meta.get_updated_u0(prob, initdata.initializeprob)), p) +end + +""" + $(TYPEDSIGNATURES) + +Check if the given system is an initialization system. +""" +function is_initializesystem(sys::AbstractSystem) + has_is_initializesystem(sys) && get_is_initializesystem(sys) +end + +""" +Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with +initialization. +""" +function unhack_observed(obseqs, eqs) + return obseqs, eqs +end + +function UnknownsInTimeIndependentInitializationError(eq, non_params) + ArgumentError(""" + Initialization equations for time-independent systems can only contain parameters. \ + Found $non_params in $eq. If the equations refer to the initial guess for unknowns, \ + use the `Initial` operator. + """) +end diff --git a/src/systems/optimal_control_interface.jl b/lib/ModelingToolkitBase/src/systems/optimal_control_interface.jl similarity index 89% rename from src/systems/optimal_control_interface.jl rename to lib/ModelingToolkitBase/src/systems/optimal_control_interface.jl index 5375ba6c87..85978b036b 100644 --- a/src/systems/optimal_control_interface.jl +++ b/lib/ModelingToolkitBase/src/systems/optimal_control_interface.jl @@ -23,7 +23,7 @@ for solving using optimization. Must provide either `dt`, the timestep between c points (which, along with the timespan, determines the number of points), or directly provide the number of points as `steps`. -To construct the problem, please load InfiniteOpt along with ModelingToolkit. +To construct the problem, please load InfiniteOpt along with ModelingToolkitBase. """ function JuMPDynamicOptProblem end """ @@ -36,7 +36,7 @@ of the interpolation arrays. Related to `JuMPDynamicOptProblem`, but directly adds the differential equations of the system as derivative constraints, rather than using a solver tableau. -To construct the problem, please load InfiniteOpt along with ModelingToolkit. +To construct the problem, please load InfiniteOpt along with ModelingToolkitBase. """ function InfiniteOptDynamicOptProblem end """ @@ -47,7 +47,7 @@ for solving using optimization. Must provide either `dt`, the timestep between c points (which, along with the timespan, determines the number of points), or directly provide the number of points as `steps`. -To construct the problem, please load CasADi along with ModelingToolkit. +To construct the problem, please load CasADi along with ModelingToolkitBase. """ function CasADiDynamicOptProblem end """ @@ -58,7 +58,7 @@ for solving using optimization. Must provide either `dt`, the timestep between c points (which, along with the timespan, determines the number of points), or directly provide the number of points as `steps`. -To construct the problem, please load Pyomo along with ModelingToolkit. +To construct the problem, please load Pyomo along with ModelingToolkitBase. """ function PyomoDynamicOptProblem end @@ -398,28 +398,23 @@ function add_cost_function!(model, sys, tspan, pmap) return end - jcosts = substitute_model_vars(model, sys, [jcosts], tspan) - jcosts = substitute_params(pmap, jcosts) - jcosts = substitute_integral(model, only(jcosts), tspan) + rules = Dict{Any, Any}() + get_model_vars_substitution_rules!(rules, model, sys, tspan) + get_param_substitution_rules!(rules, pmap) + get_integral_substitution_rules!(rules, model, jcosts, tspan) + jcosts = substitute(jcosts, rules; fold = Val(true), filterer = Returns(true)) set_objective!(model, value(jcosts)) end -""" -Substitute integrals. For an integral from (ts, te): -- Free final time problems should transcribe this to (0, 1) in the case that (ts, te) is the original timespan. Free final time problems cannot handle partial timespans. -- CasADi cannot handle partial timespans, even for non-free-final time problems. -time problems and unchanged otherwise. -""" -function substitute_integral(model, expr, tspan) - intmap = Dict() +function get_integral_substitution_rules!(rules::Dict{Any, Any}, model, expr, tspan) for int in collect_applied_operators(expr, Symbolics.Integral) op = operation(int) arg = only(arguments(value(int))) lo, hi = value.((op.domain.domain.left, op.domain.domain.right)) lo, hi = process_integral_bounds(model, (lo, hi), tspan) - intmap[int] = lowered_integral(model, arg, lo, hi) + arg = substitute(arg, rules; fold = Val(true), filterer = Returns(true)) + rules[int] = lowered_integral(model, arg, lo, hi) end - Symbolics.substitute(expr, intmap) end function process_integral_bounds(model, integral_span, tspan) @@ -435,24 +430,19 @@ function process_integral_bounds(model, integral_span, tspan) end end -"""Substitute variables like x(1.5), x(t), etc. with the corresponding model variables.""" -function substitute_model_vars(model, sys, exprs, tspan) +function get_model_vars_substitution_rules!(rules::Dict{Any, Any}, model, sys, tspan) x_ops = [operation(unwrap(st)) for st in unknowns(sys)] c_ops = [operation(unwrap(ct)) for ct in unbound_inputs(sys)] t = get_iv(sys) - - exprs = map( - c -> Symbolics.fast_substitute(c, whole_t_map(model, t, x_ops, c_ops)), exprs) - + merge!(rules, whole_t_map(model, t, x_ops, c_ops)) (ti, tf) = tspan if symbolic_type(tf) === ScalarSymbolic() _tf = model.tₛ + ti - exprs = map( - c -> Symbolics.fast_substitute(c, free_t_map(model, tf, x_ops, c_ops)), exprs) - exprs = map(c -> Symbolics.fast_substitute(c, Dict(tf => _tf)), exprs) + merge!(rules, free_t_map(model, tf, x_ops, c_ops)) + rules[tf] = _tf end - exprs = map(c -> Symbolics.fast_substitute(c, fixed_t_map(model, x_ops, c_ops)), exprs) - exprs + merge!(rules, fixed_t_map(model, x_ops, c_ops)) + return nothing end """Mappings for variables that depend on the final time parameter, x(tf).""" @@ -488,9 +478,12 @@ function add_user_constraints!(model, sys, tspan, pmap) is_free_final(model) && check_constraint_vars(cons_dvs) - jconstraints = substitute_toterm(cons_dvs, jconstraints) - jconstraints = substitute_model_vars(model, sys, jconstraints, tspan) - jconstraints = substitute_params(pmap, jconstraints) + rules = Dict{Any, Any}() + get_toterm_substitution_rules!(rules, cons_dvs) + get_model_vars_substitution_rules!(rules, model, sys, tspan) + get_param_substitution_rules!(rules, pmap) + # `fixpoint_sub` to recursively substitute into `toterm` rules + jconstraints = fixpoint_sub(jconstraints, rules; fold = Val(true), filterer = Returns(true)) for c in jconstraints add_constraint!(model, c) @@ -498,15 +491,16 @@ function add_user_constraints!(model, sys, tspan, pmap) end function add_equational_constraints!(model, sys, pmap, tspan) - diff_eqs = substitute_model_vars(model, sys, diff_equations(sys), tspan) - diff_eqs = substitute_params(pmap, diff_eqs) - diff_eqs = substitute_differentials(model, sys, diff_eqs) + rules = Dict{Any, Any}() + get_model_vars_substitution_rules!(rules, model, sys, tspan) + get_param_substitution_rules!(rules, pmap) + get_differential_substitution_rules!(rules, model, sys) + diff_eqs = substitute(diff_equations(sys), rules; fold = Val(true), filterer = Returns(true)) for eq in diff_eqs - add_constraint!(model, eq.lhs ~ eq.rhs * model.tₛ) + add_constraint!(model, eq.lhs ~ unwrap_const(eq.rhs) * model.tₛ) end - alg_eqs = substitute_model_vars(model, sys, alg_equations(sys), tspan) - alg_eqs = substitute_params(pmap, alg_eqs) + alg_eqs = substitute(alg_equations(sys), rules; fold = Val(true), filterer = Returns(true)) for eq in alg_eqs add_constraint!(model, eq.lhs ~ eq.rhs) end @@ -515,21 +509,24 @@ end function set_objective! end objective_value(sol::DynamicOptSolution) = objective_value(sol.model) -function substitute_differentials(model, sys, eqs) +function get_differential_substitution_rules!(rules::Dict{Any, Any}, model, sys) t = get_iv(sys) D = Differential(t) - diffsubmap = Dict([D(lowered_var(model, :U, i, t)) => lowered_derivative(model, i) - for i in 1:length(unknowns(sys))]) - eqs = map(c -> Symbolics.substitute(c, diffsubmap), eqs) + diffsubmap = Dict([D(unk) => lowered_derivative(model, i) + for (i, unk) in enumerate(unknowns(sys))]) + merge!(rules, diffsubmap) + return nothing end -function substitute_toterm(vars, exprs) - toterm_map = Dict([u => default_toterm(value(u)) for u in vars]) - exprs = map(c -> Symbolics.fast_substitute(c, toterm_map), exprs) +function get_toterm_substitution_rules!(rules::Dict{Any, Any}, vars) + for u in vars + ttu = default_toterm(unwrap(u)) + isequal(u, ttu) || (rules[u] = ttu) + end end -function substitute_params(pmap::Dict, exprs) - exprs = map(c -> Symbolics.fixpoint_sub(c, pmap), exprs) +function get_param_substitution_rules!(rules::Dict{Any, Any}, pmap) + left_merge!(rules, pmap) end function check_constraint_vars(vars) diff --git a/src/systems/parameter_buffer.jl b/lib/ModelingToolkitBase/src/systems/parameter_buffer.jl similarity index 93% rename from src/systems/parameter_buffer.jl rename to lib/ModelingToolkitBase/src/systems/parameter_buffer.jl index a2421b5646..12091b508f 100644 --- a/src/systems/parameter_buffer.jl +++ b/lib/ModelingToolkitBase/src/systems/parameter_buffer.jl @@ -1,5 +1,5 @@ -symconvert(::Type{Symbolics.Struct{T}}, x) where {T} = convert(T, x) symconvert(::Type{T}, x::V) where {T, V} = convert(promote_type(T, V), x) +symconvert(::Type{T}, x::V) where {T <: Real, V} = convert(T, x) symconvert(::Type{Real}, x::Integer) = convert(Float16, x) symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) @@ -55,42 +55,46 @@ function MTKParameters( else error("Cannot create MTKParameters if system does not have index_cache") end - all_ps = Set(unwrap.(parameters(sys; initial_parameters = true))) - union!(all_ps, default_toterm.(unwrap.(parameters(sys; initial_parameters = true)))) - - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - op = to_varmap(op, ps) - symbols_to_symbolics!(sys, op) - defs = add_toterms(recursive_unwrap(defaults(sys))) - - is_time_dependent(sys) && add_observed!(sys, op) - add_parameter_dependencies!(sys, op) - - u0map = anydict() - pmap = anydict() - if fast_path - missing_pars = missingvars(op, ps) - else - _, missing_pars = build_operating_point!(sys, op, - u0map, pmap, defs, dvs, ps) - end - if t0 !== nothing - op[get_iv(sys)] = t0 + all_ps = Set(get_ps(sys)) + if !fast_path + op = operating_point_preprocess(sys, op) + if floatT === nothing + floatT = float(float_type_from_varmap(op)) + end + op = build_operating_point(sys, op; fast_path = true) + bound_ps = bound_parameters(sys) + bound_ics = intersect(bound_ps, keys(op)) + isempty(bound_ics) || throw(BoundInitialConditionsError(collect(bound_ics))) + no_override_merge!(op, bindings(sys)) + missing_ps = setdiff(all_ps, keys(op)) + to_rm = Set{SymbolicT}() + for p in missing_ps + Moshi.Match.@match p begin + BSImpl.Term(; f, args) && if f isa Initial end => begin + op[p] = args[1] + continue + end + _ => nothing + end + arr, isarr = split_indexed_var(p) + isarr || continue + haskey(op, arr) || continue + push!(to_rm, p) + end + setdiff!(missing_ps, keys(op), to_rm) + isempty(missing_ps) || throw(MissingParametersError(collect(missing_ps))) end if floatT === nothing floatT = float(float_type_from_varmap(op)) end - isempty(missing_pars) || throw(MissingParametersError(collect(missing_pars))) - evaluate_varmap!(op, ps; limit = substitution_limit) - - p = op - filter!(p) do kvp - kvp[1] in all_ps + if t0 !== nothing + op[get_iv(sys)] = t0 end + evaluate_varmap!(op, all_ps; limit = substitution_limit) + tunable_buffer = Vector{ic.tunable_buffer_size.type}( undef, ic.tunable_buffer_size.length) initials_buffer = Vector{ic.initials_buffer_size.type}( @@ -128,11 +132,24 @@ function MTKParameters( end return done end - for (sym, val) in p - sym = unwrap(sym) - val = unwrap(val) + for sym in all_ps + val = fixpoint_sub(sym, op; maxiters = max(div(substitution_limit, 2), 2), fold = Val(true)) ctype = symtype(sym) - if symbolic_type(val) !== NotSymbolic() + if !SU.isconst(val) + Moshi.Match.@match sym begin + BSImpl.Term(; f, args) && if f isa Initial end => begin + if isequal(val, args[1]) + if Symbolics.isarraysymbolic(sym) + val = BSImpl.Const{VartypeT}(fill(false, size(val))) + else + val = BSImpl.Const{VartypeT}(false) + end + end + end + _ => nothing + end + end + if !SU.isconst(val) error("Could not evaluate value of parameter $sym. Missing values for variables in expression $val.") end if ctype <: FnType @@ -141,20 +158,15 @@ function MTKParameters( if ctype == Real && floatT !== nothing ctype = floatT end - val = symconvert(ctype, val) - done = set_value(sym, val) - if !done && Symbolics.isarraysymbolic(sym) - 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) + if isinitial(sym) + if val === COMMON_NOTHING + val = COMMON_FALSE + elseif SU.is_array_shape(SU.shape(val)) && any(Base.Fix2(===, COMMON_NOTHING) ∘ Base.Fix1(getindex, val), SU.stable_eachindex(val)) + val = map(x -> x === COMMON_NOTHING ? false : unwrap_const(x), collect(val)) end end + val = symconvert(ctype, unwrap_const(val)) + set_value(sym, val) end tunable_buffer = narrow_buffer_type(tunable_buffer; p_constructor) if isempty(tunable_buffer) @@ -484,11 +496,11 @@ function validate_parameter_type(ic::IndexCache, p, idx::ParameterIndex, val) end stype = symtype(p) sz = if stype <: AbstractArray - Symbolics.shape(p) == Symbolics.Unknown() ? Symbolics.Unknown() : size(p) + size(p) elseif stype <: Number size(p) else - Symbolics.Unknown() + SU.Unknown(-1) end validate_parameter_type(ic, stype, sz, p, idx, val) end @@ -500,7 +512,7 @@ function validate_parameter_type(ic::IndexCache, idx::ParameterIndex, val) stype = AbstractArray{<:stype} end validate_parameter_type( - ic, stype, Symbolics.Unknown(), nothing, idx, val) + ic, stype, SU.Unknown(-1), nothing, idx, val) end function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) @@ -520,7 +532,7 @@ function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) :validate_parameter_type, sym === nothing ? index : sym, stype, val)) end # ... and must match sizes - if stype <: AbstractArray && sz != Symbolics.Unknown() && size(val) != sz + if stype <: AbstractArray && !(sz isa SU.Unknown) && size(val) != sz throw(InvalidParameterSizeException(sym, val)) end # Early exit @@ -558,6 +570,122 @@ function SymbolicIndexingInterface.remake_buffer(indp, oldbuf::MTKParameters, id _remake_buffer(indp, oldbuf, idxs, vals) end +function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) + return __remake_buffer(indp, oldbuf, idxs, vals; validate) +end + +function __remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) + newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) + @set! newbuf.initials = similar(oldbuf.initials, Any) + @set! newbuf.discrete = Tuple(similar(buf, Any) for buf in newbuf.discrete) + @set! newbuf.constant = Tuple(similar(buf, Any) for buf in newbuf.constant) + @set! newbuf.nonnumeric = Tuple(similar(buf, Any) for buf in newbuf.nonnumeric) + + function handle_parameter(ic, sym, idx, val) + if validate + if sym === nothing + validate_parameter_type(ic, idx, val) + else + validate_parameter_type(ic, sym, idx, val) + end + end + # `ParameterIndex(idx)` turns off size validation since it relies on there + # being an existing value + set_parameter!(newbuf, val, ParameterIndex(idx)) + end + + handled_idxs = Set{ParameterIndex}() + # 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 (idx, val) in zip(idxs, vals) + sym = nothing + if val === missing + val = get_temporary_value(idx) + end + if symbolic_type(idx) == ScalarSymbolic() + sym = idx + idx = parameter_index(ic, sym) + if idx === nothing + @warn "Symbolic variable $sym is not a (non-dependent) parameter in the system" + continue + end + idx in handled_idxs && continue + handle_parameter(ic, sym, idx, val) + push!(handled_idxs, idx) + elseif symbolic_type(idx) == ArraySymbolic() + sym = idx + idx = parameter_index(ic, sym) + if idx === nothing + symbolic_has_known_size(sym) || + throw(ParameterNotInSystem(sym)) + size(sym) == size(val) || throw(InvalidParameterSizeException(sym, val)) + + for (i, vali) in zip(eachindex(sym), eachindex(val)) + idx = parameter_index(ic, sym[i]) + if idx === nothing + @warn "Symbolic variable $sym is not a (non-dependent) parameter in the system" + continue + end + # Intentionally don't check handled_idxs here because array variables always take priority + # See Issue#2804 + handle_parameter(ic, sym[i], idx, val[vali]) + push!(handled_idxs, idx) + end + else + idx in handled_idxs && continue + handle_parameter(ic, sym, idx, val) + push!(handled_idxs, idx) + end + else # NotSymbolic + if !(idx isa ParameterIndex) + throw(ArgumentError("Expected index for parameter to be a symbolic variable or `ParameterIndex`, got $idx")) + end + handle_parameter(ic, nothing, idx, val) + end + end + + @set! newbuf.tunable = narrow_buffer_type_and_fallback_undefs( + oldbuf.tunable, newbuf.tunable) + if eltype(newbuf.tunable) <: Integer + T = promote_type(eltype(newbuf.tunable), Float64) + @set! newbuf.tunable = T.(newbuf.tunable) + end + @set! newbuf.initials = narrow_buffer_type_and_fallback_undefs( + oldbuf.initials, newbuf.initials) + if eltype(newbuf.initials) <: Integer + T = promote_type(eltype(newbuf.initials), Float64) + @set! newbuf.initials = T.(newbuf.initials) + end + @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) + for (oldv, newv) in zip(oldbuf.nonnumeric, newbuf.nonnumeric) + for i in eachindex(oldv) + isassigned(newv, i) && continue + newv[i] = oldv[i] + end + end + @set! newbuf.nonnumeric = Tuple( + typeof(oldv)(newv) for (oldv, newv) in zip(oldbuf.nonnumeric, newbuf.nonnumeric)) + if !ArrayInterface.ismutable(oldbuf) + @set! newbuf.tunable = similar_type(oldbuf.tunable, eltype(newbuf.tunable))(newbuf.tunable) + @set! newbuf.initials = similar_type(oldbuf.initials, eltype(newbuf.initials))(newbuf.initials) + @set! newbuf.discrete = ntuple(Val(length(newbuf.discrete))) do i + similar_type.(oldbuf.discrete[i], eltype(newbuf.discrete[i]))(newbuf.discrete[i]) + end + @set! newbuf.constant = ntuple(Val(length(newbuf.constant))) do i + similar_type.(oldbuf.constant[i], eltype(newbuf.constant[i]))(newbuf.constant[i]) + end + @set! newbuf.nonnumeric = ntuple(Val(length(newbuf.nonnumeric))) do i + similar_type.(oldbuf.nonnumeric[i], eltype(newbuf.nonnumeric[i]))(newbuf.nonnumeric[i]) + end + end + return newbuf +end + # For type-inference when using `SII.setp_oop` @generated function _remake_buffer( indp, oldbuf::MTKParameters{T, I, D, C, N, H}, @@ -691,122 +819,6 @@ end return expr end -function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) - return __remake_buffer(indp, oldbuf, idxs, vals; validate) -end - -function __remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) - newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) - @set! newbuf.initials = similar(oldbuf.initials, Any) - @set! newbuf.discrete = Tuple(similar(buf, Any) for buf in newbuf.discrete) - @set! newbuf.constant = Tuple(similar(buf, Any) for buf in newbuf.constant) - @set! newbuf.nonnumeric = Tuple(similar(buf, Any) for buf in newbuf.nonnumeric) - - function handle_parameter(ic, sym, idx, val) - if validate - if sym === nothing - validate_parameter_type(ic, idx, val) - else - validate_parameter_type(ic, sym, idx, val) - end - end - # `ParameterIndex(idx)` turns off size validation since it relies on there - # being an existing value - set_parameter!(newbuf, val, ParameterIndex(idx)) - end - - handled_idxs = Set{ParameterIndex}() - # 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 (idx, val) in zip(idxs, vals) - sym = nothing - if val === missing - val = get_temporary_value(idx) - end - if symbolic_type(idx) == ScalarSymbolic() - sym = idx - idx = parameter_index(ic, sym) - if idx === nothing - @warn "Symbolic variable $sym is not a (non-dependent) parameter in the system" - continue - end - idx in handled_idxs && continue - handle_parameter(ic, sym, idx, val) - push!(handled_idxs, idx) - elseif symbolic_type(idx) == ArraySymbolic() - sym = idx - idx = parameter_index(ic, sym) - if idx === nothing - Symbolics.shape(sym) == Symbolics.Unknown() && - throw(ParameterNotInSystem(sym)) - size(sym) == size(val) || throw(InvalidParameterSizeException(sym, val)) - - for (i, vali) in zip(eachindex(sym), eachindex(val)) - idx = parameter_index(ic, sym[i]) - if idx === nothing - @warn "Symbolic variable $sym is not a (non-dependent) parameter in the system" - continue - end - # Intentionally don't check handled_idxs here because array variables always take priority - # See Issue#2804 - handle_parameter(ic, sym[i], idx, val[vali]) - push!(handled_idxs, idx) - end - else - idx in handled_idxs && continue - handle_parameter(ic, sym, idx, val) - push!(handled_idxs, idx) - end - else # NotSymbolic - if !(idx isa ParameterIndex) - throw(ArgumentError("Expected index for parameter to be a symbolic variable or `ParameterIndex`, got $idx")) - end - handle_parameter(ic, nothing, idx, val) - end - end - - @set! newbuf.tunable = narrow_buffer_type_and_fallback_undefs( - oldbuf.tunable, newbuf.tunable) - if eltype(newbuf.tunable) <: Integer - T = promote_type(eltype(newbuf.tunable), Float64) - @set! newbuf.tunable = T.(newbuf.tunable) - end - @set! newbuf.initials = narrow_buffer_type_and_fallback_undefs( - oldbuf.initials, newbuf.initials) - if eltype(newbuf.initials) <: Integer - T = promote_type(eltype(newbuf.initials), Float64) - @set! newbuf.initials = T.(newbuf.initials) - end - @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) - for (oldv, newv) in zip(oldbuf.nonnumeric, newbuf.nonnumeric) - for i in eachindex(oldv) - isassigned(newv, i) && continue - newv[i] = oldv[i] - end - end - @set! newbuf.nonnumeric = Tuple( - typeof(oldv)(newv) for (oldv, newv) in zip(oldbuf.nonnumeric, newbuf.nonnumeric)) - if !ArrayInterface.ismutable(oldbuf) - @set! newbuf.tunable = similar_type(oldbuf.tunable, eltype(newbuf.tunable))(newbuf.tunable) - @set! newbuf.initials = similar_type(oldbuf.initials, eltype(newbuf.initials))(newbuf.initials) - @set! newbuf.discrete = ntuple(Val(length(newbuf.discrete))) do i - similar_type.(oldbuf.discrete[i], eltype(newbuf.discrete[i]))(newbuf.discrete[i]) - end - @set! newbuf.constant = ntuple(Val(length(newbuf.constant))) do i - similar_type.(oldbuf.constant[i], eltype(newbuf.constant[i]))(newbuf.constant[i]) - end - @set! newbuf.nonnumeric = ntuple(Val(length(newbuf.nonnumeric))) do i - similar_type.(oldbuf.nonnumeric[i], eltype(newbuf.nonnumeric[i]))(newbuf.nonnumeric[i]) - end - end - return newbuf -end - function as_any_buffer(p::MTKParameters) @set! p.tunable = similar(p.tunable, Any) @set! p.initials = similar(p.initials, Any) diff --git a/src/systems/pde/pdesystem.jl b/lib/ModelingToolkitBase/src/systems/pde/pdesystem.jl similarity index 92% rename from src/systems/pde/pdesystem.jl rename to lib/ModelingToolkitBase/src/systems/pde/pdesystem.jl index d44a9cbd3f..709186ffe3 100644 --- a/src/systems/pde/pdesystem.jl +++ b/lib/ModelingToolkitBase/src/systems/pde/pdesystem.jl @@ -48,10 +48,10 @@ struct PDESystem <: AbstractSystem "The parameters." ps::Any """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. + Initial conditions for variables (unknowns/observables/parameters) which can be + changed/overridden. When constructing a numerical problem from the system. """ - defaults::Dict + initial_conditions::SymmapT """ Type of the system. """ @@ -91,7 +91,7 @@ struct PDESystem <: AbstractSystem gui_metadata::Union{Nothing, GUIMetadata} @add_kwonly function PDESystem(eqs, bcs, domain, ivs, dvs, ps = SciMLBase.NullParameters(); - defaults = Dict(), + initial_conditions = Dict(), systems = [], connector_type = nothing, metadata = nothing, @@ -131,7 +131,7 @@ struct PDESystem <: AbstractSystem analytic_func = analytic_func isa Dict ? analytic_func : analytic_func |> Dict end - new(eqs, bcs, domain, ivs, dvs, ps, defaults, connector_type, systems, analytic, + new(eqs, bcs, domain, ivs, dvs, ps, initial_conditions, connector_type, systems, analytic, analytic_func, name, description, metadata, gui_metadata) end end @@ -163,6 +163,6 @@ function Base.show(io::IO, ::MIME"text/plain", sys::PDESystem) println(io, "Dependent Variables: ", get_dvs(sys)) println(io, "Independent Variables: ", get_ivs(sys)) println(io, "Parameters: ", get_ps(sys)) - print(io, "Default Parameter Values", get_defaults(sys)) + print(io, "Default Parameter Values", get_initial_conditions(sys)) return nothing end diff --git a/src/systems/problem_utils.jl b/lib/ModelingToolkitBase/src/systems/problem_utils.jl similarity index 80% rename from src/systems/problem_utils.jl rename to lib/ModelingToolkitBase/src/systems/problem_utils.jl index ab7735e05f..c6fb46f088 100644 --- a/src/systems/problem_utils.jl +++ b/lib/ModelingToolkitBase/src/systems/problem_utils.jl @@ -17,10 +17,10 @@ anydict(x) = AnyDict(x) """ $(TYPEDSIGNATURES) -Check if `x` is a symbolic with known size. Assumes `Symbolics.shape(unwrap(x))` +Check if `x` is a symbolic with known size. Assumes `SymbolicUtils.shape(unwrap(x))` is a valid operation. """ -is_sized_array_symbolic(x) = Symbolics.shape(unwrap(x)) != Symbolics.Unknown() +symbolic_has_known_size(x) = !(SU.shape(unwrap(x)) isa SU.Unknown) """ $(TYPEDSIGNATURES) @@ -104,85 +104,6 @@ function get_and_getindex(varmap, var, idx) return val[idx] end -""" - $(TYPEDSIGNATURES) - -Ensure `varmap` contains entries for all variables in `vars` by using values from -`fallbacks` if they don't already exist in `varmap`. Return the set of all variables in -`vars` not present in `varmap` or `fallbacks`. If an array variable in `vars` does not -exist in `varmap` or `fallbacks`, each of its scalarized elements will be searched for. -In case none of the scalarized elements exist, the array variable will be reported as -missing. In case some of the scalarized elements exist, the missing elements will be -reported as missing. If `fallbacks` contains both the scalarized and non-scalarized forms, -the latter will take priority. - -Variables as they are specified in `vars` will take priority over their `toterm` forms. -""" -function add_fallbacks!( - varmap::AnyDict, vars::Vector, fallbacks::Dict; toterm = default_toterm) - missingvars = Set() - arrvars = Set() - for var in vars - haskey(varmap, var) && continue - ttvar = toterm(var) - haskey(varmap, ttvar) && continue - - # array symbolics with a defined size may be present in the scalarized form - if Symbolics.isarraysymbolic(var) && is_sized_array_symbolic(var) - val = map(eachindex(var)) do idx - # @something is lazy and saves from writing a massive if-elseif-else - @something(get(varmap, var[idx], nothing), - get(varmap, ttvar[idx], nothing), get_and_getindex(fallbacks, var, idx), - get_and_getindex(fallbacks, ttvar, idx), get( - fallbacks, var[idx], nothing), - get(fallbacks, ttvar[idx], nothing), Some(nothing)) - end - # only push the missing entries - mask = map(x -> x === nothing, val) - if all(mask) - push!(missingvars, var) - elseif any(mask) - for i in eachindex(var) - if mask[i] - push!(missingvars, var) - else - varmap[var[i]] = val[i] - end - end - else - varmap[var] = val - end - else - if iscall(var) && operation(var) == getindex - args = arguments(var) - arrvar = args[1] - ttarrvar = toterm(arrvar) - idxs = args[2:end] - val = @something get(varmap, arrvar, nothing) get(varmap, ttarrvar, nothing) get( - fallbacks, arrvar, nothing) get(fallbacks, ttarrvar, nothing) Some(nothing) - if val !== nothing - val = val[idxs...] - is_sized_array_symbolic(arrvar) && push!(arrvars, arrvar) - end - else - val = nothing - end - val = @something val get(fallbacks, var, nothing) get(fallbacks, ttvar, nothing) Some(nothing) - if val === nothing - push!(missingvars, var) - else - varmap[var] = val - end - end - end - - for arrvar in arrvars - varmap[arrvar] = collect(arrvar) - end - - return missingvars -end - """ $(TYPEDSIGNATURES) @@ -190,29 +111,16 @@ Return the list of variables in `varlist` not present in `varmap`. Uses the same for missing array variables and `toterm` forms as [`add_fallbacks!`](@ref). """ function missingvars( - varmap::AbstractDict, varlist::Vector; toterm = default_toterm) - missingvars = Set() + varmap::AtomicArrayDict, varlist::Vector; toterm = default_toterm) + missings = Set{SymbolicT}() for var in varlist - haskey(varmap, var) && continue + var = unwrap(var) + get_possibly_indexed(varmap, var, COMMON_NOTHING) === COMMON_NOTHING || continue ttsym = toterm(var) - haskey(varmap, ttsym) && continue - - if Symbolics.isarraysymbolic(var) && is_sized_array_symbolic(var) - mask = map(eachindex(var)) do idx - !haskey(varmap, var[idx]) && !haskey(varmap, ttsym[idx]) - end - if all(mask) - push!(missingvars, var) - else - for i in eachindex(var) - mask[i] && push!(missingvars, var[i]) - end - end - else - push!(missingvars, var) - end + get_possibly_indexed(varmap, ttsym, COMMON_NOTHING) === COMMON_NOTHING || continue + push!(missings, var) end - return missingvars + return missings end """ @@ -242,7 +150,7 @@ symbolic values, all of which need to be unwrapped. Specializes when `x isa Abst to unwrap keys and values, returning an `AnyDict`. """ function recursive_unwrap(x::AbstractArray) - symbolic_type(x) == ArraySymbolic() ? unwrap(x) : recursive_unwrap.(x) + symbolic_type(x) == ArraySymbolic() ? value(x) : recursive_unwrap.(x) end function recursive_unwrap(x::SparseMatrixCSC) @@ -252,7 +160,7 @@ function recursive_unwrap(x::SparseMatrixCSC) return sparse(I, J, V, m, n) end -recursive_unwrap(x) = unwrap(x) +recursive_unwrap(x) = value(x) function recursive_unwrap(x::AbstractDict) return anydict(unwrap(k) => recursive_unwrap(v) for (k, v) in x) @@ -265,15 +173,20 @@ Add equations `eqs` to `varmap`. Assumes each element in `eqs` maps a single sym variable to an expression representing its value. In case `varmap` already contains an entry for `eq.lhs`, insert the reverse mapping if `eq.rhs` is not a number. """ -function add_observed_equations!(varmap::AbstractDict, eqs) +function add_observed_equations!(varmap::AtomicArrayDict{SymbolicT}, eqs::Vector{Equation}, bound_ps::Union{Nothing, ROSymmapT} = nothing) for eq in eqs - if var_in_varlist(eq.lhs, keys(varmap), nothing) - eq.rhs isa Number && continue - var_in_varlist(eq.rhs, keys(varmap), nothing) && continue - !iscall(eq.rhs) || issym(operation(eq.rhs)) || continue - varmap[eq.rhs] = eq.lhs + if has_possibly_indexed_key(varmap, eq.lhs) + SU.isconst(eq.rhs) && continue + has_possibly_indexed_key(varmap, eq.rhs) && continue + bound_ps isa ROSymmapT && has_possibly_indexed_key(parent(bound_ps), eq.rhs) && continue + Moshi.Match.@match eq.rhs begin + BSImpl.Term(; f, args) && if f isa SymbolicT end => nothing + BSImpl.Sym(;) => nothing + _ => continue + end + write_possibly_indexed_array!(varmap, eq.rhs, eq.lhs, COMMON_NOTHING) else - varmap[eq.lhs] = eq.rhs + write_possibly_indexed_array!(varmap, eq.lhs, eq.rhs, COMMON_NOTHING) end end end @@ -287,17 +200,6 @@ function add_observed!(sys::AbstractSystem, varmap::AbstractDict) add_observed_equations!(varmap, observed(sys)) end -""" - $(TYPEDSIGNATURES) - -Add all equations in `parameter_dependencies(sys)` to `varmap` using -[`add_observed_equations!`](@ref). -""" -function add_parameter_dependencies!(sys::AbstractSystem, varmap::AbstractDict) - has_parameter_dependencies(sys) || return nothing - add_observed_equations!(varmap, parameter_dependencies(sys)) -end - struct UnexpectedSymbolicValueInVarmap <: Exception sym::Any val::Any @@ -350,6 +252,29 @@ function Base.showerror(io::IO, e::MissingVariablesError) println(io, join(e.vars, ", ")) end +""" + $TYPEDEF + +A Moshi.jl enum to allow choosing what happens with missing guess values when building a +numerical problem from a `System`. + +# Variants + +- `MissingGuessValue.Constant(val::Number)`: Missing guesses are set to the given value + `val`. +- `MissingGuessValue.Random(rng::AbstractRNG)`: Missing guesses are set to `rand(rng)`. +- `MissingGuessValue.Error()`: Missing guess values cause an error. +""" +Moshi.Data.@data MissingGuessValue begin + Constant(Number) + Random(AbstractRNG) + Error +end + +# To be overloaded downstream by MTK +default_missing_guess_value() = default_missing_guess_value(nothing) +default_missing_guess_value(_) = MissingGuessValue.Constant(true) + """ $(TYPEDSIGNATURES) @@ -376,30 +301,74 @@ Keyword arguments: function varmap_to_vars(varmap::AbstractDict, vars::Vector; tofloat = true, use_union = false, container_type = Array, buffer_eltype = Nothing, toterm = default_toterm, check = true, allow_symbolic = false, - is_initializeprob = false, substitution_limit = 100) + is_initializeprob = false, substitution_limit = 100, missing_values = MissingGuessValue.Error()) isempty(vars) && return nothing - varmap = recursive_unwrap(varmap) + if !(varmap isa SymmapT) + varmap = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(varmap), COMMON_NOTHING) + end if toterm !== nothing add_toterms!(varmap; toterm) end if check && !allow_symbolic missing_vars = missingvars(varmap, vars; toterm) - if !isempty(missing_vars) - if is_initializeprob - throw(MissingGuessError(collect(missing_vars), collect(missing_vars))) - else - throw(MissingVariablesError(missing_vars)) + Moshi.Match.@match missing_values begin + MissingGuessValue.Constant(val) => begin + cval = BSImpl.Const{VartypeT}(val) + for var in missing_vars + if Symbolics.isarraysymbolic(var) + varmap[var] = BSImpl.Const{VartypeT}(fill(val, size(var))) + else + write_possibly_indexed_array!(varmap, var, cval, COMMON_NOTHING) + end + end + end + MissingGuessValue.Random(rng) => begin + for var in missing_vars + if Symbolics.isarraysymbolic(var) + varmap[var] = rand(rng, size(var)) + else + write_possibly_indexed_array!(varmap, var, rand(rng), COMMON_NOTHING) + end + end + end + MissingGuessValue.Error() => begin + if !isempty(missing_vars) + if is_initializeprob + throw(MissingGuessError(collect(missing_vars), collect(missing_vars))) + else + throw(MissingVariablesError(missing_vars)) + end + end + end end end evaluate_varmap!(varmap, vars; limit = substitution_limit) - vals = map(x -> get(varmap, x, x), vars) + vals = map(vars) do x + x = unwrap(x) + v = get_possibly_indexed(varmap, x, x) + Moshi.Match.@match v begin + BSImpl.Const(; val) => return val + _ => begin + Moshi.Match.@match x begin + BSImpl.Term(; f, args) && if f isa Initial && isequal(v, args[1]) end => begin + if Symbolics.isarraysymbolic(v) + return fill(false, size(v)) + else + return false + end + end + _ => return v + end + end + end + end if !allow_symbolic missingsyms = Any[] missingvals = Any[] for (sym, val) in zip(vars, vals) - symbolic_type(val) == NotSymbolic() && continue + val !== nothing && symbolic_type(val) == NotSymbolic() && continue push!(missingsyms, sym) push!(missingvals, val) end @@ -453,14 +422,14 @@ function check_substitution_cycles( for (k, v) in varmap kidx = var_to_idx[k] if symbolic_type(v) != NotSymbolic() - vars!(buffer, v) + SU.search_variables!(buffer, v) for var in buffer haskey(var_to_idx, var) || continue add_edge!(graph, kidx, var_to_idx[var]) end elseif v isa AbstractArray for val in v - vars!(buffer, val) + SU.search_variables!(buffer, val) end for var in buffer haskey(var_to_idx, var) || continue @@ -487,13 +456,13 @@ Performs symbolic substitution on the values in `varmap` for the keys in `vars`, `varmap` itself as the set of substitution rules. If an entry in `vars` is not a key in `varmap`, it is ignored. """ -function evaluate_varmap!(varmap::AbstractDict, vars; limit = 100) +function evaluate_varmap!(varmap::AtomicArrayDict, vars; limit = 100) for k in vars - v = get(varmap, k, nothing) - v === nothing && continue - symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && continue - haskey(varmap, k) || continue - varmap[k] = fixpoint_sub(v, varmap; maxiters = limit) + arr, _ = split_indexed_var(unwrap(k)) + v = get(varmap, arr, COMMON_NOTHING) + v === COMMON_NOTHING && continue + SU.isconst(v) && continue + varmap[arr] = fixpoint_sub(v, varmap; maxiters = limit, fold = Val(true)) end end @@ -542,7 +511,7 @@ If a scalarized entry already exists, it is not overridden. function scalarize_vars_in_varmap!(varmap::AbstractDict, vars) for var in vars symbolic_type(var) == ArraySymbolic() || continue - is_sized_array_symbolic(var) || continue + symbolic_has_known_size(var) || continue haskey(varmap, var) || continue for i in eachindex(var) haskey(varmap, var[i]) && continue @@ -583,76 +552,25 @@ function EmptySciMLFunction{iip}(args...; kwargs...) where {iip} end """ - $(TYPEDSIGNATURES) - -Construct the operating point of the system from the user-provided `u0map` and `pmap`, system -defaults `defs`, unknowns `dvs` and parameters `ps`. Return the operating point as a dictionary, -the list of unknowns for which no values can be determined, and the list of parameters for which -no values can be determined. + $TYPEDSIGNATURES -Also updates `u0map` and `pmap` in-place to contain all the initial conditions in `op`, split -by unknowns and parameters respectively. +For every `Initial(x)` parameter in `sys`, add `Initial(x) => x` to `op` if it does not +already contain that key. """ -function build_operating_point!(sys::AbstractSystem, - op::AbstractDict, u0map::AbstractDict, pmap::AbstractDict, defs::AbstractDict, dvs, ps) - add_toterms!(op) - missing_unknowns = add_fallbacks!(op, dvs, defs) - for (k, v) in defs - haskey(op, k) && continue - op[k] = v - end - filter_missing_values!(op; missing_values = missing_unknowns) - - merge!(op, pmap) - missing_pars = add_fallbacks!(op, ps, defs) - filter_missing_values!(op; missing_values = missing_pars) - - filter!(kvp -> kvp[2] === nothing, u0map) - filter!(kvp -> kvp[2] === nothing, pmap) - neithermap = anydict() - - for (k, v) in op - k = unwrap(k) - if is_parameter(sys, k) - pmap[k] = v - elseif has_parameter_dependency_with_lhs(sys, k) && is_variable_floatingpoint(k) && - v !== nothing && !isequal(v, Initial(k)) - op[Initial(k)] = v - pmap[Initial(k)] = v - op[k] = Initial(k) - pmap[k] = Initial(k) - elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) || - iscall(k) && - operation(k) isa Differential && is_variable(sys, arguments(k)[1]) - if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && - v !== nothing - op[Initial(k)] = v - pmap[Initial(k)] = v - op[k] = Initial(k) - v = Initial(k) +function add_initials!(sys::AbstractSystem, op::SymmapT) + for p in get_ps(sys) + haskey(op, p) && continue + Moshi.Match.@match p begin + BSImpl.Term(; f, args) && if f isa Initial end => begin + write_possibly_indexed_array!(op, p, if Symbolics.isarraysymbolic(p) + BSImpl.Const{VartypeT}(fill(false, size(p))) + else + COMMON_FALSE + end, COMMON_FALSE) end - u0map[k] = v - else - neithermap[k] = v - end - end - - if !isempty(neithermap) - for (k, v) in u0map - symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && continue - v = fixpoint_sub(v, neithermap; operator = Symbolics.Operator) - isequal(k, v) && continue - u0map[k] = v - end - for (k, v) in pmap - symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && continue - v = fixpoint_sub(v, neithermap; operator = Symbolics.Operator) - isequal(k, v) && continue - pmap[k] = v + _ => nothing end end - - return missing_unknowns, missing_pars end """ @@ -819,12 +737,14 @@ function get_mtkparameters_reconstructor(srcsys::AbstractSystem, dstsys::Abstrac # tuple of `BlockedArray`s Base.Fix2(Broadcast.BroadcastFunction(BlockedArray), blockarrsizes) ∘ Base.Fix1(broadcast, p_constructor) ∘ - concrete_getu(srcsys, syms[3]; eval_expression, eval_module) + # This `broadcast.(collect, ...)` avoids `ReshapedArray`/`SubArray`s from + # appearing in the result. + concrete_getu(srcsys, Tuple(broadcast.(collect, syms[3])); eval_expression, eval_module) end const_getter = if syms[4] == () Returns(()) else - Base.Fix1(broadcast, p_constructor) ∘ getu(srcsys, syms[4]) + Base.Fix1(broadcast, p_constructor) ∘ getu(srcsys, Tuple(syms[4])) end nonnumeric_getter = if syms[5] == () Returns(()) @@ -836,7 +756,7 @@ function get_mtkparameters_reconstructor(srcsys::AbstractSystem, dstsys::Abstrac # nonnumerics retain the assigned buffer type without narrowing Base.Fix1(broadcast, _p_constructor) ∘ Base.Fix1(Broadcast.BroadcastFunction(call), buftypes) ∘ - concrete_getu(srcsys, syms[5]; eval_expression, eval_module) + concrete_getu(srcsys, Tuple(syms[5]); eval_expression, eval_module) end getters = ( tunable_getter, initials_getter, discs_getter, const_getter, nonnumeric_getter) @@ -909,7 +829,7 @@ function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) end u0 = rip.ugetter(srcvalp) # and the eltype of the destination u0 - if T != eltype(u0) && T != Union{} + if T != eltype(u0) && T != Union{} && T !== Any u0 = T.(u0) end # apply the promotion to tunables portion @@ -975,9 +895,11 @@ end A function to be used as `update_initializeprob!` in `OverrideInitData`. Requires `is_update_oop = Val(true)` to be passed to `update_initializeprob!`. + +Any changes to this method should also be made to the one in ChainRulesCoreExt. """ function update_initializeprob!(initprob, prob) - pgetter = ChainRulesCore.@ignore_derivatives get_scimlfn(prob).initialization_data.metadata.oop_reconstruct_u0_p.pgetter + pgetter = get_scimlfn(prob).initialization_data.metadata.oop_reconstruct_u0_p.pgetter p = pgetter(prob, initprob) return remake(initprob; p) end @@ -996,11 +918,11 @@ struct InitializationMetadata{R <: ReconstructInitializeprob, GUU, SIU} """ The operating point used to construct the initialization. """ - op::Dict{Any, Any} + op::SymmapT """ The `guesses` used to construct the initialization. """ - guesses::Dict{Any, Any} + guesses::SymmapT """ The `initialization_eqs` in addition to those of the system that were used to construct the initialization. @@ -1061,7 +983,8 @@ function GetUpdatedU0(sys::AbstractSystem, initprob::SciMLBase.AbstractNonlinear eqs = equations(sys) guessvars = trues(length(dvs)) for (i, var) in enumerate(dvs) - guessvars[i] = !isequal(get(op, var, nothing), Initial(var)) + varval = get(op, var, COMMON_NOTHING) + guessvars[i] = varval === COMMON_NOTHING || !SU.isconst(varval) end get_guessvars = getu(initprob, dvs[guessvars]) get_initial_unknowns = getu(sys, Initial.(dvs)) @@ -1121,19 +1044,20 @@ constructed is in implicit DAE form (`DAEProblem`). All other keyword arguments to `InitializationProblem`. """ function maybe_build_initialization_problem( - sys::AbstractSystem, iip, op::AbstractDict, t, defs, - guesses, missing_unknowns; implicit_dae = false, + sys::AbstractSystem, iip, op::AbstractDict, t, guesses; time_dependent_init = is_time_dependent(sys), u0_constructor = identity, p_constructor = identity, floatT = Float64, initialization_eqs = [], - use_scc = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) - guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) + use_scc = true, eval_expression = false, eval_module = @__MODULE__, + implicit_dae = false, kwargs...) + guesses = merge(ModelingToolkitBase.guesses(sys), todict(guesses)) if t === nothing && is_time_dependent(sys) t = zero(floatT) end - initializeprob = ModelingToolkit.InitializationProblem{iip}( - sys, t, op; guesses, time_dependent_init, initialization_eqs, + orig_op = copy(op) + initializeprob = ModelingToolkitBase.InitializationProblem{iip}( + sys, t, op; guesses, time_dependent_init, initialization_eqs, fast_path = true, use_scc, u0_constructor, p_constructor, eval_expression, eval_module, kwargs...) if state_values(initializeprob) !== nothing _u0 = state_values(initializeprob) @@ -1169,7 +1093,9 @@ function maybe_build_initialization_problem( nothing end meta = InitializationMetadata( - copy(op), copy(guesses), Vector{Equation}(initialization_eqs), + orig_op, + as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(guesses), COMMON_NOTHING), + Vector{Equation}(initialization_eqs), use_scc, time_dependent_init, ReconstructInitializeprob( sys, initializeprob.f.sys; u0_constructor, @@ -1179,8 +1105,12 @@ function maybe_build_initialization_problem( if time_dependent_init all_init_syms = Set(all_symbols(initializeprob)) solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) - initializeprobmap = u0_constructor ∘ safe_float ∘ - getu(initializeprob, solved_unknowns) + if isempty(solved_unknowns) + initializeprobmap = Returns(nothing) + else + initializeprobmap = u0_constructor ∘ safe_float ∘ + getu(initializeprob, solved_unknowns) + end else initializeprobmap = nothing end @@ -1199,37 +1129,56 @@ function maybe_build_initialization_problem( if initializeprobmap === nothing && initializeprobpmap === nothing update_initializeprob! = nothing else - update_initializeprob! = ModelingToolkit.update_initializeprob! + update_initializeprob! = ModelingToolkitBase.update_initializeprob! end - filter!(punknowns) do p - is_parameter_solvable(p, op, defs, guesses) && get(op, p, missing) === missing + missingvars = AtomicArraySet() + for (k, v) in op + v === COMMON_MISSING && push!(missingvars, k) end - # See comment below for why `getu` is not used here. - _pgetter = build_explicit_observed_function(initializeprob.f.sys, punknowns) - pvals = _pgetter(state_values(initializeprob), parameter_values(initializeprob)) - for (p, pval) in zip(punknowns, pvals) - p = unwrap(p) - op[p] = pval - if iscall(p) && operation(p) === getindex - arrp = arguments(p)[1] - get(op, arrp, nothing) !== missing && continue - op[arrp] = collect(arrp) + if time_dependent_init + binds = bindings(sys) + for v in unknowns(sys) + has_possibly_indexed_key(parent(binds), v) && continue + if get_possibly_indexed(op, v, COMMON_NOTHING) === COMMON_NOTHING + push_as_atomic_array!(missingvars, v) + end + end + if implicit_dae + initsys = initializeprob.f.sys + for v in unknowns(sys) + v = Differential(get_iv(sys))(v) + ttv = default_toterm(v) + if get_possibly_indexed(op, v, COMMON_NOTHING) === COMMON_NOTHING && + get_possibly_indexed(op, ttv, COMMON_NOTHING) === COMMON_NOTHING && + # FIXME: Derivatives of algebraic variables aren't present + (is_variable(initsys, ttv) || has_observed_with_lhs(initsys, ttv)) + push_as_atomic_array!(missingvars, ttv) + end + end end end + for v in get_all_discretes_fast(sys) + has_possibly_indexed_key(parent(binds), v) && continue + has_possibly_indexed_key(op, v) || push_as_atomic_array!(missingvars, v) + end + for (k, v) in bindings(sys) + v === COMMON_MISSING && !has_possibly_indexed_key(op, k) && push!(missingvars, k) + end + for p in as_atomic_array_set(parameters(sys)) + haskey(op, p) || push!(missingvars, p) + end + missingvars = collect(missingvars) - if time_dependent_init - # We can't use `getu` here because that goes to `SII.observed`, which goes to - # `ObservedFunctionCache` which uses `eval_expression` and `eval_module`. If - # `eval_expression == true`, this then runs into world-age issues. Building an - # RGF here is fine since it is always discarded. We can't use `eval_module` for - # the RGF since the user may not have run RGF's init. - _ugetter = build_explicit_observed_function(initializeprob.f.sys, collect(missing_unknowns)) - uvals = _ugetter(state_values(initializeprob), parameter_values(initializeprob)) - for (v, val) in zip(missing_unknowns, uvals) - op[v] = val - end - empty!(missing_unknowns) + # We can't use `getu` here because that goes to `SII.observed`, which goes to + # `ObservedFunctionCache` which uses `eval_expression` and `eval_module`. If + # `eval_expression == true`, this then runs into world-age issues. Building an + # RGF here is fine since it is always discarded. We can't use `eval_module` for + # the RGF since the user may not have run RGF's init. + _pgetter = build_explicit_observed_function(initializeprob.f.sys, missingvars) + pvals = _pgetter(state_values(initializeprob), parameter_values(initializeprob)) + for (p, pval) in zip(missingvars, pvals) + op[p] = pval end return (; @@ -1238,6 +1187,9 @@ function maybe_build_initialization_problem( initializeprobpmap; metadata = meta, is_update_oop = Val(true))) end +rm_union(::Type{Union{T, Nothing}}) where {T} = T +rm_union(::Type{T}) where {T} = T + """ $(TYPEDSIGNATURES) @@ -1247,13 +1199,14 @@ with a constant value. function float_type_from_varmap(varmap, floatT = Bool) for (k, v) in varmap is_variable_floatingpoint(k) || continue - symbolic_type(v) == NotSymbolic() || continue + SU.isconst(v) || symbolic_type(v) isa NotSymbolic || continue is_array_of_symbolics(v) && continue - + v = unwrap_const(v) if v isa AbstractArray - floatT = promote_type(floatT, eltype(v)) + # Remove union in case some elements of the array are `nothing` + floatT = promote_type(floatT, rm_union(eltype(unwrap_const(v)))) elseif v isa Number - floatT = promote_type(floatT, typeof(v)) + floatT = promote_type(floatT, typeof(unwrap_const(v))) end end return float(floatT) @@ -1300,7 +1253,7 @@ function get_u0_constructor(u0_constructor, u0Type::Type, floatT::Type, symbolic u0_constructor === identity || return u0_constructor u0Type <: StaticArray || return u0_constructor return function (vals) - elT = if symbolic_u0 && any(x -> symbolic_type(x) != NotSymbolic(), vals) + elT = if symbolic_u0 && any(x -> x === nothing || symbolic_type(x) != NotSymbolic(), vals) nothing else floatT @@ -1326,6 +1279,42 @@ end abstract type ProblemConstructionHook end +function operating_point_preprocess(sys::AbstractSystem, op) + if op !== nothing && !(eltype(op) <: Pair) && !isempty(op) + throw(ArgumentError(""" + The operating point passed to the problem constructor must be a symbolic map. + """)) + end + op = recursive_unwrap(anydict(op)) + symbols_to_symbolics!(sys, op) + return op +end + +function build_operating_point(sys::AbstractSystem, op; fast_path = false) + if !fast_path + op = operating_point_preprocess(sys, op) + end + # Replace `nothing`s with sentinels so that `left_merge!` thinks they're values + # and doesn't override them. This is because explicit `nothing` values in `op` + # should be considered as overrides for initial conditions in `ics`. + map!(x -> @something(x, CommonSentinel()), values(op)) + op = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(op), COMMON_NOTHING) + ics = add_toterms(initial_conditions(sys); replace = is_discrete_system(sys)) + left_merge!(op, ics) + map!(values(op)) do v + v === COMMON_SENTINEL && return COMMON_NOTHING + Symbolics.isarraysymbolic(v) || return v + any(Base.Fix2(===, COMMON_SENTINEL) ∘ Base.Fix1(getindex, v), SU.stable_eachindex(v)) || return v + + new_v = map(SU.stable_eachindex(v)) do i + v[i] === COMMON_SENTINEL ? COMMON_NOTHING : v[i] + end + return SU.Const{VartypeT}(new_v) + end + filter!(Base.Fix2(!==, COMMON_NOTHING) ∘ last, op) + return op +end + """ $(TYPEDSIGNATURES) @@ -1362,7 +1351,7 @@ function process_SciMLProblem( circular_dependency_max_cycle_length = length(all_symbols(sys)), circular_dependency_max_cycles = 10, substitution_limit = 100, use_scc = true, time_dependent_init = is_time_dependent(sys), - algebraic_only = false, + algebraic_only = false, missing_guess_value = default_missing_guess_value(), allow_incomplete = false, is_initializeprob = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys; initial_parameters = true) @@ -1373,33 +1362,25 @@ function process_SciMLProblem( u0Type = pType = typeof(op) - op = to_varmap(op, dvs) - symbols_to_symbolics!(sys, op) + op = operating_point_preprocess(sys, op) + floatT = calculate_float_type(op, u0Type) + u0_eltype = something(u0_eltype, floatT) + + op = build_operating_point(sys, op; fast_path = true) check_inputmap_keys(sys, op) op = getmetadata(sys, ProblemConstructionHook, identity)(op) - defs = add_toterms(recursive_unwrap(defaults(sys)); replace = is_discrete_system(sys)) kwargs = NamedTuple(kwargs) - if eltype(eqs) <: Equation - obs, eqs = unhack_observed(observed(sys), eqs) - else - obs, _ = unhack_observed(observed(sys), Equation[x for x in eqs if x isa Equation]) - end + add_initials!(sys, op) - u0map = anydict() - pmap = anydict() - missing_unknowns, - missing_pars = build_operating_point!(sys, op, - u0map, pmap, defs, dvs, ps) + obs, eqs = unhack_observed(observed(sys), eqs) - floatT = calculate_float_type(op, u0Type) - u0_eltype = something(u0_eltype, floatT) if !is_time_dependent(sys) || is_initializesystem(sys) - add_observed_equations!(op, obs) + add_observed_equations!(op, obs, bindings(sys)) end u0_constructor = get_u0_constructor(u0_constructor, u0Type, u0_eltype, symbolic_u0) @@ -1408,13 +1389,13 @@ function process_SciMLProblem( if build_initializeprob kws = maybe_build_initialization_problem( sys, constructor <: SciMLBase.AbstractSciMLFunction{true}, - op, t, defs, guesses, missing_unknowns; - implicit_dae, warn_initialize_determined, initialization_eqs, + op, t, guesses; + warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, warn_cyclic_dependency, check_units = check_initialization_units, circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, algebraic_only, allow_incomplete, u0_constructor, p_constructor, floatT, - time_dependent_init) + time_dependent_init, missing_guess_value, implicit_dae) kwargs = merge(kwargs, kws) end @@ -1423,8 +1404,19 @@ function process_SciMLProblem( op[iv] = t end + binds = bindings(sys) + # If we aren't building an initialization problem, we aren't respecting non-parameter + # bindings anyway. + if build_initializeprob + no_override_merge_except_missing!(op, binds) + else + for p in bound_parameters(sys) + haskey(op, p) && throw(ArgumentError("Cannot provide initial value for bound parameter $p.")) + op[p] = binds[p] + end + left_merge!(op, binds) + end add_observed_equations!(op, obs) - add_parameter_dependencies!(sys, op) if warn_cyclic_dependency cycles = check_substitution_cycles( @@ -1440,10 +1432,16 @@ function process_SciMLProblem( end end - u0 = varmap_to_vars( - op, dvs; buffer_eltype = u0_eltype, container_type = u0Type, - allow_symbolic = symbolic_u0, is_initializeprob, substitution_limit) - + if is_initializeprob + u0 = varmap_to_vars( + op, dvs; buffer_eltype = u0_eltype, container_type = u0Type, + allow_symbolic = symbolic_u0, is_initializeprob, substitution_limit, + missing_values = missing_guess_value) + else + u0 = varmap_to_vars( + op, dvs; buffer_eltype = u0_eltype, container_type = u0Type, + allow_symbolic = symbolic_u0, is_initializeprob, substitution_limit) + end if u0 !== nothing u0 = u0_constructor(u0) end @@ -1475,7 +1473,7 @@ function process_SciMLProblem( end if implicit_dae - ddvs = map(Differential(iv), dvs) + ddvs = map(default_toterm ∘ Differential(iv), dvs) du0 = varmap_to_vars(op, ddvs; toterm = default_toterm, tofloat) kwargs = merge(kwargs, (; ddvs)) @@ -1483,7 +1481,7 @@ function process_SciMLProblem( du0 = nothing end - if build_initializeprob + if build_initializeprob && (u0 === nothing || eltype(u0) <: Number) t0 = t if is_time_dependent(sys) && t0 === nothing t0 = zero(floatT) @@ -1597,7 +1595,7 @@ function SymbolicTstops( if is_array_of_symbolics(val) || val isa AbstractArray collect(val) else - term(:, t0, unwrap(val), t1; type = AbstractArray{Real}) + term(:, t0, unwrap(val), t1; type = Vector{Real}) end end rps = reorder_parameters(sys) @@ -1811,17 +1809,13 @@ Return the `u0` vector for the given system `sys` and variable-value mapping `va keyword arguments are forwarded to [`varmap_to_vars`](@ref). """ function get_u0(sys::AbstractSystem, varmap; kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - op = to_varmap(varmap, dvs) - add_observed!(sys, op) - add_parameter_dependencies!(sys, op) - missing_dvs, _ = build_operating_point!( - sys, op, Dict(), Dict(), defaults(sys), dvs, ps) - - isempty(missing_dvs) || throw(MissingVariablesError(collect(missing_dvs))) + op = build_operating_point(sys, varmap) + binds = bindings(sys) + no_override_merge_except_missing!(op, binds) + obs, _ = unhack_observed(observed(sys), equations(sys)) + add_observed_equations!(op, obs) - return varmap_to_vars(op, dvs; kwargs...) + return varmap_to_vars(op, unknowns(sys); kwargs...) end """ @@ -1832,19 +1826,16 @@ keyword arguments are forwarded to [`MTKParameters`](@ref) for split systems and [`varmap_to_vars`](@ref) for non-split systems. """ function get_p(sys::AbstractSystem, varmap; split = is_split(sys), kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - op = to_varmap(varmap, dvs) - add_observed!(sys, op) - add_parameter_dependencies!(sys, op) - _, missing_ps = build_operating_point!( - sys, op, Dict(), Dict(), defaults(sys), dvs, ps) - - isempty(missing_ps) || throw(MissingParametersError(collect(missing_ps))) + op = build_operating_point(sys, varmap) + binds = bindings(sys) + no_override_merge_except_missing!(op, binds) + add_initials!(sys, op) + obs, _ = unhack_observed(observed(sys), equations(sys)) + add_observed_equations!(op, obs) if split MTKParameters(sys, op; kwargs...) else - varmap_to_vars(op, ps; kwargs...) + varmap_to_vars(op, parameters(sys; initial_parameters = true); kwargs...) end end diff --git a/src/systems/system.jl b/lib/ModelingToolkitBase/src/systems/system.jl similarity index 78% rename from src/systems/system.jl rename to lib/ModelingToolkitBase/src/systems/system.jl index 12f5d1d250..83eff004fe 100644 --- a/src/systems/system.jl +++ b/lib/ModelingToolkitBase/src/systems/system.jl @@ -3,7 +3,7 @@ struct Schedule """ Mapping of `Differential`s of variables to corresponding derivative expressions. """ - dummy_sub::Dict{Any, Any} + dummy_sub::Dict{SymbolicT, SymbolicT} end const MetadataT = Base.ImmutableDict{DataType, Any} @@ -46,7 +46,7 @@ struct System <: IntermediateDeprecationSystem this noise matrix is diagonal. Diagonal noise can be specified by providing an `N` length vector. If this field is `nothing`, the system does not have noise. """ - noise_eqs::Union{Nothing, AbstractVector, AbstractMatrix} + noise_eqs::Union{Nothing, Vector{SymbolicT}, Matrix{SymbolicT}} """ Jumps associated with the system. Each jump can be a `VariableRateJump`, `ConstantRateJump` or `MassActionJump`. See `JumpProcesses.jl` for more information. @@ -63,7 +63,7 @@ struct System <: IntermediateDeprecationSystem loss of an optimization problem. Scalar loss values must also be provided as a single- element vector. """ - costs::Vector{<:Union{BasicSymbolic, Real}} + costs::Vector{SymbolicT} """ A function which combines costs into a scalar value. This should take two arguments, the `costs` of this system and the consolidated costs of all subsystems in the order @@ -76,25 +76,25 @@ struct System <: IntermediateDeprecationSystem The variables being solved for by this system. For example, in a differential equation system, this contains the dependent variables. """ - unknowns::Vector + unknowns::Vector{SymbolicT} """ The parameters of the system. Parameters can either be variables that parameterize the problem being solved for (e.g. the spring constant of a mass-spring system) or additional unknowns not part of the main dynamics of the system (e.g. discrete/clocked variables in a hybrid ODE). """ - ps::Vector + ps::Vector{SymbolicT} """ The brownian variables of the system, created via `@brownians`. Each brownian variable represents an independent noise. A system with brownians cannot be simulated directly. It needs to be compiled using `mtkcompile` into `noise_eqs`. """ - brownians::Vector + brownians::Vector{SymbolicT} """ The independent variable for a time-dependent system, or `nothing` for a time-independent system. """ - iv::Union{Nothing, BasicSymbolic{Real}} + iv::Union{Nothing, SymbolicT} """ Equations that compute variables of a system that have been eliminated from the set of unknowns by `mtkcompile`. More generally, this contains all variables that can be @@ -108,16 +108,10 @@ struct System <: IntermediateDeprecationSystem observed::Vector{Equation} """ $INTERNAL_FIELD_WARNING - All the explicit equations relating parameters. Equations here only contain parameters - and are in the same format as `observed`. - """ - parameter_dependencies::Vector{Equation} - """ - $INTERNAL_FIELD_WARNING A mapping from the name of a variable to the actual symbolic variable in the system. This is used to enable `getproperty` syntax to access variables of a system. """ - var_to_name::Dict{Symbol, Any} + var_to_name::Dict{Symbol, SymbolicT} """ The name of the system. """ @@ -127,16 +121,21 @@ struct System <: IntermediateDeprecationSystem """ description::String """ - Default values that variables (unknowns/observables/parameters) should take when - constructing a numerical problem from the system. These values can be overridden - by initial values provided to the problem constructor. Defaults of parent systems - take priority over those in child systems. + Binding relations for variables/parameters. The bound variable (key) is completely + determined by the binding (value). Providing an initial condition for a bound variable + is an error. Bindings for variables (ones created via `@variables` and `@discretes`) + are treated as initial conditions. + """ + bindings::ROSymmapT """ - defaults::Dict + Initial conditions for variables (unknowns/observables/parameters) which can be + changed/overridden. When constructing a numerical problem from the system. + """ + initial_conditions::SymmapT """ Guess values for variables of a system that are solved for during initialization. """ - guesses::Dict + guesses::SymmapT """ A list of subsystems of this system. Used for hierarchically building models. """ @@ -167,7 +166,7 @@ struct System <: IntermediateDeprecationSystem associated error message. By default these assertions cause the generated code to output `NaN`s if violated, but can be made to error using `debug_system`. """ - assertions::Dict{BasicSymbolic, String} + assertions::Dict{SymbolicT, String} """ The metadata associated with this system, as a `Base.ImmutableDict`. This follows the same interface as SymbolicUtils.jl. Metadata can be queried and updated using @@ -193,12 +192,12 @@ struct System <: IntermediateDeprecationSystem $INTERNAL_FIELD_WARNING The list of input variables of the system. """ - inputs::OrderedSet{BasicSymbolic} + inputs::OrderedSet{SymbolicT} """ $INTERNAL_FIELD_WARNING The list of output variables of the system. """ - outputs::OrderedSet{BasicSymbolic} + outputs::OrderedSet{SymbolicT} """ The `TearingState` of the system post-simplification with `mtkcompile`. """ @@ -223,6 +222,12 @@ struct System <: IntermediateDeprecationSystem index_cache::Union{Nothing, IndexCache} """ $INTERNAL_FIELD_WARNING + Contains the dependency graph of bound parameters to avoid excessive duplicated work + during code generation. + """ + parameter_bindings_graph::Union{Nothing, ParameterBindingsGraph} + """ + $INTERNAL_FIELD_WARNING Connections that should be ignored because they were removed by an analysis point transformation. The first element of the tuple contains all such "standard" connections (ones between connector systems) and the second contains all such causal variable @@ -262,13 +267,14 @@ struct System <: IntermediateDeprecationSystem function System( tag, eqs, noise_eqs, jumps, constraints, costs, consolidate, unknowns, ps, - brownians, iv, observed, parameter_dependencies, var_to_name, name, description, - defaults, guesses, systems, initialization_eqs, continuous_events, discrete_events, - connector_type, assertions = Dict{BasicSymbolic, String}(), + brownians, iv, observed, var_to_name, name, description, bindings, + initial_conditions, guesses, systems, initialization_eqs, continuous_events, + discrete_events, connector_type, assertions = Dict{SymbolicT, String}(), metadata = MetadataT(), gui_metadata = nothing, is_dde = false, tstops = [], - inputs = Set{BasicSymbolic}(), outputs = Set{BasicSymbolic}(), + inputs = Set{SymbolicT}(), outputs = Set{SymbolicT}(), tearing_state = nothing, namespacing = true, - complete = false, index_cache = nothing, ignored_connections = nothing, + complete = false, index_cache = nothing, parameter_bindings_graph = nothing, + ignored_connections = nothing, preface = nothing, parent = nothing, initializesystem = nothing, is_initializesystem = false, is_discrete = false, isscheduled = false, schedule = nothing; checks::Union{Bool, Int} = true) @@ -278,15 +284,23 @@ struct System <: IntermediateDeprecationSystem variable $iv. """)) end - jumps = Vector{JumpType}(jumps) - if (checks == true || (checks & CheckComponents) > 0) && iv !== nothing - check_independent_variables([iv]) + @assert iv === nothing || symtype(iv) === Real + if (checks isa Bool && checks === true || checks isa Int && (checks & CheckComponents) > 0) && iv !== nothing + check_independent_variables((iv,)) check_variables(unknowns, iv) check_parameters(ps, iv) check_equations(eqs, iv) - if noise_eqs !== nothing && size(noise_eqs, 1) != length(eqs) - throw(IllFormedNoiseEquationsError(size(noise_eqs, 1), length(eqs))) + Neq = length(eqs) + if noise_eqs isa Matrix{SymbolicT} + N1 = size(noise_eqs, 1) + elseif noise_eqs isa Vector{SymbolicT} + N1 = length(noise_eqs) + elseif noise_eqs === nothing + N1 = Neq + else + error() end + N1 == Neq || throw(IllFormedNoiseEquationsError(N1, Neq)) check_equations(equations(continuous_events), iv) check_subsystems(systems) end @@ -304,20 +318,43 @@ struct System <: IntermediateDeprecationSystem end new(tag, eqs, noise_eqs, jumps, constraints, costs, consolidate, unknowns, ps, brownians, iv, - observed, parameter_dependencies, var_to_name, name, description, defaults, + observed, var_to_name, name, description, bindings, initial_conditions, guesses, systems, initialization_eqs, continuous_events, discrete_events, connector_type, assertions, metadata, gui_metadata, is_dde, tstops, inputs, outputs, tearing_state, namespacing, - complete, index_cache, ignored_connections, + complete, index_cache, parameter_bindings_graph, ignored_connections, preface, parent, initializesystem, is_initializesystem, is_discrete, isscheduled, schedule) end end +_sum_costs(costs::Vector{SymbolicT}) = SU.add_worker(VartypeT, costs) +_sum_costs(costs::Vector{Num}) = SU.add_worker(VartypeT, costs) +# `reduce` instead of `sum` because the rrule for `sum` doesn't +# handle the `init` kwarg. +_sum_costs(costs::Vector) = reduce(+, costs; init = 0.0) + function default_consolidate(costs, subcosts) - # `reduce` instead of `sum` because the rrule for `sum` doesn't - # handle the `init` kwarg. - return reduce(+, costs; init = 0.0) + reduce(+, subcosts; init = 0.0) + return _sum_costs(costs) + _sum_costs(subcosts) +end + +unwrap_vars(x) = unwrap_vars(collect(x)) +unwrap_vars(vars::AbstractArray{SymbolicT}) = vars +function unwrap_vars(vars::AbstractArray) + result = similar(vars, SymbolicT) + for i in eachindex(vars) + result[i] = SU.Const{VartypeT}(vars[i]) + end + return result +end + +defsdict(x::SymmapT) = x +function defsdict(x::Union{AbstractDict, AbstractArray{<:Pair}}) + result = SymmapT() + for (k, v) in x + result[unwrap(k)] = SU.Const{VartypeT}(v) + end + return result end """ @@ -330,80 +367,105 @@ for time-independent systems, unknowns `dvs`, parameters `ps` and brownian varia ## Keyword Arguments - `discover_from_metadata`: Whether to parse metadata of unknowns and parameters of the - system to obtain defaults and/or guesses. + system to obtain bindings, initial conditions and/or guesses. - `checks`: Whether to perform sanity checks on the passed values. All other keyword arguments are named identically to the corresponding fields in [`System`](@ref). """ -function System(eqs::Vector{Equation}, iv, dvs, ps, brownians = []; - constraints = Union{Equation, Inequality}[], noise_eqs = nothing, jumps = [], - costs = BasicSymbolic[], consolidate = default_consolidate, - observed = Equation[], parameter_dependencies = Equation[], defaults = Dict(), - guesses = Dict(), systems = System[], initialization_eqs = Equation[], +function System(eqs::Vector{Equation}, iv, dvs, ps, brownians = SymbolicT[]; + constraints = Union{Equation, Inequality}[], noise_eqs = nothing, jumps = JumpType[], + costs = SymbolicT[], consolidate = default_consolidate, + observed = Equation[], bindings = SymmapT(), initial_conditions = SymmapT(), + guesses = SymmapT(), systems = System[], initialization_eqs = Equation[], continuous_events = SymbolicContinuousCallback[], discrete_events = SymbolicDiscreteCallback[], - connector_type = nothing, assertions = Dict{BasicSymbolic, String}(), + connector_type = nothing, assertions = Dict{SymbolicT, String}(), metadata = MetadataT(), gui_metadata = nothing, - is_dde = nothing, tstops = [], inputs = OrderedSet{BasicSymbolic}(), - outputs = OrderedSet{BasicSymbolic}(), tearing_state = nothing, + is_dde = nothing, tstops = [], inputs = OrderedSet{SymbolicT}(), + outputs = OrderedSet{SymbolicT}(), tearing_state = nothing, ignored_connections = nothing, parent = nothing, description = "", name = nothing, discover_from_metadata = true, initializesystem = nothing, is_initializesystem = false, is_discrete = false, - preface = [], checks = true) + preface = [], checks = true, __legacy_defaults__ = nothing) name === nothing && throw(NoNameError()) - if !isempty(parameter_dependencies) - @warn """ - The `parameter_dependencies` keyword argument is deprecated. Please provide all - such equations as part of the normal equations of the system. - """ - eqs = Equation[eqs; parameter_dependencies] - end - iv = unwrap(iv) - ps = unwrap.(ps) - dvs = unwrap.(dvs) - filter!(!Base.Fix2(isdelay, iv), dvs) - brownians = unwrap.(brownians) + if __legacy_defaults__ !== nothing + Base.depwarn(""" + The `@mtkmodel` macro is deprecated. Please use the functional form with \ + `@components` instead. + """, :mtkmodel) + initial_conditions = __legacy_defaults__ + end - if !(eqs isa AbstractArray) - eqs = [eqs] + if !(systems isa Vector{System}) + systems = Vector{System}(systems) + end + if !(eqs isa Vector{Equation}) + eqs = Equation[eqs] end + eqs = eqs::Vector{Equation} - if noise_eqs !== nothing - noise_eqs = unwrap.(noise_eqs) + iv = unwrap(iv) + ps = vec(unwrap_vars(ps)) + dvs = vec(unwrap_vars(dvs)) + if iv !== nothing + filter!(!Base.Fix2(isdelay, iv), dvs) end + brownians = unwrap_vars(brownians) - costs = unwrap.(costs) - if isempty(costs) - costs = Union{BasicSymbolic, Real}[] + if noise_eqs !== nothing + noise_eqs = unwrap_vars(noise_eqs) end - defaults = anydict(defaults) - guesses = anydict(guesses) + costs = vec(unwrap_vars(costs)) - inputs = unwrap.(inputs) - outputs = unwrap.(outputs) - inputs = OrderedSet{BasicSymbolic}(inputs) - outputs = OrderedSet{BasicSymbolic}(outputs) + if !(inputs isa OrderedSet{SymbolicT}) + inputs = unwrap.(inputs) + inputs = OrderedSet{SymbolicT}(inputs) + end + if !(outputs isa OrderedSet{SymbolicT}) + outputs = unwrap.(outputs) + outputs = OrderedSet{SymbolicT}(outputs) + end for subsys in systems - for var in ModelingToolkit.inputs(subsys) + for var in get_inputs(subsys) push!(inputs, renamespace(subsys, var)) end - for var in ModelingToolkit.outputs(subsys) + for var in get_outputs(subsys) push!(outputs, renamespace(subsys, var)) end end - var_to_name = anydict() - - let defaults = discover_from_metadata ? defaults : Dict(), - guesses = discover_from_metadata ? guesses : Dict(), - inputs = discover_from_metadata ? inputs : Set(), - outputs = discover_from_metadata ? outputs : Set() - - process_variables!(var_to_name, defaults, guesses, dvs) - process_variables!(var_to_name, defaults, guesses, ps) - process_variables!(var_to_name, defaults, guesses, [eq.lhs for eq in observed]) - process_variables!(var_to_name, defaults, guesses, [eq.rhs for eq in observed]) + var_to_name = Dict{Symbol, SymbolicT}() + + bindings = defsdict(bindings) + initial_conditions = defsdict(initial_conditions) + guesses = defsdict(guesses) + all_dvs = as_atomic_array_set(dvs) + if iv === nothing + for k in keys(bindings) + k in all_dvs || continue + throw(ArgumentError(""" + Bindings for variables are enforced during initialization. Since \ + time-independent systems only perform parameter initialization, \ + bindings for variables in such systems are invalid. $k was found to have \ + a binding in the system $name. + """)) + end + end + let initial_conditions = discover_from_metadata ? initial_conditions : SymmapT(), + bindings = discover_from_metadata ? bindings : SymmapT(), + guesses = discover_from_metadata ? guesses : SymmapT(), + inputs = discover_from_metadata ? inputs : OrderedSet{SymbolicT}(), + outputs = discover_from_metadata ? outputs : OrderedSet{SymbolicT}() + + process_variables!(var_to_name, initial_conditions, bindings, guesses, dvs) + process_variables!(var_to_name, initial_conditions, bindings, guesses, ps) + buffer = SymbolicT[] + for eq in observed + push!(buffer, eq.lhs) + push!(buffer, eq.rhs) + end + process_variables!(var_to_name, initial_conditions, bindings, guesses, buffer) for var in dvs if isinput(var) @@ -413,15 +475,26 @@ function System(eqs::Vector{Equation}, iv, dvs, ps, brownians = []; end end end - filter!(!(isnothing ∘ last), defaults) - filter!(!(isnothing ∘ last), guesses) - defaults = anydict([unwrap(k) => unwrap(v) for (k, v) in defaults]) - guesses = anydict([unwrap(k) => unwrap(v) for (k, v) in guesses]) + filter!(!(Base.Fix1(===, COMMON_NOTHING) ∘ last), initial_conditions) + filter!(!(Base.Fix1(===, COMMON_NOTHING) ∘ last), bindings) + filter!(!(Base.Fix1(===, COMMON_NOTHING) ∘ last), guesses) + + if iv === nothing + filter!(bindings) do kvp + k = kvp[1] + if k in all_dvs + initial_conditions[k] = kvp[2] + return false + end + return true + end + end - sysnames = nameof.(systems) - unique_sysnames = Set(sysnames) - if length(unique_sysnames) != length(sysnames) - throw(NonUniqueSubsystemsError(sysnames, unique_sysnames)) + check_bindings(ps, bindings) + bindings = ROSymmapT(bindings) + + if !allunique(map(nameof, systems)) + nonunique_subsystems(systems) end continuous_events, discrete_events = create_symbolic_events( @@ -435,7 +508,11 @@ function System(eqs::Vector{Equation}, iv, dvs, ps, brownians = []; is_dde = _check_if_dde(eqs, iv, systems) end - assertions = Dict{BasicSymbolic, String}(unwrap(k) => v for (k, v) in assertions) + _assertions = Dict{SymbolicT, String}() + for (k, v) in assertions + _assertions[unwrap(k)::SymbolicT] = v + end + assertions = _assertions if isempty(metadata) metadata = MetadataT() @@ -449,15 +526,24 @@ function System(eqs::Vector{Equation}, iv, dvs, ps, brownians = []; metadata = meta end metadata = refreshed_metadata(metadata) + jumps = Vector{JumpType}(jumps) System(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), eqs, noise_eqs, jumps, constraints, - costs, consolidate, dvs, ps, brownians, iv, observed, Equation[], - var_to_name, name, description, defaults, guesses, systems, initialization_eqs, + costs, consolidate, dvs, ps, brownians, iv, observed, + var_to_name, name, description, bindings, initial_conditions, guesses, systems, initialization_eqs, continuous_events, discrete_events, connector_type, assertions, metadata, gui_metadata, is_dde, tstops, inputs, outputs, tearing_state, true, false, - nothing, ignored_connections, preface, parent, + nothing, nothing, ignored_connections, preface, parent, initializesystem, is_initializesystem, is_discrete; checks) end +@noinline function nonunique_subsystems(systems) + sysnames = nameof.(systems) + unique_sysnames = Set(sysnames) + throw(NonUniqueSubsystemsError(sysnames, unique_sysnames)) +end + +SymbolicIndexingInterface.getname(x::System) = nameof(x) + """ $(TYPEDSIGNATURES) @@ -478,16 +564,13 @@ other symbolic expressions passed to the system. function System(eqs::Vector{Equation}, iv; kwargs...) iv === nothing && return System(eqs; kwargs...) - diffvars = OrderedSet() - othervars = OrderedSet() - ps = Set() + diffvars = OrderedSet{SymbolicT}() + othervars = OrderedSet{SymbolicT}() + ps = OrderedSet{SymbolicT}() diffeqs = Equation[] othereqs = Equation[] + iv = unwrap(iv) for eq in eqs - if !(eq.lhs isa Union{Symbolic, Number, AbstractArray}) - push!(othereqs, eq) - continue - end collect_vars!(othervars, ps, eq, iv) if iscall(eq.lhs) && operation(eq.lhs) isa Differential var, _ = var_from_nested_derivative(eq.lhs) @@ -511,7 +594,7 @@ function System(eqs::Vector{Equation}, iv; kwargs...) allunknowns = union(diffvars, othervars) eqs = [diffeqs; othereqs] - brownians = Set() + brownians = Set{SymbolicT}() for x in allunknowns x = unwrap(x) if getvariabletype(x) == BROWNIAN @@ -520,10 +603,6 @@ function System(eqs::Vector{Equation}, iv; kwargs...) end setdiff!(allunknowns, brownians) - for eq in get(kwargs, :parameter_dependencies, Equation[]) - collect_vars!(allunknowns, ps, eq, iv) - end - cstrs = Vector{Union{Equation, Inequality}}(get(kwargs, :constraints, [])) cstrunknowns, cstrps = process_constraint_system(cstrs, allunknowns, ps, iv) union!(allunknowns, cstrunknowns) @@ -550,8 +629,8 @@ function System(eqs::Vector{Equation}, iv; kwargs...) noiseeqs = get(kwargs, :noise_eqs, nothing) if noiseeqs !== nothing # validate noise equations - noisedvs = OrderedSet() - noiseps = OrderedSet() + noisedvs = OrderedSet{SymbolicT}() + noiseps = OrderedSet{SymbolicT}() collect_vars!(noisedvs, noiseps, noiseeqs, iv) for dv in noisedvs dv ∈ allunknowns || @@ -573,14 +652,11 @@ the system. function System(eqs::Vector{Equation}; kwargs...) eqs = collect(eqs) - allunknowns = OrderedSet() - ps = OrderedSet() + allunknowns = OrderedSet{SymbolicT}() + ps = OrderedSet{SymbolicT}() for eq in eqs collect_vars!(allunknowns, ps, eq, nothing) end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - collect_vars!(allunknowns, ps, eq, nothing) - end for ssys in get(kwargs, :systems, System[]) collect_scoped_vars!(allunknowns, ps, ssys, nothing) end @@ -611,15 +687,13 @@ function gather_array_params(ps) 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)) + if symbolic_has_known_size(par) && all(par[i] in ps for i in eachindex(par)) push!(new_ps, par) else push!(new_ps, p) end else - if symbolic_type(p) == ArraySymbolic() && - Symbolics.shape(unwrap(p)) != Symbolics.Unknown() + if symbolic_type(p) == ArraySymbolic() && symbolic_has_known_size(p) for i in eachindex(p) delete!(new_ps, p[i]) end @@ -635,10 +709,10 @@ Process variables in constraints of the (ODE) System. """ function process_constraint_system( constraints::Vector{Union{Equation, Inequality}}, sts, ps, iv; validate = true) - isempty(constraints) && return Set(), Set() + isempty(constraints) && return OrderedSet{SymbolicT}(), OrderedSet{SymbolicT}() - constraintsts = OrderedSet() - constraintps = OrderedSet() + constraintsts = OrderedSet{SymbolicT}() + constraintps = OrderedSet{SymbolicT}() for cons in constraints collect_vars!(constraintsts, constraintps, cons, iv) union!(constraintsts, collect_applied_operators(cons, Differential)) @@ -656,8 +730,8 @@ end Process the costs for the constraint system. """ function process_costs(costs::Vector, sts, ps, iv) - coststs = OrderedSet() - costps = OrderedSet() + coststs = OrderedSet{SymbolicT}() + costps = OrderedSet{SymbolicT}() for cost in costs collect_vars!(coststs, costps, cost, iv) end @@ -680,7 +754,7 @@ function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) for var in auxvars if !iscall(var) - occursin(iv, var) && (var ∈ sts || + SU.query(isequal(iv), var) && (var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) elseif length(arguments(var)) > 1 throw(ArgumentError("Too many arguments for variable $var.")) @@ -692,8 +766,7 @@ function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) operation(var)(iv) ∈ sts || throw(ArgumentError("Variable $var is not a variable of the System. Called variables must be variables of the System.")) - isequal(arg, iv) || isparameter(arg) || arg isa Integer || - arg isa AbstractFloat || + isequal(arg, iv) || isparameter(arg) || isconst(arg) && symtype(arg) <: Real || throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) isparameter(arg) && !isequal(arg, iv) && push!(auxps, arg) @@ -725,19 +798,15 @@ differential equations. """ is_dde(sys::AbstractSystem) = has_is_dde(sys) && get_is_dde(sys) -function _check_if_dde(eqs, iv, subsystems) - is_dde = any(ModelingToolkit.is_dde, subsystems) - if !is_dde - vs = Set() - for eq in eqs - vars!(vs, eq) - is_dde = any(vs) do sym - isdelay(unwrap(sym), iv) - end - is_dde && break - end +_check_if_dde(eqs::Vector{Equation}, iv::Nothing, subsystems::Vector{System}) = false +function _check_if_dde(eqs::Vector{Equation}, iv::SymbolicT, subsystems::Vector{System}) + any(ModelingToolkitBase.is_dde, subsystems) && return true + pred = Base.Fix2(isdelay, iv) + for eq in eqs + SU.query(pred, eq.lhs) && return true + SU.query(pred, eq.rhs) && return true end - return is_dde + return false end """ @@ -751,9 +820,9 @@ function flatten(sys::System, noeqs = false) isempty(systems) && return sys costs = cost(sys) if _iszero(costs) - costs = Union{Real, BasicSymbolic}[] + costs = SymbolicT[] else - costs = [costs] + costs = SymbolicT[costs] end # We don't include `ignored_connections` in the flattened system, because # connection expansion inherently requires the hierarchy structure. If the system @@ -763,15 +832,16 @@ function flatten(sys::System, noeqs = false) parameters(sys; initial_parameters = true), brownians(sys); jumps = jumps(sys), constraints = constraints(sys), costs = costs, consolidate = default_consolidate, observed = observed(sys), - defaults = defaults(sys), guesses = guesses(sys), + bindings = bindings(sys), initial_conditions = initial_conditions(sys), + guesses = guesses(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys), assertions = assertions(sys), is_dde = is_dde(sys), tstops = symbolic_tstops(sys), initialization_eqs = initialization_equations(sys), inputs = inputs(sys), outputs = outputs(sys), - # without this, any defaults/guesses obtained from metadata that were - # later removed by the user will be re-added. Right now, we just want to - # retain `defaults(sys)` as-is. + # without this, any initial conditions/bindings/guesses obtained from metadata that + # were later removed by the user will be re-added. Right now, we just want to + # retain `initial_conditions(sys)` as-is. discover_from_metadata = false, metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys), description = description(sys), name = nameof(sys)) @@ -879,8 +949,9 @@ Given a time-dependent system `sys` of ODEs, convert it to a time-independent sy nonlinear equations that solve for the steady-state of the unknowns. This is done by replacing every derivative `D(x)` of an unknown `x` with zero. Note that this process does not retain noise equations, brownian terms, jumps or costs associated with `sys`. -All other information such as defaults, guesses, observed and initialization equations -are retained. The independent variable of `sys` becomes a parameter of the returned system. +All other information such as initial conditions, bindings, guesses, observed and +initialization equations are retained. The independent variable of `sys` becomes a +parameter of the returned system. If `sys` is hierarchical (it contains subsystems) this transformation will be applied recursively to all subsystems. The output system will be marked as `complete` if and only @@ -895,20 +966,25 @@ function NonlinearSystem(sys::System) end eqs = equations(sys) obs = observed(sys) + D = Differential(get_iv(sys)) subrules = Dict([D(x) => 0.0 for x in unknowns(sys)]) for var in brownians(sys) subrules[var] = 0.0 end eqs = map(eqs) do eq - fast_substitute(eq, subrules) + substitute(eq, subrules) + end + new_ps = [parameters(sys); get_iv(sys)] + if iscomplete(sys) + append!(new_ps, collect(bound_parameters(sys))) end - nsys = System(eqs, unknowns(sys), [parameters(sys); get_iv(sys)]; - defaults = merge(defaults(sys), Dict(get_iv(sys) => Inf)), guesses = guesses(sys), + nsys = System(eqs, unknowns(sys), new_ps; + bindings = merge(bindings(sys), Dict(get_iv(sys) => Inf)), + initial_conditions = initial_conditions(sys), guesses = guesses(sys), initialization_eqs = initialization_equations(sys), name = nameof(sys), observed = obs, systems = map(NonlinearSystem, get_systems(sys))) if iscomplete(sys) nsys = complete(nsys; split = is_split(sys)) - @set! nsys.parameter_dependencies = get_parameter_dependencies(sys) end return nsys end @@ -924,7 +1000,7 @@ Construct a time-independent [`System`](@ref) for optimizing the specified scala The system will have no equations. Unknowns and parameters of the system are inferred from the cost and other values (such as -defaults) passed to it. +initial conditions) passed to it. All keyword arguments are the same as those of the [`System`](@ref) constructor. """ @@ -952,7 +1028,7 @@ defaults to summing the specified cost and that of all subsystems. The system wi equations. Unknowns and parameters of the system are inferred from the cost and other values (such as -defaults) passed to it. +initial conditions) passed to it. All keyword arguments are the same as those of the [`System`](@ref) constructor. """ @@ -1001,7 +1077,6 @@ function JumpSystem(jumps, iv, dvs, ps; kwargs...) return System(eqs, iv, dvs, ps; jumps, kwargs...) end -# explicitly write the docstring to avoid mentioning `parameter_dependencies`. """ SDESystem(eqs::Vector{Equation}, noise, iv; is_scalar_noise = false, kwargs...) @@ -1026,15 +1101,14 @@ will automatically perform this conversion. All keyword arguments are the same as those of the [`System`](@ref) constructor. """ function SDESystem(eqs::Vector{Equation}, noise, iv; is_scalar_noise = false, - parameter_dependencies = Equation[], kwargs...) + kwargs...) if is_scalar_noise if !(noise isa Vector) throw(ArgumentError("Expected noise to be a vector if `is_scalar_noise`")) end noise = repeat(reshape(noise, (1, :)), length(eqs)) end - sys = System(eqs, iv; noise_eqs = noise, kwargs...) - @set sys.parameter_dependencies = parameter_dependencies + return System(eqs, iv; noise_eqs = noise, kwargs...) end """ @@ -1046,15 +1120,14 @@ Identical to the 3-argument `SDESystem` constructor, but uses the explicitly pro """ function SDESystem( eqs::Vector{Equation}, noise, iv, dvs, ps; is_scalar_noise = false, - parameter_dependencies = Equation[], kwargs...) + kwargs...) if is_scalar_noise if !(noise isa Vector) throw(ArgumentError("Expected noise to be a vector if `is_scalar_noise`")) end noise = repeat(reshape(noise, (1, :)), length(eqs)) end - sys = System(eqs, iv, dvs, ps; noise_eqs = noise, kwargs...) - @set sys.parameter_dependencies = parameter_dependencies + return System(eqs, iv, dvs, ps; noise_eqs = noise, kwargs...) end """ @@ -1135,8 +1208,8 @@ $(err.devents) end function supports_initialization(sys::System) - return isempty(jumps(sys)) && _iszero(cost(sys)) && - isempty(constraints(sys)) + return isempty(get_systems(sys)) && isempty(jumps(sys)) && + isempty(get_costs(sys)) && isempty(get_constraints(sys)) end safe_eachrow(::Nothing) = nothing @@ -1150,7 +1223,7 @@ safe_issetequal(x, y) = issetequal(x, y) """ $(TYPEDSIGNATURES) -Check if two systems are about equal, to the extent that ModelingToolkit.jl supports. Note +Check if two systems are about equal, to the extent that ModelingToolkitBase.jl supports. Note that if this returns `true`, the systems are not guaranteed to be exactly equivalent (unless `sysa === sysb`) but are highly likely to represent a similar mathematical problem. If this returns `false`, the systems are very likely to be different. @@ -1170,9 +1243,9 @@ function Base.isapprox(sysa::System, sysb::System) issetequal(get_ps(sysa), get_ps(sysb)) && issetequal(get_brownians(sysa), get_brownians(sysb)) && issetequal(get_observed(sysa), get_observed(sysb)) && - issetequal(get_parameter_dependencies(sysa), get_parameter_dependencies(sysb)) && isequal(get_description(sysa), get_description(sysb)) && - isequal(get_defaults(sysa), get_defaults(sysb)) && + isequal(get_bindings(sysa), get_bindings(sysb)) && + isequal(get_initial_conditions(sysa), get_initial_conditions(sysb)) && isequal(get_guesses(sysa), get_guesses(sysb)) && issetequal(get_initialization_eqs(sysa), get_initialization_eqs(sysb)) && issetequal(get_continuous_events(sysa), get_continuous_events(sysb)) && diff --git a/lib/ModelingToolkitBase/src/systems/systems.jl b/lib/ModelingToolkitBase/src/systems/systems.jl new file mode 100644 index 0000000000..1a2c007dba --- /dev/null +++ b/lib/ModelingToolkitBase/src/systems/systems.jl @@ -0,0 +1,590 @@ +const REPEATED_SIMPLIFICATION_MESSAGE = "Structural simplification cannot be applied to a completed system. Double simplification is not allowed." + +struct RepeatedStructuralSimplificationError <: Exception end + +function Base.showerror(io::IO, e::RepeatedStructuralSimplificationError) + print(io, REPEATED_SIMPLIFICATION_MESSAGE) +end + +function canonicalize_io(iovars, type::String) + iobuffer = OrderedSet{SymbolicT}() + arrsyms = AtomicArrayDict{OrderedSet{SymbolicT}}() + for var in iovars + if Symbolics.isarraysymbolic(var) + if !symbolic_has_known_size(var) + throw(ArgumentError(""" + All $(type)s must have known shape. Found $var with unknown shape. + """)) + end + union!(iobuffer, vec(collect(var)::Array{SymbolicT})::Vector{SymbolicT}) + continue + end + arr, isarr = split_indexed_var(var) + if isarr + tmp = get!(OrderedSet{SymbolicT}, arrsyms, arr) + push!(tmp, var) + end + push!(iobuffer, var) + end + + for (k, v) in arrsyms + if !symbolic_has_known_size(k) + throw(ArgumentError(""" + All $(type)s must have known shape. Found $k with unknown shape. + """)) + end + if type != "output" && length(k) != length(v) + throw(ArgumentError(""" + Part of an array variable cannot be made an $type. The entire array must be \ + an $type. Found $k which has $(length(v)) elements out of $(length(k)) in \ + the $(type)s. Either pass all scalarized elements in sorted order as $(type)s \ + or simply pass $k as an $type. + """)) + end + if type != "output" && !isequal(vec(collect(k))::Vector{SymbolicT}, collect(v)) + throw(ArgumentError(""" + Elements of scalarized array variables must be in sorted order in $(type)s. \ + Either pass all scalarized elements in sorted order as $(type)s \ + or simply pass $k as an $type. + """)) + end + end + + return iobuffer +end + +""" +$(SIGNATURES) + +Compile the given system into a form that ModelingToolkitBase can generate code for. Also +performs order reduction for ODEs and handles simple discrete/implicit-discrete systems. + +# Keyword Arguments + ++ `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. ++ `inputs`, `outputs` and `disturbance_inputs` are passed as keyword arguments.` All inputs` get converted to parameters and are allowed to be unconnected, allowing models where `n_unknowns = n_equations - n_inputs`. +""" +function mtkcompile( + sys::System; additional_passes = (), + inputs = SymbolicT[], outputs = SymbolicT[], + disturbance_inputs = SymbolicT[], + split = true, kwargs...) + isscheduled(sys) && throw(RepeatedStructuralSimplificationError()) + # Canonicalize types of arguments to prevent repeated compilation of inner methods + inputs = canonicalize_io(unwrap_vars(inputs), "input") + outputs = canonicalize_io(unwrap_vars(outputs), "output") + disturbance_inputs = canonicalize_io(unwrap_vars(disturbance_inputs), "disturbance input") + newsys = _mtkcompile(sys; + inputs, outputs, disturbance_inputs, additional_passes, + kwargs...) + for pass in additional_passes + newsys = pass(newsys) + end + @set! newsys.parent = complete(sys; split = false, flatten = false) + newsys = complete(newsys; split) + return newsys +end + +function scalarized_vars(vars) + scal = SymbolicT[] + for var in vars + if !SU.is_array_shape(SU.shape(var)) + push!(scal, var) + continue + end + for i in SU.stable_eachindex(var) + push!(scal, var[i]) + end + end + return scal +end + +function _mtkcompile(sys::AbstractSystem; kwargs...) + # TODO: convert noise_eqs to brownians for simplification + if has_noise_eqs(sys) && get_noise_eqs(sys) !== nothing + sys = noise_to_brownians(sys; names = :αₘₜₖ) + end + if !isempty(jumps(sys)) + return sys + end + if isempty(equations(sys)) && !is_time_dependent(sys) && !_iszero(cost(sys)) + return simplify_optimization_system(sys; kwargs...)::System + end + if !isempty(brownians(sys)) + return simplify_sde_system(sys; kwargs...) + end + return __mtkcompile(sys; kwargs...) +end + +function __mtkcompile(sys::AbstractSystem; + inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + outputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + disturbance_inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + fully_determined = true, + kwargs...) + sys = expand_connections(sys) + sys = discrete_unknowns_to_parameters(sys) + sys = discover_globalscoped(sys) + flat_dvs = scalarized_vars(unknowns(sys)) + original_vars = Set{SymbolicT}(flat_dvs) + eqs = flatten_equations(equations(sys)) + all_dvs = Set{SymbolicT}() + for eq in eqs + SU.search_variables!(all_dvs, eq; is_atomic = OperatorIsAtomic{Union{Initial, Pre}}()) + end + _all_dvs = Set{SymbolicT}() + for v in all_dvs + if Symbolics.isarraysymbolic(v) + for i in SU.stable_eachindex(v) + push!(_all_dvs, v[i]) + end + else + push!(_all_dvs, v) + end + end + all_dvs = _all_dvs + filter!(all_dvs) do v + v in original_vars || split_indexed_var(v)[1] in original_vars + end + setdiff!(all_dvs, inputs, disturbance_inputs) + if fully_determined === nothing + fully_determined = false + end + if fully_determined && length(eqs) > length(all_dvs) + throw(ExtraEquationsSystemException(""" + The system is unbalanced. There are $(length(eqs)) equations and \ + $(length(all_dvs)) unknowns. + """)) + elseif fully_determined && length(eqs) < length(all_dvs) + throw(ExtraVariablesSystemException(""" + The system is unbalanced. There are $(length(eqs)) equations and \ + $(length(all_dvs)) unknowns. This may also be a high-index DAE, which \ + ModelingToolkitBase.jl cannot handle. Consider using ModelingToolkit.jl to \ + simplify this system. + """)) + end + + flat_dvs = collect(all_dvs) + has_derivatives = any(hasderiv, eqs) + has_shifts = any(hasshift, eqs) + if has_derivatives && has_shifts + throw(HybridSystemNotSupportedException(""" + ModelingToolkitBase.jl cannot simplify systems with both `Shift` and \ + `Differential` operators. + """)) + end + # Nonlinear system + if !has_derivatives && !has_shifts + map!(eq -> Symbolics.COMMON_ZERO ~ (eq.rhs - eq.lhs), eqs, eqs) + @set! sys.eqs = eqs + @set! sys.unknowns = flat_dvs + return sys + end + iv = get_iv(sys)::SymbolicT + total_sub = Dict{SymbolicT, SymbolicT}() + subst = SU.Substituter{false}(total_sub, SU.default_substitute_filter) + if has_derivatives + D = Differential(iv) + + diffeq_idxs = isdiffeq.(eqs) + diffeqs = eqs[diffeq_idxs] + alg_eqs = eqs[.!diffeq_idxs] + for i in eachindex(diffeqs) + eq = diffeqs[i] + var, order = Moshi.Match.@match eq.lhs begin + BSImpl.Term(; f, args) && if f isa Differential end => (args[1], f.order::Int) + end + + @assert order >= 1 + # Simple order reduction + cur = var + for i in 1:order-1 + lhs = D(cur) + rhs = default_toterm(lhs) + push!(diffeqs, lhs ~ rhs) + cur = rhs + end + + diffeqs[i] = D(cur) ~ eq.rhs + end + + obseqs = Equation[] + else + # The "most differentiated" variable in `x(k) ~ x(k - 1) + x(k - 2)` is `x(k)`. + # To find how many times it is "differentiated", find the lowest shift. + lowest_shift = Dict{SymbolicT, Int}() + varsbuf = Set{SymbolicT}() + for eq in eqs + SU.search_variables!(varsbuf, eq.lhs; is_atomic = OperatorIsAtomic{Shift}()) + SU.search_variables!(varsbuf, eq.rhs; is_atomic = OperatorIsAtomic{Shift}()) + end + for v in varsbuf + Moshi.Match.@match v begin + BSImpl.Term(; f, args) && if f isa Shift end => begin + if f.steps > 0 + throw(ArgumentError(""" + Positive shifts are disallowed in unsimplified equations. Found $v. + """)) + end + var = args[1] + lowest_shift[var] = min(get(lowest_shift, var, 0), f.steps) + end + _ => nothing + end + end + + # "differential" equations are ones with shifted variables on the LHS + diffeq_idxs = falses(length(eqs)) + for i in eachindex(eqs) + eq = eqs[i] + if eq.lhs in all_dvs && !haskey(lowest_shift, eq.lhs) + lowest_shift[eq.lhs] = 0 + end + diffeq_idxs[i] = get(lowest_shift, eqs[i].lhs, typemax(Int)) <= 0 + end + # They actually become observed. + obseqs = eqs[diffeq_idxs] + alg_eqs = eqs[.!diffeq_idxs] + diffeqs = Equation[] + for (var, order) in lowest_shift + order = -order + @assert order >= 0 + # A variable shifted back `order` times requires `order` elements of + # history. + for i in 1:order + lhs = Shift(iv, 1)(default_toterm(Shift(iv, -i)(var))) + rhs = default_toterm(Shift(iv, -i+1)(var)) + push!(diffeqs, lhs ~ rhs) + total_sub[Shift(iv, -i)(var)] = default_toterm(Shift(iv, -i)(var)) + end + end + + _obseqs = topsort_equations(obseqs, collect(all_dvs); check = false) + _algeqs = setdiff!(obseqs, _obseqs) + for i in eachindex(_algeqs) + _algeqs[i] = Symbolics.COMMON_ZERO ~ _algeqs[i].rhs - _algeqs[i].lhs + end + obseqs = _obseqs + append!(alg_eqs, _algeqs) + end + + # Substitute derivatives used in RHS of equations + for eq in diffeqs + total_sub[eq.lhs] = eq.rhs + end + for i in eachindex(diffeqs) + eq = diffeqs[i] + diffeqs[i] = eq.lhs ~ fixpoint_sub(eq.rhs, total_sub) + end + diffvars = SymbolicT[] + # Store fixpoint subbed mapping + for eq in diffeqs + total_sub[eq.lhs] = eq.rhs + push!(diffvars, Moshi.Match.@match eq.lhs begin + BSImpl.Term(; args) => args[1] + end) + end + for i in eachindex(alg_eqs) + eq = alg_eqs[i] + alg_eqs[i] = 0 ~ subst(eq.rhs - eq.lhs) + end + for i in eachindex(obseqs) + eq = obseqs[i] + obseqs[i] = eq.lhs ~ subst(eq.rhs) + end + alg_vars = setdiff!(flat_dvs, diffvars, [eq.lhs for eq in obseqs], inputs, disturbance_inputs) + + new_eqs = [diffeqs; alg_eqs] + new_dvs = [diffvars; alg_vars] + new_ps = [get_ps(sys); collect(inputs)] + + for eq in new_eqs + if SU.query(eq.rhs) do v + Moshi.Match.@match v begin + BSImpl.Term(; f) && if f isa Union{Differential, Shift} end => true + _ => false + end + end + throw(ArgumentError(""" + ModelingToolkitBase.jl is unable to simplify such systems. Encountered \ + derivative in RHS of equation $eq. Please consider using ModelingToolkit.jl \ + for such systems. + """)) + end + end + + dummy_sub = Dict{SymbolicT, SymbolicT}() + for eq in diffeqs + dummy_sub[eq.lhs] = eq.rhs + end + var_sccs = [collect(eachindex(new_eqs))] + schedule = Schedule(var_sccs, dummy_sub) + + @set! sys.eqs = new_eqs + @set! sys.observed = obseqs + @set! sys.unknowns = new_dvs + @set! sys.ps = new_ps + @set! sys.inputs = inputs + @set! sys.outputs = outputs + @set! sys.schedule = schedule + @set! sys.isscheduled = true + return sys +end + +function simplify_sde_system(sys::AbstractSystem; kwargs...) + brown_vars = brownians(sys) + @set! sys.brownians = SymbolicT[] + sys = __mtkcompile(sys; kwargs...) + + new_eqs = copy(equations(sys)) + Is = Int[] + Js = Int[] + vals = SymbolicT[] + for (i, eq) in enumerate(new_eqs) + resid = eq.rhs + for (j, bvar) in enumerate(brown_vars) + coeff, resid, islin = Symbolics.linear_expansion(resid, bvar) + if !islin + throw(ArgumentError(""" + Expected brownian variables to appear linearly in equations. Brownian $bvar \ + appears non-linearly in equation $eq. + """)) + end + _iszero(coeff) && continue + + push!(Is, i) + push!(Js, j) + push!(vals, coeff) + end + new_eqs[i] = eq.lhs ~ resid + end + + g = Matrix(sparse(Is, Js, vals)) + @set! sys.eqs = new_eqs + # Fix for https://github.com/SciML/ModelingToolkit.jl/issues/2490 + if size(g, 2) == 1 + # If there's only one brownian variable referenced across all the equations, + # we get a Nx1 matrix of noise equations, which is a special case known as scalar noise + noise_eqs = reshape(g[:, 1], (:, 1)) + is_scalar_noise = true + elseif __num_isdiag_noise(g) + # If each column of the noise matrix has either 0 or 1 non-zero entry, then this is "diagonal noise". + # In this case, the solver just takes a vector column of equations and it interprets that to + # mean that each noise process is independent + noise_eqs = __get_num_diag_noise(g) + is_scalar_noise = false + else + noise_eqs = g + is_scalar_noise = false + end + + dummy_sub = Dict{SymbolicT, SymbolicT}() + for eq in new_eqs + isdiffeq(eq) || continue + dummy_sub[eq.lhs] = eq.rhs + end + var_sccs = [collect(eachindex(new_eqs))] + schedule = Schedule(var_sccs, dummy_sub) + + @set! sys.eqs = new_eqs + @set! sys.noise_eqs = noise_eqs + @set! sys.schedule = schedule + return sys +end + +function simplify_optimization_system(sys::System; split = true, kwargs...) + sys = flatten(sys) + cons = constraints(sys) + econs = Equation[] + icons = Inequality[] + for e in cons + if e isa Equation + push!(econs, e) + elseif e isa Inequality + push!(icons, e) + end + end + irreducible_subs = Dict{SymbolicT, SymbolicT}() + dvs = SymbolicT[] + for var in unknowns(sys) + sh = SU.shape(var)::SU.ShapeVecT + if isempty(sh) + push!(dvs, var) + else + append!(dvs, vec(collect(var)::Array{SymbolicT})::Vector{SymbolicT}) + end + end + for i in eachindex(dvs) + var = dvs[i] + if hasbounds(var) + irreducible_subs[var] = irrvar = setirreducible(var, true)::SymbolicT + dvs[i] = irrvar + end + end + subst = SU.Substituter{false}(irreducible_subs, SU.default_substitute_filter) + for i in eachindex(econs) + econs[i] = subst(econs[i]) + end + nlsys = System(econs, dvs, parameters(sys); name = :___tmp_nlsystem) + snlsys = mtkcompile(nlsys; kwargs..., fully_determined = false)::System + obs = observed(snlsys) + seqs = equations(snlsys) + trueobs, _ = unhack_observed(obs, seqs) + subs = Dict{SymbolicT, SymbolicT}() + for eq in trueobs + subs[eq.lhs] = eq.rhs + end + cons_simplified = Union{Equation, Inequality}[] + for eq in seqs + push!(cons_simplified, fixpoint_sub(eq, subs)) + end + for eq in icons + push!(cons_simplified, fixpoint_sub(eq, subs)) + end + setdiff!(dvs, keys(subs)) + newsts = dvs + @set! sys.constraints = cons_simplified + newobs = copy(observed(sys)) + append!(newobs, obs) + @set! sys.observed = newobs + newcosts = copy(get_costs(sys)) + for i in eachindex(newcosts) + newcosts[i] = fixpoint_sub(newcosts[i], subs) + end + @set! sys.costs = newcosts + @set! sys.unknowns = newsts + return sys +end + +function __num_isdiag_noise(mat) + for i in axes(mat, 1) + nnz = 0 + for j in axes(mat, 2) + nnz += !_iszero(mat[i, j]) + end + if nnz > 1 + return (false) + end + end + true +end + +function __get_num_diag_noise(mat) + map(axes(mat, 1)) do i + for j in axes(mat, 2) + mij = mat[i, j] + if !_iszero(mij) + return mij + end + end + 0 + end +end + +""" + $TYPEDSIGNATURES + +Given observed equations `eqs` and a list of variables `unknowns`, construct the incidence +graph for the equations. Also construct a `Vector{Int}` mapping indices of `eqs` to the +index in `unknowns` of the observed variable on the LHS of each equation. Return the +constructed incidence graph and index mapping. +""" +function observed2graph(eqs::Vector{Equation}, unknowns::Vector{SymbolicT})::Tuple{BipartiteGraph{Int, Nothing}, Vector{Int}} + graph = BipartiteGraph(length(eqs), length(unknowns)) + v2j = Dict{SymbolicT, Int}(unknowns .=> 1:length(unknowns)) + + # `assigns: eq -> var`, `eq` defines `var` + assigns = similar(eqs, Int) + vars = Set{SymbolicT}() + for (i, eq) in enumerate(eqs) + lhs_j = get(v2j, eq.lhs, nothing) + lhs_j === nothing && + throw(ArgumentError("The lhs $(eq.lhs) of $eq, doesn't appear in unknowns.")) + assigns[i] = lhs_j + empty!(vars) + SU.search_variables!(vars, eq.rhs; is_atomic = OperatorIsAtomic{SU.Operator}()) + for v in vars + j = get(v2j, v, nothing) + if j isa Int + add_edge!(graph, i, j) + end + end + end + + return graph, assigns +end + +""" + $(TYPEDSIGNATURES) + +Use Kahn's algorithm to topologically sort observed equations. + +Example: +```julia +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 + z ~ 2 + y ~ 2z + k + ]; + +julia> ModelingToolkit.topsort_equations(eqs, [x, y, z, k]) +3-element Vector{Equation}: + Equation(z(t), 2) + Equation(y(t), k(t) + 2z(t)) + Equation(x(t), y(t) + z(t)) +``` +""" +function topsort_equations(eqs::Vector{Equation}, unknowns::Vector{SymbolicT}; check = true) + graph, assigns = observed2graph(eqs, unknowns) + neqs = length(eqs) + degrees = zeros(Int, neqs) + + for 𝑠eq in 1:length(eqs) + var = assigns[𝑠eq] + for 𝑑eq in 𝑑neighbors(graph, var) + # 𝑠eq => 𝑑eq + degrees[𝑑eq] += 1 + end + end + + q = Queue{Int}(neqs) + for (i, d) in enumerate(degrees) + @static if pkgversion(DataStructures) >= v"0.19" + d == 0 && push!(q, i) + else + d == 0 && enqueue!(q, i) + end + end + + idx = 0 + ordered_eqs = similar(eqs, 0) + sizehint!(ordered_eqs, neqs) + while !isempty(q) + @static if pkgversion(DataStructures) >= v"0.19" + 𝑠eq = popfirst!(q) + else + 𝑠eq = dequeue!(q) + end + idx += 1 + push!(ordered_eqs, eqs[𝑠eq]) + var = assigns[𝑠eq] + for 𝑑eq in 𝑑neighbors(graph, var) + degree = degrees[𝑑eq] = degrees[𝑑eq] - 1 + @static if pkgversion(DataStructures) >= v"0.19" + degree == 0 && push!(q, 𝑑eq) + else + degree == 0 && enqueue!(q, 𝑑eq) + end + end + end + + (check && idx != neqs) && throw(ArgumentError("The equations have at least one cycle.")) + + return ordered_eqs +end + diff --git a/lib/ModelingToolkitBase/src/systems/unit_check.jl b/lib/ModelingToolkitBase/src/systems/unit_check.jl new file mode 100644 index 0000000000..3d0678166b --- /dev/null +++ b/lib/ModelingToolkitBase/src/systems/unit_check.jl @@ -0,0 +1,23 @@ +# Unit checking utilities +check_units(_...) = true +__get_unit_type(_...) = nothing +get_unit(_...) = nothing +validate(_...) = nothing +struct ValidationError <: Exception + message::String +end + +struct PleaseImportDynamicQuantities end +global t::Union{PleaseImportDynamicQuantities, Num} = PleaseImportDynamicQuantities() + +function Base.show(io::IO, ::PleaseImportDynamicQuantities) + __import_dynamic_quantities() +end + +function __import_dynamic_quantities(_...) + error(""" + Please import DynamicQuantites.jl to use this `t` and `D`. + """) +end +global D::Union{typeof(__import_dynamic_quantities), Differential} = __import_dynamic_quantities + diff --git a/src/utils.jl b/lib/ModelingToolkitBase/src/utils.jl similarity index 62% rename from src/utils.jl rename to lib/ModelingToolkitBase/src/utils.jl index 2a1267aaac..4de348e2ed 100644 --- a/src/utils.jl +++ b/lib/ModelingToolkitBase/src/utils.jl @@ -1,14 +1,3 @@ -""" - 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 """ @@ -20,7 +9,7 @@ function detime_dvs(op) if !iscall(op) op elseif issym(operation(op)) - Sym{Real}(nameof(operation(op))) + SSym(nameof(operation(op)); type = Real, shape = SU.shape(op)) else maketerm(typeof(op), operation(op), detime_dvs.(arguments(op)), metadata(op)) @@ -33,7 +22,7 @@ end Reverse `detime_dvs` for the given `dvs` using independent variable `iv`. """ function retime_dvs(op, dvs, iv) - issym(op) && return Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(op))(iv) + issym(op) && return SSym(nameof(op); type = FnType{Tuple{symtype(iv)}, Real}, shape = SU.ShapeVecT())(iv) iscall(op) ? maketerm(typeof(op), operation(op), retime_dvs.(arguments(op), (dvs,), (iv,)), metadata(op)) : @@ -84,7 +73,7 @@ end function readable_code(expr) expr = Base.remove_linenums!(_readable_code(expr)) rec_remove_macro_linenums!(expr) - JuliaFormatter.format_text(string(expr), JuliaFormatter.SciMLStyle()) + return string(expr) end # System validation enums @@ -117,11 +106,14 @@ 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." + isparameter(iv) || @invokelatest warn_indepvar(iv) end end +@noinline function warn_indepvar(iv::SymbolicT) + @warn "Independent variable $iv should be defined with @independent_variables $iv." +end + function check_parameters(ps, iv) for p in ps isequal(iv, p) && @@ -129,54 +121,50 @@ function check_parameters(ps, iv) end end -function is_delay_var(iv, var) - if Symbolics.isarraysymbolic(var) - return is_delay_var(iv, first(collect(var))) - end - args = nothing - try - args = arguments(var) - catch - return false +function is_delay_var(iv::SymbolicT, var::SymbolicT) + Moshi.Match.@match var begin + BSImpl.Term(; f, args) => begin + length(args) > 1 && return false + arg = args[1] + isequal(arg, iv) && return false + return symtype(arg) <: Real + end + _ => false end - length(args) > 1 && return false - isequal(first(args), iv) && return false - delay = iv - first(args) - delay isa Integer || - delay isa AbstractFloat || - (delay isa Num && isreal(value(delay))) end function check_variables(dvs, iv) for dv in dvs isequal(iv, dv) && throw(ArgumentError("Independent variable $iv not allowed in dependent variables.")) - (is_delay_var(iv, dv) || occursin(iv, dv)) || + (is_delay_var(iv, dv) || SU.query(isequal(iv), dv)) || throw(ArgumentError("Variable $dv is not a function of independent variable $iv.")) end end -function check_lhs(eq::Equation, op, dvs::Set) +function check_lhs(eq::Equation, ::Type{Differential}, dvs::Set) v = unwrap(eq.lhs) _iszero(v) && return - (operation(v) isa op && only(arguments(v)) in dvs) && return + op = operation(v) + op isa Differential && isone(op.order) && only(arguments(v)) in dvs && return error("$v is not a valid LHS. Please run mtkcompile before simulation.") end -check_lhs(eqs, op, dvs::Set) = +function check_lhs(eqs::Vector{Equation}, ::Type{Differential}, dvs::Set) for eq in eqs - check_lhs(eq, op, dvs) + check_lhs(eq, Differential, dvs) end +end """ collect_ivs(eqs, op = Differential) Get all the independent variables with respect to which differentials (`op`) are taken. """ -function collect_ivs(eqs, op = Differential) - vars = Set() - ivs = Set() +function collect_ivs(eqs, ::Type{op} = Differential) where {op} + vars = Set{SymbolicT}() + ivs = Set{SymbolicT}() for eq in eqs - vars!(vars, eq; op = op) + SU.search_variables!(vars, eq; is_atomic = OperatorIsAtomic{op}()) for v in vars if isoperator(v, op) collect_ivs_from_nested_operator!(ivs, v, op) @@ -187,20 +175,35 @@ function collect_ivs(eqs, op = Differential) return ivs end +struct IndepvarCheckPredicate + iv::SymbolicT +end + +function (icp::IndepvarCheckPredicate)(ex::SymbolicT) + Moshi.Match.@match ex begin + BSImpl.Term(; f) && if f isa Differential end => begin + f = f::Differential + isequal(f.x, icp.iv) || throw_multiple_iv(icp.iv, f.x) + return false + end + _ => false + end +end + +@noinline function throw_multiple_iv(iv, newiv) + throw(ArgumentError("Differential w.r.t. variable ($newiv) other than the independent variable ($iv) are not allowed.")) +end + """ check_equations(eqs, iv) Assert that equations are well-formed when building ODE, i.e., only containing a single independent variable. """ -function check_equations(eqs, iv) - ivs = collect_ivs(eqs) - display = collect(ivs) - length(ivs) <= 1 || - throw(ArgumentError("Differential w.r.t. multiple variables $display are not allowed.")) - if length(ivs) == 1 - single_iv = pop!(ivs) - isequal(single_iv, iv) || - throw(ArgumentError("Differential w.r.t. variable ($single_iv) other than the independent variable ($iv) are not allowed.")) +function check_equations(eqs::Vector{Equation}, iv::SymbolicT) + icp = IndepvarCheckPredicate(iv) + for eq in eqs + SU.query(icp, eq.lhs) + SU.query(icp, eq.rhs) end end @@ -211,10 +214,12 @@ Assert that the subsystems have the appropriate namespacing behavior. """ function check_subsystems(systems) idxs = findall(!does_namespacing, systems) - if !isempty(idxs) - names = join(" " .* string.(nameof.(systems[idxs])), "\n") - throw(ArgumentError("All subsystems must have namespacing enabled. The following subsystems do not perform namespacing:\n$(names)")) - end + isempty(idxs) || throw_bad_namespacing(systems, idxs) +end + +@noinline function throw_bad_namespacing(systems, idxs) + names = join(" " .* string.(nameof.(systems[idxs])), "\n") + throw(ArgumentError("All subsystems must have namespacing enabled. The following subsystems do not perform namespacing:\n$(names)")) end """ @@ -250,6 +255,121 @@ function iv_from_nested_derivative(x, op = Differential) end end +""" + $TYPEDSIGNATURES + +Check the validity of `bindings` given the list of parameters `ps`. This method assumes +that there are no discrete values in `ps`. +""" +function check_bindings(ps::Vector{SymbolicT}, bindings::SymmapT) + atomic_ps = AtomicArraySet() + for p in ps + push_as_atomic_array!(atomic_ps, p) + end + check_bindings(atomic_ps, bindings) +end + +function check_bindings_is_atomic(x::SymbolicT) + SU.default_is_atomic(x) && Moshi.Match.@match x begin + BSImpl.Term(; f) && if f isa Operator end => f isa Initial + BSImpl.Term(; f) && if f === getindex end => false + _ => true + end +end + +""" + $TYPEDSIGNATURES + +Check if `bindings` are valid, given a list of parameters `atomic_ps`. Assumes no values in +`atomic_ps` are discretes. +""" +function check_bindings(atomic_ps::AtomicArraySet{Dict{SymbolicT, Nothing}}, bindings::SymmapT) + varsbuf = Set{SymbolicT}() + for p in atomic_ps + val = get(bindings, p, COMMON_NOTHING) + val === COMMON_NOTHING && continue + if val === COMMON_MISSING + if !is_variable_floatingpoint(p) + throw(ArgumentError(""" + `missing` bindings are only valid for solvable parameters! Non-floating \ + point parameters cannot be solved for, and thus do not accept a binding \ + of `missing`. Found invalid parameter $p of symtype $(symtype(p)). + """)) + end + end + empty!(varsbuf) + SU.search_variables!(varsbuf, val; is_atomic = check_bindings_is_atomic) + setdiff!(varsbuf, atomic_ps) + filter!(x -> getmetadata(x, SymScope, LocalScope()) isa LocalScope, varsbuf) + filter!(!isinitial, varsbuf) + isempty(varsbuf) && continue + + throw(ArgumentError(""" + Bindings for parameters can only be functions of other parameters. For parameter \ + $p, encountered binding $val which contains non-parameter symbolics $varsbuf. If \ + you intended $p to be a discrete variable, pass it as an unknown of the system. + """)) + end +end + +function check_no_parameter_equations_recurse(ex::SymbolicT) + iscall(ex) && !check_bindings_is_atomic(ex) +end + +""" + $(TYPEDSIGNATURES) + +Validate that all equations of the system involve the unknowns/observables. +""" +function check_no_parameter_equations(sys::AbstractSystem) + if !isempty(get_systems(sys)) + throw(ArgumentError("Expected flattened system")) + end + varsbuf = Set{SymbolicT}() + pareqs = Equation[] + allowed_vars = as_atomic_array_set(unknowns(sys)) + foreach(Base.Fix1(push_as_atomic_array!, allowed_vars), observables(sys)) + foreach(Base.Fix1(push_as_atomic_array!, allowed_vars), get_all_discretes_fast(sys)) + for eq in equations(sys) + empty!(varsbuf) + Symbolics.get_variables!(varsbuf, eq, allowed_vars; is_atomic = check_bindings_is_atomic, recurse = check_no_parameter_equations_recurse) + isempty(varsbuf) && push!(pareqs, eq) + end + + if !isempty(pareqs) + error(""" + The equations of a system must involve the unknowns/observables. The following \ + equations were found to have no unknowns/observables: + $(join(string.(pareqs), "\n")) + """) + end +end + +""" + $TYPEDSIGNATURES + +Verify that bound parameters have not been provided initial conditions. Requires the \ +existence of an up-to-data `parameter_bindings_graph`. +""" +function check_no_bound_initial_conditions(sys::AbstractSystem) + bound_ps = (get_parameter_bindings_graph(sys)::ParameterBindingsGraph).bound_ps + ics = initial_conditions(sys) + bound_ics = intersect(bound_ps, keys(ics)) + isempty(bound_ics) || throw(BoundInitialConditionsError(collect(bound_ics))) +end + +struct BoundInitialConditionsError <: Exception + bound_pars::Vector{SymbolicT} +end + +function Base.showerror(io::IO, err::BoundInitialConditionsError) + print(io, """ + Bound parameters cannot have initial conditions. The following bound parameters \ + were found to have initial conditions: + $(join(string.(collect(err.bound_pars)), "\n")) + """) +end + """ $(TYPEDSIGNATURES) @@ -271,57 +391,81 @@ function setdefault(v, val) val === nothing ? v : wrap(setdefaultval(unwrap(v), value(val))) end -function process_variables!(var_to_name, defs, guesses, vars) - collect_defaults!(defs, vars) +function process_variables!(var_to_name::Dict{Symbol, SymbolicT}, initial_conditions::SymmapT, bindings::SymmapT, guesses::SymmapT, vars::Vector{SymbolicT}) + collect_defaults!(initial_conditions, bindings, vars) collect_guesses!(guesses, vars) collect_var_to_name!(var_to_name, vars) return nothing end -function process_variables!(var_to_name, defs, vars) - collect_defaults!(defs, vars) +function process_variables!(var_to_name::Dict{Symbol, SymbolicT}, initial_conditions::SymmapT, bindings::SymmapT, vars::Vector{SymbolicT}) + collect_defaults!(initial_conditions, bindings, vars) collect_var_to_name!(var_to_name, vars) return nothing end -function collect_defaults!(defs, vars) - for v in vars - symbolic_type(v) == NotSymbolic() && continue - if haskey(defs, v) || !hasdefault(unwrap(v)) || (def = getdefault(v)) === nothing - continue +function collect_defaults!(initial_conditions::SymmapT, bindings::SymmapT, v::SymbolicT) + if hasname(v) && occursin(NAMESPACE_SEPARATOR, string(getname(v))) + return + end + Moshi.Match.@match v begin + BSImpl.Const(;) => return + BSImpl.Term(; f, args) && if f === getindex end => begin + collect_defaults!(initial_conditions, bindings, args[1]) + end + BSImpl.Term(; f) && if f isa SymbolicT && SU.is_function_symbolic(f) end => nothing + _ => begin + def = Symbolics.getdefaultval(v, nothing) + def === nothing && return + def = BSImpl.Const{VartypeT}(def) + Moshi.Match.@match def begin + # `get!` here is just shorthand for "if the key doesn't exist, add this + # value". + BSImpl.Const(;) => if def === COMMON_MISSING + get!(bindings, v, def) + else + get!(initial_conditions, v, def) + end + _ => get!(bindings, v, def) + end end - defs[v] = getdefault(v) end - return defs end -function collect_guesses!(guesses, vars) +function collect_defaults!(initial_conditions::SymmapT, bindings::SymmapT, vars::Vector{SymbolicT}) for v in vars - symbolic_type(v) == NotSymbolic() && continue - if haskey(guesses, v) || !hasguess(unwrap(v)) || (def = getguess(v)) === nothing - continue + collect_defaults!(initial_conditions, bindings, v) + end +end + +function collect_guesses!(guesses::SymmapT, v::SymbolicT) + Moshi.Match.@match v begin + BSImpl.Const(;) => return + BSImpl.Term(; f, args) && if f === getindex end => begin + collect_guesses!(guesses, args[1]) + end + _ => begin + def = getguess(v) + def === nothing && return + get!(guesses, v, BSImpl.Const{VartypeT}(def)) end - guesses[v] = getguess(v) end - return guesses +end +function collect_guesses!(guesses::SymmapT, vars::Vector{SymbolicT}) + for v in vars + collect_guesses!(guesses, v) + end end -function collect_var_to_name!(vars, xs) +function collect_var_to_name!(vars::Dict{Symbol, SymbolicT}, xs::Vector{SymbolicT}) for x in xs - symbolic_type(x) == NotSymbolic() && continue - x = unwrap(x) - if hasmetadata(x, Symbolics.GetindexParent) - xarr = getmetadata(x, Symbolics.GetindexParent) - hasname(xarr) || continue - vars[Symbolics.getname(xarr)] = xarr - else - if iscall(x) && operation(x) === getindex - x = arguments(x)[1] - end - x = unwrap(x) - hasname(x) || continue - vars[Symbolics.getname(unwrap(x))] = x + x = Moshi.Match.@match x begin + BSImpl.Const(;) => continue + BSImpl.Term(; f, args) && if f === getindex end => args[1] + _ => x end + hasname(x) || continue + vars[getname(x)] = x end end @@ -329,9 +473,7 @@ end Throw error when difference/derivative operation occurs in the R.H.S. """ @noinline function throw_invalid_operator(opvar, eq, op::Type) - if op === Difference - error("The Difference operator is deprecated, use ShiftIndex instead") - elseif op === Differential + if op === Differential optext = "derivative" end msg = "The $optext variable must be isolated to the left-hand " * @@ -354,11 +496,11 @@ end Check if all the LHS are unique """ function check_operator_variables(eqs, op::T) where {T} - ops = Set() - tmp = Set() + ops = Set{SymbolicT}() + tmp = Set{SymbolicT}() for eq in eqs _check_operator_variables(eq, op) - vars!(tmp, eq.lhs) + SU.search_variables!(tmp, eq.lhs; is_atomic = OperatorIsAtomic{Differential}()) if length(tmp) == 1 x = only(tmp) if op === Differential @@ -381,103 +523,23 @@ function check_operator_variables(eqs, op::T) where {T} end end -isoperator(expr, op) = iscall(expr) && operation(expr) isa op -isoperator(op) = expr -> isoperator(expr, op) +isoperator(::Any, ::Type{T}) where {T} = false +isoperator(ex::Union{Num, Arr, CallAndWrap}, ::Type{op}) where {op} = isoperator(unwrap(ex), op) +function isoperator(expr::SymbolicT, ::Type{op}) where {op <: SU.Operator} + Moshi.Match.@match expr begin + BSImpl.Term(; f) => f isa op + _ => false + end +end +isoperator(::Type{op}) where {op <: SU.Operator} = Base.Fix2(isoperator, op) isdifferential(expr) = isoperator(expr, Differential) isdiffeq(eq) = isdifferential(eq.lhs) || isoperator(eq.lhs, Shift) isvariable(x::Num)::Bool = isvariable(value(x)) -function isvariable(x)::Bool - x isa Symbolic || return false - p = getparent(x, nothing) - p === nothing || (x = p) - hasmetadata(x, VariableSource) -end - -""" - vars(x; op=Differential) - -Return a `Set` containing all variables in `x` that appear in - - - differential equations if `op = Differential` - -Example: - -``` -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) - 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) -function vars(exprs; op = Differential) - if hasmethod(iterate, Tuple{typeof(exprs)}) - foldl((x, y) -> vars!(x, unwrap(y); op = op), exprs; init = Set()) - else - vars!(Set(), unwrap(exprs); op) - end -end -vars(exprs::SparseMatrixCSC; op = Differential) = vars(nonzeros(exprs); op) -vars(eq::Equation; op = Differential) = vars!(Set(), eq; op = op) -function vars!(vars, eq::Equation; op = Differential) - (vars!(vars, eq.lhs; op = op); vars!(vars, eq.rhs; op = op); vars) -end -function vars!(vars, O::AbstractSystem; op = Differential) - for eq in equations(O) - vars!(vars, eq; op) - end - return vars -end -function vars!(vars, O; op = Differential) - if isvariable(O) - if iscall(O) && operation(O) === getindex && iscalledparameter(first(arguments(O))) - O = first(arguments(O)) - end - if iscalledparameter(O) - f = getcalledparameter(O) - push!(vars, f) - for arg in arguments(O) - if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray - for el in arg - vars!(vars, unwrap(el); op) - end - else - vars!(vars, arg; op) - end - end - return vars - end - return push!(vars, O) - end - if symbolic_type(O) == NotSymbolic() && O isa AbstractArray - for arg in O - vars!(vars, unwrap(arg); op) - end - return vars - end - !iscall(O) && return vars - - operation(O) isa op && return push!(vars, O) - - if operation(O) === (getindex) - arr = first(arguments(O)) - iscall(arr) && operation(arr) isa op && return push!(vars, O) - isvariable(arr) && return push!(vars, O) - end - - isvariable(operation(O)) && push!(vars, O) - for arg in arguments(O) - vars!(vars, arg; op = op) - end - - return vars +function isvariable(x) + x isa SymbolicT || return false + hasmetadata(x, VariableSource) || iscall(x) && operation(x) === getindex && isvariable(arguments(x)[1])::Bool end function collect_operator_variables(sys::AbstractSystem, args...) @@ -488,16 +550,16 @@ function collect_operator_variables(eq::Equation, args...) end """ - collect_operator_variables(eqs::AbstractVector{Equation}, op) + collect_operator_variables(eqs::Vector{Equation}, ::Type{op}) where {op} Return a `Set` containing all variables that have Operator `op` applied to them. See also [`collect_differential_variables`](@ref). """ -function collect_operator_variables(eqs::AbstractVector{Equation}, op) - vars = Set() - diffvars = Set() +function collect_operator_variables(eqs::Vector{Equation}, ::Type{op}) where {op} + vars = Set{SymbolicT}() + diffvars = Set{SymbolicT}() for eq in eqs - vars!(vars, eq; op = op) + SU.search_variables!(vars, eq; is_atomic = OperatorIsAtomic{op}()) for v in vars isoperator(v, op) || continue push!(diffvars, arguments(v)[1]) @@ -518,18 +580,15 @@ Return a `Set` with all applied operators in `x`, example: @variables u(t) y(t) D = Differential(t) eq = D(y) ~ u -ModelingToolkit.collect_applied_operators(eq, Differential) == Set([D(y)]) +ModelingToolkitBase.collect_applied_operators(eq, Differential) == Set([D(y)]) ``` The difference compared to `collect_operator_variables` is that `collect_operator_variables` returns the variable without the operator applied. """ -function collect_applied_operators(x, op) - v = vars(x, op = op) - filter(v) do x - issym(x) && return false - iscall(x) && return operation(x) isa op - false - end +function collect_applied_operators(x, ::Type{op}) where {op} + v = Set{SymbolicT}() + SU.search_variables!(v, x; is_atomic = OnlyOperatorIsAtomic{op}()) + return v end """ @@ -540,12 +599,12 @@ Search through equations and parameter dependencies of `sys`, where sys is at a recursively searches through all subsystems of `sys`, increasing the depth if it is not `-1`. A depth of `-1` indicates searching for variables with `GlobalScope`. """ -function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) +function collect_scoped_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, sys::AbstractSystem, iv::Union{SymbolicT, Nothing}; depth = 1, op = Differential) if has_eqs(sys) for eq in equations(sys) eqtype_supports_collect_vars(eq) || continue if eq isa Equation - eq.lhs isa Union{Symbolic, Number} || continue + symtype(eq.lhs) <: Number || continue end collect_vars!(unknowns, parameters, eq, iv; depth, op) end @@ -619,6 +678,24 @@ function Base.showerror(io::IO, err::OperatorIndepvarMismatchError) end end +struct OnlyOperatorIsAtomic{O} end + +function (::OnlyOperatorIsAtomic{O})(ex::SymbolicT) where {O} + Moshi.Match.@match ex begin + BSImpl.Term(; f) && if f isa O end => true + _ => false + end +end + +struct OperatorIsAtomic{O} end + +function (::OperatorIsAtomic{O})(ex::SymbolicT) where {O} + SU.default_is_atomic(ex) && Moshi.Match.@match ex begin + BSImpl.Term(; f) && if f isa Operator end => f isa O + _ => true + end +end + """ $(TYPEDSIGNATURES) @@ -633,26 +710,34 @@ can be checked using `check_scope_depth`. This function should return `nothing`. """ -function collect_vars!(unknowns, parameters, expr, iv; depth = 0, op = Symbolics.Operator) - expr = unwrap(expr) - if issym(expr) - return collect_var!(unknowns, parameters, expr, iv; depth) - end - varsbuf = OrderedSet() - vars!(varsbuf, expr; op) - for var in varsbuf - if iscall(var) && operation(var) isa op - args = arguments(var) - validate_operator(operation(var), args, iv; context = expr) - isempty(args) && continue - push!(varsbuf, args[1]) - else - collect_var!(unknowns, parameters, var, iv; depth) +function collect_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, expr::SymbolicT, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) + Moshi.Match.@match expr begin + BSImpl.Const(;) => return + BSImpl.Sym(;) => return collect_var!(unknowns, parameters, expr, iv; depth) + _ => nothing + end + vars = OrderedSet{SymbolicT}() + SU.search_variables!(vars, expr; is_atomic = OperatorIsAtomic{op}()) + for var in vars + Moshi.Match.@match var begin + BSImpl.Term(; f, args) && if f isa op end => begin + validate_operator(f, args, iv; context = expr) + isempty(args) && continue + push!(vars, args[1]) + end + _ => collect_var!(unknowns, parameters, var, iv; depth) end end return nothing end +function collect_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, expr::AbstractArray, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) + for var in expr + collect_vars!(unknowns, parameters, var, iv; depth, op) + end + return nothing +end + """ $(TYPEDSIGNATURES) @@ -664,7 +749,7 @@ eqtype_supports_collect_vars(eq::Equation) = true eqtype_supports_collect_vars(eq::Inequality) = true eqtype_supports_collect_vars(eq::Pair) = true -function collect_vars!(unknowns, parameters, eq::Union{Equation, Inequality}, iv; +function collect_vars!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, eq::Union{Equation, Inequality}, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) @@ -672,12 +757,22 @@ function collect_vars!(unknowns, parameters, eq::Union{Equation, Inequality}, iv end function collect_vars!( - unknowns, parameters, p::Pair, iv; depth = 0, op = Symbolics.Operator) + unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, ex::Union{Num, Arr, CallAndWrap}, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) + collect_vars!(unknowns, parameters, unwrap(ex), iv; depth, op) +end + +function collect_vars!( + unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, p::Pair, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) collect_vars!(unknowns, parameters, p[1], iv; depth, op) collect_vars!(unknowns, parameters, p[2], iv; depth, op) return nothing end +function collect_vars!( + unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, expr, iv::Union{SymbolicT, Nothing}; depth = 0, op = Symbolics.Operator) + return nothing +end + """ $(TYPEDSIGNATURES) @@ -685,7 +780,7 @@ Identify whether `var` belongs to the current system using `depth` and scoping i Add `var` to `unknowns` or `parameters` appropriately, and search through any expressions in known metadata of `var` using `collect_vars!`. """ -function collect_var!(unknowns, parameters, var, iv; depth = 0) +function collect_var!(unknowns::OrderedSet{SymbolicT}, parameters::OrderedSet{SymbolicT}, var::SymbolicT, iv::Union{SymbolicT, Nothing}; depth = 0) isequal(var, iv) && return nothing if Symbolics.iswrapped(var) error(""" @@ -694,11 +789,11 @@ function collect_var!(unknowns, parameters, var, iv; depth = 0) Encountered a wrapped value in `collect_var!`. This function should only ever \ receive unwrapped symbolic variables. This is likely a bug in the code generating \ an expression passed to `collect_vars!` or `collect_scoped_vars!`. A common cause \ - is using `substitute` or `fast_substitute` with rules where the values are \ + is using `substitute` with rules where the values are \ wrapped symbolic variables. """) end - check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing + check_scope_depth(getmetadata(var, SymScope, LocalScope())::AllScopes, depth) || return nothing var = setmetadata(var, SymScope, LocalScope()) if iscalledparameter(var) callable = getcalledparameter(var) @@ -726,7 +821,7 @@ function check_scope_depth(scope, depth) if scope isa LocalScope return depth == 0 elseif scope isa ParentScope - return depth > 0 && check_scope_depth(scope.parent, depth - 1) + return depth > 0 && check_scope_depth(scope.parent, depth - 1)::Bool elseif scope isa GlobalScope return depth == -1 end @@ -810,7 +905,7 @@ end function _with_unit(f, x, t, args...) x = f(x, args...) - if hasmetadata(x, VariableUnit) && (t isa Symbolic && hasmetadata(t, VariableUnit)) + if hasmetadata(x, VariableUnit) && (t isa SymbolicT && hasmetadata(t, VariableUnit)) xu = getmetadata(x, VariableUnit) tu = getmetadata(t, VariableUnit) x = setmetadata(x, VariableUnit, xu / tu) @@ -820,8 +915,6 @@ end diff2term_with_unit(x, t) = _with_unit(diff2term, x, t) lower_varname_with_unit(var, iv, order) = _with_unit(lower_varname, var, iv, iv, order) -shift2term_with_unit(x, t) = _with_unit(shift2term, x, t) -lower_shift_varname_with_unit(var, iv) = _with_unit(lower_shift_varname, var, iv, iv) """ $(TYPEDSIGNATURES) @@ -840,8 +933,8 @@ end Check if `T` is an appropriate symtype for a symbolic variable representing a floating point number or array of such numbers. """ -function is_floatingpoint_symtype(T::Type) - return T == Real || T == Number || T == Complex || T <: AbstractFloat || +function is_floatingpoint_symtype(T) + return T === Real || T === Number || T === Complex || T <: AbstractFloat || T <: AbstractArray && is_floatingpoint_symtype(eltype(T)) end @@ -909,7 +1002,16 @@ Keyword arguments: `available_vars` will not be searched for in the observed equations. """ function observed_equations_used_by(sys::AbstractSystem, exprs; - involved_vars = vars(exprs; op = Union{Shift, Differential, Initial}), obs = observed(sys), available_vars = []) + involved_vars = nothing, obs = observed(sys), available_vars = Set{SymbolicT}()) + if involved_vars === nothing + involved_vars = Set{SymbolicT}() + SU.search_variables!(involved_vars, exprs; is_atomic = OperatorIsAtomic{Union{Shift, Differential, Initial}}()) + elseif !(involved_vars isa Set{SymbolicT}) + involved_vars = Set{SymbolicT}(involved_vars) + end + if !(available_vars isa Set) + available_vars = Set(available_vars) + end if iscomplete(sys) && obs == observed(sys) cache = getmetadata(sys, MutableCacheKey, nothing) obs_graph_cache = get!(cache, ObservedGraphCacheKey) do @@ -923,10 +1025,6 @@ function observed_equations_used_by(sys::AbstractSystem, exprs; graph = observed_dependency_graph(obs) end - if !(available_vars isa Set) - available_vars = Set(available_vars) - end - obsidxs = BitSet() for sym in involved_vars sym in available_vars && continue @@ -985,7 +1083,7 @@ function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) end any(isequal(expr), vars) && return expr iscall(expr) || return expr - Symbolics.shape(expr) == Symbolics.Unknown() && return expr + symbolic_has_known_size(expr) || return expr haskey(state, expr) && return state[expr] op = operation(expr) args = arguments(expr) @@ -994,19 +1092,18 @@ function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) # OR # none of `vars` are involved in `expr` if op === getindex && (issym(args[1]) || !iscalledparameter(args[1])) || - (vs = ModelingToolkit.vars(expr); intersect!(vs, vars); isempty(vs)) + (vs = SU.search_variables(expr); intersect!(vs, vars); isempty(vs)) sym = gensym(:subexpr) - stype = symtype(expr) var = similar_variable(expr, sym) state[expr] = var return var end if (op == (+) || op == (*)) && symbolic_type(expr) !== ArraySymbolic() - indep_args = [] - dep_args = [] + indep_args = SymbolicT[] + dep_args = SymbolicT[] for arg in args - _vs = ModelingToolkit.vars(arg) + _vs = SU.search_variables(arg) intersect!(_vs, vars) if !isempty(_vs) push!(dep_args, subexpressions_not_involving_vars!(arg, vars, state)) @@ -1063,25 +1160,6 @@ function symbol_to_symbolic(sys::AbstractSystem, sym; allsyms = all_symbols(sys) return sym end -""" - $(TYPEDSIGNATURES) - -Check if `var` is present in `varlist`. `iv` is the independent variable of the system, -and should be `nothing` if not applicable. -""" -function var_in_varlist(var, varlist::AbstractSet, iv) - var = unwrap(var) - # simple case - return var in varlist || - # indexed array symbolic, unscalarized array present - (iscall(var) && operation(var) === getindex && arguments(var)[1] in varlist) || - # unscalarized sized array symbolic, all scalarized elements present - (symbolic_type(var) == ArraySymbolic() && is_sized_array_symbolic(var) && - all(x -> x in varlist, collect(var))) || - # delayed variables - (isdelay(var, iv) && var_in_varlist(operation(var)(iv), varlist, iv)) -end - """ $(TYPEDSIGNATURES) @@ -1120,25 +1198,19 @@ Given a list of equations where some may be array equations, flatten the array e without scalarizing occurrences of array variables and return the new list of equations. """ function flatten_equations(eqs::Vector{Equation}) - mapreduce(vcat, eqs; init = Equation[]) do eq - islhsarr = eq.lhs isa AbstractArray || Symbolics.isarraysymbolic(eq.lhs) - isrhsarr = eq.rhs isa AbstractArray || Symbolics.isarraysymbolic(eq.rhs) - if islhsarr || isrhsarr - islhsarr && isrhsarr || - error(""" - LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must either both be array expressions \ - or both scalar - """) - size(eq.lhs) == size(eq.rhs) || - error(""" - Size of LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must match: got \ - $(size(eq.lhs)) and $(size(eq.rhs)) - """) - return vec(collect(eq.lhs) .~ collect(eq.rhs)) - else - eq + _eqs = Equation[] + for eq in eqs + if !SU.is_array_shape(SU.shape(eq.lhs)) + push!(_eqs, eq) + continue + end + lhs = vec(collect(eq.lhs)::Array{SymbolicT})::Vector{SymbolicT} + rhs = vec(collect(eq.rhs)::Array{SymbolicT})::Vector{SymbolicT} + for (l, r) in zip(lhs, rhs) + push!(_eqs, l ~ r) end end + return _eqs end const JumpType = Union{VariableRateJump, ConstantRateJump, MassActionJump} @@ -1147,7 +1219,7 @@ struct NotPossibleError <: Exception end function Base.showerror(io::IO, ::NotPossibleError) print(io, """ - This should not be possible. Please open an issue in ModelingToolkit.jl with an MWE. + This should not be possible. Please open an issue in ModelingToolkitBase.jl with an MWE. """) end @@ -1172,7 +1244,7 @@ function underscore_to_D(v, iv, inv_map) if haskey(inv_map, v) only(get(inv_map, v, [v])) else - v = ModelingToolkit.detime_dvs(v) + v = ModelingToolkitBase.detime_dvs(v) s = split(string(getname(v)), 'ˍ') if length(s) > 1 n, suffix = s @@ -1181,6 +1253,7 @@ function underscore_to_D(v, iv, inv_map) end repeats = length(suffix) ÷ length(string(iv)) D = Differential(iv) + v = SSym(Symbol(n); type = FnType{Tuple, Real, Nothing}, shape = SymbolicUtils.ShapeVecT())(iv) wrap_with_D(v, D, repeats) end end @@ -1192,3 +1265,98 @@ function wrap_with_D(n, D, repeats) wrap_with_D(D(n), D, repeats - 1) end end + +const DEFAULT_STABLE_INDEX = SU.StableIndex(Int[]) + +""" + $TYPEDSIGNATURES + +Given a symbolic variable `x`, check whether it is an indexed array symbolic. If it is, +return the array and `true`. Otherwise, return `x, false`. +""" +function split_indexed_var(x::SymbolicT) + Moshi.Match.@match x begin + BSImpl.Term(; f, args) && if f === getindex end => (args[1], true) + BSImpl.Term(; f, args) && if f isa Operator && length(args) == 1 end => begin + arr, isarr = split_indexed_var(args[1]) + isarr || return x, false + return f(arr)::SymbolicT, isarr + end + _ => return x, false + end +end + +""" + $TYPEDSIGNATURES + +Given a symbolic variable `x`, assume `split_indexed_var(x)[2]` is `true`. Return the +corresponding `SymbolicUtils.StableIndex`. +""" +function get_stable_index(x::SymbolicT) + Moshi.Match.@match x begin + BSImpl.Term(; f, args) && if f === getindex end => return SU.StableIndex{Int}(x) + BSImpl.Term(; f, args) && if f isa Operator end => return get_stable_index(args[1]) + _ => throw(ArgumentError("Invalid variable $x for `get_stable_index`.")) + end +end + +""" + $TYPEDSIGNATURES + +Merge `b` into `a`, but error if `a` already contains that key. Return the modified `a`. +""" +function no_override_merge!(a::AbstractDict, b::AbstractDict) + for (k, v) in b + if haskey(a, k) + throw(ArgumentError("Cannot merge without overriding: common key $k.")) + end + a[k] = v + end + return a +end + +""" + $TYPEDSIGNATURES + +Identical to `no_override_merge!` but `COMMON_MISSING` values in `b` are ignored. +""" +function no_override_merge_except_missing!(a::AbstractDict, b::AbstractDict) + for (k, v) in b + v === COMMON_MISSING && continue + if haskey(a, k) + throw(ArgumentError("Cannot merge without overriding: common key $k.")) + end + a[k] = v + end + return a +end + +""" + $TYPEDSIGNATURES + +Merge `b` into `a`, modifying `a`. For all keys common to `a` and `b`, +prefer the value in `a`. +""" +function left_merge!(a::AbstractDict, b::AbstractDict) + mergewith!(first ∘ tuple, a, b) +end + +function left_merge!(a::AtomicArrayDict{SymbolicT}, b::AtomicArrayDict{SymbolicT}) + for (k, v) in b + if !haskey(a, k) + a[k] = v + continue + end + + v_a = a[k] + sh = SU.shape(k) + SU.is_array_shape(sh) || continue + any(Base.Fix2(===, COMMON_NOTHING) ∘ Base.Fix1(getindex, v_a), SU.stable_eachindex(v_a)) || continue + + new_v = map(SU.stable_eachindex(v_a)) do i + v_a[i] === COMMON_NOTHING ? v[i] : v_a[i] + end + a[k] = BSImpl.Const{VartypeT}(reshape(new_v, size(v_a))) + end + mergewith!(first ∘ tuple, a, b) +end diff --git a/src/variables.jl b/lib/ModelingToolkitBase/src/variables.jl similarity index 65% rename from src/variables.jl rename to lib/ModelingToolkitBase/src/variables.jl index 46c9c95bc6..16f811eb05 100644 --- a/src/variables.jl +++ b/lib/ModelingToolkitBase/src/variables.jl @@ -44,6 +44,8 @@ struct VariableMisc end # Metadata for renamed shift variables xₜ₋₁ struct VariableUnshifted end struct VariableShift end +struct VariableTimeDomain end + Symbolics.option_to_metadata_type(::Val{:unit}) = VariableUnit Symbolics.option_to_metadata_type(::Val{:connect}) = VariableConnectType Symbolics.option_to_metadata_type(::Val{:input}) = VariableInput @@ -53,6 +55,7 @@ Symbolics.option_to_metadata_type(::Val{:state_priority}) = VariableStatePriorit Symbolics.option_to_metadata_type(::Val{:misc}) = VariableMisc Symbolics.option_to_metadata_type(::Val{:unshifted}) = VariableUnshifted Symbolics.option_to_metadata_type(::Val{:shift}) = VariableShift +Symbolics.option_to_metadata_type(::Val{:timedomain}) = VariableTimeDomain """ dump_variable_metadata(var) @@ -60,10 +63,10 @@ Symbolics.option_to_metadata_type(::Val{:shift}) = VariableShift Return all the metadata associated with symbolic variable `var` as a `NamedTuple`. ```@example -using ModelingToolkit +using ModelingToolkitBase @parameters p::Int [description = "My description", bounds = (0.5, 1.5)] -ModelingToolkit.dump_variable_metadata(p) +ModelingToolkitBase.dump_variable_metadata(p) ``` """ function dump_variable_metadata(var) @@ -179,7 +182,7 @@ struct Stream <: AbstractConnectType end # special stream connector Get the connect type of x. See also [`hasconnect`](@ref). """ getconnect(x::Num) = getconnect(unwrap(x)) -getconnect(x::Symbolic) = Symbolics.getmetadata(x, VariableConnectType, nothing) +getconnect(x::SymbolicT) = Symbolics.getmetadata(x, VariableConnectType, nothing) """ hasconnect(x) @@ -190,13 +193,13 @@ function setconnect(x, t::Type{T}) where {T <: AbstractConnectType} setmetadata(x, VariableConnectType, t) end -### Input, Output, Irreducible -isvarkind(m, x::Union{Num, Symbolics.Arr}) = isvarkind(m, value(x)) -function isvarkind(m, x) - iskind = getmetadata(x, m, nothing) - iskind !== nothing && return iskind - x = getparent(x, x) - getmetadata(x, m, false) +### Input, Output, Irreducible +isvarkind(m, x, def = false) = safe_getmetadata(m, x, def) +safe_getmetadata(m, x::Union{Num, Symbolics.Arr}, def) = safe_getmetadata(m, value(x), def) +function safe_getmetadata(m::DataType, x::SymbolicT, default) + hasmetadata(x, m) && return getmetadata(x, m) + iscall(x) && operation(x) === getindex && return safe_getmetadata(m, arguments(x)[1], default) + return default end """ @@ -218,13 +221,13 @@ setio(x, i::Bool, o::Bool) = setoutput(setinput(x, i), o) Check if variable `x` is marked as an input. """ -isinput(x) = isvarkind(VariableInput, x) +isinput(x) = isvarkind(VariableInput, x)::Bool """ $(TYPEDSIGNATURES) Check if variable `x` is marked as an output. """ -isoutput(x) = isvarkind(VariableOutput, x) +isoutput(x) = isvarkind(VariableOutput, x)::Bool # Before the solvability check, we already have handled IO variables, so # irreducibility is independent from IO. @@ -234,7 +237,7 @@ isoutput(x) = isvarkind(VariableOutput, x) Check if `x` is marked as irreducible. This prevents it from being eliminated as an observed variable in `mtkcompile`. """ -isirreducible(x) = isvarkind(VariableIrreducible, x) +isirreducible(x) = isvarkind(VariableIrreducible, x)::Bool setirreducible(x, v::Bool) = setmetadata(x, VariableIrreducible, v) state_priority(x::Union{Num, Symbolics.Arr}) = state_priority(unwrap(x)) """ @@ -245,19 +248,168 @@ chosen as a state in `mtkcompile`. """ state_priority(x) = convert(Float64, getmetadata(x, VariableStatePriority, 0.0))::Float64 -normalize_to_differential(x) = x +function normalize_to_differential(@nospecialize(op)) + if op isa Shift && op.t isa SymbolicT + return Differential(op.t) ^ op.steps + else + return op + end +end -function default_toterm(x) - if iscall(x) && (op = operation(x)) isa Operator - if !(op isa Differential) - if op isa Shift && op.steps < 0 +default_toterm(x) = x +function default_toterm(x::SymbolicT) + Moshi.Match.@match x begin + BSImpl.Term(; f, args, shape, type, metadata) && if f isa Operator end => begin + if f isa Shift && f.steps < 0 return shift2term(x) + elseif f isa Differential + return Symbolics.diff2term(x) + else + newf = normalize_to_differential(f) + f === newf && return x + x = BSImpl.Term{VartypeT}(newf, args; type, shape, metadata) + return Symbolics.diff2term(x) + end + end + _ => return x + end +end + +""" +Rename a Shift variable with negative shift, Shift(t, k)(x(t)) to xₜ₋ₖ(t). +""" +function shift2term(var::SymbolicT) + Moshi.Match.@match var begin + BSImpl.Term(f, args) && if f isa Shift end => begin + op = f + arg = args[1] + Moshi.Match.@match arg begin + BSImpl.Term(; f, args, type, shape, metadata) && if f === getindex end => begin + newargs = copy(parent(args)) + newargs[1] = shift2term(op(newargs[1])) + unshifted_args = copy(newargs) + unshifted_args[1] = ModelingToolkitBase.getunshifted(newargs[1]) + unshifted = BSImpl.Term{VartypeT}(getindex, unshifted_args; type, shape, metadata) + if metadata === nothing + metadata = Base.ImmutableDict{DataType, Any}(VariableUnshifted, unshifted) + elseif metadata isa Base.ImmutableDict{DataType, Any} + metadata = Base.ImmutableDict(metadata, VariableUnshifted, unshifted) + end + return BSImpl.Term{VartypeT}(getindex, newargs; type, shape, metadata) + end + _ => nothing + end + unshifted = ModelingToolkitBase.getunshifted(arg) + is_lowered = unshifted !== nothing + backshift = op.steps + ModelingToolkitBase.getshift(arg) + iszero(backshift) && return unshifted + io = IOBuffer() + O = (is_lowered ? unshifted : arg)::SymbolicT + write(io, getname(O)) + # Char(0x209c) = ₜ + write(io, Char(0x209c)) + # Char(0x208b) = ₋ (subscripted minus) + # Char(0x208a) = ₊ (subscripted plus) + pm = backshift > 0 ? Char(0x208a) : Char(0x208b) + write(io, pm) + _backshift = backshift + backshift = abs(backshift) + N = ndigits(backshift) + den = 10 ^ (N - 1) + for _ in 1:N + # subscripted number, e.g. ₁ + write(io, Char(0x2080 + div(backshift, den) % 10)) + den = div(den, 10) end - x = normalize_to_differential(op)(arguments(x)...) + newname = Symbol(take!(io)) + newvar = Symbolics.rename(arg, newname) + newvar = setmetadata(newvar, ModelingToolkitBase.VariableUnshifted, O) + newvar = setmetadata(newvar, ModelingToolkitBase.VariableShift, _backshift) + return newvar + end + _ => return var + end +end + +simplify_shifts(eq::Equation) = simplify_shifts(eq.lhs) ~ simplify_shifts(eq.rhs) + +function _simplify_shifts(var::SymbolicT) + Moshi.Match.@match var begin + BSImpl.Term(; f, args) && if f isa Shift && f.steps == 0 end => return args[1] + BSImpl.Term(; f = op1, args) && if op1 isa Shift end => begin + vv1 = args[1] + Moshi.Match.@match vv1 begin + BSImpl.Term(; f = op2, args = a2) && if op2 isa Shift end => begin + vv2 = a2[1] + s1 = op1.steps + s2 = op2.steps + t1 = op1.t + t2 = op2.t + return simplify_shifts(ModelingToolkitBase.Shift(t1 === nothing ? t2 : t1, s1 + s2)(vv2)) + end + _ => return var + end + end + _ => var + end +end + +""" +Simplify multiple shifts: Shift(t, k1)(Shift(t, k2)(x)) becomes Shift(t, k1+k2)(x). +""" +function simplify_shifts(var::SymbolicT) + ModelingToolkitBase.hasshift(var) || return var + return SU.Rewriters.Postwalk(_simplify_shifts)(var) +end + +""" +Distribute a shift applied to a whole expression or equation. +Shift(t, 1)(x + y) will become Shift(t, 1)(x) + Shift(t, 1)(y). +Only shifts variables whose independent variable is the same t that appears in the Shift (i.e. constants, time-independent parameters, etc. do not get shifted). +""" +function distribute_shift(var) + var = unwrap(var) + var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) + + ModelingToolkitBase.hasshift(var) || return var + shift = operation(var) + shift isa Shift || return var + + shift = operation(var) + expr = only(arguments(var)) + if expr isa Equation + return distribute_shift(shift(expr.lhs)) ~ distribute_shift(shift(expr.rhs)) + end + shiftexpr = _distribute_shift(expr, shift) + return simplify_shifts(shiftexpr) +end + +""" + $TYPEDSIGNATURES + +Whether `distribute_shift` should distribute shifts into the given operation. +""" +distribute_shift_into_operator(_) = true + +function _distribute_shift(expr, shift) + if iscall(expr) + op = operation(expr) + distribute_shift_into_operator(op) || return expr + args = arguments(expr) + + if ModelingToolkitBase.isvariable(expr) && operation(expr) !== getindex && + !ModelingToolkitBase.iscalledparameter(expr) + (length(args) == 1 && isequal(shift.t, only(args))) ? (return shift(expr)) : + (return expr) + elseif op isa Shift + return shift(expr) + else + return maketerm( + typeof(expr), operation(expr), Base.Fix2(_distribute_shift, shift).(args), + unwrap(expr).metadata) end - Symbolics.diff2term(x) else - x + return expr end end @@ -280,35 +432,25 @@ Create parameters with bounds like this @parameters p [bounds=(-1, 1)] ``` """ -function getbounds(x::Union{Num, Symbolics.Arr, SymbolicUtils.Symbolic}) - x = unwrap(x) - p = Symbolics.getparent(x, nothing) - if p === nothing - bounds = Symbolics.getmetadata(x, VariableBounds, (-Inf, Inf)) - if symbolic_type(x) == ArraySymbolic() && Symbolics.shape(x) != Symbolics.Unknown() - bounds = map(bounds) do b - b isa AbstractArray && return b - return fill(b, size(x)) - end +getbounds(x::Union{Num, Symbolics.Arr}) = getbounds(unwrap(x)) +function getbounds(x::SymbolicT) + if iscall(x) && operation(x) === getindex + bounds = getmetadata(x, VariableBounds, nothing) + bounds === nothing || return bounds + p = arguments(x)[1] + bounds = getbounds(p) + idxs = @views unwrap_const.(arguments(x)[2:end]) + return map(bounds) do b + @assert symbolic_has_known_size(p) && size(p) == size(b) + return b[idxs...] end else - # if we reached here, `x` is the result of calling `getindex` - bounds = @something Symbolics.getmetadata(x, VariableBounds, nothing) getbounds(p) - idxs = arguments(x)[2:end] - bounds = map(bounds) do b - if b isa AbstractArray - if Symbolics.shape(p) != Symbolics.Unknown() && size(p) != size(b) - throw(DimensionMismatch("Expected array variable $p with shape $(size(p)) to have bounds of identical size. Found $bounds of size $(size(bounds)).")) - end - return b[idxs...] - elseif symbolic_type(x) == ArraySymbolic() - return fill(b, size(x)) - else - return b - end + if symbolic_has_known_size(x) && SU.is_array_shape(SU.shape(x)) + return getmetadata(x, VariableBounds, (fill(-Inf, size(x)), fill(Inf, size(x)))) + else + return getmetadata(x, VariableBounds, (-Inf, Inf)) end end - return bounds end """ @@ -318,8 +460,8 @@ Determine whether symbolic variable `x` has bounds associated with it. See also [`getbounds`](@ref). """ function hasbounds(x) - b = getbounds(x) - any(isfinite.(b[1]) .|| isfinite.(b[2])) + b = getbounds(x)::NTuple{2, Any} + any(isfinite.(b[1]) .|| isfinite.(b[2]))::Bool end function setbounds(x::Num, bounds) @@ -339,9 +481,7 @@ isdisturbance(x::Num) = isdisturbance(Symbolics.unwrap(x)) Determine whether symbolic variable `x` is marked as a disturbance input. """ function isdisturbance(x) - p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) - Symbolics.getmetadata(x, VariableDisturbance, false) + isvarkind(VariableDisturbance, x)::Bool end setdisturbance(x, v) = setmetadata(x, VariableDisturbance, v) @@ -372,9 +512,7 @@ Create a tunable parameter by See also [`tunable_parameters`](@ref), [`getbounds`](@ref) """ function istunable(x, default = true) - p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) - Symbolics.getmetadata(x, VariableTunable, default) + isvarkind(VariableTunable, x, default)::Bool end ## Dist ======================================================================== @@ -398,9 +536,7 @@ getdist(u) # retrieve distribution ``` """ function getdist(x) - p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) - Symbolics.getmetadata(x, VariableDistribution, nothing) + safe_getmetadata(VariableDistribution, x, nothing) end """ @@ -441,7 +577,7 @@ function tunable_parameters( end """ - getbounds(sys::ModelingToolkit.AbstractSystem, p = parameters(sys)) + getbounds(sys::ModelingToolkitBase.AbstractSystem, p = parameters(sys)) Returns a dict with pairs `p => (lb, ub)` mapping parameters of `sys` to lower and upper bounds. Create parameters with bounds like this @@ -452,7 +588,7 @@ Create parameters with bounds like this To obtain unknown variable bounds, call `getbounds(sys, unknowns(sys))` """ -function getbounds(sys::ModelingToolkit.AbstractSystem, p = parameters(sys)) +function getbounds(sys::ModelingToolkitBase.AbstractSystem, p = parameters(sys)) Dict(p .=> getbounds.(p)) end @@ -492,9 +628,7 @@ getdescription(x::Symbolics.Arr) = getdescription(Symbolics.unwrap(x)) Return any description attached to variables `x`. If no description is attached, an empty string is returned. """ function getdescription(x) - p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) - Symbolics.getmetadata(x, VariableDescription, "") + safe_getmetadata(VariableDescription, x, "") end """ @@ -512,7 +646,7 @@ end Maps the brownianiable to an unknown. """ -tobrownian(s::Symbolic) = setmetadata(s, MTKVariableTypeCtx, BROWNIAN) +tobrownian(s::SymbolicT) = setmetadata(s, MTKVariableTypeCtx, BROWNIAN) tobrownian(s::Num) = Num(tobrownian(value(s))) isbrownian(s) = getvariabletype(s) === BROWNIAN @@ -526,10 +660,10 @@ macro brownians(xs...) x -> x isa Symbol || Meta.isexpr(x, :call) && x.args[1] == :$ || Meta.isexpr(x, :$), xs) || error("@brownians only takes scalar expressions!") - Symbolics._parse_vars(:brownian, + Symbolics.parse_vars(:brownian, Real, xs, - tobrownian) |> esc + tobrownian) end ## Guess ====================================================================== @@ -548,7 +682,7 @@ Create variables with a guess like this ``` """ function getguess(x) - Symbolics.getmetadata(x, VariableGuess, nothing) + Symbolics.getmetadata_maybe_indexed(x, VariableGuess, nothing) end """ @@ -587,7 +721,7 @@ Fetch any miscellaneous data associated with symbolic variable `x`. See also [`hasmisc(x)`](@ref). """ getmisc(x::Num) = getmisc(unwrap(x)) -getmisc(x::Symbolic) = Symbolics.getmetadata(x, VariableMisc, nothing) +getmisc(x::SymbolicT) = Symbolics.getmetadata(x, VariableMisc, nothing) """ hasmisc(x) @@ -606,7 +740,7 @@ setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) Fetch the unit associated with variable `x`. This function is a metadata getter for an individual variable, while `get_unit` is used for unit inference on more complicated sdymbolic expressions. """ getunit(x::Num) = getunit(unwrap(x)) -getunit(x::Symbolic) = Symbolics.getmetadata(x, VariableUnit, nothing) +getunit(x::SymbolicT) = Symbolics.getmetadata(x, VariableUnit, nothing) """ hasunit(x) @@ -615,10 +749,10 @@ Check if the variable `x` has a unit. hasunit(x) = getunit(x) !== nothing getunshifted(x::Num) = getunshifted(unwrap(x)) -getunshifted(x::Symbolic) = Symbolics.getmetadata(x, VariableUnshifted, nothing) +getunshifted(x::SymbolicT) = Symbolics.getmetadata(x, VariableUnshifted, nothing)::Union{SymbolicT, Nothing} getshift(x::Num) = getshift(unwrap(x)) -getshift(x::Symbolic) = Symbolics.getmetadata(x, VariableShift, 0) +getshift(x::SymbolicT) = Symbolics.getmetadata(x, VariableShift, 0)::Int ################### ### Evaluate at ### @@ -629,7 +763,7 @@ getshift(x::Symbolic) = Symbolics.getmetadata(x, VariableShift, 0) An operator that evaluates time-dependent variables at a specific absolute time point `t`. # Fields -- `t::Union{Symbolic, Number}`: The absolute time at which to evaluate the variable. +- `t::Union{SymbolicT, Number}`: The absolute time at which to evaluate the variable. # Description `EvalAt` is used to evaluate time-dependent variables at a specific time point. This is particularly @@ -648,7 +782,7 @@ time `t`. For variables that don't depend on time, `EvalAt` returns them unchang # Examples ```julia -using ModelingToolkit +using ModelingToolkitBase @variables t x(t) y(t) @parameters p @@ -677,19 +811,12 @@ end See also: [`Differential`](@ref) """ struct EvalAt <: Symbolics.Operator - t::Union{Symbolic, Number} + t::Union{SymbolicT, Number} end -function (A::EvalAt)(x::Symbolic) - if symbolic_type(x) == NotSymbolic() || !iscall(x) - if x isa Symbolics.CallWithMetadata - return x(A.t) - else - return x - end - end - - if iscall(x) && operation(x) == getindex +function (A::EvalAt)(x::SymbolicT) + iscall(x) || return x + if operation(x) === getindex arr = arguments(x)[1] term(getindex, A(arr), arguments(x)[2:end]...) elseif operation(x) isa Differential @@ -698,7 +825,7 @@ function (A::EvalAt)(x::Symbolic) else length(arguments(x)) !== 1 && error("Variable $x has too many arguments. EvalAt can only be applied to one-argument variables.") - (symbolic_type(only(arguments(x))) !== ScalarSymbolic()) && return x + SU.isconst(only(arguments(x))) && return x return operation(x)(A.t) end end @@ -706,6 +833,11 @@ end function (A::EvalAt)(x::Union{Num, Symbolics.Arr}) wrap(A(unwrap(x))) end + +function (A::EvalAt)(x::CallAndWrap) + x(A.t) +end + SymbolicUtils.isbinop(::EvalAt) = false Base.nameof(::EvalAt) = :EvalAt diff --git a/test/abstractsystem.jl b/lib/ModelingToolkitBase/test/abstractsystem.jl similarity index 90% rename from test/abstractsystem.jl rename to lib/ModelingToolkitBase/test/abstractsystem.jl index 09b21ea290..5b3e915fab 100644 --- a/test/abstractsystem.jl +++ b/lib/ModelingToolkitBase/test/abstractsystem.jl @@ -1,6 +1,6 @@ -using ModelingToolkit +using ModelingToolkitBase using Test -MT = ModelingToolkit +MT = ModelingToolkitBase @independent_variables t @variables x diff --git a/test/accessor_functions.jl b/lib/ModelingToolkitBase/test/accessor_functions.jl similarity index 90% rename from test/accessor_functions.jl rename to lib/ModelingToolkitBase/test/accessor_functions.jl index c54fb4c4ca..79cd696422 100644 --- a/test/accessor_functions.jl +++ b/lib/ModelingToolkitBase/test/accessor_functions.jl @@ -1,11 +1,11 @@ ### Preparations ### # Fetch packages. -using ModelingToolkit, Test -using ModelingToolkit: t_nounits as t, D_nounits as D -import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events, +using ModelingToolkitBase, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as D +import ModelingToolkitBase: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events, get_discrete_events, namespace_equations -import ModelingToolkit: parameters_toplevel, unknowns_toplevel, equations_toplevel, +import ModelingToolkitBase: parameters_toplevel, unknowns_toplevel, equations_toplevel, continuous_events_toplevel, discrete_events_toplevel # Creates helper functions. @@ -101,19 +101,21 @@ let # Checks `unknowns`. O(t) is eliminated by `mtkcompile` and # must be considered separately. @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot]) - @test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot]) @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot]) - @test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot]) @test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2]) - @test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2]) @test all_sets_equal(unknowns.([sys_top, sys_top_comp])..., [O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2]) - @test all_sets_equal(unknowns.([sys_top_ss])..., - [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, - sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) + if @isdefined(ModelingToolkit) + @test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot]) + @test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot]) + @test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2]) + @test all_sets_equal(unknowns.([sys_top_ss])..., + [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, + sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) + end # Checks `unknowns_toplevel`. Note that O is not eliminated here (as we go back # to original parent system). Also checks that outputs are subsets of what `get_unknowns` returns. diff --git a/lib/ModelingToolkitBase/test/analysis_points.jl b/lib/ModelingToolkitBase/test/analysis_points.jl new file mode 100644 index 0000000000..08c3282a11 --- /dev/null +++ b/lib/ModelingToolkitBase/test/analysis_points.jl @@ -0,0 +1,771 @@ +using ModelingToolkitBase, ModelingToolkitStandardLibrary.Blocks, ControlSystemsBase +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq, LinearAlgebra +using Test +using ModelingToolkitBase: t_nounits as t, D_nounits as D, AnalysisPoint, AbstractSystem +import ModelingToolkitBase as MTK +import ControlSystemsBase as CS +using Symbolics: NAMESPACE_SEPARATOR + +@testset "AnalysisPoint is lowered to `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + eqs = [connect(P.output, C.input) + connect(C.output, ap, P.input)] + sys_ap = System(eqs, t, systems = [P, C], name = :hej) + sys_ap2 = @test_nowarn expand_connections(sys_ap) + + @test all(eq -> !(eq.lhs isa AnalysisPoint), equations(sys_ap2)) + + eqs = [connect(P.output, C.input) + connect(C.output, P.input)] + sys_normal = System(eqs, t, systems = [P, C], name = :hej) + sys_normal2 = @test_nowarn expand_connections(sys_normal) + + @test issetequal(equations(sys_ap2), equations(sys_normal2)) +end + +@testset "Inverse causality throws a warning" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + @test_warn ["1-th argument", "plant_input", "not a output"] connect( + P.input, ap, C.output) + @test_nowarn connect(P.input, ap, C.output; verbose = false) +end + +# also tests `connect(input, name::Symbol, outputs...)` syntax +@testset "AnalysisPoint is accessible via `getproperty`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) + + eqs = [connect(P.output, C.input), connect(C.output, :plant_input, P.input)] + sys_ap = System(eqs, t, systems = [P, C], name = :hej) + ap2 = @test_nowarn sys_ap.plant_input + @test nameof(ap2) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) + @named sys = System(Equation[], t; systems = [sys_ap]) + ap3 = @test_nowarn sys.hej.plant_input + @test nameof(ap3) == Symbol(join(["sys", "hej", "plant_input"], NAMESPACE_SEPARATOR)) + csys = complete(sys) + ap4 = csys.hej.plant_input + @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) + nsys = toggle_namespacing(sys, false) + ap5 = nsys.hej.plant_input + @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) +end + +### Ported from MTKStdlib +if @isdefined(ModelingToolkit) + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + eqs = [connect(P.output, C.input), connect(C.output, ap, P.input)] + sys = System(eqs, t, systems = [P, C], name = :hej) + @named nested_sys = System(Equation[], t; systems = [sys]) + nonamespace_sys = toggle_namespacing(nested_sys, false) + + @testset "simplifies and solves" begin + ssys = mtkcompile(sys) + prob = ODEProblem(ssys, [P.x => 1], (0, 10)) + sol = solve(prob, Rodas5()) + @test norm(sol.u[1]) >= 1 + @test norm(sol.u[end]) < 1e-6 # This fails without the feedback through C + end + + test_cases = [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("nonamespace", nonamespace_sys, nonamespace_sys.hej.plant_input), + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) + ] + + @testset "get_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 + end + + @testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_comp_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive or negative + @test matrices.D[] == 0 + end + + #= + # Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. + using ControlSystemsBase + P = tf(1.0, [1, 1]) + C = 1 # Negative feedback assumed in ControlSystems + S = sensitivity(P, C) # or feedback(1, P*C) + T = comp_sensitivity(P, C) # or feedback(P*C) + =# + + @testset "get_looptransfer - $name" for (name, sys, ap) in test_cases + matrices, _ = get_looptransfer(sys, ap) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end + + #= + # Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. + using ControlSystemsBase + P = tf(1.0, [1, 1]) + C = -1 + L = P*C + =# + + @testset "open_loop - $name" for (name, sys, ap) in test_cases + open_sys, (du, u) = open_loop(sys, ap) + matrices, _ = linearize(open_sys, [du], [u]) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end + + # Multiple analysis points + + eqs = [connect(P.output, :plant_output, C.input) + connect(C.output, :plant_input, P.input)] + sys = System(eqs, t, systems = [P, C], name = :hej) + @named nested_sys = System(Equation[], t; systems = [sys]) + + test_cases = [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) + ] + + @testset "get_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 + end + + @testset "linearize - $name" for (name, sys, inputap, outputap) in [ + ("inner", sys, sys.plant_input, sys.plant_output), + ("nested", nested_sys, nested_sys.hej.plant_input, nested_sys.hej.plant_output) + ] + inputname = Symbol(join( + MTK.namespace_hierarchy(nameof(inputap))[2:end], NAMESPACE_SEPARATOR)) + outputname = Symbol(join( + MTK.namespace_hierarchy(nameof(outputap))[2:end], NAMESPACE_SEPARATOR)) + @testset "input - $(typeof(input)), output - $(typeof(output))" for (input, output) in [ + (inputap, outputap), + (inputname, outputap), + (inputap, outputname), + (inputname, outputname), + (inputap, [outputap]), + (inputname, [outputap]), + (inputap, [outputname]), + (inputname, [outputname]) + ] + matrices, _ = linearize(sys, input, output) + # Result should be the same as feedpack(P, 1), i.e., the closed-loop transfer function from plant input to plant output + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive + @test matrices.D[] == 0 + end + end + + @testset "linearize - variable output - $name" for (name, sys, input, output) in [ + ("inner", sys, sys.plant_input, P.output.u), + ("nested", nested_sys, nested_sys.hej.plant_input, sys.P.output.u) + ] + matrices, _ = linearize(sys, input, [output]) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive + @test matrices.D[] == 0 + end + + @testset "Complicated model" begin + # 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 System(eqs, t; + systems = [ + torque, + inertia1, + inertia2, + spring, + damper, + u + ], + name) + end + System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + end + + @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 = System(connections, t, systems = [model, pid, filt, sensor, r, er], + name = :closed_loop, initial_conditions = [ + 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 + ]) + + sys = mtkcompile(closed_loop) + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) + sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) + + matrices, ssys = linearize(closed_loop, :r, :y) + lsys = ss(matrices...) |> sminreal + @test lsys.nx == 8 + + stepres = ControlSystemsBase.step(c2d(lsys, 0.001), 4) + @test Array(stepres.y[:])≈Array(sol(0:0.001:4, idxs = model.inertia2.phi)) rtol=1e-4 + + matrices, ssys = get_sensitivity(closed_loop, :y) + So = ss(matrices...) + + matrices, ssys = get_sensitivity(closed_loop, :u) + Si = ss(matrices...) + + @test tf(So) ≈ tf(Si) + end + + @testset "Duplicate `connect` statements across subsystems with AP transforms - standard `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output, :plant_output, add.input2) + connect(add.output, C.input) + connect(C.output, P.input)] + + sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(sys_inner.P.output, sys_inner.add.input2) + connect(sys_inner.C.output, :plant_input, sys_inner.P.input) + connect(F.output, sys_inner.add.input1)] + sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the mtkcompile works correctly + ssys = mtkcompile(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) + + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems + end + + @testset "Duplicate `connect` statements across subsystems with AP transforms - causal variable `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output.u, :plant_output, add.input2.u) + connect(add.output, C.input) + connect(C.output.u, P.input.u)] + + sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(sys_inner.P.output.u, sys_inner.add.input2.u) + connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) + connect(F.output, sys_inner.add.input1)] + sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the mtkcompile works correctly + ssys = mtkcompile(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) + + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems + end + + @testset "Duplicate `connect` statements across subsystems with AP transforms - mixed `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output.u, :plant_output, add.input2.u) + connect(add.output, C.input) + connect(C.output, P.input)] + + sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(sys_inner.P.output, sys_inner.add.input2) + connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) + connect(F.output, sys_inner.add.input1)] + sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the mtkcompile works correctly + ssys = mtkcompile(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) + + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems + end + + @testset "multilevel system with loop openings" begin + @named P_inner = FirstOrder(k = 1, T = 1) + @named feedback = Feedback() + @named ref = Step() + @named sys_inner = System( + [connect(P_inner.output, :y, feedback.input2) + connect(feedback.output, :u, P_inner.input) + connect(ref.output, :r, feedback.input1)], + t, + systems = [P_inner, feedback, ref]) + + P_not_broken, _ = linearize(sys_inner, :u, :y) + @test P_not_broken.A[] == -2 + P_broken, ssys = linearize(sys_inner, :u, :y, loop_openings = [:u]) + @test isequal(initial_conditions(ssys)[ssys.d_u], ssys.feedback.output.u) + @test P_broken.A[] == -1 + P_broken, ssys = linearize(sys_inner, :u, :y, loop_openings = [:y]) + @test isequal(initial_conditions(ssys)[ssys.d_y], ssys.P_inner.output.u) + @test P_broken.A[] == -1 + + Sinner = sminreal(ss(get_sensitivity(sys_inner, :u)[1]...)) + + @named sys_inner = System( + [connect(P_inner.output, :y, feedback.input2) + connect(feedback.output, :u, P_inner.input)], + t, + systems = [P_inner, feedback]) + + @named P_outer = FirstOrder(k = rand(), T = rand()) + + @named sys_outer = System( + [connect(sys_inner.P_inner.output, :y2, P_outer.input) + connect(P_outer.output, :u2, sys_inner.feedback.input1)], + t, + systems = [P_outer, sys_inner]) + + Souter = sminreal(ss(get_sensitivity(sys_outer, sys_outer.sys_inner.u)[1]...)) + + Sinner2 = sminreal(ss(get_sensitivity( + sys_outer, sys_outer.sys_inner.u, loop_openings = [:y2])[1]...)) + + @test Sinner.nx == 1 + @test Sinner == Sinner2 + @test Souter.nx == 2 + end + + @testset "sensitivities in multivariate signals" begin + A = [-0.994 -0.0794; -0.006242 -0.0134] + B = [-0.181 -0.389; 1.1 1.12] + C = [1.74 0.72; -0.33 0.33] + D = [0.0 0.0; 0.0 0.0] + @named P = Blocks.StateSpace(A, B, C, D) + Pss = CS.ss(A, B, C, D) + + A = [-0.097;;] + B = [-0.138 -1.02] + C = [-0.076; 0.09;;] + D = [0.0 0.0; 0.0 0.0] + @named K = Blocks.StateSpace(A, B, C, D) + Kss = CS.ss(A, B, C, D) + + eqs = [connect(P.output, :plant_output, K.input) + connect(K.output, :plant_input, P.input)] + sys = System(eqs, t, systems = [P, K], name = :hej) + + matrices, _ = get_sensitivity(sys, :plant_input) + S = CS.feedback(I(2), Kss * Pss, pos_feedback = true) + + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(S) + + matrices, _ = get_comp_sensitivity(sys, :plant_input) + T = -CS.feedback(Kss * Pss, I(2), pos_feedback = true) + + # bodeplot([ss(matrices...), T]) + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(T) + + matrices, _ = get_looptransfer( + sys, :plant_input) + L = Kss * Pss + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(L) + + matrices, _ = linearize(sys, AnalysisPoint(:plant_input), :plant_output) + G = CS.feedback(Pss, Kss, pos_feedback = true) + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(G) + end + + @testset "multiple analysis points" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output, :plant_output, add.input2) + connect(add.output, C.input) + connect(C.output, :plant_input, P.input)] + + sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(F.output, sys_inner.add.input1)] + sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) + + matrices, + _ = get_sensitivity( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) + + Ps = tf(1, [1, 1]) |> ss + Cs = tf(1) |> ss + + G = CS.ss(matrices...) |> sminreal + Si = CS.feedback(1, Cs * Ps) + @test tf(G[1, 1]) ≈ tf(Si) + + So = CS.feedback(1, Ps * Cs) + @test tf(G[2, 2]) ≈ tf(So) + @test tf(G[1, 2]) ≈ tf(-CS.feedback(Cs, Ps)) + @test tf(G[2, 1]) ≈ tf(CS.feedback(Ps, Cs)) + + matrices, + _ = get_comp_sensitivity( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) + + G = CS.ss(matrices...) |> sminreal + Ti = CS.feedback(Cs * Ps) + @test tf(G[1, 1]) ≈ tf(Ti) + + To = CS.feedback(Ps * Cs) + @test tf(G[2, 2]) ≈ tf(To) + @test tf(G[1, 2]) ≈ tf(CS.feedback(Cs, Ps)) # The negative sign appears in a confusing place due to negative feedback not happening through Ps + @test tf(G[2, 1]) ≈ tf(-CS.feedback(Ps, Cs)) + + # matrices, _ = get_looptransfer(sys_outer, [:inner_plant_input, :inner_plant_output]) + matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_input) + L = CS.ss(matrices...) |> sminreal + @test tf(L) ≈ -tf(Cs * Ps) + + matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_output) + L = CS.ss(matrices...) |> sminreal + @test tf(L[1, 1]) ≈ -tf(Ps * Cs) + + # Calling looptransfer like below is not the intended way, but we can work out what it should return if we did so it remains a valid test + matrices, + _ = get_looptransfer( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) + L = CS.ss(matrices...) |> sminreal + @test tf(L[1, 1]) ≈ tf(0) + @test tf(L[2, 2]) ≈ tf(0) + @test sminreal(L[1, 2]) ≈ ss(-1) + @test tf(L[2, 1]) ≈ tf(Ps) + + matrices, + _ = linearize( + sys_outer, [sys_outer.inner.plant_input], [nameof(sys_inner.plant_output)]) + G = CS.ss(matrices...) |> sminreal + @test tf(G) ≈ tf(CS.feedback(Ps, Cs)) + end + + function normal_test_system() + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs_normal = [connect(back.output, :ap, F1.input) + connect(back.output, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named normal_inner = System(eqs_normal, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2_normal = [ + connect(step.output, normal_inner.back.input1) + ] + @named sys_normal = System(eqs2_normal, t; systems = [normal_inner, step]) + end + + sys_normal = normal_test_system() + + prob = ODEProblem(mtkcompile(sys_normal), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + matrices_normal, _ = get_sensitivity(sys_normal, sys_normal.normal_inner.ap) + + @testset "Analysis point overriding part of connection - normal connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output, F1.input, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = System(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output, :ap, inner.F1.input)] + @named sys = System(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal + end + + @testset "Analysis point overriding part of connection - variable connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output.u, F1.input.u, F2.input.u) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = System(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output.u, :ap, inner.F1.input.u)] + @named sys = System(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal + end + + @testset "Analysis point overriding part of connection - mixed connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output, F1.input, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = System(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output.u, :ap, inner.F1.input.u)] + @named sys = System(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal + end + + @testset "Ignored analysis points only affect relevant connection sets" begin + m1 = 1 + m2 = 1 + k = 1000 # Spring stiffness + c = 10 # Damping coefficient + + @named inertia1 = Inertia(; J = m1, phi = 0, w = 0) + @named inertia2 = Inertia(; J = m2, phi = 0, w = 0) + + @named spring = Spring(; c = k) + @named damper = Damper(; d = c) + + @named torque = Torque(use_support = false) + + 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 @named model = System( + eqs, t; systems = [torque, inertia1, inertia2, spring, damper, u]) + end + System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + end + + @named r = Step(start_time = 1) + @named pid = LimPID(k = 400, Ti = 0.5, Td = 1, u_max = 350) + @named filt = SecondOrder(d = 0.9, w = 10, x = 0, xd = 0) + @named sensor = AngleSensor() + @named add = Add() # To add the feedback and feedforward control signals + model = SystemModel() + @named inverse_model = SystemModel() + @named inverse_sensor = AngleSensor() + connections = [connect(r.output, :r, filt.input) # Name connection r to form an analysis point + connect(inverse_model.inertia1.flange_b, inverse_sensor.flange) # Attach the inverse sensor to the inverse model + connect(filt.output, pid.reference, inverse_sensor.phi) # the filtered reference now goes to both the PID controller and the inverse model input + connect(inverse_model.torque.tau, add.input1) + connect(pid.ctr_output, add.input2) + connect(add.output, :u, model.torque.tau) # Name connection u to form an analysis point + connect(model.inertia1.flange_b, sensor.flange) + connect(sensor.phi, :y, pid.measurement)] + closed_loop = System(connections, t, + systems = [model, inverse_model, pid, filt, sensor, inverse_sensor, r, add], + name = :closed_loop) + # just ensure the system simplifies + mats, _ = get_sensitivity(closed_loop, :y) + S = CS.ss(mats...) + fr = CS.freqrespv(S, [0.01, 1, 100]) + # https://github.com/SciML/ModelingToolkit.jl/pull/3469 + reference_fr = ComplexF64[-1.2505330104772838e-11 - 2.500062613816021e-9im, + -0.0024688370221621625 - 0.002279011866413123im, + 1.8100018764334602 + 0.3623845793211718im] + @test isapprox(fr, reference_fr) + end +end + +using DynamicQuantities + +@testset "AnalysisPoint is ignored when verifying units" begin + # no units first + @mtkmodel FirstOrderTest begin + @components begin + in = Step() + fb = Feedback() + fo = SecondOrder(k = 1, w = 1, d = 0.1) + end + @equations begin + connect(in.output, :u, fb.input1) + connect(fb.output, :e, fo.input) + connect(fo.output, :y, fb.input2) + end + end + @named model = FirstOrderTest() + @test model isa System + + @connector function UnitfulOutput(; name) + vars = @variables begin + u(t), [unit=u"m", output=true] + end + return System(Equation[], t, vars, []; name) + end + @connector function UnitfulInput(; name) + vars = @variables begin + u(t), [unit=u"m", input=true] + end + return System(Equation[], t, vars, []; name) + end + @component function UnitfulBlock(; name) + pars = @parameters begin + offset, [unit=u"m"] + start_time + height, [unit=u"m"] + duration + end + systems = @named begin + output = UnitfulOutput() + end + eqs = [ + output.u ~ offset + height*(0.5 + (1/pi)*atan(1e5*(t - start_time))) + ] + return System(eqs, t, [], pars; systems, name) + end + @mtkmodel TestAPAroundUnits begin + @components begin + input = UnitfulInput() + end + @variables begin + output(t), [output=true, unit=u"m^2"] + end + @components begin + ub = UnitfulBlock() + end + @equations begin + connect(ub.output, :ap, input) + output ~ input.u^2 + end + end + @named sys = TestAPAroundUnits() + @test sys isa System + + @mtkmodel TestAPWithNoOutputs begin + @components begin + input = UnitfulInput() + end + @variables begin + output(t), [output=true, unit=u"m^2"] + end + @components begin + ub = UnitfulBlock() + end + @equations begin + connect(ub.output, :ap, input) + output ~ input.u^2 + end + end + @named sys2 = TestAPWithNoOutputs() + @test sys2 isa System +end + diff --git a/test/basic_transformations.jl b/lib/ModelingToolkitBase/test/basic_transformations.jl similarity index 68% rename from test/basic_transformations.jl rename to lib/ModelingToolkitBase/test/basic_transformations.jl index 19899aa50f..b2376a17b6 100644 --- a/test/basic_transformations.jl +++ b/lib/ModelingToolkitBase/test/basic_transformations.jl @@ -1,6 +1,9 @@ -using ModelingToolkit, OrdinaryDiffEq, DataInterpolations, DynamicQuantities, Test +using ModelingToolkitBase, OrdinaryDiffEq, DataInterpolations, DynamicQuantities, Test using ModelingToolkitStandardLibrary.Blocks: RealInput, RealOutput -using SymbolicUtils: symtype +using Symbolics: value +using SymbolicUtils: symtype, _iszero +using ModelingToolkitBase: SymbolicContinuousCallback +using SciCompDSL @independent_variables t D = Differential(t) @@ -52,16 +55,18 @@ end M1 = System(eqs, t; initialization_eqs, name = :M) M2 = change_independent_variable(M1, s) - M1 = mtkcompile(M1; allow_symbolic = true) - M2 = mtkcompile(M2; allow_symbolic = true) - prob1 = ODEProblem(M1, [M1.s => 1.0], (1.0, 4.0)) - prob2 = ODEProblem(M2, [], (1.0, 2.0)) - sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) - sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) - ts = range(0.0, 1.0, length = 50) - ss = .√(ts) - @test all(isapprox.(sol1(ts, idxs = M1.x), sol2(ss, idxs = M2.x); atol = 1e-7)) && - all(isapprox.(sol1(ts, idxs = M1.y), sol2(ss, idxs = M2.y); atol = 1e-7)) + if @isdefined(ModelingToolkit) + M1 = mtkcompile(M1; allow_symbolic = true) + M2 = mtkcompile(M2; allow_symbolic = true) + prob1 = ODEProblem(M1, [M1.s => 1.0], (1.0, 4.0)) + prob2 = ODEProblem(M2, [], (1.0, 2.0)) + sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) + sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) + ts = range(0.0, 1.0, length = 50) + ss = .√(ts) + @test all(isapprox.(sol1(ts, idxs = M1.x), sol2(ss, idxs = M2.x); atol = 1e-7)) && + all(isapprox.(sol1(ts, idxs = M1.y), sol2(ss, idxs = M2.y); atol = 1e-7)) + end end @testset "Change independent variable (Friedmann equation)" begin @@ -102,10 +107,12 @@ end extraeqs = [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)] M3 = change_independent_variable(M2, b, extraeqs) - M1 = mtkcompile(M1) - M2 = mtkcompile(M2; allow_symbolic = true) - M3 = mtkcompile(M3; allow_symbolic = true) - @test length(unknowns(M3)) == length(unknowns(M2)) == length(unknowns(M1)) - 1 + if @isdefined(ModelingToolkit) + M1 = mtkcompile(M1) + M2 = mtkcompile(M2; allow_symbolic = true) + M3 = mtkcompile(M3; allow_symbolic = true) + @test length(unknowns(M3)) == length(unknowns(M2)) == length(unknowns(M1)) - 1 + end end @testset "Change independent variable (simple)" begin @@ -122,13 +129,15 @@ end @parameters g=9.81 v # gravitational acceleration and constant horizontal velocity Mt = System([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) # gives (x, y) as function of t, ... Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x - Mx = mtkcompile(Mx; allow_symbolic = true) - Dx = Differential(Mx.x) - u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0] - p = [v => 10.0] - prob = ODEProblem(Mx, [u0; p], (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities - sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t) ^ 2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + if @isdefined(ModelingToolkit) + Mx = mtkcompile(Mx; allow_symbolic = true) + Dx = Differential(Mx.x) + u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0] + p = [v => 10.0] + prob = ODEProblem(Mx, [u0; p], (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t) ^ 2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + end end @testset "Change independent variable (free fall with 2nd order horizontal equation)" begin @@ -136,12 +145,14 @@ end @parameters g = 9.81 # gravitational acceleration Mt = System([D(D(y)) ~ -g, D(D(x)) ~ 0], t; name = :M) # gives (x, y) as function of t, ... Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x - Mx = mtkcompile(Mx; allow_symbolic = true) - Dx = Differential(Mx.x) - u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0, Mx.xˍt => 10.0] - prob = ODEProblem(Mx, u0, (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities - sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t) ^ 2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + if @isdefined(ModelingToolkit) + Mx = mtkcompile(Mx; allow_symbolic = true) + Dx = Differential(Mx.x) + u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0, Mx.xˍt => 10.0] + prob = ODEProblem(Mx, u0, (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t) ^ 2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + end end @testset "Change independent variable (crazy 3rd order nonlinear system)" begin @@ -154,14 +165,16 @@ end ] M1 = System(eqs, t; name = :M) M2 = change_independent_variable(M1, x; add_old_diff = true) - @test_nowarn mtkcompile(M2) + if @isdefined(ModelingToolkit) + @test_nowarn mtkcompile(M2) + end # Compare to pen-and-paper result @variables x xˍt(x) xˍt(x) y(x) t(x) Dx = Differential(x) areequivalent(eq1, - eq2) = isequal(expand(eq1.lhs - eq2.lhs), 0) && - isequal(expand(eq1.rhs - eq2.rhs), 0) + eq2) = _iszero(simplify(eq1.lhs - eq2.lhs; expand=true)) && + _iszero(simplify(eq1.rhs - eq2.rhs; expand=true)) eq1lhs = xˍt^3 * (Dx^3)(y) + xˍt^2 * Dx(y) * (Dx^2)(xˍt) + xˍt * Dx(y) * (Dx(xˍt))^2 + 3 * xˍt^2 * (Dx^2)(y) * Dx(xˍt) @@ -180,9 +193,10 @@ end @independent_variables t D = Differential(t) @variables x(t) y(t) - @parameters f::LinearInterpolation (fc::LinearInterpolation)(..) # non-callable and callable - callme(interp::LinearInterpolation, input) = interp(input) - @register_symbolic callme(interp::LinearInterpolation, input) + @parameters f::DataInterpolations.AbstractInterpolation{Float64} + @parameters (fc::DataInterpolations.AbstractInterpolation{Float64})(..) # non-callable and callable + callme(interp::DataInterpolations.AbstractInterpolation, input) = interp(input) + @register_symbolic callme(interp::DataInterpolations.AbstractInterpolation, input) eqs = [ D(x) ~ 2t, D(y) ~ 1fc(t) + 2fc(x) + 3fc(y) + 1callme(f, t) + 2callme(f, x) + 3callme(f, y) @@ -202,10 +216,12 @@ end ]) _f = LinearInterpolation([1.0, 1.0], [-100.0, +100.0]) # constant value 1 - M2s = mtkcompile(M2; allow_symbolic = true) - prob = ODEProblem(M2s, [M2s.y => 0.0, fc => _f, f => _f], (1.0, 4.0)) - sol = solve(prob, Tsit5(); abstol = 1e-5) - @test isapprox(sol(4.0, idxs = M2.y), 12.0; atol = 1e-5) # Anal solution is D(y) ~ 12 => y(t) ~ 12*t + C => y(x) ~ 12*√(x) + C. With y(x=1)=0 => 12*(√(x)-1), so y(x=4) ~ 12 + if @isdefined(ModelingToolkit) + M2s = mtkcompile(M2; allow_symbolic = true) + prob = ODEProblem(M2s, [M2s.y => 0.0, fc => _f, f => _f], (1.0, 4.0)) + sol = solve(prob, Tsit5(); abstol = 1e-5) + @test isapprox(sol(4.0, idxs = M2.y), 12.0; atol = 1e-5) # Anal solution is D(y) ~ 12 => y(t) ~ 12*t + C => y(x) ~ 12*√(x) + C. With y(x=1)=0 => 12*(√(x)-1), so y(x=4) ~ 12 + end end @testset "Change independent variable (errors)" begin @@ -227,13 +243,15 @@ end @parameters g=9.81 [unit = u"m * s^-2"] # gravitational acceleration Mt = System([D_units(D_units(y)) ~ -g, D_units(D_units(x)) ~ 0], t_units; name = :M) # gives (x, y) as function of t, ... Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x - Mx = mtkcompile(Mx; allow_symbolic = true) - Dx = Differential(Mx.x) - u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t_units => 0.0, Mx.xˍt_units => 10.0] - prob = ODEProblem(Mx, u0, (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities - sol = solve(prob, Tsit5(); reltol = 1e-5) - # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t_units) ^ 2 / 2]; atol = 1e-10)) + if @isdefined(ModelingToolkit) + Mx = mtkcompile(Mx; allow_symbolic = true) + Dx = Differential(Mx.x) + u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t_units => 0.0, Mx.xˍt_units => 10.0] + prob = ODEProblem(Mx, u0, (0.0, 20.0)) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + sol = solve(prob, Tsit5(); reltol = 1e-5) + # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t_units) ^ 2 / 2]; atol = 1e-10)) + end end @testset "Change independent variable, no equations" begin @@ -284,28 +302,32 @@ end @named sys = ConnectSys() sys = complete(sys; flatten = false) new_sys = change_independent_variable(sys, sys.y; add_old_diff = true) - ss = mtkcompile(new_sys; allow_symbolic = true) - prob = ODEProblem(ss, [ss.t => 0.0, ss.x => 1.0], (0.0, 1.0)) - sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[ss.t], sol[ss.y]; atol = 1e-10)) - @test all(sol[ss.x][2:end] .< sol[ss.x][1]) + if @isdefined(ModelingToolkit) + ss = mtkcompile(new_sys; allow_symbolic = true) + prob = ODEProblem(ss, [ss.t => 0.0, ss.x => 1.0], (0.0, 1.0)) + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test all(isapprox.(sol[ss.t], sol[ss.y]; atol = 1e-10)) + @test all(sol[ss.x][2:end] .< sol[ss.x][1]) + end end @testset "Change independent variable with array variables" begin @variables x(t) y(t) z(t)[1:2] eqs = [ D(x) ~ 2, - z ~ ModelingToolkit.scalarize.([sin(y), cos(y)]), + z ~ ModelingToolkitBase.scalarize.([sin(y), cos(y)]), D(y) ~ z[1]^2 + z[2]^2 ] @named sys = System(eqs, t) sys = complete(sys) new_sys = change_independent_variable(sys, sys.x; add_old_diff = true) - ss_new_sys = mtkcompile(new_sys; allow_symbolic = true) - u0 = [new_sys.y => 0.5, new_sys.t => 0.0] - prob = ODEProblem(ss_new_sys, u0, (0.0, 0.5)) - sol = solve(prob, Tsit5(); reltol = 1e-5) - @test sol[new_sys.y][end] ≈ 0.75 + if @isdefined(ModelingToolkit) + ss_new_sys = mtkcompile(new_sys; allow_symbolic = true) + u0 = [new_sys.y => 0.5, new_sys.t => 0.0] + prob = ODEProblem(ss_new_sys, u0, (0.0, 0.5)) + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test sol[new_sys.y][end] ≈ 0.75 + end end @testset "`add_accumulations`" begin @@ -339,15 +361,17 @@ foofn(x) = 4 @register_symbolic foofn(x::AbstractFoo) @testset "`respecialize`" begin - @parameters p::AbstractFoo p2(t)::AbstractFoo = p q[1:2]::AbstractFoo r + @parameters p::AbstractFoo q[1:2]::AbstractFoo r + @discretes p2(t)::AbstractFoo rp = only(let p = nothing @parameters p::Bar end) rp2 = only(let p2 = nothing - @parameters p2(t)::Baz + @discretes p2(t)::Baz end) @variables x(t) = 1.0 - @named sys1 = System([D(x) ~ foofn(p) + foofn(p2) + x], t, [x], [p, p2, q, r]) + evt = SymbolicContinuousCallback([x ~ 1.0], [x ~ Pre(x)]; discrete_parameters = [p2]) + @named sys1 = System([D(x) ~ foofn(p) + foofn(p2) + x], t, [x], [p, p2, q, r]; initial_conditions = [p2 => p]) @test_throws ["completed systems"] respecialize(sys1) @test_throws ["completed systems"] respecialize(sys1, []) @@ -359,31 +383,31 @@ foofn(x) = 4 @test_throws ["Parameter p", "associated value"] respecialize(sys) @test_throws ["Parameter p", "associated value"] respecialize(sys, [p]) - @test_throws ["Parameter p2", "symbolic default"] respecialize(sys, [p2]) + @test_throws ["Parameter p2", "symbolic initial value"] respecialize(sys, [p2]) sys2 = respecialize(sys, [p => Bar()]) - @test ModelingToolkit.iscomplete(sys2) - @test ModelingToolkit.is_split(sys2) - ps = ModelingToolkit.get_ps(sys2) + @test ModelingToolkitBase.iscomplete(sys2) + @test ModelingToolkitBase.is_split(sys2) + ps = ModelingToolkitBase.get_ps(sys2) idx = findfirst(isequal(rp), ps) - @test defaults(sys2)[rp] == Bar() + @test value(initial_conditions(sys2)[rp]) == Bar() @test symtype(ps[idx]) <: Bar - ic = ModelingToolkit.get_index_cache(sys2) + ic = ModelingToolkitBase.get_index_cache(sys2) @test any(x -> x.type == Bar && x.length == 1, ic.nonnumeric_buffer_sizes) prob = ODEProblem(sys2, [p2 => Bar(), q => [Bar(), Bar()], r => 1], (0.0, 1.0)) @test any(x -> x isa Vector{Bar} && length(x) == 1, prob.p.nonnumeric) - defaults(sys)[p2] = Baz() + initial_conditions(sys)[p2] = Baz() sys2 = respecialize(sys, [p => Bar()]; all = true) - @test ModelingToolkit.iscomplete(sys2) - @test ModelingToolkit.is_split(sys2) - ps = ModelingToolkit.get_ps(sys2) + @test ModelingToolkitBase.iscomplete(sys2) + @test ModelingToolkitBase.is_split(sys2) + ps = ModelingToolkitBase.get_ps(sys2) idx = findfirst(isequal(rp2), ps) - @test defaults(sys2)[rp2] == Baz() + @test value(initial_conditions(sys2)[rp2]) == Baz() @test symtype(ps[idx]) <: Baz - ic = ModelingToolkit.get_index_cache(sys2) + ic = ModelingToolkitBase.get_index_cache(sys2) @test any(x -> x.type == Baz && x.length == 1, ic.nonnumeric_buffer_sizes) - delete!(defaults(sys), p2) + delete!(initial_conditions(sys), p2) prob = ODEProblem(sys2, [q => [Bar(), Bar()], r => 1], (0.0, 1.0)) @test any(x -> x isa Vector{Bar} && length(x) == 1, prob.p.nonnumeric) @test any(x -> x isa Vector{Baz} && length(x) == 1, prob.p.nonnumeric) diff --git a/test/bigsystem.jl b/lib/ModelingToolkitBase/test/bigsystem.jl similarity index 88% rename from test/bigsystem.jl rename to lib/ModelingToolkitBase/test/bigsystem.jl index 612e96c418..1b04374bd0 100644 --- a/test/bigsystem.jl +++ b/lib/ModelingToolkitBase/test/bigsystem.jl @@ -1,6 +1,7 @@ -using ModelingToolkit, LinearAlgebra, SparseArrays +using ModelingToolkitBase, LinearAlgebra, SparseArrays using Symbolics using Symbolics: scalarize +using Test # Define the constants for the PDE const α₂ = 1.0 @@ -51,8 +52,8 @@ end f(du, u, nothing, 0.0) -multithreadedf = eval(ModelingToolkit.build_function(du, u, fillzeros = true, - parallel = ModelingToolkit.MultithreadedForm())[2]) +multithreadedf = eval(ModelingToolkitBase.build_function(du, u, fillzeros = true, + parallel = ModelingToolkitBase.MultithreadedForm())[2]) MyA = zeros(N, N); AMx = zeros(N, N); @@ -86,8 +87,8 @@ FiniteDiff.finite_difference_jacobian!(J2,(du,u)->f!(du,u,nothing,nothing),u) maximum(J2 .- Array(J)) < 1e-5 =# -jac = ModelingToolkit.sparsejacobian(vec(du), vec(u)) -serialjac = eval(ModelingToolkit.build_function(vec(jac), u)[2]) +jac = ModelingToolkitBase.sparsejacobian(vec(du), vec(u)) +serialjac = eval(ModelingToolkitBase.build_function(vec(jac), u)[2]) #multithreadedjac = eval(ModelingToolkit.build_function(vec(jac), u, # parallel = ModelingToolkit.MultithreadedForm())[2]) diff --git a/lib/ModelingToolkitBase/test/binding_semantics.jl b/lib/ModelingToolkitBase/test/binding_semantics.jl new file mode 100644 index 0000000000..a6f1230aab --- /dev/null +++ b/lib/ModelingToolkitBase/test/binding_semantics.jl @@ -0,0 +1,112 @@ +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D, SymbolicContinuousCallback, SymbolicDiscreteCallback +using Symbolics: SConst +using Test + +@testset "Simple metadata bindings" begin + @variables x(t) = 1 y(t) = x + @parameters p = 1 q = p + @named sys = System([D(x) ~ p * x, D(y) ~ q * y], t) + @test isequal(bindings(sys), Dict(y => x, q => p)) + @test isequal(initial_conditions(sys), Dict(x => SConst(1), p => SConst(1))) +end + +@testset "Array bindings" begin + @variables x(t)[1:2] = [1.0, 2.0] y(t)[1:2] = [x[1], 2.0] + @parameters p[1:2] = [2.0, 3.0] q[1:2] = [p[1], 4.0] + @named sys = System(Equation[], t, [x, y], [p, q]) + @test isequal(bindings(sys), Dict(y => SConst([x[1], 2.0]), q => SConst([p[1], 4.0]))) + @test isequal(initial_conditions(sys), Dict(x => SConst([1.0, 2.0]), p => SConst([2.0, 3.0]))) +end + +@testset "Prefer keyword over metadata" begin + @variables x(t) = 1 y(t) = x + @parameters p = 1 q = p + @named sys = System([D(x) ~ p * x, D(y) ~ q * y], t; + bindings = [x => y, p => q, y => nothing, q => nothing], + initial_conditions = [y => 2, q => 2, x => nothing, p => nothing]) + @test isequal(bindings(sys), Dict(x => y, p => q)) + @test isequal(initial_conditions(sys), Dict(y => SConst(2), q => SConst(2))) +end + +@testset "Arrays are atomic" begin + @variables x(t)[1:2] + @test_throws ModelingToolkitBase.IndexedArrayKeyError System(D(x) ~ x, t; bindings = [x[1] => x[2]], name = :a) + @test_throws ModelingToolkitBase.IndexedArrayKeyError System(D(x) ~ x, t; initial_conditions = [x[1] => 1], name = :a) + @test_throws ModelingToolkitBase.IndexedArrayKeyError System(D(x) ~ x, t; guesses = [x[1] => 1], name = :a) +end + +@testset "Parameter bindings cannot involve variables" begin + @variables x(t) + @parameters p = 2x + 1 + @test_throws ["parameter p", "encountered binding", "non-parameter"] System(D(x) ~ x, t, [x], [p]; name = :a) +end + +@testset "Discrete unknowns eventually become parameters" begin + @variables x(t) + @discretes d1(t) = x d2(t) = d1 + cev = SymbolicContinuousCallback([x ~ 1.0], [d1 ~ Pre(x)]; discrete_parameters = [d1]) + dev = SymbolicDiscreteCallback(1.0, [d2 ~ Pre(x)]; discrete_parameters = [d2]) + @named sys = System([D(x) ~ x], t, [x, d1, d2], []; continuous_events = [cev], discrete_events = [dev]) + @test d1 in Set(unknowns(sys)) + @test d2 in Set(unknowns(sys)) + csys = ModelingToolkitBase.discrete_unknowns_to_parameters(sys) + @test d1 in Set(parameters(csys)) + @test d2 in Set(parameters(csys)) + if @isdefined(ModelingToolkit) + ts = TearingState(sys) + ssys = ts.sys + @test d1 in Set(parameters(ssys)) + @test d2 in Set(parameters(ssys)) + end +end + +@testset "`missing` bindings are bindings and not initial conditions" begin + @variables x(t) + @parameters p = missing + @named sys = System(D(x) ~ x * p, t) + @test bindings(sys)[p] === ModelingToolkitBase.COMMON_MISSING +end + +function SubComp(; k, name) + @parameters k = k + System(Equation[], t, [], [k]; name) +end + +function Comp(; name) + @parameters k + @variables x(t) + @named sub = SubComp(; k) + System([D(x) ~ sub.k * x + k], t; systems = [sub], name) +end + +function BadComp(; name) + @parameters k + @variables x(t) + @named sub = SubComp(; k = x) + System([D(x) ~ sub.k * x + k], t; systems = [sub], name) +end + + +@testset "Parameter can be bound to parameter in parent system" begin + @named sys = Comp() + nnsys = toggle_namespacing(sys, false) + @test issetequal(ModelingToolkitBase.get_ps(sys), [nnsys.k, nnsys.sub.k]) + @test isempty(ModelingToolkitBase.get_bindings(sys)) + @test isequal(bindings(sys)[nnsys.sub.k], nnsys.k) + csys = complete(sys) + @test issetequal(parameters(csys), [nnsys.k]) + @test issetequal(bound_parameters(csys), [nnsys.sub.k]) + + @named sys = BadComp() + nnsys = toggle_namespacing(sys, false) + @test issetequal(ModelingToolkitBase.get_ps(sys), [nnsys.k, nnsys.sub.k]) + @test_throws ["Bindings", "functions of other parameters"] complete(sys) +end + +@testset "Cannot have `missing` bindings for non-floating-point parameters" begin + @parameters p::Int q::String + @test_throws "only valid for solvable" System(Equation[], t, [], [p]; bindings = [p => missing], name = :a) + @parameters p::Int q::String + @test_throws "only valid for solvable" System(Equation[], t, [], [q]; bindings = [q => missing], name = :a) +end diff --git a/test/bvproblem.jl b/lib/ModelingToolkitBase/test/bvproblem.jl similarity index 97% rename from test/bvproblem.jl rename to lib/ModelingToolkitBase/test/bvproblem.jl index d86ba251c7..2798e98693 100644 --- a/test/bvproblem.jl +++ b/lib/ModelingToolkitBase/test/bvproblem.jl @@ -1,9 +1,10 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions using OrdinaryDiffEq using BoundaryValueDiffEqMIRK, BoundaryValueDiffEqAscher -using ModelingToolkit +using ModelingToolkitBase using SciMLBase -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D +using Test ### Test Collocation solvers on simple problems solvers = [MIRK4] @@ -279,7 +280,7 @@ end @testset "Cost function compilation" begin @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) - t = ModelingToolkit.t_nounits + t = ModelingToolkitBase.t_nounits eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] @@ -291,11 +292,11 @@ end consolidate(u, sub) = (u[1] + 3)^2 + u[2] + sum(sub; init = 0) @mtkcompile lksys = System(eqs, t; costs, consolidate) - @test_throws ModelingToolkit.SystemCompatibilityError ODEProblem( + @test_throws ModelingToolkitBase.SystemCompatibilityError ODEProblem( lksys, [u0map; parammap], tspan) prob = ODEProblem(lksys, [u0map; parammap], tspan; check_compatibility = false) sol = solve(prob, Tsit5()) - costfn = ModelingToolkit.generate_cost( + costfn = ModelingToolkitBase.generate_cost( lksys; expression = Val{false}, wrap_gfw = Val{true}) _t = tspan[2] @test costfn(sol, prob.p, _t) ≈ (sol(0.6; idxs = x(t)) + 3)^2 + sol(0.3; idxs = x(t))^2 @@ -309,7 +310,7 @@ end push!(parammap, t_c => 0.56) prob = ODEProblem(lksys, [u0map; parammap], tspan; check_compatibility = false) sol = solve(prob, Tsit5()) - costfn = ModelingToolkit.generate_cost( + costfn = ModelingToolkitBase.generate_cost( lksys; expression = Val{false}, wrap_gfw = Val{true}) @test costfn(sol, prob.p, _t) ≈ log(sol(0.56; idxs = y(t)) + sol(0.0; idxs = x(t))) - sol(0.4; idxs = x(t))^2 diff --git a/test/causal_variables_connection.jl b/lib/ModelingToolkitBase/test/causal_variables_connection.jl similarity index 52% rename from test/causal_variables_connection.jl rename to lib/ModelingToolkitBase/test/causal_variables_connection.jl index 3124ac2f3b..731f5294ee 100644 --- a/test/causal_variables_connection.jl +++ b/lib/ModelingToolkitBase/test/causal_variables_connection.jl @@ -1,5 +1,7 @@ -using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, ModelingToolkitStandardLibrary.Blocks +using SciCompDSL +using ModelingToolkitBase: t_nounits as t, D_nounits as D +using Test @testset "Error checking" begin @variables begin @@ -45,49 +47,51 @@ end @test any(isequal(sys1.P.input.u ~ sys1.C.output.u), equations(sys)) end -@testset "With Analysis Points" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = Gain(; k = -1) - - ap = AnalysisPoint(:plant_input) - eqs = [connect(P.output, C.input), connect(C.output.u, ap, P.input.u)] - sys = System(eqs, t, systems = [P, C], name = :hej) - @named nested_sys = System(Equation[], t; systems = [sys]) - - test_cases = [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - Symbol", sys, :plant_input), - ("nested - Symbol", nested_sys, nameof(sys.plant_input)) - ] - - @testset "get_sensitivity - $name" for (name, sys, ap) in test_cases - matrices, _ = get_sensitivity(sys, ap) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 1 - end +if @isdefined(ModelingToolkit) + @testset "With Analysis Points" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + eqs = [connect(P.output, C.input), connect(C.output.u, ap, P.input.u)] + sys = System(eqs, t, systems = [P, C], name = :hej) + @named nested_sys = System(Equation[], t; systems = [sys]) + + test_cases = [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) + ] + + @testset "get_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 + end - @testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases - matrices, _ = get_comp_sensitivity(sys, ap) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == 1 # both positive or negative - @test matrices.D[] == 0 - end + @testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_comp_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive or negative + @test matrices.D[] == 0 + end - @testset "get_looptransfer - $name" for (name, sys, ap) in test_cases - matrices, _ = get_looptransfer(sys, ap) - @test matrices.A[] == -1 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 0 - end + @testset "get_looptransfer - $name" for (name, sys, ap) in test_cases + matrices, _ = get_looptransfer(sys, ap) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end - @testset "open_loop - $name" for (name, sys, ap) in test_cases - open_sys, (du, u) = open_loop(sys, ap) - matrices, _ = linearize(open_sys, [du], [u]) - @test matrices.A[] == -1 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 0 + @testset "open_loop - $name" for (name, sys, ap) in test_cases + open_sys, (du, u) = open_loop(sys, ap) + matrices, _ = linearize(open_sys, [du], [u]) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end end end diff --git a/test/ccompile.jl b/lib/ModelingToolkitBase/test/ccompile.jl similarity index 72% rename from test/ccompile.jl rename to lib/ModelingToolkitBase/test/ccompile.jl index 4572119ab8..f2bc124ebe 100644 --- a/test/ccompile.jl +++ b/lib/ModelingToolkitBase/test/ccompile.jl @@ -1,12 +1,12 @@ -using ModelingToolkit, Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters a @variables x y eqs = [D(x) ~ a * x - x * y, D(y) ~ -3y + x * y] f = build_function([x.rhs for x in eqs], [x, y], [a], t, expression = Val{false}, - target = ModelingToolkit.CTarget()) + target = ModelingToolkitBase.CTarget()) f2 = eval(build_function([x.rhs for x in eqs], [x, y], [a], t)[2]) du = rand(2); du2 = rand(2); diff --git a/test/changeofvariables.jl b/lib/ModelingToolkitBase/test/changeofvariables.jl similarity index 68% rename from test/changeofvariables.jl rename to lib/ModelingToolkitBase/test/changeofvariables.jl index 08f69cb16e..bb96dd1b7b 100644 --- a/test/changeofvariables.jl +++ b/lib/ModelingToolkitBase/test/changeofvariables.jl @@ -1,5 +1,8 @@ -using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq +using ModelingToolkitBase, OrdinaryDiffEq, StochasticDiffEq using Test, LinearAlgebra +import DiffEqNoiseProcess + +common_alg = @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P() # Change of variables: z = log(x) # (this implies that x = exp(z) is automatically non-negative) @@ -18,7 +21,7 @@ eqs = [D(x) ~ α*x] tspan = (0.0, 1.0) def = [x => 1.0, α => -0.5] -@mtkcompile sys = System(eqs, t; defaults = def) +@mtkcompile sys = System(eqs, t; initial_conditions = def) prob = ODEProblem(sys, [], tspan) sol = solve(prob, Tsit5()) @@ -29,7 +32,7 @@ new_sys = change_of_variables(sys, t, forward_subs, backward_subs) @test equations(new_sys)[1] == (D(z) ~ α) new_prob = ODEProblem(new_sys, [], tspan) -new_sol = solve(new_prob, Tsit5()) +new_sol = solve(new_prob, common_alg) @test isapprox(new_sol[x][end], sol[x][end], atol = 1e-4) @@ -39,7 +42,7 @@ new_sol = solve(new_prob, Tsit5()) D = Differential(t) eqs = [D(x) ~ t^2 + α - x^2] def = [x=>1.0, α => 1.0] -@mtkcompile sys = System(eqs, t; defaults = def) +@mtkcompile sys = System(eqs, t; initial_conditions = def) @variables z(t) forward_subs = [t + α/(x+t) => z] @@ -55,23 +58,22 @@ prob = ODEProblem(sys, [], tspan) new_prob = ODEProblem(new_sys, [], tspan) sol = solve(prob, Tsit5()) -new_sol = solve(new_prob, Tsit5()) +new_sol = solve(new_prob, common_alg) @test isapprox(sol[x][end], new_sol[x][end], rtol = 1e-4) # Linear transformation to diagonal system @independent_variables t @variables x(t)[1:3] -x = reshape(x, 3, 1) D = Differential(t) A = [0.0 -1.0 0.0; -0.5 0.5 0.0; 0.0 0.0 -1.0] right = A*x -eqs = vec(D.(x) .~ right) +eqs = [D(x) ~ right] tspan = (0.0, 10.0) -u0 = [x[1] => 1.0, x[2] => 2.0, x[3] => -1.0] +u0 = [x => [1.0, 2.0, -1.0]] -@mtkcompile sys = System(eqs, t; defaults = u0) +@mtkcompile sys = System(eqs, t; initial_conditions = u0) prob = ODEProblem(sys, [], tspan) sol = solve(prob, Tsit5()) @@ -80,26 +82,28 @@ T_inv = inv(T) @variables z(t)[1:3] z = reshape(z, 3, 1) -forward_subs = vec(T_inv*x .=> z) -backward_subs = vec(x .=> T*z) +forward_subs = vec(collect(T_inv*x) .=> collect(z)) +backward_subs = vec(collect(x) .=> collect(T*z)) new_sys = change_of_variables(sys, t, forward_subs, backward_subs; simplify = true) new_prob = ODEProblem(new_sys, [], tspan) -new_sol = solve(new_prob, Tsit5()) +new_sol = solve(new_prob, common_alg) # test RHS -new_rhs = [eq.rhs for eq in equations(new_sys)] -new_A = Symbolics.value.(Symbolics.jacobian(new_rhs, z)) -A = diagm(eigen(A).values) -A = sortslices(A, dims = 1) -new_A = sortslices(new_A, dims = 1) -@test isapprox(A, new_A, rtol = 1e-10) +if @isdefined(ModelingToolkit) + new_rhs = [eq.rhs for eq in equations(new_sys)] + new_A = Symbolics.value.(Symbolics.jacobian(new_rhs, z)) + A = diagm(eigen(A).values) + A = sortslices(A, dims = 1) + new_A = sortslices(new_A, dims = 1) + @test isapprox(A, new_A, rtol = 1e-10) +end @test isapprox(new_sol[x[1], end], sol[x[1], end], rtol = 1e-4) # Change of variables for sde -noise_eqs = ModelingToolkit.get_noise_eqs -value = ModelingToolkit.value +noise_eqs = ModelingToolkitBase.get_noise_eqs +value = ModelingToolkitBase.value @independent_variables t @brownians B @@ -109,7 +113,7 @@ D = Differential(t) eqs = [D(x) ~ μ*x + σ*x*B] def = [x=>0.0, μ => 2.0, σ=>1.0] -@mtkcompile sys = System(eqs, t; defaults = def) +@mtkcompile sys = System(eqs, t; initial_conditions = def) forward_subs = [log(x) => y] backward_subs = [x => exp(y)] new_sys = change_of_variables(sys, t, forward_subs, backward_subs) @@ -127,20 +131,22 @@ def = [x=>0.0, y => 0.0, u=>0.0, μ => 2.0, σ=>1.0, α=>3.0] forward_subs = [log(x) => z, y^2 => w, log(u) => v] backward_subs = [x => exp(z), y => w^0.5, u => exp(v)] -@mtkcompile sys = System(eqs, t; defaults = def) +@mtkcompile sys = System(eqs, t; initial_conditions = def) new_sys = change_of_variables(sys, t, forward_subs, backward_subs) @test equations(new_sys)[1] == (D(z) ~ μ - 1/2*σ^2) @test equations(new_sys)[2] == (D(w) ~ α^2) @test equations(new_sys)[3] == (D(v) ~ μ - 1/2*(α^2 + σ^2)) -@test noise_eqs(new_sys)[1, 1] === value(σ) -@test noise_eqs(new_sys)[1, 2] === value(0) -@test noise_eqs(new_sys)[2, 1] === value(0) -@test noise_eqs(new_sys)[2, 2] === value(simplify(substitute(2*α*y, backward_subs[2]))) -@test noise_eqs(new_sys)[3, 1] === value(σ) -@test noise_eqs(new_sys)[3, 2] === value(α) +col1 = @isdefined(ModelingToolkit) ? 1 : 2 +col2 = 3 - col1 +@test value(noise_eqs(new_sys)[1, col1]) === value(σ) +@test value(noise_eqs(new_sys)[1, col2]) === value(0) +@test value(noise_eqs(new_sys)[2, col1]) === value(0) +@test isequal(noise_eqs(new_sys)[2, col2], simplify(substitute(2*α*y, backward_subs[2]))) +@test value(noise_eqs(new_sys)[3, col1]) === value(σ) +@test value(noise_eqs(new_sys)[3, col2]) === value(α) # Test for Brownian instead of noise -@named sys = System(eqs, t; defaults = def) +@named sys = System(eqs, t; initial_conditions = def) new_sys = change_of_variables(sys, t, forward_subs, backward_subs; simplify = false) @test simplify(equations(new_sys)[1]) == simplify((D(z) ~ μ - 1/2*σ^2 + σ*Bx)) @test simplify(equations(new_sys)[2]) == simplify((D(w) ~ α^2 + 2*α*w^0.5*By)) diff --git a/test/code_generation.jl b/lib/ModelingToolkitBase/test/code_generation.jl similarity index 79% rename from test/code_generation.jl rename to lib/ModelingToolkitBase/test/code_generation.jl index 5fb1edb3c2..a607827d39 100644 --- a/test/code_generation.jl +++ b/lib/ModelingToolkitBase/test/code_generation.jl @@ -1,5 +1,6 @@ -using ModelingToolkit, OrdinaryDiffEq, SymbolicIndexingInterface -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, OrdinaryDiffEq, SymbolicIndexingInterface +using ModelingToolkitBase: t_nounits as t, D_nounits as D +using Test @testset "`generate_custom_function`" begin @variables x(t) y(t)[1:3] @@ -7,7 +8,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D sys = complete(System(Equation[], t, [x; y], [p1, p2, p3, p4]; name = :sys)) u0 = [1.0, 2.0, 3.0, 4.0] - p = ModelingToolkit.MTKParameters(sys, []) + p = ModelingToolkitBase.MTKParameters(sys, []) fn1 = generate_custom_function( sys, x + y[1] + p1 + p2[1] + p3 * t; expression = Val(false)) @@ -66,20 +67,6 @@ end @test prob.ps[p[0]] == 1.0 sol = solve(prob, Tsit5()) @test SciMLBase.successful_retcode(sol) - - @testset "Array split across buffers" begin - @variables x(t)[0:2] - @parameters p[1:2] (f::Function)(..) - @named sys = System( - [D(x[0]) ~ p[1] * x[0] + x[2], D(x[1]) ~ p[2] * f(x) + x[2]], t) - sys = mtkcompile(sys, inputs = [x[2]], outputs = []) - @test is_parameter(sys, x[2]) - prob = ODEProblem( - sys, [x[0] => 1.0, x[1] => 1.0, x[2] => 2.0, p => ones(2), f => sum], - (0.0, 1.0)) - sol = solve(prob, Tsit5()) - @test SciMLBase.successful_retcode(sol) - end end @testset "scalarized array observed calling same function multiple times" begin @@ -91,8 +78,12 @@ end return [x, 2x] end @mtkcompile sys = System([D(x) ~ y[1] + y[2], y ~ foo(x)], t) - @test length(equations(sys)) == 1 - @test length(ModelingToolkit.observed(sys)) == 3 + if @isdefined(ModelingToolkit) + @test length(equations(sys)) == 1 + @test length(ModelingToolkitBase.observed(sys)) == 3 + else + @test length(equations(sys)) == 3 + end prob = ODEProblem(sys, [x => 1.0, foo => _tmp_fn2], (0.0, 1.0)) val[] = 0 @test_nowarn prob.f(prob.u0, prob.p, 0.0) @@ -104,7 +95,7 @@ end @mtkcompile sys = System( [D(y) ~ foo(x), D(x) ~ sum(y), zeros(2) ~ foo(prod(z))], t) @test length(equations(sys)) == 5 - @test length(ModelingToolkit.observed(sys)) == 0 + @test length(ModelingToolkitBase.observed(sys)) == 0 prob = ODEProblem( sys, [y => ones(2), z => 2ones(2), x => 3.0, foo => _tmp_fn2], (0.0, 1.0)) val[] = 0 @@ -133,12 +124,16 @@ end eqs = [ D(v1) ~ d1(t), - v2 ~ d2(t) # Some of the data parameters are not actually needed to solve the system. ] - @mtkbuild sys = System(eqs, t) + @named sys = System(eqs, t, [v1], [d1, d2]; observed = [v2 ~ d2(t)]) + sys = complete(sys) prob = ODEProblem(sys, [], (0.0, 1.0)) - sol = solve(prob, Tsit5()) - + # Manual solve because lack of tearing in MTKBase will cause `d2` to be called + # when solving initialization. + integ = init(prob, Tsit5()) + integ.ps[d2].count = 0 + solve!(integ) + sol = integ.sol @test sol.ps[d2].count == 0 end diff --git a/test/common/rc_model.jl b/lib/ModelingToolkitBase/test/common/rc_model.jl similarity index 97% rename from test/common/rc_model.jl rename to lib/ModelingToolkitBase/test/common/rc_model.jl index 8eec8d048f..5258409dbd 100644 --- a/test/common/rc_model.jl +++ b/lib/ModelingToolkitBase/test/common/rc_model.jl @@ -1,5 +1,6 @@ import ModelingToolkitStandardLibrary.Electrical as El import ModelingToolkitStandardLibrary.Blocks as Bl +using SciCompDSL @mtkmodel RCModel begin @parameters begin diff --git a/test/common/serial_inductor.jl b/lib/ModelingToolkitBase/test/common/serial_inductor.jl similarity index 98% rename from test/common/serial_inductor.jl rename to lib/ModelingToolkitBase/test/common/serial_inductor.jl index 1e930cee56..34d87c2f9c 100644 --- a/test/common/serial_inductor.jl +++ b/lib/ModelingToolkitBase/test/common/serial_inductor.jl @@ -1,5 +1,6 @@ import ModelingToolkitStandardLibrary.Electrical as El import ModelingToolkitStandardLibrary.Blocks as Bl +using SciCompDSL @mtkmodel LLModel begin @components begin diff --git a/test/complex.jl b/lib/ModelingToolkitBase/test/complex.jl similarity index 79% rename from test/complex.jl rename to lib/ModelingToolkitBase/test/complex.jl index e30ebb177e..b784a7ede1 100644 --- a/test/complex.jl +++ b/lib/ModelingToolkitBase/test/complex.jl @@ -1,12 +1,13 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t +using SciCompDSL using Test @mtkmodel ComplexModel begin @variables begin x(t) y(t) - z(t)::Complex + z(t)::Complex{Real} end @equations begin z ~ x + im * y @@ -16,7 +17,7 @@ end @test length(equations(mixed)) == 2 @testset "Complex ODEProblem" begin - using ModelingToolkit: t_nounits as t, D_nounits as D + using ModelingToolkitBase: t_nounits as t, D_nounits as D vars = @variables x(t) y(t) z(t) pars = @parameters a b @@ -29,7 +30,7 @@ end @named modlorenz = System(eqs, t) sys = mtkcompile(modlorenz) - ic = ModelingToolkit.get_index_cache(sys) + ic = ModelingToolkitBase.get_index_cache(sys) @test ic.tunable_buffer_size.type == Number u0 = ComplexF64[-4.0, 5.0, 0.0] .+ randn(ComplexF64, 3) diff --git a/test/components.jl b/lib/ModelingToolkitBase/test/components.jl similarity index 60% rename from test/components.jl rename to lib/ModelingToolkitBase/test/components.jl index 53df8658d1..6075f2f90d 100644 --- a/test/components.jl +++ b/lib/ModelingToolkitBase/test/components.jl @@ -1,28 +1,29 @@ using Test -using ModelingToolkit, OrdinaryDiffEq -using ModelingToolkit: get_component_type -using ModelingToolkit.BipartiteGraphs -using ModelingToolkit.StructuralTransformations -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, OrdinaryDiffEq +using ModelingToolkitBase: get_component_type, complete +using ModelingToolkitBase: t_nounits as t, D_nounits as D, value using ModelingToolkitStandardLibrary.Electrical using ModelingToolkitStandardLibrary.Blocks using LinearAlgebra using ModelingToolkitStandardLibrary.Thermal using SymbolicUtils: getmetadata +using BipartiteGraphs +import SymbolicUtils as SU +using SciCompDSL include("common/rc_model.jl") @testset "Basics" begin @unpack resistor, capacitor, source = rc_model function check_contract(sys) - state = ModelingToolkit.get_tearing_state(sys) + state = ModelingToolkitBase.get_tearing_state(sys) graph = state.structure.graph fullvars = state.fullvars sys = tearing_substitution(sys) eqs = equations(sys) for (i, eq) in enumerate(eqs) - actual = union(ModelingToolkit.vars(eq.lhs), ModelingToolkit.vars(eq.rhs)) - actual = filter(!ModelingToolkit.isparameter, collect(actual)) + actual = union(SU.search_variables(eq.lhs), SU.search_variables(eq.rhs)) + actual = filter(!ModelingToolkitBase.isparameter, collect(actual)) current = Set(fullvars[𝑠neighbors(graph, i)]) @test isempty(setdiff(actual, current)) end @@ -33,13 +34,26 @@ include("common/rc_model.jl") rpifun = sol.prob.f.observed(rc_model.resistor.p.i) @test rpifun.(sol.u, (sol.prob.p,), sol.t) == rpi @test any(!isequal(rpi[1]), rpi) # test that we don't have a constant system - @test sol[rc_model.resistor.p.i] == sol[resistor.p.i] == sol[capacitor.p.i] - @test sol[rc_model.resistor.n.i] == sol[resistor.n.i] == -sol[capacitor.p.i] - @test sol[rc_model.capacitor.n.i] == sol[capacitor.n.i] == -sol[capacitor.p.i] - @test iszero(sol[rc_model.ground.g.i]) - @test iszero(sol[rc_model.ground.g.v]) - @test sol[rc_model.resistor.v] == sol[resistor.v] == - sol[source.p.v] - sol[capacitor.p.v] + if @isdefined(ModelingToolkit) + @test sol[rc_model.resistor.p.i] == sol[resistor.p.i] == sol[capacitor.p.i] + @test sol[rc_model.resistor.n.i] == sol[resistor.n.i] == -sol[capacitor.p.i] + @test sol[rc_model.capacitor.n.i] == sol[capacitor.n.i] == -sol[capacitor.p.i] + @test iszero(sol[rc_model.ground.g.i]) + @test iszero(sol[rc_model.ground.g.v]) + @test sol[rc_model.resistor.v] == sol[resistor.v] == + sol[source.p.v] - sol[capacitor.p.v] + else + @test sol[rc_model.resistor.p.i] ≈ sol[resistor.p.i] atol=1e-6 + @test sol[rc_model.resistor.p.i] ≈ sol[capacitor.p.i] atol=1e-6 + @test sol[rc_model.resistor.n.i] ≈ sol[resistor.n.i] atol=1e-6 + @test sol[rc_model.resistor.n.i] ≈ -sol[capacitor.p.i] atol=1e-6 + @test sol[rc_model.capacitor.n.i] ≈ sol[capacitor.n.i] atol=1e-6 + @test sol[rc_model.capacitor.n.i] ≈ -sol[capacitor.p.i] atol=1e-6 + @test sol[rc_model.ground.g.i] ≈ zeros(length(sol.t)) atol=1e-6 + @test sol[rc_model.ground.g.v] ≈ zeros(length(sol.t)) atol=1e-6 + @test sol[rc_model.resistor.v] == sol[resistor.v] + @test sol[rc_model.resistor.v] ≈ sol[source.p.v] - sol[capacitor.p.v] + end end @named pin = Pin() @@ -52,13 +66,17 @@ include("common/rc_model.jl") completed_rc_model = complete(rc_model) @test isequal(completed_rc_model.resistor.n.i, resistor.n.i) - @test ModelingToolkit.n_expanded_connection_equations(capacitor) == 2 - @test length(equations(mtkcompile(rc_model, allow_parameter = false))) == 2 + @test ModelingToolkitBase.n_expanded_connection_equations(capacitor) == 2 + if @isdefined(ModelingToolkit) + @test length(equations(mtkcompile(rc_model, allow_parameter = false))) == 2 + end sys = mtkcompile(rc_model) - @test_throws ModelingToolkit.RepeatedStructuralSimplificationError mtkcompile(sys) - @test length(equations(sys)) == 1 - check_contract(sys) - @test !isempty(ModelingToolkit.defaults(sys)) + @test_throws ModelingToolkitBase.RepeatedStructuralSimplificationError mtkcompile(sys) + if @isdefined(ModelingToolkit) + @test length(equations(sys)) == 1 + check_contract(sys) + end + @test !isempty(ModelingToolkitBase.bindings(sys)) u0 = [capacitor.v => 0.0] prob = ODEProblem(sys, u0, (0, 10.0)) sol = solve(prob, Rodas4()) @@ -70,6 +88,7 @@ end prob = ODEProblem(sys, [sys.capacitor.v => 0.0], (0.0, 10.0)) sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) function rc_component(; name, R = 1, C = 1) local sys @parameters R=R C=C @@ -97,42 +116,47 @@ end @test_nowarn show(IOBuffer(), MIME"text/plain"(), sys_inner_outer) expand_connections(sys_inner_outer) sys_inner_outer = mtkcompile(sys_inner_outer) - @test !isempty(ModelingToolkit.defaults(sys_inner_outer)) + @test !isempty(ModelingToolkitBase.bindings(sys_inner_outer)) u0 = [rc_comp.capacitor.v => 0.0] prob = ODEProblem(sys_inner_outer, u0, (0, 10.0), sparse = true) sol_inner_outer = solve(prob, Rodas4()) - @test sol[sys.capacitor.v] ≈ sol_inner_outer[rc_comp.capacitor.v] - - prob = ODEProblem(sys, [sys.capacitor.v => 0.0], (0, 10.0)) - sol = solve(prob, Tsit5()) - - @test sol[sys.resistor.p.i] == sol[sys.capacitor.p.i] - @test sol[sys.resistor.n.i] == -sol[sys.capacitor.p.i] - @test sol[sys.capacitor.n.i] == -sol[sys.capacitor.p.i] - @test iszero(sol[sys.ground.g.i]) - @test iszero(sol[sys.ground.g.v]) - @test sol[sys.resistor.v] == sol[sys.source.p.v] - sol[sys.capacitor.p.v] + @test SciMLBase.successful_retcode(sol_inner_outer) + if @isdefined(ModelingToolkit) + @test sol[sys.capacitor.v] ≈ sol_inner_outer[rc_comp.capacitor.v] + + prob = ODEProblem(sys, [sys.capacitor.v => 0.0], (0, 10.0)) + sol = solve(prob, Tsit5()) + + @test sol[sys.resistor.p.i] == sol[sys.capacitor.p.i] + @test sol[sys.resistor.n.i] == -sol[sys.capacitor.p.i] + @test sol[sys.capacitor.n.i] == -sol[sys.capacitor.p.i] + @test iszero(sol[sys.ground.g.i]) + @test iszero(sol[sys.ground.g.v]) + @test sol[sys.resistor.v] == sol[sys.source.p.v] - sol[sys.capacitor.p.v] + end end #using Plots #plot(sol) include("common/serial_inductor.jl") -@testset "Serial inductor" begin - sys = mtkcompile(ll_model) - @test length(equations(sys)) == 2 - u0 = unknowns(sys) .=> 0 - @test_nowarn ODEProblem( - sys, [], (0, 10.0), guesses = u0, warn_initialize_determined = false) - prob = DAEProblem(sys, D.(unknowns(sys)) .=> 0, (0, 0.5), guesses = u0) - sol = solve(prob, DFBDF()) - @test sol.retcode == SciMLBase.ReturnCode.Success - - sys2 = mtkcompile(ll2_model) - @test length(equations(sys2)) == 3 - u0 = [sys.inductor2.i => 0] - prob = ODEProblem(sys, u0, (0, 10.0)) - sol = solve(prob, FBDF()) - @test SciMLBase.successful_retcode(sol) +if @isdefined(ModelingToolkit) + @testset "Serial inductor" begin + sys = mtkcompile(ll_model) + @test length(equations(sys)) == 2 + u0 = unknowns(sys) .=> 0 + @test_nowarn ODEProblem( + sys, [], (0, 10.0), guesses = u0, warn_initialize_determined = false) + prob = DAEProblem(sys, D.(unknowns(sys)) .=> 0, (0, 0.5), guesses = u0) + sol = solve(prob, DFBDF()) + @test sol.retcode == SciMLBase.ReturnCode.Success + + sys2 = mtkcompile(ll2_model) + @test length(equations(sys2)) == 3 + u0 = [sys2.inductor2.i => 0] + prob = ODEProblem(sys2, u0, (0, 10.0)) + sol = solve(prob, FBDF()) + @test SciMLBase.successful_retcode(sol) + end end @testset "Compose/extend" begin @@ -156,12 +180,12 @@ end defs[foo.a] = 3 defs[foo.b] = 300 pars = @parameters x=2 y=20 - compose(System(Equation[], t, [], pars; name, defaults = defs), foo) + compose(System(Equation[], t, [], pars; name, initial_conditions = defs), foo) end @named goo = first_model() @unpack foo = goo - @test ModelingToolkit.defaults(goo)[foo.a] == 3 - @test ModelingToolkit.defaults(goo)[foo.b] == 300 + @test value(ModelingToolkitBase.initial_conditions(goo)[foo.a]) == 3 + @test value(ModelingToolkitBase.initial_conditions(goo)[foo.b]) == 300 end function Load(; name) @@ -187,44 +211,46 @@ function Circuit(; name) end @named foo = Circuit() -@test mtkcompile(foo) isa ModelingToolkit.AbstractSystem +@test mtkcompile(foo) isa ModelingToolkitBase.AbstractSystem # BLT tests -@testset "BLT ordering" begin - function parallel_rc_model(i; name, shape, source, ground, R, C) - resistor = Resistor(name = Symbol(:resistor, i), R = R, T_dep = true) - capacitor = Capacitor(name = Symbol(:capacitor, i), C = C) - heat_capacitor = HeatCapacitor(name = Symbol(:heat_capacitor, i)) - - rc_eqs = [connect(shape.output, source.V) - connect(source.p, resistor.p) - connect(resistor.n, capacitor.p) - connect(capacitor.n, source.n, ground.g) - connect(resistor.heat_port, heat_capacitor.port)] - - compose(System(rc_eqs, t, name = Symbol(name, i)), - [resistor, capacitor, source, ground, shape, heat_capacitor]) - end - V = 2.0 - @named shape = Constant(k = V) - @named source = Voltage() - @named ground = Ground() - N = 50 - Rs = 10 .^ range(0, stop = -4, length = N) - Cs = 10 .^ range(-3, stop = 0, length = N) - rc_systems = map(1:N) do i - parallel_rc_model(i; name = :rc, source, ground, shape, R = Rs[i], C = Cs[i]) +if @isdefined(ModelingToolkit) + @testset "BLT ordering" begin + function parallel_rc_model(i; name, shape, source, ground, R, C) + resistor = Resistor(name = Symbol(:resistor, i), R = R, T_dep = true) + capacitor = Capacitor(name = Symbol(:capacitor, i), C = C) + heat_capacitor = HeatCapacitor(name = Symbol(:heat_capacitor, i)) + + rc_eqs = [connect(shape.output, source.V) + connect(source.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + connect(resistor.heat_port, heat_capacitor.port)] + + compose(System(rc_eqs, t, name = Symbol(name, i)), + [resistor, capacitor, source, ground, shape, heat_capacitor]) + end + V = 2.0 + @named shape = Constant(k = V) + @named source = Voltage() + @named ground = Ground() + N = 50 + Rs = 10 .^ range(0, stop = -4, length = N) + Cs = 10 .^ range(-3, stop = 0, length = N) + rc_systems = map(1:N) do i + parallel_rc_model(i; name = :rc, source, ground, shape, R = Rs[i], C = Cs[i]) + end + @variables E(t) = 0.0 + eqs = [ + D(E) ~ sum(((i, sys),) -> getproperty(sys, Symbol(:resistor, i)).heat_port.Q_flow, + enumerate(rc_systems)) + ] + @named _big_rc = System(eqs, t, [E], []) + @named big_rc = compose(_big_rc, rc_systems) + ts = TearingState(expand_connections(big_rc)) + # this is block upper triangular, so `istriu` needs a little leeway + @test istriu(but_ordered_incidence(ts)[1], -2) end - @variables E(t) = 0.0 - eqs = [ - D(E) ~ sum(((i, sys),) -> getproperty(sys, Symbol(:resistor, i)).heat_port.Q_flow, - enumerate(rc_systems)) - ] - @named _big_rc = System(eqs, t, [E], []) - @named big_rc = compose(_big_rc, rc_systems) - ts = TearingState(expand_connections(big_rc)) - # this is block upper triangular, so `istriu` needs a little leeway - @test istriu(but_ordered_incidence(ts)[1], -2) end # Test using constants inside subsystems @@ -250,7 +276,7 @@ end [resistor, capacitor, ground]) sys = mtkcompile(rc_model) prob = ODEProblem(sys, [sys.c1.v => 0.0], (0, 10.0)) - sol = solve(prob, Tsit5()) + sol = solve(prob, @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P()) end @testset "docstrings (#1155)" begin @@ -349,7 +375,7 @@ end @named sys = System([connect(comp2.output, comp1.input)], t; systems = [comp1, comp2]) eq = only(equations(expand_connections(sys))) # as opposed to `output.u ~ input.u` - @test isequal(eq, comp1.input.u ~ comp2.output.u) + @test isequal(eq, comp2.output.u ~ comp1.input.u) # test causal ordering of true causal cset @named input = RealInput() diff --git a/test/constants.jl b/lib/ModelingToolkitBase/test/constants.jl similarity index 76% rename from test/constants.jl rename to lib/ModelingToolkitBase/test/constants.jl index ce5c7e6e8e..2197003256 100644 --- a/test/constants.jl +++ b/lib/ModelingToolkitBase/test/constants.jl @@ -1,7 +1,6 @@ -using ModelingToolkit, OrdinaryDiffEq, Unitful +using ModelingToolkitBase, OrdinaryDiffEq, DynamicQuantities using Test -MT = ModelingToolkit -UMT = ModelingToolkit.UnitfulUnitCheck +MT = ModelingToolkitBase @constants a = 1 @test isconstant(a) @@ -12,7 +11,7 @@ UMT = ModelingToolkit.UnitfulUnitCheck D = Differential(t) eqs = [D(x) ~ a] @named sys = System(eqs, t) -prob = ODEProblem(complete(sys), [0], [0.0, 1.0]) +prob = ODEProblem(complete(sys), [x => 0], [0.0, 1.0]) sol = solve(prob, Tsit5()) # Test mtkcompile substitutions & observed values @@ -21,11 +20,15 @@ eqs = [D(x) ~ 1, @named sys = System(eqs, t) # Now eliminate the constants first simp = mtkcompile(sys) -@test equations(simp) == [D(x) ~ 1.0] +if @isdefined(ModelingToolkit) + @test equations(simp) == [D(x) ~ 1.0] +else + @test equations(simp) == [D(x) ~ 1.0, 0 ~ a-w] +end #Constant with units @constants β=1 [unit = u"m/s"] -UMT.get_unit(β) +MT.get_unit(β) @test MT.isconstant(β) @test !MT.istunable(β) @independent_variables t [unit = u"s"] diff --git a/test/dae_jacobian.jl b/lib/ModelingToolkitBase/test/dae_jacobian.jl similarity index 85% rename from test/dae_jacobian.jl rename to lib/ModelingToolkitBase/test/dae_jacobian.jl index 17b14b39e4..65f66b552c 100644 --- a/test/dae_jacobian.jl +++ b/lib/ModelingToolkitBase/test/dae_jacobian.jl @@ -1,6 +1,6 @@ -using ModelingToolkit +using ModelingToolkitBase using Sundials, Test, SparseArrays -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D # Comparing solution obtained by defining explicit Jacobian function with solution obtained from # symbolically generated Jacobian @@ -48,7 +48,7 @@ du0 = [D(u1) => 0.5, D(u2) => -2.0] p = [p1 => 1.5, p2 => 3.0] -prob = DAEProblem(complete(sys), [du0; u0; p], tspan, jac = true, sparse = true) +prob = DAEProblem(complete(sys), [du0; p], tspan, jac = true, sparse = true) sol = solve(prob, IDA(linear_solver = :KLU)) -@test maximum(sol - sol1) < 1e-12 +@test maximum(sol - sol1) < 2e-12 diff --git a/test/dde.jl b/lib/ModelingToolkitBase/test/dde.jl similarity index 64% rename from test/dde.jl rename to lib/ModelingToolkitBase/test/dde.jl index 4af5f1ad31..ef1240db49 100644 --- a/test/dde.jl +++ b/lib/ModelingToolkitBase/test/dde.jl @@ -1,6 +1,7 @@ -using ModelingToolkit, DelayDiffEq, StaticArrays, Test +using ModelingToolkitBase, DelayDiffEq, StaticArrays, Test using SymbolicIndexingInterface: is_markovian -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D +using Setfield: @set! p0 = 0.2; q0 = 0.3; @@ -40,7 +41,7 @@ eqs = [D(x₀) ~ (v0 / (1 + beta0 * (x₂(t - tau)^2))) * (p0 - q0) * x₀ - d0 (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)] @mtkcompile sys = System(eqs, t) -@test ModelingToolkit.is_dde(sys) +@test ModelingToolkitBase.is_dde(sys) @test !is_markovian(sys) prob = DDEProblem(sys, [x₀ => 1.0, x₁ => 1.0, x₂(t) => 1.0], @@ -82,12 +83,18 @@ sol = solve(prob, RKMil(), seed = 100) @brownians η τ = 1.0 eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η, delx ~ x(t - τ)] -@mtkcompile sys = System(eqs, t) -@test ModelingToolkit.has_observed_with_lhs(sys, delx) -@test ModelingToolkit.is_dde(sys) +if @isdefined(ModelingToolkit) + @mtkcompile sys = System(eqs, t) +else + @mtkcompile sys = System([eqs[1]], t) + @set! sys.observed = [eqs[2]] + sys = complete(sys) +end +@test ModelingToolkitBase.has_observed_with_lhs(sys, delx) +@test ModelingToolkitBase.is_dde(sys) @test !is_markovian(sys) @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] -@test isequal(ModelingToolkit.get_noise_eqs(sys), [α * x(t) + γ;;]) +@test isequal(ModelingToolkitBase.get_noise_eqs(sys), [α * x(t) + γ;;]) prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); @test_nowarn sol_mtk = solve(prob_mtk, RKMil(), seed = 100) @@ -114,63 +121,69 @@ eqs = [osc1.jcn ~ osc2.delx, osc2.jcn ~ osc1.delx] @named coupledOsc = System(eqs, t) @named coupledOsc = compose(coupledOsc, systems) -@test ModelingToolkit.is_dde(coupledOsc) +@test ModelingToolkitBase.is_dde(coupledOsc) @test !is_markovian(coupledOsc) @named coupledOsc2 = System(eqs, t; systems) -@test ModelingToolkit.is_dde(coupledOsc2) +@test ModelingToolkitBase.is_dde(coupledOsc2) @test !is_markovian(coupledOsc2) -for coupledOsc in [coupledOsc, coupledOsc2] - local sys = mtkcompile(coupledOsc) - @test length(equations(sys)) == 4 - @test length(unknowns(sys)) == 4 +if @isdefined(ModelingToolkit) + for coupledOsc in [coupledOsc, coupledOsc2] + local sys = mtkcompile(coupledOsc) + @test length(equations(sys)) == 4 + @test length(unknowns(sys)) == 4 + end end sys = mtkcompile(coupledOsc) prob = DDEProblem(sys, [], (0.0, 10.0); constant_lags = [sys.osc1.τ, sys.osc2.τ]) -sol = solve(prob, MethodOfSteps(Tsit5())) -obsfn = ModelingToolkit.build_explicit_observed_function( +sol = solve(prob, MethodOfSteps(@isdefined(ModelingToolkit) ? Tsit5() : Rodas5P())) +obsfn = ModelingToolkitBase.build_explicit_observed_function( sys, [sys.osc1.delx, sys.osc2.delx]) @test_nowarn sol[[sys.osc1.delx, sys.osc2.delx]] -@test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x).u +mask = sol.t .>= 0.01 +@test sol[sys.osc1.delx][mask] ≈ sol(sol.t[mask] .- 0.01; idxs = sys.osc1.x).u rtol=1e-3 +N = length(unknowns(sys)) prob_sa = DDEProblem(sys, [], (0.0, 10.0); constant_lags = [sys.osc1.τ, sys.osc2.τ], - u0_constructor = SVector{4}) -@test prob_sa.u0 isa SVector{4, Float64} - -@testset "DDE observed with array variables" begin - @component function valve(; name) - @parameters begin - open(t)::Bool = false - Kp = 2 - Ksnap = 1.1 - τ = 0.1 - end - @variables begin - opening(..) - lag_opening(t) - snap_opening(t) + u0_constructor = x -> SVector{length(x)}(x)) +@test prob_sa.u0 isa SVector{N, Float64} + +if @isdefined(ModelingToolkit) + @testset "DDE observed with array variables" begin + @component function valve(; name) + @discretes open(t)::Bool = false + @parameters begin + Kp = 2 + Ksnap = 1.1 + τ = 0.1 + end + @variables begin + opening(..) + lag_opening(t) + snap_opening(t) + end + eqs = [D(opening(t)) ~ Kp * (open - opening(t)) + lag_opening ~ opening(t - τ) + snap_opening ~ clamp(Ksnap * lag_opening - 1 / Ksnap, 0, 1)] + return System(eqs, t; name = name) end - eqs = [D(opening(t)) ~ Kp * (open - opening(t)) - lag_opening ~ opening(t - τ) - snap_opening ~ clamp(Ksnap * lag_opening - 1 / Ksnap, 0, 1)] - return System(eqs, t; name = name) - end - @component function veccy(; name) - @parameters dx[1:3] = ones(3) - @variables begin - x(t)[1:3] = zeros(3) + @component function veccy(; name) + @parameters dx[1:3] = ones(3) + @variables begin + x(t)[1:3] = zeros(3) + end + return System([D(x) ~ dx], t; name = name) end - return System([D(x) ~ dx], t; name = name) - end - @mtkcompile ssys = System( - Equation[], t; systems = [valve(name = :valve), veccy(name = :vvecs)]) - prob = DDEProblem(ssys, [ssys.valve.opening => 1.0], (0.0, 1.0)) - sol = solve(prob, MethodOfSteps(Tsit5())) - obsval = @test_nowarn sol[ssys.valve.lag_opening + sum(ssys.vvecs.x)] - @test obsval ≈ - sol(sol.t .- prob.ps[ssys.valve.τ]; idxs = ssys.valve.opening).u .+ - sum.(sol[ssys.vvecs.x]) + @mtkcompile ssys = System( + Equation[], t; systems = [valve(name = :valve), veccy(name = :vvecs)]) + prob = DDEProblem(ssys, [ssys.valve.opening => 1.0], (0.0, 1.0)) + sol = solve(prob, MethodOfSteps(Tsit5())) + obsval = @test_nowarn sol[ssys.valve.lag_opening + sum(ssys.vvecs.x)] + @test obsval ≈ + sol(sol.t .- prob.ps[ssys.valve.τ]; idxs = ssys.valve.opening).u .+ + sum.(sol[ssys.vvecs.x]) + end end @testset "Issue#3165 DDEs with non-tunables" begin diff --git a/test/debugging.jl b/lib/ModelingToolkitBase/test/debugging.jl similarity index 91% rename from test/debugging.jl rename to lib/ModelingToolkitBase/test/debugging.jl index 0ff7a0fb4d..b2b608e545 100644 --- a/test/debugging.jl +++ b/lib/ModelingToolkitBase/test/debugging.jl @@ -1,6 +1,8 @@ -using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, SymbolicIndexingInterface +using ModelingToolkitBase, OrdinaryDiffEq, StochasticDiffEq, SymbolicIndexingInterface import Logging -using ModelingToolkit: t_nounits as t, D_nounits as D, ASSERTION_LOG_VARIABLE +using ModelingToolkitBase: t_nounits as t, D_nounits as D, ASSERTION_LOG_VARIABLE +import DiffEqNoiseProcess +using Test @variables x(t) @brownians a diff --git a/test/dep_graphs.jl b/lib/ModelingToolkitBase/test/dep_graphs.jl similarity index 95% rename from test/dep_graphs.jl rename to lib/ModelingToolkitBase/test/dep_graphs.jl index 1fa166f1b7..e1c95c365d 100644 --- a/test/dep_graphs.jl +++ b/lib/ModelingToolkitBase/test/dep_graphs.jl @@ -1,7 +1,8 @@ using Test -using ModelingToolkit, Graphs, JumpProcesses, RecursiveArrayTools -using ModelingToolkit: t_nounits as t, D_nounits as D -import ModelingToolkit: value +using ModelingToolkitBase, Graphs, JumpProcesses, RecursiveArrayTools +using ModelingToolkitBase: t_nounits as t, D_nounits as D +import ModelingToolkitBase: value +using Symbolics: SymbolicT ################################# # testing for Jumps / all dgs @@ -26,7 +27,7 @@ import ModelingToolkit: value test_case_1 = (; eqs = jumps(js), # eq to vars they depend on - eq_sdeps = [Variable[], [S], [S, I], [S, R], [I], [S]], + eq_sdeps = [SymbolicT[], [S], [S, I], [S, R], [I], [S]], eq_sidepsf = [Int[], [1], [1, 2], [1, 3], [2], [1]], eq_sidepsb = [[2, 3, 4, 6], [3, 5], [4]], # eq to params they depend on @@ -49,7 +50,7 @@ import ModelingToolkit: value # filter out vrjs in making graphs eqs = filter(x -> !(x isa VariableRateJump), jumps(js)), # eq to vars they depend on - eq_sdeps = [Variable[], [S], [S, I], [S, R], [I]], + eq_sdeps = [SymbolicT[], [S], [S, I], [S, R], [I]], eq_sidepsf = [Int[], [1], [1, 2], [1, 3], [2]], eq_sidepsb = [[2, 3, 4], [3, 5], [4]], # eq to params they depend on diff --git a/test/discrete_system.jl b/lib/ModelingToolkitBase/test/discrete_system.jl similarity index 77% rename from test/discrete_system.jl rename to lib/ModelingToolkitBase/test/discrete_system.jl index ccd5b2c0a9..83a1a79221 100644 --- a/test/discrete_system.jl +++ b/lib/ModelingToolkitBase/test/discrete_system.jl @@ -3,8 +3,9 @@ - https://github.com/epirecipes/sir-julia/blob/master/markdown/function_map/function_map.md - https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#Deterministic_versus_stochastic_epidemic_models =# -using ModelingToolkit, SymbolicIndexingInterface, Test -using ModelingToolkit: t_nounits as t +using ModelingToolkitBase, SymbolicIndexingInterface, Test +using ModelingToolkitBase: t_nounits as t +using Setfield: @set! # Make sure positive shifts error @variables x(t) @@ -36,7 +37,7 @@ syss = mtkcompile(sys) df = DiscreteFunction(syss) # iip du = zeros(3) -u = ModelingToolkit.varmap_to_vars( +u = ModelingToolkitBase.varmap_to_vars( Dict([S(k - 1) => 1, I(k - 1) => 2, R(k - 1) => 3]), unknowns(syss)) p = MTKParameters(syss, [c, nsteps, δt, β, γ] .=> collect(1:5)) df.f(du, u, p, 0) @@ -50,7 +51,7 @@ reorderer = getu(syss, [S(k - 1), I(k - 1), R(k - 1)]) # Problem u0 = [S => 990.0, I => 10.0, R => 0.0] p = [β => 0.05, c => 10.0, γ => 0.25, δt => 0.1, nsteps => 400] -tspan = (0.0, ModelingToolkit.value(substitute(nsteps, p))) # value function (from Symbolics) is used to convert a Num to Float64 +tspan = (0.0, ModelingToolkitBase.value(substitute(nsteps, p))) # value function (from Symbolics) is used to convert a Num to Float64 prob_map = DiscreteProblem( syss, [u0; p], tspan; guesses = [S(k - 1) => 1.0, I(k - 1) => 1.0, R(k - 1) => 1.0]) @test prob_map.f.sys === syss @@ -71,18 +72,19 @@ recovery2 = rate_to_proportion(γ, δt) * I(k - 1) eqs2 = [S ~ S(k - 1) - infection2, I ~ I(k - 1) + infection2 - recovery2, - R ~ R(k - 1) + recovery2, - R2 ~ R] + R ~ R(k - 1) + recovery2] @mtkcompile sys = System( - eqs2, t, [S, I, R, R2], [c, nsteps, δt, β, γ]) -@test ModelingToolkit.defaults(sys) != Dict() + eqs2, t, [S, I, R], [c, nsteps, δt, β, γ]) +push!(ModelingToolkitBase.get_observed(sys), R2 ~ R) +sys = complete(sys) +@test ModelingToolkitBase.initial_conditions(sys) != Dict() -prob_map2 = DiscreteProblem(sys, [], tspan) +prob_map2 = DiscreteProblem(sys, [], tspan; guesses = [S(k-1) => 1.0, R(k-1) => 1.0, I(k-1) => 1.0]) # prob_map2 = DiscreteProblem(sys, [S(k - 1) => S, I(k - 1) => I, R(k - 1) => R], tspan) sol_map2 = solve(prob_map2, FunctionMap()); -@test sol_map.u ≈ sol_map2.u +@test sol_map[[S(k-1), I(k-1), R(k-1)]] ≈ sol_map2[[S(k-1), I(k-1), R(k-1)]] for p in parameters(sys) @test sol_map.prob.ps[p] ≈ sol_map2.prob.ps[p] end @@ -208,49 +210,58 @@ RHS2 = RHS # end @variables x(t) y(t) u(t) -eqs = [u ~ 1 - x ~ x(k - 1) + u - y ~ x + u] -@mtkcompile de = System(eqs, t) +if @isdefined(ModelingToolkit) + eqs = [x ~ x(k-1) + u, u ~ 1, y ~ x + u] + @mtkcompile de = System(eqs, t) +else + eqs = [x ~ x(k - 1) + u] + @mtkcompile de = System(eqs, t) inputs=[u] + @set! de.observed = [u ~ 1; ModelingToolkitBase.get_observed(de); y ~ x + u] + filter!(!isequal(u), ModelingToolkitBase.get_ps(de)) + empty!(ModelingToolkitBase.get_inputs(de)) + de = complete(de) +end prob = DiscreteProblem(de, [x(k - 1) => 0.0], (0, 10)) sol = solve(prob, FunctionMap()) @test sol[x] == 1:11 # 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 +if @isdefined(ModelingToolkit) + 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 System(eqs, t; name) end - @variables output(t) time(t) - eqs = [time ~ time(k - 1) + 1 - output ~ getdata(buffer, time)] - return System(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"] + 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))] + + System(eqs, t, vars, pars; systems = [y_sys], name = name) 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))] - - System(eqs, t, vars, pars; systems = [y_sys], name = name) + @test_nowarn @mtkcompile sys = System(; buffer = ones(10)) end -@test_nowarn @mtkcompile sys = System(; buffer = ones(10)) - @testset "Passing `nothing` to `u0`" begin @variables x(t) = 1 - k = ShiftIndex() + k = ShiftIndex(t) @mtkcompile sys = System([x(k) ~ x(k - 1) + 1], t) prob = @test_nowarn DiscreteProblem(sys, nothing, (0.0, 1.0)) sol = solve(prob, FunctionMap()) @@ -275,7 +286,7 @@ end @test sol[[x..., y...], end] == 8ones(4) end -@testset "Defaults are totermed appropriately" begin +@testset "initial conditionsare totermed appropriately" begin @parameters σ ρ β q @variables x(t) y(t) z(t) k = ShiftIndex(t) @@ -286,7 +297,7 @@ end @mtkcompile discsys = System( [x ~ x(k - 1) * ρ + y(k - 2), y ~ y(k - 1) * σ - z(k - 2), z ~ z(k - 1) * β + x(k - 2)], - t; defaults = [x => 1.0, y => 1.0, z => 1.0, x(k - 1) => 1.0, + t; initial_conditions = [x => 1.0, y => 1.0, z => 1.0, x(k - 1) => 1.0, y(k - 1) => 1.0, z(k - 1) => 1.0]) discprob = DiscreteProblem(discsys, p, (0, 10)) sol = solve(discprob, FunctionMap()) diff --git a/test/distributed.jl b/lib/ModelingToolkitBase/test/distributed.jl similarity index 82% rename from test/distributed.jl rename to lib/ModelingToolkitBase/test/distributed.jl index 8c3ca4dcfa..42dfddb967 100644 --- a/test/distributed.jl +++ b/lib/ModelingToolkitBase/test/distributed.jl @@ -2,8 +2,8 @@ using Distributed # add processes to workspace addprocs(2) -@everywhere using ModelingToolkit, OrdinaryDiffEq -@everywhere using ModelingToolkit: t_nounits as t, D_nounits as D +@everywhere using ModelingToolkitBase, OrdinaryDiffEq +@everywhere using ModelingToolkitBase: t_nounits as t, D_nounits as D # create the Lorenz system @everywhere @parameters σ ρ β @@ -23,7 +23,7 @@ addprocs(2) @everywhere begin using OrdinaryDiffEq - using ModelingToolkit + using ModelingToolkitBase function solve_lorenz(ode_problem) print(solve(ode_problem, Tsit5())) diff --git a/test/domain_connectors.jl b/lib/ModelingToolkitBase/test/domain_connectors.jl similarity index 90% rename from test/domain_connectors.jl rename to lib/ModelingToolkitBase/test/domain_connectors.jl index 485796d585..ae3971e342 100644 --- a/test/domain_connectors.jl +++ b/lib/ModelingToolkitBase/test/domain_connectors.jl @@ -1,5 +1,5 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D using Test @connector function HydraulicPort(; p_int, name) @@ -14,7 +14,7 @@ using Test dm(t), [connect = Flow] end - System(Equation[], t, vars, pars; name, defaults = [dm => 0]) + System(Equation[], t, vars, pars; name, initial_conditions = [dm => 0]) end @connector function HydraulicFluid(; @@ -36,7 +36,7 @@ end dm ~ 0 ] - System(eqs, t, vars, pars; name, defaults = [dm => 0]) + System(eqs, t, vars, pars; name, initial_conditions = [dm => 0]) end function FixedPressure(; p, name) @@ -143,7 +143,7 @@ function HydraulicSystem(; name) end @named odesys = HydraulicSystem() -esys = ModelingToolkit.expand_connections(odesys) +esys = ModelingToolkitBase.expand_connections(odesys) @test length(equations(esys)) == length(unknowns(esys)) csys = complete(odesys) @@ -151,5 +151,5 @@ csys = complete(odesys) sys = mtkcompile(odesys) @test length(equations(sys)) == length(unknowns(sys)) -sys_defs = ModelingToolkit.defaults(sys) +sys_defs = ModelingToolkitBase.bindings(sys) @test Symbol(sys_defs[csys.vol.port.ρ]) == Symbol(csys.fluid.ρ) diff --git a/test/dq_units.jl b/lib/ModelingToolkitBase/test/dq_units.jl similarity index 86% rename from test/dq_units.jl rename to lib/ModelingToolkitBase/test/dq_units.jl index ea1103db57..610092db93 100644 --- a/test/dq_units.jl +++ b/lib/ModelingToolkitBase/test/dq_units.jl @@ -1,18 +1,21 @@ -using ModelingToolkit, OrdinaryDiffEq, JumpProcesses, DynamicQuantities +using ModelingToolkitBase, OrdinaryDiffEq, JumpProcesses, DynamicQuantities using Symbolics +import SymbolicUtils as SU using Test -MT = ModelingToolkit -using ModelingToolkit: t, D +using SciCompDSL +MT = ModelingToolkitBase +using ModelingToolkitBase: t, D @parameters τ [unit = u"s"] γ @variables E(t) [unit = u"J"] P(t) [unit = u"W"] +const unitless = MT.get_unit(0.5) + # Basic access @test MT.get_unit(t) == u"s" @test MT.get_unit(E) == u"J" @test MT.get_unit(τ) == u"s" -@test MT.get_unit(γ) == MT.unitless -@test MT.get_unit(0.5) == MT.unitless -@test MT.get_unit(MT.SciMLBase.NullParameters()) == MT.unitless +@test MT.get_unit(γ) == unitless +@test MT.get_unit(MT.SciMLBase.NullParameters()) == unitless eqs = [D(E) ~ P - E / τ 0 ~ P] @@ -164,7 +167,7 @@ maj2 = MassActionJump(γ, [I => 1], [I => -1, R => 1]) maj1 = MassActionJump(2.0, [0 => 1], [S => 1]) maj2 = MassActionJump(γ, [S => 1], [S => -1]) -@named js4 = JumpSystem([maj1, maj2], ModelingToolkit.t_nounits, [S], [β, γ]) +@named js4 = JumpSystem([maj1, maj2], ModelingToolkitBase.t_nounits, [S], [β, γ]) @mtkmodel ParamTest begin @parameters begin @@ -178,7 +181,7 @@ end @named sys = ParamTest() @named sys = ParamTest(a = 3.0u"cm") -@test ModelingToolkit.getdefault(sys.a) ≈ 0.03 +@test ModelingToolkitBase.getdefault(sys.a) ≈ 0.03 @test_throws ErrorException ParamTest(; name = :t, a = 1.0) @test_throws ErrorException ParamTest(; name = :t, a = 1.0u"s") @@ -192,7 +195,7 @@ end @named sys = ArrayParamTest() @named sys = ArrayParamTest(a = [1.0, 3.0]u"cm") -@test ModelingToolkit.getdefault(sys.a) ≈ [0.01, 0.03] +@test ModelingToolkitBase.getdefault(sys.a) ≈ [0.01, 0.03] @testset "Initialization checks" begin @mtkmodel PendulumUnits begin @@ -213,6 +216,9 @@ end end @mtkcompile pend = PendulumUnits() u0 = [pend.x => 1.0, pend.y => 0.0] + if !@isdefined(ModelingToolkit) + u0 = [u0; D(pend.x) => 0.0; D(pend.y) => 0.0] + end p = [pend.g => 1.0, pend.L => 1.0] guess = [pend.λ => 0.0] @test prob = ODEProblem( @@ -224,7 +230,7 @@ end @variables X(tt) [unit = u"L"] DD = Differential(tt) eqs = [DD(X) ~ p - d * X + d * X] -@test ModelingToolkit.validate(eqs) +@test ModelingToolkitBase.validate(eqs) @constants begin to_m = 1, [unit = u"m"] @@ -233,7 +239,7 @@ end L(t), [unit = u"m"] L_out(t), [unit = u"1"] end -@test to_m in ModelingToolkit.vars(Symbolics.unwrap(L_out * -to_m)) +@test to_m in SU.search_variables(Symbolics.unwrap(L_out * -to_m)) # test units for registered functions let @@ -255,31 +261,3 @@ let @test MT.get_unit(x_vec) == u"1" @test MT.get_unit(x_mat) == u"1" end - -module UnitTD -using Test -using ModelingToolkit -using ModelingToolkit: t, D -using DynamicQuantities - -@mtkmodel UnitsExample begin - @parameters begin - g, [unit = u"m/s^2"] - L = 1.0, [unit = u"m"] - end - @variables begin - x(t), [unit = u"m"] - y(t), [state_priority = 10, unit = u"m"] - λ(t), [unit = u"s^-2"] - end - @equations begin - D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ L^2 - end -end - -@mtkcompile pend = UnitsExample() -@test ModelingToolkit.get_unit.(filter(x -> occursin("ˍt", string(x)), unknowns(pend))) == - [u"m/s", u"m/s"] -end diff --git a/test/equation_type_accessors.jl b/lib/ModelingToolkitBase/test/equation_type_accessors.jl similarity index 96% rename from test/equation_type_accessors.jl rename to lib/ModelingToolkitBase/test/equation_type_accessors.jl index 1bf92743ac..64d0bb8598 100644 --- a/test/equation_type_accessors.jl +++ b/lib/ModelingToolkitBase/test/equation_type_accessors.jl @@ -1,8 +1,9 @@ # 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 +using ModelingToolkitBase +import ModelingToolkitBase: get_systems, namespace_equations +import ModelingToolkitBase: is_alg_equation, is_diff_equation +import ModelingToolkitBase: t_nounits as t, D_nounits as D, wrap, get_eqs +using Test # Creates equations. @variables X(t) Y(t) Z(t) diff --git a/test/error_handling.jl b/lib/ModelingToolkitBase/test/error_handling.jl similarity index 68% rename from test/error_handling.jl rename to lib/ModelingToolkitBase/test/error_handling.jl index 6a552ae063..672102562c 100644 --- a/test/error_handling.jl +++ b/lib/ModelingToolkitBase/test/error_handling.jl @@ -1,7 +1,7 @@ using Test -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D -import ModelingToolkit: ExtraVariablesSystemException, ExtraEquationsSystemException +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D +import ModelingToolkitBase: ExtraVariablesSystemException, ExtraEquationsSystemException using ModelingToolkitStandardLibrary.Electrical @@ -14,7 +14,7 @@ function UnderdefinedConstantVoltage(; name, V = 1.0) V ~ p.v - n.v # Remove equation # 0 ~ p.i + n.i ] - System(eqs, t, [], [V], systems = [p, n], defaults = Dict(V => val), name = name) + System(eqs, t, [], [V], systems = [p, n], initial_conditions = Dict(V => val), name = name) end function OverdefinedConstantVoltage(; name, V = 1.0, I = 1.0) @@ -27,7 +27,7 @@ function OverdefinedConstantVoltage(; name, V = 1.0, I = 1.0) # Overdefine p.i and n.i n.i ~ I p.i ~ I] - System(eqs, t, [], [V, I], systems = [p, n], defaults = Dict(V => val, I => val2), + System(eqs, t, [], [V, I], systems = [p, n], initial_conditions = Dict(V => val, I => val2), name = name) end @@ -43,7 +43,7 @@ rc_eqs = [connect(source.p, resistor.p) connect(capacitor.n, source.n)] @named rc_model = System(rc_eqs, t, systems = [resistor, capacitor, source]) -@test_throws ModelingToolkit.ExtraVariablesSystemException mtkcompile(rc_model) +@test_throws ModelingToolkitBase.ExtraVariablesSystemException mtkcompile(rc_model) @named source2 = OverdefinedConstantVoltage(V = V, I = V / R) rc_eqs2 = [connect(source2.p, resistor.p) @@ -51,4 +51,4 @@ rc_eqs2 = [connect(source2.p, resistor.p) connect(capacitor.n, source2.n)] @named rc_model2 = System(rc_eqs2, t, systems = [resistor, capacitor, source2]) -@test_throws ModelingToolkit.ExtraEquationsSystemException mtkcompile(rc_model2) +@test_throws ModelingToolkitBase.ExtraEquationsSystemException mtkcompile(rc_model2) diff --git a/test/extensions/Project.toml b/lib/ModelingToolkitBase/test/extensions/Project.toml similarity index 86% rename from test/extensions/Project.toml rename to lib/ModelingToolkitBase/test/extensions/Project.toml index 4ebbd99d87..f414d049f2 100644 --- a/test/extensions/Project.toml +++ b/lib/ModelingToolkitBase/test/extensions/Project.toml @@ -1,6 +1,3 @@ -[sources] -ModelingToolkit = {path = "../.."} - [deps] BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" CasADi = "c49709b8-5c63-11e9-2fb2-69db5844192f" @@ -15,7 +12,7 @@ InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" -ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839" Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" NonlinearSolveHomotopyContinuation = "2ac3b008-d579-4536-8c91-a1a5998c2f8b" OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125" @@ -23,14 +20,20 @@ OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" OrdinaryDiffEqSDIRK = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" -Pyomo = "0e8e1daf-01b5-4eba-a626-3897743a3816" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SimpleDiffEq = "05bca326-078c-5bf0-a5bf-ce7c7982d7fd" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +TaylorDiff = "b36ab563-344f-407b-a36a-4f200bebf99c" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" +[sources] +ModelingToolkitBase = {subdir = "../.."} + [compat] CasADi = "1.0.6" +DataInterpolations = "8.8" +TaylorDiff = "0.3.5" diff --git a/test/extensions/ad.jl b/lib/ModelingToolkitBase/test/extensions/ad.jl similarity index 96% rename from test/extensions/ad.jl rename to lib/ModelingToolkitBase/test/extensions/ad.jl index 53210b66a8..cd9864c613 100644 --- a/test/extensions/ad.jl +++ b/lib/ModelingToolkitBase/test/extensions/ad.jl @@ -1,5 +1,5 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D using Zygote using SymbolicIndexingInterface using SciMLStructures @@ -12,6 +12,7 @@ using StableRNGs using ChainRulesCore using ChainRulesCore: NoTangent using ChainRulesTestUtils: test_rrule, rand_tangent +using SciCompDSL @variables x(t)[1:3] y(t) @parameters p[1:3, 1:3] q @@ -42,7 +43,7 @@ end t, vars, pars; - defaults = [ + initial_conditions = [ y0 => mh * 3.1 / (2.3 * Th0), mh => 123.4, Th0 => (4 / 11)^(1 / 3) * Tγ0, @@ -61,7 +62,7 @@ end @parameters a b[1:3] c(t) d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String @named sys = System( Equation[], t, [], [a, b, c, d, e, f, g, h], - continuous_events = [ModelingToolkit.SymbolicContinuousCallback( + continuous_events = [ModelingToolkitBase.SymbolicContinuousCallback( [a ~ 0] => [c ~ 0], discrete_parameters = c, iv = t)]) sys = complete(sys) diff --git a/test/extensions/bifurcationkit.jl b/lib/ModelingToolkitBase/test/extensions/bifurcationkit.jl similarity index 94% rename from test/extensions/bifurcationkit.jl rename to lib/ModelingToolkitBase/test/extensions/bifurcationkit.jl index 65c3f2eb37..dfd6cc3673 100644 --- a/test/extensions/bifurcationkit.jl +++ b/lib/ModelingToolkitBase/test/extensions/bifurcationkit.jl @@ -1,8 +1,8 @@ -using BifurcationKit, ModelingToolkit, Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using BifurcationKit, ModelingToolkitBase, Test, SciCompDSL +using ModelingToolkitBase: t_nounits as t, D_nounits as D # Simple pitchfork diagram, compares solution to native BifurcationKit, checks they are identical. # Checks using `jac=false` option. -let +if @isdefined(ModelingToolkit) # Creates model. @variables x(t) y(t) @parameters μ α @@ -96,7 +96,7 @@ end # Simple fold bifurcation model, checks exact position of bifurcation variable and bifurcation points. # Checks that default parameter values are accounted for. # Checks that observables (that depend on other observables, as in this case) are accounted for. -let +if @isdefined(ModelingToolkit) # Creates model, and uses `mtkcompile` to generate observables. @parameters μ p=2 @variables x(t) y(t) z(t) @@ -134,7 +134,7 @@ let @test fold_points ≈ [-1.1851851706940317, -5.6734983580551894e-6] # test that they occur at the correct parameter values). end -let +if @isdefined(ModelingToolkit) @mtkmodel FOL begin @parameters begin τ # parameters @@ -152,7 +152,7 @@ let @mtkcompile fol = FOL() par = [fol.τ => 0.0] - u0 = [fol.x => -1.0] + u0 = [fol.x => -1.0, fol.RHS => 0.9] #prob = ODEProblem(fol, u0, (0.0, 1.), par) bif_par = fol.τ @@ -164,7 +164,7 @@ let @test bf.γ.specialpoint[1].param≈0.1 atol=1e-4 rtol=1e-4 # Test with plot variable as observable - pvar = ModelingToolkit.get_var_to_name(fol)[:RHS] + pvar = ModelingToolkitBase.get_var_to_name(fol)[:RHS] bp = BifurcationProblem(fol, u0, par, bif_par; plot_var = pvar) opts_br = ContinuationPar(p_min = -1.0, p_max = 1.0) diff --git a/test/extensions/dynamic_optimization.jl b/lib/ModelingToolkitBase/test/extensions/dynamic_optimization.jl similarity index 81% rename from test/extensions/dynamic_optimization.jl rename to lib/ModelingToolkitBase/test/extensions/dynamic_optimization.jl index f6b6d75382..2cc9e21e48 100644 --- a/test/extensions/dynamic_optimization.jl +++ b/lib/ModelingToolkitBase/test/extensions/dynamic_optimization.jl @@ -1,5 +1,5 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D import InfiniteOpt using DiffEqDevTools, DiffEqBase using SimpleDiffEq @@ -7,11 +7,11 @@ using OrdinaryDiffEqSDIRK, OrdinaryDiffEqVerner, OrdinaryDiffEqTsit5, OrdinaryDi using Ipopt using DataInterpolations using CasADi -using Pyomo +# using Pyomo using Test import DiffEqBase: solve -const M = ModelingToolkit +const M = ModelingToolkitBase const ENABLE_CASADI = VERSION >= v"1.11" @@ -52,9 +52,11 @@ const ENABLE_CASADI = VERSION >= v"1.11" csol2 = solve(cprob, CasADiCollocation("ipopt", constructImplicitEuler())) @test ≈(csol2.sol.u, osol2.u, rtol = 0.001) end - pprob = PyomoDynamicOptProblem(sys, [u0map; parammap], tspan, dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test all([≈(psol.sol(t), osol2(t), rtol = 1e-2) for t in 0.0:0.01:1.0]) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(sys, [u0map; parammap], tspan, dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + @test all([≈(psol.sol(t), osol2(t), rtol = 1e-2) for t in 0.0:0.01:1.0]) + end # With a constraint u0map = Pair[] @@ -76,11 +78,13 @@ const ENABLE_CASADI = VERSION >= v"1.11" @test csol.sol(0.3; idxs = x(t)) ≈ 7.0 end - pprob = PyomoDynamicOptProblem( - lksys, [u0map; parammap], tspan; guesses = guess, dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(3))) - @test psol.sol(0.6; idxs = x(t)) ≈ 3.5 - @test psol.sol(0.3; idxs = x(t)) ≈ 7.0 + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem( + lksys, [u0map; parammap], tspan; guesses = guess, dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(3))) + @test psol.sol(0.6; idxs = x(t)) ≈ 3.5 + @test psol.sol(0.3; idxs = x(t)) ≈ 7.0 + end iprob = InfiniteOptDynamicOptProblem( lksys, [u0map; parammap], tspan; guesses = guess, dt = 0.01) @@ -104,10 +108,12 @@ const ENABLE_CASADI = VERSION >= v"1.11" jsol = solve(jprob, JuMPCollocation(Ipopt.Optimizer, constructRadauIA3())) @test all(u -> u > [1, 1], jsol.sol.u) - pprob = PyomoDynamicOptProblem( - lksys, [u0map; parammap], tspan; guesses = guess, dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", MidpointEuler())) - @test all(u -> u > [1, 1], psol.sol.u) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem( + lksys, [u0map; parammap], tspan; guesses = guess, dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", MidpointEuler())) + @test all(u -> u > [1, 1], psol.sol.u) + end if ENABLE_CASADI cprob = CasADiDynamicOptProblem( @@ -176,15 +182,17 @@ end @test is_bangbang(isol.input_sol, [-1.0], [1.0]) @test ≈(isol.sol[x(t)][end], 0.25, rtol = 1e-5) - pprob = PyomoDynamicOptProblem(block, [u0map; parammap], tspan; dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test is_bangbang(psol.input_sol, [-1.0], [1.0]) - @test ≈(psol.sol[x(t)][end], 0.25, rtol = 1e-3) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(block, [u0map; parammap], tspan; dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + @test is_bangbang(psol.input_sol, [-1.0], [1.0]) + @test ≈(psol.sol[x(t)][end], 0.25, rtol = 1e-3) + @test all([≈(psol.sol(t), osol(t), rtol = 0.05) for t in 0.0:0.01:1.0]) + end spline = ctrl_to_spline(isol.input_sol, ConstantInterpolation) oprob = ODEProblem(block_ode, [u0map; u_interp => spline], tspan) @test ≈(isol.sol.u, osol.u, rtol = 0.05) - @test all([≈(psol.sol(t), osol(t), rtol = 0.05) for t in 0.0:0.01:1.0]) ################### ### Bee example ### @@ -214,14 +222,18 @@ end csol = solve(cprob, CasADiCollocation("ipopt", constructTsitouras5())) @test is_bangbang(csol.input_sol, [0.0], [1.0]) end - pprob = PyomoDynamicOptProblem(beesys, [u0map; pmap], tspan, dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test is_bangbang(psol.input_sol, [0.0], [1.0]) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(beesys, [u0map; pmap], tspan, dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + @test is_bangbang(psol.input_sol, [0.0], [1.0]) + end @parameters (α_interp::LinearInterpolation)(..) eqs = [D(w(t)) ~ -μ * w(t) + b * s * α_interp(t) * w(t), D(q(t)) ~ -ν * q(t) + c * (1 - α_interp(t)) * s * w(t)] @mtkcompile beesys_ode = System(eqs, t) + u0map = [w(t) => 40, q(t) => 2] + pmap = [b => 1, c => 1, μ => 1, s => 1, ν => 1] oprob = ODEProblem(beesys_ode, merge(Dict(u0map), Dict(pmap), Dict(α_interp => ctrl_to_spline(jsol.input_sol, LinearInterpolation))), @@ -233,7 +245,10 @@ end end osol2 = solve(oprob, ImplicitEuler(); dt = 0.01, adaptive = false) @test ≈(osol2.u, isol.sol.u, rtol = 0.01) - @test all([≈(psol.sol(t), osol2(t), rtol = 0.01) for t in 0.0:0.01:4.0]) + + if @isdefined(Pyomo) + @test all([≈(psol.sol(t), osol2(t), rtol = 0.01) for t in 0.0:0.01:4.0]) + end end @testset "Rocket launch" begin @@ -253,7 +268,7 @@ end @named rocket = System(eqs, t, vars, ps; costs, constraints = cons) rocket = mtkcompile(rocket; inputs = [T]) - u0map = [h => h₀, m => m₀, v => 0] + u0map = [h => h₀, v => 0] pmap = [ g₀ => 1, m₀ => 1.0, h_c => 500, c => 0.5 * √(g₀ * h₀), D_c => 0.5 * 620 * m₀ / g₀, Tₘ => 3.5 * g₀ * m₀, T => 0.0, h₀ => 1, m_c => 0.6] @@ -272,9 +287,11 @@ end isol = solve(iprob, InfiniteOptCollocation(Ipopt.Optimizer)) @test isol.sol[h][end] > 1.012 - pprob = PyomoDynamicOptProblem(rocket, [u0map; pmap], (ts, te); dt = 0.001, cse = false) - psol = solve(pprob, PyomoCollocation("ipopt", LagrangeRadau(4))) - @test psol.sol[h][end] > 1.012 + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(rocket, [u0map; pmap], (ts, te); dt = 0.001, cse = false) + psol = solve(pprob, PyomoCollocation("ipopt", LagrangeRadau(4))) + @test psol.sol[h][end] > 1.012 + end # Test solution @parameters (T_interp::CubicSpline)(..) @@ -283,6 +300,10 @@ end D(m) ~ -T_interp(t) / c] @mtkcompile rocket_ode = System(eqs, t) interpmap = Dict(T_interp => ctrl_to_spline(jsol.input_sol, CubicSpline)) + u0map = [h => h₀, v => 0] + pmap = [ + g₀ => 1, m₀ => 1.0, h_c => 500, c => 0.5 * √(g₀ * h₀), D_c => 0.5 * 620 * m₀ / g₀, + h₀ => 1] oprob = ODEProblem(rocket_ode, merge(Dict(u0map), Dict(pmap), interpmap), (ts, te)) osol = solve(oprob, RadauIIA5(); adaptive = false, dt = 0.001) @test ≈(jsol.sol.u, osol.u, rtol = 0.02) @@ -295,10 +316,12 @@ end osol1 = solve(oprob1, ImplicitEuler(); adaptive = false, dt = 0.001) @test ≈(isol.sol.u, osol1.u, rtol = 0.01) - interpmap2 = Dict(T_interp => ctrl_to_spline(psol.input_sol, CubicSpline)) - oprob2 = ODEProblem(rocket_ode, merge(Dict(u0map), Dict(pmap), interpmap2), (ts, te)) - osol2 = solve(oprob2, RadauIIA5(); adaptive = false, dt = 0.001) - @test all([≈(psol.sol(t), osol2(t), rtol = 0.01) for t in 0:0.001:0.2]) + if @isdefined(Pyomo) + interpmap2 = Dict(T_interp => ctrl_to_spline(psol.input_sol, CubicSpline)) + oprob2 = ODEProblem(rocket_ode, merge(Dict(u0map), Dict(pmap), interpmap2), (ts, te)) + osol2 = solve(oprob2, RadauIIA5(); adaptive = false, dt = 0.001) + @test all([≈(psol.sol(t), osol2(t), rtol = 0.01) for t in 0:0.001:0.2]) + end end @testset "Free final time problems" begin @@ -331,10 +354,12 @@ end @test isapprox(isol.sol.t[end], 10.0, rtol = 1e-3) @test ≈(M.objective_value(isol), -92.75, atol = 0.25) - pprob = PyomoDynamicOptProblem(rocket, [u0map; pmap], (0, tf); steps = 201) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test isapprox(psol.sol.t[end], 10.0, rtol = 1e-3) - @test ≈(M.objective_value(psol), -92.75, atol = 0.1) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(rocket, [u0map; pmap], (0, tf); steps = 201) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + @test isapprox(psol.sol.t[end], 10.0, rtol = 1e-3) + @test ≈(M.objective_value(psol), -92.75, atol = 0.1) + end @variables x(..) v(..) @variables u(..) [bounds = (-1.0, 1.0), input = true] @@ -363,9 +388,11 @@ end isol = solve(iprob, InfiniteOptCollocation(Ipopt.Optimizer), verbose = true) @test isapprox(isol.sol.t[end], 2.0, atol = 1e-5) - pprob = PyomoDynamicOptProblem(block, [u0map; parammap], tspan; steps = 51) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test isapprox(psol.sol.t[end], 2.0, atol = 1e-5) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(block, [u0map; parammap], tspan; steps = 51) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + @test isapprox(psol.sol.t[end], 2.0, atol = 1e-5) + end end @testset "Cart-pole problem" begin @@ -395,26 +422,30 @@ end @named cartpole = System(eqs, t; costs, constraints = cons) cartpole = mtkcompile(cartpole; inputs = [u]) + var_order = [θ(t), x(t), D(θ(t)), D(x(t))] + u0map = [D(x(t)) => 0.0, D(θ(t)) => 0.0, θ(t) => 0.0, x(t) => 0.0] pmap = [mₖ => 1.0, mₚ => 0.2, l => 0.5, g => 9.81, u => 0] jprob = JuMPDynamicOptProblem(cartpole, [u0map; pmap], tspan; dt = 0.04) jsol = solve(jprob, JuMPCollocation(Ipopt.Optimizer, constructRK4())) - @test jsol.sol.u[end] ≈ [π, 0, 0, 0] + @test jsol.sol[var_order][end] ≈ [π, 0, 0, 0] if ENABLE_CASADI cprob = CasADiDynamicOptProblem(cartpole, [u0map; pmap], tspan; dt = 0.04) csol = solve(cprob, CasADiCollocation("ipopt", constructRK4())) - @test csol.sol.u[end] ≈ [π, 0, 0, 0] + @test csol.sol[var_order][end] ≈ [π, 0, 0, 0] end iprob = InfiniteOptDynamicOptProblem(cartpole, [u0map; pmap], tspan; dt = 0.04) isol = solve(iprob, InfiniteOptCollocation(Ipopt.Optimizer, InfiniteOpt.OrthogonalCollocation(2))) - @test isol.sol.u[end] ≈ [π, 0, 0, 0] + @test isol.sol[var_order][end] ≈ [π, 0, 0, 0] - pprob = PyomoDynamicOptProblem(cartpole, [u0map; pmap], tspan; dt = 0.04) - psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(4))) - @test psol.sol.u[end] ≈ [π, 0, 0, 0] + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(cartpole, [u0map; pmap], tspan; dt = 0.04) + psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(4))) + @test psol.sol[var_order][end] ≈ [π, 0, 0, 0] + end end @testset "Parameter defaults usage" begin @@ -449,10 +480,12 @@ end @test csol.sol.u ≈ osol.u end - pprob = PyomoDynamicOptProblem(sys, u0map, tspan, dt = 0.01) - psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(sys, u0map, tspan, dt = 0.01) + psol = solve(pprob, PyomoCollocation("ipopt", BackwardEuler())) - @test psol.sol.u ≈ osol.u rtol=1e-2 + @test psol.sol.u ≈ osol.u rtol=1e-2 + end end @testset "Parameter estimation" begin @@ -487,7 +520,7 @@ end # test with different time stepping jprob = JuMPDynamicOptProblem(sys′, u0map, tspan; dt=1/120, tune_parameters=true) - err_msg = "x is evaluated inside the cost function at 40 points that are not in the 121 collocation points." + err_msg = "Found extra 40 collocation points." @test_throws err_msg solve(jprob, JuMPCollocation(Ipopt.Optimizer, constructTsitouras5())) iprob = InfiniteOptDynamicOptProblem(sys′, u0map, tspan, dt = 1/50, tune_parameters=true) @@ -518,12 +551,13 @@ end @test csol.sol.ps[α] ≈ 2.5 rtol=1e-3 end - pprob = PyomoDynamicOptProblem(sys′, u0map, tspan, dt = 1/50, tune_parameters=true) - psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(4))) - - @test psol.sol.ps[δ] ≈ 1.8 rtol=1e-4 - @test psol.sol.ps[α] ≈ 2.5 rtol=1e-4 + if @isdefined(Pyomo) + pprob = PyomoDynamicOptProblem(sys′, u0map, tspan, dt = 1/50, tune_parameters=true) + psol = solve(pprob, PyomoCollocation("ipopt", LagrangeLegendre(4))) + @test psol.sol.ps[δ] ≈ 1.8 rtol=1e-4 + @test psol.sol.ps[α] ≈ 2.5 rtol=1e-4 + end # test with different time stepping # pprob = PyomoDynamicOptProblem(sys′, u0map, tspan, dt = 1/120, tune_parameters=true) diff --git a/test/extensions/homotopy_continuation.jl b/lib/ModelingToolkitBase/test/extensions/homotopy_continuation.jl similarity index 90% rename from test/extensions/homotopy_continuation.jl rename to lib/ModelingToolkitBase/test/extensions/homotopy_continuation.jl index 20ff262132..652d29fff8 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/lib/ModelingToolkitBase/test/extensions/homotopy_continuation.jl @@ -1,7 +1,7 @@ -using ModelingToolkit, NonlinearSolve, NonlinearSolveHomotopyContinuation, +using ModelingToolkitBase, NonlinearSolve, NonlinearSolveHomotopyContinuation, SymbolicIndexingInterface using SymbolicUtils -import ModelingToolkit as MTK +import ModelingToolkitBase as MTK using LinearAlgebra using Test @@ -184,7 +184,7 @@ end 0 ~ ((y - 3) / (y - 4)) * (n / (y - 5)) + ((x - 1.5) / (x - 5.5))^2 ], [x, y], - [n]; defaults = [n => 4]) + [n]; initial_conditions = [n => 4]) sys = complete(sys) prob = HomotopyContinuationProblem(sys, []) sol = solve(prob, singlerootalg) @@ -205,14 +205,16 @@ end @test any(<=(1e-7), prob.f.denominator([2.0, val + err], p)) end end - @test prob.f.denominator([2.0, 4.0], p)[1] <= 1e-8 + @test any(<=(1e-8), prob.f.denominator([2.0, 4.0], p)) @testset "Rational function in observed" begin @variables x=1 y=1 @mtkcompile sys = System([x^2 + y^2 - 2x - 2 ~ 0, y ~ (x - 1) / (x - 2)]) prob = HomotopyContinuationProblem(sys, []) - @test any(prob.f.denominator([2.0], parameter_values(prob)) .≈ 0.0) - @test SciMLBase.successful_retcode(solve(prob, singlerootalg)) + @test any(prob.f.denominator(2ones(length(unknowns(sys))), parameter_values(prob)) .≈ 0.0) + if @isdefined(ModelingToolkit) + @test SciMLBase.successful_retcode(solve(prob, singlerootalg)) + end end @testset "Rational function forced to common denominators" begin @@ -226,13 +228,15 @@ end end end -@testset "Non-polynomial observed not used in equations" begin - @variables x=1 y - @mtkcompile sys = System([x^2 - 2 ~ 0, y ~ sin(x)]) - prob = HomotopyContinuationProblem(sys, []) - sol = @test_nowarn solve(prob, singlerootalg) - @test sol[x] ≈ √2.0 - @test sol[y] ≈ sin(√2.0) +if @isdefined(ModelingToolkit) + @testset "Non-polynomial observed not used in equations" begin + @variables x=1 y + @mtkcompile sys = System([x^2 - 2 ~ 0, y ~ sin(x)]) + prob = HomotopyContinuationProblem(sys, []) + sol = @test_nowarn solve(prob, singlerootalg) + @test sol[x] ≈ √2.0 + @test sol[y] ≈ sin(√2.0) + end end @testset "`fraction_cancel_fn`" begin diff --git a/test/labelledarrays.jl b/lib/ModelingToolkitBase/test/extensions/labelledarrays.jl similarity index 91% rename from test/labelledarrays.jl rename to lib/ModelingToolkitBase/test/extensions/labelledarrays.jl index 96c9e1c32b..130301c75c 100644 --- a/test/labelledarrays.jl +++ b/lib/ModelingToolkitBase/test/extensions/labelledarrays.jl @@ -1,7 +1,7 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra, LabelledArrays +using ModelingToolkitBase, StaticArrays, LinearAlgebra, LabelledArrays using DiffEqBase, ForwardDiff using Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D # Define some variables @parameters σ ρ β @@ -44,7 +44,7 @@ d = LVector(x = 1.0, y = 2.0, z = 3.0) ## https://github.com/SciML/ModelingToolkit.jl/issues/1054 using LabelledArrays -using ModelingToolkit +using ModelingToolkitBase # ODE model: simple SIR model with seasonally forced contact rate function SIR!(du, u, p, t) @@ -88,5 +88,5 @@ problem = ODEProblem(SIR!, u0, tspan, p) sys = complete(modelingtoolkitize(problem)) @test all(any(isequal(x), parameters(sys)) -for x in ModelingToolkit.unwrap.(@variables(β, η, ω, φ, σ, μ))) +for x in ModelingToolkitBase.unwrap.(@variables(β, η, ω, φ, σ, μ))) @test all(isequal.(Symbol.(unknowns(sys)), Symbol.(@variables(S(t), I(t), R(t), C(t))))) diff --git a/test/extensions/test_infiniteopt.jl b/lib/ModelingToolkitBase/test/extensions/test_infiniteopt.jl similarity index 79% rename from test/extensions/test_infiniteopt.jl rename to lib/ModelingToolkitBase/test/extensions/test_infiniteopt.jl index fab44d0cf9..84bd0c07a9 100644 --- a/test/extensions/test_infiniteopt.jl +++ b/lib/ModelingToolkitBase/test/extensions/test_infiniteopt.jl @@ -1,5 +1,6 @@ -using ModelingToolkit, InfiniteOpt, JuMP, Ipopt -using ModelingToolkit: D_nounits as D, t_nounits as t, varmap_to_vars +using ModelingToolkitBase, InfiniteOpt, JuMP, Ipopt, Setfield +using ModelingToolkitBase: D_nounits as D, t_nounits as t, varmap_to_vars +using SciCompDSL @mtkmodel Pendulum begin @parameters begin @@ -25,10 +26,19 @@ model = complete(model) inputs = [model.τ] outputs = [model.y] model = mtkcompile(model; inputs, outputs) -f, dvs, psym, io_sys = ModelingToolkit.generate_control_function( +if !@isdefined(ModelingToolkit) + idx = findfirst(isequal(model.y), unknowns(model)) + @set! model.unknowns = setdiff(unknowns(model), [model.y]) + eqs = copy(equations(model)) + deleteat!(eqs, idx) + @set! model.eqs = eqs + @set! model.observed = [model.y ~ model.θ * 180 / π] + model = complete(model) +end +f, dvs, psym, io_sys = ModelingToolkitBase.generate_control_function( model, split = false) -f_obs = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs) +f_obs = ModelingToolkitBase.build_explicit_observed_function(io_sys, outputs; inputs) expected_state_order = [model.θ, model.ω] permutation = [findfirst(isequal(x), expected_state_order) for x in dvs] # This maps our expected state order to the actual state order @@ -62,8 +72,8 @@ begin end) # Trace the dynamics -x0 = ModelingToolkit.get_u0(io_sys, [model.θ => 0, model.ω => 0]) -p = ModelingToolkit.get_p(io_sys, [model.L => L]; split = false, buffer_eltype = Any) +x0 = ModelingToolkitBase.get_u0(io_sys, [model.θ => 0, model.ω => 0]) +p = ModelingToolkitBase.get_p(io_sys, [model.L => L]; split = false, buffer_eltype = Any) xp = f[1](x, u, p, τ) cp = f_obs(x, u, p, τ) # Test that it's possible to trace through an observed function diff --git a/test/function_registration.jl b/lib/ModelingToolkitBase/test/function_registration.jl similarity index 82% rename from test/function_registration.jl rename to lib/ModelingToolkitBase/test/function_registration.jl index 7ab9835433..4ce760b475 100644 --- a/test/function_registration.jl +++ b/lib/ModelingToolkitBase/test/function_registration.jl @@ -6,8 +6,8 @@ # TEST: Function registration in a module. # ------------------------------------------------ module MyModule -using ModelingToolkit, DiffEqBase, LinearAlgebra, Test -using ModelingToolkit: t_nounits as t, D_nounits as Dt +using ModelingToolkitBase, DiffEqBase, LinearAlgebra, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as Dt @parameters x @variables u(t) @@ -29,8 +29,8 @@ end # ------------------------------------------------ module MyModule2 module MyNestedModule -using ModelingToolkit, DiffEqBase, LinearAlgebra, Test -using ModelingToolkit: t_nounits as t, D_nounits as Dt +using ModelingToolkitBase, DiffEqBase, LinearAlgebra, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as Dt @parameters x @variables u(t) @@ -51,8 +51,8 @@ end # TEST: Function registration outside any modules. # ------------------------------------------------ -using ModelingToolkit, DiffEqBase, LinearAlgebra, Test -using ModelingToolkit: t_nounits as t, D_nounits as Dt +using ModelingToolkitBase, DiffEqBase, LinearAlgebra, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as Dt @parameters x @variables u(t) @@ -76,13 +76,13 @@ foo(x, y) = sin(x) * cos(y) D = Dt @register_symbolic foo(x, y) -using ModelingToolkit: value, arguments, operation +using ModelingToolkitBase: value, arguments, operation expr = value(foo(x, y)) @test operation(expr) === foo @test arguments(expr)[1] === value(x) @test arguments(expr)[2] === value(y) -ModelingToolkit.derivative(::typeof(foo), (x, y), ::Val{1}) = cos(x) * cos(y) # derivative w.r.t. the first argument -ModelingToolkit.derivative(::typeof(foo), (x, y), ::Val{2}) = -sin(x) * sin(y) # derivative w.r.t. the second argument +@register_derivative foo(x, y) 1 cos(x) * cos(y) +@register_derivative foo(x, y) 2 -sin(x) * sin(y) @test isequal(expand_derivatives(D(foo(x, y))), expand_derivatives(D(sin(x) * cos(y)))) # TEST: Function registration run from inside a function. @@ -110,7 +110,7 @@ function run_test() end run_test() -using ModelingToolkit: arguments +using ModelingToolkitBase: arguments @variables a @register_symbolic foo(x, y, z) @test 1 * foo(a, a, a) * Num(1) isa Num diff --git a/lib/ModelingToolkitBase/test/guess_propagation.jl b/lib/ModelingToolkitBase/test/guess_propagation.jl new file mode 100644 index 0000000000..cba2b353ea --- /dev/null +++ b/lib/ModelingToolkitBase/test/guess_propagation.jl @@ -0,0 +1,94 @@ +using ModelingToolkitBase, OrdinaryDiffEq +using ModelingToolkitBase: D_nounits as D, t_nounits as t +using Test + +# Standard case +@testset "Observed equation" begin + @variables x(t) [guess = 2] + @variables y(t) + eqs = [D(x) ~ 1] + initialization_eqs = [1 ~ exp(1 + x)] + + @named sys = System(eqs, t; initialization_eqs, observed = [y ~ x]) + sys = complete(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)) +end + +@testset "Guess via parameter" begin + @parameters a = -1.0 + @variables x(t) [guess = a] + + eqs = [D(x) ~ a] + + initialization_eqs = [1 ~ exp(1 + x)] + + @named sys = System(eqs, t; initialization_eqs) + sys = complete(mtkcompile(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)) +end + +if @isdefined(ModelingToolkit) + @testset "Guess via observed and parameter" begin + @parameters a = -1.0 + @variables x(t) + @variables y(t) [guess = a] + + eqs = [D(x) ~ a] + + initialization_eqs = [1 ~ exp(1 + x)] + + @named sys = System(eqs, t; initialization_eqs, observed = [y ~ x]) + sys = complete(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)) + end +end + +# Test parameters + defaults +# https://github.com/SciML/ModelingToolkit.jl/issues/2774 + +@parameters x0 +@variables x(t) +@variables y(t) = x +@mtkcompile sys = System([x ~ x0, D(y) ~ x], t) +prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) +@variables y(t) = x0 +@mtkcompile sys = System([x ~ x0, D(y) ~ x], t) +prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) +@variables y(t) = x0 +@mtkcompile sys = System([x ~ y, D(y) ~ x], t) +prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) +@test prob[x] == 1.0 +@test prob[y] == 1.0 + +@parameters x0 +@variables x(t) = x0 +@variables y(t) = x +@mtkcompile sys = System([x ~ y, D(y) ~ x], t) +prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) +@test prob[x] == 1.0 +@test prob[y] == 1.0 diff --git a/test/implicit_discrete_system.jl b/lib/ModelingToolkitBase/test/implicit_discrete_system.jl similarity index 77% rename from test/implicit_discrete_system.jl rename to lib/ModelingToolkitBase/test/implicit_discrete_system.jl index e4a3cad9ed..53bf86c752 100644 --- a/test/implicit_discrete_system.jl +++ b/lib/ModelingToolkitBase/test/implicit_discrete_system.jl @@ -1,5 +1,5 @@ -using ModelingToolkit, SymbolicIndexingInterface, Test -using ModelingToolkit: t_nounits as t +using ModelingToolkitBase, SymbolicIndexingInterface, Test +using ModelingToolkitBase: t_nounits as t using StableRNGs k = ShiftIndex(t) @@ -24,12 +24,14 @@ rng = StableRNG(22525) prob = ImplicitDiscreteProblem(sys, [x(k - 1) => 3.0], tspan) @test prob.u0 == [3.0, 1.0] - prob = ImplicitDiscreteProblem(sys, [], tspan) - @test prob.u0 == [1.0, 1.0] + prob = ImplicitDiscreteProblem(sys, [], tspan; guesses = [x(k-1) => 3.0]) + @test prob.u0 == [3.0, 1.0] @variables x(t) @mtkcompile sys = System([x(k) ~ x(k) * x(k - 1) - 3], t) - @test_throws ModelingToolkit.MissingGuessError prob=ImplicitDiscreteProblem( - sys, [], tspan) + if @isdefined(ModelingToolkit) + @test_throws ModelingToolkitBase.MissingGuessError prob=ImplicitDiscreteProblem( + sys, [], tspan) + end end @testset "System with algebraic equations" begin @@ -50,13 +52,15 @@ end for _ in 1:10 u_next = rand(rng, 3) u = rand(rng, 3) - @test correct_f(u_next, u, [], 0.0) ≈ f(u_next, u, [], 0.0) + @test reorderer(correct_f(reorderer(u_next), reorderer(u), [], 0.0)) ≈ f(u_next, u, [], 0.0) end # Initialization is satisfied. prob = ImplicitDiscreteProblem( sys, [x(k - 1) => 0.3, x(k - 2) => 0.4], (0, 10), guesses = [y => 1]) - @test length(equations(prob.f.initialization_data.initializeprob.f.sys)) == 1 + if @isdefined(ModelingToolkit) + @test length(equations(prob.f.initialization_data.initializeprob.f.sys)) == 1 + end end @testset "Handle observables in function codegen" begin @@ -64,7 +68,7 @@ end @variables x(t) y(t) z(t) eqs = [x(k) ~ x(k - 1) + x(k - 2), y(k) ~ x(k) + x(k - 2) * z(k - 1), - x + y + z ~ 2] + z ~ 2 - x - y] @mtkcompile sys = System(eqs, t) @test length(unknowns(sys)) == length(equations(sys)) == 3 @test occursin( diff --git a/test/index_cache.jl b/lib/ModelingToolkitBase/test/index_cache.jl similarity index 92% rename from test/index_cache.jl rename to lib/ModelingToolkitBase/test/index_cache.jl index 5573563d32..8530b2034f 100644 --- a/test/index_cache.jl +++ b/lib/ModelingToolkitBase/test/index_cache.jl @@ -1,5 +1,6 @@ -using ModelingToolkit, SymbolicIndexingInterface, SciMLStructures -using ModelingToolkit: t_nounits as t +using ModelingToolkitBase, SymbolicIndexingInterface, SciMLStructures +using ModelingToolkitBase: t_nounits as t +using Test # Ensure indexes of array symbolics are cached appropriately @variables x(t)[1:2] @@ -35,7 +36,7 @@ end @named sys = System(Equation[], t, [x, y, z], [p1, p2, p3]) sys = complete(sys) -ic = ModelingToolkit.get_index_cache(sys) +ic = ModelingToolkitBase.get_index_cache(sys) @test isequal(ic.symbol_to_variable[:p1], p1) @test isequal(ic.symbol_to_variable[:p2], p2) @@ -106,14 +107,14 @@ end tp1 = typeof(ParamTest(1)) @parameters (p_1::tp1)(..) = ParamTest(1) - @variables x(ModelingToolkit.t_nounits) = 0 + @variables x(ModelingToolkitBase.t_nounits) = 0 event1 = [1.0, 2, 3] => (f = update_affect!, modified = (; p_1)) @named sys = System([ - ModelingToolkit.D_nounits(x) ~ p_1(x) + ModelingToolkitBase.D_nounits(x) ~ p_1(x) ], - ModelingToolkit.t_nounits; + ModelingToolkitBase.t_nounits; discrete_events = [event1] ) ss = @test_nowarn complete(sys) diff --git a/test/initial_values.jl b/lib/ModelingToolkitBase/test/initial_values.jl similarity index 79% rename from test/initial_values.jl rename to lib/ModelingToolkitBase/test/initial_values.jl index f429777866..d594ff5e6d 100644 --- a/test/initial_values.jl +++ b/lib/ModelingToolkitBase/test/initial_values.jl @@ -1,9 +1,10 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D, get_u0 +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D, get_u0 using OrdinaryDiffEq using DataInterpolations using StaticArrays using SymbolicIndexingInterface +using Test @variables x(t)[1:3]=[1.0, 2.0, 3.0] y(t) z(t)[1:2] @@ -12,7 +13,6 @@ reorderer = getsym(sys, x) @test reorderer(get_u0(sys, [])) == [1.0, 2.0, 3.0] @test reorderer(get_u0(sys, [x => [2.0, 3.0, 4.0]])) == [2.0, 3.0, 4.0] @test reorderer(get_u0(sys, [x[1] => 2.0, x[2] => 3.0, x[3] => 4.0])) == [2.0, 3.0, 4.0] -@test get_u0(sys, [2.0, 3.0, 4.0]) == [2.0, 3.0, 4.0] @mtkcompile sys=System([ D(x)~3x, @@ -21,7 +21,7 @@ reorderer = getsym(sys, x) D(z[2])~y+z[1] ], t) simplify=false -@test_throws ModelingToolkit.MissingVariablesError get_u0(sys, []) +@test_throws ModelingToolkitBase.MissingVariablesError get_u0(sys, []) getter = getu(sys, [x..., y, z...]) @test getter(get_u0(sys, [y => 4.0, z => [5.0, 6.0]])) == collect(1.0:6.0) @test getter(get_u0(sys, [y => 4.0, z => [3y, 4y]])) == [1.0, 2.0, 3.0, 4.0, 12.0, 16.0] @@ -48,24 +48,23 @@ u_vals = [X => 3.0] var_vals = [p1 => 1.0, p2 => 2.0, X => 3.0] desired_values = [p1, p2, p3] defaults = Dict([p3 => X]) -vals = ModelingToolkit.varmap_to_vars(merge(defaults, Dict(var_vals)), desired_values) +vals = ModelingToolkitBase.varmap_to_vars(merge(defaults, Dict(var_vals)), desired_values) @test vals == [1.0, 2.0, 3.0] # Issue#2565 # Create ODESystem. @variables X1(t) X2(t) -@parameters k1 k2 Γ[1:1]=X1 + X2 +@parameters k1 k2 Γ[1:1]=missing eq = D(X1) ~ -k1 * X1 + k2 * (-X1 + Γ[1]) obs = X2 ~ Γ[1] - X1 -@mtkcompile osys_m = System([eq], t, [X1], [k1, k2, Γ[1]]; observed = [X2 ~ Γ[1] - X1]) +@mtkcompile osys_m = System([eq, obs], t, [X1, X2], [k1, k2, Γ]) # Creates ODEProblem. u0 = [X1 => 1.0, X2 => 2.0] tspan = (0.0, 1.0) ps = [k1 => 1.0, k2 => 5.0] -# Broken since we need both X1 and X2 to initialize Γ but this makes the initialization system -# overdetermined because parameter initialization isn't in yet -@test_warn "overdetermined" oprob=ODEProblem(osys_m, [u0; ps], tspan) +oprob=ODEProblem(osys_m, [u0; ps], tspan; guesses = [Γ[1] => 1.0]) +@test SciMLBase.successful_retcode(solve(oprob)) # Initialization of ODEProblem with dummy derivatives of multidimensional arrays # Issue#1283 @@ -74,49 +73,32 @@ eqs = [D(D(z)) ~ ones(2, 2)] @mtkcompile sys = System(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) -@mtkcompile sys = System( - [ - x1 ~ B1, - x2 ~ B2 - ], t; defaults = [ - A2 => 1 - A1, - B1 => A1, - B2 => A2 - ]) -prob = ODEProblem(sys, [A1 => 0.3], (0.0, 1.0)) -@test prob.ps[B1] == 0.3 -@test prob.ps[B2] == 0.7 - -@testset "default=nothing is skipped" begin +@testset "binding=nothing is skipped" begin @parameters p = nothing @variables x(t)=nothing y(t) - @named sys = System(Equation[], t, [x, y], [p]; defaults = [y => nothing]) - @test isempty(ModelingToolkit.defaults(sys)) + @named sys = System(Equation[], t, [x, y], [p]; bindings = [y => nothing]) + @test isempty(ModelingToolkitBase.bindings(sys)) end # Using indepvar in initialization # Issue#2799 @variables x(t) @parameters p -@mtkcompile sys = System([D(x) ~ p], t; defaults = [x => t, p => 2t]) +@mtkcompile sys = System([D(x) ~ p], t; bindings = [x => t, p => 2t]) prob = ODEProblem(sys, [], (1.0, 2.0)) @test prob[x] == 1.0 @test prob.ps[p] == 2.0 @testset "Array of symbolics is unwrapped" begin @variables x(t)[1:2] y(t) - @mtkcompile sys = System([D(x) ~ x, D(y) ~ t], t; defaults = [x => [y, 3.0]]) + @mtkcompile sys = System([D(x) ~ x, D(y) ~ t], t; initial_conditions = [x => [y, 3.0]]) prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) @test eltype(prob.u0) <: Float64 prob = ODEProblem(sys, [x => [y, 4.0], y => 2.0], (0.0, 1.0)) @test eltype(prob.u0) <: Float64 end -@testset "split=false systems with all parameter defaults" begin +@testset "split=false systems with all parameter initial conditions" begin @variables x(t) = 1.0 @parameters p=1.0 q=2.0 r=3.0 @mtkcompile sys=System(D(x)~p*x+q*t+r, t) split=false @@ -142,11 +124,8 @@ end @mtkcompile sys = System( [D(x) ~ x, D(y) ~ y], t; initialization_eqs = [x ~ 2y + 3, y ~ 2x], guesses = [x => 2y, y => 2x]) - @test_warn ["Cycle", "unknowns", "x", "y"] try - ODEProblem(sys, [], (0.0, 1.0), warn_cyclic_dependency = true) - catch - end - @test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem( + @test_warn ["Cycle", "unknowns", "x", "y"] ODEProblem(sys, [], (0.0, 1.0), warn_cyclic_dependency = true) + @test_throws ModelingToolkitBase.UnexpectedSymbolicValueInVarmap ODEProblem( sys, [x => 2y + 1, y => 2x], (0.0, 1.0); build_initializeprob = false, substitution_limit = 10) @@ -154,13 +133,8 @@ end @mtkcompile sys = System( [D(x) ~ x * p, D(y) ~ y * q], t; guesses = [p => 1.0, q => 2.0]) # "unknowns" because they are initialization unknowns - @test_warn ["Cycle", "unknowns", "p", "q"] try - ODEProblem(sys, [x => 1, y => 2, p => 2q, q => 3p], + @test_warn ["Cycle", "parameters", "p", "q"] ODEProblem(sys, [x => 1, y => 2, p => 2q, q => 3p], (0.0, 1.0); warn_cyclic_dependency = true) - catch - end - @test_throws ModelingToolkit.MissingGuessError ODEProblem( - sys, [x => 1, y => 2, p => 2q, q => 3p], (0.0, 1.0)) end @testset "`add_fallbacks!` checks scalarized array parameters correctly" begin @@ -168,7 +142,7 @@ end @parameters p[1:2, 1:2] @mtkcompile sys = System(D(x) ~ p * x, t) # used to throw a `MethodError` complaining about `getindex(::Nothing, ::CartesianIndex{2})` - @test_throws ModelingToolkit.MissingParametersError ODEProblem( + @test_throws "Could not evaluate" ODEProblem( sys, [x => ones(2)], (0.0, 1.0)) end @@ -180,22 +154,34 @@ end y[1] ~ x[3], y[2] ~ x[4] ] - @mtkcompile sys = System(eqs, t; defaults = [x => vcat(ones(2), y), y => x[1:2] ./ 2]) + @mtkcompile sys = System(eqs, t; bindings = [x => vcat(ones(2), y), y => x[1:2] ./ 2]) prob = ODEProblem(sys, [], (0.0, 1.0)) sol = solve(prob) @test SciMLBase.successful_retcode(sol) @test sol[x, 1] ≈ [1.0, 1.0, 0.5, 0.5] end +# Manually do index reduction to allow testing with MTKBase +function index_reduced_pend(; name) + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) yˍt(t) xˍt(t) xˍtt(t) + eqs = [ + 0 ~ 1 - y^2 - x^2 + D(y) ~ yˍt + 0 ~ -yˍt*y - xˍt*x + D(yˍt) ~ -g + y*λ + 0 ~ -yˍt^2 - xˍt^2 - x*xˍtt - y*(-g + y*λ) + ] + System(eqs, t, [xˍt, y, x, yˍt, λ], [g]; observed = [xˍtt ~ λ*x], name) +end + @testset "Missing/cyclic guesses throws error" begin @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] - @mtkcompile pend = System(eqs, t) + @named pend = index_reduced_pend() + pend = complete(pend) - @test_throws ModelingToolkit.MissingGuessError ODEProblem( + @test_throws ModelingToolkitBase.MissingGuessError ODEProblem( pend, [x => 1, g => 1], (0, 1), guesses = [y => λ, λ => y + 1]) ODEProblem(pend, [x => 1, g => 1], (0, 1), guesses = [y => λ, λ => 0.5]) @@ -324,12 +310,13 @@ end @testset "`p_constructor` keyword argument" begin @parameters g = 1.0 @variables x(t) y(t) [state_priority = 10, guess = 1.0] λ(t) [guess = 1.0] - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - @mtkcompile pend = System(eqs, t) + @named pend = index_reduced_pend() + pend = complete(pend) + guesses(pend)[y] = 1.0 + guesses(pend)[λ] = 1.0 + initial_conditions(pend)[g] = 1.0 - u0 = [x => 1.0, D(x) => 0.0] + u0 = [y => -1.0, D(y) => 0.0] u0_constructor = p_constructor = vals -> SVector{length(vals)}(vals...) tspan = (0.0, 5.0) prob = ODEProblem(pend, u0, tspan; u0_constructor, p_constructor) @@ -340,7 +327,7 @@ end @test state_values(initdata.initializeprob) isa SVector @test parameter_values(initdata.initializeprob).tunable isa SVector - @mtkcompile pend=System(eqs, t) split=false + pend = complete(pend; split=false) prob = ODEProblem(pend, u0, tspan; u0_constructor, p_constructor) @test prob.p isa SVector initdata = prob.f.initialization_data diff --git a/test/initializationsystem.jl b/lib/ModelingToolkitBase/test/initializationsystem.jl similarity index 56% rename from test/initializationsystem.jl rename to lib/ModelingToolkitBase/test/initializationsystem.jl index 40ea02f582..79f616bca9 100644 --- a/test/initializationsystem.jl +++ b/lib/ModelingToolkitBase/test/initializationsystem.jl @@ -1,71 +1,97 @@ -using ModelingToolkit, OrdinaryDiffEq, NonlinearSolve, Test +using ModelingToolkitBase, OrdinaryDiffEq, NonlinearSolve, Test using StochasticDiffEq, DelayDiffEq, StochasticDelayDiffEq, JumpProcesses using ForwardDiff, StaticArrays using SymbolicIndexingInterface, SciMLStructures using SciMLStructures: Tunable -using ModelingToolkit: t_nounits as t, D_nounits as D, observed +using ModelingToolkitBase: t_nounits as t, D_nounits as D, observed using DynamicQuantities using DiffEqBase: BrownFullBasicInit +import DiffEqNoiseProcess +using Setfield: @set! +using SciCompDSL + +const ERRMOD = @isdefined(ModelingToolkit) ? ModelingToolkit.StateSelection : ModelingToolkitBase @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] -@mtkcompile pend = System(eqs, t) - -initprob = ModelingToolkit.InitializationProblem(pend, 0.0, [g => 1]; - guesses = [ModelingToolkit.missing_variable_defaults(pend); x => 1; y => 0.2]) +@variables x(t) y(t) [state_priority = 10] λ(t) yˍt(t) xˍt(t) xˍtt(t) +# Manually do index reduction to allow testing with MTKBase +function index_reduced_pend(; name) + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) yˍt(t) xˍt(t) xˍtt(t) + if @isdefined(ModelingToolkit) + eqs = [ + D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1 + ] + mtkcompile(System(eqs, t; name)) + else + eqs = [ + 0 ~ 1 - y^2 - x^2 + D(y) ~ yˍt + # The `2` multiplier here affects the very tight tolerances in the + # tests after this + 0 ~ -2yˍt*y - 2xˍt*x + D(yˍt) ~ -g + y*λ + 0 ~ -2yˍt^2 - 2xˍt^2 - 2x*xˍtt - 2y*(-g + y*λ) + ] + System(eqs, t, [xˍt, y, x, yˍt, λ], [g]; observed = [xˍtt ~ λ*x], name) + end +end +@named pend = index_reduced_pend() +pend = complete(pend) + +# Without MTK, guesses fall back to 1.0 +# With MTK, we have dummy derivative information +# Guesses for D(x) and D(y) should not be required +initprob = ModelingToolkitBase.InitializationProblem(pend, 0.0, [g => 1]; + guesses = [x => 1, y => 0.2, λ => 0.0]) conditions = getfield.(equations(initprob.f.sys), :rhs) @test initprob isa NonlinearLeastSquaresProblem sol = solve(initprob) @test SciMLBase.successful_retcode(sol) -@test maximum(abs.(sol[conditions])) < 1e-14 +@test maximum(abs.(sol[conditions])) < 5e-14 -@test_throws ModelingToolkit.ExtraVariablesSystemException ModelingToolkit.InitializationProblem( +@test_throws ERRMOD.ExtraVariablesSystemException ModelingToolkitBase.InitializationProblem( pend, 0.0, [g => 1]; - guesses = [ModelingToolkit.missing_variable_defaults(pend); x => 1; y => 0.2], + guesses = [x => 1, y => 0.2, λ => 0.0], fully_determined = true) -initprob = ModelingToolkit.InitializationProblem(pend, 0.0, [x => 1, y => 0, g => 1]; - guesses = ModelingToolkit.missing_variable_defaults(pend)) -@test initprob isa NonlinearLeastSquaresProblem -sol = solve(initprob) -@test SciMLBase.successful_retcode(sol) -@test all(iszero, sol.u) -@test maximum(abs.(sol[conditions])) < 1e-14 - -initprob = ModelingToolkit.InitializationProblem( - pend, 0.0, [g => 1]; guesses = ModelingToolkit.missing_variable_defaults(pend)) +initprob = ModelingToolkitBase.InitializationProblem( + pend, 0.0, [g => 1]; guesses = [x => 0, y => 0.0, λ => 0.0, D(x) => 0.0, D(y) => 0.0]) @test initprob isa NonlinearLeastSquaresProblem sol = solve(initprob) @test !SciMLBase.successful_retcode(sol) || sol.retcode == SciMLBase.ReturnCode.StalledSuccess -@test_throws ModelingToolkit.ExtraVariablesSystemException ModelingToolkit.InitializationProblem( - pend, 0.0, [g => 1]; guesses = ModelingToolkit.missing_variable_defaults(pend), +@test_throws ERRMOD.ExtraVariablesSystemException ModelingToolkitBase.InitializationProblem( + pend, 0.0, [g => 1]; guesses = [x => 1, y => 0.2, λ => 0.0], fully_determined = true) prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 1.5), - guesses = ModelingToolkit.missing_variable_defaults(pend)) + guesses = [x => 1, y => 0.2, λ => 0.0]) prob.f.initializeprob isa NonlinearProblem sol = solve(prob.f.initializeprob) -@test maximum(abs.(sol[conditions])) < 1e-14 -sol = solve(prob, Rodas5P()) -@test maximum(abs.(sol[conditions][1])) < 1e-14 +@test maximum(abs.(sol[conditions])) < 5e-14 +sol = solve(prob, Rodas5P(); abstol = 1e-14) +if @isdefined(ModelingToolkit) + @test maximum(abs.(sol[conditions][1])) < 1e-14 +else + @test maximum(abs.(sol[conditions][1])) < 1e-6 +end prob = ODEProblem(pend, [x => 1, g => 1], (0.0, 1.5), - guesses = ModelingToolkit.missing_variable_defaults(pend)) + guesses = [x => 1, y => 0.2, λ => 0.0]) prob.f.initializeprob isa NonlinearLeastSquaresProblem sol = solve(prob.f.initializeprob) -@test maximum(abs.(sol[conditions])) < 1e-14 +@test maximum(abs.(sol[conditions])) < 2e-13 sol = solve(prob, Rodas5P()) -@test maximum(abs.(sol[conditions][1])) < 1e-14 +@test maximum(abs.(sol[conditions][1])) < 1e-6 -@test_throws ModelingToolkit.ExtraVariablesSystemException ODEProblem( +@test_throws ERRMOD.ExtraVariablesSystemException ODEProblem( pend, [x => 1, g => 1], (0.0, 1.5), - guesses = ModelingToolkit.missing_variable_defaults(pend), + guesses = [x => 1, y => 0.2, λ => 0.0], fully_determined = true) @connector Port begin @@ -237,35 +263,44 @@ end end @mtkcompile sys = HydraulicSystem() -initprob = ModelingToolkit.InitializationProblem(sys, 0.0) +initprob = ModelingToolkitBase.InitializationProblem(sys, 0.0) conditions = getfield.(equations(initprob.f.sys), :rhs) @test initprob isa NonlinearLeastSquaresProblem -@test length(initprob.u0) == 2 -initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) -@test SciMLBase.successful_retcode(initsol) -@test maximum(abs.(initsol[conditions])) < 1e-14 +if @isdefined(ModelingToolkit) + @test length(initprob.u0) == 4 + initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) + @test SciMLBase.successful_retcode(initsol) + @test maximum(abs.(initsol[conditions])) < 5e-14 +else + @test length(initprob.u0) == 63 + initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) + @test SciMLBase.successful_retcode(initsol) + @test maximum(abs.(initsol[conditions])) < 5e-13 +end -@test_throws ModelingToolkit.ExtraEquationsSystemException ModelingToolkit.InitializationProblem( +@test_throws ERRMOD.ExtraEquationsSystemException ModelingToolkitBase.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()) -# If initialized incorrectly, then it would be InitialFailure -@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) +if @isdefined(ModelingToolkit) + allinit = unknowns(sys) .=> initsol[unknowns(sys)] + prob = ODEProblem(sys, allinit, (0, 0.1); build_initializeprob = false) + sol = solve(prob, Rodas5P(), initializealg = BrownFullBasicInit()) + # If initialized incorrectly, then it would be InitialFailure + @test sol.retcode == SciMLBase.ReturnCode.Unstable + @test maximum(abs.(initsol[conditions][1])) < 1e-14 +end -@test_throws ModelingToolkit.ExtraEquationsSystemException ODEProblem( +@test_throws ERRMOD.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 -@test maximum(abs.(initsol[conditions][1])) < 1e-14 +if @isdefined(ModelingToolkit) + prob = ODEProblem(sys, [], (0, 0.1), check = false) + sol = solve(prob, Rodas5P()) + # If initialized incorrectly, then it would be InitialFailure + @test sol.retcode == SciMLBase.ReturnCode.Unstable + @test maximum(abs.(initsol[conditions][1])) < 1e-14 +end @connector Flange begin dx(t), [guess = 0] @@ -324,93 +359,86 @@ end end end -@mtkcompile sys = MassDamperSystem() -initprob = ModelingToolkit.InitializationProblem(sys, 0.0) -@test initprob isa NonlinearProblem -initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) -@test SciMLBase.successful_retcode(initsol) - -allinit = unknowns(sys) .=> initsol[unknowns(sys)] -prob = ODEProblem(sys, allinit, (0, 0.1)) -sol = solve(prob, Rodas5P()) -# If initialized incorrectly, then it would be InitialFailure -@test sol.retcode == SciMLBase.ReturnCode.Success +if @isdefined(ModelingToolkit) + @mtkcompile sys = MassDamperSystem() + initprob = ModelingToolkit.InitializationProblem(sys, 0.0) + @test initprob isa NonlinearProblem + initsol = solve(initprob, reltol = 1e-12, abstol = 1e-12) + @test SciMLBase.successful_retcode(initsol) -prob = ODEProblem(sys, [], (0, 0.1)) -sol = solve(prob, Rodas5P()) -@test sol.retcode == SciMLBase.ReturnCode.Success - -### Ensure that non-DAEs still throw for missing variables without the initialize system - -@parameters σ ρ β -@variables x(t) y(t) z(t) + allinit = unknowns(sys) .=> initsol[unknowns(sys)] + prob = ODEProblem(sys, allinit, (0, 0.1)) + sol = solve(prob, Rodas5P()) + # If initialized incorrectly, then it would be InitialFailure + @test sol.retcode == SciMLBase.ReturnCode.Success -eqs = [D(D(x)) ~ σ * (y - x), - D(y) ~ x * (ρ - z) - y, - D(z) ~ x * y - β * z] + prob = ODEProblem(sys, [], (0, 0.1)) + sol = solve(prob, Rodas5P()) + @test sol.retcode == SciMLBase.ReturnCode.Success +end -@mtkcompile sys = System(eqs, t) +@testset "Ensure non-DAEs throw for missing variables without initsys" begin + @parameters σ ρ β + @variables x(t) y(t) z(t) -u0 = [D(x) => 2.0, - y => 0.0, - z => 0.0] + eqs = [D(D(x)) ~ σ * (y - x), + D(y) ~ x * (ρ - z) - y, + D(z) ~ x * y - β * z] -p = [σ => 28.0, - ρ => 10.0, - β => 8 / 3] + @mtkcompile sys = System(eqs, t) -tspan = (0.0, 100.0) -@test_throws ModelingToolkit.IncompleteInitializationError prob=ODEProblem( - sys, [u0; p], tspan, jac = true) + u0 = [y => 0.0, + z => 0.0] + p = [σ => 28.0, + ρ => 10.0, + β => 8 / 3] -u0 = [y => 0.0, - z => 0.0] -@test_throws "Differential(t)(x(t))" prob=ODEProblem( - sys, [u0; p], tspan, jac = true) + tspan = (0.0, 100.0) + @test_throws "Differential(t, 1)(x(t))" prob=ODEProblem( + sys, [u0; p], tspan, jac = true) +end # DAE Initialization on ODE with nonlinear system for initial conditions # https://github.com/SciML/ModelingToolkit.jl/issues/2508 - -using ModelingToolkit, OrdinaryDiffEq, Test -using ModelingToolkit: t_nounits as t, D_nounits as D - -function System2(; name) - vars = @variables begin - dx(t), [guess = 0] - ddx(t), [guess = 0] +@testset "DAE Initialization on ODE" begin + function System2(; name) + vars = @variables begin + dx(t), [guess = 0] + ddx(t), [guess = 0] + end + eqs = [D(dx) ~ ddx] + return System(eqs, t, [dx], []; name, observed = [ddx ~ -1-dx]) end - eqs = [D(dx) ~ ddx - 0 ~ ddx + dx + 1] - return System(eqs, t, vars, []; name) -end - -@mtkcompile sys = System2() -prob = ODEProblem(sys, [sys.dx => 1], (0, 1)) # OK -prob = ODEProblem(sys, [sys.ddx => -2], (0, 1), guesses = [sys.dx => 1]) -sol = solve(prob, Tsit5()) -@test SciMLBase.successful_retcode(sol) -@test sol.u[1] == [1.0] -## Late binding initialization_eqs + @named sys = System2() + sys = complete(sys) + prob = ODEProblem(sys, [sys.dx => 1], (0, 1)) # OK + prob = ODEProblem(sys, [sys.ddx => -2], (0, 1), guesses = [sys.dx => 1]) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + @test sol.u[1] == [1.0] +end -function System3(; name) - vars = @variables begin - dx(t), [guess = 0] - ddx(t), [guess = 0] +@testset "Late binding initialization_eqs" begin + function System3(; name) + vars = @variables begin + dx(t), [guess = 0] + ddx(t), [guess = 0] + end + eqs = [D(dx) ~ ddx] + initialization_eqs = [ + ddx ~ -2 + ] + return System(eqs, t, [dx], []; name, initialization_eqs, observed = [ddx ~ -1-dx]) end - eqs = [D(dx) ~ ddx - 0 ~ ddx + dx + 1] - initialization_eqs = [ - ddx ~ -2 - ] - return System(eqs, t, vars, []; name, initialization_eqs) -end -@mtkcompile sys = System3() -prob = ODEProblem(sys, [], (0, 1), guesses = [sys.dx => 1]) -sol = solve(prob, Tsit5()) -@test SciMLBase.successful_retcode(sol) -@test sol.u[1] == [1.0] + @named sys = System3() + sys = complete(sys) + prob = ODEProblem(sys, [], (0, 1), guesses = [sys.dx => 1]) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + @test sol.u[1] == [1.0] +end # Steady state initialization @testset "Steady state initialization" begin @@ -436,22 +464,21 @@ sol = solve(prob, Tsit5()) tspan = (0.0, 0.2) prob_mtk = ODEProblem(sys, [u0; p], tspan) sol = solve(prob_mtk, Tsit5()) - @test sol[x * (ρ - z) - y][1] == 0.0 + @test sol[x * (ρ - z) - y][1] ≈ 0.0 atol=1e-10 prob_mtk.ps[Initial(D(y))] = 1.0 sol = solve(prob_mtk, Tsit5()) - @test sol[x * (ρ - z) - y][1] == 1.0 + @test sol[x * (ρ - z) - y][1] ≈ 1.0 end @variables x(t) y(t) z(t) @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 eqs = [D(x) ~ α * x - β * x * y - D(y) ~ -γ * y + δ * x * y - z ~ x + y] + D(y) ~ -γ * y + δ * x * y] -@named sys = System(eqs, t) -simpsys = mtkcompile(sys) +@named simpsys = System(eqs, t, [x, y], [α, β, γ, δ]; observed = [z ~ x + y]) +simpsys = complete(simpsys) tspan = (0.0, 10.0) prob = ODEProblem(simpsys, [D(x) => 0.0, y => 0.0], tspan, guesses = [x => 0.0]) @@ -465,53 +492,57 @@ sol = solve(prob, Tsit5()) prob = ODEProblem(simpsys, [z => 1.0, y => 1.0], tspan, guesses = [x => 2.0]) sol = solve(prob, Tsit5()) -@test sol[[x, y], 1] == [0.0, 1.0] +@test sol[[x, y], 1] ≈ [0.0, 1.0] @test_warn "underdetermined" prob = ODEProblem( simpsys, [], tspan, guesses = [x => 2.0, y => 1.0]) -# Late Binding initialization_eqs -# https://github.com/SciML/ModelingToolkit.jl/issues/2787 +@testset "Late Binding initialization_eqs" begin + # 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] -@mtkcompile pend = System(eqs, t) + @named pend = index_reduced_pend() + pend = complete(pend) -prob = ODEProblem(pend, [x => 1, g => 1], (0.0, 1.5), - guesses = [λ => 0, y => 1], initialization_eqs = [y ~ 1]) - -unsimp = generate_initializesystem(pend; op = [x => 1], initialization_eqs = [y ~ 1]) -sys = mtkcompile(unsimp; fully_determined = false) -@test length(equations(sys)) in (3, 4) # could be either depending on tearing - -# Extend two systems with initialization equations and guesses -# https://github.com/SciML/ModelingToolkit.jl/issues/2845 -@variables x(t) y(t) -@named sysx = System([D(x) ~ 0], t; initialization_eqs = [x ~ 1]) -@named sysy = System([D(y) ~ 0], t; initialization_eqs = [y^2 ~ 2], guesses = [y => 1]) -sys = complete(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 + prob = ODEProblem(pend, [x => 1, g => 1], (0.0, 1.5), + guesses = [λ => 0, y => 1], initialization_eqs = [y ~ 1]) + + unsimp = generate_initializesystem(pend; op = [x => 1], initialization_eqs = [y ~ 1]) + sys = mtkcompile(unsimp; fully_determined = false) + if @isdefined(ModelingToolkit) + @test length(equations(sys)) in (3, 4, 5) # depending on tearing + else + @test length(equations(sys)) == 7 + end +end + +@testset "Extend two systems with initialization equations and guesses" begin + # https://github.com/SciML/ModelingToolkit.jl/issues/2845 @variables x(t) y(t) - @named sys = System([x^2 + y^2 ~ 25, D(x) ~ 1], t) - ssys = mtkcompile(sys) - @test_throws ModelingToolkit.MissingGuessError ODEProblem( - ssys, [x => 3], (0, 1)) # y should have a guess + @named sysx = System([D(x) ~ 0], t; initialization_eqs = [x ~ 1]) + @named sysy = System([D(y) ~ 0], t; initialization_eqs = [y^2 ~ 2], guesses = [y => 1]) + sys = complete(extend(sysx, sysy)) + @test length(equations(generate_initializesystem(sys))) == 4 + @test length(ModelingToolkitBase.guesses(sys)) == 1 +end + +if @isdefined(ModelingToolkit) + # https://github.com/SciML/ModelingToolkit.jl/issues/2873 + @testset "Error on missing guesses" begin + @variables x(t) y(t) + @named sys = System([x^2 + y^2 ~ 25, D(x) ~ 1], t) + ssys = mtkcompile(sys) + @test_throws ModelingToolkitBase.MissingGuessError ODEProblem( + ssys, [x => 3], (0, 1)) # y should have a guess + end end # https://github.com/SciML/ModelingToolkit.jl/issues/3025 -@testset "Override defaults when setting initial conditions with unknowns(sys) or similar" begin +@testset "Override initial conditions when setting initial conditions with unknowns(sys) or similar" begin @variables x(t) y(t) # system 1 should solve to x = 1 ics1 = [x => 1] - sys1 = System([D(x) ~ 0], t; defaults = ics1, name = :sys1) |> mtkcompile + sys1 = System([D(x) ~ 0], t; initial_conditions = ics1, name = :sys1) |> mtkcompile prob1 = ODEProblem(sys1, [], (0.0, 1.0)) sol1 = solve(prob1, Tsit5()) @test all(sol1[x] .== 1) @@ -523,8 +554,10 @@ end ) |> mtkcompile ics2 = unknowns(sys1) .=> 2 # should be equivalent to "ics2 = [x => 2]" prob2 = ODEProblem(sys2, ics2, (0.0, 1.0); fully_determined = true) - sol2 = solve(prob2, Tsit5()) - @test all(sol2[x] .== 2) && all(sol2[y] .== 2) + sol2 = solve(prob2, Tsit5(); abstol = 1e-6, reltol = 1e-6) + @test SciMLBase.successful_retcode(sol2) + @test sol2[x] ≈ 2ones(length(sol2.t)) atol=1e-6 rtol=1e-6 + @test sol2[y] ≈ 2ones(length(sol2.t)) atol=1e-6 rtol=1e-6 end # https://github.com/SciML/ModelingToolkit.jl/issues/3029 @@ -536,7 +569,7 @@ end @test_nowarn ODEProblem(sys, [], (0.0, 1.0)) sys = System( - [D(D(x)) ~ 0], t; initialization_eqs = [x ~ 0, D(D(x)) ~ 0], name = :sys) |> + [D(D(x)) ~ 0], t; initialization_eqs = [x ~ 0], name = :sys) |> mtkcompile @test_nowarn ODEProblem(sys, [D(x) => 1.0], (0.0, 1.0)) end @@ -551,16 +584,16 @@ end ) |> mtkcompile prob = ODEProblem(sys, [], (0.0, 1.0)) sol = solve(prob, Tsit5()) - @test sol(1.0, idxs = sys.x) ≈ sign # system with D(x(0)) = ±1 should solve to x(1) = ±1 + @test sol(1.0, idxs = sys.x) ≈ sign atol=1e-6 # system with D(x(0)) = ±1 should solve to x(1) = ±1 end end # https://github.com/SciML/ModelingToolkit.jl/issues/2619 @parameters k1 k2 ω @variables X(t) Y(t) -eqs_1st_order = [D(Y) + Y - ω ~ 0, +eqs_1st_order = [D(Y) ~ ω - Y, X + k1 ~ Y + k2] -eqs_2nd_order = [D(D(Y)) + 2ω * D(Y) + (ω^2) * Y ~ 0, +eqs_2nd_order = [D(D(Y)) ~ -2ω * D(Y) - (ω^2) * Y, X + k1 ~ Y + k2] @mtkcompile sys_1st_order = System(eqs_1st_order, t) @mtkcompile sys_2nd_order = System(eqs_2nd_order, t) @@ -583,16 +616,17 @@ oprob_2nd_order_2 = ODEProblem(sys_2nd_order, [u0_2nd_order_2; ps], tspan) @test solve(oprob_2nd_order_1, Rosenbrock23()).retcode == SciMLBase.ReturnCode.InitialFailure sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success -@test sol[Y][1] == 2.0 -@test sol[D(Y)][1] == 0.5 +@test sol[Y][1] ≈ 2.0 +@test sol[D(Y)][1] ≈ 0.5 @testset "Vector in initial conditions" begin @variables x(t)[1:5] y(t)[1:5] @named sys = System([D(x) ~ x, D(y) ~ y], t; initialization_eqs = [y ~ -x]) sys = mtkcompile(sys) prob = ODEProblem(sys, [sys.x => ones(5)], (0.0, 1.0)) - sol = solve(prob, Tsit5(), reltol = 1e-4) - @test all(sol(1.0, idxs = sys.x) .≈ +exp(1)) && all(sol(1.0, idxs = sys.y) .≈ -exp(1)) + sol = solve(prob, Tsit5(), reltol = 1e-8) + @test sol(1.0; idxs = sys.x) ≈ fill(exp(1), 5) atol=1e-6 + @test sol(1.0; idxs = sys.y) ≈ fill(-exp(1), 5) atol=1e-6 end @testset "Initialization of parameters" begin @@ -619,163 +653,124 @@ end if ctor !== identity Problem = Problem{false} end - function test_parameter(prob, sym, val) - if prob.u0 !== nothing - @test prob.u0 isa expectedT - @test init(prob, alg).ps[sym] ≈ val - end - @test prob.p.tunable isa expectedT - initprob = prob.f.initialization_data.initializeprob - if state_values(initprob) !== nothing - @test state_values(initprob) isa expectedT - end - @test parameter_values(initprob).tunable isa expectedT - @test solve(prob, alg).ps[sym] ≈ val + browns = alg === ImplicitEM() ? [a, b] : [] + # Simple case + @named sys = System([D(x) ~ p * x + rhss[1]], t, [x], [p, q], browns; bindings = [p => missing, q => 2p], guesses = [q => 10.0], observed = [y ~ q * x]) + if alg === ImplicitEM() + tmp = mtkcompile(sys) + neqs = ModelingToolkitBase.get_noise_eqs(tmp) + @set! sys.noise_eqs = neqs + @set! sys.brownians = typeof(x)[] + @set! sys.eqs = [D(x) ~ p * x + rhss[1] - neqs[1, 1] * a] end - function test_initializesystem(prob, p, equation) - isys = prob.f.initialization_data.initializeprob.f.sys - @test is_variable(isys, p) || ModelingToolkit.has_observed_with_lhs(isys, p) - @test equation in [equations(isys); observed(isys)] + sys = complete(sys) + prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0); u0_constructor, p_constructor) + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6) + @test integ.ps[p] ≈ 0.5 atol=1e-6 + @test integ.ps[q] ≈ 1.0 atol=1e-6 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT + + # Specify `p`, solve for `y` + prob = Problem(sys, [x => 1.0, p => 1.0], (0.0, 1.0); u0_constructor, p_constructor) + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6) + @test integ.ps[p] ≈ 1.0 + @test integ.ps[q] ≈ 2.0 + @test integ[y] ≈ 2.0 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT + + # Solve for either + @mtkcompile sys = System([D(x) ~ p * x + rhss[1], D(y) ~ q * y + rhss[2]], t; + bindings = [p => missing, q => missing], + initialization_eqs = [p ~ 3 * q^2], guesses = [q => 10.0]) + # Specify `p` + prob = Problem(sys, [x => 1.0, y => 1.0, p => 12.0], (0.0, 1.0); u0_constructor, p_constructor) + if !@isdefined(ModelingToolkit) + @test prob.f.initialization_data.initializeprob[q] ≈ 10.0 atol=1e-10 + end + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6) + @test integ.ps[p] ≈ 12.0 atol=1e-6 + @test integ.ps[q] ≈ 2.0 atol=1e-6 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT + + # Specify `q` + prob = Problem(sys, [x => 1.0, y => 1.0, q => 3.0], (0.0, 1.0); u0_constructor, p_constructor) + # Specify `initializealg` because the default for OOP form is SimpleTrustRegion which + # doesn't converge with the un-torn system in MTKBase. + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6, initializealg = SciMLBase.OverrideInit(; nlsolve = NewtonRaphson())) + @test integ.ps[p] ≈ 27.0 atol=1e-6 + @test integ.ps[q] ≈ 3.0 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT + + if !(expectedT === SVector) + # Mutation + prob.ps[Initial(q)] = 2.0 + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6) + @test integ.ps[p] ≈ 12.0 atol=1e-6 + @test integ.ps[q] ≈ 2.0 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT end - u0map = Dict(x => 1.0, y => 1.0) - pmap = Dict() - pmap[q] = 1.0 - # `missing` default, equation from Problem - @mtkcompile sys = System( - [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => missing], guesses = [p => 1.0]) - pmap[p] = 2q - prob = Problem(sys, merge(u0map, pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 2.0) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2 = remake(prob2; p = setp_oop(prob2, p)(prob2, 0.0)) - test_parameter(prob2, p, 2.0) - # `missing` default, provided guess - @mtkcompile sys = System( - [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [p => 0.0]) - prob = Problem(sys, u0map, (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 2.0) - test_initializesystem(prob, p, p ~ x + y) - prob2 = remake(prob; u0 = u0map) - prob2 = remake(prob2; p = setp_oop(prob2, p)(prob2, 0.0)) - test_parameter(prob2, p, 2.0) - - # `missing` to Problem, equation from default - @mtkcompile sys = System( - [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) - pmap[p] = missing - prob = Problem(sys, merge(u0map, pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 2.0) - test_initializesystem(prob, p, p ~ 2q) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2 = remake(prob2; p = setp_oop(prob2, p)(prob2, 0.0)) - test_parameter(prob2, p, 2.0) - # `missing` to Problem, provided guess - @mtkcompile sys = System( - [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) - prob = Problem(sys, merge(u0map, pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 2.0) - test_initializesystem(prob, p, p ~ x + y) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2 = remake(prob2; p = setp_oop(prob2, p)(prob2, 0.0)) - test_parameter(prob2, p, 2.0) - - # No `missing`, default and guess - @mtkcompile sys = System( - [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 0.0]) - delete!(pmap, p) - prob = Problem(sys, merge(u0map, pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 2.0) - test_initializesystem(prob, p, p ~ 2q) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2 = remake(prob2; p = setp_oop(prob2, p)(prob2, 0.0)) - test_parameter(prob2, p, 2.0) - - # Default overridden by Problem, guess provided - @mtkcompile sys = System( - [D(x) ~ q * x + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) - _pmap = merge(pmap, Dict(p => q)) - prob = Problem(sys, merge(u0map, _pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, _pmap[q]) - test_initializesystem(prob, p, p ~ q) - # Problem dependent value with guess, no `missing` - @mtkcompile sys = System( - [D(x) ~ y * q + p + rhss[1], D(y) ~ x * p + q + rhss[2]], t; guesses = [p => 0.0]) - _pmap = merge(pmap, Dict(p => 3q)) - prob = Problem(sys, merge(u0map, _pmap), (0.0, 1.0); u0_constructor, p_constructor) - test_parameter(prob, p, 3pmap[q]) + # Make parameter solvable in `ODEProblem` ctor + @named sys = System([D(x) ~ p * x + rhss[1]], t, [x], [p, q], browns; + guesses = [y => 1.0, p => 1.0, q => 1.0], observed = [y ~ q * x + p]) + if alg === ImplicitEM() + tmp = mtkcompile(sys) + neqs = ModelingToolkitBase.get_noise_eqs(tmp) + @set! sys.noise_eqs = neqs + @set! sys.brownians = typeof(x)[] + @set! sys.eqs = [D(x) ~ p * x + rhss[1] - neqs[1, 1] * a] + end + sys = complete(sys) + prob = Problem(sys, [x => 2.0, y => 4.0, p => 1.0, q => missing], (0.0, 1.0); u0_constructor, p_constructor) + integ = init(prob, alg; abstol = 1e-6, reltol = 1e-6) + @test integ.ps[p] ≈ 1.0 + @test integ.ps[q] ≈ 1.5 + @test state_values(integ) isa expectedT + @test parameter_values(integ).tunable isa expectedT + end - # Should not be solved for: - # Override dependent default with direct value - @mtkcompile sys = System( - [D(x) ~ q * x + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) - _pmap = merge(pmap, Dict(p => 1.0)) - prob = Problem(sys, merge(u0map, _pmap), (0.0, 1.0); u0_constructor, p_constructor) - @test prob.ps[p] ≈ 1.0 - initsys = prob.f.initialization_data.initializeprob.f.sys - @test is_parameter(initsys, p) - - # Non-floating point - @parameters r::Int s::Int - @mtkcompile sys = System( - [D(x) ~ s * x + rhss[1], D(y) ~ y * r + rhss[2]], t; defaults = [s => 2r], guesses = [s => 1.0]) - prob = Problem( - sys, merge(u0map, Dict(r => 1)), (0.0, 1.0); u0_constructor, p_constructor) - @test prob.ps[r] == 1 - @test prob.ps[s] == 2 - initsys = prob.f.initialization_data.initializeprob.f.sys - @test is_parameter(initsys, r) - @test is_parameter(initsys, s) + if @isdefined(ModelingToolkit) + @testset "Null system" begin + @variables x(t) y(t) s(t) + @parameters x0 y0 + @mtkcompile sys = System([x ~ x0, y ~ y0, s ~ x + y], t; guesses = [y0 => 0.0]) + prob = ODEProblem(sys, [s => 1.0, x0 => 0.3, y0 => missing], (0.0, 1.0)) + # trivial initialization run immediately + @test prob.ps[y0] ≈ 0.7 + @test init(prob, Tsit5()).ps[y0] ≈ 0.7 + @test solve(prob, Tsit5()).ps[y0] ≈ 0.7 + end + using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, + Spring, Force, + Damper + using ModelingToolkitStandardLibrary.Mechanical: TranslationalModelica as TM + using ModelingToolkitStandardLibrary.Blocks: Constant + + @named mass = TM.Mass(; m = 1.0, s = 1.0, v = 0.0, a = 0.0) + @named fixed = Fixed(; s0 = 0.0) + @named spring = Spring(; c = 2.0, s_rel0 = nothing) + @named gravity = Force() + @named constant = Constant(; k = 9.81) + @named damper = TM.Damper(; d = 0.1) @mtkcompile sys = System( - [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) - @test_throws ModelingToolkit.MissingParametersError Problem( - sys, [x => 1.0, y => 1.0], (0.0, 1.0)) - - # Unsatisfiable initialization - prob = Problem(sys, [x => 1.0, y => 1.0, p => 2.0], (0.0, 1.0); - initialization_eqs = [x^2 + y^2 ~ 3], u0_constructor, p_constructor) - @test prob.f.initialization_data !== nothing - @test solve(prob, alg).retcode == ReturnCode.InitialFailure - cache = init(prob, alg) - @test solve!(cache).retcode == ReturnCode.InitialFailure - end - - @testset "Null system" begin - @variables x(t) y(t) s(t) - @parameters x0 y0 - @mtkcompile sys = System([x ~ x0, y ~ y0, s ~ x + y], t; guesses = [y0 => 0.0]) - prob = ODEProblem(sys, [s => 1.0, x0 => 0.3, y0 => missing], (0.0, 1.0)) + [connect(fixed.flange, spring.flange_a), connect(spring.flange_b, mass.flange_a), + connect(mass.flange_a, gravity.flange), connect(constant.output, gravity.f), + connect(fixed.flange, damper.flange_a), connect(damper.flange_b, mass.flange_a)], + t; + systems = [fixed, spring, mass, gravity, constant, damper], + guesses = [spring.s_rel0 => 1.0]) + prob = ODEProblem(sys, [spring.s_rel0 => missing], (0.0, 1.0)) # trivial initialization run immediately - @test prob.ps[y0] ≈ 0.7 - @test init(prob, Tsit5()).ps[y0] ≈ 0.7 - @test solve(prob, Tsit5()).ps[y0] ≈ 0.7 - end - - using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, - Spring, Force, - Damper - using ModelingToolkitStandardLibrary.Mechanical: TranslationalModelica as TM - using ModelingToolkitStandardLibrary.Blocks: Constant - - @named mass = TM.Mass(; m = 1.0, s = 1.0, v = 0.0, a = 0.0) - @named fixed = Fixed(; s0 = 0.0) - @named spring = Spring(; c = 2.0, s_rel0 = nothing) - @named gravity = Force() - @named constant = Constant(; k = 9.81) - @named damper = TM.Damper(; d = 0.1) - @mtkcompile sys = System( - [connect(fixed.flange, spring.flange_a), connect(spring.flange_b, mass.flange_a), - connect(mass.flange_a, gravity.flange), connect(constant.output, gravity.f), - connect(fixed.flange, damper.flange_a), connect(damper.flange_b, mass.flange_a)], - t; - systems = [fixed, spring, mass, gravity, constant, damper], - guesses = [spring.s_rel0 => 1.0]) - prob = ODEProblem(sys, [spring.s_rel0 => missing], (0.0, 1.0)) - # trivial initialization run immediately - @test prob.ps[spring.s_rel0] ≈ -3.905 - @test init(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 - @test solve(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 + @test prob.ps[spring.s_rel0] ≈ -3.905 + @test init(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 + @test solve(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 + end end @testset "NonlinearSystem initialization" begin @@ -796,7 +791,10 @@ end @test prob.f.initialization_data.update_initializeprob! === nothing @test prob.f.initialization_data.initializeprobmap === nothing @test prob.f.initialization_data.initializeprobpmap === nothing - for alg in nl_algs + @testset "$alg" for alg in nl_algs + if !@isdefined(ModelingToolkit) && (alg == Klement() || alg == DFSane()) + continue + end @test SciMLBase.successful_retcode(solve(prob, alg)) end @@ -804,7 +802,7 @@ end @test prob.f.initialization_data.update_initializeprob! === nothing @test prob.f.initialization_data.initializeprobmap === nothing @test prob.f.initialization_data.initializeprobpmap === nothing - for alg in nlls_algs + @testset "$alg" for alg in nlls_algs @test SciMLBase.successful_retcode(solve(prob, alg)) end end @@ -840,7 +838,7 @@ end # https://github.com/SciML/NonlinearSolve.jl/issues/586 eqs = [0 ~ -c * z + (q - z) * (x^2) 0 ~ p * (-x + (q - z) * x)] - @named sys = System(eqs; initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) + @named sys = System(eqs, [z, x], [c, p, q]; initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) sys = complete(sys) # @mtkcompile sys = NonlinearSystem( # [p * x^2 + q * y^3 ~ 0, x - q ~ 0]; defaults = [q => missing], @@ -892,50 +890,29 @@ end x = _x(t) @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for ( - System, Problem, alg, rhss) in [ - (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), - (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), - (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) + Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), zeros(2)), + (SDEProblem, ImplicitEM(), [a, b]), + (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - @mtkcompile sys = System( - [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [x => 0.0, p => 0.0]) + browns = alg === ImplicitEM() ? [a, b] : [] + @named sys = System( + [D(x) ~ x + rhss[1]], t, [x], [p], browns; guesses = [x => 0.0, p => 0.0], observed = [y ~ p - x]) + if alg === ImplicitEM() + tmp = mtkcompile(sys) + neqs = ModelingToolkitBase.get_noise_eqs(tmp) + @set! sys.noise_eqs = neqs + @set! sys.brownians = typeof(x)[] + @set! sys.eqs = [D(x) ~ p * x + rhss[1] - neqs[1, 1] * a] + end + sys = complete(sys) prob = Problem(sys, [y => 1.0, p => 3.0], (0.0, 1.0)) @test prob.f.initialization_data.initializeprob.ps[p] ≈ 3.0 - @test init(prob, alg)[x] ≈ 2.0 + @test init(prob, alg; abstol = 1e-6, reltol = 1e-6)[x] ≈ 2.0 atol=1e-6 prob.ps[p] = 2.0 @test prob.f.initialization_data.initializeprob.ps[p] ≈ 3.0 - @test init(prob, alg)[x] ≈ 1.0 - ModelingToolkit.defaults(prob.f.sys)[p] = missing - prob2 = remake(prob; u0 = [y => 1.0], p = [p => 3x]) - @test !is_variable(prob2.f.initialization_data.initializeprob, p) && - !is_parameter(prob2.f.initialization_data.initializeprob, p) - @test init(prob2, alg)[x] ≈ 0.5 - @test_nowarn solve(prob2, alg) - end -end - -@testset "Equations for dependent parameters" begin - @variables _x(..) - @parameters p q=5 r - @brownians a - x = _x(t) - - @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for ( - System, Problem, alg, rhss) in [ - (ModelingToolkit.System, ODEProblem, Tsit5(), 0), - (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), - (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) - ] - @mtkcompile sys = System( - [D(x) ~ 2x + r + rhss, r ~ p + 2q, q ~ p + 3], t; - guesses = [p => 1.0]) - prob = Problem(sys, [x => 1.0, p => missing], (0.0, 1.0)) - parent_isys = ModelingToolkit.get_parent(prob.f.initialization_data.initializeprob.f.sys) - @test length(equations(parent_isys)) == 4 - integ = init(prob, alg) - @test integ.ps[p] ≈ 2 + @test init(prob, alg; abstol = 1e-6, reltol = 1e-6)[x] ≈ 1.0 atol=1e-6 end end @@ -952,9 +929,18 @@ end (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - @mtkcompile sys = System( - [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [ - x => 0.0, p => 0.0]) + browns = alg === ImplicitEM() ? [a, b] : [] + @named sys = System( + [D(x) ~ x + rhss[1]], t, [x], [p], browns; bindings = [p => missing], guesses = [ + x => 0.0, p => 0.0], observed = [y ~ p - x]) + if alg === ImplicitEM() + tmp = mtkcompile(sys) + neqs = ModelingToolkitBase.get_noise_eqs(tmp) + @set! sys.noise_eqs = neqs + @set! sys.brownians = typeof(x)[] + @set! sys.eqs = [D(x) ~ p * x + rhss[1] - neqs[1, 1] * a] + end + sys = complete(sys) prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) @test init(prob, alg).ps[p] ≈ 2.0 # nonsensical value for y just to test that equations work @@ -962,7 +948,7 @@ end @test init(prob2, alg).ps[p] ≈ 3 + exp(1) # solve for `x` given `p` and `y` prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(y)]) - @test init(prob3, alg)[x] ≈ 1 - exp(1) + @test init(prob3, alg; abstol=1e-6, reltol=1e-6)[x] ≈ 1 - exp(1) atol=1e-6 @test_logs (:warn, r"overdetermined") remake( prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) prob4 = remake(prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) @@ -979,11 +965,11 @@ end x = _x(t) @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for ( - System, Problem, alg, rhss) in [ - (ModelingToolkit.System, ODEProblem, Tsit5(), 0), - (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), - (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) + Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), 0), + (SDEProblem, ImplicitEM(), a), + (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) ] alge_eqs = [y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0] @@ -1037,11 +1023,11 @@ end x = _x(t) @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for ( - System, Problem, alg, rhss) in [ - (ModelingToolkit.System, ODEProblem, Tsit5(), 0), - (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), - (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) + Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), 0), + (SDEProblem, ImplicitEM(), a), + (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) ] alge_eqs = [y^2 + 4y * p^2 ~ x^3] @mtkcompile sys = System( @@ -1070,34 +1056,26 @@ end @register_symbolic Multiplier(x::Real, y::Real) -@testset "Nonnumeric parameter dependencies are retained" begin +@testset "Nonnumeric bound parameters are retained" begin @variables x(t) y(t) @parameters foo(::Real, ::Real) p - @mtkcompile sys = System([D(x) ~ t, 0 ~ foo(x, y), foo ~ Multiplier(p, 2p)], t; + @mtkcompile sys = System([D(x) ~ t + p, 0 ~ foo(x, y)], t; bindings = [foo => Multiplier(p, 2p)], guesses = [y => -1.0]) prob = ODEProblem(sys, [x => 1.0, p => 1.0], (0.0, 1.0)) integ = init(prob, Rosenbrock23()) @test integ[y] ≈ -0.5 end -@testset "Use observed equations for guesses of observed variables" begin - @variables x(t) y(t) [state_priority = 100] - @mtkcompile sys = System( - [D(x) ~ x + t, y ~ 2x + 1], t; initialization_eqs = [x^3 + y^3 ~ 1]) - isys = ModelingToolkit.generate_initializesystem(sys) - @test isequal(defaults(isys)[y], 2x + 1) -end - @testset "Create initializeprob when unknown has dependent value" begin @variables x(t) y(t) - @mtkcompile sys = System([D(x) ~ x, D(y) ~ t * y], t; defaults = [x => 2y]) + @mtkcompile sys = System([D(x) ~ x, D(y) ~ t * y], t; bindings = [x => 2y]) prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) @test prob.f.initializeprob !== nothing integ = init(prob) @test integ[x] ≈ 2.0 @variables x(t)[1:2] y(t) - @mtkcompile sys = System([D(x) ~ x, D(y) ~ t], t; defaults = [x => [y, 3.0]]) + @mtkcompile sys = System([D(x) ~ x, D(y) ~ t], t; bindings = [x => [y, 3.0]]) prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) @test prob.f.initializeprob !== nothing integ = init(prob) @@ -1105,104 +1083,114 @@ end end @testset "units" begin - t = ModelingToolkit.t - D = ModelingToolkit.D + t = ModelingToolkitBase.t + D = ModelingToolkitBase.D @parameters g [unit = u"m/s^2"] L=1 [unit = u"m^2"] @variables x(t) [unit = u"m"] y(t) [unit = u"m" state_priority = 10] λ(t) [unit = u"s^-2"] - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ L] - @mtkcompile pend = System(eqs, t) + @variables xˍt(t) [unit = u"m/s"] yˍt(t) [unit = u"m/s"] + eqs = [ + L ~ y^2 + x^2 + D(y) ~ yˍt + yˍt*y ~ xˍt*x + D(yˍt) ~ -g + y*λ + yˍt^2 ~ -xˍt^2 - x*λ*x - y*(-g + y*λ) + ] + @named sys = System(eqs, t, [xˍt, y, x, yˍt, λ], [g, L]) + sys = mtkcompile(sys) prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 1.5), - guesses = ModelingToolkit.missing_variable_defaults(pend)) - sol = solve(prob, Rodas5P()) + guesses = [x => 1.0, y => 1.0, λ => 1.0]) + sol = solve(prob, FBDF()) @test SciMLBase.successful_retcode(sol) - prob2 = remake(prob, u0 = [x => 0.5, y=>nothing]) - sol2 = solve(prob2, Rodas5P()) + prob2 = remake(prob, u0 = [x => 0.5, y=>sqrt(3)/2]) + sol2 = solve(prob2, FBDF()) @test SciMLBase.successful_retcode(sol2) end -@testset "Issue#3205" begin - using ModelingToolkitStandardLibrary.Electrical - import ModelingToolkitStandardLibrary.Mechanical.Rotational as MR - using ModelingToolkitStandardLibrary.Blocks - using SciMLBase - - function dc_motor(R1 = 0.5) - R = R1 # [Ohm] armature resistance - L = 4.5e-3 # [H] armature inductance - k = 0.5 # [N.m/A] motor constant - J = 0.02 # [kg.m²] inertia - f = 0.01 # [N.m.s/rad] friction factor - tau_L_step = -0.3 # [N.m] amplitude of the load torque step - - @named ground = Ground() - @named source = Voltage() - @named ref = Blocks.Step(height = 0.2, start_time = 0) - @named pi_controller = Blocks.LimPI(k = 1.1, T = 0.035, u_max = 10, Ta = 0.035) - @named feedback = Blocks.Feedback() - @named R1 = Resistor(R = R) - @named L1 = Inductor(L = L) - @named emf = EMF(k = k) - @named fixed = MR.Fixed() - @named load = MR.Torque() - @named load_step = Blocks.Step(height = tau_L_step, start_time = 3) - @named inertia = MR.Inertia(J = J) - @named friction = MR.Damper(d = f) - @named speed_sensor = MR.SpeedSensor() - - connections = [connect(fixed.flange, emf.support, friction.flange_b) - connect(emf.flange, friction.flange_a, inertia.flange_a) - connect(inertia.flange_b, load.flange) - connect(inertia.flange_b, speed_sensor.flange) - connect(load_step.output, load.tau) - connect(ref.output, feedback.input1) - connect(speed_sensor.w, :y, feedback.input2) - connect(feedback.output, pi_controller.err_input) - connect(pi_controller.ctr_output, :u, source.V) - connect(source.p, R1.p) - connect(R1.n, L1.p) - connect(L1.n, emf.p) - connect(emf.n, source.n, ground.g)] - - @named model = System(connections, t, - systems = [ - ground, - ref, - pi_controller, - feedback, - source, - R1, - L1, - emf, - fixed, - load, - load_step, - inertia, - friction, - speed_sensor - ]) - end - - model = dc_motor() - sys = mtkcompile(model) - - prob = ODEProblem(sys, [sys.L1.i => 0.0], (0, 6.0)) - - @test_nowarn remake(prob, p = prob.p) -end - -@testset "Singular initialization prints a warning" begin - @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] - @mtkcompile pend = System(eqs, t) - @test_warn ["structurally singular", "initialization", "Guess", "heuristic"] ODEProblem( - pend, [x => 1, y => 0, g => 1], (0.0, 1.5), guesses = [λ => 1]) +if @isdefined(ModelingToolkit) + @testset "Issue#3205" begin + using ModelingToolkitStandardLibrary.Electrical + import ModelingToolkitStandardLibrary.Mechanical.Rotational as MR + using ModelingToolkitStandardLibrary.Blocks + using SciMLBase + + function dc_motor(R1 = 0.5) + R = R1 # [Ohm] armature resistance + L = 4.5e-3 # [H] armature inductance + k = 0.5 # [N.m/A] motor constant + J = 0.02 # [kg.m²] inertia + f = 0.01 # [N.m.s/rad] friction factor + tau_L_step = -0.3 # [N.m] amplitude of the load torque step + + @named ground = Ground() + @named source = Voltage() + @named ref = Blocks.Step(height = 0.2, start_time = 0) + @named pi_controller = Blocks.LimPI(k = 1.1, T = 0.035, u_max = 10, Ta = 0.035) + @named feedback = Blocks.Feedback() + @named R1 = Resistor(R = R) + @named L1 = Inductor(L = L) + @named emf = EMF(k = k) + @named fixed = MR.Fixed() + @named load = MR.Torque() + @named load_step = Blocks.Step(height = tau_L_step, start_time = 3) + @named inertia = MR.Inertia(J = J) + @named friction = MR.Damper(d = f) + @named speed_sensor = MR.SpeedSensor() + + connections = [connect(fixed.flange, emf.support, friction.flange_b) + connect(emf.flange, friction.flange_a, inertia.flange_a) + connect(inertia.flange_b, load.flange) + connect(inertia.flange_b, speed_sensor.flange) + connect(load_step.output, load.tau) + connect(ref.output, feedback.input1) + connect(speed_sensor.w, :y, feedback.input2) + connect(feedback.output, pi_controller.err_input) + connect(pi_controller.ctr_output, :u, source.V) + connect(source.p, R1.p) + connect(R1.n, L1.p) + connect(L1.n, emf.p) + connect(emf.n, source.n, ground.g)] + + @named model = System(connections, t, + systems = [ + ground, + ref, + pi_controller, + feedback, + source, + R1, + L1, + emf, + fixed, + load, + load_step, + inertia, + friction, + speed_sensor + ]) + end + + model = dc_motor() + sys = mtkcompile(model) + + prob = ODEProblem(sys, [sys.L1.i => 0.0], (0, 6.0)) + + @test_nowarn remake(prob, p = prob.p) + end +end + +if @isdefined(ModelingToolkit) + @testset "Singular initialization prints a warning" begin + @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] + @mtkcompile pend = System(eqs, t) + @test_warn ["structurally singular", "initialization", "Guess", "heuristic"] ODEProblem( + pend, [x => 1, y => 0, g => 1], (0.0, 1.5), guesses = [λ => 1]) + end end @testset "DAEProblem initialization" begin @@ -1224,26 +1212,26 @@ end end @testset "Guesses provided to `ODEProblem` are used in `remake`" begin - @variables x(t) y(t)=2x - @parameters p q=3x - @mtkcompile sys = System([D(x) ~ x * p + q, x^3 + y^3 ~ 3], t) + @variables x(t) y(t) + @parameters p q = missing + @mtkcompile sys = System([D(x) ~ x * p + q, x^3 + y^3 ~ 3], t; initial_conditions = [y => 2x], initialization_eqs = [q ~ 3x]) prob = ODEProblem( - sys, [p => 1.0], (0.0, 1.0); guesses = [x => 1.0, y => 1.0, q => 1.0]) + sys, [p => 1.0], (0.0, 1.0); guesses = [x => 1.0, y => 2.0, q => 1.0]) @test prob[x] == 1.0 @test prob[y] == 2.0 @test prob.ps[p] == 1.0 - @test prob.ps[q] == 3.0 + @test prob.ps[q] == (@isdefined(ModelingToolkit) ? 3.0 : 1.0) integ = init(prob) - @test integ[x] ≈ 1 / cbrt(3) - @test integ[y] ≈ 2 / cbrt(3) + @test integ[x] ≈ 1 / cbrt(3) atol = 1e-6 + @test integ[y] ≈ 2 / cbrt(3) atol = 1e-6 @test integ.ps[p] == 1.0 @test integ.ps[q]≈3 / cbrt(3) atol=1e-5 - prob2 = remake(prob; u0 = [y => 3x], p = [q => 2x]) + prob2 = remake(prob; u0 = [y => 3x]) integ2 = init(prob2) @test integ2[x]≈cbrt(3 / 28) atol=1e-5 @test integ2[y]≈3cbrt(3 / 28) atol=1e-5 @test integ2.ps[p] == 1.0 - @test integ2.ps[q]≈2cbrt(3 / 28) atol=1e-5 + @test integ2.ps[q]≈3cbrt(3 / 28) atol=1e-5 end function test_dummy_initialization_equation(prob, var) @@ -1253,43 +1241,42 @@ function test_dummy_initialization_equation(prob, var) @test idx !== nothing && is_parameter(initsys, observed(initsys)[idx].rhs) end -@testset "Remake problem with no initializeprob" begin - @variables x(t) [guess = 1.0] y(t) [guess = 1.0] - @parameters p [guess = 1.0] q [guess = 1.0] - @mtkcompile sys = System( - [D(x) ~ p * x + q * y, y ~ 2x, q ~ 2p], t) - prob = ODEProblem(sys, [x => 1.0, p => 1.0], (0.0, 1.0)) - test_dummy_initialization_equation(prob, x) - prob2 = remake(prob; u0 = [x => 2.0]) - @test prob2[x] == 2.0 - test_dummy_initialization_equation(prob2, x) - # otherwise we have `x ~ 2, y ~ 2` which is unsatisfiable - prob3 = remake(prob; u0 = [x => nothing, y => 2.0]) - @test prob3.f.initialization_data !== nothing - @test init(prob3)[x] ≈ 1.0 - prob4 = remake(prob; p = [p => 1.0]) - test_dummy_initialization_equation(prob4, x) - prob5 = remake(prob; p = [p => missing, q => 4.0]) - @test prob5.f.initialization_data !== nothing - @test init(prob5).ps[p] ≈ 2.0 -end - -@testset "Variables provided as symbols" begin - @variables x(t) [guess = 1.0] y(t) [guess = 1.0] - @parameters p [guess = 1.0] q [guess = 1.0] - @mtkcompile sys = System( - [D(x) ~ p * x + q * y, y ~ 2x, q ~ 2p], t) - prob = ODEProblem(sys, [:x => 1.0, p => 1.0], (0.0, 1.0)) - test_dummy_initialization_equation(prob, x) - prob2 = remake(prob; u0 = [:x => 2.0]) - test_dummy_initialization_equation(prob2, x) - prob3 = remake(prob; u0 = [:y => 1.0, :x => nothing]) - @test init(prob3)[x] ≈ 0.5 - @test SciMLBase.successful_retcode(solve(prob3)) +if @isdefined(ModelingToolkit) + @testset "Remake problem with no initializeprob" begin + @variables x(t) [guess = 1.0] y(t) [guess = 1.0] + @parameters p [guess = 1.0] q [guess = 1.0] + @mtkcompile sys = System( + [D(x) ~ p * x + q * y, y ~ 2x], t; bindings = [q => 2p]) + prob = ODEProblem(sys, [x => 1.0, p => 1.0], (0.0, 1.0)) + test_dummy_initialization_equation(prob, x) + prob2 = remake(prob; u0 = [x => 2.0]) + @test prob2[x] == 2.0 + test_dummy_initialization_equation(prob2, x) + # otherwise we have `x ~ 2, y ~ 2` which is unsatisfiable + prob3 = remake(prob; u0 = [x => nothing, y => 2.0]) + @test prob3.f.initialization_data !== nothing + @test init(prob3)[x] ≈ 1.0 + prob4 = remake(prob; p = [p => 1.0]) + test_dummy_initialization_equation(prob4, x) + end + + @testset "Variables provided as symbols" begin + @variables x(t) [guess = 1.0] y(t) [guess = 1.0] + @parameters p [guess = 1.0] q [guess = 1.0] + @mtkcompile sys = System( + [D(x) ~ p * x + q * y, y ~ 2x], t; bindings = [q => 2p]) + prob = ODEProblem(sys, [:x => 1.0, p => 1.0], (0.0, 1.0)) + test_dummy_initialization_equation(prob, x) + prob2 = remake(prob; u0 = [:x => 2.0]) + test_dummy_initialization_equation(prob2, x) + prob3 = remake(prob; u0 = [:y => 1.0, :x => nothing]) + @test init(prob3)[x] ≈ 0.5 + @test SciMLBase.successful_retcode(solve(prob3)) + end end @testset "Issue#3246: type promotion with parameter dependent initialization_eqs" begin - @variables x(t)=1 y(t)=1 + @variables x(t)=1 y(t) @parameters a = 1 @named sys = System([D(x) ~ 0, D(y) ~ x + a], t; initialization_eqs = [y ~ a]) @@ -1350,35 +1337,21 @@ end @test SciMLBase.successful_retcode(sol) end -@testset "Solvable array parameters with scalarized guesses" begin - @variables x(t) - @parameters p[1:2] q - @mtkcompile sys = System( - D(x) ~ p[1] + p[2] + q, t; defaults = [p[1] => q, p[2] => 2q], - guesses = [p[1] => q, p[2] => 2q]) - @test ModelingToolkit.is_parameter_solvable(p, Dict(), defaults(sys), guesses(sys)) - prob = ODEProblem(sys, [x => 1.0, q => 2.0], (0.0, 1.0)) - initsys = prob.f.initialization_data.initializeprob.f.sys - @test length(ModelingToolkit.observed(initsys)) == 4 - sol = solve(prob, Tsit5()) - @test sol.ps[p] ≈ [2.0, 4.0] -end - @testset "Issue#3318: Mutating `Initial` parameters works" begin @variables x(t) y(t)[1:2] [guess = ones(2)] @parameters p[1:2, 1:2] @mtkcompile sys = System( [D(x) ~ x, D(y) ~ p * y], t; initialization_eqs = [x^2 + y[1]^2 + y[2]^2 ~ 4]) - prob = ODEProblem(sys, [x => 1.0, y[1] => 1, p => 2ones(2, 2)], (0.0, 1.0)) - integ = init(prob, Tsit5()) - @test integ[x] ≈ 1.0 - @test integ[y] ≈ [1.0, sqrt(2.0)] + prob = ODEProblem(sys, [x => 1.0, y[1] => 1, p => 2ones(2, 2)], (0.0, 1.0); guesses = [y[2] => 2.0]) + integ = init(prob, Tsit5(); abstol = 1e-6, reltol = 1e-6) + @test integ[x] ≈ 1.0 atol=1e-6 + @test integ[y] ≈ [1.0, sqrt(2.0)] atol=1e-6 prob.ps[Initial(x)] = 0.5 - integ = init(prob, Tsit5()) + integ = init(prob, Tsit5(); abstol = 1e-6, reltol = 1e-6) @test integ[x] ≈ 0.5 @test integ[y] ≈ [1.0, sqrt(2.75)] prob.ps[Initial(y[1])] = 0.5 - integ = init(prob, Tsit5()) + integ = init(prob, Tsit5(); abstol = 1e-6, reltol = 1e-6) @test integ[x] ≈ 0.5 @test integ[y]≈[0.5, sqrt(3.5)] atol=1e-6 end @@ -1430,7 +1403,8 @@ end D(X1) ~ k1 * (Γ[1] - X1) - k2 * X1 ] obs = [X2 ~ Γ[1] - X1] - @mtkcompile osys = System(eqs, t, [X1, X2], [k1, k2, Γ]; observed = obs) + @named osys = System(eqs, t, [X1], [k1, k2, Γ]; observed = obs) + osys = complete(osys) u0 = [X1 => 1.0, X2 => 2.0] ps = [k1 => 0.1, k2 => 0.2] @@ -1440,28 +1414,30 @@ end @test integ1[X1] ≈ 1.0 end -@testset "Trivial initialization is run on problem construction" begin - @variables _x(..) y(t) - @brownians a - @parameters tot - x = _x(t) - @testset "$Problem" for (Problem, lhs, rhs) in [ - (ODEProblem, D, 0.0), - (SDEProblem, D, a), - (DDEProblem, D, _x(t - 0.1)), - (SDDEProblem, D, _x(t - 0.1) + a) - ] - @mtkcompile sys = ModelingToolkit.System([lhs(x) ~ x + rhs, x + y ~ tot], t; - guesses = [tot => 1.0], defaults = [tot => missing]) - prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) - @test prob.ps[tot] ≈ 2.0 - end - @testset "$Problem" for Problem in [NonlinearProblem, NonlinearLeastSquaresProblem] - @parameters p1 p2 - @mtkcompile sys = System([x^2 + y^2 ~ p1, (x - 1)^2 + (y - 1)^2 ~ p2, p2 ~ 2p1]; - guesses = [p1 => 0.0], defaults = [p1 => missing]) - prob = Problem(sys, [x => 1.0, y => 1.0, p2 => 6.0]) - @test prob.ps[p1] ≈ 3.0 +if @isdefined(ModelingToolkit) + @testset "Trivial initialization is run on problem construction" begin + @variables _x(..) y(t) + @brownians a + @parameters tot + x = _x(t) + @testset "$Problem" for (Problem, rhs) in [ + (ODEProblem, 0.0), + (SDEProblem, a), + (DDEProblem, _x(t - 0.1)), + (SDDEProblem, _x(t - 0.1) + a) + ] + @mtkcompile sys = ModelingToolkitBase.System([D(x) ~ x + rhs, x + y ~ tot], t; + guesses = [tot => 1.0], bindings = [tot => missing]) + prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test prob.ps[tot] ≈ 2.0 + end + @testset "$Problem" for Problem in [NonlinearProblem, NonlinearLeastSquaresProblem] + @parameters p1 p2 + @mtkcompile sys = System([x^2 + y^2 ~ p1, (x - 1)^2 + (y - 1)^2 ~ p2]; + guesses = [p1 => 0.0], bindings = [p1 => missing], initialization_eqs = [p2 ~ 2p1]) + prob = Problem(sys, [x => 1.0, y => 1.0, p2 => 6.0]) + @test prob.ps[p1] ≈ 3.0 + end end end @@ -1495,14 +1471,21 @@ end u0 = [X1 => 1.0, X2 => 2.0] ps = [k1 => 0.1, k2 => 0.2] prob = Problem(nlsys, [u0; ps]) - @test state_values(prob.f.initialization_data.initializeprob) === nothing - @test prob.ps[Γ[1]] ≈ 3.0 + if @isdefined(ModelingToolkit) + @test state_values(prob.f.initialization_data.initializeprob) === nothing + @test prob.ps[Γ[1]] ≈ 3.0 + else + integ = init(prob) + @test integ.ps[Γ[1]] ≈ 3.0 + end end - @testset "respects explicitly provided value" begin - ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] - prob = Problem(nlsys, ps) - @test prob.ps[Γ[1]] ≈ 5.0 + if @isdefined(ModelingToolkit) + @testset "respects explicitly provided value" begin + ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] + prob = Problem(nlsys, ps) + @test prob.ps[Γ[1]] ≈ 5.0 + end end @testset "fails initialization if inconsistent explicit value" begin @@ -1513,13 +1496,15 @@ end @test sol.retcode == SciMLBase.ReturnCode.InitialFailure end - @testset "Ignores initial equation if given insufficient u0" begin - u0 = [X2 => 2.0] - ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] - prob = Problem(nlsys, [u0; ps]) - sol = solve(prob) - @test SciMLBase.successful_retcode(sol) - @test sol.ps[Γ[1]] ≈ 5.0 + if @isdefined(ModelingToolkit) + @testset "Ignores initial equation if given insufficient u0" begin + u0 = [X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] + prob = Problem(nlsys, [u0; ps]) + sol = solve(prob) + @test SciMLBase.successful_retcode(sol) + @test sol.ps[Γ[1]] ≈ 5.0 + end end end @@ -1552,15 +1537,13 @@ end @testset "Issue#3570, #3552: `Initial`s/guesses are copied to `u0` during `solve`/`init`" begin @parameters g @variables x(t) [state_priority = 10] y(t) λ(t) - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - @mtkcompile pend = System(eqs, t) + @named pend = index_reduced_pend() + pend = complete(pend) prob = ODEProblem( - pend, [x => (√2 / 2), D(x) => 0.0, g => 1], (0.0, 1.5), - guesses = [λ => 1, y => √2 / 2]) - sol = solve(prob) + pend, [y => -(√2 / 2), D(y) => 0.0, g => 1], (0.0, 1.5), + guesses = [λ => 1, x => √2 / 2]) + sol = solve(prob, FBDF()) @testset "Guesses of initialization problem copied to algebraic variables" begin prob.f.initialization_data.initializeprob[λ] = 1.0 @@ -1573,7 +1556,7 @@ end prob2 = ODEProblem( pend, [x => (√2 / 2), D(y) => 0.0, g => 1], (0.0, 1.5), guesses = [λ => 1, y => √2 / 2]) - sol = solve(prob) + sol = solve(prob, FBDF()) @test SciMLBase.successful_retcode(sol) prob3 = DiffEqBase.get_updated_symbolic_problem( pend, prob2; u0 = prob2.u0, p = prob2.p) @@ -1581,22 +1564,22 @@ end end @testset "`setsym_oop`" begin - setter = setsym_oop(prob, [Initial(x)]) + setter = setsym_oop(prob, [Initial(y)]) (u0, p) = setter(prob, [0.8]) new_prob = remake(prob; u0, p, initializealg = BrownFullBasicInit()) new_sol = solve(new_prob) - @test new_sol[x, 1] ≈ 0.8 + @test new_sol[y, 1] ≈ 0.8 integ = init(new_prob) - @test integ[x] ≈ 0.8 + @test integ[y] ≈ 0.8 end @testset "`setsym`" begin - @test prob.ps[Initial(x)] ≈ √2 / 2 - prob.ps[Initial(x)] = 0.8 + @test prob.ps[Initial(y)] ≈ -√2 / 2 + prob.ps[Initial(y)] = 0.8 sol = solve(prob; initializealg = BrownFullBasicInit()) - @test sol[x, 1] ≈ 0.8 + @test sol[y, 1] ≈ 0.8 integ = init(prob; initializealg = BrownFullBasicInit()) - @test integ[x] ≈ 0.8 + @test integ[y] ≈ 0.8 end end @@ -1639,13 +1622,11 @@ end @testset "Initialization system retains `split` kwarg of parent" begin @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] - @mtkcompile pend=System(eqs, t) split=false + @named pend = index_reduced_pend() + pend = complete(pend; split = false) prob = ODEProblem( pend, [x => 1.0, D(x) => 0.0, g => 1.0], (0.0, 1.0); guesses = [y => 1.0, λ => 1.0]) - @test !ModelingToolkit.is_split(prob.f.initialization_data.initializeprob.f.sys) + @test !ModelingToolkitBase.is_split(prob.f.initialization_data.initializeprob.f.sys) end @testset "`InitializationProblem` retains `iip` of parent" begin @@ -1654,7 +1635,8 @@ end eqs = [D(D(x)) ~ λ * x D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] - @mtkcompile pend = System(eqs, t) + @named pend = index_reduced_pend() + pend = complete(pend) prob = ODEProblem(pend, SA[x => 1.0, D(x) => 0.0, g => 1.0], (0.0, 1.0); guesses = [y => 1.0, λ => 1.0]) @test !SciMLBase.isinplace(prob) @@ -1678,13 +1660,13 @@ end @mtkcompile sys = System(eqs, t) prob = ODEProblem(sys, [], (0.0, 1.0)) - sol = solve(prob, Tsit5()) + sol = solve(prob, @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P()) @test SciMLBase.successful_retcode(sol) end -@testset "Defaults removed with ` => nothing` aren't retained" begin +@testset "Initial conditions removed with ` => nothing` aren't retained" begin @variables x(t)[1:2] - @mtkcompile sys = System([D(x[1]) ~ -x[1], x[1] + x[2] ~ 3], t; defaults = [x[1] => 1]) + @mtkcompile sys = System([D(x[1]) ~ -x[1], x[1] + x[2] ~ 3], t; initial_conditions = [x => ones(2)]) prob = ODEProblem(sys, [x[1] => nothing, x[2] => 1], (0.0, 1.0)) @test SciMLBase.initialization_status(prob) == SciMLBase.FULLY_DETERMINED end diff --git a/test/input_output_handling.jl b/lib/ModelingToolkitBase/test/input_output_handling.jl similarity index 59% rename from test/input_output_handling.jl rename to lib/ModelingToolkitBase/test/input_output_handling.jl index fa1bf51a27..0d64fa4167 100644 --- a/test/input_output_handling.jl +++ b/lib/ModelingToolkitBase/test/input_output_handling.jl @@ -1,15 +1,20 @@ -using ModelingToolkit, Symbolics, Test -using ModelingToolkit: get_namespace, has_var, inputs, outputs, is_bound, bound_inputs, +using ModelingToolkitBase, Symbolics, Test +using ModelingToolkitBase: get_namespace, has_var, inputs, outputs, is_bound, bound_inputs, unbound_inputs, bound_outputs, unbound_outputs, isinput, isoutput, ExtraVariablesSystemException -using ModelingToolkit: t_nounits as t, D_nounits as D +using NonlinearSolve +using SymbolicIndexingInterface: is_parameter +using ModelingToolkitBase: t_nounits as t, D_nounits as D @variables xx(t) some_input(t) [input = true] eqs = [D(xx) ~ some_input] @named model = System(eqs, t) -@test_throws ExtraVariablesSystemException mtkcompile(model) -err = "In particular, the unset input(s) are:\n some_input(t)" +err = @isdefined(ModelingToolkit) ? ModelingToolkit.StateSelection.ExtraVariablesSystemException : ExtraVariablesSystemException @test_throws err mtkcompile(model) +if @isdefined(ModelingToolkit) + err = "In particular, the unset input(s) are:\n some_input(t)" + @test_throws err mtkcompile(model) +end # Test input handling @variables x(t) u(t) [input = true] v(t)[1:2] [input = true] @@ -49,7 +54,7 @@ err = "In particular, the unset input(s) are:\n some_input(t)" # simplification turns input variables into parameters ssys = mtkcompile(sys, inputs = [u], outputs = []) -@test ModelingToolkit.isparameter(unbound_inputs(ssys)[]) +@test is_parameter(ssys, unbound_inputs(ssys)[]) @test !is_bound(ssys, u) @test u ∈ Set(unbound_inputs(ssys)) @@ -113,28 +118,28 @@ syss = mtkcompile(sys2, outputs = [sys.y]) #@test isequal(unbound_outputs(syss), [y]) @test isequal(bound_outputs(syss), [sys.y]) -using ModelingToolkitStandardLibrary -using ModelingToolkitStandardLibrary.Mechanical.Rotational -@named inertia1 = Inertia(; J = 1) -@named inertia2 = Inertia(; J = 1) -@named spring = Rotational.Spring(; c = 10) -@named damper = Rotational.Damper(; d = 3) -@named torque = Torque(; use_support = false) -@variables y(t) = 0 -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) - y ~ inertia2.w + torque.tau.u] -model = System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name = :name, guesses = [spring.flange_a.phi => 0.0]) -model_outputs = [inertia1.w, inertia2.w, inertia1.phi, inertia2.phi] -model_inputs = [torque.tau.u] -op = Dict(torque.tau.u => 0.0) -matrices, ssys = linearize( - model, model_inputs, model_outputs; op); -@test length(ModelingToolkit.outputs(ssys)) == 4 - -if VERSION >= v"1.8" # :opaque_closure not supported before +if @isdefined(ModelingToolkit) + using ModelingToolkitStandardLibrary + using ModelingToolkitStandardLibrary.Mechanical.Rotational + @named inertia1 = Inertia(; J = 1) + @named inertia2 = Inertia(; J = 1) + @named spring = Rotational.Spring(; c = 10) + @named damper = Rotational.Damper(; d = 3) + @named torque = Torque(; use_support = false) + @variables y(t) = 0 + 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) + y ~ inertia2.w + torque.tau.u] + model = System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], + name = :name, guesses = [spring.flange_a.phi => 0.0]) + model_outputs = [inertia1.w, inertia2.w, inertia1.phi, inertia2.phi] + model_inputs = [torque.tau.u] + op = Dict(torque.tau.u => 0.0) + matrices, ssys = linearize( + model, model_inputs, model_outputs; op); + @test length(ModelingToolkit.outputs(ssys)) == 4 + let # Just to have a local scope for D matrices, ssys = linearize(model, model_inputs, [y]; op) A, B, C, D = matrices @@ -144,7 +149,7 @@ if VERSION >= v"1.8" # :opaque_closure not supported before x = randn(size(A, 1)) u = randn(size(B, 2)) p = getindex.( - Ref(ModelingToolkit.defaults_and_guesses(ssys)), + Ref(ModelingToolkit.initial_conditions_and_guesses(ssys)), parameters(ssys)) y1 = obsf(x, u, p, 0) y2 = C * x + D * u @@ -164,7 +169,7 @@ end @named sys = System(eqs, t) f, dvs, - ps, io_sys = ModelingToolkit.generate_control_function( + ps, io_sys = ModelingToolkitBase.generate_control_function( sys, [u]; simplify, split) @test isequal(dvs[], x) @@ -184,7 +189,7 @@ end @named sys = System(eqs, t) f, dvs, ps, - io_sys = ModelingToolkit.generate_control_function( + io_sys = ModelingToolkitBase.generate_control_function( sys, [u], [d]; simplify, split) @test isequal(dvs[], x) @@ -204,7 +209,7 @@ end @named sys = System(eqs, t) f, dvs, ps, - io_sys = ModelingToolkit.generate_control_function( + io_sys = ModelingToolkitBase.generate_control_function( sys, [u], [d]; simplify, split, disturbance_argument = true) @@ -226,7 +231,7 @@ end @named sys = System(eqs, t) f, dvs, ps, - io_sys = ModelingToolkit.generate_control_function( + io_sys = ModelingToolkitBase.generate_control_function( sys, [u]; known_disturbance_inputs = [d], simplify, split) @@ -249,7 +254,7 @@ end @named sys = System(eqs, t) f, dvs, ps, - io_sys = ModelingToolkit.generate_control_function( + io_sys = ModelingToolkitBase.generate_control_function( sys, [u], [d1]; # d1 is unknown (set to zero) known_disturbance_inputs = [d2], # d2 is known (function argument) simplify, split) @@ -317,12 +322,12 @@ eqs = [connect_sd(sd, mass1, mass2) @named _model = System(eqs, t) @named model = compose(_model, mass1, mass2, sd); -f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( +f, dvs, ps, io_sys = ModelingToolkitBase.generate_control_function( model, [u]; simplify = true) -@test length(dvs) == 4 +@test length(dvs) == (@isdefined(ModelingToolkit) ? 4 : 8) p = MTKParameters(io_sys, [io_sys.u => NaN]) -x = ModelingToolkit.varmap_to_vars( - merge(ModelingToolkit.defaults(model), +x = ModelingToolkitBase.varmap_to_vars( + merge(ModelingToolkitBase.initial_conditions(model), Dict(D.(unknowns(model)) .=> 0.0)), dvs) u = [rand()] out = f[1](x, u, p, 1) @@ -335,145 +340,51 @@ eqs = [D(x) ~ u] @named sys = System(eqs, t) @test_nowarn mtkcompile(sys, inputs = [u], outputs = []) -#= -## Disturbance input handling -We test that the generated disturbance dynamics is correct by calling the dynamics in two different points that differ in the disturbance state, and check that we get the same result as when we call the linearized dynamics in the same two points. The true system is linear so the linearized dynamics are exact. +@testset "IO canonicalization" begin + @variables x(t)[1:3] = zeros(3) + @variables u(t)[1:2] + y₁, y₂, y₃ = x + u1, u2 = u + k₁, k₂, k₃ = 1, 1, 1 + eqs = [D(y₁) ~ -k₁ * y₁ + k₃ * y₂ * y₃ + u1 + D(y₂) ~ k₁ * y₁ - k₃ * y₂ * y₃ - k₂ * y₂^2 + u2 + y₁ + y₂ + y₃ ~ 1] -The test below builds a double-mass model and adds an integrating disturbance to the input -=# + @named sys = System(eqs, t) -using ModelingToolkit -using ModelingToolkitStandardLibrary.Mechanical.Rotational -using ModelingToolkitStandardLibrary.Blocks + @test_throws "entire array must be an input" mtkcompile(sys; inputs = [u1]) + @test_throws "entire array must be an disturbance input" mtkcompile(sys; disturbance_inputs = [x[1]]) + @test_throws "sorted order" mtkcompile(sys; inputs = [u2, u1]) + @test_throws "sorted order" mtkcompile(sys; disturbance_inputs = [u2, u1]) -# Parameters -m1 = 1 -m2 = 1 -k = 1000 # Spring stiffness -c = 10 # Damping coefficient + ss1 = mtkcompile(sys; inputs = [u], outputs = [x]) + @test isequal(ModelingToolkitBase.inputs(ss1), [u1, u2]) + @test isequal(ModelingToolkitBase.outputs(ss1), [x[1], x[2], x[3]]) +end -@named inertia1 = Rotational.Inertia(; J = m1) -@named inertia2 = Rotational.Inertia(; J = m2) -@named spring = Rotational.Spring(; c = k) -@named damper = Rotational.Damper(; d = c) -@named torque = Rotational.Torque(; use_support = false) +using ModelingToolkitStandardLibrary.Blocks -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 @named model = System(eqs, t; - systems = [ - torque, - inertia1, - inertia2, - spring, - damper, - u - ]) +if @isdefined(ModelingToolkit) + @testset "Issue#1577" begin + # https://github.com/SciML/ModelingToolkit.jl/issues/1577 + @named c = Constant(; k = 2) + @named gain = Gain(1;) + @named int = Integrator(; k = 1) + @named fb = Feedback(;) + @named model = System( + [ + connect(c.output, fb.input1), + connect(fb.input2, int.output), + connect(fb.output, gain.input), + connect(gain.output, int.input) + ], + t, + systems = [int, gain, c, fb]) + sys = mtkcompile(model) + @test length(unknowns(sys)) == length(equations(sys)) == 1 end - System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name, guesses = [spring.flange_a.phi => 0.0]) end -model = SystemModel() # Model with load disturbance -model = complete(model) -model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] - -@named dmodel = Blocks.StateSpace([0.0], [1.0], [1.0], [0.0]) # An integrating disturbance - -@named dist = ModelingToolkit.DisturbanceModel(model.torque.tau.u, dmodel) -f, outersys, dvs, p, io_sys = ModelingToolkit.add_input_disturbance(model, dist) - -@unpack u, d = outersys -matrices, ssys = linearize(outersys, [u, d], model_outputs) - -def = ModelingToolkit.defaults(outersys) - -# Create a perturbation in the disturbance state -dstate = setdiff(dvs, model_outputs)[] -x_add = ModelingToolkit.varmap_to_vars(merge(Dict(dvs .=> 0), Dict(dstate => 1)), dvs) - -x0 = randn(5) -x1 = copy(x0) + x_add # add disturbance state perturbation -u = randn(1) -pn = MTKParameters(io_sys, []) -xp0 = f[1](x0, u, pn, 0) -xp1 = f[1](x1, u, pn, 0) - -@test xp0 ≈ matrices.A * x0 + matrices.B * [u; 0] -@test xp1 ≈ matrices.A * x1 + matrices.B * [u; 0] - -@variables x(t)[1:3] = 0 -@variables u(t)[1:2] -y₁, y₂, y₃ = x -u1, u2 = u -k₁, k₂, k₃ = 1, 1, 1 -eqs = [D(y₁) ~ -k₁ * y₁ + k₃ * y₂ * y₃ + u1 - D(y₂) ~ k₁ * y₁ - k₃ * y₂ * y₃ - k₂ * y₂^2 + u2 - y₁ + y₂ + y₃ ~ 1] - -@named sys = System(eqs, t) -m_inputs = [u[1], u[2]] -m_outputs = [y₂] -sys_simp = mtkcompile(sys, inputs = m_inputs, outputs = m_outputs) -@test issetequal(unknowns(sys_simp), collect(x[1:2])) -@test length(inputs(sys_simp)) == 2 - -# https://github.com/SciML/ModelingToolkit.jl/issues/1577 -@named c = Constant(; k = 2) -@named gain = Gain(1;) -@named int = Integrator(; k = 1) -@named fb = Feedback(;) -@named model = System( - [ - connect(c.output, fb.input1), - connect(fb.input2, int.output), - connect(fb.output, gain.input), - connect(gain.output, int.input) - ], - t, - systems = [int, gain, c, fb]) -sys = mtkcompile(model) -@test length(unknowns(sys)) == length(equations(sys)) == 1 - -## Disturbance models when plant has multiple inputs -using ModelingToolkit, LinearAlgebra -using ModelingToolkit: DisturbanceModel, get_iv, get_disturbance_system -using ModelingToolkitStandardLibrary.Blocks -A, C = [randn(2, 2) for i in 1:2] -B = [1.0 0; 0 1.0] -@named model = Blocks.StateSpace(A, B, C) -@named integrator = Blocks.StateSpace([-0.001;;], [1.0;;], [1.0;;], [0.0;;]) - -ins = collect(complete(model).input.u) -outs = collect(complete(model).output.u) - -disturbed_input = ins[1] -@named dist_integ = DisturbanceModel(disturbed_input, integrator) - -f, augmented_sys, dvs, p = ModelingToolkit.add_input_disturbance(model, - dist_integ, - ins) - -augmented_sys = complete(augmented_sys) -matrices, -ssys = linearize(augmented_sys, - [ - augmented_sys.u, - augmented_sys.input.u[2], - augmented_sys.d - ], outs; - op = [augmented_sys.u => 0.0, augmented_sys.input.u[2] => 0.0, augmented_sys.d => 0.0]) -matrices = ModelingToolkit.reorder_unknowns( - matrices, unknowns(ssys), [ssys.x[1], ssys.x[2], ssys.integrator.x[1]]) -@test matrices.A ≈ [A [1; 0]; zeros(1, 2) -0.001] -@test matrices.B == I -@test matrices.C == [C zeros(2)] -@test matrices.D == zeros(2, 3) - # Verify using ControlSystemsBase # P = ss(A,B,C,0) # G = ss(matrices...) @@ -486,9 +397,9 @@ matrices = ModelingToolkit.reorder_unknowns( ] @named sys = System(eqs, t) - (; io_sys,) = ModelingToolkit.generate_control_function( + (; io_sys,) = ModelingToolkitBase.generate_control_function( sys, [u]; simplify = true) - obsfn = ModelingToolkit.build_explicit_observed_function( + obsfn = ModelingToolkitBase.build_explicit_observed_function( io_sys, [x + u * t]; inputs = [u]) @test obsfn([1.0], [2.0], MTKParameters(io_sys, []), 3.0) ≈ [7.0] end @@ -500,7 +411,7 @@ end eqs = [D(x) ~ c * x] @mtkcompile sys = System(eqs, t, [x], [c]) - f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys) + f, dvs, ps, io_sys = ModelingToolkitBase.generate_control_function(sys) @test f[1]([0.5], nothing, MTKParameters(io_sys, []), 0.0) ≈ [1.0] end @@ -509,7 +420,7 @@ end @parameters p(::Real) = (x -> 2x) eqs = [D(x) ~ -x + p(u)] @named sys = System(eqs, t) - f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, [u]) + f, dvs, ps, io_sys = ModelingToolkitBase.generate_control_function(sys, [u]) p = MTKParameters(io_sys, []) u = [1.0] x = [1.0] @@ -521,27 +432,27 @@ end eqs = [D(x) ~ x + y + z y ~ z] @named sys = System(eqs, t) - @test issetequal(ModelingToolkit.inputs(sys), [y]) - @test issetequal(ModelingToolkit.outputs(sys), [z]) + @test issetequal(ModelingToolkitBase.inputs(sys), [y]) + @test issetequal(ModelingToolkitBase.outputs(sys), [z]) ss1 = mtkcompile(sys, inputs = [y], outputs = [z]) - @test issetequal(ModelingToolkit.inputs(ss1), [y]) - @test issetequal(ModelingToolkit.outputs(ss1), [z]) + @test issetequal(ModelingToolkitBase.inputs(ss1), [y]) + @test issetequal(ModelingToolkitBase.outputs(ss1), [z]) ss2 = mtkcompile(sys, inputs = [z], outputs = [y]) - @test issetequal(ModelingToolkit.inputs(ss2), [z]) - @test issetequal(ModelingToolkit.outputs(ss2), [y]) + @test issetequal(ModelingToolkitBase.inputs(ss2), [z]) + @test issetequal(ModelingToolkitBase.outputs(ss2), [y]) end @testset "Retain inputs when composing systems" begin @variables x(t) y(t) [input=true] @named sys = System([D(x) ~ y * x], t) csys = compose(System(Equation[], t; name = :outer), sys) - @test issetequal(ModelingToolkit.inputs(csys), [sys.y]) + @test issetequal(ModelingToolkitBase.inputs(csys), [sys.y]) # More complex hierarchy @variables z(t) [input = true] w(t) @named sys2 = System([D(w) ~ z - w], t) cosys = compose(System(Equation[], t; name = :outermost), [csys, sys2]) - @test issetequal(ModelingToolkit.inputs(cosys), [csys.sys.y, sys2.z]) + @test issetequal(ModelingToolkitBase.inputs(cosys), [csys.sys.y, sys2.z]) end diff --git a/lib/ModelingToolkitBase/test/jacobiansparsity.jl b/lib/ModelingToolkitBase/test/jacobiansparsity.jl new file mode 100644 index 0000000000..fde36c8fb1 --- /dev/null +++ b/lib/ModelingToolkitBase/test/jacobiansparsity.jl @@ -0,0 +1,174 @@ +using ModelingToolkitBase, SparseArrays, OrdinaryDiffEq, DiffEqBase, BenchmarkTools +import SymbolicUtils as SU +using Test + +N = 3 +xyd_brusselator = range(0, stop = 1, length = N) +brusselator_f(x, y, t) = (((x - 0.3)^2 + (y - 0.6)^2) <= 0.1^2) * (t >= 1.1) * 5.0 +lim(a, N) = ModelingToolkitBase.ifelse(a == N + 1, 1, ModelingToolkitBase.ifelse(a == 0, N, a)) +function brusselator_2d_loop(du, u, p, t) + A, B, alpha, dx = p + alpha = alpha / dx^2 + @inbounds for I in CartesianIndices((N, N)) + i, j = Tuple(I) + x, y = xyd_brusselator[I[1]], xyd_brusselator[I[2]] + ip1, im1, jp1, jm1 = lim(i + 1, N), lim(i - 1, N), lim(j + 1, N), + lim(j - 1, N) + du[i, + j, + 1] = alpha * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] - + 4u[i, j, 1]) + + B + u[i, j, 1]^2 * u[i, j, 2] - (A + 1) * u[i, j, 1] + + brusselator_f(x, y, t) + du[i, + j, + 2] = alpha * (u[im1, j, 2] + u[ip1, j, 2] + u[i, jp1, 2] + u[i, jm1, 2] - + 4u[i, j, 2]) + + A * u[i, j, 1] - u[i, j, 1]^2 * u[i, j, 2] + end +end + +# Test with tuple parameters +p = (3.4, 1.0, 10.0, step(xyd_brusselator)) + +function init_brusselator_2d(xyd) + N = length(xyd) + u = zeros(N, N, 2) + for I in CartesianIndices((N, N)) + x = xyd[I[1]] + y = xyd[I[2]] + u[I, 1] = 22 * (y * (1 - y))^(3 / 2) + u[I, 2] = 27 * (x * (1 - x))^(3 / 2) + end + u +end + +u0 = init_brusselator_2d(xyd_brusselator) +prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, + u0, (0.0, 11.5), p) +sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) + +# test sparse jacobian pattern only. +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = true, jac = false) +JP = prob.f.jac_prototype +@test findnz(Symbolics.jacobian_sparsity(map(x -> x.rhs, equations(sys)), + unknowns(sys)))[1:2] == + findnz(JP)[1:2] + +# test sparse jacobian +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = true, jac = true) +#@test_nowarn solve(prob, Rosenbrock23()) +@test findnz(calculate_jacobian(sys, sparse = true))[1:2] == + findnz(prob.f.jac_prototype)[1:2] +out = similar(prob.f.jac_prototype) +@test (@ballocated $(prob.f.jac.f_iip)($out, $(prob.u0), $(prob.p), 0.0)) == 0 # should not allocate + +# test when not sparse +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = false, jac = true) +@test prob.f.jac_prototype == nothing + +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = false, jac = false) +@test prob.f.jac_prototype == nothing + +# test when u0 is nothing +f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = true) +@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2] +@test eltype(f.jac_prototype) == Float64 + +f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = false) +@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2] +@test eltype(f.jac_prototype) == Float64 + +# test sparsity index pattern checking +f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = true, checkbounds = true) +out = sparse([1.0 0.0; 0.0 1.0]) # choose a wrong size on purpose +@test size(out) != size(f.jac_prototype) # check that the size is indeed wrong +@test_throws AssertionError f.jac.f_iip(out, u0, p, 0.0) # check that we get an error + +# test when u0 is not Float64 +u0 = similar(init_brusselator_2d(xyd_brusselator), Float32) +prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, + u0, (0.0, 11.5), p) +sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) + +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = true, jac = false) +@test eltype(prob.f.jac_prototype) == Float32 + +prob = ODEProblem(sys, unknowns(sys) .=> vec(u0), (0, 11.5), sparse = true, jac = true) +@test eltype(prob.f.jac_prototype) == Float32 + +if @isdefined(ModelingToolkit) + @testset "W matrix sparsity" begin + t = ModelingToolkitBase.t_nounits + D = ModelingToolkitBase.D_nounits + @parameters g + @variables x(t) y(t) λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkcompile pend = System(eqs, t) + + u0 = [x => 1, y => 0] + prob = ODEProblem( + pend, [u0; [g => 1]], (0, 11.5), guesses = [λ => 1], sparse = true, jac = true) + jac, jac! = generate_jacobian(pend; expression = Val{false}, sparse = true, checkbounds = true) + jac_prototype = ModelingToolkitBase.jacobian_sparsity(pend) + W_prototype = ModelingToolkitBase.W_sparsity(pend) + @test nnz(W_prototype) == nnz(jac_prototype) + 2 + + # jac_prototype should be the same as W_prototype + @test findnz(prob.f.jac_prototype)[1:2] == findnz(W_prototype)[1:2] + + u = zeros(5) + p = prob.p + t = 0.0 + @test_throws AssertionError jac!(similar(jac_prototype, Float64), u, p, t) + + W, W! = generate_W(pend; expression = Val{false}, sparse = true, checkbounds = true) + γ = 0.1 + M = sparse(calculate_massmatrix(pend)) + @test_throws AssertionError W!(similar(jac_prototype, Float64), u, p, γ, t) + @test W!(similar(W_prototype, Float64), u, p, γ, t) == + 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) + end + + @testset "Issue#3556: Numerical accuracy" begin + t = ModelingToolkitBase.t_nounits + D = ModelingToolkitBase.D_nounits + @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] + @mtkcompile pend = System(eqs, t) + prob = ODEProblem(pend, [x => 0.0, D(x) => 1.0, g => 1.0], (0.0, 1.0); + guesses = [y => 1.0, λ => 1.0], jac = true, sparse = true) + J = deepcopy(prob.f.jac_prototype) + prob.f.jac(J, prob.u0, prob.p, 1.0) + # this currently works but may not continue to do so + # see https://github.com/SciML/ModelingToolkit.jl/pull/3556#issuecomment-2792664039 + @test J == prob.f.jac(prob.u0, prob.p, 1.0) + @test J ≈ prob.f.jac(prob.u0, prob.p, 1.0) + end + + # https://github.com/SciML/ModelingToolkit.jl/issues/3871 + @testset "Issue#3871: Sparsity with observed derivatives" begin + t = ModelingToolkitBase.t_nounits + D = ModelingToolkitBase.D_nounits + @variables x(t) y(t) + @mtkcompile sys = System([D(x) ~ x * D(y), D(y) ~ x - y], t) + @test ModelingToolkitBase.jacobian_sparsity(sys) == [1 1; 1 1] # all nonzero + J1 = calculate_jacobian(sys) + J2 = isequal(unknowns(sys)[1], x) ? [2x-y -x; 1 -1] : [-1 1; -x 2x-y] # analytical result + @test isequal(J1, J2) + prob = ODEProblem(sys, [x => 1.0, y => 0.0], (0.0, 1.0); jac = true, sparse = true) + sol = solve(prob, FBDF()) + @test SciMLBase.successful_retcode(sol) + ts = ModelingToolkitBase.get_tearing_state(sys) + for ieq in 1:2 + vars1 = ts.fullvars[ModelingToolkitBase.BipartiteGraphs.𝑠neighbors(ts.structure.graph, ieq)] + vars2 = SU.search_variables(equations(sys)[ieq]) + @test issetequal(vars1, vars2) + end + end +end diff --git a/test/jumpsystem.jl b/lib/ModelingToolkitBase/test/jumpsystem.jl similarity index 96% rename from test/jumpsystem.jl rename to lib/ModelingToolkitBase/test/jumpsystem.jl index e30516afe3..02adb4f9fa 100644 --- a/test/jumpsystem.jl +++ b/lib/ModelingToolkitBase/test/jumpsystem.jl @@ -1,10 +1,12 @@ -using ModelingToolkit, DiffEqBase, JumpProcesses, Test, LinearAlgebra -using SymbolicIndexingInterface +using ModelingToolkitBase, DiffEqBase, JumpProcesses, Test, LinearAlgebra +using SymbolicIndexingInterface, OrderedCollections using Random, StableRNGs, NonlinearSolve using OrdinaryDiffEq -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D using BenchmarkTools -MT = ModelingToolkit +using Symbolics: SymbolicT, unwrap +import SymbolicIndexingInterface as SII +MT = ModelingToolkitBase rng = StableRNG(12345) @@ -46,6 +48,9 @@ mutable struct TestInt{U, V, T} p::V t::T end +SII.state_values(x::TestInt) = x.u +SII.parameter_values(x::TestInt) = x.p +SII.current_time(x::TestInt) = x.t mtintegrator = TestInt(u, p, tf) integrator = TestInt(u, p, tf) @test abs(mtjump1.rate(u, p, tf) - jump1.rate(u, p, tf)) < 10 * eps() @@ -73,10 +78,11 @@ jprob = JumpProblem(js2, [u₀map; parammap], tspan; aggregator = Direct(), save_positions = (false, false), rng) p = parameter_values(jprob) @test jprob.prob isa DiscreteProblem -Nsims = 30000 +Nsims = 1000 function getmean(jprob, Nsims; use_stepper = true) m = 0.0 for i in 1:Nsims + i%200 == 0 && @info i sol = use_stepper ? solve(jprob, SSAStepper()) : solve(jprob) m += sol[end, end] end @@ -112,7 +118,7 @@ mtjumps = jprob.discrete_jump_aggregation @test abs(mtjumps.rates[1](u, p, tf) - jump1.rate(u, p, tf)) < 10 * eps() @test abs(mtjumps.rates[2](u, p, tf) - jump2.rate(u, p, tf)) < 10 * eps() -ModelingToolkit.@set! mtintegrator.p = (mtintegrator.p, (1,)) +ModelingToolkitBase.@set! mtintegrator.p = parameter_values(jprob) mtjumps.affects![1](mtintegrator) jump1.affect!(integrator) @test all(integrator.u .== mtintegrator.u) @@ -191,7 +197,7 @@ sol = solve(jprob, SSAStepper()); @testset "Combined system name collisions" begin sys1 = JumpSystem([maj1, maj2], t, [S], [β, γ], name = :sys1) sys2 = JumpSystem([maj1, maj2], t, [S], [β, γ], name = :sys1) - @test_throws ModelingToolkit.NonUniqueSubsystemsError JumpSystem( + @test_throws ModelingToolkitBase.NonUniqueSubsystemsError JumpSystem( [sys1.γ ~ sys2.γ], t, [], [], systems = [sys1, sys2], name = :foo) end @@ -359,9 +365,9 @@ end j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) j3 = VariableRateJump(p3, [x3 ~ Pre(x3) + 1, x4 ~ Pre(x4) + 1]) j4 = MassActionJump(p4 * p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) - us = Set() - ps = Set() - iv = t + us = OrderedSet{SymbolicT}() + ps = OrderedSet{SymbolicT}() + iv = unwrap(t) MT.collect_vars!(us, ps, j1, iv) @test issetequal(us, [x1]) @@ -403,9 +409,9 @@ end j4 = MassActionJump(p4 * p4, [x1 => 1, x4 => 1], [x1 => -1, x4 => -1, x2 => 1]) @named js = JumpSystem([j1, j2, j3, j4], t, [x1, x2, x3, x4], [p1, p2, p3, p4]) - us = Set() - ps = Set() - iv = t + us = OrderedSet{SymbolicT}() + ps = OrderedSet{SymbolicT}() + iv = unwrap(t) MT.collect_scoped_vars!(us, ps, js, iv) @test issetequal(us, [x2]) @test issetequal(ps, [p2]) diff --git a/test/latexify.jl b/lib/ModelingToolkitBase/test/latexify.jl similarity index 95% rename from test/latexify.jl rename to lib/ModelingToolkitBase/test/latexify.jl index de5c195610..518cef5b4f 100644 --- a/test/latexify.jl +++ b/lib/ModelingToolkitBase/test/latexify.jl @@ -1,8 +1,8 @@ using Test using Latexify -using ModelingToolkit +using ModelingToolkitBase using ReferenceTests -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D using ModelingToolkitStandardLibrary.Blocks ### Tips for generating latex tests: diff --git a/lib/ModelingToolkitBase/test/latexify/50.tex b/lib/ModelingToolkitBase/test/latexify/50.tex new file mode 100644 index 0000000000..ab2d9632d1 --- /dev/null +++ b/lib/ModelingToolkitBase/test/latexify/50.tex @@ -0,0 +1,19 @@ +\begin{equation} +\left[ +\begin{array}{c} +\mathrm{connect}\left( P_{+}output, C_{+}input \right) \\ +AnalysisPoint\left( \mathtt{C.output.u}\left( t \right), plant\_input, \left[ +\begin{array}{c} +\mathtt{P.input.u}\left( t \right) \\ +\end{array} +\right] \right) \\ +\mathrm{\mathtt{P.u}}\left( t \right) = \mathrm{\mathtt{P.input.u}}\left( t \right) \\ +\mathrm{\mathtt{P.y}}\left( t \right) = \mathrm{\mathtt{P.output.u}}\left( t \right) \\ +\mathrm{\mathtt{P.y}}\left( t \right) = \mathrm{\mathtt{P.x}}\left( t \right) \\ +\frac{\mathrm{d} \cdot \mathrm{\mathtt{P.x}}\left( t \right)}{\mathrm{d}t} = \frac{ - \mathrm{\mathtt{P.x}}\left( t \right) + \mathtt{P.k} \cdot \mathrm{\mathtt{P.u}}\left( t \right)}{\mathtt{P.T}} \\ +\mathrm{\mathtt{C.u}}\left( t \right) = \mathrm{\mathtt{C.input.u}}\left( t \right) \\ +\mathrm{\mathtt{C.y}}\left( t \right) = \mathrm{\mathtt{C.output.u}}\left( t \right) \\ +\mathrm{\mathtt{C.y}}\left( t \right) = \mathtt{C.k} \cdot \mathrm{\mathtt{C.u}}\left( t \right) \\ +\end{array} +\right] +\end{equation} diff --git a/test/linearity.jl b/lib/ModelingToolkitBase/test/linearity.jl similarity index 59% rename from test/linearity.jl rename to lib/ModelingToolkitBase/test/linearity.jl index d472cdb087..ea03ee50fc 100644 --- a/test/linearity.jl +++ b/lib/ModelingToolkitBase/test/linearity.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra +using ModelingToolkitBase, StaticArrays, LinearAlgebra using DiffEqBase using Test @@ -12,16 +12,16 @@ eqs = [D(x) ~ σ * (y - x), D(y) ~ -z - y, D(z) ~ y - β * z] -@test ModelingToolkit.islinear(@named sys = System(eqs, t)) +@test ModelingToolkitBase.islinear(@named sys = System(eqs, t)) eqs2 = [D(x) ~ σ * (y - x), D(y) ~ -z - 1 / y, D(z) ~ y - β * z] -@test !ModelingToolkit.islinear(@named sys = System(eqs2, t)) +@test !ModelingToolkitBase.islinear(@named sys = System(eqs2, t)) eqs3 = [D(x) ~ σ * (y - x), D(y) ~ -z - y, D(z) ~ y - β * z + 1] -@test ModelingToolkit.isaffine(@named sys = System(eqs, t)) +@test ModelingToolkitBase.isaffine(@named sys = System(eqs, t)) diff --git a/test/linearproblem.jl b/lib/ModelingToolkitBase/test/linearproblem.jl similarity index 94% rename from test/linearproblem.jl rename to lib/ModelingToolkitBase/test/linearproblem.jl index 308006d547..f95333857f 100644 --- a/test/linearproblem.jl +++ b/lib/ModelingToolkitBase/test/linearproblem.jl @@ -1,10 +1,11 @@ -using ModelingToolkit +using ModelingToolkitBase using LinearSolve using SciMLBase using StaticArrays using SparseArrays using Test -using ModelingToolkit: t_nounits as t, D_nounits as D, SystemCompatibilityError +using ModelingToolkitBase: t_nounits as t, D_nounits as D, SystemCompatibilityError +import SymbolicUtils as SU @testset "Rejects non-affine systems" begin @variables x y @@ -15,7 +16,11 @@ end @variables x[1:3] [irreducible = true] @parameters p[1:3, 1:3] q[1:3] -@mtkbuild sys = System([p * x ~ q]) +# Do not use `mtkcompile` to ensure a consistent variable/equation +# ordering. +eqs = SU.scalarize(zeros(3) ~ q - p * x) +@named sys = System(eqs, collect(x), [p, q]) +sys = complete(sys) # sanity check @test length(unknowns(sys)) == length(equations(sys)) == 3 A = Float64[1 2 3; 4 3.5 1.7; 5.2 1.8 9.7] diff --git a/test/mass_matrix.jl b/lib/ModelingToolkitBase/test/mass_matrix.jl similarity index 90% rename from test/mass_matrix.jl rename to lib/ModelingToolkitBase/test/mass_matrix.jl index 82d1cf86a5..5061779151 100644 --- a/test/mass_matrix.jl +++ b/lib/ModelingToolkitBase/test/mass_matrix.jl @@ -1,5 +1,5 @@ -using OrdinaryDiffEq, ModelingToolkit, Test, LinearAlgebra, StaticArrays -using ModelingToolkit: t_nounits as t, D_nounits as D, MTKParameters +using OrdinaryDiffEq, ModelingToolkitBase, Test, LinearAlgebra, StaticArrays +using ModelingToolkitBase: t_nounits as t, D_nounits as D, MTKParameters @variables y(t)[1:3] @parameters k[1:3] @@ -10,7 +10,7 @@ eqs = [D(y[1]) ~ -k[1] * y[1] + k[3] * y[2] * y[3], @named sys = System(eqs, t, collect(y), [k]) sys = complete(sys) -@test_throws ModelingToolkit.OperatorIndepvarMismatchError System(eqs, y[1]) +@test_throws ModelingToolkitBase.OperatorIndepvarMismatchError System(eqs, y[1]) M = calculate_massmatrix(sys) @test M isa Diagonal @test M == [1 0 0 diff --git a/test/modelingtoolkitize.jl b/lib/ModelingToolkitBase/test/modelingtoolkitize.jl similarity index 88% rename from test/modelingtoolkitize.jl rename to lib/ModelingToolkitBase/test/modelingtoolkitize.jl index dc29f9e1a1..23b32f7728 100644 --- a/test/modelingtoolkitize.jl +++ b/lib/ModelingToolkitBase/test/modelingtoolkitize.jl @@ -1,13 +1,14 @@ -using OrdinaryDiffEq, ModelingToolkit, DataStructures, Test +using OrdinaryDiffEq, ModelingToolkitBase, DataStructures, Test using Optimization, RecursiveArrayTools, OptimizationOptimJL using SymbolicIndexingInterface -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D +using Symbolics: value using SciMLBase: parameterless_type N = 32 const xyd_brusselator = range(0, stop = 1, length = N) brusselator_f(x, y, t) = (((x - 0.3)^2 + (y - 0.6)^2) <= 0.1^2) * (t >= 1.1) * 5.0 -limit(a, N) = ModelingToolkit.ifelse(a == N + 1, 1, ModelingToolkit.ifelse(a == 0, N, a)) +limit(a, N) = ModelingToolkitBase.ifelse(a == N + 1, 1, ModelingToolkitBase.ifelse(a == 0, N, a)) function brusselator_2d_loop(du, u, p, t) A, B, alpha, dx = p alpha = alpha / dx^2 @@ -78,10 +79,10 @@ prob = OptimizationProblem(ones(3); lb = [-Inf, 0.0, 1.0], ub = [Inf, 0.0, 2.0]) end sys = complete(modelingtoolkitize(prob)) -@test !ModelingToolkit.hasbounds(unknowns(sys)[1]) -@test !ModelingToolkit.hasbounds(unknowns(sys)[2]) -@test ModelingToolkit.hasbounds(unknowns(sys)[3]) -@test ModelingToolkit.getbounds(unknowns(sys)[3]) == (1.0, 2.0) +@test !ModelingToolkitBase.hasbounds(unknowns(sys)[1]) +@test !ModelingToolkitBase.hasbounds(unknowns(sys)[2]) +@test ModelingToolkitBase.hasbounds(unknowns(sys)[3]) +@test ModelingToolkitBase.getbounds(unknowns(sys)[3]) == (1.0, 2.0) ## SIR System Regression Test @@ -177,36 +178,38 @@ rv0 = ArrayPartition(r0, v0) f = function (dy, y, μ, t) r = sqrt(sum(y[1, :] .^ 2)) dy[1, :] = y[2, :] - dy[2, :] = -μ .* y[1, :] / r^3 + dy[2, :] .= -μ .* y[1, :] / r^3 end prob = ODEProblem(f, rv0, (0.0, Δt), μ) modelingtoolkitize(prob) # Index reduction and mass matrix handling -using LinearAlgebra -function pendulum!(du, u, p, t) - x, dx, y, dy, T = u - g, L = p - du[1] = dx - du[2] = T * x - du[3] = dy - du[4] = T * y - g - du[5] = x^2 + y^2 - L^2 - return nothing +if @isdefined(ModelingToolkit) + using LinearAlgebra + function pendulum!(du, u, p, t) + x, dx, y, dy, T = u + g, L = p + du[1] = dx + du[2] = T * x + du[3] = dy + du[4] = T * y - g + du[5] = x^2 + y^2 - L^2 + return nothing + end + pendulum_fun! = ODEFunction(pendulum!, mass_matrix = Diagonal([1, 1, 1, 1, 0])) + u0 = [1.0, 0, 0, 0, 0] + p = [9.8, 1] + tspan = (0, 10.0) + pendulum_prob = ODEProblem(pendulum_fun!, u0, tspan, p) + pendulum_sys_org = complete(modelingtoolkitize(pendulum_prob)) + sts = unknowns(pendulum_sys_org) + pendulum_sys = dae_index_lowering(pendulum_sys_org) + prob = ODEProblem(pendulum_sys, Pair[], tspan) + sol = solve(prob, Rodas4()) + l2 = sol[sts[1]] .^ 2 + sol[sts[3]] .^ 2 + @test all(l -> abs(sqrt(l) - 1) < 0.05, l2) end -pendulum_fun! = ODEFunction(pendulum!, mass_matrix = Diagonal([1, 1, 1, 1, 0])) -u0 = [1.0, 0, 0, 0, 0] -p = [9.8, 1] -tspan = (0, 10.0) -pendulum_prob = ODEProblem(pendulum_fun!, u0, tspan, p) -pendulum_sys_org = complete(modelingtoolkitize(pendulum_prob)) -sts = unknowns(pendulum_sys_org) -pendulum_sys = dae_index_lowering(pendulum_sys_org) -prob = ODEProblem(pendulum_sys, Pair[], tspan) -sol = solve(prob, Rodas4()) -l2 = sol[sts[1]] .^ 2 + sol[sts[3]] .^ 2 -@test all(l -> abs(sqrt(l) - 1) < 0.05, l2) ff911 = (du, u, p, t) -> begin du[1] = u[2] + 1.0 @@ -259,8 +262,8 @@ params = OrderedDict(:a => 10, :b => 20) u0 = [1, 2.0] prob = ODEProblem(ode_prob_dict, u0, (0.0, 1.0), params) sys = modelingtoolkitize(prob) -@test [ModelingToolkit.defaults(sys)[s] for s in unknowns(sys)] == u0 -@test [ModelingToolkit.defaults(sys)[s] for s in parameters(sys)] == [10, 20] +@test [value(ModelingToolkitBase.initial_conditions(sys)[s]) for s in unknowns(sys)] == u0 +@test [value(ModelingToolkitBase.initial_conditions(sys)[s]) for s in parameters(sys)] == [10, 20] @parameters sig=10 rho=28.0 beta=8/3 @variables x(t)=100 y(t)=1.0 z(t)=1 @@ -445,10 +448,10 @@ 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(mtkcompile(sys; fully_determined = false))) == 0 +@test length(equations(sys)) == 2 +@test length(equations(mtkcompile(sys; fully_determined = false))) == 0 broken=!@isdefined(ModelingToolkit) -@testset "`modelingtoolkitize(::SDEProblem)` sets defaults" begin +@testset "`modelingtoolkitize(::SDEProblem)` sets initial conditions" begin function sdeg!(du, u, p, t) du[1] = 0.3 * u[1] du[2] = 0.3 * u[2] @@ -466,7 +469,7 @@ sys = modelingtoolkitize(prob) p = [10.0, 28.0, 2.66] sprob = SDEProblem(sdef!, sdeg!, u0, tspan, p) sys = complete(modelingtoolkitize(sprob)) - @test length(ModelingToolkit.defaults(sys)) == 3length(u0) + length(p) + @test length(ModelingToolkitBase.initial_conditions(sys)) == length(u0) + length(p) sprob2 = SDEProblem(sys, [], tspan) truevals = similar(u0) diff --git a/test/mtkparameters.jl b/lib/ModelingToolkitBase/test/mtkparameters.jl similarity index 78% rename from test/mtkparameters.jl rename to lib/ModelingToolkitBase/test/mtkparameters.jl index 566e309378..4b20066a1f 100644 --- a/test/mtkparameters.jl +++ b/lib/ModelingToolkitBase/test/mtkparameters.jl @@ -1,5 +1,5 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D, MTKParameters +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D, MTKParameters using SymbolicIndexingInterface, StaticArrays using SciMLStructures: SciMLStructures, canonicalize, Tunable, Discrete, Constants using ModelingToolkitStandardLibrary.Electrical, ModelingToolkitStandardLibrary.Blocks @@ -7,12 +7,15 @@ using BlockArrays: BlockedArray, BlockedVector, Block using OrdinaryDiffEq using ForwardDiff using JET +using Test -@parameters a b c(t) d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String +@discretes c(t) +@parameters a b d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String @named sys = System( - [b ~ 2a], t, [], [a, b, c, d, e, f, g, h]; - continuous_events = [ModelingToolkit.SymbolicContinuousCallback( - [a ~ 0] => [c ~ 0], discrete_parameters = c)], defaults = Dict(a => 0.0)) + Equation[], t, [], [a, b, c, d, e, f, g, h]; + continuous_events = [ModelingToolkitBase.SymbolicContinuousCallback( + [a ~ 0] => [c ~ 0], discrete_parameters = c)], bindings = [b => 2a], + initial_conditions = Dict(a => 0.0)) sys = complete(sys) ivs = Dict(c => 3a, d => 4, e => [5.0, 6.0, 7.0], @@ -146,7 +149,7 @@ eq = D(X) ~ p[1] - p[2] * X u0 = [X => 1.0] ps = [p => [2.0, 0.1]] p = MTKParameters(osys, [ps; u0]) -@test p.tunable == [2.0, 0.1] +@test sort(p.tunable) == [0.1, 2.0] # Ensure partial update promotes the buffer @parameters p q r @@ -167,53 +170,50 @@ 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; ps], tspan) +@test_throws "Could not evaluate" ODEProblem(sys, [u0; ps], tspan) @test_nowarn ODEProblem(sys, [u0; ps; [d => 1.0]], tspan) # 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 + @parameters p1=0.5 [tunable=true] p2=1 [tunable=true] p3=3 [tunable=false] p4=3 [tunable=true] y0 @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 - y0 ~ 2p4] + D(y) ~ -p3 * y + p4 * x * y] sys = mtkcompile(complete(System( - eqs, t, name = :sys))) + eqs, t, name = :sys, bindings = [y0 => 2p4]))) prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys, [], (0.0, 3.0)) 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 + @parameters p1=0.5 [tunable=true] (p23[1:2]=[1, 3.0]) [tunable=true] p4=3 [tunable=false] y0 @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 - y0 ~ 2p4] + D(y) ~ -p23[2] * y + p4 * x * y] sys = mtkcompile(complete(System( - eqs, t, name = :sys))) + eqs, t, name = :sys, bindings = [y0 => 2p4]))) prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys, [], (0.0, 3.0)) 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 + @parameters p1=0.5 [tunable=true] (p23[1:2]=[1, 3.0]) [tunable=true] p4::Int=3 [tunable=true] y0::Int @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 - y0 ~ 2p4] + D(y) ~ -p23[2] * y + p4 * x * y] sys = mtkcompile(complete(System( - eqs, t, name = :sys))) + eqs, t, name = :sys, bindings = [y0 => 2p4]))) prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys, [], (0.0, 3.0)) end @@ -225,24 +225,24 @@ end @inferred canonicalize(portion, ps) # broken because the size of a vector of vectors can't be determined at compile time - @test_opt target_modules=(ModelingToolkit,) canonicalize( + @test_opt target_modules=(ModelingToolkitBase,) canonicalize( portion, ps) buffer, repack, alias = canonicalize(portion, ps) # broken because dependent update functions break inference - @test_call target_modules=(ModelingToolkit,) SciMLStructures.replace( + @test_call target_modules=(ModelingToolkitBase,) SciMLStructures.replace( portion, ps, ones(length(buffer))) @inferred SciMLStructures.replace( portion, ps, ones(length(buffer))) @inferred MTKParameters SciMLStructures.replace(portion, ps, ones(length(buffer))) - @test_opt target_modules=(ModelingToolkit,) SciMLStructures.replace( + @test_opt target_modules=(ModelingToolkitBase,) SciMLStructures.replace( portion, ps, ones(length(buffer))) - @test_call target_modules=(ModelingToolkit,) SciMLStructures.replace!( + @test_call target_modules=(ModelingToolkitBase,) SciMLStructures.replace!( portion, ps, ones(length(buffer))) @inferred SciMLStructures.replace!(portion, ps, ones(length(buffer))) - @test_opt target_modules=(ModelingToolkit,) SciMLStructures.replace!( + @test_opt target_modules=(ModelingToolkitBase,) SciMLStructures.replace!( portion, ps, ones(length(buffer))) end end @@ -272,11 +272,12 @@ VDual = Vector{<:ForwardDiff.Dual} VVDual = Vector{<:Vector{<:ForwardDiff.Dual}} @testset "Parameter type validation" begin - struct Foo{T} + abstract type AbstractFooT end + struct Foo{T} <: AbstractFooT x::T end - @parameters a b::Int c::Vector{Float64} d[1:2, 1:2]::Int e::Foo{Int} f::Foo + @parameters a b::Int c::Vector{Float64} d[1:2, 1:2]::Int e::Foo{Int} f::AbstractFooT @named sys = System(Equation[], t, [], [a, b, c, d, e, f]) sys = complete(sys) ps = MTKParameters(sys, @@ -315,7 +316,7 @@ end @testset "Error on missing parameter defaults" begin @parameters a b c - @named sys = System(Equation[], t, [], [a, b]; defaults = Dict(b => 2c)) + @named sys = System(Equation[], t, [], [a, b]; initial_conditions = Dict(b => 2c)) sys = complete(sys) @test_throws ["Could not evaluate", "b", "Missing", "2c"] MTKParameters(sys, [a => 1.0]) end @@ -343,11 +344,11 @@ ps = MTKParameters([1.0, 1.0], (), (BlockedArray(zeros(4), [2, 2]),), ps2 = SciMLStructures.replace(Discrete(), ps, ones(4)) @test typeof(ps2.discrete) == typeof(ps.discrete) with_updated_parameter_timeseries_values( - sys, ps, 1 => ModelingToolkit.NestedGetIndex(([5.0, 10.0],))) + sys, ps, 1 => ModelingToolkitBase.NestedGetIndex(([5.0, 10.0],))) @test ps.discrete[1][Block(1)] == [5.0, 10.0] with_updated_parameter_timeseries_values( - sys, ps, 1 => ModelingToolkit.NestedGetIndex(([3.0, 30.0],)), - 2 => ModelingToolkit.NestedGetIndex(([4.0, 40.0],))) + sys, ps, 1 => ModelingToolkitBase.NestedGetIndex(([3.0, 30.0],)), + 2 => ModelingToolkitBase.NestedGetIndex(([4.0, 40.0],))) @test ps.discrete[1][Block(1)] == [3.0, 30.0] @test ps.discrete[1][Block(2)] == [4.0, 40.0] @test SciMLBase.get_saveable_values(sys, ps, 1).x == (ps.discrete[1][Block(1)],) @@ -366,7 +367,7 @@ tsidx2 = 2 @test length(ps.discrete[1][Block(tsidx2)]) == 3 @test length(ps.discrete[2][Block(tsidx2)]) == 0 with_updated_parameter_timeseries_values( - sys, ps, tsidx1 => ModelingToolkit.NestedGetIndex(([10.0, 11.0, 12.0], [false]))) + sys, ps, tsidx1 => ModelingToolkitBase.NestedGetIndex(([10.0, 11.0, 12.0], [false]))) @test ps.discrete[1][Block(tsidx1)] == [10.0, 11.0, 12.0] @test ps.discrete[2][Block(tsidx1)][] == false @@ -381,53 +382,55 @@ with_updated_parameter_timeseries_values( @test ps2.nonnumeric isa Tuple{Vector{Any}} end -@testset "Issue#3925: Autodiff after `subset_tunables`" begin - function circuit_model() - @named resistor1 = Resistor(R=5.0) - @named resistor2 = Resistor(R=2.0) - @named capacitor1 = Capacitor(C=2.4) - @named capacitor2 = Capacitor(C=60.0) - @named source = Voltage() - @named input_signal = Sine(frequency=1.0) - @named ground = Ground() - @named ampermeter = CurrentSensor() - - eqs = [connect(input_signal.output, source.V) - connect(source.p, capacitor1.n, capacitor2.n) - connect(source.n, resistor1.p, resistor2.p, ground.g) - connect(resistor1.n, capacitor1.p, ampermeter.n) - connect(resistor2.n, capacitor2.p, ampermeter.p)] - - @named circuit_model = System(eqs, t, - systems=[ - resistor1, resistor2, capacitor1, capacitor2, - source, input_signal, ground, ampermeter - ]) +if @isdefined(ModelingToolkit) + @testset "Issue#3925: Autodiff after `subset_tunables`" begin + function circuit_model() + @named resistor1 = Resistor(R=5.0) + @named resistor2 = Resistor(R=2.0) + @named capacitor1 = Capacitor(C=2.4) + @named capacitor2 = Capacitor(C=60.0) + @named source = Voltage() + @named input_signal = Sine(frequency=1.0) + @named ground = Ground() + @named ampermeter = CurrentSensor() + + eqs = [connect(input_signal.output, source.V) + connect(source.p, capacitor1.n, capacitor2.n) + connect(source.n, resistor1.p, resistor2.p, ground.g) + connect(resistor1.n, capacitor1.p, ampermeter.n) + connect(resistor2.n, capacitor2.p, ampermeter.p)] + + @named circuit_model = System(eqs, t, + systems=[ + resistor1, resistor2, capacitor1, capacitor2, + source, input_signal, ground, ampermeter + ], guesses = [capacitor1.i => 1.0]) + end + + model = circuit_model() + sys = mtkcompile(model) + + tunable_parameters(sys) + + sub_sys = subset_tunables(sys, [sys.capacitor2.C]) + + tunable_parameters(sub_sys) + + prob = ODEProblem(sub_sys, [sys.capacitor2.v => 0.0], (0, 3.)) + + setter = setsym_oop(prob, [sys.capacitor2.C]); + + function loss(x, ps) + setter, prob = ps + u0, p = setter(prob, x) + new_prob = remake(prob; u0, p) + sol = solve(new_prob, Rodas5P()) + sum(sol) + end + + grad = ForwardDiff.gradient(Base.Fix2(loss, (setter, prob)), [3.0]) + @test grad ≈ [0.14882627068752538] atol=1e-10 end - - model = circuit_model() - sys = mtkcompile(model) - - tunable_parameters(sys) - - sub_sys = subset_tunables(sys, [sys.capacitor2.C]) - - tunable_parameters(sub_sys) - - prob = ODEProblem(sub_sys, [sys.capacitor2.v => 0.0], (0, 3.)) - - setter = setsym_oop(prob, [sys.capacitor2.C]); - - function loss(x, ps) - setter, prob = ps - u0, p = setter(prob, x) - new_prob = remake(prob; u0, p) - sol = solve(new_prob, Rodas5P()) - sum(sol) - end - - grad = ForwardDiff.gradient(Base.Fix2(loss, (setter, prob)), [3.0]) - @test grad ≈ [0.14882627068752538] atol=1e-10 end @testset "MTKParameters can be made `isbits`" begin diff --git a/test/namespacing.jl b/lib/ModelingToolkitBase/test/namespacing.jl similarity index 78% rename from test/namespacing.jl rename to lib/ModelingToolkitBase/test/namespacing.jl index 4cc8ea7296..4fa2c4cdcb 100644 --- a/test/namespacing.jl +++ b/lib/ModelingToolkitBase/test/namespacing.jl @@ -1,6 +1,7 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D, iscomplete, does_namespacing, +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D, iscomplete, does_namespacing, renamespace +using Test @variables x(t) @parameters p @@ -31,8 +32,7 @@ nsys = toggle_namespacing(sys, false) @named inner = System([D(x) ~ x, y ~ 2x + 1], t) @test issetequal(unknowns(inner), [x, y]) ss = mtkcompile(inner) - @test isequal(only(unknowns(ss)), x) - @test isequal(only(observed(ss)), y ~ 2x + 1) + @test issetequal(unknowns(ss), [x, y]) @named sys = System(Equation[], t; systems = [inner]) xx, yy = let sys = inner @@ -42,6 +42,5 @@ nsys = toggle_namespacing(sys, false) end @test issetequal(unknowns(sys), [xx, yy]) ss = mtkcompile(sys) - @test isequal(only(unknowns(ss)), xx) - @test isequal(only(observed(ss)), yy ~ 2xx + 1) + @test isequal(unknowns(ss), [xx, yy]) end diff --git a/test/nonlinearsystem.jl b/lib/ModelingToolkitBase/test/nonlinearsystem.jl similarity index 71% rename from test/nonlinearsystem.jl rename to lib/ModelingToolkitBase/test/nonlinearsystem.jl index 4c887f5740..ee97cec585 100644 --- a/test/nonlinearsystem.jl +++ b/lib/ModelingToolkitBase/test/nonlinearsystem.jl @@ -1,11 +1,13 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra +using ModelingToolkitBase, StaticArrays, LinearAlgebra using DiffEqBase, SparseArrays using Test using NonlinearSolve using ForwardDiff using SymbolicIndexingInterface -using ModelingToolkit: value -using ModelingToolkit: get_default_or_guess, MTKParameters +using ModelingToolkitBase: value +using ModelingToolkitBase: get_default_or_guess, MTKParameters +import SymbolicUtils as SU +using Symbolics: VartypeT, unwrap canonequal(a, b) = isequal(simplify(a), simplify(b)) @@ -25,7 +27,7 @@ end eqs = [0 ~ σ * (y - x) * h, 0 ~ x * (ρ - z) - y, 0 ~ x * y - β * z] -@named ns = System(eqs, [x, y, z], [σ, ρ, β, h], defaults = Dict(x => 2)) +@named ns = System(eqs, [x, y, z], [σ, ρ, β, h], initial_conditions = Dict(x => 2)) ns2 = eval(toexpr(ns)) @test issetequal(equations(ns), equations(ns2)) @test issetequal(unknowns(ns), unknowns(ns2)) @@ -51,9 +53,9 @@ jac = calculate_jacobian(ns) @testset "nlsys jacobian" begin @test canonequal(jac[1, 1], σ * -1) @test canonequal(jac[1, 2], σ) - @test canonequal(jac[1, 3], 0) + @test canonequal(jac[1, 3], SU.Const{VartypeT}(0)) @test canonequal(jac[2, 1], ρ - z) - @test canonequal(jac[2, 2], -1) + @test canonequal(jac[2, 2], SU.Const{VartypeT}(-1)) @test canonequal(jac[2, 3], x * -1) @test canonequal(jac[3, 1], y) @test canonequal(jac[3, 2], x) @@ -72,15 +74,15 @@ nlsys_func = generate_rhs(ns) nf = NonlinearFunction(ns) jac = calculate_jacobian(ns) -@test ModelingToolkit.jacobian_sparsity(ns).colptr == sparse(jac).colptr -@test ModelingToolkit.jacobian_sparsity(ns).rowval == sparse(jac).rowval +@test ModelingToolkitBase.jacobian_sparsity(ns).colptr == sparse(Num.(jac)).colptr +@test ModelingToolkitBase.jacobian_sparsity(ns).rowval == sparse(Num.(jac)).rowval jac = generate_jacobian(ns) sH = calculate_hessian(ns) -@test getfield.(ModelingToolkit.hessian_sparsity(ns), :colptr) == +@test getfield.(ModelingToolkitBase.hessian_sparsity(ns), :colptr) == getfield.(sparse.(sH), :colptr) -@test getfield.(ModelingToolkit.hessian_sparsity(ns), :rowval) == +@test getfield.(ModelingToolkitBase.hessian_sparsity(ns), :rowval) == getfield.(sparse.(sH), :rowval) prob = NonlinearProblem(ns, [x => 1.0, y => 1.0, z => 1.0, σ => 1.0, ρ => 1.0, β => 1.0]) @@ -111,7 +113,9 @@ lorenz2 = lorenz(:lorenz2) lorenz2.F ~ lorenz1.u], [s, a], [h], systems = [lorenz1, lorenz2]) -@test_nowarn alias_elimination(connected) +if @isdefined(ModelingToolkit) + @test_nowarn alias_elimination(connected) +end # system promotion using OrdinaryDiffEq @@ -120,11 +124,13 @@ D = Differential(t) @named subsys = convert_system_indepvar(lorenz1, t) @named sys = System([D(subsys.x) ~ subsys.x + subsys.x], t, systems = [subsys]) sys = mtkcompile(sys) -u0 = [subsys.x => 1, subsys.z => 2.0, subsys.y => 1.0] -prob = ODEProblem(sys, [u0; [subsys.σ => 1, subsys.ρ => 2, subsys.β => 3]], (0, 1.0)) +u0 = [subsys.x => 1] +prob = ODEProblem(sys, [u0; [subsys.σ => 1, subsys.ρ => 2, subsys.β => 3]], (0, 1.0); guesses = [subsys.y => 1]) 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_indepvar(sys, t) +if @isdefined(ModelingToolkit) + @test_throws ArgumentError convert_system_indepvar(sys, t) +end @parameters σ ρ β @variables x y z @@ -150,7 +156,7 @@ np = NonlinearProblem( function issue819() sys1 = makesys(:sys1) sys2 = makesys(:sys1) - @test_throws ModelingToolkit.NonUniqueSubsystemsError System( + @test_throws ModelingToolkitBase.NonUniqueSubsystemsError System( [sys2.f ~ sys1.x, sys1.f ~ 0], [], [], systems = [sys1, sys2], name = :foo) end @@ -190,14 +196,16 @@ RHS2 = RHS @test isequal(RHS, RHS2) # issue #1358 -@independent_variables t -@variables v1(t) v2(t) i1(t) i2(t) -eq = [v1 ~ sin(2pi * t * h) - v1 - v2 ~ i1 - v2 ~ i2 - i1 ~ i2] -@named sys = System(eq, t) -@test length(equations(mtkcompile(sys))) == 0 +if @isdefined(ModelingToolkit) + @independent_variables t + @variables v1(t) v2(t) i1(t) i2(t) + eq = [v1 ~ sin(2pi * t * h) + v1 - v2 ~ i1 + v2 ~ i2 + i1 ~ i2] + @named sys = System(eq, t) + @test length(equations(mtkcompile(sys))) == 0 +end @testset "Remake" begin @parameters a=1.0 b=1.0 c=1.0 @@ -207,9 +215,9 @@ eq = [v1 ~ sin(2pi * t * h) eqs = [0 ~ a * (y - x) * h, 0 ~ x * (b - z) - y, 0 ~ x * y - c * z] - @named sys = System(eqs, [x, y, z], [a, b, c, h], defaults = Dict(x => 2.0)) + @named sys = System(eqs, [x, y, z], [a, b, c, h], initial_conditions = Dict(x => 2.0)) sys = complete(sys) - prob = NonlinearProblem(sys, ones(length(unknowns(sys)))) + prob = NonlinearProblem(sys, unknowns(sys) .=> ones(length(unknowns(sys)))) prob_ = remake(prob, u0 = [1.0, 2.0, 3.0], p = [a => 1.1, b => 1.2, c => 1.3]) @test prob_.u0 == [1.0, 2.0, 3.0] @@ -244,24 +252,28 @@ alg_eqs = [0 ~ p - d * X] sys = @test_nowarn System(alg_eqs; name = :name) @test isequal(only(unknowns(sys)), X) -@test all(isequal.(parameters(sys), [p, d])) +@test issetequal(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 = System(eqs, [u1, u2], [u3, u4]) -sys = mtkcompile(ns; fully_determined = false) -@test length(unknowns(sys)) == 1 +if @isdefined(ModelingToolkit) + @variables u1 u2 + @parameters u3 u4 + eqs = [u3 ~ u1 + u2, u4 ~ 2 * (u1 + u2), u3 + u4 ~ 3 * (u1 + u2)] + @named ns = System(eqs, [u1, u2], [u3, u4]) + sys = mtkcompile(ns; fully_determined = false) + @test length(unknowns(sys)) == 1 +end # Conservative -@variables X(t) -alg_eqs = [1 ~ 2X] -@named ns = System(alg_eqs) -sys = mtkcompile(ns) -@test length(equations(sys)) == 0 -sys = mtkcompile(ns; conservative = true) -@test length(equations(sys)) == 1 +if @isdefined(ModelingToolkit) + @variables X(t) + alg_eqs = [1 ~ 2X] + @named ns = System(alg_eqs) + sys = mtkcompile(ns) + @test length(equations(sys)) == 0 + sys = mtkcompile(ns; conservative = true) + @test length(equations(sys)) == 1 +end # https://github.com/SciML/ModelingToolkit.jl/issues/2858 @testset "Jacobian/Hessian with observed equations that depend on unknowns" begin @@ -270,12 +282,12 @@ sys = mtkcompile(ns; conservative = true) eqs = [0 ~ σ * (y - x) 0 ~ x * (ρ - z) - y 0 ~ x * y - β * z] - guesses = [x => 1.0, z => 0.0] + guesses = [x => 1.0, y => 1.0, z => 0.0] ps = [σ => 10.0, ρ => 26.0, β => 8 / 3] @mtkcompile ns = System(eqs) @test isequal(calculate_jacobian(ns), [(-1 - z + ρ)*σ -x*σ - 2x*(-z + ρ) -β-(x^2)]) + 2x*(-z + ρ) -β-(x^2)]) broken=!@isdefined(ModelingToolkit) # solve without analytical jacobian prob = NonlinearProblem(ns, [guesses; ps]) sol = solve(prob, NewtonRaphson()) @@ -290,8 +302,14 @@ sys = mtkcompile(ns; conservative = true) @variables x y z eqs = [0 ~ x^2 + 2z + y, z ~ y, y ~ x] # analytical solution x = y = z = 0 or -3 @mtkcompile ns = System(eqs) # solve for y with observed chain z -> y -> x - @test isequal(expand.(calculate_jacobian(ns)), [-3 // 2 - x;;]) - @test isequal(calculate_hessian(ns), [[-1;;]]) + mtkjac = expand.(calculate_jacobian(ns)) + jac1 = unwrap.([3//2 + y;;]) + jac2 = unwrap.([-3//2 - x;;]) + @test isequal(mtkjac, jac1) || isequal(mtkjac, jac2) broken=!@isdefined(ModelingToolkit) + mtkhess = calculate_hessian(ns) + hess1 = [Num[1;;]] + hess2 = [Num[-1;;]] + @test isequal(mtkhess, hess1) || isequal(mtkhess, hess2) broken=!@isdefined(ModelingToolkit) 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 @@ -318,21 +336,23 @@ end @test all(sol[x] .≈ A \ b) end -@testset "resid_prototype when system has no unknowns and an equation" begin - @variables x - @parameters p - @named sys = System([x ~ 1, x^2 - p ~ 0]) - for sys in [ - mtkcompile(sys, fully_determined = false), - mtkcompile(sys, fully_determined = false, split = false) - ] - @test length(equations(sys)) == 1 - @test length(unknowns(sys)) == 0 - T = typeof(ForwardDiff.Dual(1.0)) - prob = NonlinearProblem(sys, [p => ForwardDiff.Dual(1.0)]; check_length = false) - @test prob.f(Float64[], prob.p) isa Vector{T} - @test prob.f.resid_prototype isa Vector{T} - @test_nowarn solve(prob) +if @isdefined(ModelingToolkit) + @testset "resid_prototype when system has no unknowns and an equation" begin + @variables x + @parameters p + @named sys = System([x ~ 1, x^2 - p ~ 0]) + for sys in [ + mtkcompile(sys, fully_determined = false), + mtkcompile(sys, fully_determined = false, split = false) + ] + @test length(equations(sys)) == 1 + @test length(unknowns(sys)) == 0 + T = typeof(ForwardDiff.Dual(1.0)) + prob = NonlinearProblem(sys, [p => ForwardDiff.Dual(1.0)]; check_length = false) + @test prob.f(Float64[], prob.p) isa Vector{T} + @test prob.f.resid_prototype isa Vector{T} + @test_nowarn solve(prob) + end end end @@ -371,40 +391,40 @@ end @testset "Can convert from `System`" begin @variables x(t) y(t) @parameters p q r - @named sys = System([D(x) ~ p * x^3 + q, 0 ~ -y + q * x - r, r ~ 3p], t; - defaults = [x => 1.0, p => missing], guesses = [p => 1.0], + @named sys = System([D(x) ~ p * x^3 + q, 0 ~ -y + q * x - r], t; + initial_conditions = [x => 1.0], bindings = [p => missing, r => 3p], guesses = [p => 1.0], initialization_eqs = [p^3 + q^3 ~ 4r]) nlsys = NonlinearSystem(sys) nlsys = complete(nlsys) - defs = defaults(nlsys) - @test length(defs) == 6 - @test defs[x] == 1.0 - @test defs[p] === missing - @test isinf(defs[t]) + @test value(initial_conditions(nlsys)[x]) == 1.0 + @test value(bindings(nlsys)[p]) === missing + @test isequal(bindings(nlsys)[r], 3p) + @test isinf(value(bindings(nlsys)[t])) @test length(guesses(nlsys)) == 1 - @test guesses(nlsys)[p] == 1.0 + @test value(guesses(nlsys)[p]) == 1.0 @test length(initialization_equations(nlsys)) == 1 - @test length(parameter_dependencies(nlsys)) == 1 @test length(equations(nlsys)) == 2 - @test all(iszero, [eq.lhs for eq in equations(nlsys)]) + @test all(iszero, [value(eq.lhs) for eq in equations(nlsys)]) @test nameof(nlsys) == nameof(sys) - @test ModelingToolkit.iscomplete(nlsys) + @test ModelingToolkitBase.iscomplete(nlsys) sys1 = complete(sys; split = false) nlsys = NonlinearSystem(sys1) - @test ModelingToolkit.iscomplete(nlsys) - @test !ModelingToolkit.is_split(nlsys) + @test ModelingToolkitBase.iscomplete(nlsys) + @test !ModelingToolkitBase.is_split(nlsys) sys2 = complete(sys) nlsys = NonlinearSystem(sys2) - @test ModelingToolkit.iscomplete(nlsys) - @test ModelingToolkit.is_split(nlsys) + @test ModelingToolkitBase.iscomplete(nlsys) + @test ModelingToolkitBase.is_split(nlsys) sys3 = mtkcompile(sys) nlsys = NonlinearSystem(sys3) - @test length(equations(nlsys)) == length(ModelingToolkit.observed(nlsys)) == 1 + if @isdefined(ModelingToolkit) + @test length(equations(nlsys)) == length(ModelingToolkitBase.observed(nlsys)) == 1 + end - prob = NonlinearProblem(sys3, [q => 2.0]) + prob = NonlinearProblem(sys3, [y => 1.0, q => 2.0]) @test prob.f.initialization_data.initializeprobmap === nothing sol = solve(prob) @test SciMLBase.successful_retcode(sol) @@ -413,7 +433,7 @@ end @testset "Differential inside expression also substituted" begin @named sys = System([0 ~ y * D(x) + x^2 - p, 0 ~ x * D(y) + y * p], t) nlsys = NonlinearSystem(sys) - vs = ModelingToolkit.vars(equations(nlsys)) + vs = SU.search_variables(equations(nlsys)) @test !in(D(x), vs) @test !in(D(y), vs) end @@ -438,7 +458,7 @@ end @testset "`ProblemTypeCtx`" begin @variables x @mtkcompile sys = System( - [0 ~ x^2 - 4x + 4]; metadata = [ModelingToolkit.ProblemTypeCtx => "A"]) + [0 ~ x^2 - 4x + 4]; metadata = [ModelingToolkitBase.ProblemTypeCtx => "A"]) prob = NonlinearProblem(sys, [x => 1.0]) @test prob.problem_type == "A" end diff --git a/test/odesystem.jl b/lib/ModelingToolkitBase/test/odesystem.jl similarity index 77% rename from test/odesystem.jl rename to lib/ModelingToolkitBase/test/odesystem.jl index cf16b31a42..148b51eb35 100644 --- a/test/odesystem.jl +++ b/lib/ModelingToolkitBase/test/odesystem.jl @@ -1,5 +1,5 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra -using ModelingToolkit: get_metadata, MTKParameters, SymbolicDiscreteCallback, +using ModelingToolkitBase, StaticArrays, LinearAlgebra +using ModelingToolkitBase: get_metadata, MTKParameters, SymbolicDiscreteCallback, SymbolicContinuousCallback using SymbolicIndexingInterface using OrdinaryDiffEq, Sundials @@ -9,11 +9,12 @@ using Test using SymbolicUtils.Code using SymbolicUtils: Sym, issym using ForwardDiff -using ModelingToolkit: value -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: value +using ModelingToolkitBase: t_nounits as t, D_nounits as D using Symbolics using Symbolics: unwrap using DiffEqBase: isinplace +using SciCompDSL # Define some variables @parameters σ ρ β @@ -26,8 +27,8 @@ eqs = [D(x) ~ σ * (y - x), D(y) ~ x * (ρ - z) - y, D(z) ~ x * y - β * z * κ] -ModelingToolkit.toexpr.(eqs)[1] -@named de = System(eqs, t; defaults = Dict(x => 1)) +ModelingToolkitBase.toexpr.(eqs)[1] +@named de = System(eqs, t; initial_conditions = Dict(x => 1)) subed = substitute(de, [σ => k]) ssort(eqs) = sort(eqs, by = string) @test isequal(ssort(parameters(subed)), [k, β, κ, ρ]) @@ -36,7 +37,7 @@ ssort(eqs) = sort(eqs, by = string) D(y) ~ (ρ - z) * x - y D(z) ~ x * y - β * κ * z]) @named des[1:3] = System(eqs, t) -@test length(unique(x -> ModelingToolkit.get_tag(x), des)) == 1 +@test length(unique(x -> ModelingToolkitBase.get_tag(x), des)) == 1 de2 = eval(toexpr(de)) @test issetequal(equations(de2), eqs) @@ -65,14 +66,14 @@ f = ODEFunction(de, tgrad = true, jac = true) # iip du = zeros(3) u = collect(1:3) -p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) +p = ModelingToolkitBase.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) f.f(du, u, p, 0.1) @test du == [4, 0, -16] # oop du = @SArray zeros(3) u = SVector(1:3...) -p = ModelingToolkit.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) +p = ModelingToolkitBase.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) @test f.f(u, p, 0.1) === @SArray [4.0, 0.0, -16.0] # iip vs oop @@ -80,7 +81,7 @@ du = zeros(3) g = similar(du) J = zeros(3, 3) u = collect(1:3) -p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) +p = ModelingToolkitBase.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) f.f(du, u, p, 0.1) @test du == f(u, p, 0.1) f.tgrad(g, u, p, t) @@ -92,7 +93,7 @@ f.jac(J, u, p, t) f = ODEFunction(de; iip_config = (false, true)) du = zeros(3) u = collect(1:3) -p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) +p = ModelingToolkitBase.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) f.f(du, u, p, 0.1) @test du == [4, 0, -16] @test_throws ArgumentError f.f(u, p, 0.1) @@ -118,7 +119,7 @@ end #check sparsity f = eval(ODEFunction(de, sparsity = true, expression = Val{true})) -@test f.sparsity == ModelingToolkit.jacobian_sparsity(de) +@test f.sparsity == ModelingToolkitBase.jacobian_sparsity(de) f = eval(ODEFunction(de, sparsity = false, expression = Val{true})) @test isnothing(f.sparsity) @@ -128,12 +129,12 @@ eqs = [D(x) ~ σ * (y - x), D(z) ~ x * y - β * z * κ] @named de = System(eqs, t) de = complete(de) -ModelingToolkit.calculate_tgrad(de) +ModelingToolkitBase.calculate_tgrad(de) -tgrad_oop, tgrad_iip = eval.(ModelingToolkit.generate_tgrad(de)) +tgrad_oop, tgrad_iip = eval.(ModelingToolkitBase.generate_tgrad(de)) u = SVector(1:3...) -p = ModelingToolkit.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) +p = ModelingToolkitBase.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) @test tgrad_oop(u, p, t) == [0.0, -u[2], 0.0] du = zeros(3) tgrad_iip(du, u, p, t) @@ -166,8 +167,8 @@ eqs = [D(x) ~ σ * a, D(z) ~ x * y - β * z * κ] @named de = System(eqs, t) jac = calculate_jacobian(de) -@test ModelingToolkit.jacobian_sparsity(de).colptr == sparse(jac).colptr -@test ModelingToolkit.jacobian_sparsity(de).rowval == sparse(jac).rowval +@test ModelingToolkitBase.jacobian_sparsity(de).colptr == sparse(Num.(jac)).colptr +@test ModelingToolkitBase.jacobian_sparsity(de).rowval == sparse(Num.(jac)).rowval f = ODEFunction(complete(de)) @@ -185,28 +186,32 @@ eqs = [D(x) ~ -A * x, du ≈ f([1.0, 2.0], [1, 2, 3], 0.0) end -function lotka(u, p, t) - x = u[1] - y = u[2] - [p[1] * x - p[2] * x * y, - -p[3] * y + p[4] * x * y] -end - -prob = ODEProblem(ODEFunction{false}(lotka), [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) -de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) +@testset "modelingtoolkitize - OOP" begin + function lotka(u, p, t) + x = u[1] + y = u[2] + [p[1] * x - p[2] * x * y, + -p[3] * y + p[4] * x * y] + end -function lotka(du, u, p, t) - x = u[1] - y = u[2] - du[1] = p[1] * x - p[2] * x * y - du[2] = -p[3] * y + p[4] * x * y + prob = ODEProblem(ODEFunction{false}(lotka), [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) + de = complete(modelingtoolkitize(prob)) + ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) end -prob = ODEProblem(lotka, [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) +@testset "modelingtoolkitize - IIP" begin + function lotka(du, u, p, t) + x = u[1] + y = u[2] + du[1] = p[1] * x - p[2] * x * y + du[2] = -p[3] * y + p[4] * x * y + end -de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) + prob = ODEProblem(lotka, [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) + + de = complete(modelingtoolkitize(prob)) + ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) +end # automatic unknown detection for DAEs @parameters k₁ k₂ k₃ @@ -215,7 +220,7 @@ ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) eqs = [D(y₁) ~ -k₁ * y₁ + k₃ * y₂ * y₃, 0 ~ y₁ + y₂ + y₃ - 1, D(y₂) ~ k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃ * κ] -@named sys = System(eqs, t, defaults = [k₁ => 100, k₂ => 3e7, y₁ => 1.0]) +@named sys = System(eqs, t, initial_conditions = [k₁ => 100, k₂ => 3e7, y₁ => 1.0]) sys = complete(sys) u0 = Pair[] push!(u0, y₂ => 0.0) @@ -254,6 +259,8 @@ for p in [prob_pmap, prob_dpmap] end sol_pmap = solve(prob_pmap, Rodas5()) sol_dpmap = solve(prob_dpmap, Rodas5()) +@test sol_pmap.retcode == ReturnCode.InitialFailure +@test sol_dpmap.retcode == ReturnCode.InitialFailure @test all(isequal(0.05), sol_pmap.(0:10:100, idxs = k₁)) @test sol_pmap.u ≈ sol_dpmap.u @@ -290,7 +297,8 @@ prob3 = ODEProblem(sys, [u0; p], tspan, jac = true, sparse = true) #SparseMatrix @test prob3.f.jac_prototype isa SparseMatrixCSC prob3 = ODEProblem(sys, [u0; p], tspan, jac = true, sparsity = true) @test prob3.f.sparsity isa SparseMatrixCSC -@test_throws ArgumentError ODEProblem(sys, zeros(5), tspan) +# Symbolic initial conditions are disallowed +@test_throws ArgumentError ODEProblem(sys, zeros(3), tspan) for (prob, atol) in [(prob1, 1e-12), (prob2, 1e-12), (prob3, 1e-12)] local sol sol = solve(prob, Rodas5()) @@ -300,7 +308,7 @@ end du0 = [D(y₁) => -0.04 D(y₂) => 0.04 D(y₃) => 0.0] -prob4 = DAEProblem(sys, [du0; u0; p2], tspan) +prob4 = DAEProblem(sys, [du0; [y₁ => nothing]; p2], tspan; guesses = u01) prob5 = eval(DAEProblem(sys, [du0; u0; p2], tspan; expression = Val{true})) for prob in [prob4, prob5] local sol @@ -318,7 +326,7 @@ eqs = [D(x) ~ σ * (y - x), @test issetequal(unknowns(sys), [x, y, z]) @test issetequal(parameters(sys), [σ, β]) @test equations(sys) == eqs -@test ModelingToolkit.isautonomous(sys) +@test ModelingToolkitBase.isautonomous(sys) @testset "Issue#701: `collect_vars!` handles non-call symbolics" begin @parameters a @@ -335,7 +343,7 @@ eqs = [ 0 ~ x1 - x2 ] @named sys = System(eqs, t) -@test isequal(ModelingToolkit.get_iv(sys), t) +@test isequal(ModelingToolkitBase.get_iv(sys), t) @test isequal(unknowns(sys), [x1, x2]) @test isempty(parameters(sys)) @@ -358,7 +366,7 @@ eq = D(x) ~ r * x sys1 = makesys(:sys1) sys2 = makesys(:sys1) - @test_throws ModelingToolkit.NonUniqueSubsystemsError System( + @test_throws ModelingToolkitBase.NonUniqueSubsystemsError System( [sys2.f ~ sys1.x, D(sys1.f) ~ 0], t, systems = [sys1, sys2], name = :foo) end @@ -383,15 +391,7 @@ der = Differential(w) eqs = [ der(u1) ~ t ] -@test_throws ArgumentError ModelingToolkit.System(eqs, t, vars, pars, name = :foo) - -# check_eqs_u0 kwarg test -@variables x1(t) x2(t) -eqs = [D(x1) ~ -x1] -@named sys = System(eqs, t, [x1, x2], []) -sys = complete(sys) -@test_throws ArgumentError ODEProblem(sys, [1.0, 1.0], (0.0, 1.0)) -@test_nowarn ODEProblem(sys, [1.0, 1.0], (0.0, 1.0), check_length = false) +@test_throws ArgumentError ModelingToolkitBase.System(eqs, t, vars, pars, name = :foo) @testset "Issue#1109" begin @variables x(t)[1:3, 1:3] @@ -414,7 +414,7 @@ sys = mtkcompile(sys) @test_nowarn sys.x, sys.y, sys.p @test all(x -> x isa Symbolics.Arr, (sys.x, sys.p)) @test all(x -> x isa Symbolics.Arr, @nonamespace (sys.x, sys.p)) -@test ModelingToolkit.isvariable(Symbolics.unwrap(x[1])) +@test ModelingToolkitBase.isvariable(Symbolics.unwrap(x[1])) prob = ODEProblem(sys, [], (0, 1.0)) sol = solve(prob, Tsit5()) @test sol[2x[1] + 3x[3] + norm(x)] ≈ @@ -424,7 +424,7 @@ sol = solve(prob, Tsit5()) map((x, y) -> x[2] .+ 2y, sol[x], sol[y]), map((x, y) -> x[3] .+ 3y, sol[x], sol[y])) -using ModelingToolkit +using ModelingToolkitBase function submodel(; name) @variables y(t) @@ -468,15 +468,17 @@ eqs = [D(x) ~ foo(x, ms); D(ms) ~ bar(ms, p)] @mtkcompile outersys = compose(emptysys, sys) prob = ODEProblem( outersys, [sys.x => 1.0, sys.ms => 1:3, sys.p => ones(3, 3)], (0.0, 1.0)) -@test_nowarn solve(prob, Tsit5()) -obsfn = ModelingToolkit.build_explicit_observed_function( +sol = @test_nowarn solve(prob, Tsit5()) +obsfn = ModelingToolkitBase.build_explicit_observed_function( outersys, bar(3outersys.sys.ms, 3outersys.sys.p)) @test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) # x/x -@variables x(t) -@named sys = System([D(x) ~ x / x], t) -@test equations(alias_elimination(sys)) == [D(x) ~ 1] +if @isdefined(ModelingToolkit) + @variables x(t) + @named sys = System([D(x) ~ x / x], t) + @test equations(alias_elimination(sys)) == [D(x) ~ 1] +end # observed variable handling @variables x(t) RHS(t) @@ -535,7 +537,7 @@ sys = complete(sys) us = map(s -> (@variables $s(t))[1], syms) ps = map(s -> (@variables $s(t))[1], syms_p) buffer, = @variables $buffername[1:length(u0)] - dummy_var = Sym{Any}(:_) # this is safe because _ cannot be a rvalue in Julia + dummy_var = Symbolics.SSym(:_; type = Any) # this is safe because _ cannot be a rvalue in Julia ss = Iterators.flatten((us, ps)) vv = Iterators.flatten((u0, p0)) @@ -548,7 +550,7 @@ sys = complete(sys) D(us[i]) ~ dummy_identity(buffer[i], us[i]) end - @named sys = System(eqs, t, us, ps; defaults = defs, preface = preface) + @named sys = System(eqs, t, us, ps; initial_conditions = defs, preface = preface) sys = complete(sys) # don't build initializeprob because it will use preface in other functions and # affect `c` @@ -559,8 +561,8 @@ sys = complete(sys) end let - x = map(xx -> xx(t), Symbolics.variables(:x, 1:2, T = SymbolicUtils.FnType)) - @variables y(t) = 0 + x = map(xx -> xx(t), Symbolics.variables(:x, 1:2, T = SymbolicUtils.FnType{Tuple, Real, Nothing})) + @variables y(t) @parameters k = 1 eqs = [D(x[1]) ~ x[2] D(x[2]) ~ -x[1] - 0.5 * x[2] + k @@ -570,9 +572,11 @@ let u0 = x .=> [0.5, 0] du0 = D.(x) .=> 0.0 + if !@isdefined(ModelingToolkit) + push!(du0, D(y) => 0.0) + end prob = DAEProblem(sys, du0, (0, 50); guesses = u0) - @test prob[x] ≈ [0.5, 1.0] - @test prob.du0 ≈ [0.0, 0.0] + @test prob.du0 ≈ zeros(length(unknowns(sys))) @test prob.p isa MTKParameters @test prob.ps[k] ≈ 1 sol = solve(prob, IDA()) @@ -580,9 +584,7 @@ let @test isapprox(sol[x[1]][end], 1, atol = 1e-3) prob = DAEProblem(sys, [D(y) => 0, D(x[1]) => 0, D(x[2]) => 0], (0, 50); guesses = u0) - - @test prob[x] ≈ [0.5, 1] - @test prob.du0 ≈ [0, 0] + @test prob.du0 ≈ zeros(length(unknowns(sys))) @test prob.p isa MTKParameters @test prob.ps[k] ≈ 1 sol = solve(prob, IDA()) @@ -590,16 +592,17 @@ let prob = DAEProblem(sys, [D(y) => 0, D(x[1]) => 0, D(x[2]) => 0, k => 2], (0, 50); guesses = u0) - @test prob[x] ≈ [0.5, 3] - @test prob.du0 ≈ [0, 0] + @test prob.du0 ≈ zeros(length(unknowns(sys))) @test prob.p isa MTKParameters @test prob.ps[k] ≈ 2 sol = solve(prob, IDA()) @test isapprox(sol[x[1]][end], 2, atol = 1e-3) - # no initial conditions for D(x[1]) and D(x[2]) provided - @test_throws ModelingToolkit.MissingVariablesError prob=DAEProblem( - sys, Pair[], (0, 50); guesses = u0) + if !@isdefined(ModelingToolkit) + # no initial conditions for D(x[1]) and D(x[2]) provided + @test_throws ModelingToolkitBase.MissingVariablesError prob=DAEProblem( + sys, Pair[], (0, 50); guesses = u0) + end prob = ODEProblem(sys, Pair[x[1] => 0], (0, 50)) sol = solve(prob, Rosenbrock23()) @@ -621,7 +624,7 @@ let @test prob.ps[k2] == 1 && prob.ps[k2] isa Int end -let +if @isdefined(ModelingToolkit) @parameters C L R @variables q(t) p(t) F(t) @@ -643,13 +646,14 @@ let spm ~ 0 sph ~ a] @named sys = System(eqs, t, vars, pars) - @test_throws ModelingToolkit.ExtraEquationsSystemException mtkcompile(sys) + errmod = @isdefined(ModelingToolkit) ? ModelingToolkit.StateSelection : ModelingToolkitBase + @test_throws errmod.ExtraEquationsSystemException mtkcompile(sys) end # 1561 let vars = @variables x y - arr = ModelingToolkit.varmap_to_vars( + arr = ModelingToolkitBase.varmap_to_vars( Dict([x => 0.0, y => [0.0, 1.0]]), vars; use_union = true) #error sol = Union{Float64, Vector{Float64}}[0.0, [ 0.0, 1.0]] @@ -657,33 +661,6 @@ let @test typeof(arr) == typeof(sol) end -let - u = collect(first(@variables u(t)[1:4])) - Dt = D - - eqs = [Differential(t)(u[2]) - 1.1u[1] ~ 0 - Differential(t)(u[3]) - 1.1u[2] ~ 0 - u[1] ~ 0.0 - u[4] ~ 0.0] - - ps = [] - - @named sys = System(eqs, t, u, ps) - @test_nowarn simpsys = mtkcompile(sys) - - sys = mtkcompile(sys) - - u0 = ModelingToolkit.missing_variable_defaults(sys) - u0_expected = Pair[s => 0.0 for s in unknowns(sys)] - @test string(u0) == string(u0_expected) - - u0 = ModelingToolkit.missing_variable_defaults(sys, [1, 2]) - u0_expected = Pair[s => i for (i, s) in enumerate(unknowns(sys))] - @test string(u0) == string(u0_expected) - - @test_nowarn ODEProblem(sys, u0, (0, 1)) -end - # https://github.com/SciML/ModelingToolkit.jl/issues/1583 let @parameters k @@ -699,7 +676,7 @@ let function sys1(; name) vars = @variables x(t)=0.0 dx(t)=0.0 - System([D(x) ~ dx], t, vars, []; name, defaults = [D(x) => x]) + System([D(x) ~ dx], t, vars, []; name, initial_conditions = [D(x) => x]) end function sys2(; name) @@ -715,12 +692,14 @@ let @test isequal(parameters(s1), parameters(s1′)) @test isequal(equations(s1), equations(s1′)) - defs = Dict(s1.dx => 0.0, D(s1.x) => s1.x, s1.x => 0.0) - @test isequal(ModelingToolkit.defaults(s2), defs) + ics = initial_conditions(s2) + @test value(ics[s1.dx]) == 0.0 + @test isequal(ics[D(s1.x)], s1.x) + @test value(ics[s1.x]) == 0.0 end # https://github.com/SciML/ModelingToolkit.jl/issues/1705 -let +if @isdefined(ModelingToolkit) x0 = 0.0 v0 = 1.0 @@ -781,9 +760,13 @@ end eqs = [D(Q) ~ 1 / sin(P), D(P) ~ log(-cos(Q))] @named sys = System(eqs, t, [P, Q], []) sys = complete(debug_system(sys)) -prob = ODEProblem(sys, [], (0.0, 1.0)) -@test_throws "log(-cos(Q(t))) errors" prob.f([1, 0], prob.p, 0.0) -@test_throws "/(1, sin(P(t))) output non-finite value" prob.f([0, 2], prob.p, 0.0) +if @isdefined(ModelingToolkit) + prob = ODEProblem(sys, [], (0.0, 1.0)) + @test_throws "log(-cos(Q(t))) errors" prob.f([1, 0], prob.p, 0.0) + @test_throws "/(1, sin(P(t))) output non-finite value" prob.f([0, 2], prob.p, 0.0) +else + @test_throws "/(1, sin(P(t))) output non-finite value" ODEProblem(sys, [], (0.0, 1.0)) +end let @variables x(t) = 1 @@ -829,43 +812,43 @@ let # Issue https://github.com/SciML/ModelingToolkit.jl/issues/2322 sys_simp = mtkcompile(sys) - @test a ∈ keys(ModelingToolkit.defaults(sys_simp)) + @test a ∈ keys(ModelingToolkitBase.initial_conditions(sys_simp)) tspan = (0.0, 1) prob = ODEProblem(sys_simp, [], tspan) sol = solve(prob, Rodas4()) - @test sol(1)[]≈0.6065307685451087 rtol=1e-4 + @test sol(1; idxs=x)≈0.6065307685451087 rtol=1e-4 end # Issue#2599 -@variables x(t) y(t) -eqs = [D(x) ~ x * t, y ~ 2x] -@mtkcompile sys = System(eqs, t; continuous_events = [[y ~ 3] => [x ~ 2]]) -prob = ODEProblem(sys, [x => 1.0], (0.0, 10.0)) -@test_nowarn solve(prob, Tsit5()) +# @variables x(t) y(t) +# eqs = [D(x) ~ x * t, y ~ 2x] +# @mtkcompile sys = System(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 -@testset "Arrays in affect/condition equations" begin - @variables x(t)[1:3] - @parameters p[1:3, 1:3] - eqs = [ - D(x) ~ p * x - ] - @mtkcompile sys = System( - 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), p => ones(3, 3)], (0.0, 10.0)) - sol1 = @test_nowarn solve(prob1, Tsit5()) - - # array condition equations also used to not work - @mtkcompile sys = System( - 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), p => ones(3, 3)], (0.0, 10.0)) - sol2 = @test_nowarn solve(prob2, Tsit5()) - - @test sol1.u ≈ sol2.u -end +# @testset "Arrays in affect/condition equations" begin +# @variables x(t)[1:3] +# @parameters p[1:3, 1:3] +# eqs = [ +# D(x) ~ p * x +# ] +# @mtkcompile sys = System( +# 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), p => ones(3, 3)], (0.0, 10.0)) +# sol1 = @test_nowarn solve(prob1, Tsit5()) + +# # array condition equations also used to not work +# @mtkcompile sys = System( +# 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), p => ones(3, 3)], (0.0, 10.0)) +# sol2 = @test_nowarn solve(prob2, Tsit5()) + +# @test sol1.u ≈ sol2.u +# end # Requires fix in symbolics for `linear_expansion(p * x, D(y))` @test_skip begin @@ -933,7 +916,7 @@ end @mtkcompile model = FML2() -@test isequal(ModelingToolkit.defaults(model)[model.constant.k], model.k2[1]) +@test isequal(ModelingToolkitBase.bindings(model)[model.constant.k], model.k2[1]) @test_nowarn ODEProblem(model, [], (0.0, 10.0)) # Issue#2477 @@ -958,58 +941,66 @@ function RealExpressionSystem(; name) System(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)) +if @isdefined(ModelingToolkit) + @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)) +end # Guesses in hierarchical systems @variables x(t) y(t) @named sys = System(Equation[], t, [x], []; guesses = [x => 1.0]) @named outer = System( [D(y) ~ sys.x + t, 0 ~ t + y - sys.x * y], t, [y], []; systems = [sys]) -@test ModelingToolkit.guesses(outer)[sys.x] == 1.0 +@test value(ModelingToolkitBase.guesses(outer)[sys.x]) == 1.0 outer = mtkcompile(outer) -@test ModelingToolkit.get_guesses(outer)[sys.x] == 1.0 +@test value(ModelingToolkitBase.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 = System(Equation[], t, [x], []) -sys1 = complete(sys) -@named sys = System(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 +@testset "Storing array unknown indices in IndexCache" begin + @variables x(t)[1:2] + @named sys = System(Equation[], t, [x], []) + sys1 = complete(sys) + @named sys = System(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 -end -@variables x(t)[1:2, 1:2] -@named sys = System(Equation[], t, [x], []) -sys1 = complete(sys) -@named sys = System(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] + @variables x(t)[1:2, 1:2] + @named sys = System(Equation[], t, [x], []) + sys1 = complete(sys) + @named sys = System(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 end @testset "Non-1-indexed variable array (issue #2670)" begin @variables x(t)[0:1] # 0-indexed variable array @named sys = System([x[0] ~ 0.0, D(x[1]) ~ x[0]], t, [x], []) - @test_nowarn sys = mtkcompile(sys) - @test equations(sys) == [D(x[1]) ~ 0.0] + sys = @test_nowarn mtkcompile(sys) + if @isdefined(ModelingToolkit) + @test full_equations(sys) == [D(x[1]) ~ 0.0] + else + @test issetequal(full_equations(sys), [D(x[1]) ~ x[0], 0 ~ -x[0]]) + end end # Namespacing of array variables @@ -1038,9 +1029,9 @@ end @parameters P @variables x(t) sys = mtkcompile(System([D(x) ~ P], t, [x], [P]; name = :sys)) - obsfn = ModelingToolkit.build_explicit_observed_function( + obsfn = ModelingToolkitBase.build_explicit_observed_function( sys, [x + 1, x + P, x + t], return_inplace = true)[2] - ps = ModelingToolkit.MTKParameters(sys, [P => 2.0]) + ps = ModelingToolkitBase.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] @@ -1076,7 +1067,11 @@ end @named sys2 = System(eqs, T; initialization_eqs, guesses) prob2 = ODEProblem(mtkcompile(sys2), [], (1.0, 2.0)) sol2 = solve(prob2) - @test all(sol2[x] .== 1.0) + if @isdefined(ModelingToolkit) + @test all(sol2[x] .== 1.0) + else + @test all(sol2[x] .≈ 1.0) + end end # https://github.com/SciML/ModelingToolkit.jl/issues/2502 @@ -1087,26 +1082,26 @@ end @named B1 = System(Equation[], t, [], []) @named A2 = System(Equation[], t, [], []; metadata = A) @named B2 = System(Equation[], t, [], []; metadata = B) - n_core_metadata = length(ModelingToolkit.get_metadata(A1)) - @test length(ModelingToolkit.get_metadata(extend(A1, B1))) == n_core_metadata - meta = ModelingToolkit.get_metadata(extend(A1, B2)) + n_core_metadata = length(ModelingToolkitBase.get_metadata(A1)) + @test length(ModelingToolkitBase.get_metadata(extend(A1, B1))) == n_core_metadata + meta = ModelingToolkitBase.get_metadata(extend(A1, B2)) @test length(meta) == n_core_metadata + 1 @test meta[String] == 2 - meta = ModelingToolkit.get_metadata(extend(A2, B1)) + meta = ModelingToolkitBase.get_metadata(extend(A2, B1)) @test length(meta) == n_core_metadata + 1 @test meta[Int] == 1 - meta = ModelingToolkit.get_metadata(extend(A2, B2)) + meta = ModelingToolkitBase.get_metadata(extend(A2, B2)) @test length(meta) == n_core_metadata + 2 @test meta[Int] == 1 @test meta[String] == 2 end # https://github.com/SciML/ModelingToolkit.jl/issues/2859 -@testset "Initialization with defaults from observed equations (edge case)" begin +@testset "Initialization with initial conditions 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 = System(eqs, t; defaults) + initial_conditions = [x => 1, z => y] + @named sys = System(eqs, t; initial_conditions) ssys = mtkcompile(sys) prob = ODEProblem(ssys, [], (0.0, 1.0)) @test prob[x] == prob[y] == prob[z] == 1.0 @@ -1114,8 +1109,8 @@ end @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 = System(eqs, t; defaults) + initial_conditions = [y0 => 1, x => 1, z => y] + @named sys = System(eqs, t; initial_conditions) ssys = mtkcompile(sys) prob = ODEProblem(ssys, [], (0.0, 1.0)) @test prob[x] == prob[y] == prob[z] == 1.0 @@ -1128,11 +1123,11 @@ end [D(u) ~ (sum(u) + sum(x) + sum(p) + sum(o)) * x, o ~ prod(u) * x], t, [u..., x..., o...], [p...]) sys1 = mtkcompile(sys, inputs = [x...], outputs = []) - fn1, = ModelingToolkit.generate_rhs(sys1; expression = Val{false}) + fn1, = ModelingToolkitBase.generate_rhs(sys1; expression = Val{false}) ps = MTKParameters(sys1, [x => 2ones(2), p => 3ones(2, 2)]) @test_nowarn fn1(ones(4), ps, 4.0) sys2 = mtkcompile(sys, inputs = [x...], outputs = [], split = false) - fn2, = ModelingToolkit.generate_rhs(sys2; expression = Val{false}) + fn2, = ModelingToolkitBase.generate_rhs(sys2; expression = Val{false}) ps = zeros(8) setp(sys2, x)(ps, 2ones(2)) setp(sys2, p)(ps, 2ones(2, 2)) @@ -1175,7 +1170,7 @@ end sys = complete(sys) u0 = [sys.y => -1.0, sys.modela.x => -1.0] - p = defaults(sys) + p = initial_conditions(sys) prob = ODEProblem(sys, merge(p, Dict(u0)), (0.0, 1.0)) # evaluate @@ -1191,32 +1186,34 @@ end @test t === sys.t === sys.sys.t end -@testset "Substituting preserves parameter dependencies, defaults, guesses" begin +@testset "Substituting preserves parameter dependencies, initial conditions, guesses" begin @parameters p1 p2 @variables x(t) y(t) - @named sys = System([D(x) ~ y + p2, p2 ~ 2p1], t; - defaults = [p1 => 1.0, p2 => 2.0], guesses = [p1 => 2.0, p2 => 3.0]) + @named sys = System([D(x) ~ y + p2 + p1], t; + initial_conditions = [p1 => 1.0], guesses = [p1 => 2.0, p2 => 3.0], bindings = [p2 => 2p1]) @parameters p3 sys2 = substitute(sys, [p1 => p3]) sys2 = complete(sys2) @test length(parameters(sys2)) == 1 @test is_parameter(sys2, p3) @test !is_parameter(sys2, p1) - @test length(ModelingToolkit.defaults(sys2)) == 7 - @test ModelingToolkit.defaults(sys2)[p3] == 1.0 - @test length(ModelingToolkit.guesses(sys2)) == 2 - @test ModelingToolkit.guesses(sys2)[p3] == 2.0 + @test length(ModelingToolkitBase.initial_conditions(sys2)) == 1 + @test value(ModelingToolkitBase.initial_conditions(sys2)[p3]) == 1.0 + @test length(ModelingToolkitBase.guesses(sys2)) == 2 + @test value(ModelingToolkitBase.guesses(sys2)[p3]) == 2.0 + @test length(bindings(sys2)) == 1 + @test isequal(bindings(sys2)[p2], 2p3) end @testset "Substituting with nested systems" begin @parameters p1 p2 @variables x(t) y(t) - @named innersys = System([D(x) ~ y + p2; p2 ~ 2p1], t; - defaults = [p1 => 1.0, p2 => 2.0], guesses = [p1 => 2.0, p2 => 3.0]) + @named innersys = System([D(x) ~ y + p2 + p1], t; + initial_conditions = [p1 => 1.0], guesses = [p1 => 2.0, p2 => 3.0], bindings = [p2 => 2p1]) @parameters p3 p4 @named outersys = System( - [D(innersys.y) ~ innersys.y + p4, p4 ~ 3p3], t; - defaults = [p3 => 3.0, p4 => 9.0], guesses = [p4 => 10.0], systems = [innersys]) + [D(innersys.y) ~ innersys.y + p4 + p3], t; + initial_conditions = [p3 => 3.0], guesses = [p4 => 10.0], bindings = [p4 => 3p3], systems = [innersys]) @test_nowarn mtkcompile(outersys) @parameters p5 sys2 = substitute(outersys, [p4 => p5]) @@ -1224,13 +1221,13 @@ end @test_nowarn mtkcompile(sys2) @test length(equations(sys2)) == 2 @test length(parameters(sys2)) == 2 - @test length(full_parameters(sys2)) == 10 - @test all(!isequal(p4), full_parameters(sys2)) - @test any(isequal(p5), full_parameters(sys2)) - @test length(ModelingToolkit.defaults(sys2)) == 10 - @test ModelingToolkit.defaults(sys2)[p5] == 9.0 - @test length(ModelingToolkit.guesses(sys2)) == 3 - @test ModelingToolkit.guesses(sys2)[p5] == 10.0 + @test length(parameters(sys2; initial_parameters = true)) == 6 + @test issetequal(bound_parameters(sys2), [sys2.p5, sys2.innersys.p2]) + @test all(!isequal(p4), parameters(sys2)) + @test length(ModelingToolkitBase.initial_conditions(sys2)) == 2 + @test isequal(ModelingToolkitBase.bindings(sys2)[p5], 3p3) + @test length(ModelingToolkitBase.guesses(sys2)) == 3 + @test value(ModelingToolkitBase.guesses(sys2)[p5]) == 10.0 end @testset "Observed with inputs" begin @@ -1245,12 +1242,12 @@ end @named sys = System(eqs, t, [u..., x..., o], [p...]) sys1 = mtkcompile(sys, inputs = [x...], outputs = [o...], split = false) - @test_nowarn ModelingToolkit.build_explicit_observed_function(sys1, u; inputs = [x...]) + @test_nowarn ModelingToolkitBase.build_explicit_observed_function(sys1, u; inputs = [x...]) - obsfn = ModelingToolkit.build_explicit_observed_function( + obsfn = ModelingToolkitBase.build_explicit_observed_function( sys1, u + x + p[1:2]; inputs = [x...]) - @test obsfn(ones(2), 2ones(2), 3ones(12), 4.0) == 6ones(2) + @test obsfn(ones(length(unknowns(sys))), 2ones(2), 3ones(12), 4.0) == 6ones(2) end @testset "Passing `nothing` to `u0`" begin @@ -1263,10 +1260,10 @@ end @testset "ODEs are not DDEs" begin @variables x(t) @named sys = System(D(x) ~ x, t) - @test !ModelingToolkit.is_dde(sys) + @test !ModelingToolkitBase.is_dde(sys) @test is_markovian(sys) @named sys2 = System(Equation[], t; systems = [sys]) - @test !ModelingToolkit.is_dde(sys) + @test !ModelingToolkitBase.is_dde(sys) @test is_markovian(sys) end @@ -1319,7 +1316,7 @@ end @parameters p[1:2] q @mtkcompile sys = System(D(x) ~ sum(p) * x + q * t, t) prob = ODEProblem(sys, [x => 1.0, p => ones(2), q => 2], (0.0, 1.0)) - obsfn = ModelingToolkit.build_explicit_observed_function( + obsfn = ModelingToolkitBase.build_explicit_observed_function( sys, [p..., q], return_inplace = true)[2] buf = zeros(3) obsfn(buf, prob.u0, prob.p, 0.0) @@ -1358,24 +1355,26 @@ end @variables x(t) @parameters p @mtkcompile sys = System(D(x) ~ p * t, t) - @test ModelingToolkit.get_index_cache(sys) !== nothing + @test ModelingToolkitBase.get_index_cache(sys) !== nothing sys2 = complete(sys; split = false) - @test ModelingToolkit.get_index_cache(sys2) === nothing + @test ModelingToolkitBase.get_index_cache(sys2) === nothing end # https://github.com/SciML/SciMLBase.jl/issues/786 -@testset "Observed variables dependent on discrete parameters" begin - @variables x(t) obs(t) - @parameters c(t) - @mtkcompile sys = System([D(x) ~ c * cos(x), obs ~ c], - t, - [x, obs], - [c]; - discrete_events = [SymbolicDiscreteCallback( - 1.0 => [c ~ Pre(c) + 1], discrete_parameters = [c])]) - prob = ODEProblem(sys, [x => 0.0, c => 1.0], (0.0, 2pi)) - sol = solve(prob, Tsit5()) - @test sol[obs] ≈ 1:7 +if @isdefined(ModelingToolkit) + @testset "Observed variables dependent on discretes" begin + @variables x(t) obs(t) + @discretes c(t) + @mtkcompile sys = System([D(x) ~ c * cos(x), obs ~ c], + t, + [x, obs, c], + []; + discrete_events = [SymbolicDiscreteCallback( + 1.0 => [c ~ Pre(c) + 1], discrete_parameters = [c])]) + prob = ODEProblem(sys, [x => 0.0, c => 1.0], (0.0, 2pi)) + sol = solve(prob, Tsit5()) + @test sol[obs] ≈ 1:7 + end end @testset "DAEProblem with array parameters" begin @@ -1423,11 +1422,11 @@ end @parameters p d @variables X(t)::Int64 eq = D(X) ~ p - d * X - @test_throws ModelingToolkit.ContinuousOperatorDiscreteArgumentError @mtkcompile osys = System( + @test_throws ModelingToolkitBase.ContinuousOperatorDiscreteArgumentError @mtkcompile osys = System( [eq], t) @variables Y(t)[1:3]::String eq = D(Y) ~ [p, p, p] - @test_throws ModelingToolkit.ContinuousOperatorDiscreteArgumentError @mtkcompile osys = System( + @test_throws ModelingToolkitBase.ContinuousOperatorDiscreteArgumentError @mtkcompile osys = System( [eq], t) @variables X(t)::Complex @@ -1451,7 +1450,7 @@ end @mtkcompile sys = System(eqs, t; constraints = cons) @test issetequal(parameters(sys), [a, e, t_c]) - @parameters g(..) h i + @parameters g(::Real, ::Real) h i cons = [g(h, i) * x(3) ~ c] @mtkcompile sys = System(eqs, t; constraints = cons) @test issetequal(parameters(sys), [g, h, i, a, e, c]) @@ -1485,11 +1484,11 @@ end @testset "`build_explicit_observed_function` with `expression = true` returns `Expr`" begin @variables x(t) @mtkcompile sys = System(D(x) ~ 2x, t) - obsfn_expr = ModelingToolkit.build_explicit_observed_function( + obsfn_expr = ModelingToolkitBase.build_explicit_observed_function( sys, 2x + 1, expression = true) @test obsfn_expr isa Expr obsfn_expr_oop, - obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( + obsfn_expr_iip = ModelingToolkitBase.build_explicit_observed_function( sys, [x + 1, x + 2, x + t], return_inplace = true, expression = true) @test obsfn_expr_oop isa Expr @test obsfn_expr_iip isa Expr @@ -1523,7 +1522,7 @@ end @testset "`@named` always wraps in `ParentScope`" begin function SysA(; name, var1) @variables x(t) - scope = ModelingToolkit.getmetadata(unwrap(var1), ModelingToolkit.SymScope, nothing) + scope = ModelingToolkitBase.getmetadata(unwrap(var1), ModelingToolkitBase.SymScope, nothing) @test scope isa ParentScope @test scope.parent isa ParentScope @test scope.parent.parent isa LocalScope @@ -1545,8 +1544,8 @@ end @testset "`full_equations` doesn't recurse infinitely" begin code = """ - using ModelingToolkit - using ModelingToolkit: t_nounits as t, D_nounits as D + using ModelingToolkitBase + using ModelingToolkitBase: t_nounits as t, D_nounits as D @variables x(t)[1:3]=[0,0,1] @variables u1(t)=0 u2(t)=0 y₁, y₂, y₃ = x @@ -1565,7 +1564,7 @@ end full_equations(ss) """ - cmd = `$(Base.julia_cmd()) --project=$(@__DIR__) -e $code` + cmd = `$(Base.julia_cmd()) --project=$(pwd()) -e $code` proc = run(cmd, stdin, stdout, stderr; wait = false) sleep(180) @test !process_running(proc) @@ -1575,13 +1574,13 @@ end @testset "`ProblemTypeCtx`" begin @variables x(t) @mtkcompile sys = System( - [D(x) ~ x], t; metadata = [ModelingToolkit.ProblemTypeCtx => "A"]) + [D(x) ~ x], t; metadata = [ModelingToolkitBase.ProblemTypeCtx => "A"]) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0)) @test prob.problem_type == "A" end @testset "`substitute` retains events and metadata" begin - @parameters p(t) = 1.0 + @discretes p(t) = 1.0 @variables x(t) = 0.0 event = [0.5] => [p ~ Pre(t)] event2 = [x ~ 0.75] => [p ~ 2 * Pre(t)] @@ -1591,16 +1590,16 @@ end eq = [ D(x) ~ p ] - @named sys = System(eq, t, [x], [p], discrete_events = [event], + @named sys = System(eq, t, [x, p], [], discrete_events = [event], continuous_events = [event2], metadata = Dict(TestMeta => "test")) @variables x2(t) = 0.0 sys2 = substitute(sys, [x => x2]) - @test length(ModelingToolkit.get_discrete_events(sys)) == 1 - @test length(ModelingToolkit.get_discrete_events(sys2)) == 1 - @test length(ModelingToolkit.get_continuous_events(sys)) == 1 - @test length(ModelingToolkit.get_continuous_events(sys2)) == 1 + @test length(ModelingToolkitBase.get_discrete_events(sys)) == 1 + @test length(ModelingToolkitBase.get_discrete_events(sys2)) == 1 + @test length(ModelingToolkitBase.get_continuous_events(sys)) == 1 + @test length(ModelingToolkitBase.get_continuous_events(sys2)) == 1 @test getmetadata(sys, TestMeta, nothing) == "test" @test getmetadata(sys2, TestMeta, nothing) == "test" end @@ -1621,7 +1620,7 @@ end # ensure `@mtkbuild` works when `@mtkcompile` is not imported module MtkbuildTestModule -import ModelingToolkit: @variables, System, t_nounits as t, D_nounits as D, @mtkbuild +import ModelingToolkitBase: @variables, System, t_nounits as t, D_nounits as D, @mtkbuild import Test: @test @variables x(t) @mtkbuild sys = System(D(x) ~ t, t) diff --git a/test/optimizationsystem.jl b/lib/ModelingToolkitBase/test/optimizationsystem.jl similarity index 90% rename from test/optimizationsystem.jl rename to lib/ModelingToolkitBase/test/optimizationsystem.jl index 8b2302b5ed..9ff74018b7 100644 --- a/test/optimizationsystem.jl +++ b/lib/ModelingToolkitBase/test/optimizationsystem.jl @@ -1,6 +1,7 @@ -using ModelingToolkit, SparseArrays, Test, Optimization, OptimizationOptimJL, +using ModelingToolkitBase, SparseArrays, Test, Optimization, OptimizationOptimJL, OptimizationMOI, Ipopt, AmplNLWriter, Ipopt_jll, SymbolicIndexingInterface, LinearAlgebra +using Symbolics: value @testset "basic" begin @variables x y @@ -26,7 +27,7 @@ using ModelingToolkit, SparseArrays, Test, Optimization, OptimizationOptimJL, generate_cost(combinedsys) generate_cost_gradient(combinedsys) generate_cost_hessian(combinedsys) - hess_sparsity = ModelingToolkit.cost_hessian_sparsity(sys1) + hess_sparsity = ModelingToolkitBase.cost_hessian_sparsity(sys1) sparse_prob = OptimizationProblem(complete(sys1), [x => 1, y => 1, a => 0.0, b => 0.0], grad = true, @@ -103,7 +104,7 @@ end sol = solve(prob, AmplNLWriter.Optimizer(Ipopt_jll.amplexe)) @test sol.objective < 1.0 @test_broken sol.u≈[0.808, -0.064] atol=1e-3 - @test_broken sol[x]^2 + sol[y]^2 ≈ 1.0 + @test sol[x]^2 + sol[y]^2 ≈ 1.0 broken=@isdefined(ModelingToolkit) end @testset "rosenbrock" begin @@ -157,7 +158,7 @@ end sys2 = OptimizationSystem(o2, [y], [], name = :sys2, constraints = c2) sys = complete(OptimizationSystem(0, [], []; name = :sys, systems = [sys1, sys2], constraints = [sys1.x + sys2.y ~ 2], checks = false)) - prob = OptimizationProblem(sys, [0.0, 0.0]) + prob = OptimizationProblem(sys, unknowns(sys) .=> [0.0, 0.0]) @test isequal(constraints(sys), vcat(sys1.x + sys2.y ~ 2, sys1.x ~ 1, sys2.y ~ 1)) @test isequal(cost(sys), (sys1.x - sys1.a)^2 + (sys2.y - 1 / 2)^2) @test isequal(unknowns(sys), [sys1.x, sys2.y]) @@ -231,7 +232,7 @@ end end @testset "non-convex problem with inequalities" begin - @variables x[1:2] [bounds = (0.0, Inf)] + @variables x[1:2] [bounds = (zeros(2), fill(Inf, 2))] @named sys = OptimizationSystem(x[1] + x[2], [x...], []; constraints = [ 1.0 ≲ x[1]^2 + x[2]^2, @@ -330,25 +331,27 @@ end @test_nowarn solve(prob, NelderMead()) end -@testset "Bounded unknowns are irreducible" begin - @variables x - @variables y [bounds = (-Inf, Inf)] - @variables z [bounds = (1.0, 2.0)] - obj = x^2 + y^2 + z^2 - cons = [y ~ 2x - z ~ 2y] - @mtkcompile sys = OptimizationSystem(obj, [x, y, z], []; constraints = cons) - @test is_variable(sys, z) - @test !is_variable(sys, y) - - @variables x[1:3] [bounds = ([-Inf, -1.0, -2.0], [Inf, 1.0, 2.0])] - obj = x[1]^2 + x[2]^2 + x[3]^2 - cons = [x[2] ~ 2x[1] + 3, x[3] ~ x[1] + x[2]] - @mtkcompile sys = OptimizationSystem(obj, [x], []; constraints = cons) - @test length(unknowns(sys)) == 2 - @test !is_variable(sys, x[1]) - @test is_variable(sys, x[2]) - @test is_variable(sys, x[3]) +if @isdefined(ModelingToolkit) + @testset "Bounded unknowns are irreducible" begin + @variables x + @variables y [bounds = (-Inf, Inf)] + @variables z [bounds = (1.0, 2.0)] + obj = x^2 + y^2 + z^2 + cons = [y ~ 2x + z ~ 2y] + @mtkcompile sys = OptimizationSystem(obj, [x, y, z], []; constraints = cons) + @test is_variable(sys, z) + @test !is_variable(sys, y) + + @variables x[1:3] [bounds = ([-Inf, -1.0, -2.0], [Inf, 1.0, 2.0])] + obj = x[1]^2 + x[2]^2 + x[3]^2 + cons = [x[2] ~ 2x[1] + 3, x[3] ~ x[1] + x[2]] + @mtkcompile sys = OptimizationSystem(obj, [x], []; constraints = cons) + @test length(unknowns(sys)) == 2 + @test !is_variable(sys, x[1]) + @test is_variable(sys, x[2]) + @test is_variable(sys, x[3]) + end end @testset "Constraints work with nonnumeric parameters" begin @@ -399,7 +402,7 @@ end prob = OptimizationProblem(sys, [x => [42.0, 12.37]]; hess = true, sparse = true) symbolic_hess = Symbolics.hessian(cost(sys), unknowns(sys)) - symbolic_hess_value = Symbolics.fast_substitute(symbolic_hess, Dict(x[1] => prob[x[1]], x[2] => prob[x[2]])) + symbolic_hess_value = value.(substitute(symbolic_hess, Dict(x[1] => prob[x[1]], x[2] => prob[x[2]]); fold = Val(true))) oop_hess = prob.f.hess(prob.u0, prob.p) @test oop_hess ≈ symbolic_hess_value diff --git a/test/parameter_dependencies.jl b/lib/ModelingToolkitBase/test/parameter_bindings.jl similarity index 53% rename from test/parameter_dependencies.jl rename to lib/ModelingToolkitBase/test/parameter_bindings.jl index c39bb274e6..bcfb895649 100644 --- a/test/parameter_dependencies.jl +++ b/lib/ModelingToolkitBase/test/parameter_bindings.jl @@ -1,6 +1,6 @@ -using ModelingToolkit +using ModelingToolkitBase using Test -using ModelingToolkit: t_nounits as t, D_nounits as D, SymbolicDiscreteCallback, +using ModelingToolkitBase: t_nounits as t, D_nounits as D, SymbolicDiscreteCallback, SymbolicContinuousCallback using OrdinaryDiffEq using StochasticDiffEq @@ -9,9 +9,11 @@ using StableRNGs using SciMLStructures: canonicalize, Tunable, replace, replace! using SymbolicIndexingInterface using NonlinearSolve +import DiffEqNoiseProcess @testset "ODESystem with callbacks" begin - @parameters p1(t)=1.0 p2 + @discretes p1(t) = 1.0 + @parameters p2 = 2p1 @variables x(t) cb1 = SymbolicContinuousCallback([x ~ 2.0] => [p1 ~ 2.0], discrete_parameters = [p1]) # triggers at t=-2+√6 function affect1!(mod, obs, ctx, integ) @@ -21,13 +23,13 @@ using NonlinearSolve cb3 = SymbolicDiscreteCallback([1.0] => [p1 ~ 5.0], discrete_parameters = [p1]) @mtkcompile sys = System( - [D(x) ~ p1 * t + p2, p2 ~ 2p1], + [D(x) ~ p1 * t + p2], t; continuous_events = [cb1, cb2], discrete_events = [cb3] ) @test !(p2 in Set(parameters(sys))) - @test p2 in Set(full_parameters(sys)) + @test p2 in Set(bound_parameters(sys)) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.5), jac = true) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 @@ -49,12 +51,12 @@ using NonlinearSolve @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] +@testset "vector parameter bindings" begin + @parameters p1[1:2]=[1.0, 2.0] p2[1:2]=2p1 @variables x(t) = 0 @named sys = System( - [D(x) ~ sum(p1) * t + sum(p2), p2 ~ 2p1], + [D(x) ~ sum(p1) * t + sum(p2)], t ) prob = ODEProblem(complete(sys), [], (0.0, 1.0)) @@ -68,31 +70,31 @@ end end @testset "extend" begin - @parameters p1=1.0 p2=1.0 + @parameters p1=1.0 p2 @variables x(t) = 0 @mtkcompile sys1 = System( [D(x) ~ p1 * t + p2], t ) - @named sys2 = System( - [p2 ~ 2p1], - t + @named sys2 = System(Equation[], + t, [], []; + bindings = [p2 => 2p1] ) sys = complete(extend(sys2, sys1)) @test !(p2 in Set(parameters(sys))) - @test p2 in Set(full_parameters(sys)) + @test p2 in Set(bound_parameters(sys)) prob = ODEProblem(complete(sys), nothing, (0.0, 1.0)) get_dep = getu(prob, 2p2) @test get_dep(prob) == 4 end -@testset "getu with parameter deps" begin - @parameters p1=1.0 p2=1.0 +@testset "getu with parameter bindings" begin + @parameters p1=1.0 p2=2p1 @variables x(t) = 0 @named sys = System( - [D(x) ~ p1 * t + p2, p2 ~ 2p1], + [D(x) ~ p1 * t + p2], t ) prob = ODEProblem(complete(sys), nothing, (0.0, 1.0)) @@ -100,12 +102,12 @@ end @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] +@testset "getu with vector parameter bindings" begin + @parameters p1[1:2]=[1.0, 2.0] p2[1:2]=2p1 @variables x(t) = 0 @named sys = System( - [D(x) ~ sum(p1) * t + sum(p2), p2 ~ 2p1], + [D(x) ~ sum(p1) * t + sum(p2)], t ) prob = ODEProblem(complete(sys), [], (0.0, 1.0)) @@ -113,17 +115,17 @@ end @test get_dep(prob) == [2.0, 4.0] end -@testset "composing systems with parameter deps" begin - @parameters p1=1.0 p2=2.0 +@testset "composing systems with parameter bindings" begin + @parameters p1=1.0 p2 @variables x(t) = 0 @named sys1 = System( [D(x) ~ p1 * t + p2], - t + t; initial_conditions = [p2 => 1.0] ) @named sys2 = System( - [D(x) ~ p1 * t - p2, p2 ~ 2p1], - t + [D(x) ~ p1 * t - p2], + t, bindings = [p2 => 2p1] ) sys = complete(System(Equation[], t, systems = [sys1, sys2], name = :sys)) @@ -143,12 +145,12 @@ end new_prob = remake(prob, p = [sys2.p1 => 1.5]) - @test !isempty(ModelingToolkit.parameter_dependencies(sys)) + @test !isempty(ModelingToolkitBase.bindings(sys)) @test new_prob.ps[sys2.p1] == 1.5 @test new_prob.ps[sys2.p2] == 3.0 end -@testset "parameter dependencies across model hierarchy" begin +@testset "parameter bindings across model hierarchy" begin sys2 = let name = :sys2 @parameters p2 @variables x(t) = 1.0 @@ -157,14 +159,9 @@ end end @parameters p1 = 1.0 - parameter_dependencies = [] sys1 = System( - [sys2.p2 ~ p1 * 2.0], t, [], [p1]; name = :sys1, systems = [sys2]) - - # ensure that parameter_dependencies is type stable - # (https://github.com/SciML/ModelingToolkit.jl/pull/2978) - sys = complete(sys1) - @inferred ModelingToolkit.parameter_dependencies(sys) + Equation[], t, [], [p1]; + bindings = [sys2.p2 => 2p1], name = :sys1, systems = [sys2]) sys = mtkcompile(sys1) @@ -184,23 +181,23 @@ end sys = mtkcompile(pendulum_sys) new_tunables = [L, b] - old_tunables = copy(ModelingToolkit.tunable_parameters(sys, ModelingToolkit.parameters(sys))) - sys2 = ModelingToolkit.subset_tunables(sys, new_tunables) - sys2_tunables = ModelingToolkit.tunable_parameters(sys2, ModelingToolkit.parameters(sys2)) + old_tunables = copy(ModelingToolkitBase.tunable_parameters(sys, ModelingToolkitBase.parameters(sys))) + sys2 = ModelingToolkitBase.subset_tunables(sys, new_tunables) + sys2_tunables = ModelingToolkitBase.tunable_parameters(sys2, ModelingToolkitBase.parameters(sys2)) @test length(sys2_tunables) == 2 @test isempty(setdiff(sys2_tunables, new_tunables)) - @test_throws ArgumentError ModelingToolkit.subset_tunables(sys, [errp]) - @test_throws ArgumentError ModelingToolkit.subset_tunables(sys, [θ, L]) - sys3 = ModelingToolkit.subset_tunables(sys, []) - sys3_tunables = ModelingToolkit.tunable_parameters(sys3, ModelingToolkit.parameters(sys3)) + @test_throws ArgumentError ModelingToolkitBase.subset_tunables(sys, [errp]) + @test_throws ArgumentError ModelingToolkitBase.subset_tunables(sys, [θ, L]) + sys3 = ModelingToolkitBase.subset_tunables(sys, []) + sys3_tunables = ModelingToolkitBase.tunable_parameters(sys3, ModelingToolkitBase.parameters(sys3)) @test length(sys3_tunables) == 0 sys_incomplete = pendulum_sys - @test_throws ArgumentError ModelingToolkit.subset_tunables(sys_incomplete, new_tunables) + @test_throws ArgumentError ModelingToolkitBase.subset_tunables(sys_incomplete, new_tunables) sys_nonsplit = mtkcompile(pendulum_sys; split = false) - @test_throws ArgumentError ModelingToolkit.subset_tunables(sys_nonsplit, new_tunables) + @test_throws ArgumentError ModelingToolkitBase.subset_tunables(sys_nonsplit, new_tunables) - @test length(ModelingToolkit.tunable_parameters(sys, ModelingToolkit.parameters(sys))) == length(old_tunables) + @test length(ModelingToolkitBase.tunable_parameters(sys, ModelingToolkitBase.parameters(sys))) == length(old_tunables) end struct CallableFoo @@ -215,8 +212,8 @@ end @variables y(t) = 1 @parameters p=2 (i::CallableFoo)(..) - eqs = [D(y) ~ i(t) + p, i ~ CallableFoo(p)] - @named model = System(eqs, t, [y], [p, i]) + eqs = [D(y) ~ i(t) + p] + @named model = System(eqs, t, [y], [p, i]; bindings = [i => CallableFoo(p)]) sys = mtkcompile(model) prob = ODEProblem(sys, [], (0.0, 1.0)) @@ -225,57 +222,8 @@ end @test SciMLBase.successful_retcode(sol) end -@testset "Clock system" begin - dt = 0.1 - @variables x(t) y(t) u(t) yd(t) ud(t) r(t) z(t) - @parameters kp(t) kq - d = Clock(dt) - k = ShiftIndex(d) - - eqs = [yd ~ Sample(dt)(y) - ud ~ kp * (r - yd) + kq * z - r ~ 1.0 - u ~ Hold(ud) - D(x) ~ -x + u - y ~ x - z(k) ~ z(k - 2) + yd(k - 2) - kq ~ 2kp] - @test_throws ModelingToolkit.HybridSystemNotSupportedException @mtkcompile sys = System( - eqs, t) - - @test_skip begin - Tf = 1.0 - prob = ODEProblem(sys, - [x => 0.0, y => 0.0, kp => 1.0, z(k - 1) => 3.0, - yd(k - 1) => 0.0, z(k - 2) => 4.0, yd(k - 2) => 2.0], - (0.0, Tf)) - @test_nowarn solve(prob, Tsit5()) - - @mtkcompile sys = System(eqs, t; - discrete_events = [SymbolicDiscreteCallback( - [0.5] => [kp ~ 2.0], discrete_parameters = [kp])]) - prob = ODEProblem(sys, - [x => 0.0, y => 0.0, kp => 1.0, z(k - 1) => 3.0, - yd(k - 1) => 0.0, z(k - 2) => 4.0, yd(k - 2) => 2.0], - (0.0, Tf)) - @test prob.ps[kp] == 1.0 - @test prob.ps[kq] == 2.0 - @test_nowarn solve(prob, Tsit5()) - prob = ODEProblem(sys, - [x => 0.0, y => 0.0, kp => 1.0, z(k - 1) => 3.0, - yd(k - 1) => 0.0, z(k - 2) => 4.0, yd(k - 2) => 2.0], - (0.0, Tf)) - integ = init(prob, Tsit5()) - @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 - @parameters σ(t) ρ β + @parameters σ ρ β @variables x(t) y(t) z(t) eqs = [D(x) ~ σ * (y - x), @@ -287,34 +235,20 @@ end 0.1 * z] @named sys = System(eqs, t) - @named sdesys = SDESystem(sys, noiseeqs; parameter_dependencies = [ρ ~ 2σ]) + @named sdesys = SDESystem(sys, noiseeqs; bindings = [ρ => 2σ]) sdesys = complete(sdesys) @test !(ρ in Set(parameters(sdesys))) - @test ρ in Set(full_parameters(sdesys)) + @test ρ in Set(bound_parameters(sdesys)) prob = SDEProblem( sdesys, [x => 1.0, y => 0.0, z => 0.0, σ => 10.0, β => 2.33], (0.0, 100.0)) @test prob.ps[ρ] == 2prob.ps[σ] @test_nowarn solve(prob, SRIW1()) - - @named sys = System(eqs, t) - @named sdesys = SDESystem(sys, noiseeqs; parameter_dependencies = [ρ ~ 2σ], - discrete_events = [SymbolicDiscreteCallback( - [10.0] => [σ ~ 15.0], discrete_parameters = [σ])]) - sdesys = complete(sdesys) - prob = SDEProblem( - sdesys, [x => 1.0, y => 0.0, z => 0.0, σ => 10.0, β => 2.33], (0.0, 100.0)) - integ = init(prob, SRIW1()) - @test integ.ps[σ] == 10.0 - @test integ.ps[ρ] == 20.0 - step!(integ, 11.0) - @test integ.ps[σ] == 15.0 - @test integ.ps[ρ] == 30.0 end @testset "JumpSystem" begin rng = StableRNG(12345) - @parameters β γ(t) + @parameters β γ @constants h = 1 @variables S(t) I(t) R(t) rate₁ = β * S * I * h @@ -324,12 +258,11 @@ end j₁ = ConstantRateJump(rate₁, affect₁) j₃ = ConstantRateJump(rate₃, affect₃) @named js2 = JumpSystem( - [j₃, β ~ 0.01γ], t, [S, I, R], [β, γ, h]) + [j₃], t, [S, I, R], [β, γ, h]; bindings = [β => 0.01γ]) @test issetequal(parameters(js2), [β, γ, h]) - @test Set(full_parameters(js2)) == Set([γ, β, h]) js2 = complete(js2) @test issetequal(parameters(js2), [γ, h]) - @test Set(full_parameters(js2)) == Set([γ, β, h]) + @test Set(bound_parameters(js2)) == Set([β]) tspan = (0.0, 250.0) u₀map = [S => 999, I => 1, R => 0] parammap = [γ => 0.01] @@ -338,29 +271,15 @@ end @test jprob.ps[γ] == 0.01 @test jprob.ps[β] == 0.0001 @test_nowarn solve(jprob, SSAStepper()) - - @named js2 = JumpSystem( - [j₁, j₃, β ~ 0.01γ], t, [S, I, R], [β, γ, h]; - discrete_events = [SymbolicDiscreteCallback( - [10.0] => [γ ~ 0.02], discrete_parameters = [γ])]) - js2 = complete(js2) - jprob = JumpProblem(js2, [u₀map; parammap], tspan; aggregator = Direct(), - save_positions = (false, false), rng = rng) - integ = init(jprob, SSAStepper()) - @test integ.ps[γ] == 0.01 - @test integ.ps[β] == 0.0001 - step!(integ, 11.0) - @test integ.ps[γ] == 0.02 - @test integ.ps[β] == 0.0002 end @testset "NonlinearSystem" begin - @parameters p1=1.0 p2=1.0 + @parameters p1=1.0 p2 @variables x(t) - eqs = [0 ~ p1 * x * exp(x) + p2, p2 ~ 2p1] - @mtkcompile sys = System(eqs; parameter_dependencies = [p2 ~ 2p1]) + eqs = [0 ~ p1 * x * exp(x) + p2] + @mtkcompile sys = System(eqs; bindings = [p2 => 2p1]) @test isequal(only(parameters(sys)), p1) - @test Set(full_parameters(sys)) == Set([p1, p2, Initial(p2), Initial(x)]) + @test Set(bound_parameters(sys)) == Set([p2]) prob = NonlinearProblem(sys, [x => 1.0]) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 @@ -371,7 +290,7 @@ end end @testset "SciMLStructures interface" begin - @parameters p1=1.0 p2=1.0 + @parameters p1=1.0 p2 @variables x(t) cb1 = [x ~ 2.0] => [p1 ~ 2.0] # triggers at t=-2+√6 function affect1!(integ, u, p, ctx) @@ -381,8 +300,8 @@ end cb3 = [1.0] => [p1 ~ 5.0] @mtkcompile sys = System( - [D(x) ~ p1 * t + p2, p2 ~ 2p1], - t + [D(x) ~ p1 * t + p2], + t; bindings = [p2 => 2p1] ) prob = ODEProblem(sys, [x => 1.0, p1 => 1.0], (0.0, 1.5), jac = true) prob.ps[p1] = 3.0 @@ -407,24 +326,12 @@ end @test getp(sys, p2)(ps2) == 4.0 end -@testset "Discovery of parameters from dependencies" begin - @parameters p1 p2 - @variables x(t) y(t) - @named sys = System([D(x) ~ y + p2, p2 ~ 2p1], t) - @test is_parameter(sys, p1) - @named sys = System([x * y^2 ~ y + p2, p2 ~ 2p1]) - @test is_parameter(sys, p1) - k = ShiftIndex(t) - @named sys = System( - [x(k - 1) ~ x(k) + y(k) + p2, p2 ~ 2p1], t) - @test is_parameter(sys, p1) -end - -@testset "Scalarized array as RHS of parameter dependency" begin - @parameters p[1:2] p1 p2 +@testset "Scalarized array as RHS of parameter binding" begin + @parameters p[1:2] p1 = p[1] p2 = p[2] @variables x(t) - @named sys = System([D(x) ~ x, p1 ~ p[1], p2 ~ p[2]], t) - @test any(isequal(p), ModelingToolkit.get_ps(sys)) + @named sys = System([D(x) ~ x], t, [x], [p, p1, p2]) + sys = complete(sys) + @test any(isequal(p), ModelingToolkitBase.get_ps(sys)) sys = mtkcompile(sys) - @test length(ModelingToolkit.parameter_dependencies(sys)) == 2 + @test length(ModelingToolkitBase.bound_parameters(sys)) == 2 end diff --git a/test/pdesystem.jl b/lib/ModelingToolkitBase/test/pdesystem.jl similarity index 86% rename from test/pdesystem.jl rename to lib/ModelingToolkitBase/test/pdesystem.jl index 531816cbbb..fe44d4157d 100644 --- a/test/pdesystem.jl +++ b/lib/ModelingToolkitBase/test/pdesystem.jl @@ -1,5 +1,5 @@ -using ModelingToolkit, DiffEqBase, LinearAlgebra, Test -using ModelingToolkit: t_nounits as t, D_nounits as Dt +using ModelingToolkitBase, DiffEqBase, LinearAlgebra, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as Dt # Define some variables @parameters x diff --git a/test/precompile_test.jl b/lib/ModelingToolkitBase/test/precompile_test.jl similarity index 85% rename from test/precompile_test.jl rename to lib/ModelingToolkitBase/test/precompile_test.jl index 6fdc1b5cbf..a7be38b959 100644 --- a/test/precompile_test.jl +++ b/lib/ModelingToolkitBase/test/precompile_test.jl @@ -1,5 +1,5 @@ using Test -using ModelingToolkit +using ModelingToolkitBase using OrdinaryDiffEqDefault using Distributed @@ -10,16 +10,16 @@ using Distributed using ODEPrecompileTest u = collect(1:3) -p = ModelingToolkit.MTKParameters(ODEPrecompileTest.f_noeval_good.sys, +p = ModelingToolkitBase.MTKParameters(ODEPrecompileTest.f_noeval_good.sys, [:σ, :ρ, :β] .=> collect(4:6)) -# These cases do not work, because they get defined in the ModelingToolkit's RGF cache. -@test parentmodule(typeof(ODEPrecompileTest.f_bad.f.f_iip).parameters[2]) == ModelingToolkit -@test parentmodule(typeof(ODEPrecompileTest.f_bad.f.f_oop).parameters[2]) == ModelingToolkit +# These cases do not work, because they get defined in the ModelingToolkitBase's RGF cache. +@test parentmodule(typeof(ODEPrecompileTest.f_bad.f.f_iip).parameters[2]) == ModelingToolkitBase +@test parentmodule(typeof(ODEPrecompileTest.f_bad.f.f_oop).parameters[2]) == ModelingToolkitBase @test parentmodule(typeof(ODEPrecompileTest.f_noeval_bad.f.f_iip).parameters[2]) == - ModelingToolkit + ModelingToolkitBase @test parentmodule(typeof(ODEPrecompileTest.f_noeval_bad.f.f_oop).parameters[2]) == - ModelingToolkit + ModelingToolkitBase @test_skip begin @test_throws KeyError ODEPrecompileTest.f_bad(u, p, 0.1) @test_throws KeyError ODEPrecompileTest.f_noeval_bad(u, p, 0.1) diff --git a/test/precompile_test/ODEPrecompileTest.jl b/lib/ModelingToolkitBase/test/precompile_test/ODEPrecompileTest.jl similarity index 98% rename from test/precompile_test/ODEPrecompileTest.jl rename to lib/ModelingToolkitBase/test/precompile_test/ODEPrecompileTest.jl index c77c897655..85cc9217a6 100644 --- a/test/precompile_test/ODEPrecompileTest.jl +++ b/lib/ModelingToolkitBase/test/precompile_test/ODEPrecompileTest.jl @@ -1,5 +1,5 @@ module ODEPrecompileTest -using ModelingToolkit +using ModelingToolkitBase function system(; kwargs...) # Define some variables diff --git a/test/print_tree.jl b/lib/ModelingToolkitBase/test/print_tree.jl similarity index 83% rename from test/print_tree.jl rename to lib/ModelingToolkitBase/test/print_tree.jl index 351e95f612..c510a473b6 100644 --- a/test/print_tree.jl +++ b/lib/ModelingToolkitBase/test/print_tree.jl @@ -1,4 +1,5 @@ -using ModelingToolkit, AbstractTrees, Test +using ModelingToolkitBase, AbstractTrees, Test +using ModelingToolkitBase: t_nounits as t include("common/rc_model.jl") diff --git a/lib/ModelingToolkitBase/test/runtests.jl b/lib/ModelingToolkitBase/test/runtests.jl new file mode 100644 index 0000000000..35251c9692 --- /dev/null +++ b/lib/ModelingToolkitBase/test/runtests.jl @@ -0,0 +1,116 @@ +using SafeTestsets, Pkg, Test +# https://github.com/JuliaLang/julia/issues/54664 +import REPL + +const SciCompDSLPath = joinpath(dirname(dirname(@__DIR__)), "SciCompDSL") +const SciCompDSLPkgSpec = PackageSpec(; path = SciCompDSLPath) +Pkg.develop(SciCompDSLPkgSpec) + +const GROUP = get(ENV, "GROUP", "All") + +function activate_extensions_env() + Pkg.activate("extensions") + Pkg.develop([PackageSpec(path = dirname(@__DIR__)), SciCompDSLPkgSpec]) + Pkg.instantiate() +end + +function activate_downstream_env() + Pkg.activate("downstream") + Pkg.develop([PackageSpec(path = dirname(@__DIR__)), SciCompDSLPkgSpec]) + Pkg.instantiate() +end + +@time begin + if GROUP == "All" || GROUP == "InterfaceI" + @testset "InterfaceI" begin + @safetestset "AbstractSystem Test" include("abstractsystem.jl") + @safetestset "Variable Scope Tests" include("variable_scope.jl") + @safetestset "Parsing Test" include("variable_parsing.jl") + @safetestset "System Linearity Test" include("linearity.jl") + @safetestset "Variable binding semantics" include("binding_semantics.jl") + @safetestset "Input Output Test" include("input_output_handling.jl") + @safetestset "ODESystem Test" include("odesystem.jl") + @safetestset "Dynamic Quantities Test" include("dq_units.jl") + @safetestset "Mass Matrix Test" include("mass_matrix.jl") + @safetestset "Split Parameters Test" include("split_parameters.jl") + @safetestset "StaticArrays Test" include("static_arrays.jl") + @safetestset "Components Test" include("components.jl") + @safetestset "Error Handling" include("error_handling.jl") + @safetestset "Basic transformations" include("basic_transformations.jl") + @safetestset "Change of variables" include("changeofvariables.jl") + @safetestset "Symbolic Event Test" include("symbolic_events.jl") + @safetestset "Stream Connect Test" include("stream_connectors.jl") + @safetestset "Domain Connect Test" include("domain_connectors.jl") + @safetestset "Dependency Graph Test" include("dep_graphs.jl") + @safetestset "Function Registration Test" include("function_registration.jl") + @safetestset "Precompiled Modules Test" include("precompile_test.jl") + @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") + @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") + @safetestset "Modelingtoolkitize Test" include("modelingtoolkitize.jl") + @safetestset "Constants Test" include("constants.jl") + @safetestset "Parameter Bindings Test" include("parameter_bindings.jl") + @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") + @safetestset "System Accessor Functions Test" include("accessor_functions.jl") + @safetestset "Equations with complex values" include("complex.jl") + end + end + + if GROUP == "All" || GROUP == "Initialization" + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Initial Values Test" include("initial_values.jl") + end + + if GROUP == "All" || GROUP == "InterfaceII" + @testset "InterfaceII" begin + @safetestset "Code Generation Test" include("code_generation.jl") + @safetestset "IndexCache Test" include("index_cache.jl") + @safetestset "Variable Utils Test" include("variable_utils.jl") + @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") + @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") + @safetestset "Discrete System" include("discrete_system.jl") + @safetestset "Implicit Discrete System" include("implicit_discrete_system.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 "PDE Construction Test" include("pdesystem.jl") + @safetestset "JumpSystem Test" include("jumpsystem.jl") + @safetestset "Optimal Control + Constraints Tests" include("bvproblem.jl") + @safetestset "print_tree" include("print_tree.jl") + @safetestset "Analysis Points Test" include("analysis_points.jl") + @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") + @safetestset "Debugging Test" include("debugging.jl") + @safetestset "Namespacing test" include("namespacing.jl") + @safetestset "LinearProblem Tests" include("linearproblem.jl") + end + end + + if GROUP == "All" || GROUP == "SymbolicIndexingInterface" + @safetestset "SymbolicIndexingInterface test" include("symbolic_indexing_interface.jl") + @safetestset "SciML Problem Input Test" include("sciml_problem_inputs.jl") + @safetestset "MTKParameters Test" include("mtkparameters.jl") + end + + if GROUP == "All" || GROUP == "Extended" + @safetestset "Test Big System Usage" include("bigsystem.jl") + println("C compilation test requires gcc available in the path!") + @safetestset "C Compilation Test" include("ccompile.jl") + @testset "Distributed Test" include("distributed.jl") + @testset "Serialization" include("serialization.jl") + end + + if GROUP == "All" || GROUP == "RegressionI" + @safetestset "Latexify recipes Test" include("latexify.jl") + end + + if GROUP == "All" || GROUP == "Extensions" + activate_extensions_env() + @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") + @safetestset "LabelledArrays Test" include("extensions/labelledarrays.jl") + @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") + @safetestset "InfiniteOpt Extension Test" include("extensions/test_infiniteopt.jl") + # @safetestset "Auto Differentiation Test" include("extensions/ad.jl") + @safetestset "Dynamic Optimization Collocation Solvers" include("extensions/dynamic_optimization.jl") + end +end diff --git a/test/sciml_problem_inputs.jl b/lib/ModelingToolkitBase/test/sciml_problem_inputs.jl similarity index 93% rename from test/sciml_problem_inputs.jl rename to lib/ModelingToolkitBase/test/sciml_problem_inputs.jl index a91a8d8c7c..66e2d0eafd 100644 --- a/test/sciml_problem_inputs.jl +++ b/lib/ModelingToolkitBase/test/sciml_problem_inputs.jl @@ -1,9 +1,10 @@ ### Prepares Tests ### # Fetch packages -using ModelingToolkit, JumpProcesses, NonlinearSolve, OrdinaryDiffEq, StaticArrays, - SteadyStateDiffEq, StochasticDiffEq, SciMLBase, Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, JumpProcesses, NonlinearSolve, OrdinaryDiffEq, StaticArrays, + SteadyStateDiffEq, StochasticDiffEq, SciMLBase, Test, SymbolicUtils +import DiffEqNoiseProcess +using ModelingToolkitBase: t_nounits as t, D_nounits as D # Sets rnd number. using StableRNGs @@ -29,7 +30,7 @@ begin ] noise_eqs = fill(0.01, 3, 6) jumps = [ - MassActionJump(kp, Pair{Symbolics.BasicSymbolic{Real}, Int64}[], [X => 1]), + MassActionJump(kp, Pair{Symbolics.SymbolicT, Int64}[], [X => 1]), MassActionJump(kd, [X => 1], [X => -1]), MassActionJump(k1, [X => 1], [X => -1, Y => 1]), MassActionJump(k2, [Y => 1], [X => 1, Y => -1]), @@ -167,20 +168,18 @@ end @test_deprecated DiscreteSystem([x ~ x(k - 1) + x(k - 2)], t; name = :a) @mtkcompile discsys = System([x ~ x(k - 1) * p], t) @test_deprecated ImplicitDiscreteSystem([x ~ x(k - 1) + x(k - 2) * p * x], t; name = :a) - @mtkcompile idiscsys = System([x ~ x(k - 1) * p * x], t) + @mtkcompile idiscsys = System([x ~ x(k - 1) * p * x], t; guesses = [x(k-1) => 1.0]) @mtkcompile optsys = OptimizationSystem(x^2 + p) u0s = [ Dict(x => 1.0), [x => 1.0], - [1.0], [], nothing ] ps = [ Dict(p => 1.0), [p => 1.0], - [1.0], [], nothing, SciMLBase.NullParameters() @@ -227,9 +226,11 @@ end (nlsys, SCCNonlinearProblem{true}), (optsys, OptimizationProblem), (optsys, OptimizationProblem{true}) ] - @testset "$(typeof(u0)) - $(typeof(p))" for u0 in u0s, p in ps + if !(ctor <: SCCNonlinearProblem) || @isdefined(ModelingToolkit) + @testset "$(typeof(u0)) - $(typeof(p))" for u0 in u0s, p in ps - @test_warn ["deprecated"] ctor(sys, u0, p) + @test_warn ["deprecated"] ctor(sys, u0, p) + end end end end diff --git a/test/sdesystem.jl b/lib/ModelingToolkitBase/test/sdesystem.jl similarity index 91% rename from test/sdesystem.jl rename to lib/ModelingToolkitBase/test/sdesystem.jl index 0df0b2ce31..bea58dfea6 100644 --- a/test/sdesystem.jl +++ b/lib/ModelingToolkitBase/test/sdesystem.jl @@ -1,11 +1,14 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra +using ModelingToolkitBase, StaticArrays, LinearAlgebra using StochasticDiffEq, OrdinaryDiffEq, SparseArrays using DiffEqNoiseProcess: NoiseWrapper using Random, Test using Setfield using Statistics # imported as tt because `t` is used extensively below -using ModelingToolkit: t_nounits as tt, D_nounits as D, MTKParameters +using ModelingToolkitBase: t_nounits as tt, D_nounits as D, MTKParameters +using Symbolics: value +import SymbolicUtils as SU +import DiffEqNoiseProcess # Define some variables @parameters σ ρ β @@ -467,7 +470,7 @@ fdif!(du, u0, p, t) 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 ModelingToolkit.NonUniqueSubsystemsError SDESystem( + @test_throws ModelingToolkitBase.NonUniqueSubsystemsError SDESystem( [sys2.y ~ sys1.z], [sys2.y], t, [], [], systems = [sys1, sys2], name = :foo) end @@ -483,7 +486,7 @@ RHS2 = RHS @test isequal(RHS, RHS2) # issue #1644 -using ModelingToolkit: rename +using ModelingToolkitBase: rename eqs = [D(x) ~ x] noiseeqs = [0.1 * x] @named de = SDESystem(eqs, noiseeqs, tt, [x], []) @@ -569,7 +572,7 @@ end ## Variance reduction method u = x - demod = complete(ModelingToolkit.Girsanov_transform(de, u; θ0 = 0.1)) + demod = complete(ModelingToolkitBase.Girsanov_transform(de, u; θ0 = 0.1)) probmod = SDEProblem(demod, [u0map; parammap], (0.0, 1.0)) @@ -614,7 +617,7 @@ diffusion_eqs = [s*x 0 sys2 = SDESystem(drift_eqs, diffusion_eqs, tt, sts, ps, name = :sys1) sys2 = complete(sys2) -@test issetequal(ModelingToolkit.get_noise_eqs(sys1), ModelingToolkit.get_noise_eqs(sys2)) +@test issetequal(ModelingToolkitBase.get_noise_eqs(sys1), ModelingToolkitBase.get_noise_eqs(sys2)) prob = SDEProblem(sys1, [sts .=> [1.0, 0.0, 0.0]; ps .=> [10.0, 26.0]], (0.0, 100.0)) @@ -627,7 +630,7 @@ solve(prob, LambaEulerHeun(), seed = 1) @variables X(t) eqs = [D(X) ~ p - d * X] noise_eqs = [sqrt(p), -sqrt(d * X)] -@test_throws ModelingToolkit.IllFormedNoiseEquationsError SDESystem( +@test_throws ModelingToolkitBase.IllFormedNoiseEquationsError SDESystem( eqs, noise_eqs, t, [X], [p, d]; name = :ssys) noise_eqs = reshape([sqrt(p), -sqrt(d * X)], 1, 2) @@ -752,7 +755,7 @@ end @test_throws ErrorException solve(prob, SOSRI()).retcode==ReturnCode.Success # ImplicitEM does work for non-diagonal noise @test solve(prob, ImplicitEM()).retcode == ReturnCode.Success - @test size(ModelingToolkit.get_noise_eqs(de)) == (3, 6) + @test size(ModelingToolkitBase.get_noise_eqs(de)) == (3, 6) end @testset "Diagonal noise, less brownians than equations" begin @@ -788,32 +791,34 @@ end @test_nowarn solve(prob, ImplicitEM()) end -@testset "Issue#3212: Noise dependent on observed" begin - sts = @variables begin - x(t) = 1.0 - input(t) - [input = true] - end - ps = @parameters a = 2 - browns = @brownians η +if @isdefined(ModelingToolkit) + @testset "Issue#3212: Noise dependent on observed" begin + sts = @variables begin + x(t) = 1.0 + input(t) + [input = true] + end + ps = @parameters a = 2 + browns = @brownians η - eqs = [D(x) ~ -a * x + (input + 1) * η - input ~ 0.0] + eqs = [D(x) ~ -a * x + (input + 1) * η + input ~ 0.0] - sys = System(eqs, t, sts, ps, browns; name = :name) - sys = mtkcompile(sys) - @test ModelingToolkit.get_noise_eqs(sys) ≈ [1.0] - prob = SDEProblem(sys, [], (0.0, 1.0)) - @test_nowarn solve(prob, RKMil()) -end + sys = System(eqs, t, sts, ps, browns; name = :name) + sys = mtkcompile(sys) + @test value.(ModelingToolkitBase.get_noise_eqs(sys)) ≈ [1.0] + prob = SDEProblem(sys, [], (0.0, 1.0)) + @test_nowarn solve(prob, RKMil()) + end -@testset "Observed variables retained after `mtkcompile`" begin - @variables x(t) y(t) z(t) - @brownians a - @mtkcompile sys = System([D(x) ~ x + a, D(y) ~ y + a, z ~ x + y], t) - @test length(observed(sys)) == 1 - prob = SDEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) - @test prob[z] ≈ 2.0 + @testset "Observed variables retained after `mtkcompile`" begin + @variables x(t) y(t) z(t) + @brownians a + @mtkcompile sys = System([D(x) ~ x + a, D(y) ~ y + a, z ~ x + y], t) + @test length(observed(sys)) == 1 + prob = SDEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test prob[z] ≈ 2.0 + end end @testset "SDESystem to System" begin @@ -822,13 +827,13 @@ end @named sys = SDESystem([D(x) ~ x, D(y) ~ y, z ~ x + y], [x, y, 3], t, [x, y, z], [], is_scalar_noise = true) odesys = noise_to_brownians(sys) - vs = ModelingToolkit.vars(equations(odesys)) + vs = SU.search_variables(equations(odesys)) nbrownian = count( - v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + v -> ModelingToolkitBase.getvariabletype(v) == ModelingToolkitBase.BROWNIAN, vs) @test length(brownians(odesys)) == 3 @test nbrownian == 3 for eq in equations(odesys) - ModelingToolkit.isdiffeq(eq) || continue + ModelingToolkitBase.isdiffeq(eq) || continue @test length(arguments(eq.rhs)) == 4 end end @@ -837,12 +842,12 @@ end @named sys = SDESystem([D(x) ~ x, D(y) ~ y, z ~ x + y], [x; y; 0;;], t, [x, y, z], []; is_scalar_noise = false) odesys = noise_to_brownians(sys) - vs = ModelingToolkit.vars(equations(odesys)) + vs = SU.search_variables(equations(odesys)) nbrownian = count( - v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + v -> ModelingToolkitBase.getvariabletype(v) == ModelingToolkitBase.BROWNIAN, vs) @test nbrownian == 1 for eq in equations(odesys) - ModelingToolkit.isdiffeq(eq) || continue + ModelingToolkitBase.isdiffeq(eq) || continue @test length(arguments(eq.rhs)) == 2 end end @@ -853,9 +858,9 @@ end z+1 x+1 y+1] @named sys = SDESystem([D(x) ~ x, D(y) ~ y, D(z) ~ z], noiseeqs, t, [x, y, z], []) odesys = noise_to_brownians(sys) - vs = ModelingToolkit.vars(equations(odesys)) + vs = SU.search_variables(equations(odesys)) nbrownian = count( - v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + v -> ModelingToolkitBase.getvariabletype(v) == ModelingToolkitBase.BROWNIAN, vs) @test nbrownian == 3 for eq in equations(odesys) @test length(arguments(eq.rhs)) == 4 @@ -863,13 +868,15 @@ end end end -@testset "`mtkcompile(::SDESystem)`" begin - @variables x(t) y(t) - @mtkcompile sys = SDESystem( - [D(x) ~ x, y ~ 2x], [x, 0], t, [x, y], []) - @test length(equations(sys)) == 1 - @test length(ModelingToolkit.get_noise_eqs(sys)) == 1 - @test length(observed(sys)) == 1 +if @isdefined(ModelingToolkit) + @testset "`mtkcompile(::SDESystem)`" begin + @variables x(t) y(t) + @mtkcompile sys = SDESystem( + [D(x) ~ x, y ~ 2x], [x, 0], t, [x, y], []) + @test length(equations(sys)) == 1 + @test length(ModelingToolkitBase.get_noise_eqs(sys)) == 1 + @test length(observed(sys)) == 1 + end end # Test validating types of states @@ -878,10 +885,10 @@ end @variables X(t)::Int64 @brownians z eq2 = D(X) ~ p - d * X + z - @test_throws ModelingToolkit.ContinuousOperatorDiscreteArgumentError @mtkcompile ssys = System( + @test_throws ModelingToolkitBase.ContinuousOperatorDiscreteArgumentError @mtkcompile ssys = System( [eq2], t) noiseeq = [1] - @test_throws ModelingToolkit.ContinuousOperatorDiscreteArgumentError @named ssys = SDESystem( + @test_throws ModelingToolkitBase.ContinuousOperatorDiscreteArgumentError @named ssys = SDESystem( [eq2], [noiseeq], t) end @@ -951,8 +958,8 @@ end @test_deprecated @brownian a b c @brownian p q - @test ModelingToolkit.isbrownian(p) - @test ModelingToolkit.isbrownian(q) + @test ModelingToolkitBase.isbrownian(p) + @test ModelingToolkitBase.isbrownian(q) end @testset "noise kwarg propagation (issue #3664)" begin diff --git a/test/serialization.jl b/lib/ModelingToolkitBase/test/serialization.jl similarity index 65% rename from test/serialization.jl rename to lib/ModelingToolkitBase/test/serialization.jl index 43f2cabb6e..ac6214dbc6 100644 --- a/test/serialization.jl +++ b/lib/ModelingToolkitBase/test/serialization.jl @@ -1,13 +1,13 @@ -using ModelingToolkit, SciMLBase, Serialization, OrdinaryDiffEq -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, SciMLBase, Serialization, OrdinaryDiffEq +using ModelingToolkitBase: t_nounits as t, D_nounits as D @variables x(t) -@named sys = System([D(x) ~ -0.5 * x], t, defaults = Dict(x => 1.0)) +@named sys = System([D(x) ~ -0.5 * x], t, initial_conditions = Dict(x => 1.0)) sys = complete(sys) for prob in [ - eval(ModelingToolkit.ODEProblem{false}(sys, nothing, nothing)), - eval(ModelingToolkit.ODEProblem{false}(sys, nothing, nothing; expression = Val{true})) + eval(ModelingToolkitBase.ODEProblem{false}(sys, nothing, nothing)), + eval(ModelingToolkitBase.ODEProblem{false}(sys, nothing, nothing; expression = Val{true})) ] _fn = tempname() @@ -15,7 +15,7 @@ for prob in [ serialize(f, prob) end - _cmd = "using ModelingToolkit, Serialization; deserialize(\"$_fn\")" + _cmd = "using ModelingToolkitBase, Serialization; deserialize(\"$_fn\")" run(`$(Base.julia_cmd()) -e $(_cmd)`) end @@ -40,11 +40,11 @@ prob = ODEProblem(ss, [capacitor.v => 0.0], (0, 0.1)) sol = solve(prob, ImplicitEuler()) ## Check System with Observables ---------- -ss_exp = ModelingToolkit.toexpr(ss) +ss_exp = ModelingToolkitBase.toexpr(ss) ss_ = complete(eval(ss_exp)) prob_ = ODEProblem(ss_, [capacitor.v => 0.0], (0, 0.1)) sol_ = solve(prob_, ImplicitEuler()) -@test sol[all_obs] == sol_[all_obs] +@test sol[all_obs] == sol_[all_obs] broken=!@isdefined(ModelingToolkit) ## Check ODEProblemExpr with Observables ----------- @@ -54,4 +54,4 @@ probexpr = ODEProblem{true}(ss, [capacitor.v => 0.0], (0, 0.1); expr = Val{true} prob_obs = eval(probexpr) sol_obs = solve(prob_obs, ImplicitEuler()) @show all_obs -@test sol_obs[all_obs] == sol[all_obs] +@test sol_obs[all_obs] == sol[all_obs] broken=!@isdefined(ModelingToolkit) diff --git a/test/split_parameters.jl b/lib/ModelingToolkitBase/test/split_parameters.jl similarity index 56% rename from test/split_parameters.jl rename to lib/ModelingToolkitBase/test/split_parameters.jl index e0bd7328d9..1d23650c5c 100644 --- a/test/split_parameters.jl +++ b/lib/ModelingToolkitBase/test/split_parameters.jl @@ -1,34 +1,36 @@ -using ModelingToolkit, Test +using ModelingToolkitBase, Test using ModelingToolkitStandardLibrary.Blocks using OrdinaryDiffEq using DataInterpolations using BlockArrays: BlockedArray -using ModelingToolkit: t_nounits as t, D_nounits as D -using ModelingToolkit: MTKParameters, ParameterIndex, NONNUMERIC_PORTION +using ModelingToolkitBase: t_nounits as t, D_nounits as D, value +using ModelingToolkitBase: MTKParameters, ParameterIndex, NONNUMERIC_PORTION using SciMLStructures: Tunable, Discrete, Constants, Initials using SymbolicIndexingInterface: is_parameter, getp +using Symbolics +using SciCompDSL x = [1, 2.0, false, [1, 2, 3], Parameter(1.0)] -y = ModelingToolkit.promote_to_concrete(x) +y = ModelingToolkitBase.promote_to_concrete(x) @test eltype(y) == Union{Float64, Parameter{Float64}, Vector{Int64}} -y = ModelingToolkit.promote_to_concrete(x; tofloat = false) +y = ModelingToolkitBase.promote_to_concrete(x; tofloat = false) @test eltype(y) == Union{Bool, Float64, Int64, Parameter{Float64}, Vector{Int64}} x = [1, 2.0, false, [1, 2, 3]] -y = ModelingToolkit.promote_to_concrete(x) +y = ModelingToolkitBase.promote_to_concrete(x) @test eltype(y) == Union{Float64, Vector{Int64}} x = Any[1, 2.0, false] -y = ModelingToolkit.promote_to_concrete(x; tofloat = false) +y = ModelingToolkitBase.promote_to_concrete(x; tofloat = false) @test eltype(y) == Union{Bool, Float64, Int64} -y = ModelingToolkit.promote_to_concrete(x; use_union = false) +y = ModelingToolkitBase.promote_to_concrete(x; use_union = false) @test eltype(y) == Float64 x = Float16[1.0, 2.0, 3.0] -y = ModelingToolkit.promote_to_concrete(x) +y = ModelingToolkitBase.promote_to_concrete(x) @test eltype(y) == Float16 # ------------------------ Mixed Single Values and Vector @@ -51,52 +53,35 @@ end get_value(interp::Interpolator, t) = interp(t) @register_symbolic get_value(interp::Interpolator, t) -Symbolics.derivative(::typeof(get_value), args::NTuple{2, Any}, ::Val{2}) = 0 +@register_derivative get_value(interp, t) 2 Symbolics.COMMON_ZERO -function Sampled(; name, interp = Interpolator(Float64[], 0.0)) - pars = @parameters begin - interpolator::Interpolator = interp - end - - vars = [] - systems = @named begin - output = RealOutput() - end - - eqs = [ - output.u ~ get_value(interpolator, t) - ] - - return System(eqs, t, vars, [interpolator]; name, systems) +vars = @variables y(t) = 2.0 dy(t) +pars = @parameters begin + interpolator::Interpolator = Interpolator(x, dt) + k = 2.0 end +eqs = [ + D(y) ~ k * dy + dy ~ get_value(interpolator, t) +] -vars = @variables y(t) dy(t) ddy(t) -@named src = Sampled(; interp = Interpolator(x, dt)) -@named int = Integrator() - -eqs = [y ~ src.output.u - D(y) ~ dy - D(dy) ~ ddy - connect(src.output, int.input)] - -@named sys = System(eqs, t, vars, []; systems = [int, src]) -s = complete(sys) +@named sys = System(eqs, t, vars, pars) sys = mtkcompile(sys) prob = ODEProblem( - sys, [s.src.interpolator => Interpolator(x, dt)], (0.0, t_end); + sys, [sys.interpolator => Interpolator(x, dt)], (0.0, t_end); tofloat = false) -sol = solve(prob, ImplicitEuler()); +sol = solve(prob, Rodas5P(); abstol = 1e-8, reltol = 1e-8, tstops=time); @test sol.retcode == ReturnCode.Success -@test sol[y][end] == x[end] +@test sol[y][end] ≈ 688.6707200416254 rtol=1e-6 #TODO: remake becomes more complicated now, how to improve? -defs = ModelingToolkit.defaults(sys) -defs[s.src.interpolator] = Interpolator(2x, dt) -p′ = ModelingToolkit.MTKParameters(sys, defs) +defs = ModelingToolkitBase.initial_conditions(sys) +defs[sys.interpolator] = Interpolator(2x, dt) +p′ = ModelingToolkitBase.MTKParameters(sys, defs) prob′ = remake(prob; p = p′) -sol = solve(prob′, ImplicitEuler()); +sol = solve(prob′, Rodas5P(); abstol = 1e-8, reltol = 1e-8, tstops=time); @test sol.retcode == ReturnCode.Success -@test sol[y][end] == 2x[end] +@test sol[y][end] ≈ 1375.3416088550855 rtol=1e-6 # ------------------------ Mixed Type Converted to float (default behavior) @@ -125,78 +110,80 @@ sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success # ------------------------- Bug -using ModelingToolkit, LinearAlgebra -using ModelingToolkitStandardLibrary.Mechanical.Rotational -using ModelingToolkitStandardLibrary.Blocks -using ModelingToolkitStandardLibrary.Blocks: t -using ModelingToolkit: connect - -"A wrapper function to make symbolic indexing easier" -function wr(sys) - System(Equation[], ModelingToolkit.get_iv(sys), systems = [sys], name = :a_wrapper) -end -indexof(sym, syms) = findfirst(isequal(sym), syms) - -# Parameters -m1 = 1.0 -m2 = 1.0 -k = 10.0 # Spring stiffness -c = 3.0 # 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(use_support = false) - -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 @named model = System(eqs, - t; - systems = [torque, inertia1, inertia2, spring, damper, u]) +if @isdefined(ModelingToolkit) + using ModelingToolkitBase, LinearAlgebra + using ModelingToolkitStandardLibrary.Mechanical.Rotational + using ModelingToolkitStandardLibrary.Blocks + using ModelingToolkitStandardLibrary.Blocks: t + using ModelingToolkitBase: connect + + "A wrapper function to make symbolic indexing easier" + function wr(sys) + System(Equation[], ModelingToolkitBase.get_iv(sys), systems = [sys], name = :a_wrapper) + end + indexof(sym, syms) = findfirst(isequal(sym), syms) + + # Parameters + m1 = 1.0 + m2 = 1.0 + k = 10.0 # Spring stiffness + c = 3.0 # 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(use_support = false) + + 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 @named model = System(eqs, + t; + systems = [torque, inertia1, inertia2, spring, damper, u]) + end + System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], + name, guesses = [spring.flange_a.phi => 0.0]) end - System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name, guesses = [spring.flange_a.phi => 0.0]) -end -model = SystemModel() # Model with load disturbance -@named d = Step(start_time = 1.0, duration = 10.0, offset = 0.0, height = 1.0) # Disturbance -model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] # This is the state realization we want to control -inputs = [model.torque.tau.u] -op = [model.torque.tau.u => 0.0] -matrices, ssys = ModelingToolkit.linearize( - wr(model), inputs, model_outputs; op) - -# Design state-feedback gain using LQR -# Define cost matrices -x_costs = [model.inertia1.w => 1.0 - model.inertia2.w => 1.0 - model.inertia1.phi => 1.0 - model.inertia2.phi => 1.0] -L = randn(1, 4) # Post-multiply by `C` to get the correct input to the controller - -# This old definition of MatrixGain will work because the parameter space does not include K (an Array term) -# @component function MatrixGainAlt(K::AbstractArray; name) -# nout, nin = size(K, 1), size(K, 2) -# @named input = RealInput(; nin = nin) -# @named output = RealOutput(; nout = nout) -# eqs = [output.u[i] ~ sum(K[i, j] * input.u[j] for j in 1:nin) for i in 1:nout] -# compose(System(eqs, t, [], []; name = name), [input, output]) -# end - -@named state_feedback = MatrixGain(K = -L) # Build negative feedback into the feedback matrix -@named add = Add(; k1 = 1.0, k2 = 1.0) # To add the control signal and the disturbance - -connections = [[state_feedback.input.u[i] ~ model_outputs[i] for i in 1:4] - connect(d.output, :d, add.input1) - connect(add.input2, state_feedback.output) - connect(add.output, :u, model.torque.tau)] -@named closed_loop = System(connections, t, systems = [model, state_feedback, add, d]) -S = get_sensitivity(closed_loop, :u) + model = SystemModel() # Model with load disturbance + @named d = Step(start_time = 1.0, duration = 10.0, offset = 0.0, height = 1.0) # Disturbance + model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] # This is the state realization we want to control + inputs = [model.torque.tau.u] + op = [model.torque.tau.u => 0.0] + matrices, ssys = ModelingToolkit.linearize( + wr(model), inputs, model_outputs; op) + + # Design state-feedback gain using LQR + # Define cost matrices + x_costs = [model.inertia1.w => 1.0 + model.inertia2.w => 1.0 + model.inertia1.phi => 1.0 + model.inertia2.phi => 1.0] + L = randn(1, 4) # Post-multiply by `C` to get the correct input to the controller + + # This old definition of MatrixGain will work because the parameter space does not include K (an Array term) + # @component function MatrixGainAlt(K::AbstractArray; name) + # nout, nin = size(K, 1), size(K, 2) + # @named input = RealInput(; nin = nin) + # @named output = RealOutput(; nout = nout) + # eqs = [output.u[i] ~ sum(K[i, j] * input.u[j] for j in 1:nin) for i in 1:nout] + # compose(System(eqs, t, [], []; name = name), [input, output]) + # end + + @named state_feedback = MatrixGain(K = -L) # Build negative feedback into the feedback matrix + @named add = Add(; k1 = 1.0, k2 = 1.0) # To add the control signal and the disturbance + + connections = [[state_feedback.input.u[i] ~ model_outputs[i] for i in 1:4] + connect(d.output, :d, add.input1) + connect(add.input2, state_feedback.output) + connect(add.output, :u, model.torque.tau)] + @named closed_loop = System(connections, t, systems = [model, state_feedback, add, d]) + S = get_sensitivity(closed_loop, :u) +end @testset "Indexing MTKParameters with ParameterIndex" begin ps = MTKParameters(collect(1.0:10.0), collect(11.0:20.0), @@ -237,7 +224,7 @@ end @parameters fn(::Real) = _f1 @mtkcompile sys = System(D(x) ~ fn(t), t) @test is_parameter(sys, fn) - @test ModelingToolkit.defaults(sys)[fn] == _f1 + @test value(ModelingToolkitBase.initial_conditions(sys)[fn]) == _f1 getter = getp(sys, fn) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0)) @@ -304,7 +291,7 @@ end @named sys = ApexSystem() sysref = complete(sys) sys2 = complete(sys; split = true, flatten = false) - ps = Set(full_parameters(sys2)) + ps = Set(parameters(sys2)) @test sysref.k in ps @test sysref.subsys.c in ps @test length(ps) == 2 diff --git a/test/static_arrays.jl b/lib/ModelingToolkitBase/test/static_arrays.jl similarity index 79% rename from test/static_arrays.jl rename to lib/ModelingToolkitBase/test/static_arrays.jl index a23eeddde1..25d4b7073b 100644 --- a/test/static_arrays.jl +++ b/lib/ModelingToolkitBase/test/static_arrays.jl @@ -1,5 +1,5 @@ -using ModelingToolkit, SciMLBase, StaticArrays, Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase, SciMLBase, StaticArrays, Test +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters σ ρ β @variables x(t) y(t) z(t) diff --git a/test/steadystatesystems.jl b/lib/ModelingToolkitBase/test/steadystatesystems.jl similarity index 84% rename from test/steadystatesystems.jl rename to lib/ModelingToolkitBase/test/steadystatesystems.jl index 505e7da890..ce377d36b0 100644 --- a/test/steadystatesystems.jl +++ b/lib/ModelingToolkitBase/test/steadystatesystems.jl @@ -1,7 +1,8 @@ -using ModelingToolkit +using ModelingToolkitBase +using NonlinearSolve using SteadyStateDiffEq using Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase: t_nounits as t, D_nounits as D @parameters r @variables x(t) diff --git a/test/stream_connectors.jl b/lib/ModelingToolkitBase/test/stream_connectors.jl similarity index 94% rename from test/stream_connectors.jl rename to lib/ModelingToolkitBase/test/stream_connectors.jl index 493d9996e3..994e7ec0ea 100644 --- a/test/stream_connectors.jl +++ b/lib/ModelingToolkitBase/test/stream_connectors.jl @@ -1,6 +1,6 @@ using Test -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D @connector function TwoPhaseFluidPort(; name, P = 0.0, m_flow = 0.0, h_outflow = 0.0) pars = @parameters begin @@ -260,12 +260,12 @@ sys_exp = expand_connections(compose(sys, [sp1, sp2])) 0 ~ sp1.m_flow 0 ~ sp2.m_flow sp1.P ~ sp2.P - sp1.h_outflow ~ ModelingToolkit.instream(sp2.h_outflow) - sp2.h_outflow ~ ModelingToolkit.instream(sp1.h_outflow)]) + sp1.h_outflow ~ ModelingToolkitBase.instream(sp2.h_outflow) + sp2.h_outflow ~ ModelingToolkitBase.instream(sp1.h_outflow)]) # array var @connector function VecPin(; name) - sts = @variables v(t)[1:2]=[1.0, 0.0] i(t)[1:2]=1.0 [connect=Flow] + sts = @variables v(t)[1:2]=[1.0, 0.0] i(t)[1:2]=ones(2) [connect=Flow] System(Equation[], t, [sts...;], []; name = name) end @@ -284,7 +284,7 @@ sys = expand_connections(compose(simple, [vp1, vp2, vp3])) 0 ~ -vp1.i[2] - vp2.i[2] - vp3.i[2]]) @connector function VectorHeatPort(; name, N = 100, T0 = 0.0, Q0 = 0.0) - @variables (T(t))[1:N]=T0 (Q(t))[1:N]=Q0 [connect=Flow] + @variables (T(t))[1:N]=T0 * ones(N) (Q(t))[1:N]=Q0 * ones(N) [connect=Flow] System(Equation[], t, [T; Q], []; name = name) end @@ -294,7 +294,7 @@ end # Test the new Domain feature sys_ = expand_connections(n1m1Test) -sys_defs = ModelingToolkit.defaults(sys_) +sys_defs = ModelingToolkitBase.bindings(sys_) csys = complete(n1m1Test) @test Symbol(sys_defs[csys.pipe.port_a.rho]) == Symbol(csys.fluid.rho) @test Symbol(sys_defs[csys.pipe.port_b.rho]) == Symbol(csys.fluid.rho) @@ -317,7 +317,7 @@ csys = complete(n1m1Test) # equations --------------------------- eqs = Equation[] - System(eqs, t, vars, pars; name, defaults = [dm => 0]) + System(eqs, t, vars, pars; name, initial_conditions = [dm => 0]) end @connector function Fluid(; name, R, B, V) @@ -386,7 +386,7 @@ function StaticVolume(; P, V, name) H.dm ~ drho * V] System(eqs, t, vars, pars; name, systems, - defaults = [vrho => rho_0 * (1 + p_int / H.bulk)]) + initial_conditions = [vrho => rho_0 * (1 + p_int / H.bulk)]) end function PipeBase(; P, R, name) @@ -464,7 +464,7 @@ end @named two_fluid_system = TwoFluidSystem() sys = expand_connections(two_fluid_system) -sys_defs = ModelingToolkit.defaults(sys) +sys_defs = ModelingToolkitBase.bindings(sys) csys = complete(two_fluid_system) @test Symbol(sys_defs[csys.volume_a.H.rho]) == Symbol(csys.fluid_a.rho) @@ -502,10 +502,12 @@ end @named one_fluid_system = OneFluidSystem() sys = expand_connections(one_fluid_system) -sys_defs = ModelingToolkit.defaults(sys) +sys_defs = ModelingToolkitBase.bindings(sys) csys = complete(one_fluid_system) @test Symbol(sys_defs[csys.volume_a.H.rho]) == Symbol(csys.fluid.rho) @test Symbol(sys_defs[csys.volume_b.H.rho]) == Symbol(csys.fluid.rho) -@test_nowarn mtkcompile(one_fluid_system) +if @isdefined(ModelingToolkit) + @test_nowarn mtkcompile(one_fluid_system) +end diff --git a/test/symbolic_events.jl b/lib/ModelingToolkitBase/test/symbolic_events.jl similarity index 74% rename from test/symbolic_events.jl rename to lib/ModelingToolkitBase/test/symbolic_events.jl index 9633aaae7c..d65566a536 100644 --- a/test/symbolic_events.jl +++ b/lib/ModelingToolkitBase/test/symbolic_events.jl @@ -1,10 +1,13 @@ -using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Test +using ModelingToolkitBase , OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Test using SciMLStructures: canonicalize, Discrete -using ModelingToolkit: SymbolicContinuousCallback, +using ModelingToolkitBase: SymbolicContinuousCallback, SymbolicDiscreteCallback, t_nounits as t, D_nounits as D, affects, affect_negs, system, observed, AffectSystem +import DiffEqNoiseProcess +using SciCompDSL + using StableRNGs import SciMLBase using SymbolicIndexingInterface @@ -93,8 +96,8 @@ end @testset "ImperativeAffect constructors" begin fmfa(o, x, i, c) = nothing - m = ModelingToolkit.ImperativeAffect(fmfa) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test m.obs == [] @test m.obs_syms == [] @@ -102,8 +105,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa, (;)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (;)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test m.obs == [] @test m.obs_syms == [] @@ -111,8 +114,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (; x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -120,8 +123,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (; y = x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -129,8 +132,8 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa; observed = (; y = x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -138,8 +141,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa; modified = (; x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -147,8 +150,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa; modified = (; y = x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -156,8 +159,8 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (; x), (; x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:x] @@ -165,8 +168,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (; y = x), (; y = x)) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -174,9 +177,9 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect( + m = ModelingToolkitBase.ImperativeAffect( fmfa; modified = (; y = x), observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -184,9 +187,9 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.ImperativeAffect( + m = ModelingToolkitBase.ImperativeAffect( fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) - @test m isa ModelingToolkit.ImperativeAffect + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -194,8 +197,8 @@ end @test m.mod_syms == [:y] @test m.ctx === 3 - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) - @test m isa ModelingToolkit.ImperativeAffect + m = ModelingToolkitBase.ImperativeAffect(fmfa, (; x), (; x), 3) + @test m isa ModelingToolkitBase.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:x] @@ -217,15 +220,15 @@ end cevt2 = getfield(sys2, :continuous_events)[] @test getfield(sys2, :continuous_events)[] == SymbolicContinuousCallback(Equation[x ~ 2], nothing; zero_crossing_id = cevt2.zero_crossing_id) - @test all(ModelingToolkit.continuous_events(sys2) .== [ + @test all(ModelingToolkitBase.continuous_events(sys2) .== [ SymbolicContinuousCallback(Equation[x ~ 2], nothing; zero_crossing_id = cevt2.zero_crossing_id), SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing; zero_crossing_id = cevt1.zero_crossing_id) ]) @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) - @test length(ModelingToolkit.continuous_events(sys2)) == 2 - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) + @test length(ModelingToolkitBase.continuous_events(sys2)) == 2 + @test isequal(equations(ModelingToolkitBase.continuous_events(sys2)[1])[], x ~ 2) + @test isequal(equations(ModelingToolkitBase.continuous_events(sys2)[2])[], sys.x ~ 1) sys = complete(sys) sys_nosplit = complete(sys; split = false) @@ -235,8 +238,8 @@ end prob = ODEProblem(sys, Pair[], (0.0, 2.0)) p0 = 0 t0 = 0 - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback - cb = ModelingToolkit.generate_continuous_callbacks(sys) + @test get_callback(prob) isa ModelingToolkitBase.DiffEqCallbacks.ContinuousCallback + cb = ModelingToolkitBase.generate_continuous_callbacks(sys) cond = cb.condition out = [0.0] cond.f(out, [0], p0, t0) @@ -266,7 +269,7 @@ end prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test cb isa ModelingToolkitBase.DiffEqCallbacks.VectorContinuousCallback cond = cb.condition out = [0.0, 0.0] @@ -294,7 +297,7 @@ end @named sys = System(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown sys = complete(sys) prob = ODEProblem(sys, Pair[], (0.0, 3.0)) - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test get_callback(prob) isa ModelingToolkitBase.DiffEqCallbacks.VectorContinuousCallback sol = solve(prob, Tsit5()) @test minimum(t -> abs(t - 1), sol.t) < 1e-9 # test that the solver stepped at the first root @test minimum(t -> abs(t - 2), sol.t) < 1e-9 # test that the solver stepped at the second root @@ -316,10 +319,14 @@ end @test isequal(only(cev.affect.affect), v ~ -Pre(v)) ball = mtkcompile(ball) - @test length(ModelingToolkit.continuous_events(ball)) == 1 + @test length(ModelingToolkitBase.continuous_events(ball)) == 1 cev = only(continuous_events(ball)) @test isequal(only(equations(cev)), x ~ 0) - @test isequal(only(observed(cev.affect.system)), v ~ -Pre(v)) + if @isdefined(ModelingToolkit) + @test isequal(only(observed(cev.affect.system)), v ~ -Pre(v)) + else + @test isequal(only(equations(cev.affect.system)), 0 ~ -Pre(v) - v) + end tspan = (0.0, 5.0) prob = ODEProblem(ball, Pair[], tspan) @@ -347,12 +354,17 @@ end prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test cb isa ModelingToolkitBase.DiffEqCallbacks.VectorContinuousCallback _cevs = getfield(ball, :continuous_events) @test isequal(only(equations(_cevs[1])), x ~ 0) - @test isequal(only(observed(_cevs[1].affect.system)), vx ~ -Pre(vx)) @test issetequal(equations(_cevs[2]), [y ~ -1.5, y ~ 1.5]) - @test isequal(only(observed(_cevs[2].affect.system)), vy ~ -Pre(vy)) + if @isdefined(ModelingToolkit) + @test isequal(only(observed(_cevs[1].affect.system)), vx ~ -Pre(vx)) + @test isequal(only(observed(_cevs[2].affect.system)), vy ~ -Pre(vy)) + else + @test isequal(only(equations(_cevs[1].affect.system)), 0 ~ -Pre(vx) - vx) + @test isequal(only(equations(_cevs[2].affect.system)), 0 ~ -Pre(vy) - vy) + end cond = cb.condition out = [0.0, 0.0, 0.0] p0 = 0.0 @@ -393,7 +405,7 @@ end @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) end -# issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 +# issue https://github.com/SciML/ModelingToolkitBase.jl/issues/1386 # tests that it works for ODAESystem @testset "ODAESystem" begin @variables vs(t) v(t) vmeasured(t) @@ -403,8 +415,8 @@ end ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] @named sys = System(eq, t, continuous_events = ev) sys = mtkcompile(sys) - prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) - sol = solve(prob, Tsit5()) + prob = ODEProblem(sys, [v => 0, vmeasured => 0], (0.0, 5.1)) + sol = solve(prob, @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P()) @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event @test sol([0.25 - eps()])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property end @@ -413,8 +425,8 @@ end @testset "Handle Empty Events" begin Dₜ = D - @parameters u(t) [input = true] # Indicate that this is a controlled input - @parameters y(t) [output = true] # Indicate that this is a measured output + @discretes u(t) [input = true] # Indicate that this is a controlled input + @discretes y(t) [output = true] # Indicate that this is a measured output function Mass(; name, m = 1.0, p = 0, v = 0) ps = @parameters m = m @@ -454,7 +466,7 @@ end end model = Model(sin(30t)) sys = mtkcompile(model) - @test isempty(ModelingToolkit.continuous_events(sys)) + @test isempty(ModelingToolkitBase.continuous_events(sys)) end @testset "SDE/ODESystem Discrete Callbacks" begin @@ -469,7 +481,8 @@ end sol end - @parameters k(t) t1 t2 + @parameters t1 t2 + @discretes k(t) @variables A(t) B(t) cond1 = (t == t1) @@ -508,8 +521,8 @@ end # 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‵ = SymbolicDiscreteCallback([2.0] => affect2, discrete_parameters = [k], iv = t) - @named osys‵ = System(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) - @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) + @named osys‵ = System(eqs, t, [A], [k, t1, t2], discrete_events = [cb1‵, cb2‵]) + @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1‵, cb2‵]) testsol(osys‵, ODEProblem, Tsit5, u0, p, tspan; paramtotest = k) testsol(ssys‵, SDEProblem, RI5, u0, p, tspan; paramtotest = k) @@ -525,8 +538,8 @@ end return (; k = 1.0) end cb2‵‵ = [2.0] => (f = affect!, modified = (; k)) - @named osys4 = System(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) - @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], + @named osys4 = System(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵]) + @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵]) oprob4 = ODEProblem(complete(osys4), [u0; p], tspan) testsol(osys4, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0], paramtotest = k) @@ -573,7 +586,8 @@ end sol end - @parameters k(t) t1 t2 + @parameters t1 t2 + @discretes k(t) @variables A(t) B(t) eqs = [MassActionJump(k, [A => 1], [A => -1])] @@ -642,7 +656,7 @@ end oneosc_ce_simpl = mtkcompile(oneosc_ce) prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0)) - sol = solve(prob, Tsit5(), saveat = 0.1) + sol = solve(prob, @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P(), saveat = 0.1) @test typeof(oneosc_ce_simpl) == System @test sol(0.5, idxs = oscce.x) < 1.0 # test whether x(t) decreases over time @@ -659,9 +673,9 @@ end end cr1 = [] cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2)) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) trigsys_ss = mtkcompile(trigsys) @@ -679,10 +693,10 @@ end cr2p = [] cr1n = [] cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1p); affect_neg = (f = record_crossings, observed = (; v = c1), ctx = cr1n)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2p); affect_neg = (f = record_crossings, observed = (; v = c2), ctx = cr2n)) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) @@ -705,9 +719,9 @@ end # with nothing neg affect (pos * neg + left root find) cr1p = [] cr2p = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2p); affect_neg = nothing) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) trigsys_ss = mtkcompile(trigsys) @@ -723,9 +737,9 @@ end cr2p = [] cr1n = [] cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2p); affect_neg = (f = record_crossings, observed = (; v = c2), ctx = cr2n)) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) @@ -746,10 +760,10 @@ end @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) cr1 = [] cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1); rootfind = SciMLBase.RightRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2); rootfind = SciMLBase.RightRootFind) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) @@ -766,10 +780,10 @@ end # baseline affect w/ mixed rootfind (pos + neg + right root find) cr1 = [] cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1); rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2); rootfind = SciMLBase.RightRootFind) @named trigsys = System(eqs, t; continuous_events = [evt1, evt2]) @@ -784,10 +798,10 @@ end #flip order and ensure results are okay cr1 = [] cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( + evt1 = ModelingToolkitBase.SymbolicContinuousCallback( [c1 ~ 0], (f = record_crossings, observed = (; v = c1), ctx = cr1); rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( + evt2 = ModelingToolkitBase.SymbolicContinuousCallback( [c2 ~ 0], (f = record_crossings, observed = (; v = c2), ctx = cr2); rootfind = SciMLBase.RightRootFind) @named trigsys = System(eqs, t; continuous_events = [evt2, evt1]) @@ -800,95 +814,97 @@ end @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) end -@testset "Discrete event reinitialization (#3142)" begin - @connector LiquidPort begin - p(t)::Float64, [description = "Set pressure in bar", - guess = 1.01325] - Vdot(t)::Float64, - [description = "Volume flow rate in L/min", - guess = 0.0, - connect = Flow] - end - - @mtkmodel PressureSource begin - @components begin - port = LiquidPort() +if @isdefined(ModelingToolkit) + @testset "Discrete event reinitialization (#3142)" begin + @connector LiquidPort begin + p(t)::Float64, [description = "Set pressure in bar", + guess = 1.01325] + Vdot(t)::Float64, + [description = "Volume flow rate in L/min", + guess = 0.0, + connect = Flow] end - @parameters begin - p_set::Float64 = 1.01325, [description = "Set pressure in bar"] - end - @equations begin - port.p ~ p_set - end - end - @mtkmodel BinaryValve begin - @constants begin - p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] - ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] + @mtkmodel PressureSource begin + @components begin + port = LiquidPort() + end + @parameters begin + p_set::Float64 = 1.01325, [description = "Set pressure in bar"] + end + @equations begin + port.p ~ p_set + end end - @components begin - port_in = LiquidPort() - port_out = LiquidPort() - end - @parameters begin - k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] - k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] - ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] - end - @variables begin - S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] - Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] - Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] - end - @equations begin - # Port handling - port_in.Vdot ~ -Vdot - port_out.Vdot ~ Vdot - Δp ~ port_in.p - port_out.p - # System behavior - D(S) ~ 0.0 - Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt - end - end - # Test System - @mtkmodel TestSystem begin - @components begin - pressure_source_1 = PressureSource(p_set = 2.0) - binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) - binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) - pressure_source_2 = PressureSource(p_set = 1.0) - end - @equations begin - connect(pressure_source_1.port, binary_valve_1.port_in) - connect(binary_valve_1.port_out, binary_valve_2.port_in) - connect(binary_valve_2.port_out, pressure_source_2.port) - end - @discrete_events begin - [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] - [60] => [binary_valve_1.S ~ 1.0, binary_valve_2.Δp ~ 1.0] - [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] + @mtkmodel BinaryValve begin + @constants begin + p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] + ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] + end + @components begin + port_in = LiquidPort() + port_out = LiquidPort() + end + @parameters begin + k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] + k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] + ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] + end + @variables begin + S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] + Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] + Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] + end + @equations begin + # Port handling + port_in.Vdot ~ -Vdot + port_out.Vdot ~ Vdot + Δp ~ port_in.p - port_out.p + # System behavior + D(S) ~ 0.0 + Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt + end end - end - # Test Simulation - @mtkcompile sys = TestSystem() + # Test System + @mtkmodel TestSystem begin + @components begin + pressure_source_1 = PressureSource(p_set = 2.0) + binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) + binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) + pressure_source_2 = PressureSource(p_set = 1.0) + end + @equations begin + connect(pressure_source_1.port, binary_valve_1.port_in) + connect(binary_valve_1.port_out, binary_valve_2.port_in) + connect(binary_valve_2.port_out, pressure_source_2.port) + end + @discrete_events begin + [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] + [60] => [binary_valve_1.S ~ 1.0, binary_valve_2.Δp ~ 1.0] + [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] + end + end - # Test Simulation - prob = ODEProblem(sys, [], (0.0, 150.0)) - sol = solve(prob) - # This is singular at the second event, but the derivatives are zero so it's - # constant after that point anyway. Just make sure it hits the last event and - # had the correct `u`. - @test_broken SciMLBase.successful_retcode(sol) - @test sol.t[end] >= 120.0 - @test sol[end] == [0.0, 0.0, 0.0] + # Test Simulation + @mtkcompile sys = TestSystem() + + # Test Simulation + prob = ODEProblem(sys, [], (0.0, 150.0)) + sol = solve(prob) + # This is singular at the second event, but the derivatives are zero so it's + # constant after that point anyway. Just make sure it hits the last event and + # had the correct `u`. + @test_broken SciMLBase.successful_retcode(sol) + @test sol.t[end] >= 120.0 + @test sol[end] == [0.0, 0.0, 0.0] + end end @testset "Discrete variable timeseries" begin @variables x(t) - @parameters a(t) b(t) c(t) + @discretes a(t) b(t) c(t) cb1 = SymbolicContinuousCallback([x ~ 1.0] => [a ~ -Pre(a)], discrete_parameters = [a]) function save_affect!(mod, obs, ctx, integ) return (; b = 5.0) @@ -914,14 +930,14 @@ end D(temp) ~ furnace_on * furnace_power - temp^2 * leakage ] - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c @set! x.furnace_on = false end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + furnace_enable = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c @set! x.furnace_on = true end) @named sys = System( @@ -931,17 +947,17 @@ end sol = solve(prob, Tsit5(); dtmax = 0.01) @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false - end; initialize = ModelingToolkit.ImperativeAffect(modified = (; + end; initialize = ModelingToolkitBase.ImperativeAffect(modified = (; temp)) do x, o, c, i @set! x.temp = 0.2 end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + furnace_enable = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = true end) @named sys = System( @@ -960,9 +976,9 @@ end D(temp) ~ furnace_on * furnace_power - temp^2 * leakage ] - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect( + ModelingToolkitBase.ImperativeAffect( modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false end) @@ -976,22 +992,24 @@ end eqs = [tempsq ~ temp^2 D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect( + ModelingToolkitBase.ImperativeAffect( modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false end) @named sys = System( eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) ss = mtkcompile(sys) - @test_throws "refers to missing variable(s)" prob=ODEProblem( - ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + if @isdefined(ModelingToolkit) + @test_throws "refers to missing variable(s)" prob=ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + end @parameters not_actually_here - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on), + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on), observed = (; furnace_on, not_actually_here)) do x, o, c, i @set! x.furnace_on = false end) @@ -1001,9 +1019,9 @@ end @test_throws "refers to missing variable(s)" prob=ODEProblem( ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - furnace_off = ModelingToolkit.SymbolicContinuousCallback( + furnace_off = ModelingToolkitBase.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on), + ModelingToolkitBase.ImperativeAffect(modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i return (; fictional2 = false) end) @@ -1012,7 +1030,9 @@ end ss = mtkcompile(sys) prob = ODEProblem( ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - @test_throws "Tried to write back to" solve(prob, Tsit5()) + if @isdefined(ModelingToolkit) + @test_throws "Tried to write back to" solve(prob, Tsit5()) + end end @testset "Quadrature" begin @@ -1035,15 +1055,15 @@ end return 0 # err is interpreted as no movement end end - qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i + qAevt = ModelingToolkitBase.SymbolicContinuousCallback([cos(100 * theta) ~ 0], + ModelingToolkitBase.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i @set! x.hA = x.qA @set! x.hB = o.qB @set! x.qA = 1 @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) x end, - affect_neg = ModelingToolkit.ImperativeAffect( + affect_neg = ModelingToolkitBase.ImperativeAffect( (; qA, hA, hB, cnt), (; qB)) do x, o, c, i @set! x.hA = x.qA @set! x.hB = o.qB @@ -1051,15 +1071,15 @@ end @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) x end; rootfind = SciMLBase.RightRootFind) - qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i + qBevt = ModelingToolkitBase.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], + ModelingToolkitBase.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i @set! x.hA = o.qA @set! x.hB = x.qB @set! x.qB = 1 @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) x end, - affect_neg = ModelingToolkit.ImperativeAffect( + affect_neg = ModelingToolkitBase.ImperativeAffect( (; qB, hA, hB, cnt), (; qA)) do x, o, c, i @set! x.hA = o.qA @set! x.hB = x.qB @@ -1071,15 +1091,15 @@ end eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) ss = mtkcompile(sys) prob = ODEProblem(ss, [theta => 1e-5], (0.0, pi)) - sol = solve(prob, Tsit5(); dtmax = 0.01) + sol = solve(prob, @isdefined(ModelingToolkit) ? Tsit5() : Rodas5P(); dtmax = 0.01) @test getp(sol, cnt)(sol) == 198 # we get 2 pulses per phase cycle (cos 0 crossing) and we go to 100 cycles; we miss a few due to the initial state end @testset "Initialization" begin @variables x(t) seen = false - f = ModelingToolkit.ImperativeAffect(f = (m, o, ctx, int) -> (seen = true; return (;))) - cb1 = ModelingToolkit.SymbolicContinuousCallback( + f = ModelingToolkitBase.ImperativeAffect(f = (m, o, ctx, int) -> (seen = true; return (;))) + cb1 = ModelingToolkitBase.SymbolicContinuousCallback( [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) @@ -1090,16 +1110,16 @@ end @variables x(t) seen = false - f = ModelingToolkit.ImperativeAffect(f = (m, o, ctx, int) -> (seen = true; return (;))) - cb1 = ModelingToolkit.SymbolicContinuousCallback( + f = ModelingToolkitBase.ImperativeAffect(f = (m, o, ctx, int) -> (seen = true; return (;))) + cb1 = ModelingToolkitBase.SymbolicContinuousCallback( [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) inited = false finaled = false - a = ModelingToolkit.ImperativeAffect(f = ( + a = ModelingToolkitBase.ImperativeAffect(f = ( m, o, ctx, int) -> (inited = true; return (;))) - b = ModelingToolkit.ImperativeAffect(f = ( + b = ModelingToolkitBase.ImperativeAffect(f = ( m, o, ctx, int) -> (finaled = true; return (;))) - cb2 = ModelingToolkit.SymbolicContinuousCallback( + cb2 = ModelingToolkitBase.SymbolicContinuousCallback( [x ~ 0.1], nothing, initialize = a, finalize = b) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) @@ -1113,7 +1133,7 @@ end #periodic inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback( + cb3 = ModelingToolkitBase.SymbolicDiscreteCallback( 1.0, [x ~ 2], initialize = a, finalize = b) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) @@ -1127,7 +1147,7 @@ end seen = false inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) + cb3 = ModelingToolkitBase.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) sol = solve(prob, Tsit5()) @@ -1138,7 +1158,7 @@ end seen = false inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) + cb3 = ModelingToolkitBase.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) sol = solve(prob, Tsit5()) @@ -1150,7 +1170,7 @@ end seen = false inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback( + cb3 = ModelingToolkitBase.SymbolicDiscreteCallback( t == 1.0, f, initialize = a, finalize = b) @mtkcompile sys = System(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2)) @@ -1202,7 +1222,7 @@ end @testset "Array parameter updates in ImperativeAffect" begin function weird1(max_time; name) - params = @parameters begin + params = @discretes begin θ(t) = 0.0 end vars = @variables begin @@ -1211,7 +1231,7 @@ end eqs = reduce(vcat, Symbolics.scalarize.([ D(x) ~ 1.0 ])) - reset = ModelingToolkit.ImperativeAffect( + reset = ModelingToolkitBase.ImperativeAffect( modified = (; x, θ)) do m, o, _, i @set! m.θ = 0.0 @set! m.x = 0.0 @@ -1222,7 +1242,7 @@ end end function weird2(max_time; name) - params = @parameters begin + params = @discretes begin θ(t) = 0.0 end vars = @variables begin @@ -1238,13 +1258,13 @@ end @named wd2 = weird2(0.021) sys1 = mtkcompile(System(Equation[], t; name = :parent, - discrete_events = [0.01 => ModelingToolkit.ImperativeAffect( + discrete_events = [0.01 => ModelingToolkitBase.ImperativeAffect( modified = (; θs = reduce(vcat, [[wd1.θ]])), ctx = [1]) do m, o, c, i @set! m.θs[1] = c[] += 1 end], systems = [wd1])) sys2 = mtkcompile(System(Equation[], t; name = :parent, - discrete_events = [0.01 => ModelingToolkit.ImperativeAffect( + discrete_events = [0.01 => ModelingToolkitBase.ImperativeAffect( modified = (; θs = reduce(vcat, [[wd2.θ]])), ctx = [1]) do m, o, c, i @set! m.θs[1] = c[] += 1 end], @@ -1257,65 +1277,67 @@ end @test 100.0 ∈ sol2[sys2.wd2.θ] end -@testset "Implicit affects with Pre" begin - using ModelingToolkit: UnsolvableCallbackError - @parameters g - @variables x(t) y(t) λ(t) - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - c_evt = [t ~ 5.0] => [x ~ Pre(x) + 0.1] - @mtkcompile pend = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => -1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) - sol = solve(prob, FBDF()) - @test ≈(sol(5.000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) - @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) - - # Implicit affect with Pre - c_evt = [t ~ 5.0] => [x ~ Pre(x) + y^2] - @mtkcompile pend = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) - sol = solve(prob, FBDF()) - @test ≈(sol(5.000001, idxs = y)^2 + sol(4.999999, idxs = x), - sol(5.000001, idxs = x), rtol = 1e-4) - @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) - - # Impossible affect errors - c_evt = [t ~ 5.0] => [x ~ Pre(x) + 2] - @mtkcompile pend = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) - @test_throws UnsolvableCallbackError sol=solve(prob, FBDF()) - - # Changing both variables and parameters in the same affect. - @parameters g(t) - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - c_evt = SymbolicContinuousCallback( - [t ~ 5.0], [x ~ Pre(x) + 0.1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) - @mtkcompile pend = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) - sol = solve(prob, FBDF()) - @test sol.ps[g] ≈ [1, 2] - @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) - - # Proper re-initialization after parameter change - eqs = [y ~ g^2, D(x) ~ x] - c_evt = SymbolicContinuousCallback( - [t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) - @mtkcompile sys = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 1.0, g => 2], (0.0, 10.0)) - sol = solve(prob, FBDF()) - @test sol.ps[g] ≈ [2.0, 3.0] - @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) - @test ≈(sol(5.00000001, idxs = y), 9, rtol = 1e-4) - - # Parameters that don't appear in affects should not be mutated. - c_evt = [t ~ 5.0] => [x ~ Pre(x) + 1] - @mtkcompile sys = System(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 0.5, g => 2], (0.0, 10.0), guesses = [y => 0]) - sol = solve(prob, FBDF()) - @test prob.ps[g] == sol.ps[g] +if @isdefined(ModelingToolkit) + @testset "Implicit affects with Pre" begin + using ModelingToolkitBase: UnsolvableCallbackError + @parameters g + @variables x(t) y(t) λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 0.1] + @mtkcompile pend = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => -1, D(x) => 0, g => 1], (0.0, 10.0), guesses = [λ => 1, y => 1]) + sol = solve(prob, FBDF()) + @test ≈(sol(5.000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) + @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) + + # Implicit affect with Pre + c_evt = [t ~ 5.0] => [x ~ Pre(x) + y^2] + @mtkcompile pend = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, D(x) => 0, g => 1], (0.0, 10.0), guesses = [λ => 1, y => 1]) + sol = solve(prob, FBDF()) + @test ≈(sol(5.000001, idxs = y)^2 + sol(4.999999, idxs = x), + sol(5.000001, idxs = x), rtol = 1e-4) + @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) + + # Impossible affect errors + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 2] + @mtkcompile pend = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) + @test_throws UnsolvableCallbackError sol=solve(prob, FBDF()) + + # Changing both variables and parameters in the same affect. + @discretes g(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + c_evt = SymbolicContinuousCallback( + [t ~ 5.0], [x ~ Pre(x) + 0.1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) + @mtkcompile pend = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0, g => 1], (0.0, 10.0), guesses = [λ => 1]) + sol = solve(prob, FBDF()) + @test sol.ps[g] ≈ [1, 2] + @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) + + # Proper re-initialization after parameter change + eqs = [y ~ g^2, D(x) ~ x] + c_evt = SymbolicContinuousCallback( + [t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) + @mtkcompile sys = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(sys, [x => 1.0, g => 2], (0.0, 10.0)) + sol = solve(prob, FBDF()) + @test sol.ps[g] ≈ [2.0, 3.0] + @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) + @test ≈(sol(5.00000001, idxs = y), 9, rtol = 1e-4) + + # Parameters that don't appear in affects should not be mutated. + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 1] + @mtkcompile sys = System(eqs, t, continuous_events = c_evt) + prob = ODEProblem(sys, [x => 0.5, g => 2], (0.0, 10.0), guesses = [y => 0]) + sol = solve(prob, FBDF()) + @test prob.ps[g] == sol.ps[g] + end end @testset "Array parameter updates of parent components in ImperativeEffect" begin @@ -1326,7 +1348,7 @@ end eqs = reduce(vcat, Symbolics.scalarize.([ D(x) ~ 1.0 ])) - reset = ModelingToolkit.ImperativeAffect( + reset = ModelingToolkitBase.ImperativeAffect( modified = (; vals = Symbolics.scalarize(ParentScope.(vals)), x)) do m, o, _, i @set! m.vals = m.vals .+ 1 @set! m.x = 0.0 @@ -1335,8 +1357,8 @@ end return System(eqs, t, vars, []; name = name, continuous_events = [[x ~ max_time] => reset]) end - shared_pars = @parameters begin - vals(t)[1:2] = 0.0 + shared_pars = @discretes begin + vals(t)[1:2] = zeros(2) end @named sys = System(Equation[], t, [], Symbolics.scalarize(vals); @@ -1347,9 +1369,9 @@ end @testset "non-floating-point discretes and namespaced affects" begin function Inner(; name) - @parameters p(t)::Int + @discretes p(t)::Int @variables x(t) - cevs = ModelingToolkit.SymbolicContinuousCallback( + cevs = ModelingToolkitBase.SymbolicContinuousCallback( [x ~ 1.0], [p ~ Pre(p) + 1]; iv = t, discrete_parameters = [p]) System([D(x) ~ 1], t, [x], [p]; continuous_events = [cevs], name) end @@ -1371,11 +1393,10 @@ end p1 = ParamTest(1) tp1 = typeof(p1) @parameters (p_1::tp1)(..) = p1 - @parameters p2(t) = 1.0 + @discretes p2(t) = 1.0 @variables x(t) = 0.0 @variables x2(t) - event = ModelingToolkit.SymbolicDiscreteCallback( - [0.5] => [p2 ~ Pre(t)]; discrete_parameters = [p2]) + event = SymbolicDiscreteCallback([0.5], [p2 ~ Pre(t)]; discrete_parameters = [p2]) eq = [ D(x) ~ p2, @@ -1386,39 +1407,42 @@ end prob = ODEProblem(sys, [], (0.0, 1.0)) sol = solve(prob) @test SciMLBase.successful_retcode(sol) + @test sol[p2] ≈ [1.0, 0.5] @test sol[x, end]≈0.75 atol=1e-6 end -@testset "Symbolic affects are compiled in `complete`" begin - @parameters g - @variables x(t) [state_priority = 10.0] y(t) [guess = 1.0] - @variables λ(t) [guess = 1.0] - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - cevts = [[x ~ 0.0] => [D(x) ~ Pre(D(x)) + 1sign(Pre(D(x)))]] - @named pend = System(eqs, t; continuous_events = cevts) - - scc = only(continuous_events(pend)) - @test scc.affect isa ModelingToolkit.SymbolicAffect - - pend = mtkcompile(pend) - - scc = only(continuous_events(pend)) - @test scc.affect isa ModelingToolkit.AffectSystem - @test length(ModelingToolkit.all_equations(scc.affect)) == 5 # 1 affect, 3 algebraic, 1 observed - - u0 = [x => -1/2, D(x) => 1/2, g => 1] - prob = ODEProblem(pend, u0, (0.0, 5.0)) - sol = solve(prob, FBDF()) - @test SciMLBase.successful_retcode(sol) +if @isdefined(ModelingToolkit) + @testset "Symbolic affects are compiled in `complete`" begin + @parameters g + @variables x(t) [state_priority = 10.0] y(t) [guess = 1.0] + @variables λ(t) [guess = 1.0] + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + cevts = [[x ~ 0.0] => [D(x) ~ Pre(D(x)) + 0.1sign(Pre(D(x)))]] + @named pend = System(eqs, t; continuous_events = cevts) + + scc = only(continuous_events(pend)) + @test scc.affect isa ModelingToolkitBase.SymbolicAffect + + pend = mtkcompile(pend) + + scc = only(continuous_events(pend)) + @test scc.affect isa ModelingToolkitBase.AffectSystem + @test length(ModelingToolkitBase.all_equations(scc.affect)) == 5 # 1 affect, 3 algebraic, 1 observed + + u0 = [x => -1/2, D(x) => 1/2, g => 1] + prob = ODEProblem(pend, u0, (0.0, 5.0)) + sol = solve(prob, FBDF()) + @test SciMLBase.successful_retcode(sol) + end end @testset "Algebraic equation with input variable in symbolic affect" begin # Specifically happens when the variable marked as an input is an algebraic variable # in the affect system. @variables x(t) [input = true] y(t) - dev = ModelingToolkit.SymbolicDiscreteCallback(1.0, [y ~ Pre(y) + 1]) + dev = ModelingToolkitBase.SymbolicDiscreteCallback(1.0, [y ~ Pre(y) + 1]) @named sys = System([D(y) ~ 2x + 1, x^2 ~ 2y^3], t; discrete_events = [dev]) sys = @test_nowarn mtkcompile(sys) end @@ -1443,7 +1467,8 @@ end end @testset "Test erroneously created events yields errors" begin - @parameters p(t) d + @discretes p(t) + @parameters d @variables X(t) @test_throws "Vectors of symbolic conditions are not allowed" SymbolicDiscreteCallback([X < 5.0] => [X ~ @@ -1459,8 +1484,8 @@ end @testset "Issue#3990: Scalarized array passed to `discrete_parameters` of symbolic affect" begin N = 2 - @parameters v(t)[1:N] - @parameters M(t)[1:N, 1:N] + @discretes v(t)[1:N] + @discretes M(t)[1:N, 1:N] @variables x(t) @@ -1470,13 +1495,13 @@ end v_eq = [D(x) ~ x * Symbolics.scalarize(sum(v))] M_eq = [D(x) ~ x * Symbolics.scalarize(sum(M))] - v_event = ModelingToolkit.SymbolicDiscreteCallback( + v_event = ModelingToolkitBase.SymbolicDiscreteCallback( 1.0, [v ~ -Pre(v)], discrete_parameters = [v] ) - M_event = ModelingToolkit.SymbolicDiscreteCallback( + M_event = ModelingToolkitBase.SymbolicDiscreteCallback( 1.0, [M ~ -Pre(M)], discrete_parameters = [M] @@ -1485,10 +1510,8 @@ end @mtkcompile v_sys = System(v_eq, t; discrete_events = v_event) @mtkcompile M_sys = System(M_eq, t; discrete_events = M_event) - u0p0_map = Dict(x => 1.0, M => Mini, v => vini) - - v_prob = ODEProblem(v_sys, u0p0_map, (0.0, 2.5)) - M_prob = ODEProblem(M_sys, u0p0_map, (0.0, 2.5)) + v_prob = ODEProblem(v_sys, [x => 1.0, v => vini], (0.0, 2.5)) + M_prob = ODEProblem(M_sys, [x => 1.0, M => Mini], (0.0, 2.5)) v_sol = solve(v_prob, Tsit5()) M_sol = solve(M_prob, Tsit5()) @@ -1499,8 +1522,8 @@ end @testset "Issue#3990: Scalarized array passed to `discrete_parameters` of symbolic affect" begin N = 2 - @parameters v(t)[1:N] - @parameters M(t)[1:N, 1:N] + @discretes v(t)[1:N] + @discretes M(t)[1:N, 1:N] @variables x(t) @@ -1510,13 +1533,13 @@ end v_eq = [D(x) ~ x * Symbolics.scalarize(sum(v))] M_eq = [D(x) ~ x * Symbolics.scalarize(sum(M))] - v_event = ModelingToolkit.SymbolicDiscreteCallback( + v_event = ModelingToolkitBase.SymbolicDiscreteCallback( 1.0, [v ~ -Pre(v)], discrete_parameters = collect(v) ) - M_event = ModelingToolkit.SymbolicDiscreteCallback( + M_event = ModelingToolkitBase.SymbolicDiscreteCallback( 1.0, [M ~ -Pre(M)], discrete_parameters = vec(collect(M)) @@ -1525,10 +1548,8 @@ end @mtkcompile v_sys = System(v_eq, t; discrete_events = v_event) @mtkcompile M_sys = System(M_eq, t; discrete_events = M_event) - u0p0_map = Dict(x => 1.0, M => Mini, v => vini) - - v_prob = ODEProblem(v_sys, u0p0_map, (0.0, 2.5)) - M_prob = ODEProblem(M_sys, u0p0_map, (0.0, 2.5)) + v_prob = ODEProblem(v_sys, [x => 1.0, v => vini], (0.0, 2.5)) + M_prob = ODEProblem(M_sys, [x => 1.0, M => Mini], (0.0, 2.5)) v_sol = solve(v_prob, Tsit5()) M_sol = solve(M_prob, Tsit5()) @@ -1543,9 +1564,12 @@ end X(t)[1:2] = [4.0, 4.0] end ps = @parameters begin - k(t)[1:2] = [1, 1] kup = 2.0 end + discs = @discretes begin + k(t)[1:2] = [1, 1] + end + ps = [ps; discs] eqs = [ D(X[1]) ~ -k[1]*X[1] + k[2]*X[2] D(X[2]) ~ k[1]*X[1] - k[2]*X[2] diff --git a/test/symbolic_indexing_interface.jl b/lib/ModelingToolkitBase/test/symbolic_indexing_interface.jl similarity index 93% rename from test/symbolic_indexing_interface.jl rename to lib/ModelingToolkitBase/test/symbolic_indexing_interface.jl index 89b6907f4b..7513bb4665 100644 --- a/test/symbolic_indexing_interface.jl +++ b/lib/ModelingToolkitBase/test/symbolic_indexing_interface.jl @@ -1,7 +1,8 @@ -using ModelingToolkit, SymbolicIndexingInterface, SciMLBase -using ModelingToolkit: t_nounits as t, D_nounits as D, ParameterIndex, +using ModelingToolkitBase, SymbolicIndexingInterface, SciMLBase +using ModelingToolkitBase: t_nounits as t, D_nounits as D, ParameterIndex, SymbolicContinuousCallback using SciMLStructures: Tunable +using Test @testset "System" begin @parameters a b @@ -46,17 +47,17 @@ using SciMLStructures: Tunable @test_nowarn @inferred getter(prob) @named odesys = System( - eqs, t, [x, y], [a, b]; defaults = [xy => 3.0], observed = [xy ~ x + y]) + eqs, t, [x, y], [a, b]; initial_conditions = [xy => 3.0], observed = [xy ~ x + y]) odesys = complete(odesys) @test default_values(odesys)[xy] == 3.0 pobs = parameter_observed(odesys, a + b) @test isempty(get_all_timeseries_indexes(odesys, a + b)) @test pobs( - ModelingToolkit.MTKParameters(odesys, [a => 1.0, b => 2.0]), 0.0) ≈ 3.0 + ModelingToolkitBase.MTKParameters(odesys, [a => 1.0, b => 2.0]), 0.0) ≈ 3.0 pobs = parameter_observed(odesys, [a + b, a - b]) @test isempty(get_all_timeseries_indexes(odesys, [a + b, a - b])) @test pobs( - ModelingToolkit.MTKParameters(odesys, [a => 1.0, b => 2.0]), 0.0) ≈ [3.0, -1.0] + ModelingToolkitBase.MTKParameters(odesys, [a => 1.0, b => 2.0]), 0.0) ≈ [3.0, -1.0] end # @testset "Clock system" begin @@ -113,7 +114,7 @@ end ns = complete(ns) @test SymbolicIndexingInterface.supports_tuple_observed(ns) @test !is_time_dependent(ns) - ps = ModelingToolkit.MTKParameters(ns, [σ => 1.0, ρ => 2.0, β => 3.0]) + ps = ModelingToolkitBase.MTKParameters(ns, [σ => 1.0, ρ => 2.0, β => 3.0]) pobs = parameter_observed(ns, σ + ρ) @test isempty(get_all_timeseries_indexes(ns, σ + ρ)) @test pobs(ps) == 3.0 @@ -208,7 +209,7 @@ end @testset "Parameter dependencies as symbols" begin @variables x(t) = 1.0 @parameters a=1 b - @named model = System([D(x) ~ x + a - b, b ~ a + 1], t) + @named model = System([D(x) ~ x + a - b], t; bindings = [b => a+1]) sys = complete(model) prob = ODEProblem(sys, [], (0.0, 1.0)) @test prob.ps[b] == prob.ps[:b] @@ -227,11 +228,11 @@ end @testset "`timeseries_parameter_index` on unwrapped scalarized timeseries parameter" begin @variables x(t)[1:2] - @parameters p(t)[1:2, 1:2] + @discretes p(t)[1:2, 1:2] ev = SymbolicContinuousCallback( [x[1] ~ 2.0] => [p ~ -ones(2, 2)], discrete_parameters = [p]) @mtkcompile sys = System(D(x) ~ p * x, t; continuous_events = [ev]) - p = ModelingToolkit.unwrap(p) + p = ModelingToolkitBase.unwrap(p) @test timeseries_parameter_index(sys, p) === ParameterTimeseriesIndex(1, (1, 1)) @test timeseries_parameter_index(sys, p[1, 1]) === ParameterTimeseriesIndex(1, (1, 1, 1, 1)) diff --git a/test/test_variable_metadata.jl b/lib/ModelingToolkitBase/test/test_variable_metadata.jl similarity index 53% rename from test/test_variable_metadata.jl rename to lib/ModelingToolkitBase/test/test_variable_metadata.jl index d10e0fdc17..3357087fa0 100644 --- a/test/test_variable_metadata.jl +++ b/lib/ModelingToolkitBase/test/test_variable_metadata.jl @@ -1,15 +1,17 @@ -using ModelingToolkit +using ModelingToolkitBase +using Symbolics: value using DynamicQuantities +using Test # Bounds @variables u [bounds = (-1, 1)] @test getbounds(u) == (-1, 1) @test hasbounds(u) -@test ModelingToolkit.dump_variable_metadata(u).bounds == (-1, 1) +@test ModelingToolkitBase.dump_variable_metadata(u).bounds == (-1, 1) @variables y @test !hasbounds(y) -@test !haskey(ModelingToolkit.dump_variable_metadata(y), :bounds) +@test !haskey(ModelingToolkitBase.dump_variable_metadata(y), :bounds) @variables y[1:3] @test !hasbounds(y) @@ -21,7 +23,7 @@ for i in eachindex(y) @test b[1] == -Inf && b[2] == Inf end -@variables y[1:3] [bounds = (-1, 1)] +@variables y[1:3] [bounds = (-ones(3), ones(3))] @test hasbounds(y) @test getbounds(y)[1] == -ones(3) @test getbounds(y)[2] == ones(3) @@ -33,7 +35,7 @@ end @test getbounds(y[1:2])[1] == -ones(2) @test getbounds(y[1:2])[2] == ones(2) -@variables y[1:2, 1:2] [bounds = (-1, [1.0 Inf; 2.0 3.0])] +@variables y[1:2, 1:2] [bounds = (-ones(2, 2), [1.0 Inf; 2.0 3.0])] @test hasbounds(y) @test getbounds(y)[1] == [-1 -1; -1 -1] @test getbounds(y)[2] == [1.0 Inf; 2.0 3.0] @@ -43,7 +45,7 @@ for i in eachindex(y) @test b[1] == -1 && b[2] == [1.0 Inf; 2.0 3.0][i] end -@variables y[1:2] [bounds = (-Inf, [1.0, Inf])] +@variables y[1:2] [bounds = (-Inf * ones(2), [1.0, Inf])] @test hasbounds(y) @test getbounds(y)[1] == [-Inf, -Inf] @test getbounds(y)[2] == [1.0, Inf] @@ -56,51 +58,51 @@ end @variables y [guess = 0] @test getguess(y) == 0 @test hasguess(y) == true -@test ModelingToolkit.dump_variable_metadata(y).guess == 0 +@test ModelingToolkitBase.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 +@test ModelingToolkitBase.getdefault(y) == 0 +@test ModelingToolkitBase.hasdefault(y) == true +@test ModelingToolkitBase.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) +@test ModelingToolkitBase.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 + @test ModelingToolkitBase.dump_variable_metadata(y[i]).guess == 1.0 end @variables y @test hasguess(y) == false -@test !haskey(ModelingToolkit.dump_variable_metadata(y), :guess) +@test !haskey(ModelingToolkitBase.dump_variable_metadata(y), :guess) # Disturbance @variables u [disturbance = true] @test isdisturbance(u) -@test ModelingToolkit.dump_variable_metadata(u).disturbance +@test ModelingToolkitBase.dump_variable_metadata(u).disturbance @variables y @test !isdisturbance(y) -@test !haskey(ModelingToolkit.dump_variable_metadata(y), :disturbance) +@test !haskey(ModelingToolkitBase.dump_variable_metadata(y), :disturbance) # Tunable @parameters u [tunable = true] @test istunable(u) -@test ModelingToolkit.dump_variable_metadata(u).tunable +@test ModelingToolkitBase.dump_variable_metadata(u).tunable @parameters u2 [tunable = false] @test !istunable(u2) -@test !ModelingToolkit.dump_variable_metadata(u2).tunable +@test !ModelingToolkitBase.dump_variable_metadata(u2).tunable @parameters y @test istunable(y) -@test ModelingToolkit.dump_variable_metadata(y).tunable +@test ModelingToolkitBase.dump_variable_metadata(y).tunable # Distributions struct FakeNormal end @@ -108,11 +110,11 @@ d = FakeNormal() @parameters u [dist = d] @test hasdist(u) @test getdist(u) == d -@test ModelingToolkit.dump_variable_metadata(u).dist == d +@test ModelingToolkitBase.dump_variable_metadata(u).dist == d @parameters y @test !hasdist(y) -@test !haskey(ModelingToolkit.dump_variable_metadata(y), :dist) +@test !haskey(ModelingToolkitBase.dump_variable_metadata(y), :dist) ## System interface @independent_variables t @@ -124,10 +126,10 @@ Dₜ = Differential(t) eqs = [Dₜ(x) ~ (-k2 * x + k * u) / T y ~ x] sys = System(eqs, t, name = :tunable_first_order) -unk_meta = ModelingToolkit.dump_unknowns(sys) +unk_meta = ModelingToolkitBase.dump_unknowns(sys) @test length(unk_meta) == 3 -@test all(iszero, meta.default for meta in unk_meta) -param_meta = ModelingToolkit.dump_parameters(sys) +@test all(iszero, value(meta.default) for meta in unk_meta) +param_meta = ModelingToolkitBase.dump_parameters(sys) @test length(param_meta) == 3 @test all(!haskey(meta, :default) for meta in param_meta) @@ -159,12 +161,12 @@ sp = Set(p) @variables u [description = "This is my input"] @test getdescription(u) == "This is my input" @test hasdescription(u) -@test ModelingToolkit.dump_variable_metadata(u).desc == "This is my input" +@test ModelingToolkitBase.dump_variable_metadata(u).desc == "This is my input" @variables u @test getdescription(u) == "" @test !hasdescription(u) -@test !haskey(ModelingToolkit.dump_variable_metadata(u), :desc) +@test !haskey(ModelingToolkitBase.dump_variable_metadata(u), :desc) @independent_variables t @variables u(t) [description = "A short description of u"] @@ -176,53 +178,55 @@ sp = Set(p) # Defaults, guesses overridden by system, parameter dependencies @variables x(t)=1.0 y(t) [guess = 1.0] @parameters p=2.0 q -@named sys = System([q ~ 2p], t, [x, y], [p, q]; defaults = Dict(x => 2.0, p => 3.0), - guesses = Dict(y => 2.0)) +@named sys = System(Equation[], t, [x, y], [p, q]; + bindings = [q => 2p], initial_conditions = Dict(x => 2.0, p => 3.0), + guesses = Dict(y => 2.0)) sys = complete(sys) -unks_meta = ModelingToolkit.dump_unknowns(sys) -unks_meta = Dict([ModelingToolkit.getname(meta.var) => meta for meta in unks_meta]) -@test unks_meta[:x].default == 2.0 -@test unks_meta[:y].guess == 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) +unks_meta = ModelingToolkitBase.dump_unknowns(sys) +unks_meta = Dict([ModelingToolkitBase.getname(meta.var) => meta for meta in unks_meta]) +@test value(unks_meta[:x].default) == 1.0 +@test value(unks_meta[:x].initial_condition) == 2.0 +@test value(unks_meta[:y].guess) == 2.0 +params_meta = ModelingToolkitBase.dump_parameters(sys) +params_meta = Dict([ModelingToolkitBase.getname(meta.var) => meta for meta in params_meta]) +@test value(params_meta[:p].default) == 2.0 +@test value(params_meta[:p].initial_condition) == 3.0 # Connect @variables x [connect = Flow] @test hasconnect(x) @test getconnect(x) == Flow -@test ModelingToolkit.dump_variable_metadata(x).connect == Flow -x = ModelingToolkit.setconnect(x, ModelingToolkit.Stream) -@test getconnect(x) == ModelingToolkit.Stream +@test ModelingToolkitBase.dump_variable_metadata(x).connect == Flow +x = ModelingToolkitBase.setconnect(x, ModelingToolkitBase.Stream) +@test getconnect(x) == ModelingToolkitBase.Stream struct BadConnect end -@test_throws Exception ModelingToolkit.setconnect(x, BadConnect) +@test_throws Exception ModelingToolkitBase.setconnect(x, BadConnect) # Unit @variables x [unit = u"s"] @test hasunit(x) @test getunit(x) == u"s" -@test ModelingToolkit.dump_variable_metadata(x).unit == u"s" +@test ModelingToolkitBase.dump_variable_metadata(x).unit == u"s" # Misc data @variables x [misc = [:good]] @test hasmisc(x) @test getmisc(x) == [:good] -x = ModelingToolkit.setmisc(x, "okay") +x = ModelingToolkitBase.setmisc(x, "okay") @test getmisc(x) == "okay" # Variable Type @variables x -@test ModelingToolkit.getvariabletype(x) == ModelingToolkit.VARIABLE -@test ModelingToolkit.dump_variable_metadata(x).variable_type == ModelingToolkit.VARIABLE -@test ModelingToolkit.dump_variable_metadata(x).variable_source == :variables -x = ModelingToolkit.toparam(x) -@test ModelingToolkit.getvariabletype(x) == ModelingToolkit.PARAMETER -@test ModelingToolkit.dump_variable_metadata(x).variable_source == :variables +@test ModelingToolkitBase.getvariabletype(x) == ModelingToolkitBase.VARIABLE +@test ModelingToolkitBase.dump_variable_metadata(x).variable_type == ModelingToolkitBase.VARIABLE +@test ModelingToolkitBase.dump_variable_metadata(x).variable_source == :variables +x = ModelingToolkitBase.toparam(x) +@test ModelingToolkitBase.getvariabletype(x) == ModelingToolkitBase.PARAMETER +@test ModelingToolkitBase.dump_variable_metadata(x).variable_source == :variables @parameters y -@test ModelingToolkit.getvariabletype(y) == ModelingToolkit.PARAMETER +@test ModelingToolkitBase.getvariabletype(y) == ModelingToolkitBase.PARAMETER @brownians z -@test ModelingToolkit.getvariabletype(z) == ModelingToolkit.BROWNIAN +@test ModelingToolkitBase.getvariabletype(z) == ModelingToolkitBase.BROWNIAN diff --git a/test/variable_parsing.jl b/lib/ModelingToolkitBase/test/variable_parsing.jl similarity index 58% rename from test/variable_parsing.jl rename to lib/ModelingToolkitBase/test/variable_parsing.jl index 60b4e24d64..eb141be32a 100644 --- a/test/variable_parsing.jl +++ b/lib/ModelingToolkitBase/test/variable_parsing.jl @@ -1,16 +1,17 @@ -using ModelingToolkit +using ModelingToolkitBase using Test -using ModelingToolkit: value, Flow -using SymbolicUtils: FnType +using ModelingToolkitBase: value, Flow +using Symbolics: SSym +using SymbolicUtils: FnType, ShapeVecT @independent_variables t @variables x(t) y(t) # test multi-arg @variables z(t) # test single-arg -x1 = Num(Sym{FnType{Tuple{Any}, Real}}(:x)(value(t))) -y1 = Num(Sym{FnType{Tuple{Any}, Real}}(:y)(value(t))) -z1 = Num(Sym{FnType{Tuple{Any}, Real}}(:z)(value(t))) +x1 = Num(SSym(:x; type = FnType{Tuple, Real, Nothing}, shape = ShapeVecT())(value(t))) +y1 = Num(SSym(:y; type = FnType{Tuple, Real, Nothing}, shape = ShapeVecT())(value(t))) +z1 = Num(SSym(:z; type = FnType{Tuple, Real, Nothing}, shape = ShapeVecT())(value(t))) @test isequal(x1, x) @test isequal(y1, y) @@ -22,16 +23,16 @@ z1 = Num(Sym{FnType{Tuple{Any}, Real}}(:z)(value(t))) end @parameters σ(..) -t1 = Num(Sym{Real}(:t)) -s1 = Num(Sym{Real}(:s)) -σ1 = Num(Sym{FnType{Tuple, Real}}(:σ)) +t1 = Num(SSym(:t; type = Real, shape = ShapeVecT())) +s1 = Num(SSym(:s; type = Real, shape = ShapeVecT())) +σ1 = SSym(:σ; type = FnType{Tuple, Real, Nothing}, shape = ShapeVecT()) @test isequal(t1, t) @test isequal(s1, s) @test isequal(σ1(t), σ(t)) -@test ModelingToolkit.isparameter(t) -@test ModelingToolkit.isparameter(s) -@test ModelingToolkit.isparameter(σ) +@test ModelingToolkitBase.isparameter(t) +@test ModelingToolkitBase.isparameter(s) +@test ModelingToolkitBase.isparameter(σ) @test @macroexpand(@parameters x, y, z(t)) == @macroexpand(@parameters x y z(t)) @test @macroexpand(@variables x, y, z(t)) == @macroexpand(@variables x y z(t)) @@ -43,39 +44,23 @@ s1 = Num(Sym{Real}(:s)) end @parameters σ(..)[1:2] -@test all(ModelingToolkit.isparameter, collect(t)) -@test all(ModelingToolkit.isparameter, collect(s)) -@test all(ModelingToolkit.isparameter, Any[σ(t)[1], σ(t)[2]]) - -# fntype(n, T) = FnType{NTuple{n, Any}, T} -# t1 = Num[Variable{Real}(:t, 1), Variable{Real}(:t, 2)] -# s1 = Num[Variable{Real}(:s, 1, 1) Variable{Real}(:s, 1, 2); -# Variable{Real}(:s, 3, 1) Variable{Real}(:s, 3, 2)] -# σ1 = [Num(Variable{fntype(1, Real)}(:σ, 1)), Num(Variable{fntype(1, Real)}(:σ, 2))] -# @test isequal(t1, collect(t)) -# @test isequal(s1, collect(s)) -# @test isequal(σ1, σ) - -#@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)] -# -#@test isequal(x1, x) +@test all(ModelingToolkitBase.isparameter, collect(t)) +@test all(ModelingToolkitBase.isparameter, collect(s)) +@test all(ModelingToolkitBase.isparameter, Any[σ(t)[1], σ(t)[2]]) @variables a[1:11, 1:2] @variables a() -using Symbolics: value, VariableDefaultValue -using ModelingToolkit: VariableConnectType, VariableUnit, rename -using Unitful +using Symbolics: value, VariableDefaultValue, getdefaultval +using ModelingToolkitBase: VariableConnectType, VariableUnit, rename +using DynamicQuantities vals = [1, 2, 3, 4] -@variables x=1 xs[1:4]=vals ys[1:5]=1 +@variables x=1 xs[1:4]=vals ys[1:5]=ones(5) @test getmetadata(x, VariableDefaultValue) === 1 -@test getmetadata.(collect(xs), (VariableDefaultValue,)) == vals -@test getmetadata.(collect(ys), (VariableDefaultValue,)) == ones(Int, 5) +@test getdefaultval(xs) == vals +@test getdefaultval(ys) == ones(Int, 5) u = u"m^3/s" @variables begin @@ -100,7 +85,7 @@ end y = 2, [connect = Flow] end -@test_throws ErrorException ModelingToolkit.getdefault(x) +@test_throws ErrorException ModelingToolkitBase.getdefault(x) @test !hasmetadata(x, VariableDefaultValue) @test getmetadata(x, VariableConnectType) == Flow @test getmetadata(x, VariableUnit) == u @@ -108,7 +93,7 @@ end @test getmetadata(y, VariableConnectType) == Flow a = rename(value(x), :a) -@test_throws ErrorException ModelingToolkit.getdefault(a) +@test_throws ErrorException ModelingToolkitBase.getdefault(a) @test !hasmetadata(a, VariableDefaultValue) @test getmetadata(a, VariableConnectType) == Flow @test getmetadata(a, VariableUnit) == u @@ -129,6 +114,12 @@ a = rename(value(x), :a) @test getmetadata(p, VariableDefaultValue) == 2 @test !hasmetadata(p, VariableConnectType) @test getmetadata(p, VariableUnit) == u"m" -@test ModelingToolkit.isparameter(p) +@test ModelingToolkitBase.isparameter(p) @test_throws Any (@macroexpand @parameters p=2 [unit = u"m", abc = 2]) + +@testset "Parameters cannot be dependent" begin + @test_throws ["cannot create time-dependent"] @parameters p(t) + @test_throws ["cannot create time-independent"] @discretes p +end + diff --git a/test/variable_scope.jl b/lib/ModelingToolkitBase/test/variable_scope.jl similarity index 85% rename from test/variable_scope.jl rename to lib/ModelingToolkitBase/test/variable_scope.jl index 13b813122e..d843953947 100644 --- a/test/variable_scope.jl +++ b/lib/ModelingToolkitBase/test/variable_scope.jl @@ -1,5 +1,5 @@ -using ModelingToolkit -using ModelingToolkit: SymScope, t_nounits as t, D_nounits as D +using ModelingToolkitBase +using ModelingToolkitBase: SymScope, t_nounits as t, D_nounits as D using Symbolics: arguments, value, getname using Test @@ -28,7 +28,7 @@ eqs = [0 ~ a @named sub1 = System(Equation[], [], [], systems = [sub2]) @named sys = System(Equation[], [], [], systems = [sub1]) -names = ModelingToolkit.getname.(unknowns(sys)) +names = ModelingToolkitBase.getname.(unknowns(sys)) @test :d in names @test Symbol("sub1₊c") in names @test Symbol("sub1₊sub2₊b") in names @@ -37,13 +37,13 @@ names = ModelingToolkit.getname.(unknowns(sys)) @named foo = System(eqs, [a, b, c, d], []) @named bar = System(eqs, [a, b, c, d], []) -@test ModelingToolkit.getname(ModelingToolkit.namespace_expr( - ModelingToolkit.namespace_expr(b, +@test ModelingToolkitBase.getname(ModelingToolkitBase.namespace_expr( + ModelingToolkitBase.namespace_expr(b, foo), bar)) == Symbol("bar₊b") function renamed(nss, sym) - ModelingToolkit.getname(foldr(ModelingToolkit.renamespace, nss, init = sym)) + ModelingToolkitBase.getname(foldr(ModelingToolkitBase.renamespace, nss, init = sym)) end @test renamed([:foo :bar :baz], a) == Symbol("foo₊bar₊baz₊a") @@ -62,7 +62,7 @@ level1 = System(Equation[], t, [], []; name = :level1) ∘ level0 level2 = System(Equation[], t, [], []; name = :level2) ∘ level1 level3 = System(Equation[], t, [], []; name = :level3) ∘ level2 -ps = ModelingToolkit.getname.(parameters(level3)) +ps = ModelingToolkitBase.getname.(parameters(level3)) @test isequal(ps[1], :level2₊level1₊level0₊a) @test isequal(ps[2], :level2₊level1₊b) @@ -75,7 +75,7 @@ ps = ModelingToolkit.getname.(parameters(level3)) arr_p = [ParentScope(xx[1]), xx[2]] arr0 = System(Equation[], t, [], arr_p; name = :arr0) arr1 = System(Equation[], t, [], []; name = :arr1) ∘ arr0 -arr_ps = ModelingToolkit.getname.(parameters(arr1)) +arr_ps = ModelingToolkitBase.getname.(parameters(arr1)) @test isequal(arr_ps[1], Symbol("xx")) @test isequal(arr_ps[2], Symbol("arr0₊xx")) @@ -92,11 +92,10 @@ function Bar(; name, p = 2) end @named bar = Bar() bar = complete(bar) -@test length(parameters(bar)) == 2 -@test sort(getname.(parameters(bar))) == [:foo₊p, :p] -defs = ModelingToolkit.defaults(bar) -@test defs[bar.p] == 2 -@test isequal(defs[bar.foo.p], bar.p) +@test length(parameters(bar)) == 1 +@test getname.(parameters(bar)) == [:p] +@test value(initial_conditions(bar)[bar.p]) == 2 +@test isequal(bindings(bar)[bar.foo.p], bar.p) @testset "Issue#3101" begin @variables x1(t) x2(t) x3(t) x4(t) diff --git a/test/variable_utils.jl b/lib/ModelingToolkitBase/test/variable_utils.jl similarity index 86% rename from test/variable_utils.jl rename to lib/ModelingToolkitBase/test/variable_utils.jl index c088481925..a5ce75f928 100644 --- a/test/variable_utils.jl +++ b/lib/ModelingToolkitBase/test/variable_utils.jl @@ -1,48 +1,41 @@ -using ModelingToolkit, Test -using ModelingToolkit: value, vars, parse_variable +using ModelingToolkitBase, Test +using ModelingToolkitBase: value, parse_variable using SymbolicUtils: <ₑ +import SymbolicUtils as SU @parameters α β δ expr = (((1 / β - 1) + δ) / α)^(1 / (α - 1)) ref = sort([β, δ, α], lt = <ₑ) -sol = sort(Num.(ModelingToolkit.get_variables(expr)), lt = <ₑ) +sol = sort(Num.(ModelingToolkitBase.get_variables(expr)), lt = <ₑ) @test all(x -> x isa Num, sol[i] == ref[i] for i in 1:3) -@test all(simplify ∘ value, sol[i] == ref[i] for i in 1:3) +@test all(isequal(sol[i], ref[i]) for i in 1:3) @parameters γ s = α => γ expr = (((1 / β - 1) + δ) / α)^(1 / (α - 1)) -sol = ModelingToolkit.substitute(expr, s) +sol = ModelingToolkitBase.substitute(expr, s) new = (((1 / β - 1) + δ) / γ)^(1 / (γ - 1)) @test iszero(sol - new) # Continuous -using ModelingToolkit: isdifferential, vars, collect_differential_variables, +using ModelingToolkitBase: isdifferential, collect_differential_variables, collect_ivs @independent_variables t @variables u(t) y(t) D = Differential(t) eq = D(y) ~ u -v = vars(eq) +v = SU.search_variables(eq) @test v == Set([D(y), u]) ov = collect_differential_variables(eq) @test ov == Set(Any[y]) -aov = ModelingToolkit.collect_applied_operators(eq, Differential) +aov = ModelingToolkitBase.collect_applied_operators(eq, Differential) @test aov == Set(Any[D(y)]) ts = collect_ivs([eq]) @test ts == Set([t]) -@testset "vars searching through array of symbolics" begin - fn(x, y) = sum(x) + y - @register_symbolic fn(x::AbstractArray, y) - @variables x y z - res = vars(fn([x, y], z)) - @test length(res) == 3 -end - @testset "parse_variable with iv: $iv" for iv in [t, only(@independent_variables tt)] D = Differential(iv) function Lorenz(; name) @@ -147,7 +140,7 @@ end end @testset "isinitial" begin - t = ModelingToolkit.t_nounits + t = ModelingToolkitBase.t_nounits @variables x(t) z(t)[1:5] @parameters a b c[1:4] @test isinitial(Initial(z)) @@ -162,7 +155,8 @@ end @testset "At" begin @independent_variables u @variables x(t) v(..) w(t)[1:3] - @parameters y z(u, t) r[1:3] + @parameters y r[1:3] + @discretes z(u, t) @test EvalAt(1)(x) isa Num @test isequal(EvalAt(1)(y), y) @@ -179,8 +173,8 @@ end @test isequal(EvalAt(1)(r), r) @test isequal(EvalAt(1)(r[2]), r[2]) - _x = ModelingToolkit.unwrap(x) + _x = ModelingToolkitBase.unwrap(x) @test EvalAt(1)(_x) isa Symbolics.BasicSymbolic - @test only(arguments(EvalAt(1)(_x))) == 1 + @test value(only(arguments(EvalAt(1)(_x)))) == 1 @test EvalAt(1)(D(x)) isa Num end diff --git a/lib/SciCompDSL/LICENSE.md b/lib/SciCompDSL/LICENSE.md new file mode 100644 index 0000000000..9799bffe10 --- /dev/null +++ b/lib/SciCompDSL/LICENSE.md @@ -0,0 +1,22 @@ +The SciCompDSL.jl package is licensed under the MIT "Expat" License: + +Copyright (c) 2018-22: Yingbo Ma, Christopher Rackauckas, Julia Computing, and +contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/SciCompDSL/Project.toml b/lib/SciCompDSL/Project.toml new file mode 100644 index 0000000000..4901faa414 --- /dev/null +++ b/lib/SciCompDSL/Project.toml @@ -0,0 +1,51 @@ +name = "SciCompDSL" +uuid = "91a8cdf1-4ca6-467b-a780-87fda3fff15e" +authors = ["Aayush Sabharwal "] +version = "1.0.0" + +[deps] +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" +SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" +SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" + +[weakdeps] +DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" + +[sources] +ModelingToolkitBase = {subdir = "lib/ModelingToolkitBase"} + +[extensions] +SciCompDSLDynamicQuantitiesExt = "DynamicQuantities" + +[compat] +DocStringExtensions = "0.9.5" +DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" +MLStyle = "0.4.17" +ModelingToolkitBase = "1" +OrderedCollections = "1" +PrecompileTools = "1.2.1" +Setfield = "0.7, 0.8, 1" +SymbolicIndexingInterface = "0.3" +SymbolicUtils = "4" +Symbolics = "7" +URIs = "1" +julia = "1.10" + +[extras] +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["SafeTestsets", "Test", "Pkg", "Distributions", "DynamicQuantities", "OrdinaryDiffEqDefault", "OrdinaryDiffEqTsit5"] diff --git a/lib/SciCompDSL/ext/SciCompDSLDynamicQuantitiesExt.jl b/lib/SciCompDSL/ext/SciCompDSLDynamicQuantitiesExt.jl new file mode 100644 index 0000000000..d633434968 --- /dev/null +++ b/lib/SciCompDSL/ext/SciCompDSLDynamicQuantitiesExt.jl @@ -0,0 +1,52 @@ +module SciCompDSLDynamicQuantitiesExt + +import DynamicQuantities +const DQ = DynamicQuantities + +using ModelingToolkitBase, Symbolics +using ModelingToolkitBase: VariableUnit, setdefault +using SciCompDSL +using SciCompDSL: convert_units, NoValue, NO_VALUE + +import ModelingToolkitBase as MTK + +function SciCompDSL.convert_units(varunits::DynamicQuantities.Quantity, value) + DynamicQuantities.ustrip(DynamicQuantities.uconvert( + DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) +end + +SciCompDSL.convert_units(::DynamicQuantities.Quantity, value::NoValue) = NO_VALUE + +function SciCompDSL.convert_units( + varunits::DynamicQuantities.Quantity, value::AbstractArray{T}) where {T} + DynamicQuantities.ustrip.(DynamicQuantities.uconvert.( + DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) +end + +SciCompDSL.convert_units(::DynamicQuantities.Quantity, value::AbstractArray{Num}) = value + +SciCompDSL.convert_units(::DynamicQuantities.Quantity, value::Num) = value + +function SciCompDSL.__generate_variable_with_unit(metadata_with_exprs, name, vv, def) + unit = metadata_with_exprs[VariableUnit] + return quote + $name = if $name === $(NO_VALUE) + $setdefault($vv, $def) + else + try + $setdefault($vv, $convert_units($unit, $name)) + catch e + if isa(e, $(DynamicQuantities.DimensionError)) + error("Unable to convert units for \'" * string(:($$vv)) * "\'") + elseif isa(e, MethodError) + error("No or invalid units provided for \'" * string(:($$vv)) * + "\'") + else + rethrow(e) + end + end + end + end +end + +end diff --git a/lib/SciCompDSL/src/SciCompDSL.jl b/lib/SciCompDSL/src/SciCompDSL.jl new file mode 100644 index 0000000000..330b34d283 --- /dev/null +++ b/lib/SciCompDSL/src/SciCompDSL.jl @@ -0,0 +1,70 @@ +module SciCompDSL + +using OrderedCollections +using Symbolics +using Symbolics: getname, wrap +using SymbolicUtils: unwrap +import ModelingToolkitBase as MTKBase +using MLStyle +using URIs +using PrecompileTools +using DocStringExtensions +using SymbolicIndexingInterface +import ModelingToolkitBase: observed +using Setfield + +let allnames = names(MTKBase; all = true), + banned_names = Set{Symbol}([:eval, :include, :Variable]) + + using_expr = Expr(:using, Expr(:(:), Expr(:., :ModelingToolkitBase))) + inner_using_expr = using_expr.args[1] + + for name in allnames + name in banned_names && continue + startswith(string(name), '#') && continue + push!(inner_using_expr.args, Expr(:., name)) + end + @eval SciCompDSL $using_expr +end + +using ModelingToolkitBase: COMMON_SENTINEL, COMMON_NOTHING, COMMON_MISSING, + COMMON_TRUE, COMMON_FALSE, COMMON_INF + +@recompile_invalidations begin + include("model_parsing.jl") +end + +export @mtkmodel + +@compile_workload begin + @mtkmodel __testmod__ begin + @constants begin + c = 1.0 + end + @structural_parameters begin + structp = false + end + if structp + @variables begin + x(t) = 0.0, [description = "foo", guess = 1.0] + end + else + @variables begin + x(t) = 0.0, [description = "foo w/o structp", guess = 1.0] + end + end + @parameters begin + a = 1.0, [description = "bar"] + if structp + b = 2 * a, [description = "if"] + else + c + end + end + @equations begin + x ~ a + b + end + end +end + +end # module SciCompDSL diff --git a/src/systems/model_parsing.jl b/lib/SciCompDSL/src/model_parsing.jl similarity index 90% rename from src/systems/model_parsing.jl rename to lib/SciCompDSL/src/model_parsing.jl index 699cfee8fd..192ebbef84 100644 --- a/src/systems/model_parsing.jl +++ b/lib/SciCompDSL/src/model_parsing.jl @@ -1,7 +1,7 @@ """ $(TYPEDEF) -ModelingToolkit component or connector with metadata +ModelingToolkitBase component or connector with metadata # Fields $(FIELDS) @@ -26,19 +26,17 @@ end Base.parentmodule(m::Model) = parentmodule(m.f) -for f in (:connector, :mtkmodel) - isconnector = f == :connector ? true : false - @eval begin - macro $f(fullname::Union{Expr, Symbol}, body) - esc($(:_model_macro)(__module__, fullname, body, $isconnector)) - end - end +function MTKBase.__mtkmodel_connector(mod::Module, fullname::Union{Expr, Symbol}, body) + _model_macro(mod, fullname, body, true) +end +macro mtkmodel(fullname::Union{Expr, Symbol}, body) + esc(_model_macro(__module__, fullname, body, false)) end -flatten_equations(eqs::Vector{Equation}, eq::Equation) = vcat(eqs, [eq]) -flatten_equations(eq::Vector{Equation}, eqs::Vector{Equation}) = vcat(eq, eqs) -function flatten_equations(eqs::Vector{Union{Equation, Vector{Equation}}}) - foldl(flatten_equations, eqs; init = Equation[]) +_flatten_equations(eqs::Vector{Equation}, eq::Equation) = vcat(eqs, [eq]) +_flatten_equations(eq::Vector{Equation}, eqs::Vector{Equation}) = vcat(eq, eqs) +function _flatten_equations(eqs::Vector{Union{Equation, Vector{Equation}}}) + foldl(_flatten_equations, eqs; init = Equation[]) end function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) @@ -73,7 +71,7 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) push!(exprs.args, :(variables = [])) push!(exprs.args, :(parameters = [])) # We build `System` by default - push!(exprs.args, :(systems = ModelingToolkit.AbstractSystem[])) + push!(exprs.args, :(systems = ModelingToolkitBase.System[])) push!(exprs.args, :(equations = Union{Equation, Vector{Equation}}[])) push!(exprs.args, :(defaults = Dict{Num, Union{Number, Symbol, Function}}())) @@ -133,10 +131,10 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) @inline pop_structure_dict!.( Ref(dict), [:defaults, :kwargs, :structural_parameters]) - sys = :($type($(flatten_equations)(equations), $iv, variables, parameters; + sys = :($type($(_flatten_equations)(equations), $iv, variables, parameters; name, description = $description, systems, gui_metadata = $gui_metadata, continuous_events = [$(c_evts...)], discrete_events = [$(d_evts...)], - defaults, costs = [$(costs...)], constraints = [$(cons...)], consolidate = $consolidate)) + __legacy_defaults__ = defaults, costs = [$(costs...)], constraints = [$(cons...)], consolidate = $consolidate)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) @@ -259,6 +257,9 @@ function unit_handled_variable_value(meta, varname) return varval end +no_value_default_to_nothing(::NoValue) = nothing +no_value_default_to_nothing(x) = x + # This function parses various variable/parameter definitions. # # The comments indicate the syntax matched by a block; either when parsed directly @@ -290,7 +291,7 @@ Base.@nospecializeinfer function parse_variable_def!( Expr(:(::), a, type) => begin - type = getfield(mod, type) + type = Core.eval(mod, type) parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; def, type, meta) end @@ -334,19 +335,30 @@ Base.@nospecializeinfer function parse_variable_def!( meta = parse_metadata(mod, meta_val) varval = (@isdefined default_val) ? default_val : unit_handled_variable_value(meta, varname) + isdisc = Meta.isexpr(a, :call) if varclass == :parameters - Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@parameters ($a[$(indices...)]::$type = $varval), - $meta_val)) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc + var = :($varname = $first(@discretes ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $meta_val)) + else + var = :($varname = $first(@parameters ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $meta_val)) + end elseif varclass == :constants - Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@constants ($a[$(indices...)]::$type = $varval), - $meta_val)) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc + var = :($varname = $first(@discretes ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $meta_val)) + else + var = :($varname = $first(@constants ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $meta_val)) + end else Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@variables ($a[$(indices)]::$type = $varval), + var = :($varname = $first(@variables ($a[$(indices)]::$type = $no_value_default_to_nothing($varval)), $meta_val)) end update_array_kwargs_and_metadata!( @@ -365,48 +377,67 @@ Base.@nospecializeinfer function parse_variable_def!( def_n_meta) => begin (@isdefined type) || (type = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a + isdisc = Meta.isexpr(a, :call) if Meta.isexpr(def_n_meta, :tuple) meta = parse_metadata(mod, def_n_meta) varval = unit_handled_variable_value(meta, varname) val, def_n_meta = (def_n_meta.args[1], def_n_meta.args[2:end]) if varclass == :parameters - Meta.isexpr(a, :call) && - assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === $NO_VALUE ? $val : $varname; - $varname = $first(@parameters ($a[$(indices...)]::$type = $varval), - $(def_n_meta...))) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc + var = :($varname = $varname === $NO_VALUE ? $val : $varname; + $varname = $first(@discretes ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $(def_n_meta...))) + else + var = :($varname = $varname === $NO_VALUE ? $val : $varname; + $varname = $first(@parameters ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $(def_n_meta...))) + end elseif varclass == :constants - Meta.isexpr(a, :call) && - assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === $NO_VALUE ? $val : $varname; - $varname = $first(@constants ($a[$(indices...)]::$type = $varval), - $(def_n_meta...))) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc + var = :($varname = $varname === $NO_VALUE ? $val : $varname; + $varname = $first(@discretes ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $(def_n_meta...))) + else + var = :($varname = $varname === $NO_VALUE ? $val : $varname; + $varname = $first(@constants ($a[$(indices...)]::$type = $no_value_default_to_nothing($varval)), + $(def_n_meta...))) + end else Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === $NO_VALUE ? $val : $varname; $varname = $first(@variables $a[$(indices...)]::$type = ( - $varval), + $no_value_default_to_nothing($varval)), $(def_n_meta...))) end else if varclass == :parameters - Meta.isexpr(a, :call) && - assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; - $varname = $first(@parameters $a[$(indices...)]::$type = $varname)) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc + var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; + $varname = $first(@discretes $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + else + var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; + $varname = $first(@parameters $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + end elseif varclass == :constants - Meta.isexpr(a, :call) && - assert_unique_independent_var(dict, a.args[end]) + isdisc && assert_unique_independent_var(dict, a.args[end]) + if isdisc var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; - $varname = $first(@constants $a[$(indices...)]::$type = $varname)) + $varname = $first(@discretes $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + else + var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; + $varname = $first(@constants $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + end else Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; - $varname = $first(@variables $a[$(indices...)]::$type = $varname)) + $varname = $first(@variables $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) end varval, meta = def_n_meta, nothing end @@ -427,17 +458,26 @@ Base.@nospecializeinfer function parse_variable_def!( indices...) => begin (@isdefined type) || (type = Real) varname = a isa Expr && a.head == :call ? a.args[1] : a + isdisc = Meta.isexpr(a, :call) if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@parameters $a[$(indices...)]::$type = $varname)) + if isdisc + var = :($varname = $first(@discretes $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + else + var = :($varname = $first(@parameters $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + end elseif varclass == :constants Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@constants $a[$(indices...)]::$type = $varname)) + if isdisc + var = :($varname = $first(@discretes $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + else + var = :($varname = $first(@constants $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) + end elseif varclass == :variables Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $first(@variables $a[$(indices...)]::$type = $varname)) + var = :($varname = $first(@variables $a[$(indices...)]::$type = $no_value_default_to_nothing($varname))) else throw("Symbolic array with arbitrary length is not handled for $varclass. Please open an issue with an example.") @@ -573,7 +613,7 @@ function get_t(mod, t) 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.") + @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 as t`) from ModelingToolkitBase.") variable(:t) else throw(e) @@ -629,7 +669,7 @@ function _set_var_metadata!(metadata_with_exprs, a, m, v::Expr) a end function _set_var_metadata!(metadata_with_exprs, a, m, v) - wrap(set_scalar_metadata(unwrap(a), m, v)) + wrap(setmetadata(unwrap(a), m, v)) end function set_var_metadata(a, ms) @@ -888,32 +928,8 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ push!(exprs, ex) end -function convert_units(varunits::DynamicQuantities.Quantity, value) - DynamicQuantities.ustrip(DynamicQuantities.uconvert( - DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) -end - -convert_units(::DynamicQuantities.Quantity, value::NoValue) = NO_VALUE - -function convert_units( - varunits::DynamicQuantities.Quantity, value::AbstractArray{T}) where {T} - DynamicQuantities.ustrip.(DynamicQuantities.uconvert.( - DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) -end - -function convert_units(varunits::Unitful.FreeUnits, value) - Unitful.ustrip(varunits, value) -end - -convert_units(::Unitful.FreeUnits, value::NoValue) = NO_VALUE - -function convert_units(varunits::Unitful.FreeUnits, value::AbstractArray{T}) where {T} - Unitful.ustrip.(varunits, value) -end - -convert_units(::Unitful.FreeUnits, value::Num) = value - -convert_units(::DynamicQuantities.Quantity, value::Num) = value +function convert_units end +function __generate_variable_with_unit end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, @@ -922,26 +938,7 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) if !(vv isa Tuple) name = getname(vv) varexpr = if haskey(metadata_with_exprs, VariableUnit) - unit = metadata_with_exprs[VariableUnit] - quote - $name = if $name === $NO_VALUE - $setdefault($vv, $def) - else - try - $setdefault($vv, $convert_units($unit, $name)) - catch e - if isa(e, $(DynamicQuantities.DimensionError)) || - isa(e, $(Unitful.DimensionError)) - error("Unable to convert units for \'" * string(:($$vv)) * "\'") - elseif isa(e, MethodError) - error("No or invalid units provided for \'" * string(:($$vv)) * - "\'") - else - rethrow(e) - end - end - end - end + __generate_variable_with_unit(metadata_with_exprs, name, vv, def) else quote $name = if $name === $NO_VALUE diff --git a/test/icons/ground.svg b/lib/SciCompDSL/test/icons/ground.svg similarity index 100% rename from test/icons/ground.svg rename to lib/SciCompDSL/test/icons/ground.svg diff --git a/test/icons/oneport.png b/lib/SciCompDSL/test/icons/oneport.png similarity index 100% rename from test/icons/oneport.png rename to lib/SciCompDSL/test/icons/oneport.png diff --git a/test/icons/pin.png b/lib/SciCompDSL/test/icons/pin.png similarity index 100% rename from test/icons/pin.png rename to lib/SciCompDSL/test/icons/pin.png diff --git a/test/icons/resistor.svg b/lib/SciCompDSL/test/icons/resistor.svg similarity index 100% rename from test/icons/resistor.svg rename to lib/SciCompDSL/test/icons/resistor.svg diff --git a/test/model_parsing.jl b/lib/SciCompDSL/test/model_parsing.jl similarity index 85% rename from test/model_parsing.jl rename to lib/SciCompDSL/test/model_parsing.jl index 6a3e29fda4..c8f471e581 100644 --- a/test/model_parsing.jl +++ b/lib/SciCompDSL/test/model_parsing.jl @@ -1,20 +1,21 @@ -using ModelingToolkit, Symbolics, Test -using ModelingToolkit: get_connector_type, get_defaults, get_gui_metadata, +using ModelingToolkitBase, Symbolics, Test +using ModelingToolkitBase: get_connector_type, get_initial_conditions, get_gui_metadata, get_systems, get_ps, getdefault, getname, readable_code, scalarize, symtype, VariableDescription, RegularConnector, - get_unit + get_unit, value using SymbolicIndexingInterface using URIs: URI using Distributions -using DynamicQuantities, OrdinaryDiffEq -using ModelingToolkit: t, D +using DynamicQuantities, OrdinaryDiffEqDefault, OrdinaryDiffEqTsit5 +using SciCompDSL +using ModelingToolkitBase: t, D ENV["MTK_ICONS_DIR"] = "$(@__DIR__)/icons" # Mock module used to test if the `@mtkmodel` macro works with fully-qualified names as well. module MyMockModule -using ModelingToolkit, DynamicQuantities -using ModelingToolkit: t, D +using ModelingToolkitBase, DynamicQuantities, SciCompDSL +using ModelingToolkitBase: t, D export Pin @connector Pin begin @@ -155,10 +156,10 @@ res__R = 100u"Ω" @mtkcompile rc = RC(; C_val, R_val, resistor.R = res__R) prob = ODEProblem(rc, [], (0, 1e9)) sol = solve(prob) -defs = ModelingToolkit.defaults(rc) -@test sol[rc.capacitor.v, end] ≈ defs[rc.constant.k] +defs = ModelingToolkitBase.initial_conditions(rc) +@test sol[rc.capacitor.v, end] ≈ value(defs[rc.constant.k]) resistor = getproperty(rc, :resistor; namespace = false) -@test ModelingToolkit.description(rc) == "An RC circuit." +@test ModelingToolkitBase.description(rc) == "An RC circuit." @test getname(rc.resistor) === getname(resistor) @test getname(rc.resistor.R) === getname(resistor.R) @test getname(rc.resistor.v) === getname(resistor.v) @@ -179,15 +180,15 @@ resistor = getproperty(rc, :resistor; namespace = false) URI("https://upload.wikimedia.org/wikipedia/commons/7/78/Capacitor_symbol.svg") @test OnePort.structure[:icon] == URI("file:///" * abspath(ENV["MTK_ICONS_DIR"], "oneport.png")) -@test ModelingToolkit.get_gui_metadata(rc.resistor.p).layout == Pin.structure[:icon] == +@test ModelingToolkitBase.get_gui_metadata(rc.resistor.p).layout == Pin.structure[:icon] == URI("file:///" * abspath(ENV["MTK_ICONS_DIR"], "pin.png")) -@test length(equations(rc)) == 1 +@test length(equations(rc)) == 1 broken=!@isdefined(ModelingToolkit) @testset "Constants" begin @mtkmodel PiModel begin @constants begin - _p::Irrational = π, [description = "Value of Pi.", unit = u"V"] + _p = π, [description = "Value of Pi.", unit = u"V"] end @parameters begin p = _p, [description = "Assign constant `_p` value."] @@ -201,8 +202,7 @@ resistor = getproperty(rc, :resistor; namespace = false) @named pi_model = PiModel() - @test typeof(ModelingToolkit.getdefault(pi_model.p)) <: - SymbolicUtils.BasicSymbolic{Irrational} + @test symtype(ModelingToolkitBase.getdefault(pi_model.p)) <: Real @test getdefault(getdefault(pi_model.p)) == π end @@ -218,7 +218,7 @@ end kval c(t) = cval + jval d = 2 - d2[1:2] = 2 + d2[1:2] = 2ones(2) e, [description = "e"] e2[1:2], [description = "e2"] f = 3, [description = "f"] @@ -227,7 +227,7 @@ end i(t) = 4, [description = "i(t)"] j(t) = jval, [description = "j(t)"] k = kval, [description = "k"] - l(t)[1:2, 1:3] = 2, [description = "l is more than 1D"] + l(t)[1:2, 1:3] = 2ones(2, 3), [description = "l is more than 1D"] n # test defaults with Number input n2 # test defaults with Function input end @@ -258,7 +258,7 @@ end @test hasmetadata(model.i, VariableDescription) @test hasmetadata(model.j, VariableDescription) @test hasmetadata(model.k, VariableDescription) - @test all(collect(hasmetadata.(model.l, ModelingToolkit.VariableDescription))) + @test hasmetadata(model.l, ModelingToolkitBase.VariableDescription) @test all(lastindex.([model.a2, model.b2, model.d2, model.e2, model.h2]) .== 2) @test size(model.l) == (2, 3) @@ -275,8 +275,8 @@ end @test all(getdefault.(scalarize(model.l)) .== 2) @test isequal(getdefault(model.j), model.jval) @test isequal(getdefault(model.k), model.kval) - @test get_defaults(model)[model.n] == 1.0 - @test get_defaults(model)[model.n2] == 5 + @test value(get_initial_conditions(model)[model.n]) == 1.0 + @test value(get_initial_conditions(model)[model.n2]) == 5 @test MockModel.structure[:defaults] == Dict(:n => 1.0, :n2 => "g()") end @@ -288,25 +288,25 @@ end M end @parameters begin - (l(t)[1:2, 1:3] = 1), [description = "l is more than 1D"] - (l2(t)[1:N, 1:M] = 2), + (l(t)[1:2, 1:3] = ones(2, 3)), [description = "l is more than 1D"] + (l2(t)[1:N, 1:M] = 2ones(N, M)), [description = "l is more than 1D, with arbitrary length"] - (l3(t)[1:3] = 3), [description = "l2 is 1D"] - (l4(t)[1:N] = 4), [description = "l2 is 1D, with arbitrary length"] - (l5(t)[1:3]::Int = 5), [description = "l3 is 1D and has a type"] - (l6(t)[1:N]::Int = 6), + (l3(t)[1:3] = 3ones(3)), [description = "l2 is 1D"] + (l4(t)[1:N] = 4ones(N)), [description = "l2 is 1D, with arbitrary length"] + (l5(t)[1:3]::Int = 5ones(Int, 3)), [description = "l3 is 1D and has a type"] + (l6(t)[1:N]::Int = 6ones(Int, N)), [description = "l3 is 1D and has a type, with arbitrary length"] end end N, M = 4, 5 @named arr = TupleInArrayDef(; N, M) - @test getdefault(arr.l) == 1 - @test getdefault(arr.l2) == 2 - @test getdefault(arr.l3) == 3 - @test getdefault(arr.l4) == 4 - @test getdefault(arr.l5) == 5 - @test getdefault(arr.l6) == 6 + @test getdefault(arr.l) == ones(2, 3) + @test getdefault(arr.l2) == 2ones(N, M) + @test getdefault(arr.l3) == 3ones(3) + @test getdefault(arr.l4) == 4ones(N) + @test getdefault(arr.l5) == 5ones(Int, 3) + @test getdefault(arr.l6) == 6ones(Int, N) @test size(arr.l2) == (N, M) @test size(arr.l4) == (N,) @@ -327,7 +327,7 @@ end par4(t)::Float64 = 1 # converts 1 to 1.0 of Float64 type par5[1:3]::BigFloat par6(t)[1:3]::BigFloat - par7(t)[1:3, 1:3]::BigFloat = 1.0, [description = "with description"] + par7(t)[1:3, 1:3]::BigFloat = ones(BigFloat, 3, 3), [description = "with description"] end end @@ -467,7 +467,7 @@ end @test A.structure[:components] == [[:cc, :C]] end -using ModelingToolkit: D_nounits +using ModelingToolkitBase: D_nounits @testset "Event handling in MTKModel" begin @mtkmodel M begin @variables begin @@ -476,7 +476,7 @@ using ModelingToolkit: D_nounits z(t) end @equations begin - x ~ -D_nounits(x) + D_nounits(x) ~ -x D_nounits(y) ~ 0 D_nounits(z) ~ 0 end @@ -502,8 +502,8 @@ end # `Expr` type and `unit` metadata can be precompiled. module PrecompilationTest push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) -using Unitful, Test, ModelParsingPrecompile, ModelingToolkit -using ModelingToolkit: getdefault, scalarize +using DynamicQuantities, Test, ModelParsingPrecompile, ModelingToolkitBase +using ModelingToolkitBase: getdefault, scalarize @testset "Precompile packages with MTKModels" begin using ModelParsingPrecompile: ModelWithComponentArray @@ -762,7 +762,7 @@ end @named component = Component() component = complete(component; flatten = false) - @test nameof.(ModelingToolkit.get_systems(component)) == [ + @test nameof.(ModelingToolkitBase.get_systems(component)) == [ :comprehension_1, :comprehension_2, :written_out_for_1, @@ -821,7 +821,7 @@ end j_guess = getguess(guess_model.j) @test symbolic_type(j_guess) == ScalarSymbolic() - @test readable_code(j_guess) == "l(t) / i(t) + k(t)" + @test readable_code(j_guess) in ["k(t) + l(t) / i(t)", "l(t) / i(t) + k(t)"] i_guess = getguess(guess_model.i) @test symbolic_type(i_guess) == ScalarSymbolic() @@ -845,15 +845,15 @@ end end @named ordermodel = OrderModel() ordermodel = complete(ordermodel) - defs = ModelingToolkit.defaults(ordermodel) - @test defs[ordermodel.c] == 1 - @test defs[ordermodel.d] == 1 + defs = ModelingToolkitBase.initial_conditions(ordermodel) + @test value(defs[ordermodel.c]) == 1 + @test value(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 + defs = ModelingToolkitBase.initial_conditions(ordermodel) + @test value(defs[ordermodel.c]) == 2 + @test value(defs[ordermodel.d]) == 1 end @testset "Vector defaults" begin @@ -893,7 +893,7 @@ end @testset "Duplicate names" begin mod = @__MODULE__ - @test_throws ErrorException ModelingToolkit._model_macro(mod, :ATest, + @test_throws ErrorException SciCompDSL._model_macro(mod, :ATest, :(begin @variables begin a(t) @@ -901,7 +901,7 @@ end end end), false) - @test_throws ErrorException ModelingToolkit._model_macro(mod, :ATest, + @test_throws ErrorException SciCompDSL._model_macro(mod, :ATest, :(begin @variables begin a(t) @@ -990,7 +990,7 @@ struct CustomStruct end end end @named sys = MyModel(p = CustomStruct()) - @test ModelingToolkit.defaults(sys)[@nonamespace sys.p] == CustomStruct() + @test value(ModelingToolkitBase.initial_conditions(sys)[@nonamespace sys.p]) == CustomStruct() end @testset "Variables are not callable symbolics" begin @@ -1007,7 +1007,7 @@ end vars = Symbolics.get_variables(only(equations(ex))) @test length(vars) == 2 for u in Symbolics.unwrap.(unknowns(ex)) - @test !Symbolics.hasmetadata(u, Symbolics.CallWithParent) + @test !SymbolicUtils.is_function_symbolic(u) @test any(isequal(u), vars) end end @@ -1035,18 +1035,21 @@ end @named ex = Example() ex = complete(ex) - costs = ModelingToolkit.get_costs(ex) - constrs = ModelingToolkit.get_constraints(ex) + costs = ModelingToolkitBase.get_costs(ex) + constrs = ModelingToolkitBase.get_constraints(ex) @test isequal(costs[1], ex.x + ex.y) @test isequal(costs[2], EvalAt(1)(ex.y)^2) @test isequal(constrs[1], EvalAt(0.3)(ex.x) ~ 3) @test isequal(constrs[2], ex.y ≲ 4) - @test ModelingToolkit.get_consolidate(ex)([1, 2], [3, 4]) ≈ 8 + log(2) + @test ModelingToolkitBase.get_consolidate(ex)([1, 2], [3, 4]) ≈ 8 + log(2) @test Example.structure[:constraints] == ["(EvalAt(0.3))(x) ~ 3", "y ≲ 4"] @test Example.structure[:costs] == ["x + y", "(EvalAt(1))(y) ^ 2"] end @testset "Model Level Metadata" begin + t = ModelingToolkitBase.t_nounits + D = ModelingToolkitBase.D_nounits + @show getmetadata(t.val, ModelingToolkitBase.VariableUnit, nothing) struct Author end struct MyVersion end struct License end @@ -1083,15 +1086,15 @@ end @named test_model = TestMetadataModel() struct UnknownMetaKey end - @test ModelingToolkit.getmetadata(test_model, Author, nothing) == "Test Author" - @test ModelingToolkit.getmetadata(test_model, MyVersion, nothing) == "1.0.0" - @test ModelingToolkit.getmetadata(test_model, UnknownMetaKey, nothing) === nothing - @test ModelingToolkit.getmetadata(test_model, MyBool, nothing) === false - @test ModelingToolkit.getmetadata(test_model, NewInt, nothing) === 1 + @test ModelingToolkitBase.getmetadata(test_model, Author, nothing) == "Test Author" + @test ModelingToolkitBase.getmetadata(test_model, MyVersion, nothing) == "1.0.0" + @test ModelingToolkitBase.getmetadata(test_model, UnknownMetaKey, nothing) === nothing + @test ModelingToolkitBase.getmetadata(test_model, MyBool, nothing) === false + @test ModelingToolkitBase.getmetadata(test_model, NewInt, nothing) === 1 end @testset "Pass parameters of higher level models as structural parameters" begin - let D=ModelingToolkit.D_nounits, t=ModelingToolkit.t_nounits + let D=ModelingToolkitBase.D_nounits, t=ModelingToolkitBase.t_nounits """ ╭─────────╮ in │ K │ out @@ -1154,24 +1157,51 @@ end end end - @mtkbuild sys = ClosedSystem() + @named sys = ClosedSystem() @test length(parameters(sys)) == 4 - @test length(unknowns(sys)) == 2 - - p = MTKParameters(sys, defaults(sys)) - u = [0.5 for i in 1:2] - du = zeros(2) - # update du for given u and p - ODEFunction(sys).f.f_iip(du, u, p, 0.0) - - # find indices of lag1 and lag2 states (might be reordered due to simplification details) - symnames = string.(ModelingToolkit.getname.(variable_symbols(sys))) - lag1idx = findall(contains("1"), symnames) |> only - lag2idx = findall(contains("2"), symnames) |> only - - # check du values - K1, K2, T1, T2 = 1, 2, 0.1, 0.2 - @test du[lag1idx] ≈ (K1*1.0 - u[lag1idx]) / T1 - @test du[lag2idx] ≈ (K2*u[lag1idx] - u[lag2idx]) / T2 + + if @isdefined(ModelingToolkit) + @mtkbuild sys = ClosedSystem() + @test length(parameters(sys)) == 4 + @test length(unknowns(sys)) == 2 + + p = MTKParameters(sys, merge(initial_conditions(sys), bindings(sys))) + u = [0.5 for i in 1:2] + du = zeros(2) + # update du for given u and p + ODEFunction(sys).f.f_iip(du, u, p, 0.0) + + # find indices of lag1 and lag2 states (might be reordered due to simplification details) + symnames = string.(ModelingToolkitBase.getname.(variable_symbols(sys))) + lag1idx = findall(contains("1"), symnames) |> only + lag2idx = findall(contains("2"), symnames) |> only + + # check du values + K1, K2, T1, T2 = 1, 2, 0.1, 0.2 + @test du[lag1idx] ≈ (K1*1.0 - u[lag1idx]) / T1 + @test du[lag2idx] ≈ (K2*u[lag1idx] - u[lag2idx]) / T2 + end end end + +@testset "Scope of defaults in the systems generated by @named" begin + @mtkmodel MoreThanOneArg begin + @variables begin + x(t) + y(t) + z(t) + end + end + + @parameters begin + l + m + n + end + + @named model = MoreThanOneArg(x = l, y = m, z = n) + + @test getmetadata(getdefault(model.x), SymScope) == ParentScope(LocalScope()) + @test getmetadata(getdefault(model.y), SymScope) == ParentScope(LocalScope()) + @test getmetadata(getdefault(model.z), SymScope) == ParentScope(LocalScope()) +end diff --git a/test/precompile_test/ModelParsingPrecompile.jl b/lib/SciCompDSL/test/precompile_test/ModelParsingPrecompile.jl similarity index 52% rename from test/precompile_test/ModelParsingPrecompile.jl rename to lib/SciCompDSL/test/precompile_test/ModelParsingPrecompile.jl index 87177d519f..01bf0a07f3 100644 --- a/test/precompile_test/ModelParsingPrecompile.jl +++ b/lib/SciCompDSL/test/precompile_test/ModelParsingPrecompile.jl @@ -1,14 +1,15 @@ module ModelParsingPrecompile -using ModelingToolkit, Unitful -using ModelingToolkit: t +using ModelingToolkitBase, DynamicQuantities +using ModelingToolkitBase: t +using SciCompDSL @mtkmodel ModelWithComponentArray begin @constants begin k = 1, [description = "Default val of R"] end @parameters begin - r(t)[1:3] = k, [description = "Parameter array", unit = u"Ω"] + r(t)[1:3] = [k, k, k], [description = "Parameter array", unit = u"Ω"] end end diff --git a/lib/SciCompDSL/test/runtests.jl b/lib/SciCompDSL/test/runtests.jl new file mode 100644 index 0000000000..aee0e76610 --- /dev/null +++ b/lib/SciCompDSL/test/runtests.jl @@ -0,0 +1,16 @@ +using SafeTestsets, Pkg, Test + +const MTKBasePath = joinpath(dirname(dirname(@__DIR__)), "ModelingToolkitBase") +const MTKBasePkgSpec = PackageSpec(; path = MTKBasePath) + +const MTKPath = dirname(dirname(dirname(@__DIR__))) +const MTKPkgSpec = PackageSpec(; path = MTKPath) + +Pkg.develop([MTKBasePkgSpec, MTKPkgSpec]) + +@safetestset "Model parsing - MTKBase" include("model_parsing.jl") +@safetestset "Model parsing - MTK" begin + using ModelingToolkit + import ModelingToolkitBase + include("model_parsing.jl") +end diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 4a7909e41e..2818e3f0b8 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -6,28 +6,29 @@ using PrecompileTools, Reexport @recompile_invalidations begin using StaticArrays using Symbolics + # ONLY here for the invalidations + import REPL end import SymbolicUtils +import SymbolicUtils as SU import SymbolicUtils: iscall, arguments, operation, maketerm, promote_symtype, - Symbolic, isadd, ismul, ispow, issym, FnType, + isadd, ismul, ispow, issym, FnType, isconst, BSImpl, @rule, Rewriters, substitute, metadata, BasicSymbolic, - Sym, Term + symtype using SymbolicUtils.Code import SymbolicUtils.Code: toexpr import SymbolicUtils.Rewriters: Chain, Postwalk, Prewalk, Fixpoint using DocStringExtensions -using SpecialFunctions, NaNMath -using DiffEqCallbacks +@recompile_invalidations begin + using DiffEqBase, SciMLBase, ForwardDiff +end using Graphs -import ExprTools: splitdef, combinedef import OrderedCollections -using DiffEqNoiseProcess: DiffEqNoiseProcess, WienerProcess using SymbolicIndexingInterface using LinearAlgebra, SparseArrays using InteractiveUtils -using JumpProcesses using DataStructures @static if pkgversion(DataStructures) >= v"0.19" import DataStructures: IntDisjointSet @@ -36,368 +37,138 @@ else const IntDisjointSet = IntDisjointSets end using Base.Threads -using Latexify, Unitful, ArrayInterface -using Setfield, ConstructionBase +using Setfield import Libdl using DocStringExtensions using Base: RefValue using Combinatorics -import Distributions -import FunctionWrappersWrappers -import FunctionWrappers: FunctionWrapper -using URIs: URI -using SciMLStructures -using Compat -using AbstractTrees -using DiffEqBase, SciMLBase, ForwardDiff using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain, PeriodicClock, Clock, SolverStepClock, ContinuousClock, OverrideInit, NoInit -using Distributed -import JuliaFormatter -using MLStyle import Moshi using Moshi.Data: @data import SCCNonlinearSolve -using ImplicitDiscreteSolve using Reexport -using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix -import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!, - undef_blocks, blocks using OffsetArrays: Origin import CommonSolve -import EnumX -import ChainRulesCore -import ChainRulesCore: Tangent, ZeroTangent, NoTangent, zero_tangent, unthunk using RuntimeGeneratedFunctions using RuntimeGeneratedFunctions: drop_expr -using Symbolics: degree -using Symbolics: _parse_vars, value, @derivatives, get_variables, - exprs_occur_in, symbolic_linear_solve, build_expr, unwrap, wrap, - VariableSource, getname, variable, - NAMESPACE_SEPARATOR, set_scalar_metadata, setdefaultval, - hasnode, fixpoint_sub, fast_substitute, - CallWithMetadata, CallWithParent +using Symbolics: degree, VartypeT, SymbolicT +using Symbolics: parse_vars, value, @derivatives, get_variables, + exprs_occur_in, symbolic_linear_solve, unwrap, wrap, + VariableSource, getname, variable, COMMON_ZERO, + NAMESPACE_SEPARATOR, setdefaultval, Arr, + hasnode, fixpoint_sub, CallAndWrap, SArgsT, SSym, STerm 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, + rhss, lhss, gradient, jacobian, hessian, derivative, sparsejacobian, sparsehessian, - substituter, scalarize, getparent, hasderiv, hasdiff + scalarize, hasderiv +import ModelingToolkitBase as MTKBase import DiffEqBase: @add_kwonly -export independent_variables, unknowns, observables, parameters, full_parameters, - continuous_events, discrete_events @reexport using Symbolics @reexport using UnPack +@reexport using ModelingToolkitBase RuntimeGeneratedFunctions.init(@__MODULE__) -import DynamicQuantities, Unitful -const DQ = DynamicQuantities - import DifferentiationInterface as DI using ADTypes: AutoForwardDiff import SciMLPublic: @public import PreallocationTools import PreallocationTools: DiffCache import FillArrays +using BipartiteGraphs +import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!, + undef_blocks, blocks -export @derivatives +@recompile_invalidations begin + import StateSelection + import StateSelection: CLIL + import ModelingToolkitTearing as MTKTearing + using ModelingToolkitTearing: TearingState, SystemStructure -for fun in [:toexpr] - @eval begin - function $fun(eq::Equation; kw...) - Expr(:call, :(==), $fun(eq.lhs; kw...), $fun(eq.rhs; kw...)) - end + ModelingToolkitBase.complete(dg::StateSelection.DiffGraph) = BipartiteGraphs.complete(dg) +end - function $fun(ineq::Inequality; kw...) - if ineq.relational_op == Symbolics.leq - Expr(:call, :(<=), $fun(ineq.lhs; kw...), $fun(ineq.rhs; kw...)) - else - Expr(:call, :(>=), $fun(ineq.lhs; kw...), $fun(ineq.rhs; kw...)) - end +macro import_mtkbase() + allnames = names(MTKBase; all = true) + banned_names = Set{Symbol}([:eval, :include, :Variable]) + using_expr = Expr(:using, Expr(:(:), Expr(:., :ModelingToolkitBase))) + inner_using_expr = using_expr.args[1] + + public_expr = :(@public) + inner_public_expr = Expr(:tuple) + push!(public_expr.args, inner_public_expr) + + for name in allnames + name in banned_names && continue + startswith(string(name), '#') && continue + push!(inner_using_expr.args, Expr(:., name)) + if Base.ispublic(MTKBase, name) && !Base.isexported(MTKBase, name) + push!(inner_public_expr.args, name) end + end - $fun(eqs::AbstractArray; kw...) = map(eq -> $fun(eq; kw...), eqs) - $fun(x::Integer; kw...) = x - $fun(x::AbstractFloat; kw...) = x + quote + $using_expr + $(esc(public_expr)) end end -const INTERNAL_FIELD_WARNING = """ -This field is internal API. It may be removed or changed without notice in a non-breaking \ -release. Usage of this field is not advised. -""" - -const INTERNAL_ARGS_WARNING = """ -The following arguments are internal API. They may be removed or changed without notice \ -in a non-breaking release. Usage of these arguments is not advised. -""" - -""" -$(TYPEDEF) - -Abstract supertype of all system types. Any custom system types must subtype this. -""" -abstract type AbstractSystem end -# Solely so that `ODESystem` can be deprecated and still act as a valid type. -# See `deprecations.jl`. -abstract type IntermediateDeprecationSystem <: AbstractSystem end - -function independent_variable end - -# this has to be included early to deal with dependency issues -include("structural_transformation/bareiss.jl") -function complete end -function var_derivative! end -function var_derivative_graph! end -include("bipartite_graph.jl") -using .BipartiteGraphs - -export EvalAt -include("variables.jl") -include("parameters.jl") -include("independent_variables.jl") -include("constants.jl") - -include("utils.jl") - -include("systems/index_cache.jl") -include("systems/parameter_buffer.jl") -include("systems/abstractsystem.jl") -include("systems/model_parsing.jl") -include("systems/connectiongraph.jl") -include("systems/connectors.jl") -include("systems/state_machines.jl") -include("systems/analysis_points.jl") -include("systems/imperative_affect.jl") -include("systems/callbacks.jl") -include("systems/system.jl") -include("systems/codegen_utils.jl") -include("problems/docs.jl") -include("systems/codegen.jl") -include("systems/problem_utils.jl") -include("linearization.jl") -include("systems/solver_nlprob.jl") - -include("problems/compatibility.jl") -include("problems/odeproblem.jl") -include("problems/ddeproblem.jl") -include("problems/daeproblem.jl") -include("problems/sdeproblem.jl") -include("problems/sddeproblem.jl") -include("problems/nonlinearproblem.jl") -include("problems/intervalnonlinearproblem.jl") -include("problems/implicitdiscreteproblem.jl") -include("problems/discreteproblem.jl") -include("problems/optimizationproblem.jl") -include("problems/jumpproblem.jl") -include("problems/initializationproblem.jl") -include("problems/sccnonlinearproblem.jl") -include("problems/bvproblem.jl") -include("problems/linearproblem.jl") - -include("modelingtoolkitize/common.jl") -include("modelingtoolkitize/odeproblem.jl") -include("modelingtoolkitize/sdeproblem.jl") -include("modelingtoolkitize/optimizationproblem.jl") -include("modelingtoolkitize/nonlinearproblem.jl") - -include("systems/nonlinear/homotopy_continuation.jl") -include("systems/nonlinear/initializesystem.jl") -include("systems/diffeqs/basic_transformations.jl") - -include("systems/pde/pdesystem.jl") +@import_mtkbase -include("systems/sparsematrixclil.jl") +using ModelingToolkitBase: COMMON_SENTINEL, COMMON_NOTHING, COMMON_MISSING, + COMMON_TRUE, COMMON_FALSE, COMMON_INF -include("systems/unit_check.jl") -include("systems/validation.jl") -include("systems/dependency_graphs.jl") -include("clock.jl") -include("discretedomain.jl") -include("systems/systemstructure.jl") -include("systems/clock_inference.jl") -include("systems/systems.jl") -include("systems/if_lifting.jl") - -include("debugging.jl") -include("systems/alias_elimination.jl") -include("structural_transformation/StructuralTransformations.jl") - -@reexport using .StructuralTransformations -include("inputoutput.jl") - -include("adjoints.jl") -include("deprecations.jl") - -const t_nounits = let - only(@independent_variables t) -end -const t_unitful = let - only(@independent_variables t [unit = Unitful.u"s"]) -end -const t = let - only(@independent_variables t [unit = DQ.u"s"]) +@recompile_invalidations begin + include("linearization.jl") + include("systems/analysis_points.jl") + include("systems/solver_nlprob.jl") + + include("problems/docs.jl") + include("systems/codegen.jl") + include("problems/semilinearodeproblem.jl") + include("problems/sccnonlinearproblem.jl") + + include("discretedomain.jl") + include("systems/systemstructure.jl") + include("initialization.jl") + include("systems/systems.jl") + include("systems/clock_inference.jl") + include("systems/if_lifting.jl") + include("systems/substitute_component.jl") + + include("systems/alias_elimination.jl") + include("structural_transformation/StructuralTransformations.jl") end -const D_nounits = Differential(t_nounits) -const D_unitful = Differential(t_unitful) -const D = Differential(t) +@reexport using .StructuralTransformations -export ODEFunction, convert_system_indepvar, - System, OptimizationSystem, JumpSystem, SDESystem, NonlinearSystem, ODESystem -export SDEFunction -export SystemStructure -export DiscreteProblem, DiscreteFunction -export ImplicitDiscreteProblem, ImplicitDiscreteFunction -export ODEProblem, SDEProblem -export NonlinearFunction -export NonlinearProblem -export IntervalNonlinearFunction -export IntervalNonlinearProblem -export OptimizationProblem, constraints -export SteadyStateProblem -export JumpProblem export SemilinearODEFunction, SemilinearODEProblem -export alias_elimination, flatten -export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream, - instream -export initial_state, transition, activeState, entry, ticksInState, timeInState -export @component, @mtkmodel, @mtkcompile, @mtkbuild -export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance, - istunable, getdist, hasdist, - tunable_parameters, isirreducible, getdescription, hasdescription, - hasunit, getunit, hasconnect, getconnect, - hasmisc, getmisc, state_priority, - subset_tunables -export liouville_transform, change_independent_variable, substitute_component, - add_accumulations, noise_to_brownians, Girsanov_transform, change_of_variables, - fractional_to_ordinary, linear_fractional_to_ordinary -export respecialize -export PDESystem -export Differential, expand_derivatives, @derivatives -export Equation, ConstrainedEquation -export Term, Sym -export SymScope, LocalScope, ParentScope, GlobalScope -export independent_variable, equations, observed, full_equations, jumps, cost, - brownians -export initialization_equations, guesses, defaults, parameter_dependencies, hierarchy -export mtkcompile, expand_connections, linearize, linearization_function, - LinearizationProblem, linearization_ap_transform, structural_simplify +export alias_elimination +export linearize, linearization_function, + LinearizationProblem, linearization_ap_transform export solve -export Pre +export map_variables_to_equations, substitute_component -export calculate_jacobian, generate_jacobian, generate_rhs, generate_custom_function, - generate_W, calculate_hessian -export calculate_control_jacobian, generate_control_jacobian -export calculate_tgrad, generate_tgrad -export generate_cost, calculate_cost_gradient, generate_cost_gradient -export calculate_cost_hessian, generate_cost_hessian -export calculate_massmatrix, generate_diffusion_function -export stochastic_integral_transform export TearingState -export BipartiteGraph, equation_dependencies, variable_dependencies -export eqeq_dependencies, varvar_dependencies -export asgraph, asdigraph -export map_variables_to_equations - -export toexpr, get_variables -export simplify, substitute -export build_function -export modelingtoolkitize -export generate_initializesystem, Initial, isinitial, InitializationProblem - -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, @brownians, @brownian -export @named, @nonamespace, @namespace, extend, compose, complete, toggle_namespacing -export debug_system - -#export ContinuousClock, 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, sampletime, SampleTime export Clock, SolverStepClock, TimeDomain +export get_sensitivity_function, get_comp_sensitivity_function, + get_looptransfer_function, get_sensitivity, get_comp_sensitivity, get_looptransfer -export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables - -export HomotopyContinuationProblem - -export AnalysisPoint, get_sensitivity_function, get_comp_sensitivity_function, - get_looptransfer_function, get_sensitivity, get_comp_sensitivity, get_looptransfer, - open_loop function FMIComponent end -include("systems/optimal_control_interface.jl") -export AbstractDynamicOptProblem, JuMPDynamicOptProblem, InfiniteOptDynamicOptProblem, - CasADiDynamicOptProblem, PyomoDynamicOptProblem -export AbstractCollocation, JuMPCollocation, InfiniteOptCollocation, - CasADiCollocation, PyomoCollocation -export DynamicOptSolution - -@public apply_to_variables, equations_toplevel, unknowns_toplevel, parameters_toplevel -@public continuous_events_toplevel, discrete_events_toplevel, assertions, is_alg_equation -@public is_diff_equation, Equality, linearize_symbolic, reorder_unknowns -@public similarity_transform, inputs, outputs, bound_inputs, unbound_inputs, bound_outputs -@public unbound_outputs, is_bound -@public AbstractSystem, CheckAll, CheckNone, CheckComponents, CheckUnits -@public t, D, t_nounits, D_nounits, t_unitful, D_unitful -@public SymbolicContinuousCallback, SymbolicDiscreteCallback -@public VariableType, MTKVariableTypeCtx, VariableBounds, VariableConnectType -@public VariableDescription, VariableInput, VariableIrreducible, VariableMisc -@public VariableOutput, VariableStatePriority, VariableUnit, collect_scoped_vars! -@public collect_var_to_name!, collect_vars!, eqtype_supports_collect_vars, hasdefault -@public getdefault, setdefault, iscomplete, isparameter, modified_unknowns! -@public renamespace, namespace_equations - -for prop in [SYS_PROPS; [:continuous_events, :discrete_events]] - getter = Symbol(:get_, prop) - hasfn = Symbol(:has_, prop) - @eval @public $getter, $hasfn -end - -PrecompileTools.@compile_workload begin - using ModelingToolkit - @variables x(ModelingToolkit.t_nounits) - @named sys = System([ModelingToolkit.D_nounits(x) ~ -x], ModelingToolkit.t_nounits) - prob = ODEProblem(mtkcompile(sys), [x => 30.0], (0, 100), jac = true) - @mtkmodel __testmod__ begin - @constants begin - c = 1.0 - end - @structural_parameters begin - structp = false - end - if structp - @variables begin - x(t) = 0.0, [description = "foo", guess = 1.0] - end - else - @variables begin - x(t) = 0.0, [description = "foo w/o structp", guess = 1.0] - end - end - @parameters begin - a = 1.0, [description = "bar"] - if structp - b = 2 * a, [description = "if"] - else - c - end - end - @equations begin - x ~ a + b - end - end -end +@public linearize_symbolic, reorder_unknowns +@public similarity_transform +include(pkgdir(ModelingToolkitBase, "src", "precompile.jl")) end # module diff --git a/src/bipartite_graph.jl b/src/bipartite_graph.jl deleted file mode 100644 index 6e4f359617..0000000000 --- a/src/bipartite_graph.jl +++ /dev/null @@ -1,863 +0,0 @@ -module BipartiteGraphs - -import ModelingToolkit: complete - -export BipartiteEdge, BipartiteGraph, DiCMOBiGraph, Unassigned, unassigned, - Matching, InducedCondensationGraph, maximal_matching, - construct_augmenting_path!, MatchedCondensationGraph - -export 𝑠vertices, 𝑑vertices, has_𝑠vertex, has_𝑑vertex, 𝑠neighbors, 𝑑neighbors, - 𝑠edges, 𝑑edges, nsrcs, ndsts, SRC, DST, set_neighbors!, invview, - delete_srcs!, delete_dsts! - -using DocStringExtensions -using UnPack -using SparseArrays -using Graphs -using Setfield - -### Matching -struct Unassigned - global unassigned - const unassigned = Unassigned.instance -end -# Behaves as a scalar -Base.length(u::Unassigned) = 1 -Base.size(u::Unassigned) = () -Base.iterate(u::Unassigned) = (unassigned, nothing) -Base.iterate(u::Unassigned, state) = nothing - -Base.show(io::IO, ::Unassigned) = printstyled(io, "u"; color = :light_black) - -#U=> :Unassigned =# -struct Matching{U, V <: AbstractVector} <: AbstractVector{Union{U, Int}} - match::V - inv_match::Union{Nothing, V} -end -# These constructors work around https://github.com/JuliaLang/julia/issues/41948 -function Matching{V}(m::Matching) where {V} - eltype(m) === Union{V, Int} && return M - VUT = typeof(similar(m.match, Union{V, Int})) - Matching{V}(convert(VUT, m.match), - m.inv_match === nothing ? nothing : convert(VUT, m.inv_match)) -end -Matching(m::Matching) = m -Matching{U}(v::V) where {U, V <: AbstractVector} = Matching{U, V}(v, nothing) -function Matching{U}(v::V, iv::Union{V, Nothing}) where {U, V <: AbstractVector} - Matching{U, V}(v, iv) -end -function Matching(v::V) where {U, V <: AbstractVector{Union{U, Int}}} - Matching{@isdefined(U) ? U : Unassigned, V}(v, nothing) -end -function Matching(m::Int) - Matching{Unassigned}(Union{Int, Unassigned}[unassigned for _ in 1:m], nothing) -end -function Matching{U}(m::Int) where {U} - Matching{Union{Unassigned, U}}(Union{Int, Unassigned, U}[unassigned for _ in 1:m], - nothing) -end - -Base.size(m::Matching) = Base.size(m.match) -Base.getindex(m::Matching, i::Integer) = m.match[i] -Base.iterate(m::Matching, state...) = iterate(m.match, state...) -function Base.copy(m::Matching{U}) where {U} - Matching{U}(copy(m.match), m.inv_match === nothing ? nothing : copy(m.inv_match)) -end -function Base.setindex!(m::Matching{U}, v::Union{Integer, U}, i::Integer) where {U} - if m.inv_match !== nothing - oldv = m.match[i] - # TODO: maybe default Matching to always have an `inv_match`? - - # To maintain the invariant that `m.inv_match[m.match[i]] == i`, we need - # to unassign the matching at `m.inv_match[v]` if it exists. - if v isa Int && 1 <= v <= length(m.inv_match) && (iv = m.inv_match[v]) isa Int - m.match[iv] = unassigned - end - if isa(oldv, Int) - @assert m.inv_match[oldv] == i - m.inv_match[oldv] = unassigned - end - if isa(v, Int) - for vv in (length(m.inv_match) + 1):v - push!(m.inv_match, unassigned) - end - m.inv_match[v] = i - end - end - return m.match[i] = v -end - -function Base.push!(m::Matching, v) - push!(m.match, v) - if v isa Integer && m.inv_match !== nothing - for vv in (length(m.inv_match) + 1):v - push!(m.inv_match, unassigned) - end - m.inv_match[v] = length(m.match) - end -end - -function complete(m::Matching{U}, - N = maximum((x for x in m.match if isa(x, Int)); init = 0)) where {U} - m.inv_match !== nothing && return m - inv_match = Union{U, Int}[unassigned for _ in 1:N] - for (i, eq) in enumerate(m.match) - isa(eq, Int) || continue - inv_match[eq] = i - end - return Matching{U}(collect(m.match), inv_match) -end - -@noinline function require_complete(m::Matching) - m.inv_match === nothing && - throw(ArgumentError("Backwards matching not defined. `complete` the matching first.")) -end - -function invview(m::Matching{U, V}) where {U, V} - require_complete(m) - return Matching{U, V}(m.inv_match, m.match) -end - -### -### Edges & Vertex -### -@enum VertType SRC DST - -struct BipartiteEdge{I <: Integer} <: Graphs.AbstractEdge{I} - src::I - dst::I - function BipartiteEdge(src::I, dst::V) where {I, V} - T = promote_type(I, V) - new{T}(T(src), T(dst)) - end -end - -Graphs.src(edge::BipartiteEdge) = edge.src -Graphs.dst(edge::BipartiteEdge) = edge.dst - -function Base.show(io::IO, edge::BipartiteEdge) - @unpack src, dst = edge - print(io, "[src: ", src, "] => [dst: ", dst, "]") -end - -Base.:(==)(a::BipartiteEdge, b::BipartiteEdge) = src(a) == src(b) && dst(a) == dst(b) - -### -### Graph -### -""" -$(TYPEDEF) - -A bipartite graph representation between two, possibly distinct, sets of vertices -(source and dependencies). Maps source vertices, labelled `1:N₁`, to vertices -on which they depend (labelled `1:N₂`). - -# Fields -$(FIELDS) - -# Example -```julia -using ModelingToolkit - -ne = 4 -srcverts = 1:4 -depverts = 1:2 - -# six source vertices -fadjlist = [[1],[1],[2],[2],[1],[1,2]] - -# two vertices they depend on -badjlist = [[1,2,5,6],[3,4,6]] - -bg = BipartiteGraph(7, fadjlist, badjlist) -``` -""" -mutable struct BipartiteGraph{I <: Integer, M} <: Graphs.AbstractGraph{I} - ne::Int - fadjlist::Vector{Vector{I}} # `fadjlist[src] => dsts` - badjlist::Union{Vector{Vector{I}}, I} # `badjlist[dst] => srcs` or `ndsts` - metadata::M -end -function BipartiteGraph(ne::Integer, fadj::AbstractVector, - badj::Union{AbstractVector, Integer} = maximum(maximum, fadj); - metadata = nothing) - BipartiteGraph(ne, fadj, badj, metadata) -end -function BipartiteGraph(fadj::AbstractVector, - badj::Union{AbstractVector, Integer} = maximum(maximum, fadj); - metadata = nothing) - BipartiteGraph(mapreduce(length, +, fadj; init = 0), fadj, badj, metadata) -end - -@noinline function require_complete(g::BipartiteGraph) - g.badjlist isa AbstractVector || - throw(ArgumentError("The graph has no back edges. Use `complete`.")) -end - -function invview(g::BipartiteGraph) - require_complete(g) - BipartiteGraph(g.ne, g.badjlist, g.fadjlist) -end - -function complete(g::BipartiteGraph{I}) where {I} - isa(g.badjlist, AbstractVector) && return g - badjlist = Vector{I}[Vector{I}() for _ in 1:(g.badjlist)] - for (s, l) in enumerate(g.fadjlist) - for d in l - push!(badjlist[d], s) - end - end - BipartiteGraph(g.ne, g.fadjlist, badjlist) -end - -# Matrix whose only purpose is to pretty-print the bipartite graph -struct BipartiteAdjacencyList - u::Union{Vector{Int}, Nothing} - highlight_u::Union{Set{Int}, Nothing} - match::Union{Int, Bool, Unassigned} -end -function BipartiteAdjacencyList(u::Union{Vector{Int}, Nothing}) - BipartiteAdjacencyList(u, nothing, unassigned) -end - -struct HighlightInt - i::Int - highlight::Symbol - match::Bool -end -Base.typeinfo_implicit(::Type{HighlightInt}) = true -function Base.show(io::IO, hi::HighlightInt) - if hi.match - printstyled(io, "(", color = hi.highlight) - printstyled(io, hi.i, color = hi.highlight) - printstyled(io, ")", color = hi.highlight) - else - printstyled(io, hi.i, color = hi.highlight) - end -end - -function Base.show(io::IO, l::BipartiteAdjacencyList) - if l.match === true - printstyled(io, "∫ ", color = :cyan) - else - printstyled(io, " ") - end - if l.u === nothing - printstyled(io, '⋅', color = :light_black) - elseif isempty(l.u) - printstyled(io, '∅', color = :light_black) - elseif l.highlight_u === nothing - print(io, l.u) - else - match = l.match - isa(match, Bool) && (match = unassigned) - function choose_color(i) - solvable = i in l.highlight_u - matched = i == match - if !matched && solvable - :default - elseif !matched && !solvable - :light_black - elseif matched && solvable - :light_yellow - elseif matched && !solvable - :magenta - end - end - if !isempty(setdiff(l.highlight_u, l.u)) - # Only for debugging, shouldn't happen in practice - print(io, - map(union(l.u, l.highlight_u)) do i - HighlightInt(i, !(i in l.u) ? :light_red : choose_color(i), - i == match) - end) - else - print(io, map(l.u) do i - HighlightInt(i, choose_color(i), i == match) - end) - end - end -end - -struct Label - s::String - c::Symbol -end -Label(s::AbstractString) = Label(s, :nothing) -Label(x::Integer) = Label(string(x)) -Base.show(io::IO, l::Label) = printstyled(io, l.s, color = l.c) - -struct BipartiteGraphPrintMatrix <: - AbstractMatrix{Union{Label, Int, BipartiteAdjacencyList}} - bpg::BipartiteGraph -end -Base.size(bgpm::BipartiteGraphPrintMatrix) = (max(nsrcs(bgpm.bpg), ndsts(bgpm.bpg)) + 1, 3) -function Base.getindex(bgpm::BipartiteGraphPrintMatrix, i::Integer, j::Integer) - checkbounds(bgpm, i, j) - if i == 1 - return (Label.(("#", "src", "dst")))[j] - elseif j == 1 - return i - 1 - elseif j == 2 - return BipartiteAdjacencyList(i - 1 <= nsrcs(bgpm.bpg) ? - 𝑠neighbors(bgpm.bpg, i - 1) : nothing) - elseif j == 3 - return BipartiteAdjacencyList(i - 1 <= ndsts(bgpm.bpg) ? - 𝑑neighbors(bgpm.bpg, i - 1) : nothing) - else - @assert false - end -end - -function Base.show(io::IO, b::BipartiteGraph) - print(io, "BipartiteGraph with (", length(b.fadjlist), ", ", - isa(b.badjlist, Int) ? b.badjlist : length(b.badjlist), ") (𝑠,𝑑)-vertices\n") - Base.print_matrix(io, BipartiteGraphPrintMatrix(b)) -end - -""" -```julia -Base.isequal(bg1::BipartiteGraph{T}, bg2::BipartiteGraph{T}) where {T <: Integer} -``` - -Test whether two [`BipartiteGraph`](@ref)s are equal. -""" -function Base.isequal(bg1::BipartiteGraph{T}, bg2::BipartiteGraph{T}) where {T <: Integer} - iseq = (bg1.ne == bg2.ne) - iseq &= (bg1.fadjlist == bg2.fadjlist) - iseq &= (bg1.badjlist == bg2.badjlist) - iseq -end - -""" -$(SIGNATURES) - -Build an empty `BipartiteGraph` with `nsrcs` sources and `ndsts` destinations. -""" -function BipartiteGraph(nsrcs::T, ndsts::T, backedge::Val{B} = Val(true); - metadata = nothing) where {T, B} - fadjlist = map(_ -> T[], 1:nsrcs) - badjlist = B ? map(_ -> T[], 1:ndsts) : ndsts - BipartiteGraph(0, fadjlist, badjlist, metadata) -end - -function Base.copy(bg::BipartiteGraph) - BipartiteGraph(bg.ne, map(copy, bg.fadjlist), map(copy, bg.badjlist), - deepcopy(bg.metadata)) -end -Base.eltype(::Type{<:BipartiteGraph{I}}) where {I} = I -function Base.empty!(g::BipartiteGraph) - foreach(empty!, g.fadjlist) - g.badjlist isa AbstractVector && foreach(empty!, g.badjlist) - g.ne = 0 - if g.metadata !== nothing - foreach(empty!, g.metadata) - end - g -end -Base.length(::BipartiteGraph) = error("length is not well defined! Use `ne` or `nv`.") - -if isdefined(Graphs, :has_contiguous_vertices) - Graphs.has_contiguous_vertices(::Type{<:BipartiteGraph}) = false -end -Graphs.is_directed(::Type{<:BipartiteGraph}) = false -Graphs.vertices(g::BipartiteGraph) = (𝑠vertices(g), 𝑑vertices(g)) -𝑠vertices(g::BipartiteGraph) = axes(g.fadjlist, 1) -function 𝑑vertices(g::BipartiteGraph) - g.badjlist isa AbstractVector ? axes(g.badjlist, 1) : Base.OneTo(g.badjlist) -end -has_𝑠vertex(g::BipartiteGraph, v::Integer) = v in 𝑠vertices(g) -has_𝑑vertex(g::BipartiteGraph, v::Integer) = v in 𝑑vertices(g) -function 𝑠neighbors(g::BipartiteGraph, i::Integer, - with_metadata::Val{M} = Val(false)) where {M} - M ? zip(g.fadjlist[i], g.metadata[i]) : g.fadjlist[i] -end -function 𝑑neighbors(g::BipartiteGraph, j::Integer, - with_metadata::Val{M} = Val(false)) where {M} - require_complete(g) - M ? zip(g.badjlist[j], (g.metadata[i][j] for i in g.badjlist[j])) : g.badjlist[j] -end -Graphs.ne(g::BipartiteGraph) = g.ne -Graphs.nv(g::BipartiteGraph) = sum(length, vertices(g)) -Graphs.edgetype(g::BipartiteGraph{I}) where {I} = BipartiteEdge{I} - -nsrcs(g::BipartiteGraph) = length(𝑠vertices(g)) -ndsts(g::BipartiteGraph) = length(𝑑vertices(g)) - -function Graphs.has_edge(g::BipartiteGraph, edge::BipartiteEdge) - @unpack src, dst = edge - (src in 𝑠vertices(g) && dst in 𝑑vertices(g)) || return false # edge out of bounds - insorted(dst, 𝑠neighbors(g, src)) -end -Base.in(edge::BipartiteEdge, g::BipartiteGraph) = Graphs.has_edge(g, edge) - -### Maximal matching -""" - construct_augmenting_path!(m::Matching, g::BipartiteGraph, vsrc, dstfilter, vcolor=falses(ndsts(g)), ecolor=nothing) -> path_found::Bool - -Try to construct an augmenting path in matching and if such a path is found, -update the matching accordingly. -""" -function construct_augmenting_path!(matching::Matching, g::BipartiteGraph, vsrc, dstfilter, - dcolor = falses(ndsts(g)), scolor = nothing) - scolor === nothing || (scolor[vsrc] = true) - - # if a `vdst` is unassigned and the edge `vsrc <=> vdst` exists - for vdst in 𝑠neighbors(g, vsrc) - if dstfilter(vdst) && matching[vdst] === unassigned - matching[vdst] = vsrc - return true - end - end - - # for every `vsrc` such that edge `vsrc <=> vdst` exists and `vdst` is uncolored - for vdst in 𝑠neighbors(g, vsrc) - (dstfilter(vdst) && !dcolor[vdst]) || continue - dcolor[vdst] = true - if construct_augmenting_path!(matching, g, matching[vdst], dstfilter, dcolor, - scolor) - matching[vdst] = vsrc - return true - end - end - return false -end - -""" - maximal_matching(g::BipartiteGraph, [srcfilter], [dstfilter]) - -For a bipartite graph `g`, construct a maximal matching of destination to source -vertices, subject to the constraint that vertices for which `srcfilter` or `dstfilter`, -return `false` may not be matched. -""" -function maximal_matching(g::BipartiteGraph, srcfilter = vsrc -> true, - dstfilter = vdst -> true, ::Type{U} = Unassigned) where {U} - matching = Matching{U}(max(nsrcs(g), ndsts(g))) - foreach(Iterators.filter(srcfilter, 𝑠vertices(g))) do vsrc - construct_augmenting_path!(matching, g, vsrc, dstfilter) - end - return matching -end - -### -### Populate -### -struct NoMetadata end -const NO_METADATA = NoMetadata() - -function Graphs.add_edge!(g::BipartiteGraph, i::Integer, j::Integer, md = NO_METADATA) - add_edge!(g, BipartiteEdge(i, j), md) -end -function Graphs.add_edge!(g::BipartiteGraph, edge::BipartiteEdge, md = NO_METADATA) - @unpack fadjlist, badjlist = g - s, d = src(edge), dst(edge) - (has_𝑠vertex(g, s) && has_𝑑vertex(g, d)) || error("edge ($edge) out of range.") - @inbounds list = fadjlist[s] - index = searchsortedfirst(list, d) - @inbounds (index <= length(list) && list[index] == d) && return false # edge already in graph - insert!(list, index, d) - if md !== NO_METADATA - insert!(g.metadata[s], index, md) - end - - g.ne += 1 - if badjlist isa AbstractVector - @inbounds list = badjlist[d] - index = searchsortedfirst(list, s) - insert!(list, index, s) - end - return true # edge successfully added -end - -function Graphs.rem_edge!(g::BipartiteGraph, i::Integer, j::Integer) - Graphs.rem_edge!(g, BipartiteEdge(i, j)) -end -function Graphs.rem_edge!(g::BipartiteGraph, edge::BipartiteEdge) - @unpack fadjlist, badjlist = g - s, d = src(edge), dst(edge) - (has_𝑠vertex(g, s) && has_𝑑vertex(g, d)) || error("edge ($edge) out of range.") - @inbounds list = fadjlist[s] - index = searchsortedfirst(list, d) - @inbounds (index <= length(list) && list[index] == d) || - error("graph does not have edge $edge") - deleteat!(list, index) - g.ne -= 1 - if badjlist isa AbstractVector - @inbounds list = badjlist[d] - index = searchsortedfirst(list, s) - deleteat!(list, index) - end - return true # edge successfully deleted -end - -function Graphs.add_vertex!(g::BipartiteGraph{T}, type::VertType) where {T} - if type === DST - if g.badjlist isa AbstractVector - push!(g.badjlist, T[]) - return length(g.badjlist) - else - g.badjlist += 1 - return g.badjlist - end - elseif type === SRC - push!(g.fadjlist, T[]) - return length(g.fadjlist) - else - error("type ($type) must be either `DST` or `SRC`") - end -end - -function set_neighbors!(g::BipartiteGraph, i::Integer, new_neighbors) - old_neighbors = g.fadjlist[i] - old_nneighbors = length(old_neighbors) - new_nneighbors = length(new_neighbors) - g.ne += new_nneighbors - old_nneighbors - if isa(g.badjlist, AbstractVector) - for n in old_neighbors - @inbounds list = g.badjlist[n] - index = searchsortedfirst(list, i) - if 1 <= index <= length(list) && list[index] == i - deleteat!(list, index) - end - end - for n in new_neighbors - @inbounds list = g.badjlist[n] - index = searchsortedfirst(list, i) - if !(1 <= index <= length(list) && list[index] == i) - insert!(list, index, i) - end - end - end - if iszero(new_nneighbors) # this handles Tuple as well - # Warning: Aliases old_neighbors - empty!(g.fadjlist[i]) - else - g.fadjlist[i] = unique!(sort(new_neighbors)) - end -end - -function delete_srcs!(g::BipartiteGraph{I}, srcs; rm_verts = false) where {I} - for s in srcs - set_neighbors!(g, s, ()) - end - if rm_verts - old_to_new_idxs = collect(one(I):I(nsrcs(g))) - for s in srcs - old_to_new_idxs[s] = zero(I) - end - offset = zero(I) - for i in eachindex(old_to_new_idxs) - if iszero(old_to_new_idxs[i]) - offset += one(I) - continue - end - old_to_new_idxs[i] -= offset - end - - if g.badjlist isa AbstractVector - for i in 1:ndsts(g) - for j in eachindex(g.badjlist[i]) - g.badjlist[i][j] = old_to_new_idxs[g.badjlist[i][j]] - end - filter!(!iszero, g.badjlist[i]) - end - end - deleteat!(g.fadjlist, srcs) - end - g -end -function delete_dsts!(g::BipartiteGraph, srcs; rm_verts = false) - delete_srcs!(invview(g), srcs; rm_verts) -end - -### -### Edges iteration -### -Graphs.edges(g::BipartiteGraph) = BipartiteEdgeIter(g, Val(SRC)) -𝑠edges(g::BipartiteGraph) = BipartiteEdgeIter(g, Val(SRC)) -𝑑edges(g::BipartiteGraph) = BipartiteEdgeIter(g, Val(DST)) - -struct BipartiteEdgeIter{T, G} <: Graphs.AbstractEdgeIter - g::G - type::Val{T} -end - -Base.length(it::BipartiteEdgeIter) = ne(it.g) -Base.eltype(it::BipartiteEdgeIter) = edgetype(it.g) - -function Base.iterate(it::BipartiteEdgeIter{SRC, <:BipartiteGraph{T}}, - state = (1, 1, SRC)) where {T} - @unpack g = it - neqs = nsrcs(g) - neqs == 0 && return nothing - eq, jvar = state - - while eq <= neqs - eq′ = eq - vars = 𝑠neighbors(g, eq′) - if jvar > length(vars) - eq += 1 - jvar = 1 - continue - end - edge = BipartiteEdge(eq′, vars[jvar]) - state = (eq, jvar + 1, SRC) - return edge, state - end - return nothing -end - -function Base.iterate(it::BipartiteEdgeIter{DST, <:BipartiteGraph{T}}, - state = (1, 1, DST)) where {T} - @unpack g = it - nvars = ndsts(g) - nvars == 0 && return nothing - ieq, jvar = state - - while jvar <= nvars - eqs = 𝑑neighbors(g, jvar) - if ieq > length(eqs) - ieq = 1 - jvar += 1 - continue - end - edge = BipartiteEdge(eqs[ieq], jvar) - state = (ieq + 1, jvar, DST) - return edge, state - end - return nothing -end - -### -### Utils -### -function Graphs.incidence_matrix(g::BipartiteGraph, val = true) - I = Int[] - J = Int[] - for i in 𝑠vertices(g), n in 𝑠neighbors(g, i) - - push!(I, i) - push!(J, n) - end - S = sparse(I, J, val, nsrcs(g), ndsts(g)) -end - -""" - struct DiCMOBiGraph - -This data structure implements a "directed, contracted, matching-oriented" view of an -original (undirected) bipartite graph. It has two modes, depending on the `Transposed` -flag, which switches the direction of the induced matching. - -Essentially the graph adapter performs two largely orthogonal functions -[`Transposed == true` differences are indicated in square brackets]: - - 1. It pairs an undirected bipartite graph with a matching of the destination vertex. - - This matching is used to induce an orientation on the otherwise undirected graph: - Matched edges pass from destination to source [source to destination], all other edges - pass in the opposite direction. - - 2. It exposes the graph view obtained by contracting the destination [source] vertices - along the matched edges. - -The result of this operation is an induced, directed graph on the source [destination] vertices. -The resulting graph has a few desirable properties. In particular, this graph -is acyclic if and only if the induced directed graph on the original bipartite -graph is acyclic. - -# Hypergraph interpretation - -Consider the bipartite graph `B` as the incidence graph of some hypergraph `H`. -Note that a matching `M` on `B` in the above sense is equivalent to determining -an (1,n)-orientation on the hypergraph (i.e. each directed hyperedge has exactly -one head, but any arbitrary number of tails). In this setting, this is simply -the graph formed by expanding each directed hyperedge into `n` ordinary edges -between the same vertices. -""" -mutable struct DiCMOBiGraph{Transposed, I, G <: BipartiteGraph{I}, M <: Matching} <: - Graphs.AbstractGraph{I} - graph::G - ne::Union{Missing, Int} - matching::M - function DiCMOBiGraph{Transposed}(g::G, ne::Union{Missing, Int}, - m::M) where {Transposed, I, G <: BipartiteGraph{I}, M} - new{Transposed, I, G, M}(g, ne, m) - end -end -function DiCMOBiGraph{Transposed}(g::BipartiteGraph) where {Transposed} - DiCMOBiGraph{Transposed}(g, 0, Matching(ndsts(g))) -end -function DiCMOBiGraph{Transposed}(g::BipartiteGraph, m::M) where {Transposed, M} - DiCMOBiGraph{Transposed}(g, missing, m) -end - -function invview(g::DiCMOBiGraph{Transposed}) where {Transposed} - DiCMOBiGraph{!Transposed}(invview(g.graph), g.ne, invview(g.matching)) -end - -Graphs.is_directed(::Type{<:DiCMOBiGraph}) = true -function Graphs.nv(g::DiCMOBiGraph{Transposed}) where {Transposed} - Transposed ? ndsts(g.graph) : nsrcs(g.graph) -end -function Graphs.vertices(g::DiCMOBiGraph{Transposed}) where {Transposed} - Transposed ? 𝑑vertices(g.graph) : 𝑠vertices(g.graph) -end - -struct CMONeighbors{Transposed, V} - g::DiCMOBiGraph{Transposed} - v::V - function CMONeighbors{Transposed}(g::DiCMOBiGraph{Transposed}, - v::V) where {Transposed, V} - new{Transposed, V}(g, v) - end -end - -Graphs.outneighbors(g::DiCMOBiGraph{false}, v) = CMONeighbors{false}(g, v) -Graphs.inneighbors(g::DiCMOBiGraph{false}, v) = inneighbors(invview(g), v) -Base.iterate(c::CMONeighbors{false}) = iterate(c, (c.g.graph.fadjlist[c.v],)) -function Base.iterate(c::CMONeighbors{false}, (l, state...)) - while true - r = iterate(l, state...) - r === nothing && return nothing - # If this is a matched edge, skip it, it's reversed in the induced - # directed graph. Otherwise, if there is no matching for this destination - # edge, also skip it, since it got deleted in the contraction. - vsrc = c.g.matching[r[1]] - if vsrc === c.v || !isa(vsrc, Int) - state = (r[2],) - continue - end - return vsrc, (l, r[2]) - end -end -Base.length(c::CMONeighbors{false}) = count(_ -> true, c) - -liftint(f, x) = (!isa(x, Int)) ? nothing : f(x) -liftnothing(f, x) = x === nothing ? nothing : f(x) - -_vsrc(c::CMONeighbors{true}) = c.g.matching[c.v] -_neighbors(c::CMONeighbors{true}) = liftint(vsrc -> c.g.graph.fadjlist[vsrc], _vsrc(c)) -Base.length(c::CMONeighbors{true}) = something(liftnothing(length, _neighbors(c)), 1) - 1 -Graphs.inneighbors(g::DiCMOBiGraph{true}, v) = CMONeighbors{true}(g, v) -Graphs.outneighbors(g::DiCMOBiGraph{true}, v) = outneighbors(invview(g), v) -Base.iterate(c::CMONeighbors{true}) = liftnothing(ns -> iterate(c, (ns,)), _neighbors(c)) -function Base.iterate(c::CMONeighbors{true}, (l, state...)) - while true - r = iterate(l, state...) - r === nothing && return nothing - if r[1] === c.v - state = (r[2],) - continue - end - return r[1], (l, r[2]) - end -end - -function _edges(g::DiCMOBiGraph{Transposed}) where {Transposed} - Transposed ? - ((w => v for w in inneighbors(g, v)) for v in vertices(g)) : - ((v => w for w in outneighbors(g, v)) for v in vertices(g)) -end - -Graphs.edges(g::DiCMOBiGraph) = (Graphs.SimpleEdge(p) for p in Iterators.flatten(_edges(g))) -function Graphs.ne(g::DiCMOBiGraph) - if g.ne === missing - g.ne = mapreduce(x -> length(x.iter), +, _edges(g)) - end - return g.ne -end - -Graphs.has_edge(g::DiCMOBiGraph{true}, a, b) = a in inneighbors(g, b) -Graphs.has_edge(g::DiCMOBiGraph{false}, a, b) = b in outneighbors(g, a) -# This definition is required for `induced_subgraph` to work -(::Type{<:DiCMOBiGraph})(n::Integer) = SimpleDiGraph(n) - -# Condensation Graphs -abstract type AbstractCondensationGraph <: AbstractGraph{Int} end -function (T::Type{<:AbstractCondensationGraph})(g, sccs::Vector{Union{Int, Vector{Int}}}) - scc_assignment = Vector{Int}(undef, isa(g, BipartiteGraph) ? ndsts(g) : nv(g)) - for (i, c) in enumerate(sccs) - for v in c - scc_assignment[v] = i - end - end - T(g, sccs, scc_assignment) -end -function (T::Type{<:AbstractCondensationGraph})(g, sccs::Vector{Vector{Int}}) - T(g, Vector{Union{Int, Vector{Int}}}(sccs)) -end - -Graphs.is_directed(::Type{<:AbstractCondensationGraph}) = true -Graphs.nv(icg::AbstractCondensationGraph) = length(icg.sccs) -Graphs.vertices(icg::AbstractCondensationGraph) = Base.OneTo(nv(icg)) - -""" - struct MatchedCondensationGraph - -For some bipartite-graph and an orientation induced on its destination contraction, -records the condensation DAG of the digraph formed by the orientation. I.e. this -is a DAG of connected components formed by the destination vertices of some -underlying bipartite graph. -N.B.: This graph does not store explicit neighbor relations of the sccs. -Therefor, the edge multiplicity is derived from the underlying bipartite graph, -i.e. this graph is not strict. -""" -struct MatchedCondensationGraph{G <: DiCMOBiGraph} <: AbstractCondensationGraph - graph::G - # Records the members of a strongly connected component. For efficiency, - # trivial sccs (with one vertex member) are stored inline. Note: the sccs - # here need not be stored in topological order. - sccs::Vector{Union{Int, Vector{Int}}} - # Maps the vertices back to the scc of which they are a part - scc_assignment::Vector{Int} -end - -function Graphs.outneighbors(mcg::MatchedCondensationGraph, cc::Integer) - Iterators.flatten((mcg.scc_assignment[v′] - for v′ in outneighbors(mcg.graph, v) if mcg.scc_assignment[v′] != cc) - for v in mcg.sccs[cc]) -end - -function Graphs.inneighbors(mcg::MatchedCondensationGraph, cc::Integer) - Iterators.flatten((mcg.scc_assignment[v′] - for v′ in inneighbors(mcg.graph, v) if mcg.scc_assignment[v′] != cc) - for v in mcg.sccs[cc]) -end - -""" - struct InducedCondensationGraph - -For some bipartite-graph and a topologicall sorted list of connected components, -represents the condensation DAG of the digraph formed by the orientation. I.e. this -is a DAG of connected components formed by the destination vertices of some -underlying bipartite graph. -N.B.: This graph does not store explicit neighbor relations of the sccs. -Therefor, the edge multiplicity is derived from the underlying bipartite graph, -i.e. this graph is not strict. -""" -struct InducedCondensationGraph{G <: BipartiteGraph} <: AbstractCondensationGraph - graph::G - # Records the members of a strongly connected component. For efficiency, - # trivial sccs (with one vertex member) are stored inline. Note: the sccs - # here are stored in topological order. - sccs::Vector{Union{Int, Vector{Int}}} - # Maps the vertices back to the scc of which they are a part - scc_assignment::Vector{Int} -end - -function _neighbors(icg::InducedCondensationGraph, cc::Integer) - Iterators.flatten(Iterators.flatten(icg.graph.fadjlist[vsrc] - for vsrc in icg.graph.badjlist[v]) - for v in icg.sccs[cc]) -end - -function Graphs.outneighbors(icg::InducedCondensationGraph, v::Integer) - (icg.scc_assignment[n] for n in _neighbors(icg, v) if icg.scc_assignment[n] > v) -end - -function Graphs.inneighbors(icg::InducedCondensationGraph, v::Integer) - (icg.scc_assignment[n] for n in _neighbors(icg, v) if icg.scc_assignment[n] < v) -end - -end # module diff --git a/src/clock.jl b/src/clock.jl deleted file mode 100644 index df3b6f4b47..0000000000 --- a/src/clock.jl +++ /dev/null @@ -1,117 +0,0 @@ -@data InferredClock begin - Inferred - InferredDiscrete(Int) -end - -const InferredTimeDomain = InferredClock.Type -using .InferredClock: Inferred, InferredDiscrete - -function InferredClock.InferredDiscrete() - return InferredDiscrete(0) -end - -Base.Broadcast.broadcastable(x::InferredTimeDomain) = Ref(x) - -struct VariableTimeDomain end -Symbolics.option_to_metadata_type(::Val{:timedomain}) = VariableTimeDomain - -is_concrete_time_domain(::TimeDomain) = true -is_concrete_time_domain(_) = false - -""" - is_continuous_domain(x) - -true if `x` contains only continuous-domain signals. -See also [`has_continuous_domain`](@ref) -""" -function is_continuous_domain(x) - issym(x) && return getmetadata(x, VariableTimeDomain, false) == ContinuousClock() - !has_discrete_domain(x) && has_continuous_domain(x) -end - -get_time_domain(_, x) = get_time_domain(x) -function get_time_domain(x) - if iscall(x) && operation(x) isa Operator - output_timedomain(x) - else - getmetadata(x, VariableTimeDomain, nothing) - end -end -get_time_domain(x::Num) = get_time_domain(value(x)) - -has_time_domain(_, x) = has_time_domain(x) -""" - has_time_domain(x) - -Determine if variable `x` has a time-domain attributed to it. -""" -function has_time_domain(x::Symbolic) - # getmetadata(x, ContinuousClock, nothing) !== nothing || - # getmetadata(x, Discrete, nothing) !== nothing - getmetadata(x, VariableTimeDomain, nothing) !== nothing -end -has_time_domain(x::Num) = has_time_domain(value(x)) -has_time_domain(x) = false - -for op in [Differential] - @eval input_timedomain(::$op, arg = nothing) = (ContinuousClock(),) - @eval output_timedomain(::$op, arg = nothing) = ContinuousClock() -end - -""" - has_discrete_domain(x) - -true if `x` contains discrete signals (`x` may or may not contain continuous-domain signals). `x` may be an expression or equation. -See also [`is_discrete_domain`](@ref) -""" -function has_discrete_domain(x) - issym(x) && return is_discrete_domain(x) - hasshift(x) || hassample(x) || hashold(x) -end - -""" - has_continuous_domain(x) - -true if `x` contains continuous signals (`x` may or may not contain discrete-domain signals). `x` may be an expression or equation. -See also [`is_continuous_domain`](@ref) -""" -function has_continuous_domain(x) - issym(x) && return is_continuous_domain(x) - hasderiv(x) || hasdiff(x) || hassample(x) || hashold(x) -end - -""" - is_hybrid_domain(x) - -true if `x` contains both discrete and continuous-domain signals. `x` may be an expression or equation. -""" -is_hybrid_domain(x) = has_discrete_domain(x) && has_continuous_domain(x) - -""" - is_discrete_domain(x) - -true if `x` contains only discrete-domain signals. -See also [`has_discrete_domain`](@ref) -""" -function is_discrete_domain(x) - if hasmetadata(x, VariableTimeDomain) || issym(x) - return is_discrete_time_domain(getmetadata(x, VariableTimeDomain, false)) - end - !has_discrete_domain(x) && has_continuous_domain(x) -end - -sampletime(c) = Moshi.Match.@match c begin - x::SciMLBase.AbstractClock => nothing - PeriodicClock(dt) => dt - _ => nothing -end - -struct ClockInferenceException <: Exception - msg::Any -end - -function Base.showerror(io::IO, cie::ClockInferenceException) - print(io, "ClockInferenceException: ", cie.msg) -end - -struct IntegerSequence end diff --git a/src/discretedomain.jl b/src/discretedomain.jl index a17c447631..2aed70e636 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -1,402 +1,4 @@ using Symbolics: Operator, Num, Term, value, recursive_hasoperator -""" - $(TYPEDSIGNATURES) +MTKBase.ShiftIndex() = MTKBase.ShiftIndex(MTKTearing.Inferred()) -Trait to be implemented for operators which determines whether application of the operator -generates a semantically different variable or not. For example, `Differential` and `Shift` -are not transparent but `Sample` and `Hold` are. Defaults to `false` if not implemented. -""" -is_transparent_operator(x) = is_transparent_operator(typeof(x)) -is_transparent_operator(::Type) = false - -""" - $(TYPEDSIGNATURES) - -Trait to be implemented for operators which determines whether the operator is applied to -a time-varying quantity and results in a time-varying quantity. For example, `Initial` and -`Pre` are not time-varying since while they are applied to variables, the application -results in a non-discrete-time parameter. `Differential`, `Shift`, `Sample` and `Hold` are -all time-varying operators. All time-varying operators must implement `input_timedomain` and -`output_timedomain`. -""" -is_timevarying_operator(x) = is_timevarying_operator(typeof(x)) -is_timevarying_operator(::Type{<:Symbolics.Operator}) = true -is_timevarying_operator(::Type) = false - -""" - function SampleTime() - -`SampleTime()` can be used in the equations of a hybrid system to represent time sampled -at the inferred clock for that equation. -""" -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 - -function validate_operator(op::SampleTime, args, iv; context = nothing) end - -# Shift - -""" -$(TYPEDEF) - -Represents a shift operator. - -# Fields -$(FIELDS) - -# Examples - -```jldoctest -julia> using Symbolics - -julia> Δ = Shift(t) -(::Shift) (generic function with 2 methods) -``` -""" -struct Shift <: Operator - """Fixed Shift""" - t::Union{Nothing, Symbolic} - steps::Int - Shift(t, steps = 1) = new(value(t), steps) -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 - if Symbolics.isarraysymbolic(x) - Symbolics.array_term(D, x) - else - term(D, x) - end -end -function (D::Shift)(x::Union{Num, Symbolics.Arr}, allow_zero = false) - !allow_zero && D.steps == 0 && return x - vt = value(x) - 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") - elseif op isa Shift - if D.t === nothing || isequal(D.t, op.t) - arg = arguments(vt)[1] - newsteps = D.steps + op.steps - return wrap(newsteps == 0 ? arg : Shift(D.t, newsteps)(arg)) - end - end - end - wrap(D(vt, allow_zero)) -end -SymbolicUtils.promote_symtype(::Shift, t) = t - -Base.show(io::IO, D::Shift) = print(io, "Shift(", D.t, ", ", D.steps, ")") - -Base.:(==)(D1::Shift, D2::Shift) = isequal(D1.t, D2.t) && isequal(D1.steps, D2.steps) -Base.hash(D::Shift, u::UInt) = hash(D.steps, hash(D.t, xor(u, 0x055640d6d952f101))) - -Base.:^(D::Shift, n::Integer) = Shift(D.t, D.steps * n) -Base.literal_pow(f::typeof(^), D::Shift, ::Val{n}) where {n} = Shift(D.t, D.steps * n) - -function validate_operator(op::Shift, args, iv; context = nothing) - isequal(op.t, iv) || throw(OperatorIndepvarMismatchError(op, iv, context)) - op.steps <= 0 || error(""" - Only non-positive shifts are allowed. Found shift of $(op.steps) in $context. - """) -end - -hasshift(eq::Equation) = hasshift(eq.lhs) || hasshift(eq.rhs) - -""" - hasshift(O) - -Returns true if the expression or equation `O` contains [`Shift`](@ref) terms. -""" -hasshift(O) = recursive_hasoperator(Shift, O) - -# Sample - -""" -$(TYPEDEF) - -Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal. - -# Constructors -`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete())` -`Sample(dt::Real)` - -`Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`. - -# Fields -$(FIELDS) - -# Examples - -```jldoctest -julia> using Symbolics - -julia> t = ModelingToolkit.t_nounits - -julia> Δ = Sample(0.01) -(::Sample) (generic function with 2 methods) -``` -""" -struct Sample <: Operator - clock::Any - Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete()) = new(clock) -end - -is_transparent_operator(::Type{Sample}) = true - -function Sample(arg::Real) - arg = unwrap(arg) - if symbolic_type(arg) == NotSymbolic() - Sample(Clock(arg)) - else - Sample()(arg) - end -end -(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, ")") - -Base.:(==)(D1::Sample, D2::Sample) = isequal(D1.clock, D2.clock) -Base.hash(D::Sample, u::UInt) = hash(D.clock, xor(u, 0x055640d6d952f101)) - -function validate_operator(op::Sample, args, iv; context = nothing) - arg = unwrap(only(args)) - if !is_variable_floatingpoint(arg) - throw(ContinuousOperatorDiscreteArgumentError(op, arg, context)) - end - if isparameter(arg) - throw(ArgumentError(""" - Expected argument of $op to be an unknown, found $arg which is a parameter. - """)) - end -end - -""" - hassample(O) - -Returns true if the expression or equation `O` contains [`Sample`](@ref) terms. -""" -hassample(O) = recursive_hasoperator(Sample, unwrap(O)) - -# Hold - -""" -$(TYPEDEF) - -Represents a hold operator. A continuous-time signal is produced by holding a discrete-time signal `x` with zero-order hold. - -``` -cont_x = Hold()(disc_x) -``` -""" -struct Hold <: Operator -end - -is_transparent_operator(::Type{Hold}) = true - -(D::Hold)(x) = Term{symtype(x)}(D, Any[x]) -(D::Hold)(x::Number) = 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) - -function validate_operator(op::Hold, args, iv; context = nothing) - # TODO: maybe validate `VariableTimeDomain`? - return nothing -end - -""" - hashold(O) - -Returns true if the expression or equation `O` contains [`Hold`](@ref) terms. -""" -hashold(O) = recursive_hasoperator(Hold, unwrap(O)) - -# ShiftIndex - -""" - ShiftIndex - -The `ShiftIndex` operator allows you to index a signal and obtain a shifted discrete-time signal. If the signal is continuous-time, the signal is sampled before shifting. - -# Examples - -``` -julia> t = ModelingToolkit.t_nounits; - -julia> @variables x(t); - -julia> k = ShiftIndex(t, 0.1); - -julia> x(k) # no shift -x(t) - -julia> x(k+1) # shift -Shift(1)(x(t)) -``` -""" -struct ShiftIndex - clock::Union{InferredTimeDomain, TimeDomain, IntegerSequence} - steps::Int - function ShiftIndex( - clock::Union{TimeDomain, InferredTimeDomain, IntegerSequence} = Inferred(), steps::Int = 0) - new(clock, steps) - end - ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps) - ShiftIndex(::Num, steps::Int) = new(IntegerSequence(), steps) -end - -function (xn::Num)(k::ShiftIndex) - @unpack clock, steps = k - x = value(xn) - # Verify that the independent variables of k and x match and that the expression doesn't have multiple variables - vars = ModelingToolkit.vars(x) - if length(vars) != 1 - error("Cannot shift a multivariate expression $x. Either create a new unknown and shift this, or shift the individual variables in the expression.") - end - var = only(vars) - if !iscall(var) - throw(ArgumentError("Cannot shift time-independent variable $var")) - end - if operation(var) == getindex - var = first(arguments(var)) - end - if length(arguments(var)) != 1 - error("Cannot shift an expression with multiple independent variables $x.") - end - - # d, _ = propagate_time_domain(xn) - # if d != clock # this is only required if the variable has another clock - # xn = Sample(t, clock)(xn) - # end - # QUESTION: should we return a variable with time domain set to k.clock? - xn = setmetadata(xn, VariableTimeDomain, k.clock) - if steps == 0 - return xn # x(k) needs no shift operator if the step of k is 0 - end - Shift(t, steps)(xn) # a shift of k steps -end - -function (xn::Symbolics.Arr)(k::ShiftIndex) - @unpack clock, steps = k - x = value(xn) - # Verify that the independent variables of k and x match and that the expression doesn't have multiple variables - vars = ModelingToolkit.vars(x) - if length(vars) != 1 - error("Cannot shift a multivariate expression $x. Either create a new unknown and shift this, or shift the individual variables in the expression.") - end - var = only(vars) - if !iscall(var) - throw(ArgumentError("Cannot shift time-independent variable $var")) - end - if length(arguments(var)) != 1 - error("Cannot shift an expression with multiple independent variables $x.") - end - - # d, _ = propagate_time_domain(xn) - # if d != clock # this is only required if the variable has another clock - # xn = Sample(t, clock)(xn) - # end - # QUESTION: should we return a variable with time domain set to k.clock? - xn = wrap(setmetadata(unwrap(xn), VariableTimeDomain, k.clock)) - if steps == 0 - return xn # x(k) needs no shift operator if the step of k is 0 - end - Shift(t, steps)(xn) # a shift of k steps -end - -Base.:+(k::ShiftIndex, i::Int) = ShiftIndex(k.clock, k.steps + i) -Base.:-(k::ShiftIndex, i::Int) = k + (-i) - -""" - input_timedomain(op::Operator) - -Return the time-domain type (`ContinuousClock()` or `InferredDiscrete()`) that `op` operates on. -Should return a tuple containing the time domain type for each argument to the operator. -""" -function input_timedomain(s::Shift, arg = nothing) - if has_time_domain(arg) - return get_time_domain(arg) - end - (InferredDiscrete(),) -end - -""" - output_timedomain(op::Operator) - -Return the time-domain type (`ContinuousClock()` or `InferredDiscrete()`) that `op` results in. -""" -function output_timedomain(s::Shift, arg = nothing) - if has_time_domain(t, arg) - return get_time_domain(t, arg) - end - InferredDiscrete() -end - -input_timedomain(::Sample, _ = nothing) = (ContinuousClock(),) -output_timedomain(s::Sample, _ = nothing) = s.clock - -function input_timedomain(h::Hold, arg = nothing) - if has_time_domain(arg) - return get_time_domain(arg) - end - (InferredDiscrete(),) # the Hold accepts any discrete -end -output_timedomain(::Hold, _ = nothing) = ContinuousClock() - -sampletime(op::Sample, _ = nothing) = sampletime(op.clock) -sampletime(op::ShiftIndex, _ = nothing) = sampletime(op.clock) - -function output_timedomain(x) - if isoperator(x, Operator) - args = arguments(x) - return output_timedomain(operation(x), if length(args) == 1 - args[] - else - args - end) - else - throw(ArgumentError("$x of type $(typeof(x)) is not an operator expression")) - end -end - -function input_timedomain(x) - if isoperator(x, Operator) - args = arguments(x) - return input_timedomain(operation(x), if length(args) == 1 - args[] - else - args - end) - else - throw(ArgumentError("$x of type $(typeof(x)) is not an operator expression")) - end -end - -function ZeroCrossing(expr; name = gensym(), up = true, down = true, kwargs...) - return SymbolicContinuousCallback( - [expr ~ 0], up ? ImperativeAffect(Returns(nothing)) : nothing; - affect_neg = down ? ImperativeAffect(Returns(nothing)) : nothing, - kwargs..., zero_crossing_id = name) -end - -function SciMLBase.Clocks.EventClock(cb::SymbolicContinuousCallback) - return SciMLBase.Clocks.EventClock(cb.zero_crossing_id) -end diff --git a/src/initialization.jl b/src/initialization.jl new file mode 100644 index 0000000000..af92f817c6 --- /dev/null +++ b/src/initialization.jl @@ -0,0 +1,47 @@ +MTKBase.singular_check(ts::TearingState) = StateSelection.singular_check(ts) + +function MTKBase.get_initialization_problem_type(sys::System, isys::System; + warn_initialize_determined = true, + use_scc = true, kwargs...) + neqs = length(equations(isys)) + nunknown = length(unknowns(isys)) + ts = get_tearing_state(isys)::TearingState + + if use_scc + scc_message = """ + `SCCNonlinearProblem` can only be used for initialization of fully determined \ + systems and hence will not be used here. + """ + else + scc_message = "" + end + + if warn_initialize_determined && neqs > nunknown + @warn overdetermined_initialization_message(neqs, nunknown, scc_message) + end + if warn_initialize_determined && neqs < nunknown + @warn underdetermined_initialization_message(neqs, nunknown, scc_message) + end + + unassigned_vars = MTKBase.singular_check(ts) + if neqs == nunknown && isempty(unassigned_vars) + if use_scc && neqs > 0 + if is_split(isys) + SCCNonlinearProblem + else + @warn """ + `SCCNonlinearProblem` can only be used with `split = true` systems. \ + Simplify your `System` with `split = true` or pass `use_scc = false` to \ + disable this warning + """ + NonlinearProblem + end + else + NonlinearProblem + end + else + NonlinearLeastSquaresProblem + end +end + +MTKBase.default_missing_guess_value(::Nothing) = MTKBase.MissingGuessValue.Error() diff --git a/src/linearization.jl b/src/linearization.jl index a163e65c73..6d2801d9fb 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -285,7 +285,7 @@ function (linfun::LinearizationFunction)(u, p, t) for (k, v) in p if is_parameter(linfun, k) v = fixpoint_sub(v, p) - setp(linfun, k)(newps, v) + setp(linfun, k)(newps, value(v)) end end p = newps @@ -508,7 +508,7 @@ function linearize_symbolic(sys::AbstractSystem, inputs, sts = unknowns(sys) t = get_iv(sys) ps = parameters(sys; initial_parameters = true) - p = reorder_parameters(sys, ps) + p = Tuple(reorder_parameters(sys, ps)) fun_expr = generate_rhs(sys; expression = Val{true})[1] fun = eval_or_rgf(fun_expr; eval_expression, eval_module) @@ -613,60 +613,6 @@ function Base.showerror(io::IO, err::IONotFoundError) end end -""" -Modify the variable metadata of system variables to indicate which ones are inputs, outputs, and disturbances. Needed for `inputs`, `outputs`, `disturbances`, `unbound_inputs`, `unbound_outputs` to return the proper subsets. -""" -function markio!(state, orig_inputs, inputs, outputs, disturbances; check = true) - fullvars = get_fullvars(state) - inputset = Dict{Any, Bool}(i => false for i in inputs) - outputset = Dict{Any, Bool}(o => false for o in outputs) - disturbanceset = Dict{Any, Bool}(d => false for d in disturbances) - for (i, v) in enumerate(fullvars) - if v in keys(inputset) - if v in keys(outputset) - v = setio(v, true, true) - outputset[v] = true - else - v = setio(v, true, false) - end - inputset[v] = true - fullvars[i] = v - elseif v in keys(outputset) - v = setio(v, false, true) - outputset[v] = true - fullvars[i] = v - else - if isinput(v) - push!(orig_inputs, v) - end - v = setio(v, false, false) - fullvars[i] = v - end - - if v in keys(disturbanceset) - v = setio(v, true, false) - v = setdisturbance(v, true) - disturbanceset[v] = true - fullvars[i] = v - end - end - if check - ikeys = keys(filter(!last, inputset)) - if !isempty(ikeys) - throw(IONotFoundError("inputs", nameof(state.sys), ikeys)) - end - dkeys = keys(filter(!last, disturbanceset)) - if !isempty(dkeys) - throw(IONotFoundError("disturbance inputs", nameof(state.sys), dkeys)) - end - okeys = keys(filter(!last, outputset)) - if !isempty(okeys) - throw(IONotFoundError("outputs", nameof(state.sys), okeys)) - end - end - state, orig_inputs -end - """ (; A, B, C, D), simplified_sys, extras = linearize(sys, inputs, outputs; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false, kwargs...) (; A, B, C, D), extras = linearize(simplified_sys, lin_fun; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false) @@ -772,7 +718,7 @@ function linearize(sys, lin_fun::LinearizationFunction; t = 0.0, op = Dict(), allow_input_derivatives = false, p = DiffEqBase.NullParameters()) prob = LinearizationProblem(lin_fun, t) - op = anydict(op) + op = as_atomic_dict_with_defaults(Dict{SymbolicT, SymbolicT}(op), COMMON_NOTHING) evaluate_varmap!(op, keys(op)) for (k, v) in op v === nothing && continue diff --git a/src/problems/docs.jl b/src/problems/docs.jl index 01855dafbe..c4f009a702 100644 --- a/src/problems/docs.jl +++ b/src/problems/docs.jl @@ -1,205 +1,6 @@ struct SemilinearODEFunction{iip, spec} end struct SemilinearODEProblem{iip, spec} end -const U0_P_DOCS = """ -The order of unknowns is determined by `unknowns(sys)`. If the system is split -[`is_split`](@ref) create an [`MTKParameters`](@ref) object. Otherwise, a parameter vector. -Initial values provided in terms of other variables will be symbolically evaluated. -The type of `op` will be used to determine the type of the containers. For example, if -given as an `SArray` of key-value pairs, `u0` will be an appropriately sized `SVector` -and the parameter object will be an `MTKParameters` object with `SArray`s inside. -""" - -const EVAL_EXPR_MOD_KWARGS = """ -- `eval_expression`: Whether to compile any functions via `eval` or - `RuntimeGeneratedFunctions`. -- `eval_module`: If `eval_expression == true`, the module to `eval` into. Otherwise, the - module in which to generate the `RuntimeGeneratedFunction`. -""" - -const INITIALIZEPROB_KWARGS = """ -- `guesses`: The guesses for variables in the system, used as initial values for the - initialization problem. -- `warn_initialize_determined`: Warn if the initialization system is under/over-determined. -- `initialization_eqs`: Extra equations to use in the initialization problem. -- `fully_determined`: Override whether the initialization system is fully determined. -- `use_scc`: Whether to use `SCCNonlinearProblem` for initialization if the system is fully - determined. -""" - -const PROBLEM_KWARGS = """ -$EVAL_EXPR_MOD_KWARGS -$INITIALIZEPROB_KWARGS -- `check_initialization_units`: Enable or disable unit checks when constructing the - initialization problem. -- `tofloat`: Passed to [`varmap_to_vars`](@ref) when building the parameter vector of - a non-split system. -- `u0_eltype`: The `eltype` of the `u0` vector. If `nothing`, finds the promoted floating point - type from `op`. -- `u0_constructor`: A function to apply to the `u0` value returned from - [`varmap_to_vars`](@ref). - to construct the final `u0` value. -- `p_constructor`: A function to apply to each array buffer created when constructing the - parameter object. -- `warn_cyclic_dependency`: Whether to emit a warning listing out cycles in initial - conditions provided for unknowns and parameters. -- `circular_dependency_max_cycle_length`: Maximum length of cycle to check for. Only - applicable if `warn_cyclic_dependency == true`. -- `circular_dependency_max_cycles`: Maximum number of cycles to check for. Only applicable - if `warn_cyclic_dependency == true`. -- `substitution_limit`: The number times to substitute initial conditions into each other - to attempt to arrive at a numeric value. -""" - -const TIME_DEPENDENT_PROBLEM_KWARGS = """ -- `callback`: An extra callback or `CallbackSet` to add to the problem, in addition to the - ones defined symbolically in the system. -""" - -const PROBLEM_INTERNALS_HEADER = """ -# Extended docs - -The following API is internal and may change or be removed without notice. Its usage is -highly discouraged. -""" - -const INTERNAL_INITIALIZEPROB_KWARGS = """ -- `time_dependent_init`: Whether to build a time-dependent initialization for the problem. A - time-dependent initialization solves for a consistent `u0`, whereas a time-independent one - only runs parameter initialization. -- `algebraic_only`: Whether to build the initialization problem using only algebraic equations. -- `allow_incomplete`: Whether to allow incomplete initialization problems. -""" - -const PROBLEM_INTERNAL_KWARGS = """ -- `build_initializeprob`: If `false`, avoids building the initialization problem. -- `check_length`: Whether to check the number of equations along with number of unknowns and - length of `u0` vector for consistency. If `false`, do not check with equations. This is - forwarded to `check_eqs_u0`. -$INTERNAL_INITIALIZEPROB_KWARGS -""" - -function problem_ctors(prob, istd) - if istd - """ - SciMLBase.$prob(sys::System, op, tspan::NTuple{2}; kwargs...) - SciMLBase.$prob{iip}(sys::System, op, tspan::NTuple{2}; kwargs...) - SciMLBase.$prob{iip, specialize}(sys::System, op, tspan::NTuple{2}; kwargs...) - """ - else - """ - SciMLBase.$prob(sys::System, op; kwargs...) - SciMLBase.$prob{iip}(sys::System, op; kwargs...) - SciMLBase.$prob{iip, specialize}(sys::System, op; kwargs...) - """ - end -end - -function problem_ctors(prob::Type{<:SemilinearODEProblem}, istd) - @assert istd - """ - SciMLBase.$prob(sys::System, op, tspan::NTuple{2}; kwargs...) - SciMLBase.$prob{iip}(sys::System, op, tspan::NTuple{2}; kwargs...) - SciMLBase.$prob{iip, specialize}(sys::System, op, tspan::NTuple{2}; stiff_linear = true, stiff_quadratic = false, stiff_nonlinear = false, kwargs...) - """ -end - -function prob_fun_common_kwargs(T, istd) - return """ - - `check_compatibility`: Whether to check if the given system `sys` contains all the - information necessary to create a `$T` and no more. If disabled, assumes that `sys` - at least contains the necessary information. - - `expression`: `Val{true}` to return an `Expr` that constructs the corresponding - problem instead of the problem itself. `Val{false}` otherwise. - $(istd ? " Constructing the expression does not support callbacks" : "") - """ -end - -function problem_docstring(prob, func, istd; init = true, extra_body = "", - extra_kwargs = "", extra_kwargs_desc = "") - if func isa DataType - func = "`$func`" - end - return """ - $(problem_ctors(prob, istd)) - - Build a `$prob` given a system `sys` and operating point `op` - $(istd ? " and timespan `tspan`" : ""). `iip` is a boolean indicating whether the - problem should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` subtype - indicating the level of specialization of the $func. The operating point should be an - iterable collection of key-value pairs mapping variables/parameters in the system to the - (initial) values they should take in `$prob`. Any values not provided will fallback to - the corresponding default (if present). - - $(init ? istd ? TIME_DEPENDENT_INIT : TIME_INDEPENDENT_INIT : "") - - $extra_body - - # Keyword arguments - - $PROBLEM_KWARGS - $(istd ? TIME_DEPENDENT_PROBLEM_KWARGS : "") - $(prob_fun_common_kwargs(prob, istd)) - $(extra_kwargs) - All other keyword arguments are forwarded to the $func constructor. - $(extra_kwargs_desc) - - $PROBLEM_INTERNALS_HEADER - - $PROBLEM_INTERNAL_KWARGS - """ -end - -const TIME_DEPENDENT_INIT = """ -ModelingToolkit will build an initialization problem where all initial values for -unknowns or observables of `sys` (either explicitly provided or in defaults) will -be constraints. To remove an initial condition in the defaults (without providing -a replacement) give the corresponding variable a value of `nothing` in the operating -point. The initialization problem will also run parameter initialization. See the -[Initialization](@ref initialization) documentation for more information. -""" - -const TIME_INDEPENDENT_INIT = """ -ModelingToolkit will build an initialization problem that will run parameter -initialization. Since it does not solve for initial values of unknowns, observed -equations will not be initialization constraints. If an initialization equation -of the system must involve the initial value of an unknown `x`, it must be used as -`Initial(x)` in the equation. For example, an equation to be used to solve for parameter -`p` in terms of unknowns `x` and `y` must be provided as `Initial(x) + Initial(y) ~ p` -instead of `x + y ~ p`. See the [Initialization](@ref initialization) documentation -for more information. -""" - -const BV_EXTRA_BODY = """ -Boundary value conditions are supplied to Systems in the form of a list of constraints. -These equations should specify values that state variables should take at specific points, -as in `x(0.5) ~ 1`). More general constraints that should hold over the entire solution, -such as `x(t)^2 + y(t)^2`, should be specified as one of the equations used to build the -`System`. - -If a `System` without `constraints` is specified, it will be treated as an initial value problem. - -```julia - @parameters g t_c = 0.5 - @variables x(..) y(t) λ(t) - eqs = [D(D(x(t))) ~ λ * x(t) - D(D(y)) ~ λ * y - g - x(t)^2 + y^2 ~ 1] - cstr = [x(0.5) ~ 1] - @mtkcompile pend = System(eqs, t; constraints = cstrs) - - tspan = (0.0, 1.5) - u0map = [x(t) => 0.6, y => 0.8] - parammap = [g => 1] - guesses = [λ => 1] - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) -``` - -If the `System` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting -`BVProblem` must be solved using BVDAE solvers, such as Ascher. -""" - const SEMILINEAR_EXTRA_BODY = """ This is a special form of an ODE which uses a `SplitFunction` internally. The equations are separated into linear, quadratic and general terms and phrased as matrix operations. See @@ -227,21 +28,7 @@ non-`nothing`. In other words, both of the functions in the split form must be n """ for (mod, prob, func, istd, kws) in [ - (SciMLBase, :ODEProblem, ODEFunction, true, (;)), - (SciMLBase, :SteadyStateProblem, ODEFunction, false, (;)), - (SciMLBase, :BVProblem, ODEFunction, true, - (; init = false, extra_body = BV_EXTRA_BODY)), - (SciMLBase, :DAEProblem, DAEFunction, true, (;)), - (SciMLBase, :DDEProblem, DDEFunction, true, (;)), - (SciMLBase, :SDEProblem, SDEFunction, true, (;)), - (SciMLBase, :SDDEProblem, SDDEFunction, true, (;)), - (JumpProcesses, :JumpProblem, "inner SciMLFunction", true, (; init = false)), - (SciMLBase, :DiscreteProblem, DiscreteFunction, true, (;)), - (SciMLBase, :ImplicitDiscreteProblem, ImplicitDiscreteFunction, true, (;)), - (SciMLBase, :NonlinearProblem, NonlinearFunction, false, (;)), - (SciMLBase, :NonlinearLeastSquaresProblem, NonlinearFunction, false, (;)), (SciMLBase, :SCCNonlinearProblem, NonlinearFunction, false, (; init = false)), - (SciMLBase, :OptimizationProblem, OptimizationFunction, false, (; init = false)), (ModelingToolkit, :SemilinearODEProblem, :SemilinearODEFunction, @@ -253,149 +40,10 @@ for (mod, prob, func, istd, kws) in [ for (k, v) in pairs(kws) push!(kwexpr.args, Expr(:kw, k, v)) end - @eval @doc problem_docstring($kwexpr, $mod.$prob, $func, $istd) $mod.$prob -end - -function function_docstring( - func, istd, optionals; extra_body = "", extra_kwargs = "", extra_kwargs_desc = "") - return """ - $func(sys::System; kwargs...) - $func{iip}(sys::System; kwargs...) - $func{iip, specialize}(sys::System; kwargs...) - - Create a `$func` from the given `sys`. `iip` is a boolean indicating whether the - function should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` - subtype indicating the level of specialization of the $func. - - $(extra_body) - - Beyond the arguments listed below, this constructor accepts all keyword arguments - supported by the DifferentialEquations.jl `solve` function. For a complete list - and detailed descriptions, see the [DifferentialEquations.jl solve documentation](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/). - - # Keyword arguments - - - `u0`: The `u0` vector for the corresponding problem, if available. Can be obtained - using [`ModelingToolkit.get_u0`](@ref). - - `p`: The parameter object for the corresponding problem, if available. Can be obtained - using [`ModelingToolkit.get_p`](@ref). - $(istd ? TIME_DEPENDENT_FUNCTION_KWARGS : "") - $EVAL_EXPR_MOD_KWARGS - - `checkbounds`: Whether to enable bounds checking in the generated code. - - `simplify`: Whether to `simplify` any symbolically computed jacobians/hessians/etc. - - `cse`: Whether to enable Common Subexpression Elimination (CSE) on the generated code. - This typically improves performance of the generated code but reduces readability. - - `sparse`: Whether to generate jacobian/hessian/etc. functions that return/operate on - sparse matrices. Also controls whether the mass matrix is sparse, wherever applicable. - $(prob_fun_common_kwargs(func, istd)) - $(process_optional_function_kwargs(optionals)) - $(extra_kwargs) - - `kwargs...`: Additional keyword arguments passed to the solver - - All other keyword arguments are forwarded to the `$func` struct constructor. - $(extra_kwargs_desc) - """ -end - -const TIME_DEPENDENT_FUNCTION_KWARGS = """ -- `t`: The initial time for the corresponding problem, if available. -""" - -const JAC_KWARGS = """ -- `jac`: Whether to symbolically compute and generate code for the jacobian function. -""" - -const TGRAD_KWARGS = """ -- `tgrad`: Whether to symbolically compute and generate code for the `tgrad` function. -""" - -const SPARSITY_KWARGS = """ -- `sparsity`: Whether to provide symbolically compute and provide sparsity patterns for the - jacobian/hessian/etc. -""" - -const RESID_PROTOTYPE_KWARGS = """ -- `resid_prototype`: The prototype of the residual function `f` for a problem involving a - nonlinear solve where the residual and `u0` have different sizes. -""" - -const GRAD_KWARGS = """ -- `grad`: Whether the symbolically compute and generate code for the gradient of the cost - function with respect to unknowns. -""" - -const HESS_KWARGS = """ -- `hess`: Whether to symbolically compute and generate code for the hessian function. -""" - -const CONSH_KWARGS = """ -- `cons_h`: Whether to symbolically compute and generate code for the hessian function of - constraints. Since the constraint function is vector-valued, the hessian is a vector - of hessian matrices. -""" - -const CONSJ_KWARGS = """ -- `cons_j`: Whether to symbolically compute and generate code for the jacobian function of - constraints. -""" - -const CONSSPARSE_KWARGS = """ -- `cons_sparse`: Identical to the `sparse` keyword, but specifically for jacobian/hessian - functions of the constraints. -""" - -const INPUTFN_KWARGS = """ -- `inputs`: The variables in the input vector. The system must have been simplified using - `mtkcompile` with these variables passed as `inputs`. -- `disturbance_inputs`: The disturbance input variables. The system must have been - simplified using `mtkcompile` with these variables passed as `disturbance_inputs`. -""" - -const CONTROLJAC_KWARGS = """ -- `controljac`: Whether to symbolically compute and generate code for the jacobian of - the ODE with respect to the inputs. -""" - -const OPTIONAL_FN_KWARGS_DICT = Dict( - :jac => JAC_KWARGS, - :tgrad => TGRAD_KWARGS, - :sparsity => SPARSITY_KWARGS, - :resid_prototype => RESID_PROTOTYPE_KWARGS, - :grad => GRAD_KWARGS, - :hess => HESS_KWARGS, - :cons_h => CONSH_KWARGS, - :cons_j => CONSJ_KWARGS, - :cons_sparse => CONSSPARSE_KWARGS, - :inputfn => INPUTFN_KWARGS, - :controljac => CONTROLJAC_KWARGS -) - -const SPARSITY_OPTIONALS = Set([:jac, :hess, :cons_h, :cons_j, :controljac]) - -const CONS_SPARSITY_OPTIONALS = Set([:cons_h, :cons_j]) - -function process_optional_function_kwargs(choices::Vector{Symbol}) - if !isdisjoint(choices, SPARSITY_OPTIONALS) - push!(choices, :sparsity) - end - if !isdisjoint(choices, CONS_SPARSITY_OPTIONALS) - push!(choices, :cons_sparse) - end - join(map(Base.Fix1(getindex, OPTIONAL_FN_KWARGS_DICT), choices), "\n") + @eval @doc MTKBase.problem_docstring($kwexpr, $mod.$prob, $func, $istd) $mod.$prob end for (mod, func, istd, optionals, kws) in [ - (SciMLBase, :ODEFunction, true, [:jac, :tgrad], (;)), - (SciMLBase, :ODEInputFunction, true, [:inputfn, :jac, :tgrad, :controljac], (;)), - (SciMLBase, :DAEFunction, true, [:jac, :tgrad], (;)), - (SciMLBase, :DDEFunction, true, Symbol[], (;)), - (SciMLBase, :SDEFunction, true, [:jac, :tgrad], (;)), - (SciMLBase, :SDDEFunction, true, Symbol[], (;)), - (SciMLBase, :DiscreteFunction, true, Symbol[], (;)), - (SciMLBase, :ImplicitDiscreteFunction, true, Symbol[], (;)), - (SciMLBase, :NonlinearFunction, false, [:resid_prototype, :jac], (;)), - (SciMLBase, :IntervalNonlinearFunction, false, Symbol[], (;)), - (SciMLBase, :OptimizationFunction, false, [:jac, :grad, :hess, :cons_h, :cons_j], (;)), (ModelingToolkit, :SemilinearODEFunction, true, @@ -407,86 +55,5 @@ for (mod, func, istd, optionals, kws) in [ for (k, v) in pairs(kws) push!(kwexpr.args, Expr(:kw, k, v)) end - @eval @doc function_docstring($kwexpr, $mod.$func, $istd, $optionals) $mod.$func + @eval @doc MTKBase.function_docstring($kwexpr, $mod.$func, $istd, $optionals) $mod.$func end - -@doc """ - SciMLBase.HomotopyNonlinearFunction(sys::System; kwargs...) - SciMLBase.HomotopyNonlinearFunction{iip}(sys::System; kwargs...) - SciMLBase.HomotopyNonlinearFunction{iip, specialize}(sys::System; kwargs...) - -Create a `HomotopyNonlinearFunction` from the given `sys`. `iip` is a boolean indicating -whether the function should be in-place. `specialization` is a `SciMLBase.AbstractSpecalize` -subtype indicating the level of specialization of the $func. - -# Keyword arguments - -- `u0`: The `u0` vector for the corresponding problem, if available. Can be obtained - using [`ModelingToolkit.get_u0`](@ref). -- `p`: The parameter object for the corresponding problem, if available. Can be obtained - using [`ModelingToolkit.get_p`](@ref). -$EVAL_EXPR_MOD_KWARGS -- `checkbounds`: Whether to enable bounds checking in the generated code. -- `simplify`: Whether to `simplify` any symbolically computed jacobians/hessians/etc. -- `cse`: Whether to enable Common Subexpression Elimination (CSE) on the generated code. - This typically improves performance of the generated code but reduces readability. -- `fraction_cancel_fn`: The function to use to simplify fractions in the polynomial - expression. A more powerful function can increase processing time but be able to - eliminate more rational functions, thus improving solve time. Should be a function that - takes a symbolic expression containing zero or more fraction expressions and returns the - simplified expression. While this defaults to `SymbolicUtils.simplify_fractions`, a viable - alternative is `SymbolicUtils.quick_cancel` - -All keyword arguments are forwarded to the wrapped `NonlinearFunction` constructor. -""" SciMLBase.HomotopyNonlinearFunction - -@doc """ - SciMLBase.IntervalNonlinearProblem(sys::System, uspan::NTuple{2}, parammap = SciMLBase.NullParameters(); kwargs...) - -Create an `IntervalNonlinearProblem` from the given `sys`. This is only valid for a system -of nonlinear equations with a single equation and unknown. `uspan` is the interval in which -the root is to be found, and `parammap` is an iterable collection of key-value pairs -providing values for the parameters in the system. - -$TIME_INDEPENDENT_INIT - -# Keyword arguments - -$PROBLEM_KWARGS -$(prob_fun_common_kwargs(IntervalNonlinearProblem, false)) - -All other keyword arguments are forwarded to the `IntervalNonlinearFunction` constructor. - -$PROBLEM_INTERNALS_HEADER - -$PROBLEM_INTERNAL_KWARGS -""" SciMLBase.IntervalNonlinearProblem - -@doc """ - SciMLBase.LinearProblem(sys::System, op; kwargs...) - SciMLBase.LinearProblem{iip}(sys::System, op; kwargs...) - -Build a `LinearProblem` given a system `sys` and operating point `op`. `iip` is a boolean -indicating whether the problem should be in-place. The operating point should be an -iterable collection of key-value pairs mapping variables/parameters in the system to the -(initial) values they should take in `LinearProblem`. Any values not provided will -fallback to the corresponding default (if present). - -Note that since `u0` is optional for `LinearProblem`, values of unknowns do not need to be -specified in `op` to create a `LinearProblem`. In such a case, `prob.u0` will be `nothing` -and attempting to symbolically index the problem with an unknown, observable, or expression -depending on unknowns/observables will error. - -Updating the parameters automatically updates the `A` and `b` arrays. - -# Keyword arguments - -$PROBLEM_KWARGS -$(prob_fun_common_kwargs(LinearProblem, false)) - -All other keyword arguments are forwarded to the $func constructor. - -$PROBLEM_INTERNALS_HEADER - -$PROBLEM_INTERNAL_KWARGS -""" SciMLBase.LinearProblem diff --git a/src/problems/sccnonlinearproblem.jl b/src/problems/sccnonlinearproblem.jl index 20afd4d7a8..a3b5f49a25 100644 --- a/src/problems/sccnonlinearproblem.jl +++ b/src/problems/sccnonlinearproblem.jl @@ -41,11 +41,11 @@ function SCCNonlinearFunction{iip}( eval_module = @__MODULE__, cse = true, kwargs...) where {iip} ps = parameters(sys; initial_parameters = true) subsys = System( - _eqs, _dvs, ps; observed = _obs, name = nameof(sys), defaults = defaults(sys)) - @set! subsys.parameter_dependencies = parameter_dependencies(sys) + _eqs, _dvs, ps; observed = _obs, name = nameof(sys), bindings = bindings(sys), initial_conditions = initial_conditions(sys)) if get_index_cache(sys) !== nothing @set! subsys.index_cache = subset_unknowns_observed( get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) + @set! subsys.parameter_bindings_graph = ParameterBindingsGraph(subsys) @set! subsys.complete = true end # generate linear problem instead @@ -263,7 +263,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::System, op; eval_expression = f for (i, (f, vscc)) in enumerate(zip(nlfuns, var_sccs)) _u0 = SymbolicUtils.Code.create_array( typeof(u0), eltype(u0), Val(1), Val(length(vscc)), u0[vscc]...) - symbolic_idxs = findall(x -> symbolic_type(x) != NotSymbolic(), _u0) + symbolic_idxs = findall(x -> x === nothing || symbolic_type(x) !== NotSymbolic(), _u0) if f isa LinearFunction _u0 = isempty(symbolic_idxs) ? _u0 : zeros(u0_eltype, length(_u0)) _u0 = u0_eltype.(_u0) diff --git a/src/problems/odeproblem.jl b/src/problems/semilinearodeproblem.jl similarity index 57% rename from src/problems/odeproblem.jl rename to src/problems/semilinearodeproblem.jl index b89da04d49..9875fb6c84 100644 --- a/src/problems/odeproblem.jl +++ b/src/problems/semilinearodeproblem.jl @@ -1,113 +1,3 @@ -@fallback_iip_specialize function SciMLBase.ODEFunction{iip, spec}( - sys::System; u0 = nothing, p = nothing, tgrad = false, jac = false, - t = nothing, eval_expression = false, eval_module = @__MODULE__, sparse = false, - steady_state = false, checkbounds = false, sparsity = false, analytic = nothing, - simplify = false, cse = true, initialization_data = nothing, expression = Val{false}, - check_compatibility = true, nlstep = false, nlstep_compile = true, nlstep_scc = false, - kwargs...) where {iip, spec} - check_complete(sys, ODEFunction) - check_compatibility && check_compatible_system(ODEFunction, sys) - - f = generate_rhs(sys; expression, wrap_gfw = Val{true}, - eval_expression, eval_module, checkbounds = checkbounds, cse, - kwargs...) - - if spec === SciMLBase.FunctionWrapperSpecialize && iip - if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on ODEFunction.") - end - if expression == Val{true} - f = :($(SciMLBase.wrapfun_iip)($f, ($u0, $u0, $p, $t))) - else - f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) - end - end - - if tgrad - _tgrad = generate_tgrad( - sys; expression, wrap_gfw = Val{true}, - simplify, cse, eval_expression, eval_module, checkbounds, kwargs...) - else - _tgrad = nothing - end - - if jac - _jac = generate_jacobian( - sys; expression, wrap_gfw = Val{true}, - simplify, sparse, cse, eval_expression, eval_module, checkbounds, kwargs...) - else - _jac = nothing - end - - M = calculate_massmatrix(sys) - _M = concrete_massmatrix(M; sparse, u0) - - if nlstep - ode_nlstep = generate_ODENLStepData(sys, u0, p, M, nlstep_compile, nlstep_scc) - else - ode_nlstep = nothing - end - - observedfun = ObservedFunctionCache( - sys; expression, steady_state, eval_expression, eval_module, checkbounds, cse) - - _W_sparsity = W_sparsity(sys) - W_prototype = calculate_W_prototype(_W_sparsity; u0, sparse) - - args = (; f) - kwargs = (; - sys = sys, - jac = _jac, - tgrad = _tgrad, - mass_matrix = _M, - jac_prototype = W_prototype, - observed = observedfun, - sparsity = sparsity ? _W_sparsity : nothing, - analytic = analytic, - initialization_data, - nlstep_data = ode_nlstep) - - maybe_codegen_scimlfn(expression, ODEFunction{iip, spec}, args; kwargs...) -end - -@fallback_iip_specialize function SciMLBase.ODEProblem{iip, spec}( - sys::System, op, tspan; - callback = nothing, check_length = true, eval_expression = false, - expression = Val{false}, eval_module = @__MODULE__, check_compatibility = true, - kwargs...) where {iip, spec} - check_complete(sys, ODEProblem) - check_compatibility && check_compatible_system(ODEProblem, sys) - - f, u0, - p = process_SciMLProblem(ODEFunction{iip, spec}, sys, op; - t = tspan !== nothing ? tspan[1] : tspan, check_length, eval_expression, - eval_module, expression, check_compatibility, kwargs...) - - kwargs = process_kwargs( - sys; expression, callback, eval_expression, eval_module, op, kwargs...) - - ptype = getmetadata(sys, ProblemTypeCtx, StandardODEProblem()) - args = (; f, u0, tspan, p, ptype) - maybe_codegen_scimlproblem(expression, ODEProblem{iip}, args; kwargs...) -end - -@fallback_iip_specialize function DiffEqBase.SteadyStateProblem{iip, spec}( - sys::System, op; check_length = true, check_compatibility = true, - expression = Val{false}, kwargs...) where {iip, spec} - check_complete(sys, SteadyStateProblem) - check_compatibility && check_compatible_system(SteadyStateProblem, sys) - - f, u0, - p = process_SciMLProblem(ODEFunction{iip}, sys, op; - steady_state = true, check_length, check_compatibility, expression, - time_dependent_init = false, kwargs...) - - kwargs = process_kwargs(sys; expression, kwargs...) - args = (; f, u0, p) - - maybe_codegen_scimlproblem(expression, SteadyStateProblem{iip}, args; kwargs...) -end - @fallback_iip_specialize function SemilinearODEFunction{iip, specialize}( sys::System; u0 = nothing, p = nothing, t = nothing, semiquadratic_form = nothing, @@ -206,7 +96,7 @@ end _u0_eltype = something(u0_eltype, floatT) guess = copy(guesses(sys)) - defs = copy(defaults(sys)) + defs = copy(initial_conditions(sys)) if A !== nothing guess[linear_matrix_param] = fill(NaN, size(A)) defs[linear_matrix_param] = A @@ -220,7 +110,7 @@ end defs[diffcache_par] = DiffCache(zeros(DiffEqBase.value(_u0_eltype), cachelen)) end @set! sys.guesses = guess - @set! sys.defaults = defs + @set! sys.initial_conditions = defs f, u0, p = process_SciMLProblem(SemilinearODEFunction{iip, spec}, sys, op; @@ -266,10 +156,8 @@ function add_semiquadratic_parameters(sys::System, A, B, C) return sys end -function check_compatible_system( - T::Union{Type{ODEFunction}, Type{ODEProblem}, Type{DAEFunction}, - Type{DAEProblem}, Type{SteadyStateProblem}, Type{SemilinearODEFunction}, - Type{SemilinearODEProblem}}, +function MTKBase.check_compatible_system( + T::Union{Type{SemilinearODEFunction}, Type{SemilinearODEProblem}}, sys::System) check_time_dependent(sys, T) check_not_dde(sys) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 1cd4c7615f..8b144ead81 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -3,52 +3,46 @@ module StructuralTransformations using Setfield: @set!, @set using UnPack: @unpack -using Symbolics: unwrap, linear_expansion, fast_substitute +using Symbolics: unwrap, linear_expansion, VartypeT, SymbolicT import Symbolics using SymbolicUtils +using SymbolicUtils: BSImpl using SymbolicUtils.Code using SymbolicUtils.Rewriters -using SymbolicUtils: maketerm, iscall +using SymbolicUtils: maketerm, iscall, symtype +import SymbolicUtils as SU +import Moshi using ModelingToolkit -using ModelingToolkit: System, AbstractSystem, var_from_nested_derivative, Differential, - unknowns, equations, vars, Symbolic, diff2term_with_unit, - shift2term_with_unit, value, - operation, arguments, Sym, Term, simplify, symbolic_linear_solve, +using ModelingToolkitBase: System, AbstractSystem, var_from_nested_derivative, Differential, + unknowns, equations, diff2term_with_unit, + value, + operation, arguments, simplify, symbolic_linear_solve, isdiffeq, isdifferential, isirreducible, empty_substitutions, get_substitutions, get_tearing_state, get_iv, independent_variables, - has_tearing_state, defaults, InvalidSystemException, + has_tearing_state, InvalidSystemException, ExtraEquationsSystemException, ExtraVariablesSystemException, - vars!, invalidate_cache!, - vars!, invalidate_cache!, Shift, - IncrementalCycleTracker, add_edge_checked!, topological_sort, + invalidate_cache!, Shift, + topological_sort, filter_kwargs, lower_varname_with_unit, - lower_shift_varname_with_unit, setio, SparseMatrixCLIL, - get_fullvars, has_equations, observed, - Schedule, schedule, iscomplete, get_schedule + setio, + has_equations, observed, + Schedule, schedule, iscomplete, get_schedule, VariableUnshifted, + VariableShift, DerivativeDict, shift2term, simplify_shifts, + distribute_shift -using ModelingToolkit.BipartiteGraphs -import .BipartiteGraphs: invview, complete -import ModelingToolkit: var_derivative!, var_derivative_graph! +using BipartiteGraphs +import BipartiteGraphs: invview, complete, IncrementalCycleTracker, add_edge_checked! using Graphs -using ModelingToolkit: algeqs, EquationsView, - SystemStructure, TransformationState, TearingState, - mtkcompile!, - isdiffvar, isdervar, isalgvar, isdiffeq, algeqs, is_only_discrete, - dervars_range, diffvars_range, algvars_range, - DiffGraph, complete!, - get_fullvars, system_subset -using SymbolicIndexingInterface: symbolic_type, ArraySymbolic, NotSymbolic +using ModelingToolkit: mtkcompile! +using SymbolicIndexingInterface: symbolic_type, ArraySymbolic, NotSymbolic, getname using ModelingToolkit.DiffEqBase using ModelingToolkit.StaticArrays -using RuntimeGeneratedFunctions: @RuntimeGeneratedFunction, - RuntimeGeneratedFunctions, - drop_expr - -RuntimeGeneratedFunctions.init(@__MODULE__) +import Symbolics: Num, Arr, CallAndWrap +import CommonSolve using SparseArrays @@ -56,24 +50,29 @@ using SimpleNonlinearSolve using DocStringExtensions -export tearing, dae_index_lowering, check_consistency +import ModelingToolkitBase as MTKBase +import StateSelection +import StateSelection: CLIL, SelectedState +import ModelingToolkitTearing as MTKTearing +using ModelingToolkitTearing: TearingState, SystemStructure, ReassembleAlgorithm, + DefaultReassembleAlgorithm + +export tearing, dae_index_lowering export dummy_derivative -export sorted_incidence_matrix, - pantelides!, pantelides_reassemble, find_solvables!, - linear_subsys_adjmat! +export sorted_incidence_matrix, pantelides_reassemble, find_solvables! export tearing_substitution -export torn_system_jacobian_sparsity -export full_equations export but_ordered_incidence, lowest_order_variable_mask, highest_order_variable_mask -export computed_highest_diff_variables -export shift2term, lower_shift_varname, simplify_shifts, distribute_shift include("utils.jl") -include("tearing.jl") include("pantelides.jl") -include("bipartite_tearing/modia_tearing.jl") + +function tearing_substitution(sys::AbstractSystem; kwargs...) + neweqs = full_equations(sys::AbstractSystem; kwargs...) + @set! sys.eqs = neweqs + # @set! sys.substitutions = nothing + @set! sys.schedule = nothing +end + include("symbolics_tearing.jl") -include("partial_state_selection.jl") -include("codegen.jl") end # module diff --git a/src/structural_transformation/bareiss.jl b/src/structural_transformation/bareiss.jl deleted file mode 100644 index 602f656c27..0000000000 --- a/src/structural_transformation/bareiss.jl +++ /dev/null @@ -1,358 +0,0 @@ -# Keeps compatibility with bariess code moved to Base/stdlib on older releases - -using LinearAlgebra -using SparseArrays -using SparseArrays: AbstractSparseMatrixCSC, getcolptr - -macro swap(a, b) - esc(:(($a, $b) = ($b, $a))) -end - -# https://github.com/JuliaLang/julia/pull/42678 -@static if VERSION > v"1.8.0-DEV.762" - import Base: swaprows! -else - function swaprows!(a::AbstractMatrix, i, j) - i == j && return - rows = axes(a, 1) - @boundscheck i in rows || throw(BoundsError(a, (:, i))) - @boundscheck j in rows || throw(BoundsError(a, (:, j))) - for k in axes(a, 2) - @inbounds a[i, k], a[j, k] = a[j, k], a[i, k] - end - end - function Base.circshift!(a::AbstractVector, shift::Integer) - n = length(a) - n == 0 && return - shift = mod(shift, n) - shift == 0 && return - reverse!(a, 1, shift) - reverse!(a, shift + 1, length(a)) - reverse!(a) - return a - end - function Base.swapcols!(A::AbstractSparseMatrixCSC, i, j) - i == j && return - - # For simplicity, let i denote the smaller of the two columns - j < i && @swap(i, j) - - colptr = getcolptr(A) - irow = colptr[i]:(colptr[i + 1] - 1) - jrow = colptr[j]:(colptr[j + 1] - 1) - - function rangeexchange!(arr, irow, jrow) - if length(irow) == length(jrow) - for (a, b) in zip(irow, jrow) - @inbounds @swap(arr[i], arr[j]) - end - return - end - # This is similar to the triple-reverse tricks for - # circshift!, except that we have three ranges here, - # so it ends up being 4 reverse calls (but still - # 2 overall reversals for the memory range). Like - # circshift!, there's also a cycle chasing algorithm - # with optimal memory complexity, but the performance - # tradeoffs against this implementation are non-trivial, - # so let's just do this simple thing for now. - # See https://github.com/JuliaLang/julia/pull/42676 for - # discussion of circshift!-like algorithms. - reverse!(@view arr[irow]) - reverse!(@view arr[jrow]) - reverse!(@view arr[(last(irow) + 1):(first(jrow) - 1)]) - reverse!(@view arr[first(irow):last(jrow)]) - end - rangeexchange!(rowvals(A), irow, jrow) - rangeexchange!(nonzeros(A), irow, jrow) - - if length(irow) != length(jrow) - @inbounds colptr[(i + 1):j] .+= length(jrow) - length(irow) - end - return nothing - end - function swaprows!(A::AbstractSparseMatrixCSC, i, j) - # For simplicity, let i denote the smaller of the two rows - j < i && @swap(i, j) - - rows = rowvals(A) - vals = nonzeros(A) - for col in 1:size(A, 2) - rr = nzrange(A, col) - iidx = searchsortedfirst(@view(rows[rr]), i) - has_i = iidx <= length(rr) && rows[rr[iidx]] == i - - jrange = has_i ? (iidx:last(rr)) : rr - jidx = searchsortedlast(@view(rows[jrange]), j) - has_j = jidx != 0 && rows[jrange[jidx]] == j - - if !has_j && !has_i - # Has neither row - nothing to do - continue - elseif has_i && has_j - # This column had both i and j rows - swap them - @swap(vals[rr[iidx]], vals[jrange[jidx]]) - elseif has_i - # Update the rowval and then rotate both nonzeros - # and the remaining rowvals into the correct place - rows[rr[iidx]] = j - jidx == 0 && continue - rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), -1) - circshift!(@view(rows[rotate_range]), -1) - else - # Same as i, but in the opposite direction - @assert has_j - rows[jrange[jidx]] = i - iidx > length(rr) && continue - rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), 1) - circshift!(@view(rows[rotate_range]), 1) - end - end - return nothing - end -end - -function bareiss_update!(zero!, M::StridedMatrix, k, swapto, pivot, - prev_pivot::Base.BitInteger) - flag = zero(prev_pivot) - prev_pivot = Base.MultiplicativeInverses.SignedMultiplicativeInverse(prev_pivot) - @inbounds for i in (k + 1):size(M, 2) - Mki = M[k, i] - @simd ivdep for j in (k + 1):size(M, 1) - M[j, i], r = divrem(M[j, i] * pivot - M[j, k] * Mki, prev_pivot) - flag = flag | r - end - end - iszero(flag) || error("Overflow occurred") - zero!(M, (k + 1):size(M, 1), k) -end - -function bareiss_update!(zero!, M::StridedMatrix, k, swapto, pivot, prev_pivot) - @inbounds for i in (k + 1):size(M, 2), j in (k + 1):size(M, 1) - - M[j, i] = exactdiv(M[j, i] * pivot - M[j, k] * M[k, i], prev_pivot) - end - zero!(M, (k + 1):size(M, 1), k) -end - -@views function bareiss_update!(zero!, M::AbstractMatrix, k, swapto, pivot, prev_pivot) - if prev_pivot isa Base.BitInteger - prev_pivot = Base.MultiplicativeInverses.SignedMultiplicativeInverse(prev_pivot) - end - V = M[(k + 1):end, (k + 1):end] - V .= exactdiv.(V .* pivot .- M[(k + 1):end, k] * M[k, (k + 1):end]', prev_pivot) - zero!(M, (k + 1):size(M, 1), k) - if M isa AbstractSparseMatrixCSC - dropzeros!(M) - end -end - -function bareiss_update_virtual_colswap!(zero!, M::AbstractMatrix, k, swapto, pivot, - prev_pivot) - if prev_pivot isa Base.BitInteger - prev_pivot = Base.MultiplicativeInverses.SignedMultiplicativeInverse(prev_pivot) - end - V = @view M[(k + 1):end, :] - V .= @views exactdiv.(V .* pivot .- M[(k + 1):end, swapto[2]] * M[k, :]', prev_pivot) - zero!(M, (k + 1):size(M, 1), swapto[2]) -end - -bareiss_zero!(M, i, j) = M[i, j] .= zero(eltype(M)) - -function find_pivot_col(M, i) - p = findfirst(!iszero, @view M[i, i:end]) - p === nothing && return nothing - idx = CartesianIndex(i, p + i - 1) - (idx, M[idx]) -end - -function find_pivot_any(M, i) - p = findfirst(!iszero, @view M[i:end, i:end]) - p === nothing && return nothing - idx = p + CartesianIndex(i - 1, i - 1) - (idx, M[idx]) -end - -const bareiss_colswap = (Base.swapcols!, swaprows!, bareiss_update!, bareiss_zero!) -const bareiss_virtcolswap = ((M, i, j) -> nothing, swaprows!, - bareiss_update_virtual_colswap!, bareiss_zero!) - -""" - bareiss!(M, [swap_strategy]) - -Perform Bareiss's fraction-free row-reduction algorithm on the matrix `M`. -Optionally, a specific pivoting method may be specified. - -swap_strategy is an optional argument that determines how the swapping of rows and columns is performed. -bareiss_colswap (the default) swaps the columns and rows normally. -bareiss_virtcolswap pretends to swap the columns which can be faster for sparse matrices. -""" -function bareiss!(M::AbstractMatrix{T}, swap_strategy = bareiss_colswap; - find_pivot = find_pivot_any, column_pivots = nothing) where {T} - swapcols!, swaprows!, update!, zero! = swap_strategy - prev = one(eltype(M)) - n = size(M, 1) - pivot = one(T) - column_permuted = false - for k in 1:n - r = find_pivot(M, k) - r === nothing && return (k - 1, pivot, column_permuted) - (swapto, pivot) = r - if column_pivots !== nothing && k != swapto[2] - column_pivots[k] = swapto[2] - column_permuted |= true - end - if CartesianIndex(k, k) != swapto - swapcols!(M, k, swapto[2]) - swaprows!(M, k, swapto[1]) - end - update!(zero!, M, k, swapto, pivot, prev) - prev = pivot - end - return (n, pivot, column_permuted) -end - -function nullspace(A; col_order = nothing) - n = size(A, 2) - workspace = zeros(Int, 2 * n) - column_pivots = @view workspace[1:n] - pivots_cache = @view workspace[(n + 1):(2n)] - @inbounds for i in 1:n - column_pivots[i] = i - end - B = copy(A) - (rank, d, column_permuted) = bareiss!(B; column_pivots) - reduce_echelon!(B, rank, d, pivots_cache) - - # The first rank entries in col_order are columns that give a basis - # for the column space. The remainder give the free variables. - if col_order !== nothing - resize!(col_order, size(A, 2)) - col_order .= 1:size(A, 2) - for (i, cp) in enumerate(column_pivots) - @swap(col_order[i], col_order[cp]) - end - end - - fill!(pivots_cache, 0) - N = ModelingToolkit.reduced_echelon_nullspace(rank, B, pivots_cache) - apply_inv_pivot_rows!(N, column_pivots) -end - -function apply_inv_pivot_rows!(M, ipiv) - for i in size(M, 1):-1:1 - swaprows!(M, i, ipiv[i]) - end - M -end - -### -### Modified from AbstractAlgebra.jl -### -### https://github.com/Nemocas/AbstractAlgebra.jl/blob/4803548c7a945f3f7bd8c63f8bb7c79fac92b11a/LICENSE.md -function reduce_echelon!(A::AbstractMatrix{T}, rank, d, - pivots_cache = zeros(Int, size(A, 2))) where {T} - m, n = size(A) - isreduced = true - @inbounds for i in 1:rank - for j in 1:(i - 1) - if A[j, i] != zero(T) - isreduced = false - @goto out - end - end - if A[i, i] != one(T) - isreduced = false - @goto out - end - end - @label out - @inbounds for i in (rank + 1):m, j in 1:n - - A[i, j] = zero(T) - end - isreduced && return A - - @inbounds if rank > 1 - t = zero(T) - q = zero(T) - d = -d - pivots = pivots_cache - np = rank - j = k = 1 - for i in 1:rank - while iszero(A[i, j]) - pivots[np + k] = j - j += 1 - k += 1 - end - pivots[i] = j - j += 1 - end - while k <= n - rank - pivots[np + k] = j - j += 1 - k += 1 - end - for k in 1:(n - rank) - for i in (rank - 1):-1:1 - t = A[i, pivots[np + k]] * d - for j in (i + 1):rank - t += A[i, pivots[j]] * A[j, pivots[np + k]] + q - end - A[i, pivots[np + k]] = exactdiv(-t, A[i, pivots[i]]) - end - end - d = -d - for i in 1:rank - for j in 1:rank - if i == j - A[j, pivots[i]] = d - else - A[j, pivots[i]] = zero(T) - end - end - end - end - return A -end - -function reduced_echelon_nullspace(rank, A::AbstractMatrix{T}, - pivots_cache = zeros(Int, size(A, 2))) where {T} - n = size(A, 2) - nullity = n - rank - U = zeros(T, n, nullity) - @inbounds if rank == 0 - for i in 1:nullity - U[i, i] = one(T) - end - elseif nullity != 0 - pivots = @view pivots_cache[1:rank] - nonpivots = @view pivots_cache[(rank + 1):n] - j = k = 1 - for i in 1:rank - while iszero(A[i, j]) - nonpivots[k] = j - j += 1 - k += 1 - end - pivots[i] = j - j += 1 - end - while k <= nullity - nonpivots[k] = j - j += 1 - k += 1 - end - d = -A[1, pivots[1]] - for i in 1:nullity - for j in 1:rank - U[pivots[j], i] = A[j, nonpivots[i]] - end - U[nonpivots[i], i] = d - end - end - return U -end diff --git a/src/structural_transformation/bipartite_tearing/modia_tearing.jl b/src/structural_transformation/bipartite_tearing/modia_tearing.jl deleted file mode 100644 index b931c61137..0000000000 --- a/src/structural_transformation/bipartite_tearing/modia_tearing.jl +++ /dev/null @@ -1,137 +0,0 @@ -# This code is derived from the Modia project and is licensed as follows: -# https://github.com/ModiaSim/Modia.jl/blob/b61daad643ef7edd0c1ccce6bf462c6acfb4ad1a/LICENSE - -function try_assign_eq!(ict::IncrementalCycleTracker, vj::Integer, eq::Integer) - G = ict.graph - add_edge_checked!(ict, Iterators.filter(!=(vj), 𝑠neighbors(G.graph, eq)), vj) do G - G.matching[vj] = eq - G.ne += length(𝑠neighbors(G.graph, eq)) - 1 - end -end - -function try_assign_eq!(ict::IncrementalCycleTracker, vars, v_active, eq::Integer, - condition::F = _ -> true) where {F} - G = ict.graph - for vj in vars - (vj in v_active && G.matching[vj] === unassigned && condition(vj)) || continue - try_assign_eq!(ict, vj, eq) && return true - end - return false -end - -function tearEquations!(ict::IncrementalCycleTracker, Gsolvable, es::Vector{Int}, - v_active::BitSet, isder′::F) where {F} - check_der = isder′ !== nothing - if check_der - has_der = Ref(false) - isder = let has_der = has_der, isder′ = isder′ - v -> begin - r = isder′(v) - has_der[] |= r - r - end - end - end - # Heuristic: As a first pass, try to assign any equations that only have one - # solvable variable. - for only_single_solvable in (true, false) - for eq in es # iterate only over equations that are not in eSolvedFixed - vs = Gsolvable[eq] - ((length(vs) == 1) ⊻ only_single_solvable) && continue - if check_der - # if there're differentiated variables, then only consider them - try_assign_eq!(ict, vs, v_active, eq, isder) - if has_der[] - has_der[] = false - continue - end - end - try_assign_eq!(ict, vs, v_active, eq) - end - end - - return ict -end - -function tear_graph_block_modia!(var_eq_matching, ict, solvable_graph, eqs, vars, - isder::F) where {F} - tearEquations!(ict, solvable_graph.fadjlist, eqs, vars, isder) - for var in vars - var_eq_matching[var] = ict.graph.matching[var] - end - return nothing -end - -function build_var_eq_matching(structure::SystemStructure; - varfilter::F2, eqfilter::F3) where {F2, F3} - @unpack graph, solvable_graph = structure - var_eq_matching = maximal_matching(graph, eqfilter, varfilter, MatchedVarT) - matching_len = max(length(var_eq_matching), - maximum(x -> x isa Int ? x : 0, var_eq_matching, init = 0)) - return complete(var_eq_matching, matching_len), matching_len -end - -@kwdef struct ModiaTearing{F, F2, F3} - isder::F = nothing - varfilter::F2 = Returns(true) - eqfilter::F3 = Returns(true) -end - -function (alg::ModiaTearing)(structure::SystemStructure) - # It would be possible here to simply iterate over all variables and attempt to - # use tearEquations! to produce a matching that greedily selects the minimal - # number of torn variables. However, we can do this process faster if we first - # compute the strongly connected components. In the absence of cycles and - # non-solvability, a maximal matching on the original graph will give us an - # optimal assignment. However, even with cycles, we can use the maximal matching - # to give us a good starting point for a good matching and then proceed to - # reverse edges in each scc to improve the solution. Note that it is possible - # to have optimal solutions that cannot be found by this process. We will not - # find them here [TODO: It would be good to have an explicit example of this.] - - isder = alg.isder - varfilter = alg.varfilter - eqfilter = alg.eqfilter - @unpack graph, solvable_graph = structure - var_eq_matching, matching_len = build_var_eq_matching(structure; varfilter, eqfilter) - full_var_eq_matching = copy(var_eq_matching) - var_sccs = find_var_sccs(graph, var_eq_matching) - 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 - 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) - # 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 TearingResult(var_eq_matching, full_var_eq_matching, var_sccs), (;) -end diff --git a/src/structural_transformation/codegen.jl b/src/structural_transformation/codegen.jl deleted file mode 100644 index 37a5b380ac..0000000000 --- a/src/structural_transformation/codegen.jl +++ /dev/null @@ -1,115 +0,0 @@ -using LinearAlgebra - -using ModelingToolkit: process_events - -const MAX_INLINE_NLSOLVE_SIZE = 8 - -function torn_system_with_nlsolve_jacobian_sparsity(state, var_eq_matching, var_sccs, - nlsolve_scc_idxs, eqs_idxs, states_idxs) - graph = state.structure.graph - - # The sparsity pattern of `nlsolve(f, u, p)` w.r.t `p` is difficult to - # determine in general. Consider the "simplest" case, a linear system. We - # have - # A u = p. - # Clearly, the sparsity of `u` depends on the sparsity of both `p` and `A` - # in a non-trivial way. However, in the generic case, `u` is dense even when - # `A` and `p` are sparse. For instance - # - # ```julia - # julia> using Random, SparseArrays - # - # julia> A = sprand(MersenneTwister(1234), 100, 100, 0.1); - # - # julia> p = sprand(MersenneTwister(12345), 100, 0.05); - # - # julia> count(x->abs(x) < 1e-5, A \ Vector(p)) - # 0 - # ``` - # - # Let 𝑇 be the set of tearing variables and 𝑉 be the set of all *unknowns* in - # the residual equations. In the following code, we are going to assume the - # connection between 𝑇 (the `u` in from above) and 𝑉 ∖ 𝑇 (the `p` in from - # above) has full incidence. - # - # Note that as we are reducing algebraic equations numerically, it could be - # the case that a later partition (a BLT block) contains tearing variables - # from other partitions. - # - # We know that partitions are BLT ordered. Hence, the tearing variables in - # each partition is unique, and all unknowns in a partition must be - # either differential variables or algebraic tearing variables that are - # from previous partitions. Hence, we can build the dependency chain as we - # traverse the partitions. - - var_rename = ones(Int64, ndsts(graph)) - nlsolve_vars = Int[] - for i in nlsolve_scc_idxs, c in var_sccs[i] - - append!(nlsolve_vars, c) - for v in c - var_rename[v] = 0 - end - end - masked_cumsum!(var_rename) - - dig = DiCMOBiGraph{true}(graph, var_eq_matching) - - fused_var_deps = map(1:ndsts(graph)) do v - BitSet(v′ for v′ in neighborhood(dig, v, Inf; dir = :in) if var_rename[v′] != 0) - end - - for scc in var_sccs[nlsolve_scc_idxs] - if length(scc) >= 2 - deps = fused_var_deps[scc[1]] - for c in 2:length(scc) - union!(deps, fused_var_deps[c]) - fused_var_deps[c] = deps - end - end - end - - var2idx = Dict{Int, Int}(v => i for (i, v) in enumerate(states_idxs)) - eqs2idx = Dict{Int, Int}(v => i for (i, v) in enumerate(eqs_idxs)) - - I = Int[] - J = Int[] - s = state.structure - for ieq in 𝑠vertices(graph) - nieq = get(eqs2idx, ieq, 0) - nieq == 0 && continue - for ivar in 𝑠neighbors(graph, ieq) - isdervar(s, ivar) && continue - if var_rename[ivar] != 0 - push!(I, nieq) - push!(J, var2idx[ivar]) - else - for dvar in fused_var_deps[ivar] - isdervar(s, dvar) && continue - niv = get(var2idx, dvar, 0) - niv == 0 && continue - push!(I, nieq) - push!(J, niv) - end - end - end - end - sparse(I, J, true, length(eqs_idxs), length(states_idxs)) -end - -""" - find_solve_sequence(sccs, vars) - -given a set of `vars`, find the groups of equations we need to solve for -to obtain the solution to `vars` -""" -function find_solve_sequence(sccs, vars) - subset = filter(i -> !isdisjoint(sccs[i], vars), 1:length(sccs)) - isempty(subset) && return [] - vars′ = mapreduce(i -> sccs[i], union, subset) - if vars′ == vars - return subset - else - return find_solve_sequence(sccs, vars′) - end -end diff --git a/src/structural_transformation/pantelides.jl b/src/structural_transformation/pantelides.jl index 871bd99ef4..7768e6937c 100644 --- a/src/structural_transformation/pantelides.jl +++ b/src/structural_transformation/pantelides.jl @@ -2,18 +2,20 @@ ### Reassemble: structural information -> system ### +const NOTHING_EQ = nothing ~ nothing + function pantelides_reassemble(state::TearingState, var_eq_matching) fullvars = state.fullvars @unpack var_to_diff, eq_to_diff = state.structure sys = state.sys # Step 1: write derivative equations in_eqs = equations(sys) - out_eqs = Vector{Any}(undef, nv(eq_to_diff)) - fill!(out_eqs, nothing) + out_eqs = Vector{Equation}(undef, nv(eq_to_diff)) + fill!(out_eqs, NOTHING_EQ) out_eqs[1:length(in_eqs)] .= in_eqs - out_vars = Vector{Any}(undef, nv(var_to_diff)) - fill!(out_vars, nothing) + out_vars = Vector{SymbolicT}(undef, nv(var_to_diff)) + fill!(out_vars, ModelingToolkit.COMMON_NOTHING) out_vars[1:length(fullvars)] .= fullvars iv = get_iv(sys) @@ -22,7 +24,7 @@ function pantelides_reassemble(state::TearingState, var_eq_matching) for (varidx, diff) in edges(var_to_diff) # fullvars[diff] = D(fullvars[var]) vi = out_vars[varidx] - @assert vi!==nothing "Something went wrong on reconstructing unknowns from variable association list" + @assert vi!==ModelingToolkit.COMMON_NOTHING "Something went wrong on reconstructing unknowns from variable association list" # `fullvars[i]` needs to be not a `D(...)`, because we want the DAE to be # first-order. if isdifferential(vi) @@ -31,14 +33,13 @@ function pantelides_reassemble(state::TearingState, var_eq_matching) out_vars[diff] = D(vi) end - d_dict = Dict(zip(fullvars, 1:length(fullvars))) - lhss = Set{Any}([x.lhs for x in in_eqs if isdiffeq(x)]) + d_dict = Dict{SymbolicT, Int}(zip(fullvars, 1:length(fullvars))) for (eqidx, diff) in edges(eq_to_diff) # LHS variable is looked up from var_to_diff # the var_to_diff[i]-th variable is the differentiated version of var at i eq = out_eqs[eqidx] - lhs = if !(eq.lhs isa Symbolic) - 0 + lhs = if SU.isconst(eq.lhs) + Symbolics.COMMON_ZERO elseif isdiffeq(eq) # look up the variable that represents D(lhs) lhsarg1 = arguments(eq.lhs)[1] @@ -48,22 +49,22 @@ function pantelides_reassemble(state::TearingState, var_eq_matching) D(eq.lhs) else # remove clashing equations - lhs = Num(nothing) + lhs = ModelingToolkit.COMMON_NOTHING end else D(eq.lhs) end rhs = ModelingToolkit.expand_derivatives(D(eq.rhs)) - rhs = fast_substitute(rhs, state.param_derivative_map) + rhs = substitute(rhs, state.param_derivative_map) substitution_dict = Dict(x.lhs => x.rhs - for x in out_eqs if x !== nothing && x.lhs isa Symbolic) + for x in out_eqs if x !== NOTHING_EQ && !SU.isconst(x.lhs)) sub_rhs = substitute(rhs, substitution_dict) out_eqs[diff] = lhs ~ sub_rhs end final_vars = unique(filter(x -> !(operation(x) isa Differential), fullvars)) final_eqs = map(identity, - filter(x -> value(x.lhs) !== nothing, + filter(x -> x.lhs !== ModelingToolkit.COMMON_NOTHING, out_eqs[sort(filter(x -> x !== unassigned, var_eq_matching))])) @set! sys.eqs = final_eqs @@ -71,141 +72,6 @@ function pantelides_reassemble(state::TearingState, var_eq_matching) return sys end -""" - computed_highest_diff_variables(structure) - -Computes which variables are the "highest-differentiated" for purposes of -pantelides. Ordinarily this is relatively straightforward. However, in our -case, there is one complicating condition: - - We allow variables in the structure graph that don't appear in the - system at all. What we are interested in is the highest-differentiated - variable that actually appears in the system. - -This function takes care of these complications are returns a boolean array -for every variable, indicating whether it is considered "highest-differentiated". -""" -function computed_highest_diff_variables(structure) - @unpack graph, var_to_diff = structure - - nvars = length(var_to_diff) - varwhitelist = falses(nvars) - for var in 1:nvars - if var_to_diff[var] === nothing && !varwhitelist[var] - # This variable is structurally highest-differentiated, but may not actually appear in the - # system (complication 1 above). Ascend the differentiation graph to find the highest - # differentiated variable that does appear in the system or the alias graph). - while isempty(𝑑neighbors(graph, var)) - var′ = invview(var_to_diff)[var] - var′ === nothing && break - var = var′ - end - varwhitelist[var] = true - end - end - - # Remove any variables from the varwhitelist for whom a higher-differentiated - # var is already on the whitelist. - for var in 1:nvars - varwhitelist[var] || continue - var′ = var - while (var′ = var_to_diff[var′]) !== nothing - if varwhitelist[var′] - varwhitelist[var] = false - break - end - end - end - - return varwhitelist -end - -""" - pantelides!(state::TransformationState; kwargs...) - -Perform Pantelides algorithm. -""" -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) - vcolor = falses(nvars) - ecolor = falses(neqs) - var_eq_matching = Matching(nvars) - neqs′ = neqs - nnonemptyeqs = count( - eq -> !isempty(𝑠neighbors(graph, eq)) && eq_to_diff[eq] === nothing, - 1:neqs′) - - varwhitelist = computed_highest_diff_variables(state.structure) - - if nnonemptyeqs > count(varwhitelist) - throw(InvalidSystemException("System is structurally singular")) - end - - for k in 1:neqs′ - eq′ = k - eq_to_diff[eq′] === nothing || continue - isempty(𝑠neighbors(graph, eq′)) && continue - pathfound = false - # In practice, `maxiters=8000` should never be reached, otherwise, the - # index would be on the order of thousands. - for iii in 1:maxiters - # run matching on (dx, y) variables - # - # the derivatives and algebraic variables are zeros in the variable - # association list - resize!(vcolor, nvars) - fill!(vcolor, false) - resize!(ecolor, neqs) - fill!(ecolor, false) - pathfound = construct_augmenting_path!(var_eq_matching, graph, eq′, - v -> varwhitelist[v], vcolor, ecolor) - pathfound && break # terminating condition - if is_only_discrete(state.structure) - error("The discrete system has high structural index. This is not supported.") - end - for var in eachindex(vcolor) - vcolor[var] || continue - if var_to_diff[var] === nothing - # introduce a new variable - nvars += 1 - var_diff = var_derivative!(state, var) - push!(var_eq_matching, unassigned) - push!(varwhitelist, false) - @assert length(var_eq_matching) == var_diff - end - varwhitelist[var] = false - varwhitelist[var_to_diff[var]] = true - end - - for eq in eachindex(ecolor) - ecolor[eq] || continue - # introduce a new equation - neqs += 1 - eq_derivative!(state, eq; kwargs...) - end - - for var in eachindex(vcolor) - vcolor[var] || continue - # the newly introduced `var`s and `eq`s have the inherits - # assignment - var_eq_matching[var_to_diff[var]] = eq_to_diff[var_eq_matching[var]] - end - eq′ = eq_to_diff[eq′] - end # for _ in 1:maxiters - pathfound || - error("maxiters=$maxiters reached! File a bug report if your system has a reasonable index (<100), and you are using the default `maxiters`. Try to increase the maxiters by `pantelides(sys::System; maxiters=1_000_000)` if your system has an incredibly high index and it is truly extremely large.") - end # for k in 1:neqs′ - - finalize && for var in 1:ndsts(graph) - varwhitelist[var] && continue - var_eq_matching[var] = unassigned - end - return var_eq_matching -end - """ dae_index_lowering(sys::System; kwargs...) -> System @@ -215,6 +81,6 @@ instead, which calls this function internally. """ function dae_index_lowering(sys::System; kwargs...) state = TearingState(sys) - var_eq_matching = pantelides!(state; finalize = false, kwargs...) + var_eq_matching = StateSelection.pantelides!(state; finalize = false, kwargs...) return invalidate_cache!(pantelides_reassemble(state, var_eq_matching)) end diff --git a/src/structural_transformation/partial_state_selection.jl b/src/structural_transformation/partial_state_selection.jl deleted file mode 100644 index 98e40e9988..0000000000 --- a/src/structural_transformation/partial_state_selection.jl +++ /dev/null @@ -1,232 +0,0 @@ -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; kwargs...)) - dummy_derivative_graph!(state.structure, var_eq_matching, jac, state_priority, log; kwargs...) -end - -struct DummyDerivativeSummary - var_sccs::Vector{Vector{Int}} - state_priority::Vector{Vector{Float64}} -end - -function dummy_derivative_graph!( - structure::SystemStructure, var_eq_matching, jac = nothing, - state_priority = nothing, ::Val{log} = Val(false); - tearing_alg::TearingAlgorithm = DummyDerivativeTearing(), kwargs...) where {log} - @unpack eq_to_diff, var_to_diff, graph = structure - diff_to_eq = invview(eq_to_diff) - diff_to_var = invview(var_to_diff) - invgraph = invview(graph) - extended_sp = let state_priority = state_priority, var_to_diff = var_to_diff, - diff_to_var = diff_to_var - - var -> begin - min_p = max_p = 0.0 - while var_to_diff[var] !== nothing - var = var_to_diff[var] - end - while true - p = state_priority(var) - max_p = max(max_p, p) - min_p = min(min_p, p) - (var = diff_to_var[var]) === nothing && break - end - min_p < 0 ? min_p : max_p - end - end - - var_sccs = find_var_sccs(graph, var_eq_matching) - var_perm = Int[] - var_dummy_scc = Vector{Int}[] - var_state_priority = Vector{Float64}[] - eqcolor = falses(nsrcs(graph)) - dummy_derivatives = Int[] - col_order = Int[] - neqs = nsrcs(graph) - nvars = ndsts(graph) - eqs = Int[] - vars = Int[] - next_eq_idxs = Int[] - next_var_idxs = Int[] - new_eqs = Int[] - new_vars = Int[] - eqs_set = BitSet() - for vars′ in var_sccs - empty!(eqs) - empty!(vars) - for var in vars′ - eq = var_eq_matching[var] - eq isa Int || continue - diff_to_eq[eq] === nothing || push!(eqs, eq) - if var_to_diff[var] !== nothing - error("Invalid SCC") - end - (diff_to_var[var] !== nothing && is_present(structure, var)) && push!(vars, var) - end - isempty(eqs) && continue - - rank_matching = Matching(max(nvars, neqs)) - isfirst = true - if jac === nothing - J = nothing - else - _J = jac(eqs, vars) - # only accept small integers to avoid overflow - is_all_small_int = all(_J) do x′ - x = unwrap(x′) - x isa Number || return false - isinteger(x) && typemin(Int8) <= x <= typemax(Int8) - end - J = is_all_small_int ? Int.(unwrap.(_J)) : nothing - end - while true - nrows = length(eqs) - iszero(nrows) && break - - if state_priority !== nothing && isfirst - sp = extended_sp.(vars) - resize!(var_perm, length(sp)) - sortperm!(var_perm, sp) - permute!(vars, var_perm) - permute!(sp, var_perm) - push!(var_dummy_scc, copy(vars)) - push!(var_state_priority, sp) - end - # TODO: making the algorithm more robust - # 1. If the Jacobian is a integer matrix, use Bareiss to check - # linear independence. (done) - # - # 2. If the Jacobian is a single row, generate pivots. (Dynamic - # state selection.) - # - # 3. If the Jacobian is a polynomial matrix, use Gröbner basis (?) - if J !== nothing - if !isfirst - J = J[next_eq_idxs, next_var_idxs] - end - N = ModelingToolkit.nullspace(J; col_order) # modifies col_order - rank = length(col_order) - size(N, 2) - for i in 1:rank - push!(dummy_derivatives, vars[col_order[i]]) - end - else - empty!(eqs_set) - union!(eqs_set, eqs) - rank = 0 - for var in vars - eqcolor .= false - # We need `invgraph` here because we are matching from - # variables to equations. - pathfound = construct_augmenting_path!(rank_matching, invgraph, var, - Base.Fix2(in, eqs_set), eqcolor) - pathfound || continue - push!(dummy_derivatives, var) - rank += 1 - rank == nrows && break - end - fill!(rank_matching, unassigned) - end - if rank != nrows - @warn "The DAE system is singular!" - end - - # prepare the next iteration - if J !== nothing - empty!(next_eq_idxs) - empty!(next_var_idxs) - end - empty!(new_eqs) - empty!(new_vars) - for (i, eq) in enumerate(eqs) - ∫eq = diff_to_eq[eq] - # descend by one diff level, but the next iteration of equations - # must still be differentiated - ∫eq === nothing && continue - ∫∫eq = diff_to_eq[∫eq] - ∫∫eq === nothing && continue - if J !== nothing - push!(next_eq_idxs, i) - end - push!(new_eqs, ∫eq) - end - for (i, var) in enumerate(vars) - ∫var = diff_to_var[var] - ∫var === nothing && continue - ∫∫var = diff_to_var[∫var] - ∫∫var === nothing && continue - if J !== nothing - push!(next_var_idxs, i) - end - push!(new_vars, ∫var) - end - eqs, new_eqs = new_eqs, eqs - vars, new_vars = new_vars, vars - isfirst = false - end - end - - if (n_diff_eqs = count(!isnothing, diff_to_eq)) != - (n_dummys = length(dummy_derivatives)) - @warn "The number of dummy derivatives ($n_dummys) does not match the number of differentiated equations ($n_diff_eqs)." - end - - tearing_result, extra = tearing_alg(structure, BitSet(dummy_derivatives)) - extra = (; extra..., ddsummary = DummyDerivativeSummary(var_dummy_scc, var_state_priority)) - return tearing_result, extra -end - -function is_present(structure, v)::Bool - @unpack var_to_diff, graph = structure - while true - # if a higher derivative is present, then it's present - isempty(𝑑neighbors(graph, v)) || return true - v = var_to_diff[v] - v === nothing && return false - end -end - -# Derivatives that are either in the dummy derivatives set or ended up not -# participating in the system at all are not considered differential -function is_some_diff(structure, dummy_derivatives, v)::Bool - !(v in dummy_derivatives) && is_present(structure, v) -end - -# We don't want tearing to give us `y_t ~ D(y)`, so we skip equations with -# actually differentiated variables. -function isdiffed((structure, dummy_derivatives), v)::Bool - @unpack var_to_diff, graph = structure - diff_to_var = invview(var_to_diff) - diff_to_var[v] !== nothing && is_some_diff(structure, dummy_derivatives, v) -end - -struct DummyDerivativeTearing <: TearingAlgorithm end - -function (::DummyDerivativeTearing)(structure::SystemStructure, dummy_derivatives::Union{BitSet, Tuple{}} = ()) - @unpack var_to_diff = structure - # We can eliminate variables that are not selected (differential - # variables). Selected unknowns are differentiated variables that are not - # dummy derivatives. - can_eliminate = falses(length(var_to_diff)) - for (v, dv) in enumerate(var_to_diff) - dv = var_to_diff[v] - if dv === nothing || !is_some_diff(structure, dummy_derivatives, dv) - can_eliminate[v] = true - end - end - modia_tearing = ModiaTearing(; - isder = Base.Fix1(isdiffed, (structure, dummy_derivatives)), - varfilter = Base.Fix1(getindex, can_eliminate) - ) - tearing_result, _ = modia_tearing(structure) - - for v in 𝑑vertices(structure.graph) - is_present(structure, v) || continue - dv = var_to_diff[v] - (dv === nothing || !is_some_diff(structure, dummy_derivatives, dv)) && continue - tearing_result.var_eq_matching[v] = SelectedState() - end - - return tearing_result, (; can_eliminate) -end diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 540944bb6a..aefbea0311 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -1,1329 +1,8 @@ -using OffsetArrays: Origin - -# N.B. assumes `slist` and `dlist` are unique -function substitution_graph(graph, slist, dlist, var_eq_matching) - ns = length(slist) - nd = length(dlist) - ns == nd || error("internal error") - newgraph = BipartiteGraph(ns, nd) - erename = uneven_invmap(nsrcs(graph), slist) - vrename = uneven_invmap(ndsts(graph), dlist) - for e in 𝑠vertices(graph) - ie = erename[e] - ie == 0 && continue - for v in 𝑠neighbors(graph, e) - iv = vrename[v] - iv == 0 && continue - add_edge!(newgraph, ie, iv) - end - end - - newmatching = Matching(ns) - for (v, e) in enumerate(var_eq_matching) - (e === unassigned || e === SelectedState()) && continue - iv = vrename[v] - ie = erename[e] - iv == 0 && continue - ie == 0 && error("internal error") - newmatching[iv] = ie - end - - return DiCMOBiGraph{true}(newgraph, complete(newmatching)) -end - -function var_derivative_graph!(s::SystemStructure, v::Int) - sg = g = add_vertex!(s.graph, DST) - var_diff = add_vertex!(s.var_to_diff) - add_edge!(s.var_to_diff, v, var_diff) - s.solvable_graph === nothing || (sg = add_vertex!(s.solvable_graph, DST)) - @assert sg == g == var_diff - return var_diff -end - -function var_derivative!(ts::TearingState, v::Int) - s = ts.structure - var_diff = var_derivative_graph!(s, v) - sys = ts.sys - D = Differential(get_iv(sys)) - push!(ts.fullvars, D(ts.fullvars[v])) - return var_diff -end - -function eq_derivative_graph!(s::SystemStructure, eq::Int) - add_vertex!(s.graph, SRC) - s.solvable_graph === nothing || add_vertex!(s.solvable_graph, SRC) - # the new equation is created by differentiating `eq` - eq_diff = add_vertex!(s.eq_to_diff) - add_edge!(s.eq_to_diff, eq, eq_diff) - return eq_diff -end - -function eq_derivative!(ts::TearingState, ieq::Int; kwargs...) - s = ts.structure - - eq_diff = eq_derivative_graph!(s, ieq) - - sys = ts.sys - eq = equations(ts)[ieq] - eq = 0 ~ fast_substitute( - ModelingToolkit.derivative( - eq.rhs - eq.lhs, get_iv(sys); throw_no_derivative = true), ts.param_derivative_map) - - vs = ModelingToolkit.vars(eq.rhs) - for v in vs - # parameters with unknown derivatives have a value of `nothing` in the map, - # so use `missing` as the default. - get(ts.param_derivative_map, v, missing) === nothing || continue - _original_eq = equations(ts)[ieq] - error(""" - Encountered derivative of discrete variable `$(only(arguments(v)))` when \ - differentiating equation `$(_original_eq)`. This may indicate a model error or a \ - missing equation of the form `$v ~ ...` that defines this derivative. - """) - end - - push!(equations(ts), eq) - # Analyze the new equation and update the graph/solvable_graph - # First, copy the previous incidence and add the derivative terms. - # That's a superset of all possible occurrences. find_solvables! will - # remove those that doesn't actually occur. - eq_diff = length(equations(ts)) - for var in 𝑠neighbors(s.graph, ieq) - add_edge!(s.graph, eq_diff, var) - 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, kwargs...) - - return eq_diff -end - -function tearing_substitution(sys::AbstractSystem; kwargs...) - neweqs = full_equations(sys::AbstractSystem; kwargs...) - @set! sys.eqs = neweqs - # @set! sys.substitutions = nothing - @set! sys.schedule = nothing -end - -function solve_equation(eq, var, simplify) - rhs = value(symbolic_linear_solve(eq, var; simplify = simplify, check = false)) - occursin(var, rhs) && throw(EquationSolveErrors(eq, var, rhs)) - var ~ rhs -end - -function substitute_vars!(structure, subs, cache = Int[], callback! = nothing; - exclude = ()) - @unpack graph, solvable_graph = structure - for su in subs - su === nothing && continue - v, v′ = su - eqs = 𝑑neighbors(graph, v) - # Note that the iterator is not robust under deletion and - # insertion. Hence, we have a copy here. - resize!(cache, length(eqs)) - for eq in copyto!(cache, eqs) - eq in exclude && continue - rem_edge!(graph, eq, v) - add_edge!(graph, eq, v′) - - if BipartiteEdge(eq, v) in solvable_graph - rem_edge!(solvable_graph, eq, v) - add_edge!(solvable_graph, eq, v′) - end - callback! !== nothing && callback!(eq, su) - end - end - return structure -end - -function to_mass_matrix_form(neweqs, ieq, graph, fullvars, isdervar::F, - var_to_diff) where {F} - eq = neweqs[ieq] - if !(eq.lhs isa Number && eq.lhs == 0) - eq = 0 ~ eq.rhs - eq.lhs - end - rhs = eq.rhs - if rhs isa Symbolic - # Check if the RHS is solvable in all unknown variable derivatives and if those - # the linear terms for them are all zero. If so, move them to the - # LHS. - dervar::Union{Nothing, Int} = nothing - for var in 𝑠neighbors(graph, ieq) - if isdervar(var) - if dervar !== nothing - error("$eq has more than one differentiated variable!") - end - dervar = var - end - end - dervar === nothing && return (0 ~ rhs), dervar - new_lhs = var = fullvars[dervar] - # 0 ~ a * D(x) + b - # D(x) ~ -b/a - a, b, islinear = linear_expansion(rhs, var) - if !islinear - return (0 ~ rhs), nothing - end - new_rhs = -b / a - return (new_lhs ~ new_rhs), invview(var_to_diff)[dervar] - else # a number - if abs(rhs) > 100eps(float(rhs)) - @warn "The equation $eq is not consistent. It simplified to 0 == $rhs." - end - return nothing - end -end - -#= -function check_diff_graph(var_to_diff, fullvars) - diff_to_var = invview(var_to_diff) - for (iv, v) in enumerate(fullvars) - ov, order = var_from_nested_derivative(v) - graph_order = 0 - vv = iv - while true - vv = diff_to_var[vv] - vv === nothing && break - graph_order += 1 - end - @assert graph_order==order "graph_order: $graph_order, order: $order for variable $v" - end -end -=# - -""" -Replace derivatives of non-selected unknown variables by dummy derivatives. - -State selection may determine that some differential variables are -algebraic variables in disguise. The derivative of such variables are -called dummy derivatives. - -`SelectedState` information is no longer needed after this function is called. -State selection is done. All non-differentiated variables are algebraic -variables, and all variables that appear differentiated are differential variables. -""" -function substitute_derivatives_algevars!( - ts::TearingState, neweqs, var_eq_matching, dummy_sub; iv = nothing, D = nothing) - @unpack fullvars, sys, structure = ts - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - diff_to_var = invview(var_to_diff) - - for var in 1:length(fullvars) - dv = var_to_diff[var] - dv === nothing && continue - if var_eq_matching[var] !== SelectedState() - dd = fullvars[dv] - v_t = setio(diff2term_with_unit(unwrap(dd), unwrap(iv)), false, false) - for eq in 𝑑neighbors(graph, dv) - dummy_sub[dd] = v_t - neweqs[eq] = fast_substitute(neweqs[eq], dd => v_t) - end - fullvars[dv] = v_t - # If we have: - # x -> D(x) -> D(D(x)) - # We need to to transform it to: - # x x_t -> D(x_t) - # update the structural information - dx = dv - x_t = v_t - while (ddx = var_to_diff[dx]) !== nothing - dx_t = D(x_t) - for eq in 𝑑neighbors(graph, ddx) - neweqs[eq] = fast_substitute(neweqs[eq], fullvars[ddx] => dx_t) - end - fullvars[ddx] = dx_t - dx = ddx - x_t = dx_t - end - diff_to_var[dv] = nothing - end - end -end - -#= -There are three cases where we want to generate new variables to convert -the system into first order (semi-implicit) ODEs. - -1. To first order: -Whenever higher order differentiated variable like `D(D(D(x)))` appears, -we introduce new variables `x_t`, `x_tt`, and `x_ttt` and new equations -``` -D(x_tt) = x_ttt -D(x_t) = x_tt -D(x) = x_t -``` -and replace `D(x)` to `x_t`, `D(D(x))` to `x_tt`, and `D(D(D(x)))` to -`x_ttt`. - -2. To implicit to semi-implicit ODEs: -2.1: Unsolvable derivative: -If one derivative variable `D(x)` is unsolvable in all the equations it -appears in, then we introduce a new variable `x_t`, a new equation -``` -D(x) ~ x_t -``` -and replace all other `D(x)` to `x_t`. - -2.2: Solvable derivative: -If one derivative variable `D(x)` is solvable in at least one of the -equations it appears in, then we introduce a new variable `x_t`. One of -the solvable equations must be in the form of `0 ~ L(D(x), u...)` and -there exists a function `l` such that `D(x) ~ l(u...)`. We should replace -it to -``` -0 ~ x_t - l(u...) -D(x) ~ x_t -``` -and replace all other `D(x)` to `x_t`. - -Observe that we don't need to actually introduce a new variable `x_t`, as -the above equations can be lowered to -``` -x_t := l(u...) -D(x) ~ x_t -``` -where `:=` denotes assignment. - -As a final note, in all the above cases where we need to introduce new -variables and equations, don't add them when they already exist. - -###### DISCRETE SYSTEMS ####### - -Documenting the differences to structural simplification for discrete systems: - -In discrete systems everything gets shifted forward a timestep by `shift_discrete_system` -in order to properly generate the difference equations. - -In the system x(k) ~ x(k-1) + x(k-2), becomes Shift(t, 1)(x(t)) ~ x(t) + Shift(t, -1)(x(t)) - -The lowest-order term is Shift(t, k)(x(t)), instead of x(t). As such we actually want -dummy variables for the k-1 lowest order terms instead of the k-1 highest order terms. - -Shift(t, -1)(x(t)) -> x\_{t-1}(t) - -Since Shift(t, -1)(x) is not a derivative, it is directly substituted in `fullvars`. -No equation or variable is added for it. - -For ODESystems D(D(D(x))) in equations is recursively substituted as D(x) ~ x_t, D(x_t) ~ x_tt, etc. -The analogue for discrete systems, Shift(t, 1)(Shift(t,1)(Shift(t,1)(Shift(t, -3)(x(t))))) -does not actually appear. So `total_sub` in generate_system_equations` is directly -initialized with all of the lowered variables `Shift(t, -3)(x) -> x_t-3(t)`, etc. -=# -""" -Generate new derivative variables for the system. - -Effects on the system structure: -- fullvars: add the new derivative variables x_t -- neweqs: add the identity equations for the new variables, D(x) ~ x_t -- graph: update graph with the new equations and variables, and their connections -- solvable_graph: mark the new equation as solvable for `D(x)` -- var_eq_matching: match D(x) to the added identity equation `D(x) ~ x_t` -- full_var_eq_matching: match `x_t` to the equation that `D(x)` used to match to, and - match `D(x)` to `D(x) ~ x_t` -- var_sccs: Replace `D(x)` in its SCC by `x_t`, and add `D(x)` in its own SCC. Return - the new list of SCCs. -""" -function generate_derivative_variables!( - ts::TearingState, neweqs, var_eq_matching, full_var_eq_matching, - var_sccs; mm, iv = nothing, D = nothing) - @unpack fullvars, sys, structure = ts - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - eq_var_matching = invview(var_eq_matching) - diff_to_var = invview(var_to_diff) - is_discrete = is_only_discrete(structure) - linear_eqs = mm === nothing ? Dict{Int, Int}() : - Dict(reverse(en) for en in enumerate(mm.nzrows)) - - # We need the inverse mapping of `var_sccs` to update it efficiently later. - v_to_scc = Vector{NTuple{2, Int}}(undef, ndsts(graph)) - for (i, scc) in enumerate(var_sccs), (j, v) in enumerate(scc) - - v_to_scc[v] = (i, j) - end - # Pairs of `(x_t, dx)` added below - v_t_dvs = NTuple{2, Int}[] - - # For variable x, make dummy derivative x_t if the - # derivative is in the system - for v in 1:length(var_to_diff) - dv = var_to_diff[v] - # if the variable is not differentiated, there is nothing to do - dv isa Int || continue - # if we will solve for the differentiated variable, there is nothing to do - solved = var_eq_matching[dv] isa Int - solved && continue - - # If there's `D(x) = x_t` already, update mappings and continue without - # adding new equations/variables - dd = find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) - if dd === nothing - # there is no such pre-existing equation - # generate the dummy derivative variable - dx = fullvars[dv] - order, lv = var_order(dv, diff_to_var) - x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : - lower_varname_with_unit(fullvars[lv], iv, order) - - # Add `x_t` to the graph - v_t = add_dd_variable!(structure, fullvars, x_t, dv) - # Add `D(x) - x_t ~ 0` to the graph - dummy_eq = add_dd_equation!(structure, neweqs, 0 ~ dx - x_t, dv, v_t) - # Update graph to say, all the equations featuring D(x) also feature x_t - for e in 𝑑neighbors(graph, dv) - add_edge!(graph, e, v_t) - end - # Update matching - push!(var_eq_matching, unassigned) - push!(full_var_eq_matching, unassigned) - - # We also need to substitute all occurrences of `D(x)` with `x_t` in all equations - # except `dummy_eq`, but that is handled in `generate_system_equations!` since - # we will solve for `D(x) ~ x_t` and add it to the substitution map. - dd = dummy_eq, v_t - end - # there is a duplicate `D(x) ~ x_t` equation - # `dummy_eq` is the index of the equation - # `v_t` is the dummy derivative variable - dummy_eq, v_t = dd - var_to_diff[v_t] = var_to_diff[dv] - old_matched_eq = full_var_eq_matching[dv] - full_var_eq_matching[dv] = var_eq_matching[dv] = dummy_eq - full_var_eq_matching[v_t] = old_matched_eq - eq_var_matching[dummy_eq] = dv - push!(v_t_dvs, (v_t, dv)) - end - - # tuples of (index, scc) indicating that `scc` has to be inserted at - # index `index` in `var_sccs`. Same length as `v_t_dvs` because we will - # have one new SCC per new variable. - sccs_to_insert = similar(v_t_dvs, Tuple{Int, Vector{Int}}) - # mapping of SCC index to indexes in the SCC to delete - idxs_to_remove = Dict{Int, Vector{Int}}() - for (k, (v_t, dv)) in enumerate(v_t_dvs) - # replace `dv` with `v_t` - i, j = v_to_scc[dv] - var_sccs[i][j] = v_t - if v_t <= length(v_to_scc) - # v_t wasn't added by this process, it was already present. Which - # means we need to remove it from whatever SCC it is in, since it is - # now in this one - i_, j_ = v_to_scc[v_t] - scc_del_idxs = get!(() -> Int[], idxs_to_remove, i_) - push!(scc_del_idxs, j_) - end - # `dv` still needs to be present in some SCC. Since we solve for `dv` from - # `0 ~ D(x) - x_t`, it is in its own SCC. This new singleton SCC is solved - # immediately before the one that `dv` used to be in (`i`) - sccs_to_insert[k] = (i, [dv]) - end - sort!(sccs_to_insert, by = first) - # remove the idxs we need to remove - for (i, idxs) in idxs_to_remove - deleteat!(var_sccs[i], idxs) - end - new_sccs = insert_sccs(var_sccs, sccs_to_insert) - - if mm !== nothing - @set! mm.ncols = ndsts(graph) - end - - return new_sccs -end - -""" - $(TYPEDSIGNATURES) - -Given a list of SCCs and a list of SCCs to insert at specific indices, insert them and -return the new SCC vector. -""" -function insert_sccs( - var_sccs::Vector{Vector{Int}}, sccs_to_insert::Vector{Tuple{Int, Vector{Int}}}) - # insert the new SCCs, accounting for the fact that we might have multiple entries - # in `sccs_to_insert` to be inserted at the same index. - old_idx = 1 - insert_idx = 1 - new_sccs = similar(var_sccs, length(var_sccs) + length(sccs_to_insert)) - for i in eachindex(new_sccs) - # if we have SCCs to insert, and the index we have to insert them at is the current - # one in the old list of SCCs - if insert_idx <= length(sccs_to_insert) && sccs_to_insert[insert_idx][1] == old_idx - # insert it - new_sccs[i] = sccs_to_insert[insert_idx][2] - insert_idx += 1 - else - # otherwise, insert the old SCC - new_sccs[i] = copy(var_sccs[old_idx]) - old_idx += 1 - end - end - - filter!(!isempty, new_sccs) - return new_sccs -end - -""" -Check if there's `D(x) ~ x_t` already. -""" -function find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) - for eq in 𝑑neighbors(solvable_graph, dv) - mi = get(linear_eqs, eq, 0) - iszero(mi) && continue - row = @view mm[mi, :] - nzs = nonzeros(row) - rvs = SparseArrays.nonzeroinds(row) - # note that `v_t` must not be differentiated - if length(nzs) == 2 && - (abs(nzs[1]) == 1 && nzs[1] == -nzs[2]) && - (v_t = rvs[1] == dv ? rvs[2] : rvs[1]; - diff_to_var[v_t] === nothing) - @assert dv in rvs - return eq, v_t - end - end - return nothing -end - -""" -Add a dummy derivative variable x_t corresponding to symbolic variable D(x) -which has index dv in `fullvars`. Return the new index of x_t. -""" -function add_dd_variable!(s::SystemStructure, fullvars, x_t, dv) - push!(fullvars, simplify_shifts(x_t)) - v_t = length(fullvars) - v_t_idx = add_vertex!(s.var_to_diff) - add_vertex!(s.graph, DST) - # TODO: do we care about solvable_graph? We don't use them after - # `dummy_derivative_graph`. - add_vertex!(s.solvable_graph, DST) - s.var_to_diff[v_t] = s.var_to_diff[dv] - v_t -end - -""" -Add the equation D(x) - x_t ~ 0 to `neweqs`. `dv` and `v_t` are the indices -of the higher-order derivative variable and the newly-introduced dummy -derivative variable. Return the index of the new equation in `neweqs`. -""" -function add_dd_equation!(s::SystemStructure, neweqs, eq, dv, v_t) - push!(neweqs, eq) - add_vertex!(s.graph, SRC) - dummy_eq = length(neweqs) - add_edge!(s.graph, dummy_eq, dv) - add_edge!(s.graph, dummy_eq, v_t) - add_vertex!(s.solvable_graph, SRC) - add_edge!(s.solvable_graph, dummy_eq, dv) - dummy_eq -end - -""" -Solve the equations in `neweqs` to obtain the final equations of the -system. - -For each equation of `neweqs`, do one of the following: - 1. If the equation is solvable for a differentiated variable D(x), - then solve for D(x), and add D(x) ~ sol as a differential equation - of the system. - 2. If the equation is solvable for an un-differentiated variable x, - solve for x and then add x ~ sol as a solved equation. These will - become observables. - 3. If the equation is not solvable, add it as an algebraic equation. - -Solved equations are added to `total_sub`. Occurrences of differential -or solved variables on the RHS of the final equations will get substituted. -The topological sort of the equations ensures that variables are solved for -before they appear in equations. - -Reorder the equations and unknowns to be in the BLT sorted form. - -Return the new equations, the solved equations, -the new orderings, and the number of solved variables and equations. -""" -function generate_system_equations!(state::TearingState, neweqs, var_eq_matching, - full_var_eq_matching, var_sccs, extra_eqs_vars; - simplify = false, iv = nothing, D = nothing) - @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - eq_var_matching = invview(var_eq_matching) - full_eq_var_matching = invview(full_var_eq_matching) - diff_to_var = invview(var_to_diff) - extra_eqs, extra_vars = extra_eqs_vars - - total_sub = Dict() - if is_only_discrete(structure) - for (i, v) in enumerate(fullvars) - op = operation(v) - op isa Shift && (op.steps < 0) && - begin - lowered = lower_shift_varname_with_unit(v, iv) - total_sub[v] = lowered - fullvars[i] = lowered - end - end - end - - eq_generator = EquationGenerator(state, total_sub, D, iv) - - # We need to solve extra equations before everything to repsect - # topological order. - for eq in extra_eqs - var = eq_var_matching[eq] - var isa Int || continue - codegen_equation!(eq_generator, neweqs[eq], eq, var; simplify) - end - - # if the variable is present in the equations either as-is or differentiated - ispresent = let var_to_diff = var_to_diff, graph = graph - i -> (!isempty(𝑑neighbors(graph, i)) || - (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) - end - - digraph = DiCMOBiGraph{false}(graph, var_eq_matching) - idep = iv - for (i, scc) in enumerate(var_sccs) - # note that the `vscc <-> escc` relation is a set-to-set mapping, and not - # point-to-point. - vscc, escc = get_sorted_scc(digraph, full_var_eq_matching, var_eq_matching, scc) - var_sccs[i] = vscc - - if length(escc) != length(vscc) - isempty(escc) && continue - escc = setdiff(escc, extra_eqs) - isempty(escc) && continue - vscc = setdiff(vscc, extra_vars) - isempty(vscc) && continue - end - - offset = 1 - for ieq in escc - iv = eq_var_matching[ieq] - eq = neweqs[ieq] - codegen_equation!(eq_generator, neweqs[ieq], ieq, iv; simplify) - end - end - - for eq in extra_eqs - var = eq_var_matching[eq] - var isa Int && continue - codegen_equation!(eq_generator, neweqs[eq], eq, var; simplify) - end - - @unpack neweqs′, eq_ordering, var_ordering, solved_eqs, solved_vars = eq_generator - - is_diff_eq = .!iszero.(var_ordering) - # Generate new equations and orderings - diff_vars = var_ordering[is_diff_eq] - diff_vars_set = BitSet(diff_vars) - if length(diff_vars_set) != length(diff_vars) - error("Tearing internal error: lowering DAE into semi-implicit ODE failed!") - end - solved_vars_set = BitSet(solved_vars) - # We filled zeros for algebraic variables, so fill them properly here - offset = 1 - for (i, v) in enumerate(var_ordering) - v == 0 || continue - # find the next variable which is not differential or solved, is not the - # derivative of another variable and is present in the equations - index = findnext(1:ndsts(graph), offset) do j - !(j in diff_vars_set || j in solved_vars_set) && diff_to_var[j] === nothing && - ispresent(j) - end - # in case of overdetermined systems, this may not be present - index === nothing && break - var_ordering[i] = index - offset = index + 1 - end - filter!(!iszero, var_ordering) - var_ordering = [var_ordering; setdiff(1:ndsts(graph), var_ordering, solved_vars_set)] - neweqs = neweqs′ - return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), - length(solved_vars_set) -end - -""" - $(TYPEDSIGNATURES) - -Sort the provided SCC `scc`, given the `digraph` of the system constructed using -`var_eq_matching` along with both the matchings of the system. -""" -function get_sorted_scc( - digraph::DiCMOBiGraph, full_var_eq_matching::Matching, var_eq_matching::Matching, scc::Vector{Int}) - eq_var_matching = invview(var_eq_matching) - full_eq_var_matching = invview(full_var_eq_matching) - # obtain the matched equations in the SCC - scc_eqs = Int[full_var_eq_matching[v] for v in scc if full_var_eq_matching[v] isa Int] - # obtain the equations in the SCC that are linearly solvable - scc_solved_eqs = Int[var_eq_matching[v] for v in scc if var_eq_matching[v] isa Int] - # obtain the subgraph of the contracted graph involving the solved equations - subgraph, varmap = Graphs.induced_subgraph(digraph, scc_solved_eqs) - # topologically sort the solved equations and append the remainder - scc_eqs = [varmap[reverse(topological_sort(subgraph))]; - setdiff(scc_eqs, scc_solved_eqs)] - # the variables of the SCC are obtained by inverse mapping the sorted equations - # and appending the rest - scc_vars = [eq_var_matching[e] for e in scc_eqs if eq_var_matching[e] isa Int] - append!(scc_vars, setdiff(scc, scc_vars)) - return scc_vars, scc_eqs -end - -""" - $(TYPEDSIGNATURES) - -Struct containing the information required to generate equations of a system, as well as -the generated equations and associated metadata. -""" -struct EquationGenerator{S, D, I} - """ - `TearingState` of the system. - """ - state::S - """ - Substitutions to perform in all subsequent equations. For each differential equation - `D(x) ~ f(..)`, the substitution `D(x) => f(..)` is added to the rules. - """ - total_sub::Dict{Any, Any} - """ - The differential operator, or `nothing` if not applicable. - """ - D::D - """ - The independent variable, or `nothing` if not applicable. - """ - idep::I - """ - The new generated equations of the system. - """ - neweqs′::Vector{Equation} - """ - `eq_ordering[i]` is the index `neweqs′[i]` was originally at in the untorn equations of - the system. This is used to permute the state of the system into BLT sorted form. - """ - eq_ordering::Vector{Int} - """ - `var_ordering[i]` is the index in `state.fullvars` of the variable at the `i`th index in - the BLT sorted form. - """ - var_ordering::Vector{Int} - """ - List of linearly solved (observed) equations. - """ - solved_eqs::Vector{Equation} - """ - `eq_ordering` for `solved_eqs`. - """ - solved_vars::Vector{Int} -end - -function EquationGenerator(state, total_sub, D, idep) - EquationGenerator( - state, total_sub, D, idep, Equation[], Int[], Int[], Equation[], Int[]) -end - -""" - $(TYPEDSIGNATURES) - -Check if equation at index `ieq` is linearly solvable for variable at index `iv`. -""" -function is_solvable(eg::EquationGenerator, ieq, iv) - solvable_graph = eg.state.structure.solvable_graph - return ieq isa Int && iv isa Int && BipartiteEdge(ieq, iv) in solvable_graph -end - -""" - $(TYPEDSIGNATURES) - - If `iv` is like D(x) or Shift(t, 1)(x) -""" -function is_dervar(eg::EquationGenerator, iv::Int) - diff_to_var = invview(eg.state.structure.var_to_diff) - diff_to_var[iv] !== nothing -end - -""" - $(TYPEDSIGNATURES) - -Appropriately codegen the given equation `eq`, which occurs at index `ieq` in the untorn -list of equations and is matched to variable at index `iv`. -""" -function codegen_equation!(eg::EquationGenerator, - eq::Equation, ieq::Int, iv::Union{Int, Unassigned}; simplify = false) - # We generate equations ordered by the matched variables - # Solvable equations of differential variables D(x) become differential equations - # Solvable equations of non-differential variables become observable equations - # Non-solvable equations become algebraic equations. - @unpack state, total_sub, neweqs′, eq_ordering, var_ordering = eg - @unpack solved_eqs, solved_vars, D, idep = eg - @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - diff_to_var = invview(var_to_diff) - - issolvable = is_solvable(eg, ieq, iv) - isdervar = issolvable && is_dervar(eg, iv) - isdisc = is_only_discrete(structure) - # The variable is derivative variable and the "most differentiated" - # This is only used for discrete systems, and basically refers to - # `Shift(t, 1)(x(k))` in `Shift(t, 1)(x(k)) ~ x(k) + x(k-1)`. As illustrated in - # the docstring for `add_additional_history!`, this is an exception and needs to be - # treated like a solved equation rather than a differential equation. - is_highest_diff = iv isa Int && isdervar && var_to_diff[iv] === nothing - if issolvable && isdervar && (!isdisc || !is_highest_diff) - var = fullvars[iv] - isnothing(D) && throw(UnexpectedDifferentialError(equations(sys)[ieq])) - order, lv = var_order(iv, diff_to_var) - dx = D(simplify_shifts(fullvars[lv])) - - neweq = make_differential_equation(var, dx, eq, total_sub) - # We will add `neweq.lhs` to `total_sub`, so any equation involving it won't be - # incident on it. Remove the edges incident on `iv` from the graph, and add - # the replacement vertices from `ieq` so that the incidence is still correct. - for e in 𝑑neighbors(graph, iv) - e == ieq && continue - for v in 𝑠neighbors(graph, ieq) - add_edge!(graph, e, v) - end - rem_edge!(graph, e, iv) - end - - total_sub[simplify_shifts(neweq.lhs)] = neweq.rhs - # Substitute unshifted variables x(k), y(k) on RHS of implicit equations - if is_only_discrete(structure) - var_to_diff[iv] === nothing && (total_sub[var] = neweq.rhs) - end - push!(neweqs′, neweq) - push!(eq_ordering, ieq) - push!(var_ordering, diff_to_var[iv]) - elseif issolvable - var = fullvars[iv] - neweq = make_solved_equation(var, eq, total_sub; simplify) - if neweq !== nothing - # backshift solved equations to calculate the value of the variable at the - # current time. This works because we added one additional history element - # in `add_additional_history!`. - if isdisc - neweq = backshift_expr(neweq, idep) - end - push!(solved_eqs, neweq) - push!(solved_vars, iv) - end - else - neweq = make_algebraic_equation(eq, total_sub) - # For the same reason as solved equations (they are effectively the same) - if isdisc - neweq = backshift_expr(neweq, idep) - end - push!(neweqs′, neweq) - push!(eq_ordering, ieq) - # we push a dummy to `var_ordering` here because `iv` is `unassigned` - push!(var_ordering, 0) - end -end - -""" -Occurs when a variable D(x) occurs in a non-differential system. -""" -struct UnexpectedDifferentialError - eq::Equation -end - -function Base.showerror(io::IO, err::UnexpectedDifferentialError) - error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(err.eq)") -end - -""" -Generate a first-order differential equation whose LHS is `dx`. - -`var` and `dx` represent the same variable, but `var` may be a higher-order differential and `dx` is always first-order. For example, if `var` is D(D(x)), then `dx` would be `D(x_t)`. Solve `eq` for `var`, substitute previously solved variables, and return the differential equation. -""" -function make_differential_equation(var, dx, eq, total_sub) - dx ~ simplify_shifts(Symbolics.fixpoint_sub( - Symbolics.symbolic_linear_solve(eq, var), - total_sub; operator = ModelingToolkit.Shift)) -end - -""" -Generate an algebraic equation. Substitute solved variables into `eq` and return the equation. -""" -function make_algebraic_equation(eq, total_sub) - rhs = eq.rhs - if !(eq.lhs isa Number && eq.lhs == 0) - rhs = eq.rhs - eq.lhs - end - 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub)) -end - -""" -Solve equation `eq` for `var`, substitute previously solved variables, and return the solved equation. -""" -function make_solved_equation(var, eq, total_sub; simplify = false) - residual = eq.lhs - eq.rhs - a, b, islinear = linear_expansion(residual, var) - @assert islinear - # 0 ~ a * var + b - # var ~ -b/a - if ModelingToolkit._iszero(a) - @warn "Tearing: solving $eq for $var is singular!" - return nothing - else - rhs = -b / a - return var ~ simplify_shifts(Symbolics.fixpoint_sub( - simplify ? - Symbolics.simplify(rhs) : rhs, - total_sub; operator = ModelingToolkit.Shift)) - end -end - -""" -Given the ordering returned by `generate_system_equations!`, update the -tearing state to account for the new order. Permute the variables and equations. -Eliminate the solved variables and equations from the graph and permute the -graph's vertices to account for the new variable/equation ordering. -""" -function reorder_vars!(state::TearingState, var_eq_matching, var_sccs, eq_ordering, - var_ordering, nsolved_eq, nsolved_var) - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure - - eqsperm = zeros(Int, nsrcs(graph)) - for (i, v) in enumerate(eq_ordering) - eqsperm[v] = i - end - varsperm = zeros(Int, ndsts(graph)) - for (i, v) in enumerate(var_ordering) - varsperm[v] = i - end - - # Contract the vertices in the structure graph to make the structure match - # the new reality of the system we've just created. - new_graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, - nsolved_eq, nsolved_var) - new_solvable_graph = contract_variables(solvable_graph, var_eq_matching, varsperm, eqsperm, - nsolved_eq, nsolved_var) - - new_var_to_diff = complete(DiffGraph(length(var_ordering))) - for (v, d) in enumerate(var_to_diff) - v′ = varsperm[v] - (v′ > 0 && d !== nothing) || continue - d′ = varsperm[d] - new_var_to_diff[v′] = d′ > 0 ? d′ : nothing - end - new_eq_to_diff = complete(DiffGraph(length(eq_ordering))) - for (v, d) in enumerate(eq_to_diff) - v′ = eqsperm[v] - (v′ > 0 && d !== nothing) || continue - d′ = eqsperm[d] - new_eq_to_diff[v′] = d′ > 0 ? d′ : nothing - end - new_fullvars = state.fullvars[var_ordering] - - # Update the SCCs - var_ordering_set = BitSet(var_ordering) - for scc in var_sccs - # Map variables to their new indices - map!(v -> varsperm[v], scc, scc) - # Remove variables not in the reduced set - filter!(!iszero, scc) - end - # Remove empty SCCs - filter!(!isempty, var_sccs) - - # Update system structure - @set! state.structure.graph = complete(new_graph) - @set! state.structure.solvable_graph = complete(new_solvable_graph) - @set! state.structure.var_to_diff = new_var_to_diff - @set! state.structure.eq_to_diff = new_eq_to_diff - @set! state.fullvars = new_fullvars - state -end - -""" -Update the system equations, unknowns, and observables after simplification. -""" -function update_simplified_system!( - state::TearingState, neweqs, solved_eqs, dummy_sub, var_sccs, extra_unknowns; - array_hack = true, D = nothing, iv = nothing) - @unpack fullvars, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - diff_to_var = invview(var_to_diff) - # Since we solved the highest order derivative variable in discrete systems, - # we make a list of the solved variables and avoid including them in the - # unknowns. - solved_vars = Set() - if is_only_discrete(structure) - for eq in solved_eqs - var = eq.lhs - if isequal(eq.lhs, eq.rhs) - var = lower_shift_varname_with_unit(D(eq.lhs), iv) - end - push!(solved_vars, var) - end - filter!(eq -> !isequal(eq.lhs, eq.rhs), solved_eqs) - end - - ispresent = let var_to_diff = var_to_diff, graph = graph - i -> (!isempty(𝑑neighbors(graph, i)) || - (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) - end - - sys = state.sys - obs_sub = dummy_sub - 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); solved_eqs; - fast_substitute(state.additional_observed, obs_sub)] - - unknown_idxs = filter( - i -> diff_to_var[i] === nothing && ispresent(i) && !(fullvars[i] in solved_vars), eachindex(state.fullvars)) - unknowns = state.fullvars[unknown_idxs] - unknowns = [unknowns; extra_unknowns] - if is_only_discrete(structure) - # Algebraic variables are shifted forward by one, so we backshift them. - unknowns = map(enumerate(unknowns)) do (i, var) - if iscall(var) && operation(var) isa Shift && operation(var).steps == 1 - # We might have shifted a variable with io metadata. That is irrelevant now - # because we handled io variables earlier in `_mtkcompile!` so just ignore - # it here. - setio(backshift_expr(var, iv), false, false) - else - var - end - end - end - @set! sys.unknowns = unknowns - - obs = tearing_hacks(sys, obs, unknowns, neweqs; array = array_hack) - - @set! sys.eqs = neweqs - @set! sys.observed = obs - - # Only makes sense for time-dependent - if ModelingToolkit.has_schedule(sys) - unknowns_set = BitSet(unknown_idxs) - for scc in var_sccs - intersect!(scc, unknowns_set) - end - filter!(!isempty, var_sccs) - @set! sys.schedule = Schedule(var_sccs, dummy_sub) - end - if ModelingToolkit.has_isscheduled(sys) - @set! sys.isscheduled = true - end - return sys -end - -""" -Give the order of the variable indexed by dv. -""" -function var_order(dv, diff_to_var) - order = 0 - while (dv′ = diff_to_var[dv]) !== nothing - order += 1 - dv = dv′ - end - order, dv -end - -""" -Main internal function for structural simplification for DAE systems and discrete systems. -Generate dummy derivative variables, new equations in terms of variables, return updated -system and tearing state. - -Terminology and Definition: - -A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can -characterize variables in `u(t)` into two classes: differential variables -(denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential -variables are marked as `SelectedState` and they are differentiated in the -DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually -appear in the system. Algebraic variables are variables that are not -differential variables. - -# Arguments - -- `state`: The `TearingState` of the system. -- `var_eq_matching`: The maximal matching after state selection. -- `full_var_eq_matching`: The maximal matching prior to state selection. -- `var_sccs`: The topologically sorted strongly connected components of the system - according to `full_var_eq_matching`. -""" -@kwdef struct DefaultReassembleAlgorithm <: ReassembleAlgorithm - simplify::Bool = false - array_hack::Bool = true -end - -function (alg::DefaultReassembleAlgorithm)(state::TearingState, tearing_result::TearingResult, mm::Union{SparseMatrixCLIL, Nothing}; fully_determined::Bool = true, kw...) - @unpack simplify, array_hack = alg - @unpack var_eq_matching, full_var_eq_matching, var_sccs = tearing_result - - extra_eqs_vars = get_extra_eqs_vars( - state, var_eq_matching, full_var_eq_matching, fully_determined) - neweqs = collect(equations(state)) - dummy_sub = Dict() - - if ModelingToolkit.has_iv(state.sys) - iv = get_iv(state.sys) - if !is_only_discrete(state.structure) - D = Differential(iv) - else - D = Shift(iv, 1) - end - else - iv = D = nothing - end - - extra_unknowns = state.fullvars[extra_eqs_vars[2]] - if is_only_discrete(state.structure) - var_sccs = add_additional_history!( - state, neweqs, var_eq_matching, full_var_eq_matching, var_sccs; iv, D) - end - - # Structural simplification - substitute_derivatives_algevars!(state, neweqs, var_eq_matching, dummy_sub; iv, D) - - var_sccs = generate_derivative_variables!( - state, neweqs, var_eq_matching, full_var_eq_matching, var_sccs; mm, iv, D) - - neweqs, solved_eqs, - eq_ordering, - var_ordering, - nelim_eq, - nelim_var = generate_system_equations!( - state, neweqs, var_eq_matching, full_var_eq_matching, - var_sccs, extra_eqs_vars; simplify, iv, D) - - state = reorder_vars!( - state, var_eq_matching, var_sccs, eq_ordering, var_ordering, nelim_eq, nelim_var) - # var_eq_matching and full_var_eq_matching are now invalidated - - sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_sccs, - extra_unknowns; array_hack, iv, D) - - @set! state.sys = sys - @set! sys.tearing_state = state - return invalidate_cache!(sys) -end - -""" - $(TYPEDSIGNATURES) - -Add one more history equation for discrete systems. For example, if we have - -```julia -Shift(t, 1)(x(k-1)) ~ x(k) -Shift(t, 1)(x(k)) ~ x(k) + x(k-1) -``` - -This turns it into - -```julia -Shift(t, 1)(x(k-2)) ~ x(k-1) -Shift(t, 1)(x(k-1)) ~ x(k) -Shift(t, 1)(x(k)) ~ x(k) + x(k-1) -``` - -Thus adding an additional unknown as well. Later, the highest derivative equation will -be backshifted by one and turned into an observed equation, resulting in: - -```julia -Shift(t, 1)(x(k-2)) ~ x(k-1) -Shift(t, 1)(x(k-1)) ~ x(k) - -x(k) ~ x(k-1) + x(k-2) -``` - -Where the last equation is the observed equation. -""" -function add_additional_history!( - state::TearingState, neweqs::Vector, var_eq_matching::Matching, - full_var_eq_matching::Matching, var_sccs::Vector{Vector{Int}}; iv, D) - @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure - eq_var_matching = invview(var_eq_matching) - diff_to_var = invview(var_to_diff) - is_discrete = is_only_discrete(structure) - digraph = DiCMOBiGraph{false}(graph, var_eq_matching) - - # We need the inverse mapping of `var_sccs` to update it efficiently later. - v_to_scc = Vector{NTuple{2, Int}}(undef, ndsts(graph)) - for (i, scc) in enumerate(var_sccs), (j, v) in enumerate(scc) - - v_to_scc[v] = (i, j) - end - - vars_to_backshift = BitSet() - eqs_to_backshift = BitSet() - # add history for differential variables - for ivar in 1:length(fullvars) - ieq = var_eq_matching[ivar] - # the variable to backshift is a state variable which is not the - # derivative of any other one. - ieq isa SelectedState || continue - diff_to_var[ivar] === nothing || continue - push!(vars_to_backshift, ivar) - end - - inserts = Tuple{Int, Vector{Int}}[] - - for var in vars_to_backshift - add_backshifted_var!(state, var, iv) - # all backshifted vars are differential vars, hence SelectedState - push!(var_eq_matching, SelectedState()) - push!(full_var_eq_matching, unassigned) - # add to the SCCs right before the variable that was backshifted - push!(inserts, (v_to_scc[var][1], [length(fullvars)])) - end - - sort!(inserts, by = first) - new_sccs = insert_sccs(var_sccs, inserts) - return new_sccs -end - -""" - $(TYPEDSIGNATURES) - -Add the backshifted version of variable `ivar` to the system. -""" -function add_backshifted_var!(state::TearingState, ivar::Int, iv) - @unpack fullvars, structure = state - @unpack var_to_diff, graph, solvable_graph = structure - - var = fullvars[ivar] - newvar = simplify_shifts(Shift(iv, -1)(var)) - push!(fullvars, newvar) - inewvar = add_vertex!(var_to_diff) - add_edge!(var_to_diff, inewvar, ivar) - add_vertex!(graph, DST) - add_vertex!(solvable_graph, DST) - return inewvar -end - -""" - $(TYPEDSIGNATURES) - -Backshift the given expression `ex`. -""" -function backshift_expr(ex, iv) - ex isa Symbolic || return ex - return descend_lower_shift_varname_with_unit( - simplify_shifts(distribute_shift(Shift(iv, -1)(ex))), iv) -end - -function backshift_expr(ex::Equation, iv) - return backshift_expr(ex.lhs, iv) ~ backshift_expr(ex.rhs, iv) -end - -""" - $(TYPEDSIGNATURES) - -Return a 2-tuple of integer vectors containing indices of extra equations and variables -respectively. For fully-determined systems, both of these are empty. Overdetermined systems -have extra equations, and underdetermined systems have extra variables. -""" -function get_extra_eqs_vars( - state::TearingState, var_eq_matching::Matching, full_var_eq_matching::Matching, fully_determined::Bool) - fully_determined && return Int[], Int[] - - extra_eqs = Int[] - extra_vars = Int[] - full_eq_var_matching = invview(full_var_eq_matching) - - for v in 𝑑vertices(state.structure.graph) - eq = full_var_eq_matching[v] - eq isa Int && continue - # Only if the variable is also unmatched in `var_eq_matching`. - # Otherwise, `SelectedState` differential variables from order lowering - # are also considered "extra" - var_eq_matching[v] === unassigned || continue - push!(extra_vars, v) - end - for eq in 𝑠vertices(state.structure.graph) - v = full_eq_var_matching[eq] - v isa Int && continue - push!(extra_eqs, eq) - end - - return extra_eqs, extra_vars -end - -""" -# HACK - -Add equations for array observed variables. If `p[i] ~ (...)` are equations, add an -equation `p ~ [p[1], p[2], ...]` allow topsort to reorder them only add the new equation -if all `p[i]` are present and the unscalarized form is used in any equation (observed or -not) we first count the number of times the scalarized form of each observed variable -occurs in observed equations (and unknowns if it's split). -""" -function tearing_hacks(sys, obs, unknowns, neweqs; array = true) - # map of array observed variable (unscalarized) to number of its - # scalarized terms that appear in observed equations - arr_obs_occurrences = Dict() - for (i, eq) in enumerate(obs) - lhs = eq.lhs - rhs = eq.rhs - - array || continue - iscall(lhs) || continue - operation(lhs) === getindex || continue - Symbolics.shape(lhs) != Symbolics.Unknown() || continue - arg1 = arguments(lhs)[1] - cnt = get(arr_obs_occurrences, arg1, 0) - arr_obs_occurrences[arg1] = cnt + 1 - continue - end - - # count variables in unknowns if they are scalarized forms of variables - # also present as observed. e.g. if `x[1]` is an unknown and `x[2] ~ (..)` - # is an observed equation. - for sym in unknowns - iscall(sym) || continue - operation(sym) === getindex || continue - Symbolics.shape(sym) != Symbolics.Unknown() || continue - arg1 = arguments(sym)[1] - cnt = get(arr_obs_occurrences, arg1, 0) - cnt == 0 && continue - arr_obs_occurrences[arg1] = cnt + 1 - end - - obs_arr_eqs = Equation[] - for (arrvar, cnt) in arr_obs_occurrences - cnt == length(arrvar) || continue - # firstindex returns 1 for multidimensional array symbolics - firstind = Tuple(first(eachindex(arrvar))) - scal = [arrvar[i] for i in eachindex(arrvar)] - # respect non-1-indexed arrays - # TODO: get rid of this hack together with the above hack, then remove OffsetArrays dependency - # `change_origin` is required because `Origin(firstind)(scal)` makes codegen - # try to `create_array(OffsetArray{...}, ...)` which errors. - # `term(Origin(firstind), scal)` doesn't retain the `symtype` and `size` - # of `scal`. - rhs = change_origin(firstind, scal) - push!(obs_arr_eqs, arrvar ~ rhs) - end - append!(obs, obs_arr_eqs) - - return obs -end - -# PART OF HACK -function change_origin(origin, arr) - if all(isone, origin) - return arr - end - return Origin(origin)(arr) -end - -@register_array_symbolic change_origin(origin::Any, arr::AbstractArray) begin - size = size(arr) - eltype = eltype(arr) - ndims = ndims(arr) -end - -function tearing(state::TearingState; tearing_alg::TearingAlgorithm = DummyDerivativeTearing(), +function tearing(state::TearingState; + tearing_alg::StateSelection.TearingAlgorithm = StateSelection.DummyDerivativeTearing(), kwargs...) - state.structure.solvable_graph === nothing && find_solvables!(state; kwargs...) - complete!(state.structure) + state.structure.solvable_graph === nothing && StateSelection.find_solvables!(state; kwargs...) + StateSelection.complete!(state.structure) tearing_alg(state.structure) end @@ -1352,8 +31,22 @@ function dummy_derivative(sys, state = TearingState(sys); mm = nothing, fully_determined = true, kwargs...) jac = let state = state (eqs, vars) -> begin - symeqs = EquationsView(state)[eqs] - Symbolics.jacobian((x -> x.rhs).(symeqs), state.fullvars[vars]) + symeqs = equations(state)[eqs] + _J = Symbolics.jacobian((x -> x.rhs).(symeqs), state.fullvars[vars]) + J = similar(_J, Int) + for i in eachindex(_J) + el = _J[i] + Moshi.Match.@match el begin + BSImpl.Const(; val) && if val isa Number end => begin + isinteger(val)::Bool || return nothing + val = Int(val) + typemin(Int) <= val <= typemax(Int) || return nothing + J[i] = val + end + _ => return nothing + end + end + return J end end state_priority = let state = state @@ -1371,7 +64,7 @@ function dummy_derivative(sys, state = TearingState(sys); p end end - tearing_result, extras = dummy_derivative_graph!( + tearing_result, extras = StateSelection.dummy_derivative_graph!( state, jac; state_priority, kwargs...) reassemble_alg(state, tearing_result, mm; fully_determined) end diff --git a/src/structural_transformation/tearing.jl b/src/structural_transformation/tearing.jl deleted file mode 100644 index 77a004a823..0000000000 --- a/src/structural_transformation/tearing.jl +++ /dev/null @@ -1,147 +0,0 @@ -struct EquationSolveError - eq::Any - var::Any - rhs::Any -end - -function Base.showerror(io::IO, ese::EquationSolveError) - print(io, "EquationSolveError: While solving\n\n\t") - print(io, ese.eq) - print(io, "\nfor ") - printstyled(io, var, bold = true) - print(io, ", obtained RHS\n\n\tt") - println(io, rhs) -end - -function masked_cumsum!(A::Vector) - acc = zero(eltype(A)) - for i in eachindex(A) - iszero(A[i]) && continue - A[i] = (acc += A[i]) - end -end - -function contract_variables(graph::BipartiteGraph, var_eq_matching::Matching, - var_rename, eq_rename, nelim_eq, nelim_var) - dig = DiCMOBiGraph{true}(graph, var_eq_matching) - - # Update bipartite graph - var_deps = map(1:ndsts(graph)) do v - [var_rename[v′] - for v′ in neighborhood(dig, v, Inf; dir = :in) if var_rename[v′] != 0] - end - - newgraph = BipartiteGraph(nsrcs(graph) - nelim_eq, ndsts(graph) - nelim_var) - for e in 𝑠vertices(graph) - ne = eq_rename[e] - ne == 0 && continue - for v in 𝑠neighbors(graph, e) - newvar = var_rename[v] - if newvar != 0 - add_edge!(newgraph, ne, newvar) - else - for nv in var_deps[v] - add_edge!(newgraph, ne, nv) - end - end - end - end - - return newgraph -end - -""" - algebraic_variables_scc(sys) - -Find strongly connected components of algebraic variables in a system. -""" -function algebraic_variables_scc(state::TearingState) - graph = state.structure.graph - # skip over differential equations - algvars = BitSet(findall(v -> isalgvar(state.structure, v), 1:ndsts(graph))) - algeqs = BitSet(findall(map(1:nsrcs(graph)) do eq - all(v -> !isdervar(state.structure, v), - 𝑠neighbors(graph, eq)) - end)) - var_eq_matching = complete( - maximal_matching(graph, e -> e in algeqs, v -> v in algvars), ndsts(graph)) - var_sccs = find_var_sccs(complete(graph), var_eq_matching) - - 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 - -struct SelectedState end -const MatchingT{T} = Matching{T, Vector{Union{T, Int}}} -const MatchedVarT = Union{Unassigned, SelectedState} -const VarEqMatchingT = MatchingT{MatchedVarT} - -""" - $TYPEDEF - -A struct containing the results of tearing. - -# Fields - -$TYPEDFIELDS -""" -struct TearingResult - """ - The variable-equation matching. Differential variables are matched to `SelectedState`. - The derivative of a differential variable is matched to the corresponding differential - equation. Solved variables are matched to the equation they are solved from. Algebraic - variables are matched to `unassigned`. - """ - var_eq_matching::VarEqMatchingT - """ - The variable-equation matching prior to tearing. This is the maximal matching used to - compute `var_sccs` (see below). For generating the torn system, `var_eq_matching` is - the source of truth. This should only be used to identify algebraic equations in each - SCC. - """ - full_var_eq_matching::VarEqMatchingT - """ - The partitioning of variables into strongly connected components (SCCs). The SCCs are - sorted in dependency order, so each SCC depends on variables in previous SCCs. - """ - var_sccs::Vector{Vector{Int}} -end - -""" - $TYPEDEF - -Supertype for all tearing algorithms. A tearing algorithm takes as input the -`SystemStructure` along with any other necessary arguments. - -The output of a tearing algorithm must be a `TearingResult` and a `NamedTuple` of -any additional data computed in the process that may be useful for further processing. -""" -abstract type TearingAlgorithm end - -""" - $TYPEDEF - -Supertype for all reassembling algorithms. A reassembling algorithm takes as input the -`TearingState`, `TearingResult` and integer incidence matrix `mm::SparseMatrixCLIL`. The -matrix `mm` may be `nothing`. The algorithm must also accept arbitrary keyword arguments. -The following keyword arguments will always be provided: -- `fully_determined::Bool`: flag indicating whether the system is fully determined. - -The output of a reassembling algorithm must be the torn system. - -A reassemble algorithm must also implement `with_fully_determined` -""" -abstract type ReassembleAlgorithm end diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 3fa4f28aa9..dafa5dc78d 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -1,211 +1,7 @@ -### -### Bipartite graph utilities -### - -""" - maximal_matching(s::SystemStructure, eqfilter=eq->true, varfilter=v->true) -> Matching - -Find equation-variable maximal bipartite matching. `s.graph` is a bipartite graph. -""" -function BipartiteGraphs.maximal_matching(s::SystemStructure, eqfilter = eq -> true, - varfilter = v -> true) - maximal_matching(s.graph, eqfilter, varfilter) -end - -n_concrete_eqs(state::TransformationState) = n_concrete_eqs(state.structure) -n_concrete_eqs(structure::SystemStructure) = n_concrete_eqs(structure.graph) -function n_concrete_eqs(graph::BipartiteGraph) - neqs = count(e -> !isempty(𝑠neighbors(graph, e)), 𝑠vertices(graph)) -end - -function error_reporting(state, bad_idxs, n_highest_vars, iseqs, orig_inputs) - io = IOBuffer() - neqs = n_concrete_eqs(state) - if iseqs - error_title = "More equations than variables, here are the potential extra equation(s):\n" - out_arr = has_equations(state) ? equations(state)[bad_idxs] : bad_idxs - else - error_title = "More variables than equations, here are the potential extra variable(s):\n" - out_arr = get_fullvars(state)[bad_idxs] - unset_inputs = intersect(out_arr, orig_inputs) - n_missing_eqs = n_highest_vars - neqs - n_unset_inputs = length(unset_inputs) - if n_unset_inputs > 0 - println(io, "In particular, the unset input(s) are:") - Base.print_array(io, unset_inputs) - println(io) - println(io, "The rest of potentially unset variable(s) are:") - end - end - - Base.print_array(io, out_arr) - msg = String(take!(io)) - if iseqs - throw(ExtraEquationsSystemException("The system is unbalanced. There are " * - "$n_highest_vars highest order derivative variables " - * "and $neqs equations.\n" - * error_title - * msg)) - else - throw(ExtraVariablesSystemException("The system is unbalanced. There are " * - "$n_highest_vars highest order derivative variables " - * "and $neqs equations.\n" - * error_title - * msg)) - end -end - -### -### Structural check -### - -""" - $(TYPEDSIGNATURES) - -Check if the `state` represents a singular system, and return the unmatched variables. -""" -function singular_check(state::TransformationState) - @unpack graph, var_to_diff = state.structure - fullvars = get_fullvars(state) - # This is defined to check if Pantelides algorithm terminates. For more - # details, check the equation (15) of the original paper. - extended_graph = (@set graph.fadjlist = Vector{Int}[graph.fadjlist; - map(collect, edges(var_to_diff))]) - extended_var_eq_matching = maximal_matching(extended_graph) - - nvars = ndsts(graph) - unassigned_var = [] - for (vj, eq) in enumerate(extended_var_eq_matching) - vj > nvars && break - if eq === unassigned && !isempty(𝑑neighbors(graph, vj)) - push!(unassigned_var, fullvars[vj]) - end - end - return unassigned_var -end - -""" - $(TYPEDSIGNATURES) - -Check the consistency of `state`, given the inputs `orig_inputs`. If `nothrow == false`, -throws an error if the system is under-/over-determined or singular. In this case, if the -function returns it will return `true`. If `nothrow == true`, it will return `false` -instead of throwing an error. The singular case will print a warning. -""" -function check_consistency(state::TransformationState, orig_inputs; nothrow = false) - fullvars = get_fullvars(state) - neqs = n_concrete_eqs(state) - @unpack graph, var_to_diff = state.structure - highest_vars = computed_highest_diff_variables(complete!(state.structure)) - n_highest_vars = 0 - for (v, h) in enumerate(highest_vars) - h || continue - isempty(𝑑neighbors(graph, v)) && continue - n_highest_vars += 1 - end - is_balanced = n_highest_vars == neqs - - if neqs > 0 && !is_balanced - nothrow && return false - varwhitelist = var_to_diff .== nothing - var_eq_matching = maximal_matching(graph, eq -> true, v -> varwhitelist[v]) # not assigned - # Just use `error_reporting` to do conditional - iseqs = n_highest_vars < neqs - if iseqs - eq_var_matching = invview(complete(var_eq_matching, nsrcs(graph))) # extra equations - bad_idxs = findall(isequal(unassigned), @view eq_var_matching[1:nsrcs(graph)]) - else - bad_idxs = findall(isequal(unassigned), var_eq_matching) - end - error_reporting(state, bad_idxs, n_highest_vars, iseqs, orig_inputs) - end - - unassigned_var = singular_check(state) - - if !isempty(unassigned_var) || !is_balanced - if nothrow - return false - end - io = IOBuffer() - Base.print_array(io, unassigned_var) - unassigned_var_str = String(take!(io)) - errmsg = "The system is structurally singular! " * - "Here are the problematic variables: \n" * - unassigned_var_str - throw(InvalidSystemException(errmsg)) - end - - return true -end - ### ### BLT ordering ### -""" - find_var_sccs(g::BipartiteGraph, assign=nothing) - -Find strongly connected components of the variables defined by `g`. `assign` -gives the undirected bipartite graph a direction. When `assign === nothing`, we -assume that the ``i``-th variable is assigned to the ``i``-th equation. -""" -function find_var_sccs(g::BipartiteGraph, assign = nothing) - cmog = DiCMOBiGraph{true}(g, - Matching(assign === nothing ? Base.OneTo(nsrcs(g)) : assign)) - sccs = Graphs.strongly_connected_components(cmog) - cgraph = MatchedCondensationGraph(cmog, sccs) - toporder = topological_sort(cgraph) - permute!(sccs, toporder) - foreach(sort!, sccs) - return sccs -end - -function sorted_incidence_matrix(ts::TransformationState, val = true; only_algeqs = false, - only_algvars = false) - var_eq_matching, var_scc = algebraic_variables_scc(ts) - s = ts.structure - graph = ts.structure.graph - varsmap = zeros(Int, ndsts(graph)) - eqsmap = zeros(Int, nsrcs(graph)) - varidx = 0 - eqidx = 0 - for vs in var_scc, v in vs - - eq = var_eq_matching[v] - if eq !== unassigned - eqsmap[eq] = (eqidx += 1) - varsmap[v] = (varidx += 1) - end - end - for i in diffvars_range(s) - varsmap[i] = (varidx += 1) - end - for i in dervars_range(s) - varsmap[i] = (varidx += 1) - end - for i in 1:nsrcs(graph) - if eqsmap[i] == 0 - eqsmap[i] = (eqidx += 1) - end - end - - I = Int[] - J = Int[] - algeqs_set = algeqs(s) - for eq in 𝑠vertices(graph) - only_algeqs && (eq in algeqs_set || continue) - for var in 𝑠neighbors(graph, eq) - only_algvars && (isalgvar(s, var) || continue) - i = eqsmap[eq] - j = varsmap[var] - (iszero(i) || iszero(j)) && continue - push!(I, i) - push!(J, j) - end - end - sparse(I, J, val, nsrcs(graph), ndsts(graph)) -end - """ $(TYPEDSIGNATURES) @@ -230,134 +26,22 @@ end ### ### Structural and symbolic utilities ### - -function find_eq_solvables!(state::TearingState, ieq, to_rm = Int[], coeffs = nothing; - may_be_zero = false, - allow_symbolic = false, allow_parameter = true, - conservative = false, - kwargs...) - fullvars = state.fullvars - @unpack graph, solvable_graph = state.structure - eq = equations(state)[ieq] - term = value(eq.rhs - eq.lhs) - all_int_vars = true - coeffs === nothing || empty!(coeffs) - empty!(to_rm) - for j in 𝑠neighbors(graph, ieq) - var = fullvars[j] - isirreducible(var) && (all_int_vars = false; continue) - a, b, islinear = linear_expansion(term, var) - a, b = unwrap(a), unwrap(b) - islinear || (all_int_vars = false; continue) - if a isa Symbolic - all_int_vars = false - if !allow_symbolic - if allow_parameter - # if any of the variables in `a` are present in fullvars (taking into account arrays) - if any( - v -> any(isequal(v), fullvars) || - symbolic_type(v) == ArraySymbolic() && - Symbolics.shape(v) != Symbolics.Unknown() && - any(x -> any(isequal(x), fullvars), collect(v)), - vars( - a; op = Union{Differential, Shift, Pre, Sample, Hold, Initial})) - continue - end - else - continue - end - end - add_edge!(solvable_graph, ieq, j) - continue - end - if !(a isa Number) - all_int_vars = false - continue - end - # When the expression is linear with numeric `a`, then we can safely - # only consider `b` for the following iterations. - term = b - if isone(abs(a)) - coeffs === nothing || push!(coeffs, convert(Int, a)) - else - all_int_vars = false - conservative && continue - end - if a != 0 - add_edge!(solvable_graph, ieq, j) - else - if may_be_zero - push!(to_rm, j) - else - @warn "Internal error: Variable $var was marked as being in $eq, but was actually zero" - end - end - end - for j in to_rm - rem_edge!(graph, ieq, j) - end - all_int_vars, term -end - -function find_solvables!(state::TearingState; kwargs...) - @assert state.structure.solvable_graph === nothing - eqs = equations(state) - graph = state.structure.graph - state.structure.solvable_graph = BipartiteGraph(nsrcs(graph), ndsts(graph)) - to_rm = Int[] - for ieq in 1:length(eqs) - find_eq_solvables!(state, ieq, to_rm; kwargs...) - end - return nothing -end - -function linear_subsys_adjmat!(state::TransformationState; kwargs...) - graph = state.structure.graph - if state.structure.solvable_graph === nothing - state.structure.solvable_graph = BipartiteGraph(nsrcs(graph), ndsts(graph)) - end - linear_equations = Int[] - eqs = equations(state.sys) - eadj = Vector{Int}[] - cadj = Vector{Int}[] - coeffs = Int[] - to_rm = Int[] - for i in eachindex(eqs) - all_int_vars, rhs = find_eq_solvables!(state, i, to_rm, coeffs; kwargs...) - - # Check if all unknowns in the equation is both linear and homogeneous, - # i.e. it is in the form of - # - # ``∑ c_i * v_i = 0``, - # - # where ``c_i`` ∈ ℤ and ``v_i`` denotes unknowns. - if all_int_vars && Symbolics._iszero(rhs) - push!(linear_equations, i) - push!(eadj, copy(𝑠neighbors(graph, i))) - push!(cadj, copy(coeffs)) - end - end - - mm = SparseMatrixCLIL(nsrcs(graph), - ndsts(graph), - linear_equations, eadj, cadj) - return mm -end - -highest_order_variable_mask(ts) = +function highest_order_variable_mask(ts) let v2d = ts.structure.var_to_diff v -> isempty(outneighbors(v2d, v)) end +end -lowest_order_variable_mask(ts) = +function lowest_order_variable_mask(ts) let v2d = ts.structure.var_to_diff v -> isempty(inneighbors(v2d, v)) end +end function but_ordered_incidence(ts::TearingState, varmask = highest_order_variable_mask(ts)) graph = complete(ts.structure.graph) - var_eq_matching = complete(maximal_matching(graph, _ -> true, varmask)) - scc = find_var_sccs(graph, var_eq_matching) + var_eq_matching = complete(maximal_matching(graph; srcfilter = _ -> true, dstfilter = varmask)) + scc = StateSelection.find_var_sccs(graph, var_eq_matching) vordering = Vector{Int}(undef, 0) bb = Int[1] sizehint!(vordering, ndsts(graph)) @@ -388,13 +72,13 @@ the analytically solved equations/variables before the unsolved ones. """ function reordered_matrix(sys::System, torn_matching) s = TearingState(sys) - complete!(s.structure) + StateSelection.complete!(s.structure) @unpack graph = s.structure eqs = equations(sys) nvars = ndsts(graph) max_matching = complete(maximal_matching(graph)) torn_matching = complete(torn_matching) - sccs = find_var_sccs(graph, max_matching) + sccs = StateSelection.find_var_sccs(graph, max_matching) I, J = Int[], Int[] ii = 0 M = Int[] @@ -409,7 +93,7 @@ function reordered_matrix(sys::System, torn_matching) for es in e_solved isdiffeq(eqs[es]) && continue ii += 1 - js = [M[x] for x in 𝑠neighbors(graph, es) if isalgvar(s.structure, x)] + js = [M[x] for x in 𝑠neighbors(graph, es) if StateSelection.isalgvar(s.structure, x)] append!(I, fill(ii, length(js))) append!(J, js) end @@ -420,7 +104,7 @@ function reordered_matrix(sys::System, torn_matching) for er in e_residual isdiffeq(eqs[er]) && continue ii += 1 - js = [M[x] for x in 𝑠neighbors(graph, er) if isalgvar(s.structure, x)] + js = [M[x] for x in 𝑠neighbors(graph, er) if StateSelection.isalgvar(s.structure, x)] append!(I, fill(ii, length(js))) append!(J, js) end @@ -441,175 +125,3 @@ function uneven_invmap(n::Int, list) end return rename end - -function torn_system_jacobian_sparsity(sys) - state = get_tearing_state(sys) - state isa TearingState || return nothing - @unpack structure = state - @unpack graph, var_to_diff = structure - - neqs = nsrcs(graph) - nsts = ndsts(graph) - states_idxs = findall(!Base.Fix1(isdervar, structure), 1:nsts) - var2idx = uneven_invmap(nsts, states_idxs) - I = Int[] - J = Int[] - for ieq in 1:neqs - for ivar in 𝑠neighbors(graph, ieq) - nivar = get(var2idx, ivar, 0) - nivar == 0 && continue - push!(I, ieq) - push!(J, nivar) - end - end - return sparse(I, J, true, neqs, neqs) -end - -### -### Misc -### - -""" -Handle renaming variable names for discrete structural simplification. Three cases: -- positive shift: do nothing -- zero shift: x(t) => Shift(t, 0)(x(t)) -- negative shift: rename the variable -""" -function lower_shift_varname(var, iv) - op = operation(var) - if op isa Shift - return shift2term(var) - else - return Shift(iv, 0)(var, true) - end -end - -function descend_lower_shift_varname_with_unit(var, iv) - symbolic_type(var) == NotSymbolic() && return var - ModelingToolkit._with_unit(descend_lower_shift_varname, var, iv, iv) -end -function descend_lower_shift_varname(var, iv) - iscall(var) || return var - op = operation(var) - if op isa Shift - return shift2term(var) - else - args = arguments(var) - args = map(Base.Fix2(descend_lower_shift_varname, iv), args) - return maketerm(typeof(var), op, args, Symbolics.metadata(var)) - end -end - -""" -Rename a Shift variable with negative shift, Shift(t, k)(x(t)) to xₜ₋ₖ(t). -""" -function shift2term(var) - iscall(var) || return var - op = operation(var) - op isa Shift || return var - iv = op.t - arg = only(arguments(var)) - if operation(arg) === getindex - idxs = arguments(arg)[2:end] - newvar = shift2term(op(first(arguments(arg))))[idxs...] - unshifted = ModelingToolkit.getunshifted(newvar)[idxs...] - newvar = setmetadata(newvar, ModelingToolkit.VariableUnshifted, unshifted) - return newvar - end - is_lowered = !isnothing(ModelingToolkit.getunshifted(arg)) - - backshift = is_lowered ? op.steps + ModelingToolkit.getshift(arg) : op.steps - - # Char(0x208b) = ₋ (subscripted minus) - # Char(0x208a) = ₊ (subscripted plus) - pm = backshift > 0 ? Char(0x208a) : Char(0x208b) - # subscripted number, e.g. ₁ - num = join(Char(0x2080 + d) for d in reverse!(digits(abs(backshift)))) - # Char(0x209c) = ₜ - # ds = ₜ₋₁ - ds = join([Char(0x209c), pm, num]) - - O = is_lowered ? ModelingToolkit.getunshifted(arg) : arg - oldop = operation(O) - newname = backshift != 0 ? Symbol(string(nameof(oldop)), ds) : - Symbol(string(nameof(oldop))) - - newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), - Symbolics.children(O), Symbolics.metadata(O)) - newvar = setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) - newvar = setmetadata(newvar, ModelingToolkit.VariableUnshifted, O) - newvar = setmetadata(newvar, ModelingToolkit.VariableShift, backshift) - return newvar -end - -function isdoubleshift(var) - return ModelingToolkit.isoperator(var, ModelingToolkit.Shift) && - ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) -end - -""" -Simplify multiple shifts: Shift(t, k1)(Shift(t, k2)(x)) becomes Shift(t, k1+k2)(x). -""" -function simplify_shifts(var) - ModelingToolkit.hasshift(var) || return var - var isa Equation && return simplify_shifts(var.lhs) ~ simplify_shifts(var.rhs) - (op = operation(var)) isa Shift && op.steps == 0 && return first(arguments(var)) - if isdoubleshift(var) - op1 = operation(var) - vv1 = arguments(var)[1] - op2 = operation(vv1) - vv2 = arguments(vv1)[1] - s1 = op1.steps - s2 = op2.steps - t1 = op1.t - t2 = op2.t - return simplify_shifts(ModelingToolkit.Shift(t1 === nothing ? t2 : t1, s1 + s2)(vv2)) - else - return maketerm(typeof(var), operation(var), simplify_shifts.(arguments(var)), - unwrap(var).metadata) - end -end - -""" -Distribute a shift applied to a whole expression or equation. -Shift(t, 1)(x + y) will become Shift(t, 1)(x) + Shift(t, 1)(y). -Only shifts variables whose independent variable is the same t that appears in the Shift (i.e. constants, time-independent parameters, etc. do not get shifted). -""" -function distribute_shift(var) - var = unwrap(var) - var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) - - ModelingToolkit.hasshift(var) || return var - shift = operation(var) - shift isa Shift || return var - - shift = operation(var) - expr = only(arguments(var)) - if expr isa Equation - return distribute_shift(shift(expr.lhs)) ~ distribute_shift(shift(expr.rhs)) - end - shiftexpr = _distribute_shift(expr, shift) - return simplify_shifts(shiftexpr) -end - -function _distribute_shift(expr, shift) - if iscall(expr) - op = operation(expr) - (op isa Union{Pre, Initial, Sample, Hold}) && return expr - args = arguments(expr) - - if ModelingToolkit.isvariable(expr) && operation(expr) !== getindex && - !ModelingToolkit.iscalledparameter(expr) - (length(args) == 1 && isequal(shift.t, only(args))) ? (return shift(expr)) : - (return expr) - elseif op isa Shift - return shift(expr) - else - return maketerm( - typeof(expr), operation(expr), Base.Fix2(_distribute_shift, shift).(args), - unwrap(expr).metadata) - end - else - return expr - end -end diff --git a/src/systems/alias_elimination.jl b/src/systems/alias_elimination.jl index dc25378b4a..8f746ba288 100644 --- a/src/systems/alias_elimination.jl +++ b/src/systems/alias_elimination.jl @@ -1,59 +1,17 @@ using SymbolicUtils: Rewriters using Graphs.Experimental.Traversals -function alias_eliminate_graph!(state::TransformationState; kwargs...) - mm = linear_subsys_adjmat!(state; kwargs...) - if size(mm, 1) == 0 - return mm # No linear subsystems - end - - @unpack graph, var_to_diff, solvable_graph = state.structure - mm = alias_eliminate_graph!(state, mm; kwargs...) - s = state.structure - for g in (s.graph, s.solvable_graph) - g === nothing && continue - for (ei, e) in enumerate(mm.nzrows) - set_neighbors!(g, e, mm.row_cols[ei]) - end - end - - return mm -end - -# For debug purposes -function aag_bareiss(sys::AbstractSystem) - state = TearingState(sys) - complete!(state.structure) - mm = linear_subsys_adjmat!(state) - return aag_bareiss!(state.structure.graph, state.structure.var_to_diff, mm) -end - -function extreme_var(var_to_diff, v, level = nothing, ::Val{descend} = Val(true); - callback = _ -> nothing) where {descend} - g = descend ? invview(var_to_diff) : var_to_diff - callback(v) - while (v′ = g[v]) !== nothing - v::Int = v′ - callback(v) - if level !== nothing - descend ? (level -= 1) : (level += 1) - end - end - level === nothing ? v : (v => level) -end - alias_elimination(sys) = alias_elimination!(TearingState(sys))[1] -function alias_elimination!(state::TearingState; kwargs...) +function alias_elimination!(state::TearingState; fully_determined = true, kwargs...) sys = state.sys - complete!(state.structure) - graph_orig = copy(state.structure.graph) - mm = alias_eliminate_graph!(state; kwargs...) + StateSelection.complete!(state.structure) + variable_underconstrained! = ZeroVariablesIfFullyDetermined!(fully_determined === true) + mm = StateSelection.structural_singularity_removal!(state; variable_underconstrained!, kwargs...) fullvars = state.fullvars @unpack var_to_diff, graph, solvable_graph = state.structure - subs = Dict() - obs = Equation[] + subs = Dict{SymbolicT, SymbolicT}() # If we encounter y = -D(x), then we need to expand the derivative when # D(y) appears in the equation, so that D(-D(x)) becomes -D(D(x)). to_expand = Int[] @@ -62,17 +20,22 @@ function alias_elimination!(state::TearingState; kwargs...) dels = Int[] eqs = collect(equations(state)) resize!(eqs, nsrcs(graph)) + + __trivial_eq_rhs = let fullvars = fullvars + function trivial_eq_rhs(pair) + var, coeff = pair + iszero(coeff) && return Symbolics.COMMON_ZERO + return coeff * fullvars[var] + end + end for (ei, e) in enumerate(mm.nzrows) vs = 𝑠neighbors(graph, e) if isempty(vs) # remove empty equations push!(dels, e) else - rhs = mapfoldl(+, pairs(nonzerosmap(@view mm[ei, :]))) do (var, coeff) - iszero(coeff) && return 0 - return coeff * fullvars[var] - end - eqs[e] = 0 ~ rhs + rhs = mapfoldl(__trivial_eq_rhs, +, pairs(CLIL.nonzerosmap(@view mm[ei, :]))) + eqs[e] = Symbolics.COMMON_ZERO ~ rhs end end deleteat!(eqs, sort!(dels)) @@ -92,21 +55,22 @@ function alias_elimination!(state::TearingState; kwargs...) n_new_eqs = idx - lineqs = BitSet(mm.nzrows) eqs_to_update = BitSet() - nvs_orig = ndsts(graph_orig) for ieq in eqs_to_update eq = eqs[ieq] - eqs[ieq] = fast_substitute(eq, subs) + eqs[ieq] = substitute(eq, subs) + end + new_nparentrows = nsrcs(graph) + new_row_cols = eltype(mm.row_cols)[] + new_row_vals = eltype(mm.row_vals)[] + new_nzrows = Int[] + for (i, eq) in enumerate(mm.nzrows) + old_to_new_eq[eq] > 0 || continue + push!(new_row_cols, mm.row_cols[i]) + push!(new_row_vals, mm.row_vals[i]) + push!(new_nzrows, old_to_new_eq[eq]) end - @set! mm.nparentrows = nsrcs(graph) - @set! mm.row_cols = eltype(mm.row_cols)[mm.row_cols[i] - for (i, eq) in enumerate(mm.nzrows) - if old_to_new_eq[eq] > 0] - @set! mm.row_vals = eltype(mm.row_vals)[mm.row_vals[i] - for (i, eq) in enumerate(mm.nzrows) - if old_to_new_eq[eq] > 0] - @set! mm.nzrows = Int[old_to_new_eq[eq] for eq in mm.nzrows if old_to_new_eq[eq] > 0] + mm = typeof(mm)(new_nparentrows, mm.ncols, new_nzrows, new_row_cols, new_row_vals) for old_ieq in to_expand ieq = old_to_new_eq[old_ieq] @@ -116,7 +80,7 @@ function alias_elimination!(state::TearingState; kwargs...) diff_to_var = invview(var_to_diff) new_graph = BipartiteGraph(n_new_eqs, ndsts(graph)) new_solvable_graph = BipartiteGraph(n_new_eqs, ndsts(graph)) - new_eq_to_diff = DiffGraph(n_new_eqs) + new_eq_to_diff = StateSelection.DiffGraph(n_new_eqs) eq_to_diff = state.structure.eq_to_diff for (i, ieq) in enumerate(old_to_new_eq) ieq > 0 || continue @@ -126,7 +90,7 @@ function alias_elimination!(state::TearingState; kwargs...) end # update DiffGraph - new_var_to_diff = DiffGraph(length(var_to_diff)) + new_var_to_diff = StateSelection.DiffGraph(length(var_to_diff)) for v in 1:length(var_to_diff) new_var_to_diff[v] = var_to_diff[v] end @@ -138,244 +102,21 @@ function alias_elimination!(state::TearingState; kwargs...) sys = state.sys @set! sys.eqs = eqs state.sys = sys - return invalidate_cache!(sys), mm -end - -""" -$(SIGNATURES) - -Find the first linear variable such that `𝑠neighbors(adj, i)[j]` is true given -the `constraint`. -""" -@inline function find_first_linear_variable(M::SparseMatrixCLIL, - range, - mask, - constraint) - eadj = M.row_cols - @inbounds for i in range - vertices = eadj[i] - if constraint(length(vertices)) - for (j, v) in enumerate(vertices) - if (mask === nothing || mask[v]) - return (CartesianIndex(i, v), M.row_vals[i][j]) - end - end - end - end - return nothing -end - -@inline function find_first_linear_variable(M::AbstractMatrix, - range, - mask, - constraint) - @inbounds for i in range - row = @view M[i, :] - if constraint(count(!iszero, row)) - for (v, val) in enumerate(row) - if mask === nothing || mask[v] - return CartesianIndex(i, v), val - end - end - end - end - return nothing -end - -function find_masked_pivot(variables, M, k) - r = find_first_linear_variable(M, k:size(M, 1), variables, isequal(1)) - r !== nothing && return r - r = find_first_linear_variable(M, k:size(M, 1), variables, isequal(2)) - r !== nothing && return r - r = find_first_linear_variable(M, k:size(M, 1), variables, _ -> true) - return r -end - -count_nonzeros(a::AbstractArray) = count(!iszero, a) - -# N.B.: Ordinarily sparse vectors allow zero stored elements. -# Here we have a guarantee that they won't, so we can make this identification -count_nonzeros(a::CLILVector) = nnz(a) - -# Linear variables are highest order differentiated variables that only appear -# in linear equations with only linear variables. Also, if a variable's any -# derivatives is nonlinear, then all of them are not linear variables. -function find_linear_variables(graph, linear_equations, var_to_diff, irreducibles) - stack = Int[] - linear_variables = falses(length(var_to_diff)) - var_to_lineq = Dict{Int, BitSet}() - mark_not_linear! = let linear_variables = linear_variables, stack = stack, - var_to_lineq = var_to_lineq - - v -> begin - linear_variables[v] = false - push!(stack, v) - while !isempty(stack) - v = pop!(stack) - eqs = get(var_to_lineq, v, nothing) - eqs === nothing && continue - for eq in eqs, v′ in 𝑠neighbors(graph, eq) - - if linear_variables[v′] - linear_variables[v′] = false - push!(stack, v′) - end - end - end - end - end - for eq in linear_equations, v in 𝑠neighbors(graph, eq) - - linear_variables[v] = true - vlineqs = get!(() -> BitSet(), var_to_lineq, v) - push!(vlineqs, eq) - end - for v in irreducibles - lv = extreme_var(var_to_diff, v) - while true - mark_not_linear!(lv) - lv = var_to_diff[lv] - lv === nothing && break - end - end - - linear_equations_set = BitSet(linear_equations) - for (v, islinear) in enumerate(linear_variables) - islinear || continue - lv = extreme_var(var_to_diff, v) - oldlv = lv - remove = invview(var_to_diff)[v] !== nothing - while !remove - for eq in 𝑑neighbors(graph, lv) - if !(eq in linear_equations_set) - remove = true - end - end - lv = var_to_diff[lv] - lv === nothing && break - end - lv = oldlv - remove && while true - mark_not_linear!(lv) - lv = var_to_diff[lv] - lv === nothing && break - end - end - - return linear_variables -end - -function aag_bareiss!(structure, mm_orig::SparseMatrixCLIL{T, Ti}) where {T, Ti} - @unpack graph, var_to_diff = structure - mm = copy(mm_orig) - linear_equations_set = BitSet(mm_orig.nzrows) - - # All unassigned (not a pivot) algebraic variables that only appears in - # linear algebraic equations can be set to 0. - # - # For all the other variables, we can update the original system with - # Bareiss'ed coefficients as Gaussian elimination is nullspace preserving - # and we are only working on linear homogeneous subsystem. - - is_algebraic = let var_to_diff = var_to_diff - v -> var_to_diff[v] === nothing === invview(var_to_diff)[v] - end - is_linear_variables = is_algebraic.(1:length(var_to_diff)) - is_highest_diff = computed_highest_diff_variables(structure) - for i in 𝑠vertices(graph) - # only consider linear algebraic equations - (i in linear_equations_set && all(is_algebraic, 𝑠neighbors(graph, i))) && - continue - for j in 𝑠neighbors(graph, i) - is_linear_variables[j] = false - end - end - solvable_variables = findall(is_linear_variables) - - local bar - try - bar = do_bareiss!(mm, mm_orig, is_linear_variables, is_highest_diff) - catch e - e isa OverflowError || rethrow(e) - mm = convert(SparseMatrixCLIL{BigInt, Ti}, mm_orig) - bar = do_bareiss!(mm, mm_orig, is_linear_variables, is_highest_diff) + # This phrasing infers the return type as `Union{Tuple{...}}` instead of + # `Tuple{Union{...}, ...}` + if mm isa CLIL.SparseMatrixCLIL{BigInt, Int} + return invalidate_cache!(sys), mm + else + return invalidate_cache!(sys), mm end - - return mm, solvable_variables, bar end -function do_bareiss!(M, Mold, is_linear_variables, is_highest_diff) - rank1r = Ref{Union{Nothing, Int}}(nothing) - rank2r = Ref{Union{Nothing, Int}}(nothing) - find_pivot = let rank1r = rank1r - (M, k) -> begin - if rank1r[] === nothing - r = find_masked_pivot(is_linear_variables, M, k) - r !== nothing && return r - rank1r[] = k - 1 - end - if rank2r[] === nothing - r = find_masked_pivot(is_highest_diff, M, k) - r !== nothing && return r - rank2r[] = k - 1 - end - # TODO: It would be better to sort the variables by - # derivative order here to enable more elimination - # opportunities. - return find_masked_pivot(nothing, M, k) - end - end - pivots = Int[] - find_and_record_pivot = let pivots = pivots - (M, k) -> begin - r = find_pivot(M, k) - r === nothing && return nothing - push!(pivots, r[1][2]) - return r - end - end - myswaprows! = let Mold = Mold - (M, i, j) -> begin - Mold !== nothing && swaprows!(Mold, i, j) - swaprows!(M, i, j) - end - end - bareiss_ops = ((M, i, j) -> nothing, myswaprows!, - bareiss_update_virtual_colswap_mtk!, bareiss_zero!) - - rank3, = bareiss!(M, bareiss_ops; find_pivot = find_and_record_pivot) - rank2 = something(rank2r[], rank3) - rank1 = something(rank1r[], rank2) - (rank1, rank2, rank3, pivots) +struct ZeroVariablesIfFullyDetermined! + fully_determined::Bool end -function alias_eliminate_graph!(state::TransformationState, ils::SparseMatrixCLIL; - fully_determined = true, kwargs...) - @unpack structure = state - @unpack graph, solvable_graph, var_to_diff, eq_to_diff = state.structure - # Step 1: Perform Bareiss factorization on the adjacency matrix of the linear - # subsystem of the system we're interested in. - # - ils, solvable_variables, (rank1, rank2, rank3, pivots) = aag_bareiss!(structure, ils) - - if fully_determined == true - ## Step 2: Simplify the system using the Bareiss factorization - rk1vars = BitSet(@view pivots[1:rank1]) - for v in solvable_variables - v in rk1vars && continue - @set! ils.nparentrows += 1 - push!(ils.nzrows, ils.nparentrows) - push!(ils.row_cols, [v]) - push!(ils.row_vals, [convert(eltype(ils), 1)]) - add_vertex!(graph, SRC) - add_vertex!(solvable_graph, SRC) - add_edge!(graph, ils.nparentrows, v) - add_edge!(solvable_graph, ils.nparentrows, v) - add_vertex!(eq_to_diff) - end - end - - return ils +function (zvifd::ZeroVariablesIfFullyDetermined!)(structure::SystemStructure, ils::CLIL.SparseMatrixCLIL, v::Int) + zvifd.fully_determined ? StateSelection.force_var_to_zero!(structure, ils, v) : ils end function exactdiv(a::Integer, b) @@ -385,99 +126,3 @@ function exactdiv(a::Integer, b) end swap!(v, i, j) = v[i], v[j] = v[j], v[i] - -""" -$(SIGNATURES) - -Use Kahn's algorithm to topologically sort observed equations. - -Example: -```julia -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 - z ~ 2 - y ~ 2z + k - ]; - -julia> ModelingToolkit.topsort_equations(eqs, [x, y, z, k]) -3-element Vector{Equation}: - Equation(z(t), 2) - Equation(y(t), k(t) + 2z(t)) - Equation(x(t), y(t) + z(t)) -``` -""" -function topsort_equations(eqs, unknowns; check = true) - graph, assigns = observed2graph(eqs, unknowns) - neqs = length(eqs) - degrees = zeros(Int, neqs) - - for 𝑠eq in 1:length(eqs) - var = assigns[𝑠eq] - for 𝑑eq in 𝑑neighbors(graph, var) - # 𝑠eq => 𝑑eq - degrees[𝑑eq] += 1 - end - end - - q = Queue{Int}(neqs) - for (i, d) in enumerate(degrees) - @static if pkgversion(DataStructures) >= v"0.19" - d == 0 && push!(q, i) - else - d == 0 && enqueue!(q, i) - end - end - - idx = 0 - ordered_eqs = similar(eqs, 0) - sizehint!(ordered_eqs, neqs) - while !isempty(q) - @static if pkgversion(DataStructures) >= v"0.19" - 𝑠eq = popfirst!(q) - else - 𝑠eq = dequeue!(q) - end - idx += 1 - push!(ordered_eqs, eqs[𝑠eq]) - var = assigns[𝑠eq] - for 𝑑eq in 𝑑neighbors(graph, var) - degree = degrees[𝑑eq] = degrees[𝑑eq] - 1 - @static if pkgversion(DataStructures) >= v"0.19" - degree == 0 && push!(q, 𝑑eq) - else - degree == 0 && enqueue!(q, 𝑑eq) - end - end - end - - (check && idx != neqs) && throw(ArgumentError("The equations have at least one cycle.")) - - return ordered_eqs -end - -function observed2graph(eqs, unknowns) - graph = BipartiteGraph(length(eqs), length(unknowns)) - v2j = Dict(unknowns .=> 1:length(unknowns)) - - # `assigns: eq -> var`, `eq` defines `var` - assigns = similar(eqs, Int) - - for (i, eq) in enumerate(eqs) - lhs_j = get(v2j, eq.lhs, nothing) - lhs_j === nothing && - throw(ArgumentError("The lhs $(eq.lhs) of $eq, doesn't appear in unknowns.")) - assigns[i] = lhs_j - vs = vars(eq.rhs; op = Symbolics.Operator) - for v in vs - j = get(v2j, v, nothing) - j !== nothing && add_edge!(graph, i, j) - end - end - - return graph, assigns -end diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 9836c05ce8..ec50838f84 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -1,835 +1,3 @@ -""" - $(TYPEDEF) - AnalysisPoint(input, name::Symbol, outputs::Vector) - -Create an AnalysisPoint for linear analysis. Analysis points can be created by calling - -``` -connect(out, :ap_name, in...) -``` - -Where `out` is the output being connected to the inputs `in...`. All involved -connectors (input and outputs) are required to either have an unknown named -`u` or a single unknown, all of which should have the same size. - -See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref), [`open_loop`](@ref) - -# Fields - -$(TYPEDFIELDS) - -# Example - -```julia -using ModelingToolkit -using ModelingToolkitStandardLibrary.Blocks -using ModelingToolkit: t_nounits as t - -@named P = FirstOrder(k = 1, T = 1) -@named C = Gain(; k = -1) -t = ModelingToolkit.get_iv(P) - -eqs = [connect(P.output, C.input) - connect(C.output, :plant_input, P.input)] -sys = System(eqs, t, systems = [P, C], name = :feedback_system) - -matrices_S, _ = get_sensitivity(sys, :plant_input) # Compute the matrices of a state-space representation of the (input) sensitivity function. -matrices_T, _ = get_comp_sensitivity(sys, :plant_input) -``` - -Continued linear analysis and design can be performed using ControlSystemsBase.jl. -Create `ControlSystemsBase.StateSpace` objects using - -```julia -using ControlSystemsBase, Plots -S = ss(matrices_S...) -T = ss(matrices_T...) -bodeplot([S, T], lab = ["S" "T"]) -``` - -The sensitivity functions obtained this way should be equivalent to the ones obtained with the code below - -```julia -using ControlSystemsBase -P = tf(1.0, [1, 1]) -C = 1 # Negative feedback assumed in ControlSystems -S = sensitivity(P, C) # or feedback(1, P*C) -T = comp_sensitivity(P, C) # or feedback(P*C) -``` -""" -struct AnalysisPoint - """ - The input to the connection. In the context of ModelingToolkitStandardLibrary.jl, - this is a `RealOutput` connector. - """ - input::Any - """ - The name of the analysis point. - """ - name::Symbol - """ - The outputs of the connection. In the context of ModelingToolkitStandardLibrary.jl, - these are all `RealInput` connectors. - """ - outputs::Union{Nothing, Vector{Any}} - - function AnalysisPoint(input, name::Symbol, outputs; verbose = true) - # input to analysis point should be an output variable - if verbose && input !== nothing - var = ap_var(input) - isoutput(var) || ap_warning(1, name, true) - end - # outputs of analysis points should be input variables - if verbose && outputs !== nothing - for (i, output) in enumerate(outputs) - var = ap_var(output) - isinput(var) || ap_warning(2 + i, name, false) - end - end - - return new(input, name, outputs) - end -end - -function ap_warning(arg::Int, name::Symbol, should_be_output) - causality = should_be_output ? "output" : "input" - @warn """ - The $(arg)-th argument to analysis point $(name) was not a $causality. This is supported in \ - order to handle inverse models, but may not be what you intended. - - If you are building a forward mode (causal), you may want to swap this argument with \ - one on the opposite side of the name of the analysis point provided to `connect`. \ - Learn more about the causality of analysis points in the docstring for `AnalysisPoint`. \ - Silence this message using `connect(out, :name, in...; warn = false)`. - """ -end - -AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing) -""" - $(TYPEDSIGNATURES) - -Create an `AnalysisPoint` with the given name, with no input or outputs specified. -""" -AnalysisPoint(name::Symbol) = AnalysisPoint(nothing, name, nothing) - -Base.nameof(ap::AnalysisPoint) = ap.name - -Base.show(io::IO, ap::AnalysisPoint) = show(io, MIME"text/plain"(), ap) -function Base.show(io::IO, ::MIME"text/plain", ap::AnalysisPoint) - if ap.input === nothing - print(io, "0") - return - end - if get(io, :compact, false) - print(io, - "AnalysisPoint($(ap_var(ap.input)), $(ap_var.(ap.outputs)); name=$(ap.name))") - else - print(io, "AnalysisPoint(") - printstyled(io, ap.name, color = :cyan) - if ap.input !== nothing && ap.outputs !== nothing - print(io, " from ") - printstyled(io, ap_var(ap.input), color = :green) - print(io, " to ") - if length(ap.outputs) == 1 - printstyled(io, ap_var(ap.outputs[1]), color = :blue) - else - printstyled(io, "[", join(ap_var.(ap.outputs), ", "), "]", color = :blue) - end - end - print(io, ")") - end -end - -Symbolics.hide_lhs(::AnalysisPoint) = true - -@latexrecipe function f(ap::AnalysisPoint) - index --> :subscript - snakecase --> true - ap.input === nothing && return 0 - outs = Expr(:vect) - append!(outs.args, ap_var.(ap.outputs)) - return Expr(:call, :AnalysisPoint, ap_var(ap.input), ap.name, outs) -end - -function Base.show(io::IO, ::MIME"text/latex", ap::AnalysisPoint) - print(io, latexify(ap)) -end - -""" - $(TYPEDSIGNATURES) - -Convert an `AnalysisPoint` to a standard connection. -""" -function to_connection(ap::AnalysisPoint) - return connect(ap.input, ap.outputs...) -end - -""" - $(TYPEDSIGNATURES) - -Namespace an `AnalysisPoint` by namespacing the involved systems and the name of the point. -""" -function renamespace(sys, ap::AnalysisPoint) - return AnalysisPoint( - ap.input === nothing ? nothing : renamespace(sys, ap.input), - renamespace(sys, ap.name), - ap.outputs === nothing ? nothing : map(Base.Fix1(renamespace, sys), ap.outputs) - ) -end - -# create analysis points via `connect` -function connect(in, ap::AnalysisPoint, outs...; verbose = true) - return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs); verbose) -end - -""" - connect(output_connector, ap_name::Symbol, input_connector; verbose = true) - connect(output_connector, ap::AnalysisPoint, input_connector; verbose = true) - -Connect `output_connector` and `input_connector` with an [`AnalysisPoint`](@ref) inbetween. -The incoming connection `output_connector` is expected to be an output connector (for -example, `ModelingToolkitStandardLibrary.Blocks.RealOutput`), and vice versa. - -*PLEASE NOTE*: The connection is assumed to be *causal*, meaning that - -```julia -@named P = FirstOrder(k = 1, T = 1) -@named C = Gain(; k = -1) -connect(C.output, :plant_input, P.input) -``` - -is correct, whereas - -```julia -connect(P.input, :plant_input, C.output) -``` - -typically is not (unless the model is an inverse model). - -# Arguments - -- `output_connector`: An output connector -- `input_connector`: An input connector -- `ap`: An explicitly created [`AnalysisPoint`](@ref) -- `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be - created automatically. - -# Keyword arguments - -- `verbose`: Warn if an input is connected to an output (reverse causality). Silence this - warning if you are analyzing an inverse model. -""" -function connect(in::AbstractSystem, name::Symbol, out, outs...; verbose = true) - return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose) -end - -function connect( - in::ConnectableSymbolicT, name::Symbol, out::ConnectableSymbolicT, - outs::ConnectableSymbolicT...; verbose = true) - allvars = (in, out, outs...) - validate_causal_variables_connection(allvars) - return AnalysisPoint() ~ AnalysisPoint( - unwrap(in), name, unwrap.([out; collect(outs)]); verbose) -end - -""" - $(TYPEDSIGNATURES) - -Return all the namespaces in `name`. Namespaces should be separated by `.` or -`$NAMESPACE_SEPARATOR`. -""" -function namespace_hierarchy(name::Symbol) - map( - Symbol, split(string(name), ('.', NAMESPACE_SEPARATOR))) -end - -""" - $(TYPEDSIGNATURES) - -Remove all `AnalysisPoint`s in `sys` and any of its subsystems, replacing them by equivalent connections. -""" -function remove_analysis_points(sys::AbstractSystem) - eqs = map(get_eqs(sys)) do eq - eq.lhs isa AnalysisPoint ? to_connection(eq.rhs) : eq - end - @set! sys.eqs = eqs - @set! sys.systems = map(remove_analysis_points, get_systems(sys)) - - return sys -end - -""" - $(TYPEDSIGNATURES) - -Given a system involved in an `AnalysisPoint`, get the variable to be used in the -connection. This is the variable named `u` if present, and otherwise the only -variable in the system. If the system does not have a variable named `u` and -contains multiple variables, throw an error. -""" -function ap_var(sys::AbstractSystem) - if hasproperty(sys, :u) - return sys.u - end - x = unknowns(sys) - length(x) == 1 && return renamespace(sys, x[1]) - error("Could not determine the analysis-point variable in system $(nameof(sys)). To use an analysis point, apply it to a connection between causal blocks which have a variable named `u` or a single unknown of the same size.") -end - -""" - $(TYPEDSIGNATURES) - -For an `AnalysisPoint` involving causal variables. Simply return the variable. -""" -function ap_var(var::ConnectableSymbolicT) - return var -end - -""" - $(TYPEDEF) - -The supertype of all transformations that can be applied to an `AnalysisPoint`. All -concrete subtypes must implement `apply_transformation`. -""" -abstract type AnalysisPointTransformation end - -""" - apply_transformation(tf::AnalysisPointTransformation, sys::AbstractSystem) - -Apply the given analysis point transformation `tf` to the system `sys`. Throw an error if -any analysis points referred to in `tf` are not present in `sys`. Return a tuple -containing the modified system as the first element, and a tuple of the additional -variables added by the transformation as the second element. -""" -function apply_transformation end - -""" - $(TYPEDSIGNATURES) - -Given a namespaced subsystem `target` of root system `root`, return a modified copy of -`root` with `target` modified according to `fn` alongside any extra variables added -by `fn`. - -`fn` is a function which takes the instance of `target` present in the hierarchy of -`root`, and returns a 2-tuple consisting of the modified version of `target` and a tuple -of the extra variables added. -""" -function modify_nested_subsystem(fn, root::AbstractSystem, target::AbstractSystem) - modify_nested_subsystem( - fn, root, nameof(target)) -end -""" - $(TYPEDSIGNATURES) - -Apply the modification to the system containing the namespaced analysis point `target`. -""" -function modify_nested_subsystem(fn, root::AbstractSystem, target::AnalysisPoint) - modify_nested_subsystem( - fn, root, @view namespace_hierarchy(nameof(target))[1:(end - 1)]) -end -""" - $(TYPEDSIGNATURES) - -Apply the modification to the nested subsystem of `root` whose namespaced name matches -the provided name `target`. The namespace separator in `target` should be `.` or -`$NAMESPACE_SEPARATOR`. The `target` may include `nameof(root)` as the first namespace. -""" -function modify_nested_subsystem(fn, root::AbstractSystem, target::Symbol) - modify_nested_subsystem( - fn, root, namespace_hierarchy(target)) -end - -""" - $(TYPEDSIGNATURES) - -Apply the modification to the nested subsystem of `root` where the name of the subsystem at -each level in the hierarchy is given by elements of `hierarchy`. For example, if -`hierarchy = [:a, :b, :c]`, the system being searched for will be `root.a.b.c`. Note that -the hierarchy may include the name of the root system, in which the first element will be -ignored. For example, `hierarchy = [:root, :a, :b, :c]` also searches for `root.a.b.c`. -An empty `hierarchy` will apply the modification to `root`. -""" -function modify_nested_subsystem( - fn, root::AbstractSystem, hierarchy::AbstractVector{Symbol}) - # no hierarchy, so just apply to the root - if isempty(hierarchy) - return fn(root) - end - # ignore the name of the root - if nameof(root) != hierarchy[1] - error(""" - Invalid analysis point name `$(join(hierarchy, NAMESPACE_SEPARATOR))`. The name - must include the name of the root system `$(nameof(root))`. This typically happens - when using an analysis point obtained by calling `getproperty` on a system marked - as `complete` to linearize a system that is not marked as `complete`. - """) - end - hierarchy = @view hierarchy[2:end] - - # recursive helper function which does the searching and modification - function _helper(sys::AbstractSystem, i::Int) - if i > length(hierarchy) - # we reached past the end, so everything matched and - # `sys` is the system to modify. - sys, vars = fn(sys) - else - # find the subsystem with the given name and error otherwise - cur = hierarchy[i] - idx = findfirst(subsys -> nameof(subsys) == cur, get_systems(sys)) - idx === nothing && - error("System $(join([nameof(root); hierarchy[1:i-1]], '.')) does not have a subsystem named $cur.") - - # recurse into new subsystem - newsys, vars = _helper(get_systems(sys)[idx], i + 1) - # update this system with modified subsystem - @set! sys.systems[idx] = newsys - end - # only namespace variables from inner systems - if i != 1 - vars = ntuple(Val(length(vars))) do i - renamespace(sys, vars[i]) - end - end - return sys, vars - end - - return _helper(root, 1) -end - -""" - $(TYPEDSIGNATURES) - -Given a system `sys` and analysis point `ap`, return the index in `get_eqs(sys)` -containing an equation which has as it's RHS an analysis point with name `nameof(ap)`. -""" -function analysis_point_index(sys::AbstractSystem, ap::AnalysisPoint) - analysis_point_index( - sys, nameof(ap)) -end -""" - $(TYPEDSIGNATURES) - -Search for the analysis point with the given `name` in `get_eqs(sys)`. -""" -function analysis_point_index(sys::AbstractSystem, name::Symbol) - name = namespace_hierarchy(name)[end] - findfirst(get_eqs(sys)) do eq - eq.lhs isa AnalysisPoint && nameof(eq.rhs) == name - end -end - -""" - $(TYPEDSIGNATURES) - -Create a new variable of the same `symtype` and size as `var`, using `name` as the base -name for the new variable. `iv` denotes the independent variable of the system. Prefix -`d_` to the name of the new variable if `perturb == true`. Return the new symbolic -variable and the appropriate zero value for it. -""" -function get_analysis_variable(var, name, iv; perturb = true) - var = unwrap(var) - if perturb - name = Symbol(:d_, name) - end - if symbolic_type(var) == ArraySymbolic() - T = Array{eltype(symtype(var)), ndims(var)} - pvar = unwrap(only(@variables $name(iv)::T)) - pvar = setmetadata(pvar, Symbolics.ArrayShapeCtx, Symbolics.shape(var)) - default = zeros(eltype(symtype(var)), size(var)) - else - T = symtype(var) - pvar = unwrap(only(@variables $name(iv)::T)) - default = zero(T) - end - return pvar, default -end - -function with_analysis_point_ignored(sys::AbstractSystem, ap::AnalysisPoint) - has_ignored_connections(sys) || return sys - ignored = get_ignored_connections(sys) - if ignored === nothing - ignored = Connection[] - else - ignored = copy(ignored) - end - if ap.outputs === nothing - error("Empty analysis point") - end - - push!(ignored, Connection([unwrap(ap.input); unwrap.(ap.outputs)])) - - return @set sys.ignored_connections = ignored -end - -#### PRIMITIVE TRANSFORMATIONS - -const DOC_WILL_REMOVE_AP = """ - Note that this transformation will remove `ap`, causing any subsequent transformations \ - referring to it to fail.\ - """ - -const DOC_ADDED_VARIABLE = """ - The added variable(s) will have a default of zero, of the appropriate type and size.\ - """ - -""" - $(TYPEDEF) - -A transformation which breaks the connection referred to by `ap`. If `add_input == true`, -it will add a new input variable which connects to the outputs of the analysis point. -`apply_transformation` returns the new input variable (if added) as the auxiliary -information. The new input variable will have the name `Symbol(:d_, nameof(ap))`. - -$DOC_WILL_REMOVE_AP - -$DOC_ADDED_VARIABLE - -## Fields - -$(TYPEDFIELDS) -""" -struct Break <: AnalysisPointTransformation - """ - The analysis point to break. - """ - ap::AnalysisPoint - """ - Whether to add a new input variable connected to all the outputs of `ap`. - """ - add_input::Bool - """ - Whether the default of the added input variable should be the input of `ap`. Only - applicable if `add_input == true`. - """ - default_outputs_to_input::Bool - """ - Whether the added input is a parameter. Only applicable if `add_input == true`. - """ - added_input_is_param::Bool -end - -""" - $(TYPEDSIGNATURES) - -`Break` the given analysis point `ap`. -""" -function Break(ap::AnalysisPoint, add_input::Bool = false, default_outputs_to_input = false) - Break(ap, add_input, default_outputs_to_input, false) -end - -function apply_transformation(tf::Break, sys::AbstractSystem) - modify_nested_subsystem(sys, tf.ap) do breaksys - # get analysis point - ap_idx = analysis_point_index(breaksys, tf.ap) - ap_idx === nothing && - error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - breaksys_eqs = copy(get_eqs(breaksys)) - @set! breaksys.eqs = breaksys_eqs - - ap = breaksys_eqs[ap_idx].rhs - deleteat!(breaksys_eqs, ap_idx) - - breaksys = with_analysis_point_ignored(breaksys, ap) - - tf.add_input || return breaksys, () - - ap_ivar = ap_var(ap.input) - new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) - for outsys in ap.outputs - push!(breaksys_eqs, ap_var(outsys) ~ new_var) - end - defs = copy(get_defaults(breaksys)) - defs[new_var] = if tf.default_outputs_to_input - ap_ivar - else - new_def - end - @set! breaksys.defaults = defs - if tf.added_input_is_param - ps = copy(get_ps(breaksys)) - push!(ps, new_var) - @set! breaksys.ps = ps - else - unks = copy(get_unknowns(breaksys)) - push!(unks, new_var) - @set! breaksys.unknowns = unks - end - - return breaksys, (new_var,) - end -end - -""" - $(TYPEDEF) - -A transformation which returns the variable corresponding to the input of the analysis -point. Does not modify the system. - -## Fields - -$(TYPEDFIELDS) -""" -struct GetInput <: AnalysisPointTransformation - """ - The analysis point to get the input of. - """ - ap::AnalysisPoint -end - -function apply_transformation(tf::GetInput, sys::AbstractSystem) - modify_nested_subsystem(sys, tf.ap) do ap_sys - # get the analysis point - ap_idx = analysis_point_index(ap_sys, tf.ap) - ap_idx === nothing && - error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - # get the analysis point - ap_sys_eqs = get_eqs(ap_sys) - ap = ap_sys_eqs[ap_idx].rhs - - # input variable - ap_ivar = ap_var(ap.input) - return ap_sys, (ap_ivar,) - end -end - -""" - $(TYPEDEF) - -A transformation that creates a new input variable which is added to the input of -the analysis point before connecting to the outputs. The new variable will have the name -`Symbol(:d_, nameof(ap))`. - -If `with_output == true`, also creates an additional new variable which has the value -provided to the outputs after the above modification. This new variable has the same name -as the analysis point and will be the second variable in the tuple of new variables returned -from `apply_transformation`. - -$DOC_WILL_REMOVE_AP - -$DOC_ADDED_VARIABLE - -## Fields - -$(TYPEDFIELDS) -""" -struct PerturbOutput <: AnalysisPointTransformation - """ - The analysis point to modify - """ - ap::AnalysisPoint - """ - Whether to add an additional output variable. - """ - with_output::Bool -end - -""" - $(TYPEDSIGNATURES) - -Add an input without an additional output variable. -""" -PerturbOutput(ap::AnalysisPoint) = PerturbOutput(ap, false) - -function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) - modify_nested_subsystem(sys, tf.ap) do ap_sys - # get analysis point - ap_idx = analysis_point_index(ap_sys, tf.ap) - ap_idx === nothing && - error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - # modified equations - ap_sys_eqs = copy(get_eqs(ap_sys)) - @set! ap_sys.eqs = ap_sys_eqs - ap = ap_sys_eqs[ap_idx].rhs - # remove analysis point - deleteat!(ap_sys_eqs, ap_idx) - ap_sys = with_analysis_point_ignored(ap_sys, ap) - - # add equations involving new variable - ap_ivar = ap_var(ap.input) - new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) - for outsys in ap.outputs - push!(ap_sys_eqs, ap_var(outsys) ~ ap_ivar + wrap(new_var)) - end - # add variable - unks = copy(get_unknowns(ap_sys)) - push!(unks, new_var) - @set! ap_sys.unknowns = unks - # add default - defs = copy(get_defaults(ap_sys)) - defs[new_var] = new_def - @set! ap_sys.defaults = defs - - tf.with_output || return ap_sys, (new_var,) - - # add output variable, equation, default - out_var, - out_def = get_analysis_variable( - ap_ivar, nameof(ap), get_iv(sys); perturb = false) - push!(ap_sys_eqs, out_var ~ ap_ivar + wrap(new_var)) - push!(unks, out_var) - - return ap_sys, (new_var, out_var) - end -end - -""" - $(TYPEDEF) - -A transformation which adds a variable named `name` to the system containing the analysis -point `ap`. $DOC_ADDED_VARIABLE - -# Fields - -$(TYPEDFIELDS) -""" -struct AddVariable <: AnalysisPointTransformation - """ - The analysis point in the system to modify, and whose input should be used as the - template for the new variable. - """ - ap::AnalysisPoint - """ - The name of the added variable. - """ - name::Symbol -end - -""" - $(TYPEDSIGNATURES) - -Add a new variable to the system containing analysis point `ap` with the same name as the -analysis point. -""" -AddVariable(ap::AnalysisPoint) = AddVariable(ap, nameof(ap)) - -function apply_transformation(tf::AddVariable, sys::AbstractSystem) - modify_nested_subsystem(sys, tf.ap) do ap_sys - # get analysis point - ap_idx = analysis_point_index(ap_sys, tf.ap) - ap_idx === nothing && - error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - ap_sys_eqs = get_eqs(ap_sys) - ap = ap_sys_eqs[ap_idx].rhs - - # add equations involving new variable - ap_ivar = ap_var(ap.input) - new_var, - new_def = get_analysis_variable( - ap_ivar, tf.name, get_iv(sys); perturb = false) - # add variable - unks = copy(get_unknowns(ap_sys)) - push!(unks, new_var) - @set! ap_sys.unknowns = unks - return ap_sys, (new_var,) - end -end - -#### DERIVED TRANSFORMATIONS - -""" - $(TYPEDSIGNATURES) - -A transformation enable calculating the sensitivity function about the analysis point `ap`. -The returned added variables are `(du, u)` where `du` is the perturbation added to the -input, and `u` is the output after perturbation. - -$DOC_WILL_REMOVE_AP - -$DOC_ADDED_VARIABLE -""" -SensitivityTransform(ap::AnalysisPoint) = PerturbOutput(ap, true) - -""" - $(TYPEDEF) - -A transformation to enable calculating the complementary sensitivity function about the -analysis point `ap`. The returned added variables are `(du, u)` where `du` is the -perturbation added to the outputs and `u` is the input to the analysis point. - -$DOC_WILL_REMOVE_AP - -$DOC_ADDED_VARIABLE - -# Fields - -$(TYPEDFIELDS) -""" -struct ComplementarySensitivityTransform <: AnalysisPointTransformation - """ - The analysis point to modify. - """ - ap::AnalysisPoint -end - -function apply_transformation(cst::ComplementarySensitivityTransform, sys::AbstractSystem) - sys, (u,) = apply_transformation(GetInput(cst.ap), sys) - sys, - (du,) = apply_transformation( - AddVariable( - cst.ap, Symbol(namespace_hierarchy(nameof(cst.ap))[end], :_comp_sens_du)), - sys) - sys, (_du,) = apply_transformation(PerturbOutput(cst.ap), sys) - - # `PerturbOutput` adds the equation `input + _du ~ output` - # but comp sensitivity wants `output + du ~ input`. Thus, `du ~ -_du`. - eqs = copy(get_eqs(sys)) - @set! sys.eqs = eqs - push!(eqs, du ~ -wrap(_du)) - - defs = copy(get_defaults(sys)) - @set! sys.defaults = defs - defs[du] = -wrap(_du) - return sys, (du, u) -end - -""" - $(TYPEDEF) - -A transformation to enable calculating the loop transfer function about the analysis point -`ap`. The returned added variables are `(du, u)` where `du` feeds into the outputs of `ap` -and `u` is the input of `ap`. - -$DOC_WILL_REMOVE_AP - -$DOC_ADDED_VARIABLE - -# Fields - -$(TYPEDFIELDS) -""" -struct LoopTransferTransform <: AnalysisPointTransformation - """ - The analysis point to modify. - """ - ap::AnalysisPoint -end - -function apply_transformation(tf::LoopTransferTransform, sys::AbstractSystem) - sys, (u,) = apply_transformation(GetInput(tf.ap), sys) - sys, (du,) = apply_transformation(Break(tf.ap, true), sys) - return sys, (du, u) -end - -""" - $(TYPEDSIGNATURES) - -A utility function to get the "canonical" form of a list of analysis points. Always returns -a list of values. Any value that cannot be turned into an `AnalysisPoint` (i.e. isn't -already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. `Symbol` names of -`AnalysisPoint`s are namespaced with `sys`. -""" -canonicalize_ap(sys::AbstractSystem, ap::Symbol) = [AnalysisPoint(renamespace(sys, ap))] -function canonicalize_ap(sys::AbstractSystem, ap::AnalysisPoint) - if does_namespacing(sys) - return [ap] - else - return [renamespace(sys, ap)] - end -end -canonicalize_ap(sys::AbstractSystem, ap) = [ap] -function canonicalize_ap(sys::AbstractSystem, aps::Vector) - mapreduce(Base.Fix1(canonicalize_ap, sys), vcat, aps; init = []) -end - """ $(TYPEDSIGNATURES) @@ -859,6 +27,7 @@ const DOC_SYS_MODIFIER = """ additional transformations, returning the modified system. The modified system is passed to `linearization_function`. """ + """ $(TYPEDSIGNATURES) @@ -887,7 +56,6 @@ function get_linear_analysis_function( end linearization_function(system_modifier(sys), dus, us; kwargs...) end - """ $(TYPEDSIGNATURES) @@ -952,24 +120,6 @@ for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer] end end -""" - $(TYPEDSIGNATURES) - -Apply `LoopTransferTransform` to the analysis point `ap` and return the -result of `apply_transformation`. - -# Keyword Arguments - -- `system_modifier`: a function which takes the modified system and returns a new system - with any required further modifications performed. -""" -function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = identity) - ap = only(canonicalize_ap(sys, ap)) - tf = LoopTransferTransform(ap) - sys, vars = apply_transformation(tf, sys) - return system_modifier(sys), vars -end - """ sys, input_vars, output_vars = $(TYPEDSIGNATURES) @@ -1062,36 +212,4 @@ Compute the (linearized) loop-transfer function in analysis point `ap`, from `ap See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`open_loop`](@ref). """ get_looptransfer -# -""" - generate_control_function(sys::ModelingToolkit.AbstractSystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) - -When called with analysis points as input arguments, we assume that all analysis points corresponds to connections that should be opened (broken). The use case for this is to get rid of input signal blocks, such as `Step` or `Sine`, since these are useful for simulation but are not needed when using the plant model in a controller or state estimator. -""" -function generate_control_function( - sys::ModelingToolkit.AbstractSystem, input_ap_name::Union{ - Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, - dist_ap_name::Union{ - Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}} = nothing; - system_modifier = identity, - kwargs...) - input_ap_name = canonicalize_ap(sys, input_ap_name) - u = [] - for input_ap in input_ap_name - sys, (du, _) = open_loop(sys, input_ap) - push!(u, du) - end - if dist_ap_name === nothing - return ModelingToolkit.generate_control_function(system_modifier(sys), u; kwargs...) - end - - dist_ap_name = canonicalize_ap(sys, dist_ap_name) - d = [] - for dist_ap in dist_ap_name - sys, (du, _) = open_loop(sys, dist_ap) - push!(d, du) - end - - ModelingToolkit.generate_control_function(system_modifier(sys), u, d; kwargs...) -end diff --git a/src/systems/clock_inference.jl b/src/systems/clock_inference.jl index 76c91f61c6..8a6faecbd5 100644 --- a/src/systems/clock_inference.jl +++ b/src/systems/clock_inference.jl @@ -1,383 +1,20 @@ -@data ClockVertex begin - Variable(Int) - Equation(Int) - InitEquation(Int) - Clock(SciMLBase.AbstractClock) +function MTKTearing.input_timedomain(::Sample, _::MTKTearing.IOTimeDomainArgsT = nothing) + MTKTearing.InputTimeDomainElT[ContinuousClock()] end - -struct ClockInference{S} - """Tearing state.""" - ts::S - """The time domain (discrete clock, continuous) of each equation.""" - eq_domain::Vector{TimeDomain} - """The time domain (discrete clock, continuous) of each initialization equation.""" - init_eq_domain::Vector{TimeDomain} - """The output time domain (discrete clock, continuous) of each variable.""" - var_domain::Vector{TimeDomain} - inference_graph::HyperGraph{ClockVertex.Type} - """The set of variables with concrete domains.""" - inferred::BitSet -end - -function ClockInference(ts::TransformationState) - @unpack structure = ts - @unpack graph = structure - eq_domain = TimeDomain[ContinuousClock() for _ in 1:nsrcs(graph)] - init_eq_domain = TimeDomain[ContinuousClock() - for _ in 1:length(initialization_equations(ts.sys))] - var_domain = TimeDomain[ContinuousClock() for _ in 1:ndsts(graph)] - inferred = BitSet() - for (i, v) in enumerate(get_fullvars(ts)) - d = get_time_domain(ts, v) - if is_concrete_time_domain(d) - push!(inferred, i) - var_domain[i] = d - end - end - inference_graph = HyperGraph{ClockVertex.Type}() - for i in 1:nsrcs(graph) - add_vertex!(inference_graph, ClockVertex.Equation(i)) - end - for i in eachindex(initialization_equations(ts.sys)) - add_vertex!(inference_graph, ClockVertex.InitEquation(i)) - end - for i in 1:ndsts(graph) - varvert = ClockVertex.Variable(i) - add_vertex!(inference_graph, varvert) - v = ts.fullvars[i] - d = get_time_domain(v) - is_concrete_time_domain(d) || continue - dvert = ClockVertex.Clock(d) - add_vertex!(inference_graph, dvert) - add_edge!(inference_graph, (varvert, dvert)) - end - ClockInference(ts, eq_domain, init_eq_domain, var_domain, inference_graph, 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, metadata(ex)) - end -end - -""" -Update the equation-to-time domain mapping by inferring the time domain from the variables. -""" -function infer_clocks!(ci::ClockInference) - @unpack ts, eq_domain, init_eq_domain, var_domain, inferred, inference_graph = ci - @unpack var_to_diff, graph = ts.structure - fullvars = get_fullvars(ts) - isempty(inferred) && return ci - - var_to_idx = Dict(fullvars .=> eachindex(fullvars)) - - # all shifted variables have the same clock as the unshifted variant - for (i, v) in enumerate(fullvars) - iscall(v) || continue - operation(v) isa Shift || continue - unshifted = only(arguments(v)) - add_edge!(inference_graph, ( - ClockVertex.Variable(i), ClockVertex.Variable(var_to_idx[unshifted]))) - end - - # preallocated buffers: - # variables in each equation - varsbuf = Set() - # variables in each argument to an operator - arg_varsbuf = Set() - # hyperedge for each equation - hyperedge = Set{ClockVertex.Type}() - # hyperedge for each argument to an operator - arg_hyperedge = Set{ClockVertex.Type}() - # mapping from `i` in `InferredDiscrete(i)` to the vertices in that inferred partition - relative_hyperedges = Dict{Int, Set{ClockVertex.Type}}() - - function infer_equation(ieq, eq, is_initialization_equation) - empty!(varsbuf) - empty!(hyperedge) - # get variables in equation - vars!(varsbuf, eq; op = Symbolics.Operator) - # add the equation to the hyperedge - eq_node = if is_initialization_equation - ClockVertex.InitEquation(ieq) - else - ClockVertex.Equation(ieq) - end - push!(hyperedge, eq_node) - for var in varsbuf - idx = get(var_to_idx, var, nothing) - # if this is just a single variable, add it to the hyperedge - if idx isa Int - push!(hyperedge, ClockVertex.Variable(idx)) - # we don't immediately `continue` here because this variable might be a - # `Sample` or similar and we want the clock information from it if it is. - end - # now we only care about synchronous operators - iscall(var) || continue - op = operation(var) - is_timevarying_operator(op) || continue - - # arguments and corresponding time domains - args = arguments(var) - tdomains = input_timedomain(op) - if !(tdomains isa AbstractArray || tdomains isa Tuple) - tdomains = [tdomains] - end - nargs = length(args) - ndoms = length(tdomains) - if nargs != ndoms - throw(ArgumentError(""" - Operator $op applied to $nargs arguments $args but only returns $ndoms \ - domains $tdomains from `input_timedomain`. - """)) - end - - # each relative clock mapping is only valid per operator application - empty!(relative_hyperedges) - for (arg, domain) in zip(args, tdomains) - empty!(arg_varsbuf) - empty!(arg_hyperedge) - # get variables in argument - vars!(arg_varsbuf, arg; op = Union{Differential, Shift}) - # get hyperedge for involved variables - for v in arg_varsbuf - vidx = get(var_to_idx, v, nothing) - vidx === nothing && continue - push!(arg_hyperedge, ClockVertex.Variable(vidx)) - end - - Moshi.Match.@match domain begin - # If the time domain for this argument is a clock, then all variables in this edge have that clock. - x::SciMLBase.AbstractClock => begin - # add the clock to the edge - push!(arg_hyperedge, ClockVertex.Clock(x)) - # add the edge to the graph - add_edge!(inference_graph, arg_hyperedge) - end - # We only know that this time domain is inferred. Treat it as a unique domain, all we know is that the - # involved variables have the same clock. - InferredClock.Inferred() => add_edge!(inference_graph, arg_hyperedge) - # All `InferredDiscrete` with the same `i` have the same clock (including output domain) so we don't - # add the edge, and instead add this to the `relative_hyperedges` mapping. - InferredClock.InferredDiscrete(i) => begin - relative_edge = get!(() -> Set{ClockVertex.Type}(), relative_hyperedges, i) - union!(relative_edge, arg_hyperedge) - end - end - end - - outdomain = output_timedomain(op) - Moshi.Match.@match outdomain begin - x::SciMLBase.AbstractClock => begin - push!(hyperedge, ClockVertex.Clock(x)) - end - InferredClock.Inferred() => nothing - InferredClock.InferredDiscrete(i) => begin - buffer = get(relative_hyperedges, i, nothing) - if buffer !== nothing - union!(hyperedge, buffer) - delete!(relative_hyperedges, i) - end - end - end - - for (_, relative_edge) in relative_hyperedges - add_edge!(inference_graph, relative_edge) - end - end - - add_edge!(inference_graph, hyperedge) - end - for (ieq, eq) in enumerate(equations(ts)) - infer_equation(ieq, eq, false) - end - for (ieq, eq) in enumerate(initialization_equations(ts.sys)) - infer_equation(ieq, eq, true) - end - - clock_partitions = connectionsets(inference_graph) - for partition in clock_partitions - clockidxs = findall(vert -> Moshi.Data.isa_variant(vert, ClockVertex.Clock), partition) - if isempty(clockidxs) - push!(partition, ClockVertex.Clock(ContinuousClock())) - push!(clockidxs, length(partition)) - end - if length(clockidxs) > 1 - vidxs = Int[vert.:1 - for vert in partition - if Moshi.Data.isa_variant(vert, ClockVertex.Variable)] - clks = [vert.:1 for vert in view(partition, clockidxs)] - throw(ArgumentError(""" - Found clock partition with multiple associated clocks. Involved variables: \ - $(fullvars[vidxs]). Involved clocks: $(clks). - """)) - end - - clock = partition[only(clockidxs)].:1 - for vert in partition - Moshi.Match.@match vert begin - ClockVertex.Variable(i) => (var_domain[i] = clock) - ClockVertex.Equation(i) => (eq_domain[i] = clock) - ClockVertex.InitEquation(i) => (init_eq_domain[i] = clock) - ClockVertex.Clock(_) => nothing - end - end - end - - ci = substitute_sample_time(ci, ts) - return ci +function MTKTearing.output_timedomain(s::Sample, _::MTKTearing.IOTimeDomainArgsT = nothing) + s.clock end -function resize_or_push!(v, val, idx) - n = length(v) - if idx > n - for _ in (n + 1):idx - push!(v, Int[]) +function MTKTearing.input_timedomain(::Hold, args::MTKTearing.IOTimeDomainArgsT = nothing) + if args !== nothing + arg = args[1] + if MTKTearing.has_time_domain(arg) + return MTKTearing.InputTimeDomainElT[MTKTearing.get_time_domain(arg)] end - resize!(v, idx) end - push!(v[idx], val) + MTKTearing.InputTimeDomainElT[MTKTearing.InferredDiscrete()] # the Hold accepts any discrete end -function is_time_domain_conversion(v) - iscall(v) || return false - o = operation(v) - o isa Operator || return false - itd = input_timedomain(o) - allequal(itd) || return true - isempty(itd) && return true - otd = output_timedomain(o) - itd[1] == otd || return true - return false -end - -""" -For multi-clock systems, create a separate system for each clock in the system, along with associated equations. Return the updated tearing state, and the sets of clocked variables associated with each time domain. -""" -function split_system(ci::ClockInference{S}) where {S} - @unpack ts, eq_domain, init_eq_domain, var_domain, inferred = ci - fullvars = get_fullvars(ts) - @unpack graph = ts.structure - continuous_id = Ref(0) - clock_to_id = Dict{TimeDomain, Int}() - id_to_clock = TimeDomain[] - eq_to_cid = Vector{Int}(undef, nsrcs(graph)) - cid_to_eq = Vector{Int}[] - init_eq_to_cid = Vector{Int}(undef, length(initialization_equations(ts.sys))) - cid_to_init_eq = Vector{Int}[] - var_to_cid = Vector{Int}(undef, ndsts(graph)) - cid_to_var = Vector{Int}[] - # cid_counter = number of clocks - cid_counter = Ref(0) - - # populates clock_to_id and id_to_clock - # checks if there is a continuous_id (for some reason? clock to id does this too) - for (i, d) in enumerate(eq_domain) - cid = let cid_counter = cid_counter, id_to_clock = id_to_clock, - continuous_id = continuous_id - - # Fill the clock_to_id dict as you go, - # ContinuousClock() => 1, ... - get!(clock_to_id, d) do - cid = (cid_counter[] += 1) - push!(id_to_clock, d) - if d == ContinuousClock() - continuous_id[] = cid - end - cid - end - end - eq_to_cid[i] = cid - resize_or_push!(cid_to_eq, i, cid) - end - # NOTE: This assumes that there is at least one equation for each clock - for _ in 1:length(cid_to_eq) - push!(cid_to_init_eq, Int[]) - end - for (i, d) in enumerate(init_eq_domain) - cid = clock_to_id[d] - init_eq_to_cid[i] = cid - push!(cid_to_init_eq[cid], i) - end - continuous_id = continuous_id[] - # for each clock partition what are the input (indexes/vars) - input_idxs = map(_ -> Int[], 1:cid_counter[]) - inputs = map(_ -> Any[], 1:cid_counter[]) - # var_domain corresponds to fullvars/all variables in the system - nvv = length(var_domain) - # put variables into the right clock partition - # keep track of inputs to each partition - for i in 1:nvv - d = var_domain[i] - cid = get(clock_to_id, d, 0) - @assert cid!==0 "Internal error! Variable $(fullvars[i]) doesn't have a inferred time domain." - var_to_cid[i] = cid - v = fullvars[i] - if is_time_domain_conversion(v) - push!(input_idxs[cid], i) - push!(inputs[cid], fullvars[i]) - end - resize_or_push!(cid_to_var, i, cid) - end - - # breaks the system up into a continuous and 0 or more discrete systems - tss = similar(cid_to_eq, S) - for (id, (ieqs, iieqs, ivars)) in enumerate(zip(cid_to_eq, cid_to_init_eq, cid_to_var)) - ts_i = system_subset(ts, ieqs, iieqs, ivars) - if id != continuous_id - ts_i = shift_discrete_system(ts_i) - @set! ts_i.structure.only_discrete = true - end - tss[id] = ts_i - end - # put the continuous system at the back - if continuous_id != 0 - tss[continuous_id], tss[end] = tss[end], tss[continuous_id] - inputs[continuous_id], inputs[end] = inputs[end], inputs[continuous_id] - id_to_clock[continuous_id], - id_to_clock[end] = id_to_clock[end], - id_to_clock[continuous_id] - continuous_id = lastindex(tss) - end - return tss, inputs, continuous_id, id_to_clock +function MTKTearing.output_timedomain(::Hold, _::MTKTearing.IOTimeDomainArgsT = nothing) + ContinuousClock() end diff --git a/src/systems/codegen.jl b/src/systems/codegen.jl index fbbc9b6c9e..0147c0048c 100644 --- a/src/systems/codegen.jl +++ b/src/systems/codegen.jl @@ -1,1252 +1,24 @@ -const GENERATE_X_KWARGS = """ -- `expression`: `Val{true}` if this should return an `Expr` (or tuple of `Expr`s) of the - generated code. `Val{false}` otherwise. -- `wrap_gfw`: `Val{true}` if the returned functions should be wrapped in a callable - struct to make them callable using the expected syntax. The callable struct itself is - internal API. If `expression == Val{true}`, the returned expression will construct the - callable struct. If this function returns a tuple of functions/expressions, both will - be identical if `wrap_gfw == Val{true}`. -$EVAL_EXPR_MOD_KWARGS -""" - -const EXPERIMENTAL_WARNING = """ -!!! warn - - This API is experimental and may change in a future non-breaking release. -""" - -""" - $(TYPEDSIGNATURES) - -Generate the RHS function for the [`equations`](@ref) of a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `implicit_dae`: Whether the generated function should be in the implicit form. Applicable - only for ODEs/DAEs or discrete systems. Instead of `f(u, p, t)` (`f(du, u, p, t)` for the - in-place form) the function is `f(du, u, p, t)` (respectively `f(resid, du, u, p, t)`). -- `override_discrete`: Whether to assume the system is discrete regardless of - `is_discrete_system(sys)`. -- `scalar`: Whether to generate a single-out-of-place function that returns a scalar for - the only equation in the system. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_rhs(sys::System; implicit_dae = false, - scalar = false, expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, override_discrete = false, - kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - eqs = equations(sys) - obs = observed(sys) - u = dvs - p = reorder_parameters(sys, ps) - t = get_iv(sys) - ddvs = nothing - extra_assignments = Assignment[] - - # used for DAEProblem and ImplicitDiscreteProblem - if implicit_dae - if override_discrete || is_discrete_system(sys) - # ImplicitDiscrete case - D = Shift(t, 1) - rhss = map(eqs) do eq - # Algebraic equations get shifted forward 1, to match with differential - # equations - _iszero(eq.lhs) ? distribute_shift(D(eq.rhs)) : (eq.rhs - eq.lhs) - end - # Handle observables in algebraic equations, since they are shifted - shifted_obs = Equation[distribute_shift(D(eq)) for eq in obs] - obsidxs = observed_equations_used_by(sys, rhss; obs = shifted_obs) - ddvs = map(D, dvs) - - append!(extra_assignments, - [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) - for i in obsidxs]) - else - D = Differential(t) - ddvs = map(D, dvs) - rhss = [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] - end - else - if !override_discrete && !is_discrete_system(sys) - check_operator_variables(eqs, Differential) - check_lhs(eqs, Differential, Set(dvs)) - end - rhss = [eq.rhs for eq in eqs] - end - - if !isempty(assertions(sys)) - rhss[end] += unwrap(get_assertions_expr(sys)) - end - - # TODO: add an optional check on the ordering of observed equations - if scalar - rhss = only(rhss) - u = only(u) - end - - args = (u, p...) - p_start = 2 - if t !== nothing - args = (args..., t) - end - if implicit_dae - args = (ddvs, args...) - p_start += 1 - end - - res = build_function_wrapper(sys, rhss, args...; p_start, extra_assignments, - expression = Val{true}, expression_module = eval_module, kwargs...) - nargs = length(args) - length(p) + 1 - if is_dde(sys) - p_start += 1 - nargs += 1 - end - return maybe_compile_function( - expression, wrap_gfw, (p_start, nargs, is_split(sys)), - res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Generate the diffusion function for the noise equations of a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_diffusion_function(sys::System; expression = Val{true}, - wrap_gfw = Val{false}, eval_expression = false, - eval_module = @__MODULE__, kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - eqs = get_noise_eqs(sys) - if ndims(eqs) == 2 && size(eqs, 2) == 1 - # scalar noise - eqs = vec(eqs) - end - p = reorder_parameters(sys, ps) - res = build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) - if expression == Val{true} - return res - end - f_oop, f_iip = eval_or_rgf.(res; eval_expression, eval_module) - p_start = 2 - nargs = 3 - if is_dde(sys) - p_start += 1 - nargs += 1 - end - return maybe_compile_function( - expression, wrap_gfw, (p_start, nargs, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Calculate the gradient of the equations of `sys` with respect to the independent variable. -`simplify` is forwarded to `Symbolics.expand_derivatives`. -""" -function calculate_tgrad(sys::System; simplify = false) - # We need to remove explicit time dependence on the unknown because when we - # have `u(t) * t` we want to have the tgrad to be `u(t)` instead of `u'(t) * - # t + u(t)`. - rhs = [detime_dvs(eq.rhs) for eq in full_equations(sys)] - iv = get_iv(sys) - xs = unknowns(sys) - rule = Dict(map((x, xt) -> xt => x, detime_dvs.(xs), xs)) - rhs = substitute.(rhs, Ref(rule)) - tgrad = [expand_derivatives(Differential(iv)(r), simplify) for r in rhs] - reverse_rule = Dict(map((x, xt) -> x => xt, detime_dvs.(xs), xs)) - tgrad = Num.(substitute.(tgrad, Ref(reverse_rule))) - return tgrad -end - -""" - $(TYPEDSIGNATURES) - -Calculate the jacobian of the equations of `sys`. - -# Keyword arguments - -- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. -- `dvs`: The variables with respect to which the jacobian should be computed. -""" -function calculate_jacobian(sys::System; - sparse = false, simplify = false, dvs = unknowns(sys)) - obs = Dict(eq.lhs => eq.rhs for eq in observed(sys)) - rhs = map(eq -> fixpoint_sub(eq.rhs - eq.lhs, obs), equations(sys)) - - if sparse - jac = sparsejacobian(rhs, dvs; simplify) - if get_iv(sys) !== nothing - # Add nonzeros of W as non-structural zeros of the Jacobian - # (to ensure equal results for oop and iip Jacobian) - JIs, JJs, JVs = findnz(jac) - WIs, WJs, _ = findnz(W_sparsity(sys)) - append!(JIs, WIs) # explicitly put all W's indices also in J, - append!(JJs, WJs) # even if it duplicates some indices - append!(JVs, zeros(eltype(JVs), length(WIs))) # add zero - jac = SparseArrays.sparse(JIs, JJs, JVs) # values at duplicate indices are summed; not overwritten - end - else - jac = jacobian(rhs, dvs; simplify) - end - - return jac -end - -""" - $(TYPEDSIGNATURES) - -Generate the jacobian function for the equations of a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). -- `checkbounds`: Whether to check correctness of indices at runtime if `sparse`. - Also forwarded to `build_function_wrapper`. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_jacobian(sys::System; - simplify = false, sparse = false, eval_expression = false, - eval_module = @__MODULE__, expression = Val{true}, wrap_gfw = Val{false}, - checkbounds = false, kwargs...) - dvs = unknowns(sys) - jac = calculate_jacobian(sys; simplify, sparse, dvs) - p = reorder_parameters(sys) - t = get_iv(sys) - if t !== nothing && sparse && checkbounds - wrap_code = assert_jac_length_header(sys) # checking sparse J indices at runtime is expensive for large systems - else - wrap_code = (identity, identity) - end - args = (dvs, p...) - nargs = 2 - if is_time_dependent(sys) - args = (args..., t) - nargs = 3 - end - res = build_function_wrapper(sys, jac, args...; wrap_code, expression = Val{true}, - expression_module = eval_module, checkbounds, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, nargs, is_split(sys)), res; eval_expression, eval_module) -end - -function assert_jac_length_header(sys) - W = W_sparsity(sys) - identity, - function add_header(expr) - Func(expr.args, [], expr.body, - [:(@assert $(SymbolicUtils.Code.toexpr(term(findnz, expr.args[1])))[1:2] == - $(findnz(W)[1:2]))]) - end -end - -""" - $(TYPEDSIGNATURES) - -Generate the tgrad function for the equations of a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`: Forwarded to [`calculate_tgrad`](@ref). - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_tgrad( - sys::System; - simplify = false, eval_expression = false, eval_module = @__MODULE__, - expression = Val{true}, wrap_gfw = Val{false}, kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - tgrad = calculate_tgrad(sys, simplify = simplify) - p = reorder_parameters(sys, ps) - res = build_function_wrapper(sys, tgrad, - dvs, - p..., - get_iv(sys); - expression = Val{true}, - expression_module = eval_module, - kwargs...) - - return maybe_compile_function( - expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Return an array of symbolic hessians corresponding to the equations of the system. - -# Keyword Arguments - -- `sparse`: Controls whether the symbolic hessians are sparse matrices -- `simplify`: Forwarded to `Symbolics.hessian` -""" -function calculate_hessian(sys::System; simplify = false, sparse = false) - rhs = [eq.rhs - eq.lhs for eq in full_equations(sys)] - dvs = unknowns(sys) - if sparse - hess = map(rhs) do expr - Symbolics.sparsehessian(expr, dvs; simplify)::AbstractSparseArray - end - else - hess = [Symbolics.hessian(expr, dvs; simplify) for expr in rhs] - end - - return hess -end - -""" - $(TYPEDSIGNATURES) - -Return the sparsity pattern of the hessian of the equations of `sys`. -""" -function Symbolics.hessian_sparsity(sys::System) - hess = calculate_hessian(sys; sparse = true) - return similar.(hess, Float64) -end - -const W_GAMMA = only(@variables ˍ₋gamma) - -""" - $(TYPEDSIGNATURES) - -Generate the `W = γ * M + J` function for the equations of a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). -- `checkbounds`: Whether to check correctness of indices at runtime if `sparse`. - Also forwarded to `build_function_wrapper`. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_W(sys::System; - simplify = false, sparse = false, expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, checkbounds = false, kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - M = calculate_massmatrix(sys; simplify) - if sparse - M = SparseArrays.sparse(M) - end - J = calculate_jacobian(sys; simplify, sparse, dvs) - W = W_GAMMA * M + J - t = get_iv(sys) - if t !== nothing && sparse && checkbounds - wrap_code = assert_jac_length_header(sys) - else - wrap_code = (identity, identity) - end - - p = reorder_parameters(sys, ps) - res = build_function_wrapper(sys, W, dvs, p..., W_GAMMA, t; wrap_code, - p_end = 1 + length(p), checkbounds, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, 4, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Generate the DAE jacobian `γ * J′ + J` function for the equations of a [`System`](@ref). -`J′` is the jacobian of the equations with respect to the `du` vector, and `J` is the -standard jacobian. - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_jacobian`](@ref). - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_dae_jacobian(sys::System; simplify = false, sparse = false, - expression = Val{true}, wrap_gfw = Val{false}, eval_expression = false, - eval_module = @__MODULE__, kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - jac_u = calculate_jacobian(sys; simplify = simplify, sparse = sparse) - t = get_iv(sys) - derivatives = Differential(t).(unknowns(sys)) - jac_du = calculate_jacobian(sys; simplify = simplify, sparse = sparse, - dvs = derivatives) - dvs = unknowns(sys) - jac = W_GAMMA * jac_du + jac_u - p = reorder_parameters(sys, ps) - res = build_function_wrapper(sys, jac, derivatives, dvs, p..., W_GAMMA, t; - p_start = 3, p_end = 2 + length(p), kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (3, 5, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Generate the history function for a [`System`](@ref), given a symbolic representation of -the `u0` vector prior to the initial time. - -# Keyword Arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_history(sys::System, u0; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, kwargs...) - p = reorder_parameters(sys) - res = build_function_wrapper(sys, u0, p..., get_iv(sys); expression = Val{true}, - expression_module = eval_module, p_start = 1, p_end = length(p), - similarto = typeof(u0), wrap_delays = false, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (1, 2, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Calculate the mass matrix of `sys`. `simplify` controls whether `Symbolics.simplify` is -applied to the symbolic mass matrix. Returns a `Diagonal` or `LinearAlgebra.I` wherever -possible. -""" -function calculate_massmatrix(sys::System; simplify = false) - eqs = [eq for eq in equations(sys)] - M = zeros(length(eqs), length(eqs)) - for (i, eq) in enumerate(eqs) - if iscall(eq.lhs) && operation(eq.lhs) isa Differential - st = var_from_nested_derivative(eq.lhs)[1] - j = variable_index(sys, st) - M[i, j] = 1 - else - _iszero(eq.lhs) || - error("Only semi-explicit constant mass matrices are currently supported. Faulty equation: $eq.") - end - end - M = simplify ? Symbolics.simplify.(M) : M - if isdiag(M) - M = Diagonal(M) - end - # M should only contain concrete numbers - M == I ? I : M -end - -""" - $(TYPEDSIGNATURES) - -Return a modified version of mass matrix `M` which is of a similar type to `u0`. `sparse` -controls whether the mass matrix should be a sparse matrix. -""" -function concrete_massmatrix(M; sparse = false, u0 = nothing) - if sparse && !(u0 === nothing || M === I) - SparseArrays.sparse(M) - elseif u0 === nothing || M === I - M - elseif M isa Diagonal - Diagonal(ArrayInterface.restructure(u0, diag(M))) - else - ArrayInterface.restructure(u0 .* u0', M) - end -end - -""" - $(TYPEDSIGNATURES) - -Return the sparsity pattern of the jacobian of `sys` as a matrix. -""" -function jacobian_sparsity(sys::System) - sparsity = torn_system_jacobian_sparsity(sys) - sparsity === nothing || return sparsity - - Symbolics.jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in unknowns(sys)]) -end - -""" - $(TYPEDSIGNATURES) - -Return the sparsity pattern of the DAE jacobian of `sys` as a matrix. - -See also: [`generate_dae_jacobian`](@ref). -""" -function jacobian_dae_sparsity(sys::System) - J1 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in unknowns(sys)]) - derivatives = Differential(get_iv(sys)).(unknowns(sys)) - J2 = jacobian_sparsity([eq.rhs for eq in full_equations(sys)], - [dv for dv in derivatives]) - J1 + J2 -end - -""" - $(TYPEDSIGNATURES) - -Return the sparsity pattern of the `W` matrix of `sys`. - -See also: [`generate_W`](@ref). -""" -function W_sparsity(sys::System) - jac_sparsity = jacobian_sparsity(sys) - (n, n) = size(jac_sparsity) - M = calculate_massmatrix(sys) - M_sparsity = M isa UniformScaling ? sparse(I(n)) : - SparseMatrixCSC{Bool, Int64}((!iszero).(M)) - jac_sparsity .| M_sparsity -end - -""" - $(TYPEDSIGNATURES) - -Return the matrix to use as the jacobian prototype given the W-sparsity matrix of the -system. This is not the same as the jacobian sparsity pattern. - -# Keyword arguments - -- `u0`: The `u0` vector for the problem. -- `sparse`: The prototype is `nothing` for non-sparse matrices. -""" -function calculate_W_prototype(W_sparsity; u0 = nothing, sparse = false) - sparse || return nothing - uElType = u0 === nothing ? Float64 : eltype(u0) - return similar(W_sparsity, uElType) -end - -function isautonomous(sys::System) - tgrad = calculate_tgrad(sys; simplify = true) - all(iszero, tgrad) -end - -function get_bv_solution_symbol(ns) - only(@variables BV_SOLUTION(..)[1:ns]) -end - -function get_constraint_unknown_subs!(subs::Dict, cons::Vector, stidxmap::Dict, iv, sol) - vs = vars(cons) - for v in vs - iscall(v) || continue - op = operation(v) - args = arguments(v) - issym(op) && length(args) == 1 || continue - newv = op(iv) - haskey(stidxmap, newv) || continue - subs[v] = sol(args[1])[stidxmap[newv]] - end -end - -""" - $(TYPEDSIGNATURES) - -Generate the boundary condition function for a [`System`](@ref) given the state vector `u0`, -the indexes of `u0` to consider as hard constraints `u0_idxs` and the initial time `t0`. - -# Keyword Arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_boundary_conditions(sys::System, u0, u0_idxs, t0; expression = Val{true}, - wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, - kwargs...) - iv = get_iv(sys) - sts = unknowns(sys) - ps = parameters(sys) - np = length(ps) - ns = length(sts) - stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) - - # sol = get_bv_solution_symbol(ns) - - cons = [con.lhs - con.rhs for con in constraints(sys)] - # conssubs = Dict() - # get_constraint_unknown_subs!(conssubs, cons, stidxmap, iv, sol) - # cons = map(x -> fast_substitute(x, conssubs), cons) - - init_conds = Any[] - for i in u0_idxs - expr = BVP_SOLUTION(t0)[i] - u0[i] - push!(init_conds, expr) - end - - exprs = vcat(init_conds, cons) - _p = reorder_parameters(sys, ps) - - res = build_function_wrapper(sys, exprs, _p..., iv; output_type = Array, - p_start = 1, histfn = (p, t) -> BVP_SOLUTION(t), - histfn_symbolic = BVP_SOLUTION, wrap_delays = true, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Generate the cost function for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_cost(sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, kwargs...) - obj = cost(sys) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - - if is_time_dependent(sys) - wrap_delays = true - p_start = 1 - p_end = length(ps) - args = (ps..., get_iv(sys)) - nargs = 3 - else - wrap_delays = false - p_start = 2 - p_end = length(ps) + 1 - args = (dvs, ps...) - nargs = 2 - end - res = build_function_wrapper( - sys, obj, args...; expression = Val{true}, p_start, p_end, wrap_delays, - histfn = (p, t) -> BVP_SOLUTION(t), histfn_symbolic = BVP_SOLUTION, kwargs...) - if expression == Val{true} - return res - end - f_oop = eval_or_rgf(res; eval_expression, eval_module) - return maybe_compile_function( - expression, wrap_gfw, (2, nargs, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Calculate the gradient of the consolidated cost of `sys` with respect to the unknowns. -`simplify` is forwarded to `Symbolics.gradient`. -""" -function calculate_cost_gradient(sys::System; simplify = false) - obj = cost(sys) - dvs = unknowns(sys) - return Symbolics.gradient(obj, dvs; simplify) -end - -""" - $(TYPEDSIGNATURES) - -Generate the gradient of the cost function with respect to unknowns for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`: Forwarded to [`calculate_cost_gradient`](@ref). - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_cost_gradient( - sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, simplify = false, kwargs...) - obj = cost(sys) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - exprs = calculate_cost_gradient(sys; simplify) - res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Calculate the hessian of the consolidated cost of `sys` with respect to the unknowns. -`simplify` is forwarded to `Symbolics.hessian`. `sparse` controls whether a sparse -matrix is returned. -""" -function calculate_cost_hessian(sys::System; sparse = false, simplify = false) - obj = cost(sys) - dvs = unknowns(sys) - if sparse - return Symbolics.sparsehessian(obj, dvs; simplify)::AbstractSparseArray - else - return Symbolics.hessian(obj, dvs; simplify) - end -end - -""" - $(TYPEDSIGNATURES) - -Return the sparsity pattern for the hessian of the cost function of `sys`. -""" -function cost_hessian_sparsity(sys::System) - return similar(calculate_cost_hessian(sys; sparse = true), Float64) -end - -""" - $(TYPEDSIGNATURES) - -Generate the hessian of the cost function for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_cost_hessian`](@ref). -- `return_sparsity`: Whether to also return the sparsity pattern of the hessian as the - second return value. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_cost_hessian( - sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, simplify = false, - sparse = false, return_sparsity = false, kwargs...) - obj = cost(sys) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - sparsity = nothing - exprs = calculate_cost_hessian(sys; sparse, simplify) - if sparse - sparsity = similar(exprs, Float64) - end - res = build_function_wrapper(sys, exprs, dvs, ps...; expression = Val{true}, kwargs...) - fn = maybe_compile_function( - expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) - - return return_sparsity ? (fn, sparsity) : fn -end - -function canonical_constraints(sys::System) - return map(constraints(sys)) do cstr - Symbolics.canonical_form(cstr).lhs - end -end - -""" - $(TYPEDSIGNATURES) - -Generate the constraint function for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_cons(sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, kwargs...) - cons = canonical_constraints(sys) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - res = build_function_wrapper(sys, cons, dvs, ps...; expression = Val{true}, kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Return the jacobian of the constraints of `sys` with respect to unknowns. - -# Keyword arguments - -- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. -- `return_sparsity`: Whether to also return the sparsity pattern of the jacobian. -""" -function calculate_constraint_jacobian(sys::System; simplify = false, sparse = false, - return_sparsity = false) - cons = canonical_constraints(sys) - dvs = unknowns(sys) - sparsity = nothing - if sparse - jac = Symbolics.sparsejacobian(cons, dvs; simplify)::AbstractSparseArray - sparsity = similar(jac, Float64) - else - jac = Symbolics.jacobian(cons, dvs; simplify) - end - return return_sparsity ? (jac, sparsity) : jac -end - -""" - $(TYPEDSIGNATURES) - -Generate the jacobian of the constraint function for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_constraint_jacobian`](@ref). -- `return_sparsity`: Whether to also return the sparsity pattern of the jacobian as the - second return value. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_constraint_jacobian( - sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, return_sparsity = false, - simplify = false, sparse = false, kwargs...) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - jac, - sparsity = calculate_constraint_jacobian( - sys; simplify, sparse, return_sparsity = true) - res = build_function_wrapper(sys, jac, dvs, ps...; expression = Val{true}, kwargs...) - fn = maybe_compile_function( - expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) - return return_sparsity ? (fn, sparsity) : fn -end - -""" - $(TYPEDSIGNATURES) - -Return the hessian of the constraints of `sys` with respect to unknowns. - -# Keyword arguments - -- `simplify`, `sparse`: Forwarded to `Symbolics.hessian`. -- `return_sparsity`: Whether to also return the sparsity pattern of the hessian. -""" -function calculate_constraint_hessian( - sys::System; simplify = false, sparse = false, return_sparsity = false) - cons = canonical_constraints(sys) - dvs = unknowns(sys) - sparsity = nothing - if sparse - hess = map(cons) do cstr - Symbolics.sparsehessian(cstr, dvs; simplify)::AbstractSparseArray - end - sparsity = similar.(hess, Float64) - else - hess = [Symbolics.hessian(cstr, dvs; simplify) for cstr in cons] - end - return return_sparsity ? (hess, sparsity) : hess -end - -""" - $(TYPEDSIGNATURES) - -Generate the hessian of the constraint function for a [`System`](@ref). - -# Keyword Arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_constraint_hessian`](@ref). -- `return_sparsity`: Whether to also return the sparsity pattern of the hessian as the - second return value. - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_constraint_hessian( - sys::System; expression = Val{true}, wrap_gfw = Val{false}, - eval_expression = false, eval_module = @__MODULE__, return_sparsity = false, - simplify = false, sparse = false, kwargs...) - dvs = unknowns(sys) - ps = reorder_parameters(sys) - hess, - sparsity = calculate_constraint_hessian( - sys; simplify, sparse, return_sparsity = true) - res = build_function_wrapper(sys, hess, dvs, ps...; expression = Val{true}, kwargs...) - fn = maybe_compile_function( - expression, wrap_gfw, (2, 2, is_split(sys)), res; eval_expression, eval_module) - return return_sparsity ? (fn, sparsity) : fn -end - -""" - $(TYPEDSIGNATURES) - -Calculate the jacobian of the equations of `sys` with respect to the inputs. - -# Keyword arguments - -- `simplify`, `sparse`: Forwarded to `Symbolics.jacobian`. -""" -function calculate_control_jacobian(sys::AbstractSystem; - sparse = false, simplify = false) - rhs = [eq.rhs for eq in full_equations(sys)] - ctrls = unbound_inputs(sys) - - if sparse - jac = sparsejacobian(rhs, ctrls, simplify = simplify) - else - jac = jacobian(rhs, ctrls, simplify = simplify) - end - - return jac -end - -""" - $(TYPEDSIGNATURES) - -Generate the jacobian function of the equations of `sys` with respect to the inputs. - -# Keyword arguments - -$GENERATE_X_KWARGS -- `simplify`, `sparse`: Forwarded to [`calculate_constraint_hessian`](@ref). - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_control_jacobian(sys::AbstractSystem; - expression = Val{true}, wrap_gfw = Val{false}, eval_expression = false, - eval_module = @__MODULE__, simplify = false, sparse = false, kwargs...) - dvs = unknowns(sys) - ps = parameters(sys; initial_parameters = true) - jac = calculate_control_jacobian(sys; simplify = simplify, sparse = sparse) - p = reorder_parameters(sys, ps) - res = build_function_wrapper(sys, jac, dvs, p..., get_iv(sys); kwargs...) - return maybe_compile_function( - expression, wrap_gfw, (2, 3, is_split(sys)), res; eval_expression, eval_module) -end - -function generate_rate_function(js::System, rate) - p = reorder_parameters(js) - build_function_wrapper(js, rate, unknowns(js), p..., - get_iv(js), - expression = Val{true}) -end - -function generate_affect_function(js::System, affect; kwargs...) - compile_equational_affect(affect, js; checkvars = false, kwargs...) -end - -function assemble_vrj( - js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) - rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) - outputvars = (value(affect.lhs) for affect in vrj.affect!) - outputidxs = [unknowntoid[var] for var in outputvars] - affect = generate_affect_function(js, vrj.affect!; eval_expression, eval_module) - VariableRateJump(rate, affect; save_positions = vrj.save_positions) -end - -function assemble_crj( - js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) - rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) - outputvars = (value(affect.lhs) for affect in crj.affect!) - outputidxs = [unknowntoid[var] for var in outputvars] - affect = generate_affect_function(js, crj.affect!; eval_expression, eval_module) - ConstantRateJump(rate, affect) -end - -# assemble a numeric MassActionJump from a MT symbolics MassActionJumps -function assemble_maj(majv::Vector{U}, unknowntoid, pmapper) where {U <: MassActionJump} - rs = [numericrstoich(maj.reactant_stoch, unknowntoid) for maj in majv] - ns = [numericnstoich(maj.net_stoch, unknowntoid) for maj in majv] - MassActionJump(rs, ns; param_mapper = pmapper, nocopy = true) -end - -function numericrstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} - rs = Vector{Pair{Int, W}}() - for (wspec, stoich) in mtrs - spec = value(wspec) - if !iscall(spec) && _iszero(spec) - push!(rs, 0 => stoich) - else - push!(rs, unknowntoid[spec] => stoich) - end - end - sort!(rs) - rs -end - -function numericnstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} - ns = Vector{Pair{Int, W}}() - for (wspec, stoich) in mtrs - spec = value(wspec) - !iscall(spec) && _iszero(spec) && - error("Net stoichiometry can not have a species labelled 0.") - push!(ns, unknowntoid[spec] => stoich) - end - sort!(ns) -end - -""" - build_explicit_observed_function(sys, ts; kwargs...) -> Function(s) - -Generates a function that computes the observed value(s) `ts` in the system `sys`, while making the assumption that there are no cycles in the equations. - -## Arguments -- `sys`: The system for which to generate the function -- `ts`: The symbolic observed values whose value should be computed - -## Keywords -- `return_inplace = false`: If true and the observed value is a vector, then return both the in place and out of place methods. -- `expression = false`: Generates a Julia `Expr`` computing the observed value if `expression` is true -- `eval_expression = false`: If true and `expression = false`, evaluates the returned function in the module `eval_module` -- `output_type = Array` the type of the array generated by a out-of-place vector-valued function -- `param_only = false` if true, only allow the generated function to access system parameters -- `inputs = nothing` additinoal symbolic variables that should be provided to the generated function -- `disturbance_inputs = nothing` symbolic variables representing unknown disturbance inputs (removed from parameters, not added as function arguments) -- `known_disturbance_inputs = nothing` symbolic variables representing known disturbance inputs (removed from parameters, added as function arguments) -- `checkbounds = true` checks bounds if true when destructuring parameters -- `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. -- `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. -- `mkarray`: only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. -- `cse = true`: Whether to use Common Subexpression Elimination (CSE) to generate a more efficient function. -- `wrap_delays = is_dde(sys)`: Whether to add an argument for the history function and use - it to calculate all delayed variables. - -## Returns - -The return value will be either: -* a single function `f_oop` if the input is a scalar or if the input is a Vector but `return_inplace` is false -* the out of place and in-place functions `(f_ip, f_oop)` if `return_inplace` is true and the input is a `Vector` - -The function(s) `f_oop` (and potentially `f_ip`) will be: -* `RuntimeGeneratedFunction`s by default, -* A Julia `Expr` if `expression` is true, -* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true and `expression` is false. - -The signatures will be of the form `g(...)` with arguments: - -- `output` for in-place functions -- `unknowns` if `param_only` is `false` -- `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` -- `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted -- `t` if the system is time-dependent; for example systems of nonlinear equations will not have `t` -- `known_disturbance_inputs` if provided; these are disturbance inputs that are known and provided as arguments - -For example, a function `g(op, unknowns, p..., inputs, t, known_disturbances)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, -an array of inputs `inputs` is given, `known_disturbance_inputs` is provided, and `param_only` is false for a time-dependent system. -""" -function build_explicit_observed_function(sys, ts; - inputs = nothing, - disturbance_inputs = nothing, - known_disturbance_inputs = nothing, - disturbance_argument = false, - expression = false, - eval_expression = false, - eval_module = @__MODULE__, - output_type = Array, - checkbounds = true, - ps = parameters(sys; initial_parameters = true), - return_inplace = false, - param_only = false, - op = Operator, - throw = true, - cse = true, - mkarray = nothing, - wrap_delays = is_dde(sys)) - # TODO: cleanup - is_tuple = ts isa Tuple - if is_tuple - ts = collect(ts) - output_type = Tuple - end - - allsyms = all_symbols(sys) - if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray - ts = map(x -> symbol_to_symbolic(sys, x; allsyms), ts) - else - ts = symbol_to_symbolic(sys, ts; allsyms) - end - - vs = ModelingToolkit.vars(ts; op) - namespace_subs = Dict() - ns_map = Dict{Any, Any}(renamespace(sys, eq.lhs) => eq.lhs for eq in observed(sys)) - for sym in unknowns(sys) - ns_map[renamespace(sys, sym)] = sym - if iscall(sym) && operation(sym) === getindex - ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] - end - end - for sym in full_parameters(sys) - ns_map[renamespace(sys, sym)] = sym - if iscall(sym) && operation(sym) === getindex - ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] - end - end - allsyms = Set(all_symbols(sys)) - iv = has_iv(sys) ? get_iv(sys) : nothing - for var in vs - var = unwrap(var) - newvar = get(ns_map, var, nothing) - if newvar !== nothing - namespace_subs[var] = newvar - var = newvar - end - if throw && !var_in_varlist(var, allsyms, iv) - Base.throw(ArgumentError("Symbol $var is not present in the system.")) - end - end - ts = fast_substitute(ts, namespace_subs) - - obsfilter = if param_only - if is_split(sys) - let ic = get_index_cache(sys) - eq -> !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) - end - else - Returns(false) - end - else - Returns(true) - end - dvs = if param_only - () - else - (unknowns(sys),) - end - if inputs === nothing - inputs = () - else - ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list - inputs = (inputs,) - end - # Handle backward compatibility for disturbance_argument - if disturbance_argument - Base.depwarn("The `disturbance_argument` keyword argument is deprecated. Use `known_disturbance_inputs` instead. " * - "For `disturbance_argument=true`, pass `known_disturbance_inputs=disturbance_inputs, disturbance_inputs=nothing`. " * - "For `disturbance_argument=false`, use `disturbance_inputs` as before.", - :build_explicit_observed_function) - if known_disturbance_inputs !== nothing - error("Cannot specify both `disturbance_argument=true` and `known_disturbance_inputs`") - end - known_disturbance_inputs = disturbance_inputs - disturbance_inputs = nothing - end - - # Remove disturbance inputs from parameters (both known and unknown) - if disturbance_inputs !== nothing - ps = setdiff(ps, disturbance_inputs) - end - if known_disturbance_inputs !== nothing - ps = setdiff(ps, known_disturbance_inputs) - known_disturbance_inputs = (known_disturbance_inputs,) - else - known_disturbance_inputs = () - end - ps = reorder_parameters(sys, ps) - iv = if is_time_dependent(sys) - (get_iv(sys),) - else - () - end - args = (dvs..., inputs..., ps..., iv..., known_disturbance_inputs...) - p_start = length(dvs) + length(inputs) + 1 - p_end = length(dvs) + length(inputs) + length(ps) - fns = build_function_wrapper( - sys, ts, args...; p_start, p_end, filter_observed = obsfilter, - output_type, mkarray, try_namespaced = true, expression = Val{true}, cse, - wrap_delays) - if fns isa Tuple - if expression - return return_inplace ? fns : fns[1] - end - oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) - f = GeneratedFunctionWrapper{( - p_start + wrap_delays, length(args) - length(ps) + 1 + wrap_delays, is_split(sys))}( - oop, iip) - return return_inplace ? (f, f) : f - else - if expression - return fns - end - f = eval_or_rgf(fns; eval_expression, eval_module) - f = GeneratedFunctionWrapper{( - p_start + wrap_delays, length(args) - length(ps) + 1 + wrap_delays, is_split(sys))}( - f, nothing) - return f - end -end - -""" - $(TYPEDSIGNATURES) - -Return matrix `A` and vector `b` such that the system `sys` can be represented as -`A * x = b` where `x` is `unknowns(sys)`. Errors if the system is not affine. - -# Keyword arguments - -- `sparse`: return a sparse `A`. -""" -function calculate_A_b(sys::System; sparse = false) - rhss = [eq.rhs for eq in full_equations(sys)] - dvs = unknowns(sys) - - A = Matrix{Any}(undef, length(rhss), length(dvs)) - b = Vector{Any}(undef, length(rhss)) - for (i, rhs) in enumerate(rhss) - # mtkcompile makes this `0 ~ rhs` which typically ends up giving - # unknowns negative coefficients. If given the equations `A * x ~ b` - # it will simplify to `0 ~ b - A * x`. Thus this negation usually leads - # to more comprehensible user API. - resid = -rhs - for (j, var) in enumerate(dvs) - p, q, islinear = Symbolics.linear_expansion(resid, var) - if !islinear - throw(ArgumentError("System is not linear. Equation $((0 ~ rhs)) is not linear in unknown $var.")) - end - A[i, j] = p - resid = q +function MTKBase.torn_system_jacobian_sparsity(sys) + state = get_tearing_state(sys) + state isa TearingState || return nothing + @unpack structure = state + @unpack graph, var_to_diff = structure + + neqs = nsrcs(graph) + nsts = ndsts(graph) + states_idxs = findall(!Base.Fix1(isdervar, structure), 1:nsts) + var2idx = uneven_invmap(nsts, states_idxs) + I = Int[] + J = Int[] + for ieq in 1:neqs + for ivar in 𝑠neighbors(graph, ieq) + nivar = get(var2idx, ivar, 0) + nivar == 0 && continue + push!(I, ieq) + push!(J, nivar) end - # negate beucause `resid` is the residual on the LHS - b[i] = -resid - end - - @assert all(Base.Fix1(isassigned, A), eachindex(A)) - @assert all(Base.Fix1(isassigned, A), eachindex(b)) - - if sparse - A = SparseArrays.sparse(A) end - return A, b -end - -""" - $(TYPEDSIGNATURES) - -Given a system `sys` and the `A` from [`calculate_A_b`](@ref) generate the function that -updates `A` given the parameter object. - -# Keyword arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_update_A(sys::System, A::AbstractMatrix; expression = Val{true}, - wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, cachesyms = (), kwargs...) - ps = reorder_parameters(sys) - - res = build_function_wrapper( - sys, A, ps..., cachesyms...; p_start = 1, expression = Val{true}, - similarto = typeof(A), kwargs...) - return maybe_compile_function(expression, wrap_gfw, (1, 1, is_split(sys)), res; - eval_expression, eval_module) -end - -""" - $(TYPEDSIGNATURES) - -Given a system `sys` and the `b` from [`calculate_A_b`](@ref) generate the function that -updates `b` given the parameter object. - -# Keyword arguments - -$GENERATE_X_KWARGS - -All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). -""" -function generate_update_b(sys::System, b::AbstractVector; expression = Val{true}, - wrap_gfw = Val{false}, eval_expression = false, eval_module = @__MODULE__, cachesyms = (), kwargs...) - ps = reorder_parameters(sys) - - res = build_function_wrapper( - sys, b, ps..., cachesyms...; p_start = 1, expression = Val{true}, - similarto = typeof(b), kwargs...) - return maybe_compile_function(expression, wrap_gfw, (1, 1, is_split(sys)), res; - eval_expression, eval_module) + return sparse(I, J, true, neqs, neqs) end """ @@ -1421,12 +193,12 @@ extra parmameters added by [`add_semiquadratic_parameters`](@ref). ## Keyword Arguments $SEMILINEAR_A_B_C_KWARGS -$GENERATE_X_KWARGS +$(MTKBase.GENERATE_X_KWARGS) All other keyword arguments are forwarded to [`build_function_wrapper`](@ref). $SEMILINEAR_A_B_C_CONSTRAINT -$EXPERIMENTAL_WARNING +$(MTKBase.EXPERIMENTAL_WARNING) """ function generate_semiquadratic_functions(sys::System, A, B, C; stiff_linear = true, stiff_quadratic = false, stiff_nonlinear = false, expression = Val{true}, wrap_gfw = Val{false}, @@ -1456,6 +228,7 @@ function generate_semiquadratic_functions(sys::System, A, B, C; stiff_linear = t iip_x = generated_argument_name(2) oop_x = generated_argument_name(1) + shape = SU.Unknown(-1) ## iip f1_iip_ir = Assignment[] f2_iip_ir = Assignment[] @@ -1470,19 +243,24 @@ function generate_semiquadratic_functions(sys::System, A, B, C; stiff_linear = t B_vals = map(eachindex(eqs)) do i B[i] === nothing && return nothing tmp_buf = term( - PreallocationTools.get_tmp, diffcache_par, Symbolics.DEFAULT_OUTSYM) - tmp_buf = term(view, tmp_buf, 1:length(dvs)) + PreallocationTools.get_tmp, diffcache_par, Symbolics.DEFAULT_OUTSYM; + type = Vector{Real}, shape) + tmp_buf = term(view, tmp_buf, 1:length(dvs); type = Vector{Real}, shape) - result = term(*, term(transpose, iip_x), :__tmp_B_1) + result = term(*, term(transpose, iip_x; type = Matrix{Real}, shape), + :__tmp_B_1; type = Vector{Real}, shape) # if both write to the same buffer, don't overwrite if stiff_quadratic == stiff_nonlinear && C !== nothing - result = term(+, result, term(getindex, Symbolics.DEFAULT_OUTSYM, i)) + result = term(+, result, term(getindex, Symbolics.DEFAULT_OUTSYM, i; + type = Real, shape); + type = Real, shape) end intermediates = [ Assignment(:__tmp_B_buffer, tmp_buf), Assignment(:__tmp_B_1, term(mul!, :__tmp_B_buffer, - term(UpperTriangular, quadratic_forms[i]), iip_x)) + term(UpperTriangular, quadratic_forms[i]; type = Matrix{Real}, shape), + iip_x; type = Any, shape)) ] return AtIndex(i, Let(intermediates, result)) end @@ -1497,27 +275,38 @@ function generate_semiquadratic_functions(sys::System, A, B, C; stiff_linear = t push!(A_ir, Assignment(:__tmp_A, term(mul!, Symbolics.DEFAULT_OUTSYM, - linear_matrix_param, iip_x, true, retain_old))) + linear_matrix_param, iip_x, true, retain_old; type = Any, shape))) end ## oop f1_terms = [] f2_terms = [] if A !== nothing - push!(stiff_linear ? f1_terms : f2_terms, term(*, linear_matrix_param, oop_x)) + push!(stiff_linear ? f1_terms : f2_terms, + term(*, linear_matrix_param, oop_x; type = Vector{Real}, shape)) end if B !== nothing B_elems = map(eachindex(eqs)) do i B[i] === nothing && return 0 term( - *, term(transpose, oop_x), term(UpperTriangular, quadratic_forms[i]), oop_x) + *, term(transpose, oop_x; type = Matrix{Real}, shape), + term(UpperTriangular, quadratic_forms[i]; type = Matrix{Real}, shape), + oop_x; type = Matrix{Real}, shape) end push!(stiff_quadratic ? f1_terms : f2_terms, MakeArray(B_elems, oop_x)) end if C !== nothing push!(stiff_nonlinear ? f1_terms : f2_terms, MakeArray(C, oop_x)) end - f1_expr = length(f1_terms) == 1 ? only(f1_terms) : term(+, f1_terms...) - f2_expr = length(f2_terms) == 1 ? only(f2_terms) : term(+, f2_terms...) + f1_expr = if length(f1_terms) == 1 + only(f1_terms) + else + term(+, f1_terms...; type = Vector{Real}, shape) + end + f2_expr = if length(f2_terms) == 1 + only(f2_terms) + else + term(+, f2_terms...; type = Vector{Real}, shape) + end f1_iip = build_function_wrapper( sys, nothing, Symbolics.DEFAULT_OUTSYM, dvs, ps..., iv; p_start = 3, @@ -1527,8 +316,14 @@ function generate_semiquadratic_functions(sys::System, A, B, C; stiff_linear = t extra_assignments = f2_iip_ir, expression = Val{true}, kwargs...) f1_oop = build_function_wrapper( sys, f1_expr, dvs, ps..., iv; expression = Val{true}, kwargs...) + if f1_oop isa NTuple{2, Expr} + f1_oop = f1_oop[1] + end f2_oop = build_function_wrapper( sys, f2_expr, dvs, ps..., iv; expression = Val{true}, kwargs...) + if f2_oop isa NTuple{2, Expr} + f2_oop = f2_oop[1] + end f1 = maybe_compile_function(expression, wrap_gfw, (2, 3, is_split(sys)), (f1_oop, f1_iip); eval_expression, eval_module) @@ -1718,3 +513,4 @@ function get_semiquadratic_W_sparsity( SparseMatrixCSC{Bool, Int64}((!iszero).(mm)) return (!_iszero).(jac) .| M_sparsity end + diff --git a/src/systems/if_lifting.jl b/src/systems/if_lifting.jl index aba9dbdcbf..a21524de8a 100644 --- a/src/systems/if_lifting.jl +++ b/src/systems/if_lifting.jl @@ -31,7 +31,7 @@ Given a symbolic condition `expr` and the condition `dep` it depends on, update mapping in `cw` and generate a new discrete variable if necessary. """ function new_cond_sym(cw::CondRewriter, expr, dep) - if !iscall(expr) || operation(expr) != Base.:(<) || !iszero(arguments(expr)[2]) + if !iscall(expr) || operation(expr) != Base.:(<) || !SU._iszero(arguments(expr)[2]) throw(ArgumentError("`expr` passed to `new_cond_sym` must be of the form `f(args...) < 0`. Got $expr.")) end # check if the same expression exists in the mapping @@ -47,7 +47,7 @@ function new_cond_sym(cw::CondRewriter, expr, dep) cvar = gensym("cond") st = symtype(expr) iv = cw.iv - cv = unwrap(first(@parameters $(cvar)(iv)::st = true)) # TODO: real init + cv = unwrap(first(@discretes $(cvar)(iv)::st = true)) # TODO: real init cw.conditions[cv] = (dependency = dep, expression = expr) return cv end @@ -76,18 +76,20 @@ and family. """ function (cw::CondRewriter)(expr, dep) # single variable, trivial case - if issym(expr) || iscall(expr) && issym(operation(expr)) - return (expr, expr, !expr) - # literal boolean or integer - elseif expr isa Bool - return (expr, expr, !expr) - elseif expr isa Int - return (expr, true, true) - # other singleton symbolic variables - elseif !iscall(expr) - @warn "Automatic conversion of if statements to events requires use of a limited conditional grammar; see the documentation. Skipping due to $expr" - return (expr, true, true) # error case => conservative assumption is that both true and false have to be evaluated - elseif operation(expr) == Base.:(|) # OR of two conditions + Moshi.Match.@match expr begin + BSImpl.Sym(;) => return (expr, expr, !expr) + BSImpl.Term(; f) && if f isa SymbolicT && !SU.is_function_symbolic(f) end => begin + return (expr, expr, !expr) + end + BSImpl.Const(; val) && if val isa Bool end => return (expr, expr, !expr) + BSImpl.Const(; val) && if val isa Int end => return (expr, COMMON_TRUE, COMMON_TRUE) + if !iscall(expr) end => begin + @warn "Automatic conversion of if statements to events requires use of a limited conditional grammar; see the documentation. Skipping due to $expr" + return (expr, COMMON_TRUE, COMMON_TRUE) # error case => conservative assumption is that both true and false have to be evaluated + end + _ => nothing + end + if operation(expr) == Base.:(|) # OR of two conditions a, b = arguments(expr) (rw_conda, truea, falsea) = cw(a, dep) # only evaluate second if first is false @@ -118,7 +120,7 @@ function (cw::CondRewriter)(expr, dep) (rw, ctrue, cfalse) = cw(a, dep) return (!rw, cfalse, ctrue) elseif operation(expr) == Base.:(<) - if !isequal(arguments(expr)[2], 0) + if !SU._iszero(arguments(expr)[2]) throw(ArgumentError("Expected comparison to be written as `f(args...) < 0`. Found $expr.")) end @@ -194,7 +196,7 @@ function (v::VarsUsedInCondition)(expr) args = arguments(expr) if op == ifelse cond, branch_a, branch_b = arguments(expr) - vars!(v.vars, cond) + SU.search_variables!(v.vars, cond) v(branch_a) v(branch_b) end @@ -210,7 +212,7 @@ in the expression, `Differential(iv)` is in the expression, or a dependent varia as `@variables x(iv)` is in the expression. """ function expression_is_time_dependent(expr, iv) - any(vars(expr)) do sym + any(SU.search_variables(expr)) do sym sym = unwrap(sym) isequal(sym, iv) && return true iscall(sym) || return false @@ -461,12 +463,12 @@ function IfLifting(sys::System) obs = copy(observed(sys)) # get variables used by `eqs` - syms = vars(eqs) + syms = SU.search_variables(eqs) # get observed equations used by `eqs` obs_idxs = observed_equations_used_by(sys, eqs; involved_vars = syms) # and the variables used in those equations for i in obs_idxs - vars!(syms, obs[i]) + SU.search_variables!(syms, obs[i]) end # get all integral variables used in conditions @@ -529,7 +531,7 @@ function IfLifting(sys::System) new_cond_vars_graph = observed_dependency_graph(new_cond_dep_eqs) new_callbacks = continuous_events(sys) - new_defaults = defaults(sys) + new_initial_conditions = copy(initial_conditions(sys)) new_ps = Vector{SymbolicParam}(parameters(sys)) for var in new_cond_vars @@ -541,11 +543,11 @@ function IfLifting(sys::System) initialize = up_affect, rootfind = SciMLBase.RightRootFind) push!(new_callbacks, cb) - new_defaults[var] = getdefault(var) + new_initial_conditions[var] = getdefault(var) push!(new_ps, var) end - @set! sys.defaults = new_defaults + @set! sys.initial_conditions = new_initial_conditions @set! sys.eqs = eqs # do not need to topsort because we didn't modify the order @set! sys.observed = obs diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl deleted file mode 100644 index 8cd96a8f97..0000000000 --- a/src/systems/nonlinear/initializesystem.jl +++ /dev/null @@ -1,846 +0,0 @@ -""" - $(TYPEDSIGNATURES) - -Generate the initialization system for `sys`. The initialization system is a system of -nonlinear equations that solve for the full set of initial conditions of `sys` given -specified constraints. - -The initialization system can be of two types: time-dependent and time-independent. -Time-dependent initialization systems solve for the initial values of unknowns as well as -the values of solvable parameters of the system. Time-independent initialization systems -only solve for solvable parameters of the system. - -# Keyword arguments - -- `time_dependent_init`: Whether to create an initialization system for a time-dependent - system. A time-dependent initialization requires a time-dependent `sys`, but a time- - independent initialization can be created regardless. -- `op`: The operating point of user-specified initial conditions of variables in `sys`. -- `initialization_eqs`: Additional initialization equations to use apart from those in - `initialization_equations(sys)`. -- `guesses`: Additional guesses to use apart from those in `guesses(sys)`. -- `default_dd_guess`: Default guess for dummy derivative variables in time-dependent - initialization. -- `algebraic_only`: If `false`, does not use initialization equations (provided via the - keyword or part of the system) to construct initialization. -- `check_defguess`: Whether to error when a variable does not have a default or guess - despite ModelingToolkit expecting it to. -- `name`: The name of the initialization system. - -All other keyword arguments are forwarded to the [`System`](@ref) constructor. -""" -function generate_initializesystem( - sys::AbstractSystem; time_dependent_init = is_time_dependent(sys), kwargs...) - if time_dependent_init - generate_initializesystem_timevarying(sys; kwargs...) - else - generate_initializesystem_timeindependent(sys; kwargs...) - end -end - -""" -$(TYPEDSIGNATURES) - -Generate `System` of nonlinear equations which initializes a problem from specified initial conditions of a time-dependent `AbstractSystem`. -""" -function generate_initializesystem_timevarying(sys::AbstractSystem; - op = Dict(), - initialization_eqs = [], - guesses = Dict(), - default_dd_guess = Bool(0), - algebraic_only = false, - check_units = true, check_defguess = false, - name = nameof(sys), kwargs...) - eqs = equations(sys) - if !(eqs isa Vector{Equation}) - eqs = Equation[x for x in eqs if x isa Equation] - end - trueobs, eqs = unhack_observed(observed(sys), eqs) - # remove any observed equations that directly or indirectly contain - # delayed unknowns - isempty(trueobs) || filter_delay_equations_variables!(sys, trueobs) - vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) - vars_set = Set(vars) # for efficient in-lookup - arrvars = Set() - for var in vars - if iscall(var) && operation(var) === getindex - push!(arrvars, first(arguments(var))) - end - end - - eqs_ics = Equation[] - defs = copy(defaults(sys)) # copy so we don't modify sys.defaults - additional_guesses = anydict(guesses) - guesses = merge(get_guesses(sys), additional_guesses) - idxs_diff = isdiffeq.(eqs) - - # PREPROCESSING - op = anydict(op) - if isempty(op) - op = copy(defs) - end - scalarize_vars_in_varmap!(op, arrvars) - u0map = anydict() - pmap = anydict() - build_operating_point!(sys, op, u0map, pmap, Dict(), unknowns(sys), - parameters(sys; initial_parameters = true)) - for (k, v) in op - if has_parameter_dependency_with_lhs(sys, k) && is_variable_floatingpoint(k) - pmap[k] = v - end - end - initsys_preprocessing!(u0map, defs) - - # 1) Use algebraic equations of system as initialization constraints - idxs_alge = .!idxs_diff - append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations - - eqs_diff = eqs[idxs_diff] - D = Differential(get_iv(sys)) - diffmap = merge( - Dict(eq.lhs => eq.rhs for eq in eqs_diff), - Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) - ) - - if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) - # 2) process dummy derivatives and u0map into initialization system - # prepare map for dummy derivative substitution - for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) - # set dummy derivatives to default_dd_guess unless specified - push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) - end - function process_u0map_with_dummysubs(y, x) - y = get(schedule.dummy_sub, y, y) - y = fixpoint_sub(y, diffmap) - # FIXME: DAEs provide initial conditions that require reducing the system - # to index zero. If `isdifferential(y)`, an initial condition was given for an - # algebraic variable, so ignore it. Otherwise, the initialization system - # gets a `D(y) ~ ...` equation and errors. This is the same behavior as v9. - if isdifferential(y) - return - end - # If we have `D(x) ~ x` and provide [D(x) => x, x => 1.0] to `u0map`, then - # without this condition `defs` would get `x => x` instead of retaining - # `x => 1.0`. - isequal(y, x) && return - if y ∈ vars_set - # variables specified in u0 overrides defaults - push!(defs, y => x) - elseif y isa Symbolics.Arr - # TODO: don't scalarize arrays - merge!(defs, Dict(scalarize(y .=> x))) - elseif y isa Symbolics.BasicSymbolic - # y is a derivative expression expanded; add it to the initialization equations - push!(eqs_ics, y ~ x) - else - error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") - end - end - for (y, x) in u0map - if Symbolics.isarraysymbolic(y) - process_u0map_with_dummysubs.(collect(y), collect(x)) - else - process_u0map_with_dummysubs(y, x) - end - end - else - # TODO: Check if this is still necessary - # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) - for (k, v) in u0map - defs[k] = v - end - end - - # 3) process other variables - for var in vars - if var ∈ keys(op) - push!(eqs_ics, var ~ op[var]) - elseif var ∈ keys(guesses) - push!(defs, var => guesses[var]) - elseif check_defguess - error("Invalid setup: variable $(var) has no default value or initial guess") - end - end - - # 4) process explicitly provided initialization equations - if !algebraic_only - initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] - for eq in initialization_eqs - eq = fixpoint_sub(eq, diffmap) # expand dummy derivatives - push!(eqs_ics, eq) - end - end - - # 5) process parameters as initialization unknowns - solved_params = setup_parameter_initialization!( - sys, pmap, defs, guesses, eqs_ics; check_defguess) - - # 6) parameter dependencies become equations, their LHS become unknowns - # non-numeric dependent parameters stay as parameter dependencies - new_parameter_deps = solve_parameter_dependencies!( - sys, solved_params, eqs_ics, defs, guesses) - - # 7) handle values provided for dependent parameters similar to values for observed variables - handle_dependent_parameter_constraints!(sys, pmap, eqs_ics) - - # parameters do not include ones that became initialization unknowns - pars = Vector{SymbolicParam}(filter( - !in(solved_params), parameters(sys; initial_parameters = true))) - push!(pars, get_iv(sys)) - - # 8) use observed equations for guesses of observed variables if not provided - guessed = Set(keys(defs)) # x(t), D(x(t)), ... - guessed = union(guessed, Set(default_toterm.(guessed))) # x(t), D(x(t)), xˍt(t), ... - for eq in trueobs - if !(eq.lhs in guessed) - defs[eq.lhs] = eq.rhs - #push!(guessed, eq.lhs) # should not encounter eq.lhs twice, so don't need to track it - end - end - append!(eqs_ics, trueobs) - - vars = [vars; collect(solved_params)] - - initials = Dict(k => v for (k, v) in pmap if isinitial(k)) - merge!(defs, initials) - isys = System(Vector{Equation}(eqs_ics), - vars, - pars; - defaults = defs, - checks = check_units, - name, - is_initializesystem = true, - kwargs...) - @set isys.parameter_dependencies = new_parameter_deps -end - -""" -$(TYPEDSIGNATURES) - -Generate `System` of nonlinear equations which initializes a problem from specified initial conditions of a time-independent `AbstractSystem`. -""" -function generate_initializesystem_timeindependent(sys::AbstractSystem; - op = Dict(), - initialization_eqs = [], - guesses = Dict(), - algebraic_only = false, - check_units = true, check_defguess = false, - name = nameof(sys), kwargs...) - eqs = equations(sys) - trueobs, eqs = unhack_observed(observed(sys), eqs) - vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) - - eqs_ics = Equation[] - defs = copy(defaults(sys)) # copy so we don't modify sys.defaults - additional_guesses = anydict(guesses) - guesses = merge(get_guesses(sys), additional_guesses) - - # PREPROCESSING - op = anydict(op) - u0map = anydict() - pmap = anydict() - build_operating_point!(sys, op, u0map, pmap, Dict(), unknowns(sys), - parameters(sys; initial_parameters = true)) - for (k, v) in op - if has_parameter_dependency_with_lhs(sys, k) && is_variable_floatingpoint(k) - pmap[k] = v - end - end - initsys_preprocessing!(u0map, defs) - - # Calculate valid `Initial` parameters. These are unknowns for - # which constant initial values were provided. By this point, - # they have been separated into `x => Initial(x)` in `u0map` - # and `Initial(x) => val` in `pmap`. - valid_initial_parameters = Set{BasicSymbolic}() - for (k, v) in u0map - isequal(Initial(k), v) || continue - push!(valid_initial_parameters, v) - end - - # get the initialization equations - if !algebraic_only - initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] - end - - # only include initialization equations where all the involved `Initial` - # parameters are valid. - vs = Set() - initialization_eqs = filter(initialization_eqs) do eq - empty!(vs) - vars!(vs, eq; op = Initial) - allpars = full_parameters(sys) - for p in allpars - if symbolic_type(p) == ArraySymbolic() && - Symbolics.shape(p) != Symbolics.Unknown() - append!(allpars, Symbolics.scalarize(p)) - end - end - allpars = Set(allpars) - non_params = filter(!in(allpars), vs) - # error if non-parameters are present in the initialization equations - if !isempty(non_params) - throw(UnknownsInTimeIndependentInitializationError(eq, non_params)) - end - filter!(x -> iscall(x) && isinitial(x), vs) - invalid_initials = setdiff(vs, valid_initial_parameters) - return isempty(invalid_initials) - end - - append!(eqs_ics, initialization_eqs) - - # process parameters as initialization unknowns - solved_params = setup_parameter_initialization!( - sys, pmap, defs, guesses, eqs_ics; check_defguess) - - # parameter dependencies become equations, their LHS become unknowns - # non-numeric dependent parameters stay as parameter dependencies - new_parameter_deps = solve_parameter_dependencies!( - sys, solved_params, eqs_ics, defs, guesses) - - # handle values provided for dependent parameters similar to values for observed variables - handle_dependent_parameter_constraints!(sys, pmap, eqs_ics) - - # parameters do not include ones that became initialization unknowns - pars = Vector{SymbolicParam}(filter( - !in(solved_params), parameters(sys; initial_parameters = true))) - vars = collect(solved_params) - - initials = Dict(k => v for (k, v) in pmap if isinitial(k)) - merge!(defs, initials) - isys = System(Vector{Equation}(eqs_ics), - vars, - pars; - defaults = defs, - checks = check_units, - name, - is_initializesystem = true, - kwargs...) - @set isys.parameter_dependencies = new_parameter_deps -end - -""" - $(TYPEDSIGNATURES) - -Preprocessing step for initialization. Currently removes key `k` from `defs` and `u0map` -if `k => nothing` is present in `u0map`. -""" -function initsys_preprocessing!(u0map::AbstractDict, defs::AbstractDict) - for (k, v) in u0map - v === nothing || continue - delete!(defs, k) - end - filter_missing_values!(u0map) -end - -""" - $(TYPEDSIGNATURES) - -Update `defs` and `eqs_ics` appropriately for parameter initialization. Return a dictionary -mapping solvable parameters to their `tovar` variants. -""" -function setup_parameter_initialization!( - sys::AbstractSystem, pmap::AbstractDict, defs::AbstractDict, - guesses::AbstractDict, eqs_ics::Vector{Equation}; check_defguess = false) - solved_params = Set() - for p in parameters(sys) - if is_parameter_solvable(p, pmap, defs, guesses) - # If either of them are `missing` the parameter is an unknown - # But if the parameter is passed a value, use that as an additional - # equation in the system - _val1 = get_possibly_array_fallback_singletons(pmap, p) - _val2 = get_possibly_array_fallback_singletons(defs, p) - _val3 = get_possibly_array_fallback_singletons(guesses, p) - varp = tovar(p) - push!(solved_params, p) - # Has a default of `missing`, and (either an equation using the value passed to `ODEProblem` or a guess) - if _val2 === missing - if _val1 !== nothing && _val1 !== missing - push!(eqs_ics, varp ~ _val1) - push!(defs, varp => _val1) - elseif _val3 !== nothing - # assuming an equation exists (either via algebraic equations or initialization_eqs) - push!(defs, varp => _val3) - elseif check_defguess - error("Invalid setup: parameter $(p) has no default value, initial value, or guess") - end - # `missing` passed to `ODEProblem`, and (either an equation using default or a guess) - elseif _val1 === missing - if _val2 !== nothing && _val2 !== missing - push!(eqs_ics, varp ~ _val2) - push!(defs, varp => _val2) - elseif _val3 !== nothing - push!(defs, varp => _val3) - elseif check_defguess - error("Invalid setup: parameter $(p) has no default value, initial value, or guess") - end - # given a symbolic value to ODEProblem - elseif symbolic_type(_val1) != NotSymbolic() || is_array_of_symbolics(_val1) - push!(eqs_ics, varp ~ _val1) - push!(defs, varp => _val3) - # No value passed to `ODEProblem`, but a default and a guess are present - # _val2 !== missing is implied by it falling this far in the elseif chain - elseif _val1 === nothing && _val2 !== nothing - push!(eqs_ics, varp ~ _val2) - push!(defs, varp => _val3) - else - # _val1 !== missing and _val1 !== nothing, so a value was provided to ODEProblem - # This would mean `is_parameter_solvable` returned `false`, so we never end up - # here - error("This should never be reached") - end - end - end - - return solved_params -end - -""" - $(TYPEDSIGNATURES) - -Add appropriate parameter dependencies as initialization equations. Return the new list of -parameter dependencies for the initialization system. -""" -function solve_parameter_dependencies!(sys::AbstractSystem, solved_params::AbstractSet, - eqs_ics::Vector{Equation}, defs::AbstractDict, guesses::AbstractDict) - new_parameter_deps = Equation[] - for eq in parameter_dependencies(sys) - if !is_variable_floatingpoint(eq.lhs) - push!(new_parameter_deps, eq) - continue - end - varp = tovar(eq.lhs) - push!(solved_params, eq.lhs) - push!(eqs_ics, eq) - guessval = get(guesses, eq.lhs, eq.rhs) - push!(defs, varp => guessval) - end - - return new_parameter_deps -end - -""" - $(TYPEDSIGNATURES) - -Turn values provided for parameter dependencies into initialization equations. -""" -function handle_dependent_parameter_constraints!(sys::AbstractSystem, pmap::AbstractDict, - eqs_ics::Vector{Equation}) - for (k, v) in merge(defaults(sys), pmap) - if is_variable_floatingpoint(k) && has_parameter_dependency_with_lhs(sys, k) - push!(eqs_ics, k ~ v) - end - end - - return nothing -end - -""" - $(TYPEDSIGNATURES) - -Get a new symbolic variable of the same type and size as `sym`, which is a parameter. -""" -function get_initial_value_parameter(sym) - sym = default_toterm(unwrap(sym)) - name = hasname(sym) ? getname(sym) : Symbol(sym) - if iscall(sym) && operation(sym) === getindex - name = Symbol(name, :_, join(arguments(sym)[2:end], "_")) - end - name = Symbol(name, :ₘₜₖ_₀) - newvar = unwrap(similar_variable(sym, name; use_gensym = false)) - return toparam(newvar) -end - -""" - $(TYPEDSIGNATURES) - -Given `sys` and a list of observed equations `trueobs`, remove all the equations that -directly or indirectly contain a delayed unknown of `sys`. -""" -function filter_delay_equations_variables!(sys::AbstractSystem, trueobs::Vector{Equation}) - is_time_dependent(sys) || return trueobs - banned_vars = Set() - idxs_to_remove = Int[] - for (i, eq) in enumerate(trueobs) - _has_delays(sys, eq.rhs, banned_vars) || continue - push!(idxs_to_remove, i) - push!(banned_vars, eq.lhs) - end - return deleteat!(trueobs, idxs_to_remove) -end - -""" - $(TYPEDSIGNATURES) - -Check if the expression `ex` contains a delayed unknown of `sys` or a term in -`banned`. -""" -function _has_delays(sys::AbstractSystem, ex, banned) - ex = unwrap(ex) - ex in banned && return true - if symbolic_type(ex) == NotSymbolic() - if is_array_of_symbolics(ex) - return any(x -> _has_delays(sys, x, banned), ex) - end - return false - end - iscall(ex) || return false - op = operation(ex) - args = arguments(ex) - if iscalledparameter(ex) - return any(x -> _has_delays(sys, x, banned), args) - end - if issym(op) && length(args) == 1 && is_variable(sys, op(get_iv(sys))) && - iscall(args[1]) && get_iv(sys) in vars(args[1]) - return true - end - return any(x -> _has_delays(sys, x, banned), args) -end - -function get_possibly_array_fallback_singletons(varmap, p) - if haskey(varmap, p) - return varmap[p] - end - if symbolic_type(p) == ArraySymbolic() - is_sized_array_symbolic(p) || return nothing - scal = collect(p) - if all(x -> haskey(varmap, x), scal) - res = [varmap[x] for x in scal] - if any(x -> x === nothing, res) - return nothing - elseif any(x -> x === missing, res) - return missing - end - return res - end - elseif iscall(p) && operation(p) == getindex - arrp = arguments(p)[1] - val = get_possibly_array_fallback_singletons(varmap, arrp) - if val === nothing - return nothing - elseif val === missing - return missing - else - return val - end - end - return nothing -end - -function is_parameter_solvable(p, pmap, defs, guesses) - p = unwrap(p) - is_variable_floatingpoint(p) || return false - _val1 = pmap isa AbstractDict ? get_possibly_array_fallback_singletons(pmap, p) : - nothing - _val2 = get_possibly_array_fallback_singletons(defs, p) - _val3 = get_possibly_array_fallback_singletons(guesses, p) - # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to - # the ODEProblem and it has a default and a guess) - return ((_val1 === missing || _val2 === missing) || - (symbolic_type(_val1) != NotSymbolic() || is_array_of_symbolics(_val1) || - _val1 === nothing && _val2 !== nothing)) && _val3 !== nothing -end - -function SciMLBase.remake_initialization_data( - sys::AbstractSystem, odefn, u0, t0, p, newu0, newp) - if u0 === missing && p === missing - return odefn.initialization_data - end - - oldinitdata = odefn.initialization_data - - # We _always_ build initialization now. So if we didn't build it before, don't do - # it now - oldinitdata === nothing && return nothing - - if !(eltype(u0) <: Pair) && !(eltype(p) <: Pair) - oldinitdata === nothing && return nothing - - oldinitprob = oldinitdata.initializeprob - oldinitprob === nothing && return nothing - - meta = oldinitdata.metadata - meta isa InitializationMetadata || return oldinitdata - - reconstruct_fn = meta.oop_reconstruct_u0_p - # the history function doesn't matter because `reconstruct_fn` is only going to - # update the values of parameters, which aren't time dependent. The reason it - # is called is because `Initial` parameters are calculated from the corresponding - # state values. - history_fn = is_time_dependent(sys) && !is_markovian(sys) ? Returns(newu0) : nothing - new_initu0, - new_initp = reconstruct_fn( - ProblemState(; u = newu0, p = newp, t = t0, h = history_fn), oldinitprob) - if oldinitprob.f.resid_prototype === nothing - newf = oldinitprob.f - else - newf = remake(oldinitprob.f; - resid_prototype = calculate_resid_prototype( - length(oldinitprob.f.resid_prototype), new_initu0, new_initp)) - end - initprob = remake(oldinitprob; f = newf, u0 = new_initu0, p = new_initp) - return @set oldinitdata.initializeprob = initprob - end - - dvs = unknowns(sys) - ps = parameters(sys) - u0map = to_varmap(u0, dvs) - symbols_to_symbolics!(sys, u0map) - add_toterms!(u0map) - pmap = to_varmap(p, ps) - symbols_to_symbolics!(sys, pmap) - guesses = Dict() - defs = defaults(sys) - use_scc = true - initialization_eqs = Equation[] - op = anydict() - - if oldinitdata !== nothing && oldinitdata.metadata isa InitializationMetadata - meta = oldinitdata.metadata - op = copy(meta.op) - merge!(guesses, meta.guesses) - use_scc = meta.use_scc - initialization_eqs = meta.additional_initialization_eqs - time_dependent_init = meta.time_dependent_init - else - # there is no initializeprob, so the original problem construction - # had no solvable parameters and had the differential variables - # specified in `u0map`. - if u0 === missing - # the user didn't pass `u0` to `remake`, so they want to retain - # existing values. Fill the differential variables in `u0map`, - # initialization will either be elided or solve for the algebraic - # variables - diff_idxs = isdiffeq.(equations(sys)) - for i in eachindex(dvs) - diff_idxs[i] || continue - u0map[dvs[i]] = newu0[i] - end - end - # ensure all unknowns have guesses in case they weren't given one - # and become solvable - for i in eachindex(dvs) - haskey(guesses, dvs[i]) && continue - guesses[dvs[i]] = newu0[i] - end - if p === missing - # the user didn't pass `p` to `remake`, so they want to retain - # existing values. Fill all parameters in `pmap` so that none of - # them are solvable. - for p in ps - pmap[p] = getp(sys, p)(newp) - end - end - # all non-solvable parameters need values regardless - for p in ps - haskey(pmap, p) && continue - is_parameter_solvable(p, pmap, defs, guesses) && continue - pmap[p] = getp(sys, p)(newp) - end - end - if t0 === nothing && is_time_dependent(sys) - t0 = 0.0 - end - merge!(op, u0map, pmap) - filter_missing_values!(op) - - u0map = anydict() - pmap = anydict() - missing_unknowns, - missing_pars = build_operating_point!(sys, op, - u0map, pmap, defs, dvs, ps) - floatT = float_type_from_varmap(op) - u0_constructor = p_constructor = identity - if newu0 isa StaticArray - u0_constructor = vals -> SymbolicUtils.Code.create_array( - typeof(newu0), floatT, Val(1), Val(length(vals)), vals...) - end - if newp isa StaticArray || newp isa MTKParameters && newp.initials isa StaticArray - p_constructor = vals -> SymbolicUtils.Code.create_array( - typeof(newp.initials), floatT, Val(1), Val(length(vals)), vals...) - end - kws = maybe_build_initialization_problem( - sys, SciMLBase.isinplace(odefn), op, t0, defs, guesses, - missing_unknowns; time_dependent_init, use_scc, initialization_eqs, floatT, - u0_constructor, p_constructor, allow_incomplete = true, check_units = false) - - odefn = remake(odefn; kws...) - return SciMLBase.remake_initialization_data(sys, odefn, newu0, t0, newp, newu0, newp) -end - -promote_type_with_nothing(::Type{T}, ::Nothing) where {T} = T -promote_type_with_nothing(::Type{T}, ::StaticVector{0}) where {T} = T -function promote_type_with_nothing(::Type{T}, ::AbstractArray{T2}) where {T, T2} - promote_type(T, T2) -end -function promote_type_with_nothing(::Type{T}, p::MTKParameters) where {T} - promote_type_with_nothing(promote_type_with_nothing(T, p.tunable), p.initials) -end - -promote_with_nothing(::Type, ::Nothing) = nothing -promote_with_nothing(::Type, x::StaticVector{0}) = x -promote_with_nothing(::Type{T}, x::AbstractArray{T}) where {T} = x -function promote_with_nothing(::Type{T}, x::AbstractArray{T2}) where {T, T2} - if ArrayInterface.ismutable(x) - y = similar(x, T) - copyto!(y, x) - return y - else - yT = similar_type(x, T) - return yT(x) - end -end -function promote_with_nothing(::Type{T}, p::MTKParameters) where {T} - tunables = promote_with_nothing(T, p.tunable) - p = SciMLStructures.replace(SciMLStructures.Tunable(), p, tunables) - initials = promote_with_nothing(T, p.initials) - p = SciMLStructures.replace(SciMLStructures.Initials(), p, initials) - return p -end - -function promote_u0_p(u0, p, t0) - T = Union{} - T = promote_type_with_nothing(T, u0) - T = promote_type_with_nothing(T, p) - - u0 = promote_with_nothing(T, u0) - p = promote_with_nothing(T, p) - return u0, p -end - -function SciMLBase.late_binding_update_u0_p( - prob, sys::AbstractSystem, u0, p, t0, newu0, newp) - supports_initialization(sys) || return newu0, newp - prob isa IntervalNonlinearProblem && return newu0, newp - prob isa LinearProblem && return newu0, newp - - initdata = prob.f.initialization_data - meta = initdata === nothing ? nothing : initdata.metadata - - newu0, newp = promote_u0_p(newu0, newp, t0) - - # non-symbolic u0 updates initials... - if eltype(u0) <: Pair - syms = [] - vals = [] - allsyms = all_symbols(sys) - for (k, v) in u0 - v === nothing && continue - (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue - if k isa Symbol - k2 = symbol_to_symbolic(sys, k; allsyms) - # if it is returned as-is, there is no match so skip it - k2 === k && continue - k = k2 - end - is_parameter(sys, Initial(k)) || continue - push!(syms, Initial(k)) - push!(vals, v) - end - newp = setp_oop(sys, syms)(newp, vals) - else - allsyms = nothing - # if `p` is not provided or is symbolic - p === missing || eltype(p) <: Pair || return newu0, newp - (newu0 === nothing || isempty(newu0)) && return newu0, newp - initdata === nothing && return newu0, newp - meta = initdata.metadata - meta isa InitializationMetadata || return newu0, newp - newp = p === missing ? copy(newp) : newp - - if length(newu0) != length(prob.u0) - throw(ArgumentError("Expected `newu0` to be of same length as unknowns ($(length(prob.u0))). Got $(typeof(newu0)) of length $(length(newu0))")) - end - newp = meta.set_initial_unknowns!(newp, newu0) - end - - if eltype(p) <: Pair - syms = [] - vals = [] - if allsyms === nothing - allsyms = all_symbols(sys) - end - for (k, v) in p - v === nothing && continue - (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue - if k isa Symbol - k2 = symbol_to_symbolic(sys, k; allsyms) - # if it is returned as-is, there is no match so skip it - k2 === k && continue - k = k2 - end - is_parameter(sys, Initial(k)) || continue - push!(syms, Initial(k)) - push!(vals, v) - end - newp = setp_oop(sys, syms)(newp, vals) - end - - return newu0, newp -end - -function DiffEqBase.get_updated_symbolic_problem( - sys::AbstractSystem, prob; u0 = state_values(prob), - p = parameter_values(prob), kw...) - supports_initialization(sys) || return prob - initdata = prob.f.initialization_data - initdata isa SciMLBase.OverrideInitData || return prob - meta = initdata.metadata - meta isa InitializationMetadata || return prob - meta.get_updated_u0 === nothing && return prob - - u0 === nothing && return remake(prob; p) - - t0 = is_time_dependent(prob) ? current_time(prob) : nothing - - if p isa MTKParameters - buffer = p.initials - else - buffer = p - end - - u0 = DiffEqBase.promote_u0(u0, buffer, t0) - - if ArrayInterface.ismutable(u0) - T = typeof(u0) - else - T = StaticArrays.similar_type(u0) - end - - return remake(prob; u0 = T(meta.get_updated_u0(prob, initdata.initializeprob)), p) -end - -""" - $(TYPEDSIGNATURES) - -Check if the given system is an initialization system. -""" -function is_initializesystem(sys::AbstractSystem) - has_is_initializesystem(sys) && get_is_initializesystem(sys) -end - -""" -Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with -initialization. -""" -function unhack_observed(obseqs::Vector{Equation}, eqs::Vector{Equation}) - rm_idxs = Int[] - for (i, eq) in enumerate(obseqs) - iscall(eq.rhs) || continue - if operation(eq.rhs) == StructuralTransformations.change_origin - push!(rm_idxs, i) - continue - end - end - - obseqs = obseqs[setdiff(eachindex(obseqs), rm_idxs)] - return obseqs, eqs -end - -function UnknownsInTimeIndependentInitializationError(eq, non_params) - ArgumentError(""" - Initialization equations for time-independent systems can only contain parameters. \ - Found $non_params in $eq. If the equations refer to the initial guess for unknowns, \ - use the `Initial` operator. - """) -end diff --git a/src/systems/solver_nlprob.jl b/src/systems/solver_nlprob.jl index badfe21efb..d4772018c1 100644 --- a/src/systems/solver_nlprob.jl +++ b/src/systems/solver_nlprob.jl @@ -55,7 +55,7 @@ function inner_nlsystem(sys::System, mm, nlstep_compile::Bool) subrules = Dict([v => unwrap(gamma2*v + inner_tmp[i]) for (i, v) in enumerate(dvs)]) subrules[t] = unwrap(c) - new_rhss = map(Base.Fix2(fast_substitute, subrules), rhss) + new_rhss = map(Base.Fix2(substitute, subrules), rhss) new_rhss = collect(outer_tmp) .+ gamma1 .* new_rhss .- gamma3 * mm * dvs new_eqs = [0 ~ rhs for rhs in new_rhss] diff --git a/src/systems/sparsematrixclil.jl b/src/systems/sparsematrixclil.jl deleted file mode 100644 index cddf316084..0000000000 --- a/src/systems/sparsematrixclil.jl +++ /dev/null @@ -1,346 +0,0 @@ -""" - SparseMatrixCLIL{T, Ti} - -The SparseMatrixCLIL represents a sparse matrix in two distinct ways: - - 1. As a sparse (in both row and column) n x m matrix - 2. As a row-dense, column-sparse k x m matrix - -The data structure keeps a permutation between the row order of the two representations. -Swapping the rows in one does not affect the other. - -On construction, the second representation is equivalent to the first with fully-sparse -rows removed, though this may cease being true as row permutations are being applied -to the matrix. - -The default structure of the `SparseMatrixCLIL` type is the second structure, while -the first is available via the thin `AsSubMatrix` wrapper. -""" -struct SparseMatrixCLIL{T, Ti <: Integer} <: AbstractSparseMatrix{T, Ti} - nparentrows::Int - ncols::Int - nzrows::Vector{Ti} - row_cols::Vector{Vector{Ti}} # issorted - row_vals::Vector{Vector{T}} -end -Base.size(S::SparseMatrixCLIL) = (length(S.nzrows), S.ncols) -function Base.copy(S::SparseMatrixCLIL{T, Ti}) where {T, Ti} - SparseMatrixCLIL(S.nparentrows, S.ncols, copy(S.nzrows), map(copy, S.row_cols), - map(copy, S.row_vals)) -end -function swaprows!(S::SparseMatrixCLIL, i, j) - i == j && return - swap!(S.nzrows, i, j) - swap!(S.row_cols, i, j) - swap!(S.row_vals, i, j) -end - -function Base.convert(::Type{SparseMatrixCLIL{T, Ti}}, S::SparseMatrixCLIL) where {T, Ti} - return SparseMatrixCLIL(S.nparentrows, - S.ncols, - copy.(S.nzrows), - copy.(S.row_cols), - [T.(row) for row in S.row_vals]) -end - -function SparseMatrixCLIL(mm::AbstractMatrix) - nrows, ncols = size(mm) - row_cols = [findall(!iszero, row) for row in eachrow(mm)] - row_vals = [row[cols] for (row, cols) in zip(eachrow(mm), row_cols)] - SparseMatrixCLIL(nrows, ncols, Int[1:length(row_cols);], row_cols, row_vals) -end - -struct CLILVector{T, Ti} <: AbstractSparseVector{T, Ti} - vec::SparseVector{T, Ti} -end -Base.hash(v::CLILVector, s::UInt) = hash(v.vec, s) ⊻ 0xc71be0e9ccb75fbd -Base.size(v::CLILVector) = Base.size(v.vec) -Base.getindex(v::CLILVector, idx::Integer...) = Base.getindex(v.vec, idx...) -Base.setindex!(vec::CLILVector, v, idx::Integer...) = Base.setindex!(vec.vec, v, idx...) -function Base.view(a::SparseMatrixCLIL, i::Integer, ::Colon) - CLILVector(SparseVector(a.ncols, a.row_cols[i], a.row_vals[i])) -end -SparseArrays.nonzeroinds(a::CLILVector) = SparseArrays.nonzeroinds(a.vec) -SparseArrays.nonzeros(a::CLILVector) = SparseArrays.nonzeros(a.vec) -SparseArrays.nnz(a::CLILVector) = nnz(a.vec) - -function Base.setindex!(S::SparseMatrixCLIL, v::CLILVector, i::Integer, c::Colon) - if v.vec.n != S.ncols - throw(BoundsError(v, 1:(S.ncols))) - end - any(iszero, v.vec.nzval) && error("setindex failed") - S.row_cols[i] = copy(v.vec.nzind) - S.row_vals[i] = copy(v.vec.nzval) - return v -end - -zero!(a::AbstractArray{T}) where {T} = a[:] .= zero(T) -zero!(a::SparseVector) = (empty!(a.nzind); empty!(a.nzval)) -zero!(a::CLILVector) = zero!(a.vec) -SparseArrays.dropzeros!(a::CLILVector) = SparseArrays.dropzeros!(a.vec) - -struct NonZeros{T <: AbstractArray} - v::T -end -Base.pairs(nz::NonZeros{<:CLILVector}) = NonZerosPairs(nz.v) - -struct NonZerosPairs{T <: AbstractArray} - v::T -end - -Base.IteratorSize(::Type{<:NonZerosPairs}) = Base.SizeUnknown() -# N.B.: Because of how we're using this, this must be robust to modification of -# the underlying vector. As such, we treat this as an iteration over indices -# that happens to short cut using the sparse structure and sortedness of the -# array. -function Base.iterate(nzp::NonZerosPairs{<:CLILVector}, (idx, col)) - v = nzp.v.vec - nzind = v.nzind - nzval = v.nzval - if idx > length(nzind) - idx = length(col) - end - oldcol = nzind[idx] - if col != oldcol - # The vector was changed since the last iteration. Find our - # place in the vector again. - tail = col > oldcol ? (@view nzind[(idx + 1):end]) : (@view nzind[1:idx]) - tail_i = searchsortedfirst(tail, col + 1) - # No remaining indices. - tail_i > length(tail) && return nothing - new_idx = col > oldcol ? idx + tail_i : tail_i - new_col = nzind[new_idx] - return (new_col => nzval[new_idx], (new_idx, new_col)) - end - idx == length(nzind) && return nothing - new_col = nzind[idx + 1] - return (new_col => nzval[idx + 1], (idx + 1, new_col)) -end - -function Base.iterate(nzp::NonZerosPairs{<:CLILVector}) - v = nzp.v.vec - nzind = v.nzind - nzval = v.nzval - isempty(nzind) && return nothing - return nzind[1] => nzval[1], (1, nzind[1]) -end - -# Arguably this is how nonzeros should behave in the first place, but let's -# build something that works for us here and worry about it later. -nonzerosmap(a::CLILVector) = NonZeros(a) - -using FindFirstFunctions: findfirstequal - -function bareiss_update_virtual_colswap_mtk!(zero!, M::SparseMatrixCLIL, k, swapto, pivot, - last_pivot; pivot_equal_optimization = true) - # for ei in nzrows(>= k) - eadj = M.row_cols - old_cadj = M.row_vals - vpivot = swapto[2] - - ## N.B.: Micro-optimization - # - # For rows that do not have an entry in the eliminated column, all this - # update does is multiply the row in question by `pivot/last_pivot` (the - # result of which is guaranteed to be integer by general properties of the - # bareiss algorithm, even if `pivot/last_pivot` is not). - # - # Thus, when `pivot == last pivot`, we can skip the update for any rows that - # do not have an entry in the eliminated column (because we'd simply be - # multiplying by 1). - # - # As an additional MTK-specific enhancement, we further allow the case - # when the absolute values are equal, i.e. effectively multiplying the row - # by `-1`. To ensure this is legal, we need to show two things. - # 1. The multiplication does not change the answer and - # 2. The multiplication does not affect the fraction-freeness of the Bareiss - # algorithm. - # - # For point 1, remember that we're working on a system of linear equations, - # so it is always legal for us to multiply any row by a scalar without changing - # the underlying system of equations. - # - # For point 2, note that the factorization we're now computing is the same - # as if we had multiplied the corresponding row (accounting for row swaps) - # in the original matrix by `last_pivot/pivot`, ensuring that the matrix - # itself has integral entries when `last_pivot/pivot` is integral (here we - # have -1, which counts). We could use the more general integrality - # condition, but that would in turn disturb the growth bounds on the - # factorization matrix entries that the bareiss algorithm guarantees. To be - # conservative, we leave it at this, as this captures the most important - # case for MTK (where most pivots are `1` or `-1`). - pivot_equal = pivot_equal_optimization && abs(pivot) == abs(last_pivot) - @inbounds for ei in (k + 1):size(M, 1) - # eliminate `v` - coeff = 0 - ivars = eadj[ei] - vj = findfirstequal(vpivot, ivars) - if vj !== nothing - coeff = old_cadj[ei][vj] - deleteat!(old_cadj[ei], vj) - deleteat!(eadj[ei], vj) - elseif pivot_equal - continue - end - - # the pivot row - kvars = eadj[k] - kcoeffs = old_cadj[k] - # the elimination target - ivars = eadj[ei] - icoeffs = old_cadj[ei] - - numkvars = length(kvars) - numivars = length(ivars) - tmp_incidence = similar(eadj[ei], numkvars + numivars) - tmp_coeffs = similar(old_cadj[ei], numkvars + numivars) - tmp_len = 0 - kvind = ivind = 0 - if _debug_mode - # in debug mode, we at least check to confirm we're iterating over - # `v`s in the correct order - vars = sort(union(ivars, kvars)) - vi = 0 - end - if numivars > 0 && numkvars > 0 - kvv = kvars[kvind += 1] - ivv = ivars[ivind += 1] - dobreak = false - while true - if kvv == ivv - v = kvv - ck = kcoeffs[kvind] - ci = icoeffs[ivind] - kvind += 1 - ivind += 1 - if kvind > numkvars - dobreak = true - else - kvv = kvars[kvind] - end - if ivind > numivars - dobreak = true - else - ivv = ivars[ivind] - end - p1 = Base.Checked.checked_mul(pivot, ci) - p2 = Base.Checked.checked_mul(coeff, ck) - ci = exactdiv(Base.Checked.checked_sub(p1, p2), last_pivot) - elseif kvv < ivv - v = kvv - ck = kcoeffs[kvind] - kvind += 1 - if kvind > numkvars - dobreak = true - else - kvv = kvars[kvind] - end - p2 = Base.Checked.checked_mul(coeff, ck) - ci = exactdiv(Base.Checked.checked_neg(p2), last_pivot) - else # kvv > ivv - v = ivv - ci = icoeffs[ivind] - ivind += 1 - if ivind > numivars - dobreak = true - else - ivv = ivars[ivind] - end - ci = exactdiv(Base.Checked.checked_mul(pivot, ci), last_pivot) - end - if _debug_mode - @assert v == vars[vi += 1] - end - if v != vpivot && !iszero(ci) - tmp_incidence[tmp_len += 1] = v - tmp_coeffs[tmp_len] = ci - end - dobreak && break - end - elseif numkvars > 0 - ivind = 1 - kvv = kvars[kvind += 1] - elseif numivars > 0 - kvind = 1 - ivv = ivars[ivind += 1] - end - if kvind <= numkvars - v = kvv - while true - if _debug_mode - @assert v == vars[vi += 1] - end - if v != vpivot - ck = kcoeffs[kvind] - p2 = Base.Checked.checked_mul(coeff, ck) - ci = exactdiv(Base.Checked.checked_neg(p2), last_pivot) - if !iszero(ci) - tmp_incidence[tmp_len += 1] = v - tmp_coeffs[tmp_len] = ci - end - end - (kvind == numkvars) && break - v = kvars[kvind += 1] - end - elseif ivind <= numivars - v = ivv - while true - if _debug_mode - @assert v == vars[vi += 1] - end - if v != vpivot - p1 = Base.Checked.checked_mul(pivot, icoeffs[ivind]) - ci = exactdiv(p1, last_pivot) - if !iszero(ci) - tmp_incidence[tmp_len += 1] = v - tmp_coeffs[tmp_len] = ci - end - end - (ivind == numivars) && break - v = ivars[ivind += 1] - end - end - resize!(tmp_incidence, tmp_len) - resize!(tmp_coeffs, tmp_len) - eadj[ei] = tmp_incidence - old_cadj[ei] = tmp_coeffs - end -end - -function bareiss_update_virtual_colswap_mtk!(zero!, M::AbstractMatrix, k, swapto, pivot, - last_pivot; pivot_equal_optimization = true) - if pivot_equal_optimization - error("MTK pivot micro-optimization not implemented for `$(typeof(M))`. - Turn off the optimization for debugging or use a different matrix type.") - end - bareiss_update_virtual_colswap!(zero!, M, k, swapto, pivot, last_pivot) -end - -struct AsSubMatrix{T, Ti <: Integer} <: AbstractSparseMatrix{T, Ti} - M::SparseMatrixCLIL{T, Ti} -end -Base.size(S::AsSubMatrix) = (S.M.nparentrows, S.M.ncols) - -function Base.getindex(S::SparseMatrixCLIL{T}, i1::Integer, i2::Integer) where {T} - checkbounds(S, i1, i2) - - col = S.row_cols[i1] - nncol = searchsortedfirst(col, i2) - (nncol > length(col) || col[nncol] != i2) && return zero(T) - - return S.row_vals[i1][nncol] -end - -function Base.getindex(S::AsSubMatrix{T}, i1::Integer, i2::Integer) where {T} - checkbounds(S, i1, i2) - S = S.M - - nnrow = findfirst(==(i1), S.nzrows) - isnothing(nnrow) && return zero(T) - - col = S.row_cols[nnrow] - nncol = searchsortedfirst(col, i2) - (nncol > length(col) || col[nncol] != i2) && return zero(T) - - return S.row_vals[nnrow][nncol] -end diff --git a/src/systems/state_machines.jl b/src/systems/state_machines.jl deleted file mode 100644 index ea65981804..0000000000 --- a/src/systems/state_machines.jl +++ /dev/null @@ -1,188 +0,0 @@ -_nameof(s) = nameof(s) -_nameof(s::Union{Int, Symbol}) = s -abstract type StateMachineOperator end -Base.broadcastable(x::StateMachineOperator) = Ref(x) -Symbolics.hide_lhs(_::StateMachineOperator) = true -struct InitialState <: StateMachineOperator - s::Any -end -Base.show(io::IO, s::InitialState) = print(io, "initial_state(", _nameof(s.s), ")") -initial_state(s) = Equation(InitialState(nothing), InitialState(s)) - -Base.@kwdef struct Transition{A, B, C} <: StateMachineOperator - from::A = nothing - to::B = nothing - cond::C = nothing - immediate::Bool = true - reset::Bool = true - synchronize::Bool = false - priority::Int = 1 - function Transition(from, to, cond, immediate, reset, synchronize, priority) - cond = unwrap(cond) - new{typeof(from), typeof(to), typeof(cond)}(from, to, cond, immediate, - reset, synchronize, - priority) - end -end -function Base.:(==)(transition1::Transition, transition2::Transition) - transition1.from == transition2.from && - transition1.to == transition2.to && - isequal(transition1.cond, transition2.cond) && - transition1.immediate == transition2.immediate && - transition1.reset == transition2.reset && - transition1.synchronize == transition2.synchronize && - transition1.priority == transition2.priority -end - -""" - transition(from, to, cond; immediate::Bool = true, reset::Bool = true, synchronize::Bool = false, priority::Int = 1) - -Create a transition from state `from` to state `to` that is enabled when transitioncondition `cond` evaluates to `true`. - -# Arguments: -- `from`: The source state of the transition. -- `to`: The target state of the transition. -- `cond`: A transition condition that evaluates to a Bool, such as `ticksInState() >= 2`. -- `immediate`: If `true`, the transition will fire at the same tick as it becomes true, if `false`, the actions of the state are evaluated first, and the transition fires during the next tick. -- `reset`: If true, the destination state `to` is reset to its initial condition when the transition fires. -- `synchronize`: If true, the transition will only fire if all sub-state machines in the source state are in their final (terminal) state. A final state is one that has no outgoing transitions. -- `priority`: If a state has more than one outgoing transition, all outgoing transitions must have a unique priority. The transitions are evaluated in priority order, i.e., the transition with priority 1 is evaluated first. -""" -function transition(from, to, cond; - immediate::Bool = true, reset::Bool = true, synchronize::Bool = false, - priority::Int = 1) - Equation( - Transition(), Transition(; from, to, cond, immediate, reset, - synchronize, priority)) -end -function Base.show(io::IO, s::Transition) - print(io, _nameof(s.from), " → ", _nameof(s.to), " if (", s.cond, ") [") - print(io, "immediate: ", Int(s.immediate), ", ") - print(io, "reset: ", Int(s.reset), ", ") - print(io, "sync: ", Int(s.synchronize), ", ") - print(io, "prio: ", s.priority, "]") -end - -function activeState end -function entry end -function ticksInState end -function timeInState end - -for (s, T) in [(:timeInState, :Real), - (:ticksInState, :Integer), - (:entry, :Bool), - (:activeState, :Bool)] - seed = hash(s) - @eval begin - $s(x) = wrap(term($s, x)) - SymbolicUtils.promote_symtype(::typeof($s), _...) = $T - function SymbolicUtils.show_call(io, ::typeof($s), args) - if isempty(args) - print(io, $s, "()") - else - arg = only(args) - print(io, $s, "(", arg isa Number ? arg : nameof(arg), ")") - end - end - end - if s != :activeState - @eval $s() = wrap(term($s)) - end -end - -@doc """ - timeInState() - timeInState(state) - -Get the time (in seconds) spent in a state in a finite state machine. - -When used to query the time spent in the enclosing state, the method without arguments is used, i.e., -```julia -@mtkmodel FSM begin - ... - @equations begin - var(k+1) ~ timeInState() >= 2 ? 0.0 : var(k) - end -end -``` - -If used to query the residence time of another state, the state is passed as an argument. - -This operator can be used in both equations and transition conditions. - -See also [`ticksInState`](@ref) and [`entry`](@ref) -""" timeInState - -@doc """ - ticksInState() - ticksInState(state) - -Get the number of ticks spent in a state in a finite state machine. - -When used to query the number of ticks spent in the enclosing state, the method without arguments is used, i.e., -```julia -@mtkmodel FSM begin - ... - @equations begin - var(k+1) ~ ticksInState() >= 2 ? 0.0 : var(k) - end -end -``` - -If used to query the number of ticks in another state, the state is passed as an argument. - -This operator can be used in both equations and transition conditions. - -See also [`timeInState`](@ref) and [`entry`](@ref) -""" ticksInState - -@doc """ - entry() - entry(state) - -When used in a finite-state machine, this operator returns true at the first tick when the state is active, and false otherwise. - -When used to query the entry of the enclosing state, the method without arguments is used, when used to query the entry of another state, the state is passed as an argument. - -This can be used to perform a unique action when entering a state. -""" -entry - -@doc """ - activeState(state) - -When used in a finite state machine, this operator returns `true` if the queried state is active and false otherwise. -""" activeState - -function vars!(vars, O::Transition; op = Differential) - vars!(vars, O.from) - vars!(vars, O.to) - vars!(vars, O.cond; op) - return vars -end -function vars!(vars, O::InitialState; op = Differential) - vars!(vars, O.s; op) - return vars -end -function vars!(vars, O::StateMachineOperator; op = Differential) - error("Unhandled state machine operator") -end - -function namespace_expr( - O::Transition, sys, n = nameof(sys); ivs = independent_variables(sys)) - return Transition( - O.from === nothing ? O.from : renamespace(sys, O.from), - O.to === nothing ? O.to : renamespace(sys, O.to), - O.cond === nothing ? O.cond : namespace_expr(O.cond, sys), - O.immediate, O.reset, O.synchronize, O.priority - ) -end - -function namespace_expr( - O::InitialState, sys, n = nameof(sys); ivs = independent_variables(sys)) - return InitialState(O.s === nothing ? O.s : renamespace(sys, O.s)) -end - -function namespace_expr(O::StateMachineOperator, sys, n = nameof(sys); kwargs...) - error("Unhandled state machine operator") -end diff --git a/src/systems/substitute_component.jl b/src/systems/substitute_component.jl new file mode 100644 index 0000000000..686c2615b4 --- /dev/null +++ b/src/systems/substitute_component.jl @@ -0,0 +1,178 @@ +""" + $(TYPEDSIGNATURES) + +Validate the rules for replacement of subcomponents as defined in `substitute_component`. +""" +function validate_replacement_rule( + rule::Pair{T, T}; namespace = []) where {T <: AbstractSystem} + lhs, rhs = rule + + iscomplete(lhs) && throw(ArgumentError("LHS of replacement rule cannot be completed.")) + iscomplete(rhs) && throw(ArgumentError("RHS of replacement rule cannot be completed.")) + + rhs_h = namespace_hierarchy(nameof(rhs)) + if length(rhs_h) != 1 + throw(ArgumentError("RHS of replacement rule must not be namespaced.")) + end + rhs_h[1] == namespace_hierarchy(nameof(lhs))[end] || + throw(ArgumentError("LHS and RHS must have the same name.")) + + if !isequal(get_iv(lhs), get_iv(rhs)) + throw(ArgumentError("LHS and RHS of replacement rule must have the same independent variable.")) + end + + lhs_u = get_unknowns(lhs) + rhs_u = Dict(get_unknowns(rhs) .=> nothing) + for u in lhs_u + if !haskey(rhs_u, u) + if isempty(namespace) + throw(ArgumentError("RHS of replacement rule does not contain unknown $u.")) + else + throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain unknown $u.")) + end + end + ru = getkey(rhs_u, u, nothing) + name = join([namespace; nameof(lhs); (hasname(u) ? getname(u) : Symbol(u))], + NAMESPACE_SEPARATOR) + l_connect = something(getconnect(u), Equality) + r_connect = something(getconnect(ru), Equality) + if l_connect != r_connect + throw(ArgumentError("Variable $(name) should have connection metadata $(l_connect),")) + end + + l_input = isinput(u) + r_input = isinput(ru) + if l_input != r_input + throw(ArgumentError("Variable $name has differing causality. Marked as `input = $l_input` in LHS and `input = $r_input` in RHS.")) + end + l_output = isoutput(u) + r_output = isoutput(ru) + if l_output != r_output + throw(ArgumentError("Variable $name has differing causality. Marked as `output = $l_output` in LHS and `output = $r_output` in RHS.")) + end + end + + lhs_p = get_ps(lhs) + rhs_p = Set(get_ps(rhs)) + for p in lhs_p + if !(p in rhs_p) + if isempty(namespace) + throw(ArgumentError("RHS of replacement rule does not contain parameter $p")) + else + throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain parameter $p.")) + end + end + end + + lhs_s = get_systems(lhs) + rhs_s = Dict(nameof(s) => s for s in get_systems(rhs)) + + for s in lhs_s + if haskey(rhs_s, nameof(s)) + rs = rhs_s[nameof(s)] + if isconnector(s) + name = join([namespace; nameof(lhs); nameof(s)], NAMESPACE_SEPARATOR) + if !isconnector(rs) + throw(ArgumentError("Subsystem $name of RHS is not a connector.")) + end + if (lct = get_connector_type(s)) !== (rct = get_connector_type(rs)) + throw(ArgumentError("Subsystem $name of RHS has connection type $rct but LHS has $lct.")) + end + end + validate_replacement_rule(s => rs; namespace = [namespace; nameof(rhs)]) + continue + end + name1 = join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR) + throw(ArgumentError("$name1 of replacement rule does not contain subsystem $(nameof(s)).")) + end +end + +""" + $(TYPEDSIGNATURES) + +Chain `getproperty` calls on `root` in the order given in `hierarchy`. + +# Keyword Arguments + +- `skip_namespace_first`: Whether to avoid namespacing in the first `getproperty` call. +""" +function recursive_getproperty( + root::AbstractSystem, hierarchy::Vector{Symbol}; skip_namespace_first = true) + cur = root + for (i, name) in enumerate(hierarchy) + cur = getproperty(cur, name; namespace = i > 1 || !skip_namespace_first) + end + return unwrap(cur) +end + +""" + $(TYPEDSIGNATURES) + +Recursively descend through `sys`, finding all connection equations and re-creating them +using the names of the involved variables/systems and finding the required variables/ +systems in the hierarchy. +""" +function recreate_connections(sys::AbstractSystem) + eqs = map(get_eqs(sys)) do eq + eq.lhs isa Union{Connection, AnalysisPoint} || return eq + if eq.lhs isa Connection + oldargs = get_systems(eq.rhs) + else + ap::AnalysisPoint = eq.rhs + oldargs = [ap.input; ap.outputs] + end + newargs = map(get_systems(eq.rhs)::Union{Vector{System}, Vector{SymbolicT}}) do arg + name = arg isa AbstractSystem ? nameof(arg) : getname(arg) + hierarchy = namespace_hierarchy(name) + newarg = recursive_getproperty(sys, hierarchy) + return newarg + end + if eq.lhs isa Connection + return eq.lhs ~ Connection(newargs) + else + return eq.lhs ~ AnalysisPoint(newargs[1], eq.rhs.name, newargs[2:end]) + end + end + @set! sys.eqs = eqs + @set! sys.systems = map(recreate_connections, get_systems(sys)) + return sys +end + +""" + $(TYPEDSIGNATURES) + +Given a hierarchical system `sys` and a rule `lhs => rhs`, replace the subsystem `lhs` in +`sys` by `rhs`. The `lhs` must be the namespaced version of a subsystem of `sys` (e.g. +obtained via `sys.inner.component`). The `rhs` must be valid as per the following +conditions: + +1. `rhs` must not be namespaced. +2. The name of `rhs` must be the same as the unnamespaced name of `lhs`. +3. Neither one of `lhs` or `rhs` can be marked as complete. +4. Both `lhs` and `rhs` must share the same independent variable. +5. `rhs` must contain at least all of the unknowns and parameters present in + `lhs`. +6. Corresponding unknowns in `rhs` must share the same connection and causality + (input/output) metadata as their counterparts in `lhs`. +7. For each subsystem of `lhs`, there must be an identically named subsystem of `rhs`. + These two corresponding subsystems must satisfy conditions 3, 4, 5, 6, 7. If the + subsystem of `lhs` is a connector, the corresponding subsystem of `rhs` must also + be a connector of the same type. + +`sys` also cannot be marked as complete. +""" +function substitute_component(sys::T, rule::Pair{T, T}) where {T <: AbstractSystem} + iscomplete(sys) && + throw(ArgumentError("Cannot replace subsystems of completed systems")) + + validate_replacement_rule(rule) + + lhs, rhs = rule + hierarchy = namespace_hierarchy(nameof(lhs)) + + newsys, _ = modify_nested_subsystem(sys, hierarchy) do inner + return rhs, () + end + return recreate_connections(newsys) +end + diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 9173be44fc..faa48f5db0 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -1,13 +1,5 @@ -const REPEATED_SIMPLIFICATION_MESSAGE = "Structural simplification cannot be applied to a completed system. Double simplification is not allowed." - -struct RepeatedStructuralSimplificationError <: Exception end - -function Base.showerror(io::IO, e::RepeatedStructuralSimplificationError) - print(io, REPEATED_SIMPLIFICATION_MESSAGE) -end - -""" -$(SIGNATURES) +@doc """ + function mtkcompile(sys::System; kwargs...) Compile the given system into a form that ModelingToolkit can generate code for. Also performs a variety of symbolic-numeric enhancements. For ODEs, this includes processes @@ -26,60 +18,17 @@ present in the equations of the system will be removed in this process. + `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. + `inputs`, `outputs` and `disturbance_inputs` are passed as keyword arguments.` All inputs` get converted to parameters and are allowed to be unconnected, allowing models where `n_unknowns = n_equations - n_inputs`. + `sort_eqs=true` controls whether equations are sorted lexicographically before simplification or not. -""" -function mtkcompile( - sys::AbstractSystem; additional_passes = [], simplify = false, split = true, - allow_symbolic = false, allow_parameter = true, conservative = false, fully_determined = true, - inputs = Any[], outputs = Any[], - disturbance_inputs = Any[], array_hack = true, - kwargs...) - isscheduled(sys) && throw(RepeatedStructuralSimplificationError()) - reassemble_alg = get(kwargs, :reassemble_alg, - StructuralTransformations.DefaultReassembleAlgorithm(; simplify, array_hack)) - newsys′ = __mtkcompile(sys; - allow_symbolic, allow_parameter, conservative, fully_determined, - inputs, outputs, disturbance_inputs, additional_passes, reassemble_alg, - kwargs...) - if newsys′ isa Tuple - @assert length(newsys′) == 2 - newsys = newsys′[1] - else - newsys = newsys′ - end - for pass in additional_passes - newsys = pass(newsys) - end - if has_parent(newsys) - @set! newsys.parent = complete(sys; split = false, flatten = false) - end - newsys = complete(newsys; split) - if newsys′ isa Tuple - idxs = [parameter_index(newsys, i) for i in io[1]] - return newsys, idxs - else - return newsys - end -end +""" mtkcompile -function __mtkcompile(sys::AbstractSystem; - inputs = Any[], outputs = Any[], - disturbance_inputs = Any[], +function MTKBase.__mtkcompile(sys::System; + inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + outputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + disturbance_inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), sort_eqs = true, kwargs...) - # TODO: convert noise_eqs to brownians for simplification - if has_noise_eqs(sys) && get_noise_eqs(sys) !== nothing - sys = noise_to_brownians(sys; names = :αₘₜₖ) - end - if !isempty(jumps(sys)) - return sys - end - if isempty(equations(sys)) && !is_time_dependent(sys) && !_iszero(cost(sys)) - return simplify_optimization_system(sys; kwargs..., sort_eqs) - end - sys, statemachines = extract_top_level_statemachines(sys) sys = expand_connections(sys) - state = TearingState(sys) + state = TearingState(sys; sort_eqs) append!(state.statemachines, statemachines) @unpack structure, fullvars = state @@ -100,10 +49,11 @@ function __mtkcompile(sys::AbstractSystem; else Is = Int[] Js = Int[] - vals = Num[] + vals = SymbolicT[] make_eqs_zero_equals!(state) new_eqs = copy(equations(state)) - dvar2eq = Dict{Any, Int}() + dvar2eq = Dict{SymbolicT, Int}() + eqs = equations(state) for (v, dv) in enumerate(var_to_diff) dv === nothing && continue deqs = 𝑑neighbors(graph, dv) @@ -120,7 +70,7 @@ function __mtkcompile(sys::AbstractSystem; brown = fullvars[bj] (coeff, residual, islinear) = Symbolics.linear_expansion(eq, brown) islinear || error("$brown isn't linear in $eq") - new_eqs[i] = 0 ~ residual + new_eqs[i] = COMMON_ZERO ~ residual push!(vals, coeff) end g = Matrix(sparse(Is, Js, vals)) @@ -133,7 +83,7 @@ function __mtkcompile(sys::AbstractSystem; ode_sys = mtkcompile( sys; inputs, outputs, disturbance_inputs, kwargs...) eqs = equations(ode_sys) - sorted_g_rows = zeros(Num, length(eqs), size(g, 2)) + sorted_g_rows = fill(COMMON_ZERO, length(eqs), size(g, 2)) for (i, eq) in enumerate(eqs) dvar = eq.lhs # differential equations always precede algebraic equations @@ -162,87 +112,21 @@ function __mtkcompile(sys::AbstractSystem; noise_eqs = substitute_observed(ode_sys, noise_eqs) ssys = System(Vector{Equation}(full_equations(ode_sys)), - get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); noise_eqs, - name = nameof(ode_sys), observed = observed(ode_sys), defaults = defaults(sys), + get_iv(ode_sys), unknowns(ode_sys), + [parameters(ode_sys); collect(bound_parameters(ode_sys))]; noise_eqs, + name = nameof(ode_sys), observed = observed(ode_sys), bindings = bindings(sys), + initial_conditions = initial_conditions(sys), assertions = assertions(sys), guesses = guesses(sys), initialization_eqs = initialization_equations(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys), gui_metadata = get_gui_metadata(sys)) - @set! ssys.parameter_dependencies = get_parameter_dependencies(sys) return ssys end end -function simplify_optimization_system(sys::System; split = true, kwargs...) - sys = flatten(sys) - cons = constraints(sys) - econs = Equation[] - icons = similar(cons, 0) - for e in cons - if e isa Equation - push!(econs, e) - else - push!(icons, e) - end - end - irreducible_subs = Dict() - dvs = mapreduce(Symbolics.scalarize, vcat, unknowns(sys)) - if !(dvs isa Array) - dvs = [dvs] - end - for i in eachindex(dvs) - var = dvs[i] - if hasbounds(var) - irreducible_subs[var] = irrvar = setirreducible(var, true) - dvs[i] = irrvar - end - end - econs = fast_substitute.(econs, (irreducible_subs,)) - nlsys = System(econs, dvs, parameters(sys); name = :___tmp_nlsystem) - snlsys = mtkcompile(nlsys; kwargs..., fully_determined = false) - obs = observed(snlsys) - seqs = equations(snlsys) - trueobs, _ = unhack_observed(obs, seqs) - subs = Dict(eq.lhs => eq.rhs for eq in trueobs) - cons_simplified = similar(cons, length(icons) + length(seqs)) - for (i, eq) in enumerate(Iterators.flatten((seqs, icons))) - cons_simplified[i] = fixpoint_sub(eq, subs) - end - newsts = setdiff(dvs, keys(subs)) - @set! sys.constraints = cons_simplified - @set! sys.observed = [observed(sys); obs] - newcost = fixpoint_sub.(get_costs(sys), (subs,)) - @set! sys.costs = newcost - @set! sys.unknowns = newsts - return sys -end - -function __num_isdiag_noise(mat) - for i in axes(mat, 1) - nnz = 0 - for j in axes(mat, 2) - if !isequal(mat[i, j], 0) - nnz += 1 - end - end - if nnz > 1 - return (false) - end - end - true -end - -function __get_num_diag_noise(mat) - map(axes(mat, 1)) do i - for j in axes(mat, 2) - mij = mat[i, j] - if !isequal(mij, 0) - return mij - end - end - 0 - end +function MTKBase.simplify_sde_system(sys::System; kwargs...) + __mtkcompile(sys; kwargs...) end """ @@ -283,9 +167,9 @@ function map_variables_to_equations(sys::AbstractSystem; rename_dummy_derivative graph = ts.structure.graph algvars = BitSet(findall( - Base.Fix1(StructuralTransformations.isalgvar, ts.structure), 1:ndsts(graph))) + Base.Fix1(StateSelection.isalgvar, ts.structure), 1:ndsts(graph))) algeqs = BitSet(findall(1:nsrcs(graph)) do eq - all(!Base.Fix1(isdervar, ts.structure), 𝑠neighbors(graph, eq)) + all(!Base.Fix1(StateSelection.isdervar, ts.structure), 𝑠neighbors(graph, eq)) end) alge_var_eq_matching = complete(maximal_matching(graph, in(algeqs), in(algvars))) for (i, eq) in enumerate(alge_var_eq_matching) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 6d65dc9418..0b4a703b33 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -1,312 +1,4 @@ -using DataStructures -using Symbolics: linear_expansion, unwrap -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, - isparameter, Connection, - independent_variables, SparseMatrixCLIL, AbstractSystem, - equations, isirreducible, input_timedomain, TimeDomain, - InferredTimeDomain, - VariableType, getvariabletype, has_equations, System -using ..BipartiteGraphs -import ..BipartiteGraphs: invview, complete -using Graphs -using UnPack -using Setfield -using SparseArrays - -function quick_cancel_expr(expr) - Rewriters.Postwalk(quick_cancel, - similarterm = (x, f, args; - kws...) -> maketerm(typeof(x), f, args, - SymbolicUtils.metadata(x), - kws...))(expr) -end - -export SystemStructure, TransformationState, TearingState, mtkcompile! -export isdiffvar, isdervar, isalgvar, isdiffeq, algeqs, is_only_discrete -export dervars_range, diffvars_range, algvars_range -export DiffGraph, complete! -export get_fullvars, system_subset - -struct DiffGraph <: Graphs.AbstractGraph{Int} - primal_to_diff::Vector{Union{Int, Nothing}} - diff_to_primal::Union{Nothing, Vector{Union{Int, Nothing}}} -end - -DiffGraph(primal_to_diff::Vector{Union{Int, Nothing}}) = DiffGraph(primal_to_diff, nothing) -function DiffGraph(n::Integer, with_badj::Bool = false) - DiffGraph(Union{Int, Nothing}[nothing for _ in 1:n], - with_badj ? Union{Int, Nothing}[nothing for _ in 1:n] : nothing) -end - -function Base.copy(dg::DiffGraph) - DiffGraph(copy(dg.primal_to_diff), - dg.diff_to_primal === nothing ? nothing : copy(dg.diff_to_primal)) -end - -@noinline function require_complete(dg::DiffGraph) - dg.diff_to_primal === nothing && - error("Not complete. Run `complete` first.") -end - -Graphs.is_directed(dg::DiffGraph) = true -function Graphs.edges(dg::DiffGraph) - (i => v for (i, v) in enumerate(dg.primal_to_diff) if v !== nothing) -end -Graphs.nv(dg::DiffGraph) = length(dg.primal_to_diff) -Graphs.ne(dg::DiffGraph) = count(x -> x !== nothing, dg.primal_to_diff) -Graphs.vertices(dg::DiffGraph) = Base.OneTo(nv(dg)) -function Graphs.outneighbors(dg::DiffGraph, var::Integer) - diff = dg.primal_to_diff[var] - return diff === nothing ? () : (diff,) -end -function Graphs.inneighbors(dg::DiffGraph, var::Integer) - require_complete(dg) - diff = dg.diff_to_primal[var] - return diff === nothing ? () : (diff,) -end -function Graphs.add_vertex!(dg::DiffGraph) - push!(dg.primal_to_diff, nothing) - if dg.diff_to_primal !== nothing - push!(dg.diff_to_primal, nothing) - end - return length(dg.primal_to_diff) -end - -function Graphs.add_edge!(dg::DiffGraph, var::Integer, diff::Integer) - dg[var] = diff -end - -# Also pass through the array interface for ease of use -Base.:(==)(dg::DiffGraph, v::AbstractVector) = dg.primal_to_diff == v -Base.:(==)(dg::AbstractVector, v::DiffGraph) = v == dg.primal_to_diff -Base.eltype(::DiffGraph) = Union{Int, Nothing} -Base.size(dg::DiffGraph) = size(dg.primal_to_diff) -Base.length(dg::DiffGraph) = length(dg.primal_to_diff) -Base.getindex(dg::DiffGraph, var::Integer) = dg.primal_to_diff[var] -Base.getindex(dg::DiffGraph, a::AbstractArray) = [dg[x] for x in a] - -function Base.setindex!(dg::DiffGraph, val::Union{Integer, Nothing}, var::Integer) - if dg.diff_to_primal !== nothing - old_pd = dg.primal_to_diff[var] - if old_pd !== nothing - dg.diff_to_primal[old_pd] = nothing - end - if val !== nothing - #old_dp = dg.diff_to_primal[val] - #old_dp === nothing || error("Variable already assigned.") - dg.diff_to_primal[val] = var - end - end - return dg.primal_to_diff[var] = val -end -Base.iterate(dg::DiffGraph, state...) = iterate(dg.primal_to_diff, state...) - -function complete(dg::DiffGraph) - dg.diff_to_primal !== nothing && return dg - diff_to_primal = Union{Int, Nothing}[nothing for _ in 1:length(dg.primal_to_diff)] - for (var, diff) in edges(dg) - diff_to_primal[diff] = var - end - return DiffGraph(dg.primal_to_diff, diff_to_primal) -end - -function invview(dg::DiffGraph) - require_complete(dg) - return DiffGraph(dg.diff_to_primal, dg.primal_to_diff) -end - -struct DiffChainIterator{Descend} - var_to_diff::DiffGraph - v::Int -end - -function Base.iterate(di::DiffChainIterator{Descend}, v = nothing) where {Descend} - if v === nothing - vv = di.v - return (vv, vv) - end - g = Descend ? invview(di.var_to_diff) : di.var_to_diff - v′ = g[v] - v′ === nothing ? nothing : (v′, v′) -end - -abstract type TransformationState{T} end -abstract type AbstractTearingState{T} <: TransformationState{T} end - -get_fullvars(ts::TransformationState) = ts.fullvars -has_equations(::TransformationState) = true - -Base.@kwdef mutable struct SystemStructure - """Maps the index of variable x to the index of variable D(x).""" - var_to_diff::DiffGraph - """Maps the index of an algebraic equation to the index of the equation it is differentiated into.""" - eq_to_diff::DiffGraph - # Can be access as - # `graph` to automatically look at the bipartite graph - # or as `torn` to assert that tearing has run. - """Graph that connects equations to variables that appear in them.""" - graph::BipartiteGraph{Int, Nothing} - """Graph that connects equations to the variable they will be solved for during simplification.""" - solvable_graph::Union{BipartiteGraph{Int, Nothing}, Nothing} - """Variable types (brownian, variable, parameter) in the system.""" - var_types::Union{Vector{VariableType}, Nothing} - """Whether the system is discrete.""" - only_discrete::Bool -end - -function Base.copy(structure::SystemStructure) - var_types = structure.var_types === nothing ? nothing : copy(structure.var_types) - SystemStructure(copy(structure.var_to_diff), copy(structure.eq_to_diff), - copy(structure.graph), copy(structure.solvable_graph), - var_types, structure.only_discrete) -end - -is_only_discrete(s::SystemStructure) = s.only_discrete -isdervar(s::SystemStructure, i) = invview(s.var_to_diff)[i] !== nothing -function isalgvar(s::SystemStructure, i) - s.var_to_diff[i] === nothing && - invview(s.var_to_diff)[i] === nothing -end -function isdiffvar(s::SystemStructure, i) - s.var_to_diff[i] !== nothing && invview(s.var_to_diff)[i] === nothing -end - -function dervars_range(s::SystemStructure) - Iterators.filter(Base.Fix1(isdervar, s), Base.OneTo(ndsts(s.graph))) -end -function diffvars_range(s::SystemStructure) - Iterators.filter(Base.Fix1(isdiffvar, s), Base.OneTo(ndsts(s.graph))) -end -function algvars_range(s::SystemStructure) - Iterators.filter(Base.Fix1(isalgvar, s), Base.OneTo(ndsts(s.graph))) -end - -function algeqs(s::SystemStructure) - BitSet(findall(map(1:nsrcs(s.graph)) do eq - all(v -> !isdervar(s, v), 𝑠neighbors(s.graph, eq)) - end)) -end - -function complete!(s::SystemStructure) - s.var_to_diff = complete(s.var_to_diff) - s.eq_to_diff = complete(s.eq_to_diff) - s.graph = complete(s.graph) - if s.solvable_graph !== nothing - s.solvable_graph = complete(s.solvable_graph) - end - s -end - -mutable struct TearingState{T <: AbstractSystem} <: AbstractTearingState{T} - """The system of equations.""" - sys::T - """The set of variables of the system.""" - fullvars::Vector{BasicSymbolic} - structure::SystemStructure - extra_eqs::Vector - param_derivative_map::Dict{BasicSymbolic, Any} - original_eqs::Vector{Equation} - """ - Additional user-provided observed equations. The variables calculated here - are not used in the rest of the system. - """ - additional_observed::Vector{Equation} - statemachines::Vector{T} -end - -TransformationState(sys::AbstractSystem) = TearingState(sys) -function system_subset(ts::TearingState, ieqs::Vector{Int}, iieqs::Vector{Int}, ivars::Vector{Int}) - eqs = equations(ts) - initeqs = initialization_equations(ts.sys) - @set! ts.sys.eqs = eqs[ieqs] - @set! ts.sys.initialization_eqs = initeqs[iieqs] - @set! ts.original_eqs = ts.original_eqs[ieqs] - @set! ts.structure = system_subset(ts.structure, ieqs, ivars) - if all(eq -> eq.rhs isa StateMachineOperator, get_eqs(ts.sys)) - names = Symbol[] - for eq in get_eqs(ts.sys) - if eq.lhs isa Transition - push!(names, first(namespace_hierarchy(nameof(eq.rhs.from)))) - push!(names, first(namespace_hierarchy(nameof(eq.rhs.to)))) - elseif eq.lhs isa InitialState - push!(names, first(namespace_hierarchy(nameof(eq.rhs.s)))) - else - error("Unhandled state machine operator") - end - end - @set! ts.statemachines = filter(x -> nameof(x) in names, ts.statemachines) - else - @set! ts.statemachines = eltype(ts.statemachines)[] - end - @set! ts.fullvars = ts.fullvars[ivars] - ts -end - -function system_subset(structure::SystemStructure, ieqs::Vector{Int}, ivars::Vector{Int}) - @unpack graph = structure - fadj = Vector{Int}[] - eq_to_diff = DiffGraph(length(ieqs)) - var_to_diff = DiffGraph(length(ivars)) - - ne = 0 - old_to_new_var = zeros(Int, ndsts(graph)) - for (i, iv) in enumerate(ivars) - old_to_new_var[iv] = i - end - for (i, iv) in enumerate(ivars) - structure.var_to_diff[iv] === nothing && continue - var_to_diff[i] = old_to_new_var[structure.var_to_diff[iv]] - end - for (j, eq_i) in enumerate(ieqs) - var_adj = [old_to_new_var[i] for i in graph.fadjlist[eq_i]] - @assert all(!iszero, var_adj) - ne += length(var_adj) - push!(fadj, var_adj) - eq_to_diff[j] = structure.eq_to_diff[eq_i] - end - @set! structure.graph = complete(BipartiteGraph(ne, fadj, length(ivars))) - @set! structure.eq_to_diff = eq_to_diff - @set! structure.var_to_diff = complete(var_to_diff) - structure -end - -function Base.show(io::IO, state::TearingState) - print(io, "TearingState of ", typeof(state.sys)) -end - -struct EquationsView{T} <: AbstractVector{Any} - ts::TearingState{T} -end -equations(ts::TearingState) = EquationsView(ts) -Base.size(ev::EquationsView) = (length(equations(ev.ts.sys)) + length(ev.ts.extra_eqs),) -function Base.getindex(ev::EquationsView, i::Integer) - eqs = equations(ev.ts.sys) - if i > length(eqs) - return ev.ts.extra_eqs[i - length(eqs)] - end - return eqs[i] -end -function Base.push!(ev::EquationsView, eq) - push!(ev.ts.extra_eqs, eq) -end - -function is_time_dependent_parameter(p, allps, iv) - return iv !== nothing && p in allps && iscall(p) && - (operation(p) === getindex && - is_time_dependent_parameter(arguments(p)[1], allps, iv) || - (args = arguments(p); length(args)) == 1 && isequal(only(args), iv)) -end - -function symbolic_contains(var, set) - var in set || - symbolic_type(var) == ArraySymbolic() && - Symbolics.shape(var) != Symbolics.Unknown() && - all(x -> x in set, Symbolics.scalarize(var)) -end +export mtkcompile! """ $(TYPEDSIGNATURES) @@ -315,21 +7,21 @@ Descend through the system hierarchy and look for statemachines. Remove equation the inner statemachine systems. Return the new `sys` and an array of top-level statemachines. """ -function extract_top_level_statemachines(sys::AbstractSystem) +function extract_top_level_statemachines(sys::System) eqs = get_eqs(sys) - - if !isempty(eqs) && all(eq -> eq.lhs isa StateMachineOperator, eqs) + predicate = Base.Fix2(isa, MTKTearing.StateMachineOperator) ∘ SU.unwrap_const + if !isempty(eqs) && all(predicate, eqs) # top-level statemachine with_removed = @set sys.systems = map(remove_child_equations, get_systems(sys)) return with_removed, [sys] - elseif !isempty(eqs) && any(eq -> eq.lhs isa StateMachineOperator, eqs) + elseif !isempty(eqs) && any(predicate, eqs) # error: can't mix error("Mixing statemachine equations and standard equations in a top-level statemachine is not allowed.") else # descend subsystems = get_systems(sys) - newsubsystems = eltype(subsystems)[] - statemachines = eltype(subsystems)[] + newsubsystems = System[] + statemachines = System[] for subsys in subsystems newsubsys, sub_statemachines = extract_top_level_statemachines(subsys) push!(newsubsystems, newsubsys) @@ -345,585 +37,97 @@ end Return `sys` with all equations (including those in subsystems) removed. """ -function remove_child_equations(sys::AbstractSystem) - @set! sys.eqs = eltype(get_eqs(sys))[] +function remove_child_equations(sys::System) + @set! sys.eqs = Equation[] @set! sys.systems = map(remove_child_equations, get_systems(sys)) return sys end -function TearingState(sys; quick_cancel = false, check = true, sort_eqs = true) - # flatten system - sys = flatten(sys) - sys = process_parameter_equations(sys) - ivs = independent_variables(sys) - iv = length(ivs) == 1 ? ivs[1] : nothing - # flatten array equations - eqs = flatten_equations(equations(sys)) - original_eqs = copy(eqs) - neqs = length(eqs) - param_derivative_map = Dict{BasicSymbolic, Any}() - # * Scalarize unknowns - dvs = Set{BasicSymbolic}() - fullvars = BasicSymbolic[] - for x in unknowns(sys) - push!(dvs, x) - xx = Symbolics.scalarize(x) - if xx isa AbstractArray - union!(dvs, xx) - end - end - ps = Set{Symbolic}() - for x in full_parameters(sys) - push!(ps, x) - if symbolic_type(x) == ArraySymbolic() && Symbolics.shape(x) != Symbolics.Unknown() - xx = Symbolics.scalarize(x) - union!(ps, xx) - end - end - browns = Set{BasicSymbolic}() - for x in brownians(sys) - push!(browns, x) - xx = Symbolics.scalarize(x) - if xx isa AbstractArray - union!(browns, xx) - end - end - var2idx = Dict{BasicSymbolic, Int}() - var_types = VariableType[] - addvar! = let fullvars = fullvars, dvs = dvs, var2idx = var2idx, var_types = var_types - (var, vtype) -> get!(var2idx, var) do - push!(dvs, var) - push!(fullvars, var) - push!(var_types, vtype) - return length(fullvars) - end - end - - # build symbolic incidence - symbolic_incidence = Vector{BasicSymbolic}[] - varsbuf = Set() - eqs_to_retain = trues(length(eqs)) - for (i, eq) in enumerate(eqs) - _eq = eq - if iscall(eq.lhs) && (op = operation(eq.lhs)) isa Differential && - isequal(op.x, iv) && is_time_dependent_parameter(only(arguments(eq.lhs)), ps, iv) - # parameter derivatives are opted out by specifying `D(p) ~ missing`, but - # we want to store `nothing` in the map because that means `fast_substitute` - # will ignore the rule. We will this identify the presence of `eq′.lhs` in - # the differentiated expression and error. - param_derivative_map[eq.lhs] = coalesce(eq.rhs, nothing) - eqs_to_retain[i] = false - # change the equation if the RHS is `missing` so the rest of this loop works - eq = 0.0 ~ coalesce(eq.rhs, 0.0) - end - is_statemachine_equation = false - if eq.lhs isa StateMachineOperator - is_statemachine_equation = true - eq = eq - rhs = eq.rhs - elseif _iszero(eq.lhs) - rhs = quick_cancel ? quick_cancel_expr(eq.rhs) : eq.rhs - else - lhs = quick_cancel ? quick_cancel_expr(eq.lhs) : eq.lhs - rhs = quick_cancel ? quick_cancel_expr(eq.rhs) : eq.rhs - eq = 0 ~ rhs - lhs - end - empty!(varsbuf) - vars!(varsbuf, eq; op = Symbolics.Operator) - incidence = Set{BasicSymbolic}() +function make_eqs_zero_equals!(ts::TearingState) + neweqs = map(enumerate(get_eqs(ts.sys))) do kvp + i, eq = kvp isalgeq = true - for v in varsbuf - # additionally track brownians in fullvars - if v in browns - addvar!(v, BROWNIAN) - push!(incidence, v) - end - - # TODO: Can we handle this without `isparameter`? - if symbolic_contains(v, ps) || - getmetadata(v, SymScope, LocalScope()) isa GlobalScope && isparameter(v) - if is_time_dependent_parameter(v, ps, iv) && - !haskey(param_derivative_map, Differential(iv)(v)) - # Parameter derivatives default to zero - they stay constant - # between callbacks - param_derivative_map[Differential(iv)(v)] = 0.0 - end - continue - end - - isequal(v, iv) && continue - isdelay(v, iv) && continue - - if !symbolic_contains(v, dvs) - isvalid = iscall(v) && - (operation(v) isa Shift || isempty(arguments(v)) || - is_transparent_operator(operation(v))) - v′ = v - while !isvalid && iscall(v′) && operation(v′) isa Union{Differential, Shift} - v′ = arguments(v′)[1] - if v′ in dvs || getmetadata(v′, SymScope, LocalScope()) isa GlobalScope - isvalid = true - break - end - end - if !isvalid - throw(ArgumentError("$v is present in the system but $v′ is not an unknown.")) - end - - addvar!(v, VARIABLE) - if iscall(v) && operation(v) isa Symbolics.Operator && !isdifferential(v) && - (it = input_timedomain(v)) !== nothing - for v′ in arguments(v) - addvar!(setmetadata(v′, VariableTimeDomain, it), VARIABLE) - end - end - end - - isalgeq &= !isdifferential(v) - - if symbolic_type(v) == ArraySymbolic() - vv = collect(v) - union!(incidence, vv) - map(vv) do vi - addvar!(vi, VARIABLE) - end - else - push!(incidence, v) - addvar!(v, VARIABLE) - end - end - if isalgeq || is_statemachine_equation - eqs[i] = eq - else - eqs[i] = eqs[i].lhs ~ rhs - end - push!(symbolic_incidence, collect(incidence)) - end - - dervaridxs = OrderedSet{Int}() - for (i, v) in enumerate(fullvars) - while isdifferential(v) - push!(dervaridxs, i) - v = arguments(v)[1] - i = addvar!(v, VARIABLE) - end - end - eqs = eqs[eqs_to_retain] - original_eqs = original_eqs[eqs_to_retain] - neqs = length(eqs) - symbolic_incidence = symbolic_incidence[eqs_to_retain] - - if sort_eqs - # sort equations lexicographically to reduce simplification issues - # depending on order due to NP-completeness of tearing. - sortidxs = Base.sortperm(string.(eqs)) # "by = string" creates more strings - eqs = eqs[sortidxs] - original_eqs = original_eqs[sortidxs] - symbolic_incidence = symbolic_incidence[sortidxs] - end - - # Handle shifts - find lowest shift and add intermediates with derivative edges - ### Handle discrete variables - lowest_shift = Dict() - for var in fullvars - if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) - steps = operation(var).steps - if steps > 0 - error("Only non-positive shifts allowed. Found $var with a shift of $steps") - end - v = arguments(var)[1] - lowest_shift[v] = min(get(lowest_shift, v, 0), steps) + for j in 𝑠neighbors(ts.structure.graph, i) + isalgeq &= invview(ts.structure.var_to_diff)[j] === nothing end - end - for var in fullvars - if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) - op = operation(var) - steps = op.steps - v = arguments(var)[1] - lshift = lowest_shift[v] - tt = op.t - elseif haskey(lowest_shift, var) - lshift = lowest_shift[var] - steps = 0 - tt = iv - v = var + if isalgeq + return 0 ~ eq.rhs - eq.lhs else - continue - end - if lshift < steps - push!(dervaridxs, var2idx[var]) - end - for s in (steps - 1):-1:(lshift + 1) - sf = Shift(tt, s) - dvar = sf(v) - idx = addvar!(dvar, VARIABLE) - if !(idx in dervaridxs) - push!(dervaridxs, idx) - end - end - end - - # sort `fullvars` such that the mass matrix is as diagonal as possible. - dervaridxs = collect(dervaridxs) - sorted_fullvars = OrderedSet(fullvars[dervaridxs]) - var_to_old_var = Dict(zip(fullvars, fullvars)) - for dervaridx in dervaridxs - dervar = fullvars[dervaridx] - diffvar = var_to_old_var[lower_order_var(dervar, iv)] - if !(diffvar in sorted_fullvars) - push!(sorted_fullvars, diffvar) - end - end - for v in fullvars - if !(v in sorted_fullvars) - push!(sorted_fullvars, v) + return eq end end - new_fullvars = collect(sorted_fullvars) - sortperm = indexin(new_fullvars, fullvars) - fullvars = new_fullvars - var_types = var_types[sortperm] - var2idx = Dict(fullvars .=> eachindex(fullvars)) - dervaridxs = 1:length(dervaridxs) - - # build `var_to_diff` - nvars = length(fullvars) - diffvars = [] - var_to_diff = DiffGraph(nvars, true) - for dervaridx in dervaridxs - dervar = fullvars[dervaridx] - diffvar = lower_order_var(dervar, iv) - diffvaridx = var2idx[diffvar] - push!(diffvars, diffvar) - var_to_diff[diffvaridx] = dervaridx - end - - # build incidence graph - graph = BipartiteGraph(neqs, nvars, Val(false)) - for (ie, vars) in enumerate(symbolic_incidence), v in vars - - jv = var2idx[v] - add_edge!(graph, ie, jv) - end - - @set! sys.eqs = eqs - - eq_to_diff = DiffGraph(nsrcs(graph)) - - ts = TearingState(sys, fullvars, - SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, false), - Any[], param_derivative_map, original_eqs, Equation[], typeof(sys)[]) - return ts + copyto!(get_eqs(ts.sys), neweqs) end -""" - $(TYPEDSIGNATURES) -Preemptively identify observed equations in the system and tear them. This happens before -any simplification. The equations torn by this process are ones that are already given in -an explicit form in the system and where the LHS is not present in any other equation of -the system except for other such preempitvely torn equations. """ -function trivial_tearing!(ts::TearingState) - @assert length(ts.original_eqs) == length(equations(ts)) - # equations that can be trivially torn an observed equations - trivial_idxs = BitSet() - # equations to never check - blacklist = BitSet() - torn_eqs = Equation[] - # variables that have been matched to trivially torn equations - matched_vars = BitSet() - # variable to index in fullvars - var_to_idx = Dict{Any, Int}(ts.fullvars .=> eachindex(ts.fullvars)) - sys_eqs = equations(ts) - - complete!(ts.structure) - var_to_diff = ts.structure.var_to_diff - graph = ts.structure.graph - while true - # track whether we added an equation to the trivial list this iteration - added_equation = false - for (i, eq) in enumerate(ts.original_eqs) - # don't check already torn equations - i in trivial_idxs && continue - i in blacklist && continue - # ensure it is an observed equation matched to a variable in fullvars - vari = get(var_to_idx, eq.lhs, 0) - iszero(vari) && continue - # don't tear irreducible variables - if isirreducible(eq.lhs) - push!(blacklist, i) - continue - end - # Edge case for `var ~ var` equations. They don't show up in the incidence - # graph because `TearingState` makes them `0 ~ 0`, but they do cause `var` - # to show up twice in `original_eqs` which fails the assertion. - sys_eq = sys_eqs[i] - if isequal(sys_eq.lhs, 0) && isequal(sys_eq.rhs, 0) - continue - end - - # if a variable was the LHS of two trivial observed equations, we wouldn't have - # included it in the list. Error if somehow it made it through. - @assert !(vari in matched_vars) - # don't tear differential/shift equations (or differentiated/shifted variables) - var_to_diff[vari] === nothing || continue - invview(var_to_diff)[vari] === nothing || continue - # get the equations that the candidate matched variable is present in, except - # those equations which have already been torn as observed - eqidxs = setdiff(𝑑neighbors(graph, vari), trivial_idxs) - # it should only be present in this equation - length(eqidxs) == 1 || continue - eqi = only(eqidxs) - @assert eqi == i - - # for every variable present in this equation, make sure it isn't _only_ - # present in trivial equations - isvalid = true - for v in 𝑠neighbors(graph, eqi) - v == vari && continue - v in matched_vars && continue - # `> 1` and not `0` because one entry will be this equation (`eqi`) - isvalid &= count(!in(trivial_idxs), 𝑑neighbors(graph, v)) > 1 - isvalid || break - end - isvalid || continue - # skip if the LHS is present in the RHS, since then this isn't explicit - if occursin(eq.lhs, eq.rhs) - push!(blacklist, i) - continue +Turn input variables into parameters of the system. +""" +function inputs_to_parameters!(state::TearingState, inputsyms::OrderedSet{SymbolicT}, outputsyms::OrderedSet{SymbolicT}) + @unpack structure, fullvars, sys = state + @unpack var_to_diff, graph, solvable_graph = structure + @assert solvable_graph === nothing + + var_reidx = zeros(Int, length(fullvars)) + nvar = 0 + new_fullvars = SymbolicT[] + for (i, v) in enumerate(fullvars) + if v in inputsyms + if var_to_diff[i] !== nothing + error("Input $(fullvars[i]) is differentiated!") end - - added_equation = true - push!(trivial_idxs, eqi) - push!(torn_eqs, eq) - push!(matched_vars, vari) - end - - # if we didn't add an equation this iteration, we won't add one next iteration - added_equation || break - end - - deleteat!(var_to_diff.primal_to_diff, matched_vars) - deleteat!(var_to_diff.diff_to_primal, matched_vars) - deleteat!(ts.structure.eq_to_diff.primal_to_diff, trivial_idxs) - deleteat!(ts.structure.eq_to_diff.diff_to_primal, trivial_idxs) - delete_srcs!(ts.structure.graph, trivial_idxs; rm_verts = true) - delete_dsts!(ts.structure.graph, matched_vars; rm_verts = true) - if ts.structure.solvable_graph !== nothing - delete_srcs!(ts.structure.solvable_graph, trivial_idxs; rm_verts = true) - delete_dsts!(ts.structure.solvable_graph, matched_vars; rm_verts = true) - end - if ts.structure.var_types !== nothing - deleteat!(ts.structure.var_types, matched_vars) - end - deleteat!(ts.fullvars, matched_vars) - deleteat!(ts.original_eqs, trivial_idxs) - ts.additional_observed = torn_eqs - sys = ts.sys - eqs = copy(get_eqs(sys)) - deleteat!(eqs, trivial_idxs) - @set! sys.eqs = eqs - ts.sys = sys - return ts -end - -function lower_order_var(dervar, t) - if isdifferential(dervar) - diffvar = arguments(dervar)[1] - elseif ModelingToolkit.isoperator(dervar, ModelingToolkit.Shift) - s = operation(dervar) - step = s.steps - 1 - vv = arguments(dervar)[1] - if step != 0 - diffvar = Shift(s.t, step)(vv) + var_reidx[i] = -1 else - diffvar = vv + nvar += 1 + var_reidx[i] = nvar + push!(new_fullvars, v) end - else - return Shift(t, -1)(dervar) - end - diffvar -end - -function shift_discrete_system(ts::TearingState) - @unpack fullvars, sys = ts - discvars = OrderedSet() - eqs = equations(sys) - for eq in eqs - vars!(discvars, eq; op = Union{Sample, Hold, Pre}) end - iv = get_iv(sys) - - discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) - for k in discvars - if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold, Pre})) - - for i in eachindex(fullvars) - fullvars[i] = StructuralTransformations.simplify_shifts(fast_substitute( - fullvars[i], discmap; operator = Union{Sample, Hold, Pre})) - end - for i in eachindex(eqs) - eqs[i] = StructuralTransformations.simplify_shifts(fast_substitute( - eqs[i], discmap; operator = Union{Sample, Hold, Pre})) + ninputs = length(inputsyms) + @set! sys.inputs = inputsyms + @set! sys.outputs = outputsyms + if ninputs == 0 + state.sys = sys + return state end - @set! ts.sys.eqs = eqs - @set! ts.fullvars = fullvars - return ts -end -using .BipartiteGraphs: Label, BipartiteAdjacencyList -struct SystemStructurePrintMatrix <: - AbstractMatrix{Union{Label, BipartiteAdjacencyList}} - bpg::BipartiteGraph - highlight_graph::Union{Nothing, BipartiteGraph} - var_to_diff::DiffGraph - eq_to_diff::DiffGraph - var_eq_matching::Union{Matching, Nothing} -end + nvars = ndsts(graph) - ninputs + new_graph = BipartiteGraph(nsrcs(graph), nvars, Val(false)) -""" -Create a SystemStructurePrintMatrix to display the contents -of the provided SystemStructure. -""" -function SystemStructurePrintMatrix(s::SystemStructure) - return SystemStructurePrintMatrix(complete(s.graph), - s.solvable_graph === nothing ? nothing : - complete(s.solvable_graph), - complete(s.var_to_diff), - complete(s.eq_to_diff), - nothing) -end -Base.size(bgpm::SystemStructurePrintMatrix) = (max(nsrcs(bgpm.bpg), ndsts(bgpm.bpg)) + 1, 9) -function compute_diff_label(diff_graph, i, symbol) - di = i - 1 <= length(diff_graph) ? diff_graph[i - 1] : nothing - return di === nothing ? Label("") : Label(string(di, symbol)) -end -function Base.getindex(bgpm::SystemStructurePrintMatrix, i::Integer, j::Integer) - checkbounds(bgpm, i, j) - if i <= 1 - return (Label.(("#", "∂ₜ", " ", " eq", "", "#", "∂ₜ", " ", " v")))[j] - elseif j == 5 - colors = Base.text_colors - return Label("|", :light_black) - elseif j == 2 - return compute_diff_label(bgpm.eq_to_diff, i, '↑') - elseif j == 3 - return compute_diff_label(invview(bgpm.eq_to_diff), i, '↓') - elseif j == 7 - return compute_diff_label(bgpm.var_to_diff, i, '↑') - elseif j == 8 - return compute_diff_label(invview(bgpm.var_to_diff), i, '↓') - elseif j == 1 - return Label((i - 1 <= length(bgpm.eq_to_diff)) ? string(i - 1) : "") - elseif j == 6 - return Label((i - 1 <= length(bgpm.var_to_diff)) ? string(i - 1) : "") - elseif j == 4 - return BipartiteAdjacencyList( - i - 1 <= nsrcs(bgpm.bpg) ? - 𝑠neighbors(bgpm.bpg, i - 1) : nothing, - bgpm.highlight_graph !== nothing && - i - 1 <= nsrcs(bgpm.highlight_graph) ? - Set(𝑠neighbors(bgpm.highlight_graph, i - 1)) : - nothing, - bgpm.var_eq_matching !== nothing && - (i - 1 <= length(invview(bgpm.var_eq_matching))) ? - invview(bgpm.var_eq_matching)[i - 1] : unassigned) - elseif j == 9 - match = unassigned - if bgpm.var_eq_matching !== nothing && i - 1 <= length(bgpm.var_eq_matching) - match = bgpm.var_eq_matching[i - 1] - isa(match, Union{Int, Unassigned}) || (match = true) # Selected Unknown + for ie in 1:nsrcs(graph) + for iv in 𝑠neighbors(graph, ie) + iv = var_reidx[iv] + iv > 0 || continue + add_edge!(new_graph, ie, iv) end - return BipartiteAdjacencyList( - i - 1 <= ndsts(bgpm.bpg) ? - 𝑑neighbors(bgpm.bpg, i - 1) : nothing, - bgpm.highlight_graph !== nothing && - i - 1 <= ndsts(bgpm.highlight_graph) ? - Set(𝑑neighbors(bgpm.highlight_graph, i - 1)) : - nothing, match) - else - @assert false end -end -function Base.show(io::IO, mime::MIME"text/plain", s::SystemStructure) - @unpack graph, solvable_graph, var_to_diff, eq_to_diff = s - if !get(io, :limit, true) || !get(io, :mtk_limit, true) - print(io, "SystemStructure with ", length(s.graph.fadjlist), " equations and ", - isa(s.graph.badjlist, Int) ? s.graph.badjlist : length(s.graph.badjlist), - " variables\n") - Base.print_matrix(io, SystemStructurePrintMatrix(s)) - else - S = incidence_matrix(s.graph, Num(Sym{Real}(:×))) - print(io, "Incidence matrix:") - show(io, mime, S) + new_var_to_diff = StateSelection.DiffGraph(nvars, true) + for (i, v) in enumerate(var_to_diff) + new_i = var_reidx[i] + (new_i < 1 || v === nothing) && continue + new_v = var_reidx[v] + @assert new_v > 0 + new_var_to_diff[new_i] = new_v end -end + @set! structure.var_to_diff = complete(new_var_to_diff) + @set! structure.graph = complete(new_graph) -struct MatchedSystemStructure - structure::SystemStructure - var_eq_matching::Matching -end - -""" -Create a SystemStructurePrintMatrix to display the contents -of the provided MatchedSystemStructure. -""" -function SystemStructurePrintMatrix(ms::MatchedSystemStructure) - return SystemStructurePrintMatrix(complete(ms.structure.graph), - complete(ms.structure.solvable_graph), - complete(ms.structure.var_to_diff), - complete(ms.structure.eq_to_diff), - complete(ms.var_eq_matching, - nsrcs(ms.structure.graph))) -end - -function Base.copy(ms::MatchedSystemStructure) - MatchedSystemStructure(Base.copy(ms.structure), Base.copy(ms.var_eq_matching)) -end - -function Base.show(io::IO, mime::MIME"text/plain", ms::MatchedSystemStructure) - s = ms.structure - @unpack graph, solvable_graph, var_to_diff, eq_to_diff = s - print(io, "Matched SystemStructure with ", length(graph.fadjlist), " equations and ", - isa(graph.badjlist, Int) ? graph.badjlist : length(graph.badjlist), - " variables\n") - Base.print_matrix(io, SystemStructurePrintMatrix(ms)) - printstyled(io, "\n\nLegend: ") - printstyled(io, "Solvable") - print(io, " | ") - printstyled(io, "(Solvable + Matched)", color = :light_yellow) - print(io, " | ") - printstyled(io, "Unsolvable", color = :light_black) - print(io, " | ") - printstyled(io, "(Unsolvable + Matched)", color = :magenta) - print(io, " | ") - printstyled(io, " ∫", color = :cyan) - printstyled(io, " SelectedState") -end - -function make_eqs_zero_equals!(ts::TearingState) - neweqs = map(enumerate(get_eqs(ts.sys))) do kvp - i, eq = kvp - isalgeq = true - for j in 𝑠neighbors(ts.structure.graph, i) - isalgeq &= invview(ts.structure.var_to_diff)[j] === nothing - end - if isalgeq - return 0 ~ eq.rhs - eq.lhs - else - return eq - end - end - copyto!(get_eqs(ts.sys), neweqs) + @set! sys.unknowns = setdiff(unknowns(sys), inputsyms) + ps = copy(parameters(sys)) + append!(ps, inputsyms) + @set! sys.ps = ps + @set! state.sys = sys + @set! state.fullvars = Vector{SymbolicT}(new_fullvars) + @set! state.structure = structure + return state end function mtkcompile!(state::TearingState; - check_consistency = true, fully_determined = true, warn_initialize_determined = true, - inputs = Any[], outputs = Any[], - disturbance_inputs = Any[], + check_consistency = true, fully_determined = true, + inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + outputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + disturbance_inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), kwargs...) if !is_time_dependent(state.sys) return _mtkcompile!(state; check_consistency, @@ -933,11 +137,9 @@ function mtkcompile!(state::TearingState; # split_system returns one or two systems and the inputs for each # mod clock inference to be binary # if it's continuous keep going, if not then error unless given trait impl in additional passes - ci = ModelingToolkit.ClockInference(state) - ci = ModelingToolkit.infer_clocks!(ci) - time_domains = merge(Dict(state.fullvars .=> ci.var_domain), - Dict(default_toterm.(state.fullvars) .=> ci.var_domain)) - tss, clocked_inputs, continuous_id, id_to_clock = ModelingToolkit.split_system(ci) + ci = MTKTearing.ClockInference(state) + ci = MTKTearing.infer_clocks!(ci) + tss, clocked_inputs, continuous_id, id_to_clock = MTKTearing.split_system(ci) if !isempty(tss) && continuous_id == 0 # do a trait check here - handle fully discrete system additional_passes = get(kwargs, :additional_passes, nothing) @@ -956,8 +158,9 @@ function mtkcompile!(state::TearingState; if length(tss) > 1 make_eqs_zero_equals!(tss[continuous_id]) # simplify as normal - sys = _mtkcompile!(tss[continuous_id]; - inputs = inputs, discrete_inputs = clocked_inputs[continuous_id], outputs, disturbance_inputs, + sys = _mtkcompile!(tss[continuous_id]; simplify, + inputs, outputs, disturbance_inputs, + discrete_inputs = OrderedSet{SymbolicT}(clocked_inputs[continuous_id]), check_consistency, fully_determined, kwargs...) additional_passes = get(kwargs, :additional_passes, nothing) @@ -980,7 +183,7 @@ function mtkcompile!(state::TearingState; if get_is_discrete(state.sys) || continuous_id == 1 && any(Base.Fix2(isoperator, Shift), state.fullvars) state.structure.only_discrete = true - state = shift_discrete_system(state) + state = MTKTearing.shift_discrete_system(state) sys = state.sys @set! sys.is_discrete = true state.sys = sys @@ -993,46 +196,88 @@ function mtkcompile!(state::TearingState; end function _mtkcompile!(state::TearingState; - check_consistency = true, fully_determined = true, warn_initialize_determined = false, - dummy_derivative = true, discrete_inputs = Any[], - inputs = Any[], outputs = Any[], - disturbance_inputs = Any[], + check_consistency = true, fully_determined = true, + dummy_derivative = true, + discrete_inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + outputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), + disturbance_inputs::OrderedSet{SymbolicT} = OrderedSet{SymbolicT}(), kwargs...) if fully_determined isa Bool check_consistency &= fully_determined else check_consistency = true end - orig_inputs = Set() - - ModelingToolkit.markio!(state, orig_inputs, discrete_inputs, [], []) - state = ModelingToolkit.inputs_to_parameters!(state, discrete_inputs) - ModelingToolkit.markio!(state, Set(), inputs, outputs, disturbance_inputs) - state = ModelingToolkit.inputs_to_parameters!(state, [inputs; disturbance_inputs]) - trivial_tearing!(state) + orig_inputs = Set{SymbolicT}() + validate_io!(state, orig_inputs, inputs, discrete_inputs, outputs, disturbance_inputs) + # ModelingToolkit.markio!(state, orig_inputs, inputs, outputs, disturbance_inputs) + union!(inputs, disturbance_inputs) + state = ModelingToolkit.inputs_to_parameters!(state, discrete_inputs, OrderedSet{SymbolicT}()) + state = ModelingToolkit.inputs_to_parameters!(state, inputs, outputs) + StateSelection.trivial_tearing!(state) sys, mm = ModelingToolkit.alias_elimination!(state; fully_determined, kwargs...) if check_consistency - fully_determined = ModelingToolkit.check_consistency( + fully_determined = StateSelection.check_consistency( state, orig_inputs; nothrow = fully_determined === nothing) end + # This phrasing avoids making the `kwcall` dynamic dispatch due to the type of a + # keyword (`mm`) being non-concrete + if mm isa CLIL.SparseMatrixCLIL{BigInt, Int} + sys = _mtkcompile_worker!(state, sys, mm; fully_determined, dummy_derivative, kwargs...) + else + sys =_mtkcompile_worker!(state, sys, mm; fully_determined, dummy_derivative, kwargs...) + end + fullunknowns = [observables(sys); unknowns(sys)] + @set! sys.observed = MTKBase.topsort_equations(observed(sys), fullunknowns) + + MTKBase.invalidate_cache!(sys) +end + +function _mtkcompile_worker!(state::TearingState, sys::System, mm::CLIL.SparseMatrixCLIL{T, Int}; + fully_determined::Bool, dummy_derivative::Bool, + kwargs...) where {T} if fully_determined && dummy_derivative sys = ModelingToolkit.dummy_derivative( - sys, state; mm, check_consistency, kwargs...) + sys, state; mm, kwargs...) elseif fully_determined - var_eq_matching = pantelides!(state; finalize = false, kwargs...) + var_eq_matching = StateSelection.pantelides!(state; finalize = false, kwargs...) sys = pantelides_reassemble(state, var_eq_matching) state = TearingState(sys) - sys, mm = ModelingToolkit.alias_elimination!(state; fully_determined, kwargs...) + sys, mm::CLIL.SparseMatrixCLIL{T, Int} = ModelingToolkit.alias_elimination!(state; fully_determined, kwargs...) sys = ModelingToolkit.dummy_derivative( - sys, state; mm, check_consistency, fully_determined, kwargs...) + sys, state; mm, fully_determined, kwargs...) else sys = ModelingToolkit.tearing( - sys, state; mm, check_consistency, fully_determined, kwargs...) + sys, state; mm, fully_determined, kwargs...) end - fullunknowns = [observables(sys); unknowns(sys)] - @set! sys.observed = ModelingToolkit.topsort_equations(observed(sys), fullunknowns) + return sys +end + +function validate_io!(state::TearingState, orig_inputs::Set{SymbolicT}, inputs::OrderedSet{SymbolicT}, + discrete_inputs::OrderedSet{SymbolicT}, outputs::OrderedSet{SymbolicT}, + disturbance_inputs::OrderedSet{SymbolicT}) + for v in state.fullvars + isinput(v) && push!(orig_inputs, v) + end + fullvars_set = OrderedSet{SymbolicT}(state.fullvars) + missings = OrderedSet{SymbolicT}() + union!(missings, inputs) + setdiff!(missings, fullvars_set) + isempty(missings) || throw(IONotFoundError("inputs", nameof(state.sys), missings)) + + union!(missings, discrete_inputs) + setdiff!(missings, fullvars_set) + isempty(missings) || throw(IONotFoundError("discrete inputs", nameof(state.sys), missings)) + + union!(missings, outputs) + setdiff!(missings, fullvars_set) + isempty(missings) || throw(IONotFoundError("outputs", nameof(state.sys), missings)) + + union!(missings, disturbance_inputs) + setdiff!(missings, fullvars_set) + isempty(missings) || throw(IONotFoundError("disturbance inputs", nameof(state.sys), missings)) - ModelingToolkit.invalidate_cache!(sys) + return nothing end struct DifferentiatedVariableNotUnknownError <: Exception diff --git a/src/systems/validation.jl b/src/systems/validation.jl deleted file mode 100644 index 6372be1100..0000000000 --- a/src/systems/validation.jl +++ /dev/null @@ -1,298 +0,0 @@ -module UnitfulUnitCheck - -using ..ModelingToolkit, Symbolics, SciMLBase, Unitful, RecursiveArrayTools -using ..ModelingToolkit: ValidationError, - ModelingToolkit, Connection, instream, JumpType, VariableUnit, AnalysisPoint, - get_systems, - Conditional, Comparison -using JumpProcesses: MassActionJump, ConstantRateJump, VariableRateJump -using Symbolics: Symbolic, value, issym, isadd, ismul, ispow -const MT = ModelingToolkit - -Base.:*(x::Union{Num, Symbolic}, y::Unitful.AbstractQuantity) = x * y -Base.:/(x::Union{Num, Symbolic}, y::Unitful.AbstractQuantity) = x / y - -""" -Throw exception on invalid unit types, otherwise return argument. -""" -function screen_unit(result) - result isa Unitful.Unitlike || - throw(ValidationError("Unit must be a subtype of Unitful.Unitlike, not $(typeof(result)).")) - result isa Unitful.ScalarUnits || - throw(ValidationError("Non-scalar units such as $result are not supported. Use a scalar unit instead.")) - result == u"°" && - throw(ValidationError("Degrees are not supported. Use radians instead.")) - result -end - -""" -Test unit equivalence. - -Example of implemented behavior: - -```julia -using ModelingToolkit, Unitful -MT = ModelingToolkit -@parameters γ P [unit = u"MW"] E [unit = u"kJ"] τ [unit = u"ms"] -@test MT.equivalent(u"MW", u"kJ/ms") # Understands prefixes -@test !MT.equivalent(u"m", u"cm") # Units must be same magnitude -@test MT.equivalent(MT.get_unit(P^γ), MT.get_unit((E / τ)^γ)) # Handles symbolic exponents -``` -""" -equivalent(x, y) = isequal(1 * x, 1 * y) -const unitless = Unitful.unit(1) - -""" -Find the unit of a symbolic item. -""" -get_unit(x::Real) = unitless -get_unit(x::Unitful.Quantity) = screen_unit(Unitful.unit(x)) -get_unit(x::AbstractArray) = map(get_unit, x) -get_unit(x::Num) = get_unit(value(x)) -function get_unit(x::Union{Symbolics.ArrayOp, Symbolics.Arr, Symbolics.CallWithMetadata}) - get_literal_unit(x) -end -get_unit(op::Differential, args) = get_unit(args[1]) / get_unit(op.x) -get_unit(op::typeof(getindex), args) = get_unit(args[1]) -get_unit(x::SciMLBase.NullParameters) = unitless -get_unit(op::typeof(instream), args) = get_unit(args[1]) - -get_literal_unit(x) = screen_unit(getmetadata(x, VariableUnit, unitless)) - -function get_unit(op, args) # Fallback - result = op(1 .* get_unit.(args)...) - try - unit(result) - catch - throw(ValidationError("Unable to get unit for operation $op with arguments $args.")) - end -end - -function get_unit(op::Integral, args) - unit = 1 - if op.domain.variables isa Vector - for u in op.domain.variables - unit *= get_unit(u) - end - else - unit *= get_unit(op.domain.variables) - end - return get_unit(args[1]) * unit -end - -function get_unit(op::Conditional, args) - terms = get_unit.(args) - terms[1] == unitless || - throw(ValidationError(", in $op, [$(terms[1])] is not dimensionless.")) - equivalent(terms[2], terms[3]) || - throw(ValidationError(", in $op, units [$(terms[2])] and [$(terms[3])] do not match.")) - return terms[2] -end - -function get_unit(op::typeof(Symbolics._mapreduce), args) - if args[2] == + - get_unit(args[3]) - else - throw(ValidationError("Unsupported array operation $op")) - end -end - -function get_unit(op::Comparison, args) - terms = get_unit.(args) - equivalent(terms[1], terms[2]) || - throw(ValidationError(", in comparison $op, units [$(terms[1])] and [$(terms[2])] do not match.")) - return unitless -end - -function get_unit(x::Symbolic) - if issym(x) - get_literal_unit(x) - elseif isadd(x) - terms = get_unit.(arguments(x)) - firstunit = terms[1] - for other in terms[2:end] - termlist = join(map(repr, terms), ", ") - equivalent(other, firstunit) || - throw(ValidationError(", in sum $x, units [$termlist] do not match.")) - end - return firstunit - elseif ispow(x) - pargs = arguments(x) - base, expon = get_unit.(pargs) - @assert expon isa Unitful.DimensionlessUnits - if base == unitless - unitless - else - pargs[2] isa Number ? base^pargs[2] : (1 * base)^pargs[2] - end - elseif iscall(x) - op = operation(x) - 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 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 `iscall` is true - throw(ArgumentError("Unsupported value $x.")) - end -end - -""" -Get unit of term, returning nothing & showing warning instead of throwing errors. -""" -function safe_get_unit(term, info) - side = nothing - try - side = get_unit(term) - catch err - if err isa Unitful.DimensionError - @warn("$info: $(err.x) and $(err.y) are not dimensionally compatible.") - elseif err isa ValidationError - @warn(info*err.message) - elseif err isa MethodError - @warn("$info: no method matching $(err.f) for arguments $(typeof.(err.args)).") - else - rethrow() - end - end - side -end - -function _validate(terms::Vector, labels::Vector{String}; info::String = "") - valid = true - first_unit = nothing - first_label = nothing - for (term, label) in zip(terms, labels) - equnit = safe_get_unit(term, info * label) - if equnit === nothing - valid = false - elseif !isequal(term, 0) - if first_unit === nothing - first_unit = equnit - first_label = label - elseif !equivalent(first_unit, equnit) - valid = false - @warn("$info: units [$(first_unit)] for $(first_label) and [$(equnit)] for $(label) do not match.") - end - end - end - valid -end - -function _validate(ap::AnalysisPoint; info::String = "") - is_valid = false - if (ap.outputs == nothing) - is_valid = true - else - conn_eq = connect(ap.input, ap.outputs...) - is_valid = _validate(conn_eq.rhs, info=info) - end - return is_valid -end - -function _validate(conn::Connection; info::String = "") - valid = true - syss = get_systems(conn) - sys = first(syss) - unks = unknowns(sys) - for i in 2:length(syss) - s = syss[i] - _unks = unknowns(s) - if length(unks) != length(_unks) - valid = false - @warn("$info: connected systems $(nameof(sys)) and $(nameof(s)) have $(length(unks)) and $(length(_unks)) unknowns, cannot connect.") - continue - end - for (i, x) in enumerate(unks) - j = findfirst(isequal(x), _unks) - if j == nothing - valid = false - @warn("$info: connected systems $(nameof(sys)) and $(nameof(s)) do not have the same unknowns.") - else - aunit = safe_get_unit(x, info * string(nameof(sys)) * "#$i") - bunit = safe_get_unit(_unks[j], info * string(nameof(s)) * "#$j") - if !equivalent(aunit, bunit) - valid = false - @warn("$info: connected system unknowns $x and $(_unks[j]) have mismatched units.") - end - end - end - end - valid -end - -function validate(jump::Union{MT.VariableRateJump, - MT.ConstantRateJump}, t::Symbolic; - info::String = "") - newinfo = replace(info, "eq." => "jump") - _validate([jump.rate, 1 / t], ["rate", "1/t"], info = newinfo) && # Assuming the rate is per time units - validate(jump.affect!, info = newinfo) -end - -function validate(jump::MT.MassActionJump, t::Symbolic; info::String = "") - left_symbols = [x[1] for x in jump.reactant_stoch] #vector of pairs of symbol,int -> vector symbols - net_symbols = [x[1] for x in jump.net_stoch] - all_symbols = vcat(left_symbols, net_symbols) - allgood = _validate(all_symbols, string.(all_symbols); info) - n = sum(x -> x[2], jump.reactant_stoch, init = 0) - base_unitful = all_symbols[1] #all same, get first - allgood && _validate([jump.scaled_rates, 1 / (t * base_unitful^n)], - ["scaled_rates", "1/(t*reactants^$n))"]; info) -end - -function validate(jumps::Vector{JumpType}, t::Symbolic) - labels = ["in Mass Action Jumps,", "in Constant Rate Jumps,", "in Variable Rate Jumps,"] - majs = filter(x -> x isa MassActionJump, jumps) - crjs = filter(x -> x isa ConstantRateJump, jumps) - vrjs = filter(x -> x isa VariableRateJump, jumps) - splitjumps = [majs, crjs, vrjs] - all([validate(js, t; info) for (js, info) in zip(splitjumps, labels)]) -end - -function validate(eq::MT.Equation; info::String = "") - if typeof(eq.lhs) <: Union{AnalysisPoint, Connection} - _validate(eq.rhs; info) - else - _validate([eq.lhs, eq.rhs], ["left", "right"]; info) - end -end -function validate(eq::MT.Equation, - term::Union{Symbolic, Unitful.Quantity, Num}; info::String = "") - _validate([eq.lhs, eq.rhs, term], ["left", "right", "noise"]; info) -end -function validate(eq::MT.Equation, terms::Vector; info::String = "") - _validate(vcat([eq.lhs, eq.rhs], terms), - vcat(["left", "right"], "noise #" .* string.(1:length(terms))); info) -end - -""" -Returns true iff units of equations are valid. -""" -function validate(eqs::Vector; info::String = "") - all([validate(eqs[idx], info = info * " in eq. #$idx") for idx in 1:length(eqs)]) -end -function validate(eqs::Vector, noise::Vector; info::String = "") - all([validate(eqs[idx], noise[idx], info = info * " in eq. #$idx") - for idx in 1:length(eqs)]) -end -function validate(eqs::Vector, noise::Matrix; info::String = "") - all([validate(eqs[idx], noise[idx, :], info = info * " in eq. #$idx") - for idx in 1:length(eqs)]) -end -function validate(eqs::Vector, term::Symbolic; info::String = "") - all([validate(eqs[idx], term, info = info * " in eq. #$idx") for idx in 1:length(eqs)]) -end -validate(term::Symbolics.SymbolicUtils.Symbolic) = safe_get_unit(term, "") !== nothing - -""" -Throws error if units of equations are invalid. -""" -function MT.check_units(::Val{:Unitful}, eqs...) - validate(eqs...) || - throw(ValidationError("Some equations had invalid units. See warnings for details.")) -end - -end # module diff --git a/test/analysis_points.jl b/test/analysis_points.jl deleted file mode 100644 index 77ad59d261..0000000000 --- a/test/analysis_points.jl +++ /dev/null @@ -1,770 +0,0 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, AbstractSystem -import ModelingToolkit as MTK -using ModelingToolkitStandardLibrary -using ModelingToolkitStandardLibrary.Blocks -using ModelingToolkitStandardLibrary.Mechanical.Rotational -using ControlSystemsBase -import ControlSystemsBase as CS -using OrdinaryDiffEq, LinearAlgebra -using Test -using Symbolics: NAMESPACE_SEPARATOR -using Unitful - -@testset "AnalysisPoint is ignored when verifying units" begin - # no units first - @mtkmodel FirstOrderTest begin - @components begin - in = Step() - fb = Feedback() - fo = SecondOrder(k = 1, w = 1, d = 0.1) - end - @equations begin - connect(in.output, :u, fb.input1) - connect(fb.output, :e, fo.input) - connect(fo.output, :y, fb.input2) - end - end - @named model = FirstOrderTest() - @test model isa System - - @connector function UnitfulOutput(; name) - vars = @variables begin - u(t), [unit=u"m", output=true] - end - return System(Equation[], t, vars, []; name) - end - @connector function UnitfulInput(; name) - vars = @variables begin - u(t), [unit=u"m", input=true] - end - return System(Equation[], t, vars, []; name) - end - @component function UnitfulBlock(; name) - pars = @parameters begin - offset, [unit=u"m"] - start_time - height, [unit=u"m"] - duration - end - systems = @named begin - output = UnitfulOutput() - end - eqs = [ - output.u ~ offset + height*(0.5 + (1/pi)*atan(1e5*(t - start_time))) - ] - return System(eqs, t, [], pars; systems, name) - end - @mtkmodel TestAPAroundUnits begin - @components begin - input = UnitfulInput() - end - @variables begin - output(t), [output=true, unit=u"m^2"] - end - @components begin - ub = UnitfulBlock() - end - @equations begin - connect(ub.output, :ap, input) - output ~ input.u^2 - end - end - @named sys = TestAPAroundUnits() - @test sys isa System - - @mtkmodel TestAPWithNoOutputs begin - @components begin - input = UnitfulInput() - end - @variables begin - output(t), [output=true, unit=u"m^2"] - end - @components begin - ub = UnitfulBlock() - end - @equations begin - connect(ub.output, :ap, input) - output ~ input.u^2 - end - end - @named sys2 = TestAPWithNoOutputs() - @test sys2 isa System -end - -@testset "AnalysisPoint is lowered to `connect`" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) - - ap = AnalysisPoint(:plant_input) - eqs = [connect(P.output, C.input) - connect(C.output, ap, P.input)] - sys_ap = System(eqs, t, systems = [P, C], name = :hej) - sys_ap2 = @test_nowarn expand_connections(sys_ap) - - @test all(eq -> !(eq.lhs isa AnalysisPoint), equations(sys_ap2)) - - eqs = [connect(P.output, C.input) - connect(C.output, P.input)] - sys_normal = System(eqs, t, systems = [P, C], name = :hej) - sys_normal2 = @test_nowarn expand_connections(sys_normal) - - @test issetequal(equations(sys_ap2), equations(sys_normal2)) -end - -@testset "Inverse causality throws a warning" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) - - ap = AnalysisPoint(:plant_input) - @test_warn ["1-th argument", "plant_input", "not a output"] connect( - P.input, ap, C.output) - @test_nowarn connect(P.input, ap, C.output; verbose = false) -end - -# also tests `connect(input, name::Symbol, outputs...)` syntax -@testset "AnalysisPoint is accessible via `getproperty`" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) - - eqs = [connect(P.output, C.input), connect(C.output, :plant_input, P.input)] - sys_ap = System(eqs, t, systems = [P, C], name = :hej) - ap2 = @test_nowarn sys_ap.plant_input - @test nameof(ap2) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) - @named sys = System(Equation[], t; systems = [sys_ap]) - ap3 = @test_nowarn sys.hej.plant_input - @test nameof(ap3) == Symbol(join(["sys", "hej", "plant_input"], NAMESPACE_SEPARATOR)) - csys = complete(sys) - ap4 = csys.hej.plant_input - @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) - nsys = toggle_namespacing(sys, false) - ap5 = nsys.hej.plant_input - @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) -end - -### Ported from MTKStdlib - -@named P = FirstOrder(k = 1, T = 1) -@named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = -1) - -ap = AnalysisPoint(:plant_input) -eqs = [connect(P.output, C.input), connect(C.output, ap, P.input)] -sys = System(eqs, t, systems = [P, C], name = :hej) -@named nested_sys = System(Equation[], t; systems = [sys]) -nonamespace_sys = toggle_namespacing(nested_sys, false) - -@testset "simplifies and solves" begin - ssys = mtkcompile(sys) - prob = ODEProblem(ssys, [P.x => 1], (0, 10)) - sol = solve(prob, Rodas5()) - @test norm(sol.u[1]) >= 1 - @test norm(sol.u[end]) < 1e-6 # This fails without the feedback through C -end - -test_cases = [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("nonamespace", nonamespace_sys, nonamespace_sys.hej.plant_input), - ("inner - Symbol", sys, :plant_input), - ("nested - Symbol", nested_sys, nameof(sys.plant_input)) -] - -@testset "get_sensitivity - $name" for (name, sys, ap) in test_cases - matrices, _ = get_sensitivity(sys, ap) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 1 -end - -@testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases - matrices, _ = get_comp_sensitivity(sys, ap) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == 1 # both positive or negative - @test matrices.D[] == 0 -end - -#= -# Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. -using ControlSystemsBase -P = tf(1.0, [1, 1]) -C = 1 # Negative feedback assumed in ControlSystems -S = sensitivity(P, C) # or feedback(1, P*C) -T = comp_sensitivity(P, C) # or feedback(P*C) -=# - -@testset "get_looptransfer - $name" for (name, sys, ap) in test_cases - matrices, _ = get_looptransfer(sys, ap) - @test matrices.A[] == -1 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 0 -end - -#= -# Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. -using ControlSystemsBase -P = tf(1.0, [1, 1]) -C = -1 -L = P*C -=# - -@testset "open_loop - $name" for (name, sys, ap) in test_cases - open_sys, (du, u) = open_loop(sys, ap) - matrices, _ = linearize(open_sys, [du], [u]) - @test matrices.A[] == -1 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 0 -end - -# Multiple analysis points - -eqs = [connect(P.output, :plant_output, C.input) - connect(C.output, :plant_input, P.input)] -sys = System(eqs, t, systems = [P, C], name = :hej) -@named nested_sys = System(Equation[], t; systems = [sys]) - -test_cases = [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - Symbol", sys, :plant_input), - ("nested - Symbol", nested_sys, nameof(sys.plant_input)) -] - -@testset "get_sensitivity - $name" for (name, sys, ap) in test_cases - matrices, _ = get_sensitivity(sys, ap) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == -1 # either one negative - @test matrices.D[] == 1 -end - -@testset "linearize - $name" for (name, sys, inputap, outputap) in [ - ("inner", sys, sys.plant_input, sys.plant_output), - ("nested", nested_sys, nested_sys.hej.plant_input, nested_sys.hej.plant_output) -] - inputname = Symbol(join( - MTK.namespace_hierarchy(nameof(inputap))[2:end], NAMESPACE_SEPARATOR)) - outputname = Symbol(join( - MTK.namespace_hierarchy(nameof(outputap))[2:end], NAMESPACE_SEPARATOR)) - @testset "input - $(typeof(input)), output - $(typeof(output))" for (input, output) in [ - (inputap, outputap), - (inputname, outputap), - (inputap, outputname), - (inputname, outputname), - (inputap, [outputap]), - (inputname, [outputap]), - (inputap, [outputname]), - (inputname, [outputname]) - ] - matrices, _ = linearize(sys, input, output) - # Result should be the same as feedpack(P, 1), i.e., the closed-loop transfer function from plant input to plant output - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == 1 # both positive - @test matrices.D[] == 0 - end -end - -@testset "linearize - variable output - $name" for (name, sys, input, output) in [ - ("inner", sys, sys.plant_input, P.output.u), - ("nested", nested_sys, nested_sys.hej.plant_input, sys.P.output.u) -] - matrices, _ = linearize(sys, input, [output]) - @test matrices.A[] == -2 - @test matrices.B[] * matrices.C[] == 1 # both positive - @test matrices.D[] == 0 -end - -@testset "Complicated model" begin - # 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 System(eqs, t; - systems = [ - torque, - inertia1, - inertia2, - spring, - damper, - u - ], - name) - end - System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) - end - - @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 = System(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 - ]) - - sys = mtkcompile(closed_loop) - prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) - sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) - - matrices, ssys = linearize(closed_loop, :r, :y) - lsys = ss(matrices...) |> sminreal - @test lsys.nx == 8 - - stepres = ControlSystemsBase.step(c2d(lsys, 0.001), 4) - @test Array(stepres.y[:])≈Array(sol(0:0.001:4, idxs = model.inertia2.phi)) rtol=1e-4 - - matrices, ssys = get_sensitivity(closed_loop, :y) - So = ss(matrices...) - - matrices, ssys = get_sensitivity(closed_loop, :u) - Si = ss(matrices...) - - @test tf(So) ≈ tf(Si) -end - -@testset "Duplicate `connect` statements across subsystems with AP transforms - standard `connect`" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) - @named add = Blocks.Add(k2 = -1) - - eqs = [connect(P.output, :plant_output, add.input2) - connect(add.output, C.input) - connect(C.output, P.input)] - - sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) - - @named r = Constant(k = 1) - @named F = FirstOrder(k = 1, T = 3) - - eqs = [connect(r.output, F.input) - connect(sys_inner.P.output, sys_inner.add.input2) - connect(sys_inner.C.output, :plant_input, sys_inner.P.input) - connect(F.output, sys_inner.add.input1)] - sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) - - # test first that the mtkcompile works correctly - ssys = mtkcompile(sys_outer) - prob = ODEProblem(ssys, Pair[], (0, 10)) - @test_nowarn solve(prob, Rodas5()) - - matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) - lsys = sminreal(ss(matrices...)) - @test lsys.A[] == -2 - @test lsys.B[] * lsys.C[] == -1 # either one negative - @test lsys.D[] == 1 - - matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) - lsyso = sminreal(ss(matrices_So...)) - @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems -end - -@testset "Duplicate `connect` statements across subsystems with AP transforms - causal variable `connect`" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) - @named add = Blocks.Add(k2 = -1) - - eqs = [connect(P.output.u, :plant_output, add.input2.u) - connect(add.output, C.input) - connect(C.output.u, P.input.u)] - - sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) - - @named r = Constant(k = 1) - @named F = FirstOrder(k = 1, T = 3) - - eqs = [connect(r.output, F.input) - connect(sys_inner.P.output.u, sys_inner.add.input2.u) - connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) - connect(F.output, sys_inner.add.input1)] - sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) - - # test first that the mtkcompile works correctly - ssys = mtkcompile(sys_outer) - prob = ODEProblem(ssys, Pair[], (0, 10)) - @test_nowarn solve(prob, Rodas5()) - - matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) - lsys = sminreal(ss(matrices...)) - @test lsys.A[] == -2 - @test lsys.B[] * lsys.C[] == -1 # either one negative - @test lsys.D[] == 1 - - matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) - lsyso = sminreal(ss(matrices_So...)) - @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems -end - -@testset "Duplicate `connect` statements across subsystems with AP transforms - mixed `connect`" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) - @named add = Blocks.Add(k2 = -1) - - eqs = [connect(P.output.u, :plant_output, add.input2.u) - connect(add.output, C.input) - connect(C.output, P.input)] - - sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) - - @named r = Constant(k = 1) - @named F = FirstOrder(k = 1, T = 3) - - eqs = [connect(r.output, F.input) - connect(sys_inner.P.output, sys_inner.add.input2) - connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) - connect(F.output, sys_inner.add.input1)] - sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) - - # test first that the mtkcompile works correctly - ssys = mtkcompile(sys_outer) - prob = ODEProblem(ssys, Pair[], (0, 10)) - @test_nowarn solve(prob, Rodas5()) - - matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) - lsys = sminreal(ss(matrices...)) - @test lsys.A[] == -2 - @test lsys.B[] * lsys.C[] == -1 # either one negative - @test lsys.D[] == 1 - - matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) - lsyso = sminreal(ss(matrices_So...)) - @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems -end - -@testset "multilevel system with loop openings" begin - @named P_inner = FirstOrder(k = 1, T = 1) - @named feedback = Feedback() - @named ref = Step() - @named sys_inner = System( - [connect(P_inner.output, :y, feedback.input2) - connect(feedback.output, :u, P_inner.input) - connect(ref.output, :r, feedback.input1)], - t, - systems = [P_inner, feedback, ref]) - - P_not_broken, _ = linearize(sys_inner, :u, :y) - @test P_not_broken.A[] == -2 - P_broken, ssys = linearize(sys_inner, :u, :y, loop_openings = [:u]) - @test isequal(defaults(ssys)[ssys.d_u], ssys.feedback.output.u) - @test P_broken.A[] == -1 - P_broken, ssys = linearize(sys_inner, :u, :y, loop_openings = [:y]) - @test isequal(defaults(ssys)[ssys.d_y], ssys.P_inner.output.u) - @test P_broken.A[] == -1 - - Sinner = sminreal(ss(get_sensitivity(sys_inner, :u)[1]...)) - - @named sys_inner = System( - [connect(P_inner.output, :y, feedback.input2) - connect(feedback.output, :u, P_inner.input)], - t, - systems = [P_inner, feedback]) - - @named P_outer = FirstOrder(k = rand(), T = rand()) - - @named sys_outer = System( - [connect(sys_inner.P_inner.output, :y2, P_outer.input) - connect(P_outer.output, :u2, sys_inner.feedback.input1)], - t, - systems = [P_outer, sys_inner]) - - Souter = sminreal(ss(get_sensitivity(sys_outer, sys_outer.sys_inner.u)[1]...)) - - Sinner2 = sminreal(ss(get_sensitivity( - sys_outer, sys_outer.sys_inner.u, loop_openings = [:y2])[1]...)) - - @test Sinner.nx == 1 - @test Sinner == Sinner2 - @test Souter.nx == 2 -end - -@testset "sensitivities in multivariate signals" begin - A = [-0.994 -0.0794; -0.006242 -0.0134] - B = [-0.181 -0.389; 1.1 1.12] - C = [1.74 0.72; -0.33 0.33] - D = [0.0 0.0; 0.0 0.0] - @named P = Blocks.StateSpace(A, B, C, D) - Pss = CS.ss(A, B, C, D) - - A = [-0.097;;] - B = [-0.138 -1.02] - C = [-0.076; 0.09;;] - D = [0.0 0.0; 0.0 0.0] - @named K = Blocks.StateSpace(A, B, C, D) - Kss = CS.ss(A, B, C, D) - - eqs = [connect(P.output, :plant_output, K.input) - connect(K.output, :plant_input, P.input)] - sys = System(eqs, t, systems = [P, K], name = :hej) - - matrices, _ = get_sensitivity(sys, :plant_input) - S = CS.feedback(I(2), Kss * Pss, pos_feedback = true) - - @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(S) - - matrices, _ = get_comp_sensitivity(sys, :plant_input) - T = -CS.feedback(Kss * Pss, I(2), pos_feedback = true) - - # bodeplot([ss(matrices...), T]) - @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(T) - - matrices, _ = get_looptransfer( - sys, :plant_input) - L = Kss * Pss - @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(L) - - matrices, _ = linearize(sys, AnalysisPoint(:plant_input), :plant_output) - G = CS.feedback(Pss, Kss, pos_feedback = true) - @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(G) -end - -@testset "multiple analysis points" begin - @named P = FirstOrder(k = 1, T = 1) - @named C = ModelingToolkitStandardLibrary.Blocks.Gain(; k = 1) - @named add = Blocks.Add(k2 = -1) - - eqs = [connect(P.output, :plant_output, add.input2) - connect(add.output, C.input) - connect(C.output, :plant_input, P.input)] - - sys_inner = System(eqs, t, systems = [P, C, add], name = :inner) - - @named r = Constant(k = 1) - @named F = FirstOrder(k = 1, T = 3) - - eqs = [connect(r.output, F.input) - connect(F.output, sys_inner.add.input1)] - sys_outer = System(eqs, t, systems = [F, sys_inner, r], name = :outer) - - matrices, - _ = get_sensitivity( - sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) - - Ps = tf(1, [1, 1]) |> ss - Cs = tf(1) |> ss - - G = CS.ss(matrices...) |> sminreal - Si = CS.feedback(1, Cs * Ps) - @test tf(G[1, 1]) ≈ tf(Si) - - So = CS.feedback(1, Ps * Cs) - @test tf(G[2, 2]) ≈ tf(So) - @test tf(G[1, 2]) ≈ tf(-CS.feedback(Cs, Ps)) - @test tf(G[2, 1]) ≈ tf(CS.feedback(Ps, Cs)) - - matrices, - _ = get_comp_sensitivity( - sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) - - G = CS.ss(matrices...) |> sminreal - Ti = CS.feedback(Cs * Ps) - @test tf(G[1, 1]) ≈ tf(Ti) - - To = CS.feedback(Ps * Cs) - @test tf(G[2, 2]) ≈ tf(To) - @test tf(G[1, 2]) ≈ tf(CS.feedback(Cs, Ps)) # The negative sign appears in a confusing place due to negative feedback not happening through Ps - @test tf(G[2, 1]) ≈ tf(-CS.feedback(Ps, Cs)) - - # matrices, _ = get_looptransfer(sys_outer, [:inner_plant_input, :inner_plant_output]) - matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_input) - L = CS.ss(matrices...) |> sminreal - @test tf(L) ≈ -tf(Cs * Ps) - - matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_output) - L = CS.ss(matrices...) |> sminreal - @test tf(L[1, 1]) ≈ -tf(Ps * Cs) - - # Calling looptransfer like below is not the intended way, but we can work out what it should return if we did so it remains a valid test - matrices, - _ = get_looptransfer( - sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) - L = CS.ss(matrices...) |> sminreal - @test tf(L[1, 1]) ≈ tf(0) - @test tf(L[2, 2]) ≈ tf(0) - @test sminreal(L[1, 2]) ≈ ss(-1) - @test tf(L[2, 1]) ≈ tf(Ps) - - matrices, - _ = linearize( - sys_outer, [sys_outer.inner.plant_input], [nameof(sys_inner.plant_output)]) - G = CS.ss(matrices...) |> sminreal - @test tf(G) ≈ tf(CS.feedback(Ps, Cs)) -end - -function normal_test_system() - @named F1 = FirstOrder(k = 1, T = 1) - @named F2 = FirstOrder(k = 1, T = 1) - @named add = Blocks.Add(k1 = 1, k2 = 2) - @named back = Feedback() - - eqs_normal = [connect(back.output, :ap, F1.input) - connect(back.output, F2.input) - connect(F1.output, add.input1) - connect(F2.output, add.input2) - connect(add.output, back.input2)] - @named normal_inner = System(eqs_normal, t; systems = [F1, F2, add, back]) - - @named step = Step() - eqs2_normal = [ - connect(step.output, normal_inner.back.input1) - ] - @named sys_normal = System(eqs2_normal, t; systems = [normal_inner, step]) -end - -sys_normal = normal_test_system() - -prob = ODEProblem(mtkcompile(sys_normal), [], (0.0, 10.0)) -@test SciMLBase.successful_retcode(solve(prob, Rodas5P())) -matrices_normal, _ = get_sensitivity(sys_normal, sys_normal.normal_inner.ap) - -@testset "Analysis point overriding part of connection - normal connect" begin - @named F1 = FirstOrder(k = 1, T = 1) - @named F2 = FirstOrder(k = 1, T = 1) - @named add = Blocks.Add(k1 = 1, k2 = 2) - @named back = Feedback() - - eqs = [connect(back.output, F1.input, F2.input) - connect(F1.output, add.input1) - connect(F2.output, add.input2) - connect(add.output, back.input2)] - @named inner = System(eqs, t; systems = [F1, F2, add, back]) - - @named step = Step() - eqs2 = [connect(step.output, inner.back.input1) - connect(inner.back.output, :ap, inner.F1.input)] - @named sys = System(eqs2, t; systems = [inner, step]) - - prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) - @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) - - matrices, _ = get_sensitivity(sys, sys.ap) - @test matrices == matrices_normal -end - -@testset "Analysis point overriding part of connection - variable connect" begin - @named F1 = FirstOrder(k = 1, T = 1) - @named F2 = FirstOrder(k = 1, T = 1) - @named add = Blocks.Add(k1 = 1, k2 = 2) - @named back = Feedback() - - eqs = [connect(back.output.u, F1.input.u, F2.input.u) - connect(F1.output, add.input1) - connect(F2.output, add.input2) - connect(add.output, back.input2)] - @named inner = System(eqs, t; systems = [F1, F2, add, back]) - - @named step = Step() - eqs2 = [connect(step.output, inner.back.input1) - connect(inner.back.output.u, :ap, inner.F1.input.u)] - @named sys = System(eqs2, t; systems = [inner, step]) - - prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) - @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) - - matrices, _ = get_sensitivity(sys, sys.ap) - @test matrices == matrices_normal -end - -@testset "Analysis point overriding part of connection - mixed connect" begin - @named F1 = FirstOrder(k = 1, T = 1) - @named F2 = FirstOrder(k = 1, T = 1) - @named add = Blocks.Add(k1 = 1, k2 = 2) - @named back = Feedback() - - eqs = [connect(back.output, F1.input, F2.input) - connect(F1.output, add.input1) - connect(F2.output, add.input2) - connect(add.output, back.input2)] - @named inner = System(eqs, t; systems = [F1, F2, add, back]) - - @named step = Step() - eqs2 = [connect(step.output, inner.back.input1) - connect(inner.back.output.u, :ap, inner.F1.input.u)] - @named sys = System(eqs2, t; systems = [inner, step]) - - prob = ODEProblem(mtkcompile(sys), [], (0.0, 10.0)) - @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) - - matrices, _ = get_sensitivity(sys, sys.ap) - @test matrices == matrices_normal -end - -@testset "Ignored analysis points only affect relevant connection sets" begin - m1 = 1 - m2 = 1 - k = 1000 # Spring stiffness - c = 10 # Damping coefficient - - @named inertia1 = Inertia(; J = m1, phi = 0, w = 0) - @named inertia2 = Inertia(; J = m2, phi = 0, w = 0) - - @named spring = Spring(; c = k) - @named damper = Damper(; d = c) - - @named torque = Torque(use_support = false) - - 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 @named model = System( - eqs, t; systems = [torque, inertia1, inertia2, spring, damper, u]) - end - System(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) - end - - @named r = Step(start_time = 1) - @named pid = LimPID(k = 400, Ti = 0.5, Td = 1, u_max = 350) - @named filt = SecondOrder(d = 0.9, w = 10, x = 0, xd = 0) - @named sensor = AngleSensor() - @named add = Add() # To add the feedback and feedforward control signals - model = SystemModel() - @named inverse_model = SystemModel() - @named inverse_sensor = AngleSensor() - connections = [connect(r.output, :r, filt.input) # Name connection r to form an analysis point - connect(inverse_model.inertia1.flange_b, inverse_sensor.flange) # Attach the inverse sensor to the inverse model - connect(filt.output, pid.reference, inverse_sensor.phi) # the filtered reference now goes to both the PID controller and the inverse model input - connect(inverse_model.torque.tau, add.input1) - connect(pid.ctr_output, add.input2) - connect(add.output, :u, model.torque.tau) # Name connection u to form an analysis point - connect(model.inertia1.flange_b, sensor.flange) - connect(sensor.phi, :y, pid.measurement)] - closed_loop = System(connections, t, - systems = [model, inverse_model, pid, filt, sensor, inverse_sensor, r, add], - name = :closed_loop) - # just ensure the system simplifies - mats, _ = get_sensitivity(closed_loop, :y) - S = CS.ss(mats...) - fr = CS.freqrespv(S, [0.01, 1, 100]) - # https://github.com/SciML/ModelingToolkit.jl/pull/3469 - reference_fr = ComplexF64[-1.2505330104772838e-11 - 2.500062613816021e-9im, - -0.0024688370221621625 - 0.002279011866413123im, - 1.8100018764334602 + 0.3623845793211718im] - @test isapprox(fr, reference_fr) -end diff --git a/test/clock.jl b/test/clock.jl index 6eaed52b73..74becdd710 100644 --- a/test/clock.jl +++ b/test/clock.jl @@ -1,12 +1,16 @@ using ModelingToolkit, Test, Setfield, OrdinaryDiffEq, DiffEqCallbacks +using OrderedCollections using ModelingToolkit: ContinuousClock using ModelingToolkit: t_nounits as t, D_nounits as D using Symbolics, SymbolicUtils +using Symbolics: SymbolicT, VartypeT +using SciCompDSL +import ModelingToolkitTearing as MTKTearing function infer_clocks(sys) ts = TearingState(sys) - ci = ModelingToolkit.ClockInference(ts) - ModelingToolkit.infer_clocks!(ci), Dict(ci.ts.fullvars .=> ci.var_domain) + ci = MTKTearing.ClockInference(ts) + MTKTearing.infer_clocks!(ci), Dict(ci.ts.fullvars .=> ci.var_domain) end @info "Testing hybrid system" @@ -65,12 +69,12 @@ By inference: ci, varmap = infer_clocks(sys) eqmap = ci.eq_domain -tss, inputs, continuous_id = ModelingToolkit.split_system(deepcopy(ci)) +tss, inputs, continuous_id = MTKTearing.split_system(deepcopy(ci)) sss = ModelingToolkit._mtkcompile!( - deepcopy(tss[continuous_id]), inputs = inputs[continuous_id], outputs = []) + deepcopy(tss[continuous_id]), inputs = OrderedSet{SymbolicT}(inputs[continuous_id])) @test equations(sss) == [D(x) ~ u - x] sss = ModelingToolkit._mtkcompile!( - deepcopy(tss[1]), inputs = inputs[1], outputs = []) + deepcopy(tss[1]), inputs = OrderedSet{SymbolicT}(inputs[1])) @test isempty(equations(sss)) d = Clock(dt) k = ShiftIndex(d) @@ -127,9 +131,9 @@ eqs = [yd ~ Sample(dt)(y) initialization_eqs = [y ~ z, x ~ 1] @named sys = System(eqs, t; initialization_eqs) ts = TearingState(sys) - ci = ModelingToolkit.ClockInference(ts) + ci = MTKTearing.ClockInference(ts) @test length(ci.init_eq_domain) == 2 - ModelingToolkit.infer_clocks!(ci) + MTKTearing.infer_clocks!(ci) canonical_eqs = map(eqs) do eq if iscall(eq.lhs) && operation(eq.lhs) isa Differential return eq @@ -148,12 +152,12 @@ eqs = [yd ~ Sample(dt)(y) end struct ZeroArgOp <: Symbolics.Operator end -(o::ZeroArgOp)() = Symbolics.Term{Bool}(o, Any[]) +(o::ZeroArgOp)() = SymbolicUtils.Term{VartypeT}(o, Any[]; type = Bool, shape = []) SymbolicUtils.promote_symtype(::ZeroArgOp, T) = Union{Bool, T} SymbolicUtils.isbinop(::ZeroArgOp) = false Base.nameof(::ZeroArgOp) = :ZeroArgOp -ModelingToolkit.input_timedomain(::ZeroArgOp, _ = nothing) = () -ModelingToolkit.output_timedomain(::ZeroArgOp, _ = nothing) = Clock(0.1) +MTKTearing.input_timedomain(::ZeroArgOp, _ = nothing) = MTKTearing.InputTimeDomainElT[] +MTKTearing.output_timedomain(::ZeroArgOp, _ = nothing) = Clock(0.1) ModelingToolkit.validate_operator(::ZeroArgOp, args, iv; context = nothing) = nothing SciMLBase.is_discrete_time_domain(::ZeroArgOp) = true diff --git a/test/direct.jl b/test/direct.jl deleted file mode 100644 index 5f52a54fea..0000000000 --- a/test/direct.jl +++ /dev/null @@ -1,297 +0,0 @@ -using ModelingToolkit, StaticArrays, LinearAlgebra, SparseArrays -using DiffEqBase -using Test - -using ModelingToolkit: getdefault, getmetadata, SymScope - -canonequal(a, b) = isequal(simplify(a), simplify(b)) - -# Calculus -@parameters t σ ρ β -@variables x y z -@test isequal((Differential(z) * Differential(y) * Differential(x))(t), - Differential(z)(Differential(y)(Differential(x)(t)))) - -@test canonequal(ModelingToolkit.derivative(sin(cos(x)), x), - -sin(x) * cos(cos(x))) - -@register_symbolic no_der(x) -@test canonequal(ModelingToolkit.derivative([sin(cos(x)), hypot(x, no_der(x))], x), - [ - -sin(x) * cos(cos(x)), - x / hypot(x, no_der(x)) + - no_der(x) * Differential(x)(no_der(x)) / hypot(x, no_der(x)) - ]) - -@register_symbolic intfun(x)::Int -@test ModelingToolkit.symtype(intfun(x)) === Int - -eqs = [σ * (y - x), - x * (ρ - z) - y, - x * y - β * z] - -simpexpr = [:($(*)(σ, $(+)(y, $(*)(-1, x)))) - :($(+)($(*)(x, $(+)(ρ, $(*)(-1, z))), $(*)(-1, y))) - :($(+)($(*)(x, y), $(*)(-1, z, β)))] - -σ, β, ρ = 2 // 3, 3 // 4, 4 // 5 -x, y, z = 6 // 7, 7 // 8, 8 // 9 -for i in 1:3 - @test eval(ModelingToolkit.toexpr.(eqs)[i]) == eval(simpexpr[i]) - @test eval(ModelingToolkit.toexpr.(eqs)[i]) == eval(simpexpr[i]) -end - -@parameters σ ρ β -@variables x y z -∂ = ModelingToolkit.jacobian(eqs, [x, y, z]) -for i in 1:3 - ∇ = ModelingToolkit.gradient(eqs[i], [x, y, z]) - @test canonequal(∂[i, :], ∇) -end - -@test all(canonequal.(ModelingToolkit.gradient(eqs[1], [x, y, z]), [σ * -1, σ, 0])) -@test all(canonequal.(ModelingToolkit.hessian(eqs[1], [x, y, z]), 0)) - -du = [x^2, y^3, x^4, sin(y), x + y, x + z^2, z + x, x + y^2 + sin(z)] -reference_jac = sparse(ModelingToolkit.jacobian(du, [x, y, z])) - -@test findnz(ModelingToolkit.jacobian_sparsity(du, [x, y, z]))[[1, 2]] == - findnz(reference_jac)[[1, 2]] - -let - @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 - -@test isequal(ModelingToolkit.sparsejacobian(du, [x, y, z]), reference_jac) - -using ModelingToolkit - -rosenbrock(X) = - sum(1:(length(X) - 1)) do i - 100 * (X[i + 1] - X[i]^2)^2 + (1 - X[i])^2 - end - -@variables a, b -X = [a, b] - -spoly(x) = simplify(x, expand = true) -rr = rosenbrock(X) - -reference_hes = ModelingToolkit.hessian(rr, X) -@test findnz(sparse(reference_hes))[1:2] == - findnz(ModelingToolkit.hessian_sparsity(rr, X))[1:2] - -sp_hess = ModelingToolkit.sparsehessian(rr, X) -@test findnz(sparse(reference_hes))[1:2] == findnz(sp_hess)[1:2] -@test isequal(map(spoly, findnz(sparse(reference_hes))[3]), map(spoly, findnz(sp_hess)[3])) - -Joop, Jiip = eval.(ModelingToolkit.build_function(∂, [x, y, z], [σ, ρ, β], t)) -J = Joop([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J isa Matrix -J2 = copy(J) -Jiip(J2, [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J2 == J - -Joop, Jiip = eval.(ModelingToolkit.build_function(vcat(∂, ∂), [x, y, z], [σ, ρ, β], t)) -J = Joop([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J isa Matrix -J2 = copy(J) -Jiip(J2, [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J2 == J - -Joop, Jiip = eval.(ModelingToolkit.build_function(hcat(∂, ∂), [x, y, z], [σ, ρ, β], t)) -J = Joop([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J isa Matrix -J2 = copy(J) -Jiip(J2, [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J2 == J - -∂3 = cat(∂, ∂, dims = 3) -Joop, Jiip = eval.(ModelingToolkit.build_function(∂3, [x, y, z], [σ, ρ, β], t)) -J = Joop([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test size(J) == (3, 3, 2) -J2 = copy(J) -Jiip(J2, [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J2 == J - -s∂ = sparse(∂) -@test nnz(s∂) == 8 -Joop, -Jiip = eval.(ModelingToolkit.build_function(s∂, [x, y, z], [σ, ρ, β], t, - linenumbers = true)) -J = Joop([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test length(nonzeros(s∂)) == 8 -J2 = copy(J) -Jiip(J2, [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 1.0) -@test J2 == J - -# Function building - -@parameters σ ρ β -@variables x y z -eqs = [σ * (y - x), - x * (ρ - z) - y, - x * y - β * z] -f1, f2 = ModelingToolkit.build_function(eqs, [x, y, z], [σ, ρ, β]) -f = eval(f1) -out = [1.0, 2, 3] -o1 = f([1.0, 2, 3], [1.0, 2, 3]) -f = eval(f2) -f(out, [1.0, 2, 3], [1.0, 2, 3]) -@test all(o1 .== out) - -function test_worldage() - @parameters σ ρ β - @variables x y z - eqs = [σ * (y - x), - x * (ρ - z) - y, - x * y - β * z] - f, - f_iip = ModelingToolkit.build_function(eqs, [x, y, z], [σ, ρ, β]; - expression = Val{false}) - out = [1.0, 2, 3] - o1 = f([1.0, 2, 3], [1.0, 2, 3]) - f_iip(out, [1.0, 2, 3], [1.0, 2, 3]) -end -test_worldage() - -## No parameters -@variables x y z -eqs = [(y - x)^2, - x * (x - z) - y, - x * y - y * z] -f1, f2 = ModelingToolkit.build_function(eqs, [x, y, z]) -f = eval(f1) -out = zeros(3) -o1 = f([1.0, 2, 3]) -f = eval(f2) -f(out, [1.0, 2, 3]) -@test all(out .== o1) - -# y ^ -1 test -g = let - f(x, y) = x / y - @variables x y - ex = expand_derivatives(Differential(x)(f(x, y))) - func_ex = build_function(ex, x, y) - eval(func_ex) -end - -@test g(42, 4) == 1 / 4 - -function test_worldage() - @variables x y z - eqs = [(y - x)^2, - x * (x - z) - y, - x * y - y * z] - f, f_iip = ModelingToolkit.build_function(eqs, [x, y, z]; expression = Val{false}) - out = zeros(3) - o1 = f([1.0, 2, 3]) - f_iip(out, [1.0, 2, 3]) -end -test_worldage() - -@test_nowarn muladd(x, y, 0) -@test promote(x, 0) == (x, identity(0)) -@test_nowarn [x, y, z]' - -let - @register_symbolic foo(x) - @independent_variables t - D = Differential(t) - - @test isequal(expand_derivatives(D(foo(t))), D(foo(t))) - @test isequal(expand_derivatives(D(sin(t) * foo(t))), - cos(t) * foo(t) + sin(t) * D(foo(t))) -end - -foo(; kw...) = kw -foo(args...; kw...) = args, kw -pp = :name => :cool_name - -@named cool_name = foo() -@test collect(cool_name) == [pp] - -@named cool_name = foo(42) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [pp] - -@named cool_name = foo(42; a = 2) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [pp; :a => 2] - -@named cool_name = foo(a = 2) -@test collect(cool_name) == [pp; :a => 2] - -@named cool_name = foo(; a = 2) -@test collect(cool_name) == [pp; :a => 2] - -@named cool_name = foo(name = 2) -@test collect(cool_name) == [:name => 2] - -@named cool_name = foo(42; name = 3) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [:name => 3] - -kwargs = (; name = 3) -@named cool_name = foo(42; kwargs...) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [:name => 3] - -name = 3 -@named cool_name = foo(42; name) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [:name => name] -@named cool_name = foo(; name) -@test collect(cool_name) == [:name => name] - -ff = 3 -@named cool_name = foo(42; ff) -@test cool_name[1] == (42,) -@test collect(cool_name[2]) == [pp; :ff => ff] - -@named cool_name = foo(; ff) -@test collect(cool_name) == [pp; :ff => ff] - -foo(i; name) = (; i, name) -@named goo[1:3] = foo(10) -@test isequal(goo, [(i = 10, name = Symbol(:goo_, i)) for i in 1:3]) -@named koo 1:3 i->foo(10i) -@test isequal(koo, [(i = 10i, name = Symbol(:koo_, i)) for i in 1:3]) -xys = @named begin - x = foo(12) - y[1:3] = foo(13) -end -@test isequal(x, (i = 12, name = :x)) -@test isequal(y, [(i = 13, name = Symbol(:y_, i)) for i in 1:3]) -@test isequal(xys, [x; y]) - -@variables x [misc = "wow"] -@test SymbolicUtils.getmetadata(Symbolics.unwrap(x), ModelingToolkit.VariableMisc, - nothing) == "wow" -@parameters x [misc = "wow"] -@test SymbolicUtils.getmetadata(Symbolics.unwrap(x), ModelingToolkit.VariableMisc, - nothing) == "wow" - -# Scope of defaults in the systems generated by @named -@mtkmodel MoreThanOneArg begin - @variables begin - x(t) - y(t) - z(t) - end -end - -@parameters begin - l - m - n -end - -@named model = MoreThanOneArg(x = l, y = m, z = n) - -@test getmetadata(getdefault(model.x), SymScope) == ParentScope(LocalScope()) -@test getmetadata(getdefault(model.y), SymScope) == ParentScope(LocalScope()) -@test getmetadata(getdefault(model.z), SymScope) == ParentScope(LocalScope()) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 2b1e067847..60ae4a4c7a 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -5,6 +5,7 @@ using OrdinaryDiffEqRosenbrock using OrdinaryDiffEqNonlinearSolve using SymbolicIndexingInterface using Test +using SciCompDSL using ControlSystemsMTK: tf, ss, get_named_sensitivity, get_named_comp_sensitivity using ModelingToolkit: t_nounits as t, D_nounits as D # ============================================================================== diff --git a/test/downstream/test_disturbance_model.jl b/test/downstream/test_disturbance_model.jl index 95cc66b733..0d63aa6f26 100644 --- a/test/downstream/test_disturbance_model.jl +++ b/test/downstream/test_disturbance_model.jl @@ -6,6 +6,7 @@ analysis-point specific method for `generate_control_function`. using ModelingToolkit, OrdinaryDiffEqTsit5, LinearAlgebra, Test using ModelingToolkitStandardLibrary.Mechanical.Rotational using ModelingToolkitStandardLibrary.Blocks +using SciCompDSL import NonlinearSolve using ModelingToolkit: connect # using Plots diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index de5b8a1dac..e84012955c 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -1,4 +1,5 @@ using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq, NonlinearSolve, SciMLBase +using SciCompDSL using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK @@ -48,14 +49,14 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME) truesol = FMI.simulate( - fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"]) + fmu, (0.0, 8.0); solver = Tsit5(), saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"], tstops = collect(0.0:0.1:8.0)) @testset "v3, ME" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME) @mtkcompile sys = MTK.FMIComponent(Val(3); fmu, type = :ME) test_no_inputs_outputs(sys) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) - sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) + sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8, tstops=collect(0.0:0.1:8.0)) @test SciMLBase.successful_retcode(sol) @test sol(0.0:0.1:8.0; diff --git a/test/fractional_to_ordinary.jl b/test/fractional_to_ordinary.jl index 22df472430..b6fe6f7aa0 100644 --- a/test/fractional_to_ordinary.jl +++ b/test/fractional_to_ordinary.jl @@ -29,7 +29,7 @@ end α = 0.3 eqs = (9*gamma(1 + α)/4) - (3*t^(4 - α/2)*gamma(5 + α/2)/gamma(5 - α/2)) eqs += (gamma(9)*t^(8 - α)/gamma(9 - α)) + (3/2*t^(α/2)-t^4)^3 - x^(3/2) -sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1; matrix=true) +sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1; matrix=false) prob = ODEProblem(sys, [], tspan) sol = solve(prob, RadauIIA5(), saveat=timepoint, abstol = 1e-10, reltol = 1e-10) @@ -56,7 +56,7 @@ end D = Differential(t) tspan = (0., 220.) -sys = fractional_to_ordinary([1 - 4*x + x^2 * y, 3*x - x^2 * y], [x, y], [1.3, 0.8], 10^-8, 220; initials=[[1.2, 1], 2.8], matrix=true) +sys = fractional_to_ordinary([1 - 4*x + x^2 * y, 3*x - x^2 * y], [x, y], [1.3, 0.8], 10^-8, 220; initials=[[1.2, 1], 2.8], matrix=false) prob = ODEProblem(sys, [], tspan) sol = solve(prob, RadauIIA5(), abstol = 1e-8, reltol = 1e-8) @@ -79,7 +79,7 @@ sol = solve(prob, RadauIIA5(), abstol = 1e-5, reltol = 1e-5) @test isapprox(expect(5000), sol(5000, idxs=x_0), atol=1e-5) -msys = linear_fractional_to_ordinary([3, 2.5, 2, 1, .5, 0], [1, 1, 1, 4, 1, 4], 6*cos(t), 10^-5, 5000; initials=[1, 1, -1], matrix=true) +msys = linear_fractional_to_ordinary([3, 2.5, 2, 1, .5, 0], [1, 1, 1, 4, 1, 4], 6*cos(t), 10^-5, 5000; initials=[1, 1, -1], matrix=false) mprob = ODEProblem(sys, [], tspan) msol = solve(prob, RadauIIA5(), abstol = 1e-5, reltol = 1e-5) diff --git a/test/guess_propagation.jl b/test/guess_propagation.jl deleted file mode 100644 index 62a6ae9871..0000000000 --- a/test/guess_propagation.jl +++ /dev/null @@ -1,110 +0,0 @@ -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 = System(eqs, t; initialization_eqs) -sys = complete(mtkcompile(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 = System(eqs, t; initialization_eqs) -sys = complete(mtkcompile(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 = System(eqs, t; initialization_eqs) -sys = complete(mtkcompile(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 = System(eqs, t; initialization_eqs) -sys = complete(mtkcompile(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 -@mtkcompile sys = System([x ~ x0, D(y) ~ x], t) -prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) -@test prob[x] == 1.0 -@test prob[y] == 1.0 - -@parameters x0 -@variables x(t) -@variables y(t) = x0 -@mtkcompile sys = System([x ~ x0, D(y) ~ x], t) -prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) -@test prob[x] == 1.0 -@test prob[y] == 1.0 - -@parameters x0 -@variables x(t) -@variables y(t) = x0 -@mtkcompile sys = System([x ~ y, D(y) ~ x], t) -prob = ODEProblem(sys, [x0 => 1.0], (0.0, 1.0)) -@test prob[x] == 1.0 -@test prob[y] == 1.0 - -@parameters x0 -@variables x(t) = x0 -@variables y(t) = x -@mtkcompile sys = System([x ~ y, D(y) ~ x], t) -prob = ODEProblem(sys, [x0 => 1.0], (0.0, 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 index fef9953438..6855aa86f0 100644 --- a/test/hierarchical_initialization_eqs.jl +++ b/test/hierarchical_initialization_eqs.jl @@ -1,4 +1,5 @@ -using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit, SciCompDSL, OrdinaryDiffEq +using Test t = only(@parameters(t)) D = Differential(t) diff --git a/test/if_lifting.jl b/test/if_lifting.jl index 1fcb5947e4..56889018e1 100644 --- a/test/if_lifting.jl +++ b/test/if_lifting.jl @@ -1,5 +1,7 @@ -using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit, OrdinaryDiffEq, SciCompDSL using ModelingToolkit: t_nounits as t, D_nounits as D, IfLifting, no_if_lift +import SymbolicUtils as SU +using Test @testset "Simple `abs(x)`" begin @mtkmodel SimpleAbs begin @@ -95,10 +97,10 @@ end args = arguments(eq.rhs) @test operation(args[1]) == Base.:< @test operation(args[2]) === ifelse - condvars = ModelingToolkit.vars(arguments(args[2])[1]) + condvars = SU.search_variables(arguments(args[2])[1]) @test length(condvars) == 1 && any(isequal(only(condvars)), ps) @test operation(args[3]) === ifelse - condvars = ModelingToolkit.vars(arguments(args[3])[1]) + condvars = SU.search_variables(arguments(args[3])[1]) @test length(condvars) == 1 && any(isequal(only(condvars)), ps) end @testset "Observed variables are modified" begin diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl deleted file mode 100644 index 4423d534c1..0000000000 --- a/test/jacobiansparsity.jl +++ /dev/null @@ -1,170 +0,0 @@ -using ModelingToolkit, SparseArrays, OrdinaryDiffEq, DiffEqBase, BenchmarkTools - -N = 3 -xyd_brusselator = range(0, stop = 1, length = N) -brusselator_f(x, y, t) = (((x - 0.3)^2 + (y - 0.6)^2) <= 0.1^2) * (t >= 1.1) * 5.0 -lim(a, N) = ModelingToolkit.ifelse(a == N + 1, 1, ModelingToolkit.ifelse(a == 0, N, a)) -function brusselator_2d_loop(du, u, p, t) - A, B, alpha, dx = p - alpha = alpha / dx^2 - @inbounds for I in CartesianIndices((N, N)) - i, j = Tuple(I) - x, y = xyd_brusselator[I[1]], xyd_brusselator[I[2]] - ip1, im1, jp1, jm1 = lim(i + 1, N), lim(i - 1, N), lim(j + 1, N), - lim(j - 1, N) - du[i, - j, - 1] = alpha * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] - - 4u[i, j, 1]) + - B + u[i, j, 1]^2 * u[i, j, 2] - (A + 1) * u[i, j, 1] + - brusselator_f(x, y, t) - du[i, - j, - 2] = alpha * (u[im1, j, 2] + u[ip1, j, 2] + u[i, jp1, 2] + u[i, jm1, 2] - - 4u[i, j, 2]) + - A * u[i, j, 1] - u[i, j, 1]^2 * u[i, j, 2] - end -end - -# Test with tuple parameters -p = (3.4, 1.0, 10.0, step(xyd_brusselator)) - -function init_brusselator_2d(xyd) - N = length(xyd) - u = zeros(N, N, 2) - for I in CartesianIndices((N, N)) - x = xyd[I[1]] - y = xyd[I[2]] - u[I, 1] = 22 * (y * (1 - y))^(3 / 2) - u[I, 2] = 27 * (x * (1 - x))^(3 / 2) - end - u -end - -u0 = init_brusselator_2d(xyd_brusselator) -prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, - u0, (0.0, 11.5), p) -sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) - -# test sparse jacobian pattern only. -prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false) -JP = prob.f.jac_prototype -@test findnz(Symbolics.jacobian_sparsity(map(x -> x.rhs, equations(sys)), - unknowns(sys)))[1:2] == - findnz(JP)[1:2] - -# test sparse jacobian -prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) -#@test_nowarn solve(prob, Rosenbrock23()) -@test findnz(calculate_jacobian(sys, sparse = true))[1:2] == - findnz(prob.f.jac_prototype)[1:2] -out = similar(prob.f.jac_prototype) -@test (@ballocated $(prob.f.jac.f_iip)($out, $(prob.u0), $(prob.p), 0.0)) == 0 # should not allocate - -# test when not sparse -prob = ODEProblem(sys, u0, (0, 11.5), sparse = false, jac = true) -@test prob.f.jac_prototype == nothing - -prob = ODEProblem(sys, u0, (0, 11.5), sparse = false, jac = false) -@test prob.f.jac_prototype == nothing - -# test when u0 is nothing -f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = true) -@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2] -@test eltype(f.jac_prototype) == Float64 - -f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = false) -@test findnz(f.jac_prototype)[1:2] == findnz(JP)[1:2] -@test eltype(f.jac_prototype) == Float64 - -# test sparsity index pattern checking -f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = true, checkbounds = true) -out = sparse([1.0 0.0; 0.0 1.0]) # choose a wrong size on purpose -@test size(out) != size(f.jac_prototype) # check that the size is indeed wrong -@test_throws AssertionError f.jac.f_iip(out, u0, p, 0.0) # check that we get an error - -# test when u0 is not Float64 -u0 = similar(init_brusselator_2d(xyd_brusselator), Float32) -prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, - u0, (0.0, 11.5), p) -sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) - -prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false) -@test eltype(prob.f.jac_prototype) == Float32 - -prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) -@test eltype(prob.f.jac_prototype) == Float32 - -@testset "W matrix sparsity" begin - t = ModelingToolkit.t_nounits - D = ModelingToolkit.D_nounits - @parameters g - @variables x(t) y(t) λ(t) - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - @mtkcompile pend = System(eqs, t) - - u0 = [x => 1, y => 0] - prob = ODEProblem( - pend, [u0; [g => 1]], (0, 11.5), guesses = [λ => 1], sparse = true, jac = true) - jac, jac! = generate_jacobian(pend; expression = Val{false}, sparse = true, checkbounds = true) - jac_prototype = ModelingToolkit.jacobian_sparsity(pend) - W_prototype = ModelingToolkit.W_sparsity(pend) - @test nnz(W_prototype) == nnz(jac_prototype) + 2 - - # jac_prototype should be the same as W_prototype - @test findnz(prob.f.jac_prototype)[1:2] == findnz(W_prototype)[1:2] - - u = zeros(5) - p = prob.p - t = 0.0 - @test_throws AssertionError jac!(similar(jac_prototype, Float64), u, p, t) - - W, W! = generate_W(pend; expression = Val{false}, sparse = true, checkbounds = true) - γ = 0.1 - M = sparse(calculate_massmatrix(pend)) - @test_throws AssertionError W!(similar(jac_prototype, Float64), u, p, γ, t) - @test W!(similar(W_prototype, Float64), u, p, γ, t) == - 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) -end - -@testset "Issue#3556: Numerical accuracy" begin - t = ModelingToolkit.t_nounits - D = ModelingToolkit.D_nounits - @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] - @mtkcompile pend = System(eqs, t) - prob = ODEProblem(pend, [x => 0.0, D(x) => 1.0, g => 1.0], (0.0, 1.0); - guesses = [y => 1.0, λ => 1.0], jac = true, sparse = true) - J = deepcopy(prob.f.jac_prototype) - prob.f.jac(J, prob.u0, prob.p, 1.0) - # this currently works but may not continue to do so - # see https://github.com/SciML/ModelingToolkit.jl/pull/3556#issuecomment-2792664039 - @test J == prob.f.jac(prob.u0, prob.p, 1.0) - @test J ≈ prob.f.jac(prob.u0, prob.p, 1.0) -end - -# https://github.com/SciML/ModelingToolkit.jl/issues/3871 -@testset "Issue#3871: Sparsity with observed derivatives" begin - t = ModelingToolkit.t_nounits - D = ModelingToolkit.D_nounits - @variables x(t) y(t) - @mtkcompile sys = System([D(x) ~ x * D(y), D(y) ~ x - y], t) - @test ModelingToolkit.jacobian_sparsity(sys) == [1 1; 1 1] # all nonzero - J1 = calculate_jacobian(sys) - J2 = isequal(unknowns(sys)[1], x) ? [2x-y -x; 1 -1] : [-1 1; -x 2x-y] # analytical result - @test isequal(J1, J2) - prob = ODEProblem(sys, [x => 1.0, y => 0.0], (0.0, 1.0); jac = true, sparse = true) - sol = solve(prob, FBDF()) - @test SciMLBase.successful_retcode(sol) - ts = ModelingToolkit.get_tearing_state(sys) - for ieq in 1:2 - vars1 = ts.fullvars[ModelingToolkit.BipartiteGraphs.𝑠neighbors(ts.structure.graph, ieq)] - vars2 = ModelingToolkit.vars(equations(sys)[ieq]) - @test issetequal(vars1, vars2) - end -end diff --git a/test/latexify/10.tex b/test/latexify/10.tex deleted file mode 100644 index d91a4295ae..0000000000 --- a/test/latexify/10.tex +++ /dev/null @@ -1,5 +0,0 @@ -\begin{align} -\frac{\mathrm{d} x\left( t \right)}{\mathrm{d}t} &= \frac{\left( - x\left( t \right) + y\left( t \right) \right) \frac{\mathrm{d}}{\mathrm{d}t} \left( x\left( t \right) - y\left( t \right) \right) \sigma}{\frac{\mathrm{d} z\left( t \right)}{\mathrm{d}t}} \\ -0 &= - y\left( t \right) + \frac{1}{10} x\left( t \right) \left( - z\left( t \right) + \rho \right) \sigma \\ -\frac{\mathrm{d} z\left( t \right)}{\mathrm{d}t} &= \left( y\left( t \right) \right)^{\frac{2}{3}} x\left( t \right) - z\left( t \right) \beta -\end{align} diff --git a/test/latexify/20.tex b/test/latexify/20.tex deleted file mode 100644 index 012243e981..0000000000 --- a/test/latexify/20.tex +++ /dev/null @@ -1,5 +0,0 @@ -\begin{align} -\frac{\mathrm{d} u\_{1}\left( t \right)}{\mathrm{d}t} &= p_{3} \left( - u\_{1}\left( t \right) + u\_{2}\left( t \right) \right) \\ -0 &= - u\_{2}\left( t \right) + \frac{1}{10} \left( p_{1} - u\_{1}\left( t \right) \right) p_{2} p_{3} u\_{1}\left( t \right) \\ -\frac{\mathrm{d} u\_{3}\left( t \right)}{\mathrm{d}t} &= u\_{2}\left( t \right)^{\frac{2}{3}} u\_{1}\left( t \right) - p_{3} u\_{3}\left( t \right) -\end{align} diff --git a/test/latexify/30.tex b/test/latexify/30.tex deleted file mode 100644 index b51b73c34b..0000000000 --- a/test/latexify/30.tex +++ /dev/null @@ -1,5 +0,0 @@ -\begin{align} -\frac{\mathrm{d} u\_{1}\left( t \right)}{\mathrm{d}t} &= p_{3} \left( - u\_{1}\left( t \right) + u\_{2}\left( t \right) \right) \\ -\frac{\mathrm{d} u\_{2}\left( t \right)}{\mathrm{d}t} &= - u\_{2}\left( t \right) + \frac{1}{10} \left( p_{1} - u\_{1}\left( t \right) \right) p_{2} p_{3} u\_{1}\left( t \right) \\ -\frac{\mathrm{d} u\_{3}\left( t \right)}{\mathrm{d}t} &= u\_{2}\left( t \right)^{\frac{2}{3}} u\_{1}\left( t \right) - p_{3} u\_{3}\left( t \right) -\end{align} diff --git a/test/latexify/40.tex b/test/latexify/40.tex deleted file mode 100644 index 3807185ae2..0000000000 --- a/test/latexify/40.tex +++ /dev/null @@ -1,3 +0,0 @@ -\begin{align} -\frac{\mathrm{d} x\left( t \right)}{\mathrm{d}t} &= \frac{1 + \cos\left( t \right)}{1 + 2 x\left( t \right)} -\end{align} diff --git a/test/latexify/50.tex b/test/latexify/50.tex deleted file mode 100644 index b1e6a1fda4..0000000000 --- a/test/latexify/50.tex +++ /dev/null @@ -1,19 +0,0 @@ -\begin{equation} -\left[ -\begin{array}{c} -\mathrm{connect}\left( P_{+}output, C_{+}input \right) \\ -AnalysisPoint\left( \mathtt{C.output.u}\left( t \right), plant\_input, \left[ -\begin{array}{c} -\mathtt{P.input.u}\left( t \right) \\ -\end{array} -\right] \right) \\ -\mathtt{P.u}\left( t \right) = \mathtt{P.input.u}\left( t \right) \\ -\mathtt{P.y}\left( t \right) = \mathtt{P.output.u}\left( t \right) \\ -\mathtt{P.y}\left( t \right) = \mathtt{P.x}\left( t \right) \\ -\frac{\mathrm{d} \mathtt{P.x}\left( t \right)}{\mathrm{d}t} = \frac{ - \mathtt{P.x}\left( t \right) + \mathtt{P.k} \mathtt{P.u}\left( t \right)}{\mathtt{P.T}} \\ -\mathtt{C.u}\left( t \right) = \mathtt{C.input.u}\left( t \right) \\ -\mathtt{C.y}\left( t \right) = \mathtt{C.output.u}\left( t \right) \\ -\mathtt{C.y}\left( t \right) = \mathtt{C.k} \mathtt{C.u}\left( t \right) \\ -\end{array} -\right] -\end{equation} diff --git a/test/linalg.jl b/test/linalg.jl deleted file mode 100644 index be6fb39b1b..0000000000 --- a/test/linalg.jl +++ /dev/null @@ -1,30 +0,0 @@ -using ModelingToolkit -using LinearAlgebra -using Test - -A = [0 1 1 2 2 1 1 2 1 2 - 0 1 -1 -3 -2 2 1 -5 0 -5 - 0 1 2 2 1 1 2 1 1 2 - 0 1 1 1 2 1 1 2 2 1 - 0 2 1 2 2 2 2 1 1 1 - 0 1 1 1 2 2 1 1 2 1 - 0 2 1 2 2 1 2 1 1 2 - 0 1 7 17 14 2 1 19 4 23 - 0 1 -1 -3 -2 1 1 -4 0 -5 - 0 1 1 2 2 1 1 2 2 2] -N = ModelingToolkit.nullspace(A) -@test size(N, 2) == 3 -@test rank(N) == 3 -@test iszero(A * N) - -A = [0 1 2 0 1 0; - 0 0 0 0 0 1; - 0 0 0 0 0 1; - 1 0 1 2 0 1; - 0 0 0 2 1 0] -col_order = Int[] -N = ModelingToolkit.nullspace(A; col_order) -colspan = A[:, col_order[1:4]] # rank is 4 -@test iszero(ModelingToolkit.nullspace(colspan)) -@test !iszero(ModelingToolkit.nullspace(A[:, col_order[1:5]])) -@test !iszero(ModelingToolkit.nullspace(A[:, [col_order[1:4]..., col_order[6]]])) diff --git a/test/linearize.jl b/test/linearize.jl index c37529b885..0ffe723982 100644 --- a/test/linearize.jl +++ b/test/linearize.jl @@ -1,4 +1,6 @@ using ModelingToolkit, ADTypes, Test +using NonlinearSolve, SciCompDSL +using Symbolics: value using CommonSolve: solve # Test reorder_unknowns @@ -125,10 +127,10 @@ lsyss, ssys = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) @test isequal(lsyss.A, lsyss_ns.A) lsyss = ModelingToolkit.reorder_unknowns(lsyss, unknowns(ssys), [f.x, p.x]) -@test ModelingToolkit.fixpoint_sub(lsyss.A, ModelingToolkit.defaults(cl)) == lsys.A -@test ModelingToolkit.fixpoint_sub(lsyss.B, ModelingToolkit.defaults(cl)) == lsys.B -@test ModelingToolkit.fixpoint_sub(lsyss.C, ModelingToolkit.defaults(cl)) == lsys.C -@test ModelingToolkit.fixpoint_sub(lsyss.D, ModelingToolkit.defaults(cl)) == lsys.D +@test value.(ModelingToolkit.fixpoint_sub(lsyss.A, ModelingToolkit.initial_conditions(cl))) == lsys.A +@test value.(ModelingToolkit.fixpoint_sub(lsyss.B, ModelingToolkit.initial_conditions(cl))) == lsys.B +@test value.(ModelingToolkit.fixpoint_sub(lsyss.C, ModelingToolkit.initial_conditions(cl))) == lsys.C +@test value.(ModelingToolkit.fixpoint_sub(lsyss.D, ModelingToolkit.initial_conditions(cl))) == lsys.D ## using ModelingToolkitStandardLibrary.Blocks: LimPID k = 400 @@ -155,14 +157,14 @@ ssys2 = ModelingToolkit.linearize_symbolic(pid, [reference.u, measurement.u], [ctr_output.u]) lsyss = ModelingToolkit.reorder_unknowns(lsyss0, unknowns(ssys2), desired_order) -@test ModelingToolkit.fixpoint_sub( - lsyss.A, ModelingToolkit.defaults_and_guesses(pid)) == lsys.A -@test ModelingToolkit.fixpoint_sub( - lsyss.B, ModelingToolkit.defaults_and_guesses(pid)) == lsys.B -@test ModelingToolkit.fixpoint_sub( - lsyss.C, ModelingToolkit.defaults_and_guesses(pid)) == lsys.C -@test ModelingToolkit.fixpoint_sub( - lsyss.D, ModelingToolkit.defaults_and_guesses(pid)) == lsys.D +@test value.(ModelingToolkit.fixpoint_sub( + lsyss.A, ModelingToolkit.initial_conditions_and_guesses(pid); fold = Val(true))) == lsys.A +@test value.(ModelingToolkit.fixpoint_sub( + lsyss.B, ModelingToolkit.initial_conditions_and_guesses(pid); fold = Val(true))) == lsys.B +@test value.(ModelingToolkit.fixpoint_sub( + lsyss.C, ModelingToolkit.initial_conditions_and_guesses(pid); fold = Val(true))) == lsys.C +@test value.(ModelingToolkit.fixpoint_sub( + lsyss.D, ModelingToolkit.initial_conditions_and_guesses(pid); fold = Val(true))) == 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, desired_order, reverse(desired_order)) @@ -272,7 +274,7 @@ connections = [connect(r.output, :r, filt.input) connect(er.output, :e, pid.err_input)] closed_loop = System(connections, t, systems = [model, pid, filt, sensor, r, er], - name = :closed_loop, defaults = [ + name = :closed_loop, initial_conditions = [ model.inertia1.phi => 0.0, model.inertia2.phi => 0.0, model.inertia1.w => 0.0, @@ -294,7 +296,7 @@ closed_loop = System(connections, t, systems = [model, pid, filt, sensor, r, er] end # Model variables, with initial values needed @variables begin - m(t) = 1.5 * ρ * A, [description = "Liquid mass"] + m(t), [description = "Liquid mass"] md_i(t), [description = "Influent mass flow rate"] md_e(t), [description = "Effluent mass flow rate"] V(t), [description = "Liquid volume"] @@ -340,7 +342,7 @@ end @variables x(t) y(t) @parameters p eqs = [0 ~ x * log(y) - p] - @named sys = System(eqs, t; defaults = [p => 1.0]) + @named sys = System(eqs, t; initial_conditions = [p => 1.0]) sys = complete(sys) @test_throws ModelingToolkit.MissingGuessError linearize( sys, [x], []; op = Dict(x => 1.0), allow_input_derivatives = true) @@ -362,5 +364,5 @@ end @unpack md_i, h, m = tank_noi m_ss = 2.4000000003229878 @test_warn ["empty operating point", "warn_empty_op"] linearize( - tank_noi, [md_i], [h]; p = [md_i => 1.0]) + tank_noi, [md_i], [h]; p = [md_i => 1.0, m => m_ss]) end diff --git a/test/reduction.jl b/test/reduction.jl index 6fee37d806..6660b61b62 100644 --- a/test/reduction.jl +++ b/test/reduction.jl @@ -1,12 +1,12 @@ using ModelingToolkit, OrdinaryDiffEq, Test, NonlinearSolve, LinearAlgebra -using ModelingToolkit: topsort_equations, t_nounits as t, D_nounits as D +using ModelingToolkit: topsort_equations, t_nounits as t, D_nounits as D, unwrap @variables x(t) y(t) z(t) k(t) eqs = [x ~ y + z z ~ 2 y ~ 2z + k] -sorted_eq = topsort_equations(eqs, [x, y, z, k]) +sorted_eq = topsort_equations(eqs, unwrap.([x, y, z, k])) ref_eq = [z ~ 2 y ~ 2z + k @@ -15,7 +15,7 @@ ref_eq = [z ~ 2 @test_throws ArgumentError topsort_equations([x ~ y + z z ~ 2 - y ~ 2z + x], [x, y, z, k]) + y ~ 2z + x], unwrap.([x, y, z, k])) @parameters σ ρ β @variables x(t) y(t) z(t) a(t) u(t) F(t) @@ -189,16 +189,16 @@ eqs = [D(E) ~ k₋₁ * C - k₁ * E * S E₀ ~ E + C] @named sys = System(eqs, t, [E, C, S, P], [k₁, k₂, k₋₁, E₀]) -@test_throws ModelingToolkit.ExtraEquationsSystemException mtkcompile(sys) +@test_throws ModelingToolkit.StateSelection.ExtraEquationsSystemException mtkcompile(sys) # Example 5 from Pantelides' original paper -params = collect(@parameters y1(t) y2(t)) +params = collect(@parameters y1 y2) sts = collect(@variables x(t) u1(t) u2(t)) eqs = [0 ~ x + sin(u1 + u2) D(x) ~ x + y1 cos(x) ~ sin(y2)] @named sys = System(eqs, t, sts, params) -@test_throws ModelingToolkit.InvalidSystemException mtkcompile(sys) +@test_throws ModelingToolkit.StateSelection.InvalidSystemException mtkcompile(sys) # issue #963 @variables v47(t) v57(t) v66(t) v25(t) i74(t) i75(t) i64(t) i71(t) v1(t) v2(t) @@ -277,8 +277,8 @@ eqs = [x ~ 0 @named sys = System(eqs, t, [x, y], []) ss = mtkcompile(sys) @test isempty(equations(ss)) -@test sort(string.(observed(ss))) == ["x(t) ~ 0.0" - "xˍt(t) ~ 0.0" +@test sort(string.(observed(ss))) == ["x(t) ~ 0" + "xˍt(t) ~ 0" "y(t) ~ xˍt(t) - x(t)"] eqs = [D(D(x)) ~ -x] diff --git a/test/runtests.jl b/test/runtests.jl index 9ccbda75b3..a76a61920e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,125 +2,95 @@ using SafeTestsets, Pkg, Test # https://github.com/JuliaLang/julia/issues/54664 import REPL +const MTKBasePath = joinpath(dirname(@__DIR__), "lib", "ModelingToolkitBase") +const MTKBasePkgSpec = PackageSpec(; path = MTKBasePath) +const SciCompDSLPath = joinpath(dirname(@__DIR__), "lib", "SciCompDSL") +const SciCompDSLPkgSpec = PackageSpec(; path = SciCompDSLPath) +Pkg.develop([MTKBasePkgSpec, SciCompDSLPkgSpec]) + const GROUP = get(ENV, "GROUP", "All") function activate_fmi_env() Pkg.activate("fmi") - Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.develop([MTKBasePkgSpec, SciCompDSLPkgSpec, PackageSpec(path = dirname(@__DIR__))]) Pkg.instantiate() end function activate_extensions_env() - Pkg.activate("extensions") - Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.activate(joinpath(MTKBasePath, "test", "extensions")) + Pkg.develop([MTKBasePkgSpec, SciCompDSLPkgSpec, PackageSpec(path = dirname(@__DIR__))]) Pkg.instantiate() end function activate_downstream_env() Pkg.activate("downstream") - Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.develop([MTKBasePkgSpec, SciCompDSLPkgSpec, PackageSpec(path = dirname(@__DIR__))]) Pkg.instantiate() end +macro mtktestset(name, file) + quote + @safetestset $name begin + using ModelingToolkit + import ModelingToolkitBase + include(joinpath(pkgdir(ModelingToolkitBase), "test", $file)) + end + end +end + @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin - @safetestset "Linear Algebra Test" include("linalg.jl") - @safetestset "AbstractSystem Test" include("abstractsystem.jl") - @safetestset "Variable Scope Tests" include("variable_scope.jl") - @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") - @safetestset "Parsing Test" include("variable_parsing.jl") - @safetestset "Simplify Test" include("simplify.jl") - @safetestset "Direct Usage Test" include("direct.jl") - @safetestset "System Linearity Test" include("linearity.jl") - @safetestset "Input Output Test" include("input_output_handling.jl") + @mtktestset("Input Output Test", "input_output_handling.jl") @safetestset "Clock Test" include("clock.jl") - @safetestset "ODESystem Test" include("odesystem.jl") - @safetestset "Dynamic Quantities Test" include("dq_units.jl") - @safetestset "Unitful Quantities Test" include("units.jl") - @safetestset "Mass Matrix Test" include("mass_matrix.jl") + @mtktestset("Variable binding semantics", "binding_semantics.jl") + @mtktestset("ODESystem Test", "odesystem.jl") + @mtktestset("Dynamic Quantities Test", "dq_units.jl") @safetestset "Reduction Test" include("reduction.jl") - @safetestset "Split Parameters Test" include("split_parameters.jl") - @safetestset "StaticArrays Test" include("static_arrays.jl") - @safetestset "Components Test" include("components.jl") - @safetestset "Model Parsing Test" include("model_parsing.jl") - @safetestset "Error Handling" include("error_handling.jl") + @mtktestset("Split Parameters Test", "split_parameters.jl") + @mtktestset("Components Test", "components.jl") @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") - @safetestset "Basic transformations" include("basic_transformations.jl") - @safetestset "Change of variables" include("changeofvariables.jl") + @mtktestset("Basic transformations", "basic_transformations.jl") + @mtktestset("Change of variables", "changeofvariables.jl") @safetestset "State Selection Test" include("state_selection.jl") - @safetestset "Symbolic Event Test" include("symbolic_events.jl") - @safetestset "Stream Connect Test" include("stream_connectors.jl") - @safetestset "Domain Connect Test" include("domain_connectors.jl") - @safetestset "Dependency Graph Test" include("dep_graphs.jl") - @safetestset "Function Registration Test" include("function_registration.jl") - @safetestset "Precompiled Modules Test" include("precompile_test.jl") - @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") - @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") - @safetestset "Modelingtoolkitize Test" include("modelingtoolkitize.jl") - @safetestset "Constants Test" include("constants.jl") - @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") - @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") - @safetestset "System Accessor Functions Test" include("accessor_functions.jl") - @safetestset "Equations with complex values" include("complex.jl") + @mtktestset("Symbolic Event Test", "symbolic_events.jl") + @mtktestset("Stream Connect Test", "stream_connectors.jl") + @mtktestset("Jacobian Sparsity", "jacobiansparsity.jl") + @mtktestset("Modelingtoolkitize Test", "modelingtoolkitize.jl") + @mtktestset("Constants Test", "constants.jl") + @mtktestset("System Accessor Functions Test", "accessor_functions.jl") end end if GROUP == "All" || GROUP == "Initialization" - @safetestset "Guess Propagation" include("guess_propagation.jl") + @mtktestset("Guess Propagation", "guess_propagation.jl") @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") - @safetestset "InitializationSystem Test" include("initializationsystem.jl") - @safetestset "Initial Values Test" include("initial_values.jl") + @mtktestset("InitializationSystem Test", "initializationsystem.jl") end if GROUP == "All" || GROUP == "InterfaceII" @testset "InterfaceII" begin - @safetestset "Code Generation Test" include("code_generation.jl") - @safetestset "IndexCache Test" include("index_cache.jl") - @safetestset "Variable Utils Test" include("variable_utils.jl") - @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") - @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") - @safetestset "Discrete System" include("discrete_system.jl") - @safetestset "Implicit Discrete System" include("implicit_discrete_system.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") + @mtktestset("Code Generation Test", "code_generation.jl") + @mtktestset("OptimizationSystem Test", "optimizationsystem.jl") + @mtktestset("Discrete System", "discrete_system.jl") + @mtktestset("Implicit Discrete System", "implicit_discrete_system.jl") + @mtktestset("SDESystem Test", "sdesystem.jl") + @mtktestset("DDESystem Test", "dde.jl") + @mtktestset("NonlinearSystem Test", "nonlinearsystem.jl") @safetestset "SCCNonlinearProblem Test" include("scc_nonlinear_problem.jl") - @safetestset "PDE Construction Test" include("pdesystem.jl") - @safetestset "JumpSystem Test" include("jumpsystem.jl") - @safetestset "Optimal Control + Constraints Tests" include("bvproblem.jl") - @safetestset "print_tree" include("print_tree.jl") - @safetestset "Constraints Test" include("constraints.jl") @safetestset "IfLifting Test" include("if_lifting.jl") - @safetestset "Analysis Points Test" include("analysis_points.jl") - @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") - @safetestset "Debugging Test" include("debugging.jl") - @safetestset "Namespacing test" include("namespacing.jl") + @mtktestset("Analysis Points Test", "analysis_points.jl") + @mtktestset("Causal Variables Connection Test", "causal_variables_connection.jl") @safetestset "Subsystem replacement" include("substitute_component.jl") @safetestset "Linearization Tests" include("linearize.jl") - @safetestset "LinearProblem Tests" include("linearproblem.jl") @safetestset "Fractional Differential Equations Tests" include("fractional_to_ordinary.jl") @safetestset "SemilinearODEProblem tests" include("semilinearodeproblem.jl") end end if GROUP == "All" || GROUP == "SymbolicIndexingInterface" - @safetestset "SymbolicIndexingInterface test" include("symbolic_indexing_interface.jl") - @safetestset "SciML Problem Input Test" include("sciml_problem_inputs.jl") - @safetestset "MTKParameters Test" include("mtkparameters.jl") - end - - if GROUP == "All" || GROUP == "Extended" - @safetestset "Test Big System Usage" include("bigsystem.jl") - println("C compilation test requires gcc available in the path!") - @safetestset "C Compilation Test" include("ccompile.jl") - @testset "Distributed Test" include("distributed.jl") - @testset "Serialization" include("serialization.jl") - end - - if GROUP == "All" || GROUP == "RegressionI" - @safetestset "Latexify recipes Test" include("latexify.jl") + @mtktestset("SciML Problem Input Test", "sciml_problem_inputs.jl") + @mtktestset("MTKParameters Test", "mtkparameters.jl") end if GROUP == "All" || GROUP == "Downstream" @@ -137,11 +107,10 @@ end if GROUP == "All" || GROUP == "Extensions" activate_extensions_env() - @safetestset "Dynamic Optimization Collocation Solvers" include("extensions/dynamic_optimization.jl") - @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") - @safetestset "LabelledArrays Test" include("labelledarrays.jl") - @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") - @safetestset "InfiniteOpt Extension Test" include("extensions/test_infiniteopt.jl") - @safetestset "Auto Differentiation Test" include("extensions/ad.jl") + @mtktestset("HomotopyContinuation Extension Test", "extensions/homotopy_continuation.jl") + @mtktestset("BifurcationKit Extension Test", "extensions/bifurcationkit.jl") + @mtktestset("InfiniteOpt Extension Test", "extensions/test_infiniteopt.jl") + # @mtktestset("Auto Differentiation Test", "extensions/ad.jl") + @mtktestset("Dynamic Optimization Collocation Solvers", "extensions/dynamic_optimization.jl") end end diff --git a/test/simplify.jl b/test/simplify.jl deleted file mode 100644 index 4252e3262e..0000000000 --- a/test/simplify.jl +++ /dev/null @@ -1,50 +0,0 @@ -using ModelingToolkit -using ModelingToolkit: value -using Test - -@independent_variables t -@variables x(t) y(t) z(t) - -null_op = 0 * t -@test isequal(simplify(null_op), 0) - -one_op = 1 * t -@test isequal(simplify(one_op), t) - -identity_op = Num(Term(identity, [value(x)])) -@test isequal(simplify(identity_op), x) - -minus_op = -x -@test isequal(simplify(minus_op), -1x) -simplify(minus_op) - -@variables x - -@test toexpr(expand_derivatives(Differential(x)((x - 2)^2))) == :($(*)(2, $(+)(-2, x))) -@test toexpr(expand_derivatives(Differential(x)((x - 2)^3))) == - :($(*)(3, $(^)($(+)(-2, x), 2))) -@test toexpr(simplify(x + 2 + 3)) == :($(+)(5, x)) - -d1 = Differential(x)((-2 + x)^2) -d2 = Differential(x)(d1) -d3 = Differential(x)(d2) - -@test toexpr(expand_derivatives(d3)) == :(0) -@test toexpr(simplify(x^0)) == :(1) - -@test ModelingToolkit.substitute(value(2x + y == 1), Dict(x => 0.0, y => 0.0)) === false -@test ModelingToolkit.substitute(value(2x + y == 1), Dict(x => 0.0, y => 1.0)) === true - -# 699 -using SymbolicUtils: substitute -@independent_variables t -@parameters a(t) b(t) - -# back and forth substitution does not work for parameters with dependencies -term = value(a) -term2 = substitute(term, a => b) -@test ModelingToolkit.isparameter(term2) -@test isequal(term2, b) -term3 = substitute(term2, b => a) -@test ModelingToolkit.isparameter(term3) -@test isequal(term3, a) diff --git a/test/state_selection.jl b/test/state_selection.jl index fd7a840798..cafa036a7f 100644 --- a/test/state_selection.jl +++ b/test/state_selection.jl @@ -1,4 +1,5 @@ using ModelingToolkit, OrdinaryDiffEq, Test +import SymbolicUtils as SU using ModelingToolkit: t_nounits as t, D_nounits as D sts = @variables x1(t) x2(t) x3(t) x4(t) @@ -12,7 +13,7 @@ eqs = [x1 + x2 + u1 ~ 0 let dd = dummy_derivative(sys) has_dx1 = has_dx2 = false for eq in equations(dd) - vars = ModelingToolkit.vars(eq) + vars = SU.search_variables(eq) has_dx1 |= D(x1) in vars || D(D(x1)) in vars has_dx2 |= D(x2) in vars || D(D(x2)) in vars end @@ -251,7 +252,7 @@ let # ---------------------------------------------------------------------------- # solution ------------------------------------------------------------------- - @named catapult = System(eqs, t, vars, params, defaults = defs) + @named catapult = System(eqs, t, vars, params, initial_conditions = defs) sys = mtkcompile(catapult) prob = ODEProblem(sys, [l_2f => 0.55, damp => 1e7], (0.0, 0.1); jac = true) @test solve(prob, Rodas4()).retcode == ReturnCode.Success diff --git a/test/structural_transformation/bareiss.jl b/test/structural_transformation/bareiss.jl deleted file mode 100644 index 5d0a10c30c..0000000000 --- a/test/structural_transformation/bareiss.jl +++ /dev/null @@ -1,28 +0,0 @@ -using SparseArrays -using ModelingToolkit -import ModelingToolkit: bareiss!, find_pivot_col, bareiss_update!, swaprows! -import Base: swapcols! - -function det_bareiss!(M) - parity = 1 - _swaprows!(M, i, j) = (i != j && (parity = -parity); swaprows!(M, i, j)) - _swapcols!(M, i, j) = (i != j && (parity = -parity); swapcols!(M, i, j)) - # We only look at the last entry, so we don't care that the sub-diagonals are - # garbage. - zero!(M, i, j) = nothing - rank = bareiss!(M, (_swapcols!, _swaprows!, bareiss_update!, zero!); - find_pivot = find_pivot_col) - return parity * M[end, end] -end - -@testset "bareiss tests" begin - # copy gives a dense matrix - @testset "bareiss tests: $T" for T in (copy, sparse) - # matrix determinant pairs - for (M, d) in ((BigInt[9 1 8 0; 0 0 8 7; 7 6 8 3; 2 9 7 7], -1), - (BigInt[1 big(2)^65+1; 3 4], 4 - 3 * (big(2)^65 + 1))) - # test that the determinant was correctly computed - @test det_bareiss!(T(M)) == d - end - end -end diff --git a/test/structural_transformation/runtests.jl b/test/structural_transformation/runtests.jl index 316026c92a..0f9b07c649 100644 --- a/test/structural_transformation/runtests.jl +++ b/test/structural_transformation/runtests.jl @@ -9,6 +9,3 @@ end @safetestset "Tearing" begin include("tearing.jl") end -@safetestset "Bareiss" begin - include("bareiss.jl") -end diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index d6e76918cb..3499d8c957 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -1,12 +1,14 @@ using Test using ModelingToolkit -using ModelingToolkit: Equation -using ModelingToolkit.StructuralTransformations: SystemStructure, find_solvables! +using ModelingToolkit: Equation, observed +using ModelingToolkit.StructuralTransformations: SystemStructure using NonlinearSolve using LinearAlgebra using UnPack using SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D +import StateSelection +import SymbolicUtils as SU ### ### Nonlinear system ### @@ -21,7 +23,7 @@ eqs = [ ] @named sys = System(eqs, [u1, u2, u3, u4, u5], [h]) state = TearingState(sys) -StructuralTransformations.find_solvables!(state) +StateSelection.find_solvables!(state) io = IOBuffer() show(io, MIME"text/plain"(), state.structure) @@ -43,25 +45,27 @@ prt = String(take!(buff)) # u4 = f4(u2, u3) # u5 = f5(u4, u1) state = TearingState(sys) -find_solvables!(state) +StateSelection.find_solvables!(state) @unpack structure, fullvars = state @unpack graph, solvable_graph = state.structure int2var = Dict(eachindex(fullvars) .=> fullvars) graph2vars(graph) = map(is -> Set(map(i -> int2var[i], is)), graph.fadjlist) -@test graph2vars(graph) == [Set([u1, u5]) - Set([u1, u2]) - Set([u1, u3, u2]) - Set([u4, u3, u2]) - Set([u4, u1, u5])] -@test graph2vars(solvable_graph) == [Set([u1]) - Set([u2]) - Set([u3]) - Set([u4]) - Set([u5])] +# @test graph2vars(graph) == [ +# Set([u4, u3, u2]) +# Set([u1, u5]) +# Set([u1, u2]) +# Set([u1, u2, u3]) +# Set([u4, u1, u5]) +# ] +# @test graph2vars(solvable_graph) == [Set([u4]) +# Set([u1]) +# Set([u2]) +# Set([u3]) +# Set([u5])] newsys = tearing(sys) @test length(equations(newsys)) == 1 -@test issetequal(ModelingToolkit.vars(equations(newsys)), [u1, u4, u5]) +@test issetequal(SU.search_variables(equations(newsys)), [u1, u4, u5]) # Before: # u1 u2 u3 u4 u5 @@ -119,7 +123,7 @@ end # 0 = u5 - hypot(sin(u5), hypot(cos(sin(u5)), hypot(sin(u5), cos(sin(u5))))) tornsys = complete(tearing(sys)) @test isequal(equations(tornsys), [0 ~ u5 - hypot(u4, u1)]) -prob = NonlinearProblem(tornsys, ones(1)) +prob = NonlinearProblem(tornsys, [u5 => 1.0]) sol = solve(prob, NewtonRaphson()) @test norm(prob.f(sol.u, sol.prob.p)) < 1e-10 @@ -165,7 +169,7 @@ zgetter = getsym(prob, z) @test zgetter(du)≈xgetter(u) + sin(zgetter(u)) - prob.ps[p] * tt atol=1e-5 # test the initial guess is respected -@named sys = System(eqs, t, defaults = Dict(z => NaN)) +@named sys = System(eqs, t, initial_conditions = Dict(z => NaN)) infprob = ODEProblem(mtkcompile(sys), [x => 1.0, p => 0.2], (0, 1.0)) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) @@ -203,3 +207,64 @@ sys = mtkcompile(ms_model) prob_complex = ODEProblem(sys, u0, (0, 1.0)) sol = solve(prob_complex, Tsit5()) @test all(sol[mass.v] .== 1) + +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Blocks: Constant + +@testset "Inline linear SCCs" begin + function RCModel(; name) + pars = @parameters begin + R = 1.0 + C = 1.0 + V = 1.0 + end + systems = @named begin + resistor1 = Resistor(R = R) + resistor2 = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + eqs = [ + connect(constant.output, source.V) + connect(source.p, resistor1.p) + connect(resistor1.n, resistor2.p) + connect(resistor2.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + ] + return System(eqs, t, [], pars; systems, name) + end + + reassemble_alg1 = StructuralTransformations.DefaultReassembleAlgorithm(; inline_linear_sccs = true) + reassemble_alg2 = StructuralTransformations.DefaultReassembleAlgorithm(; inline_linear_sccs = true, analytical_linear_scc_limit = 0) + @mtkcompile sys1 = RCModel() + @mtkcompile sys2 = RCModel() reassemble_alg=reassemble_alg1 + @mtkcompile sys3 = RCModel() reassemble_alg=reassemble_alg2 + + @test length(equations(sys1)) == 2 + @test length(equations(sys2)) == 1 + @test isequal(only(unknowns(sys2)), sys2.capacitor.v) + @test length(equations(sys3)) == 1 + @test isequal(only(unknowns(sys3)), sys3.capacitor.v) + + idx = findfirst(isequal(sys3.capacitor.i), observables(sys3)) + rhs = observed(sys3)[idx].rhs + @test operation(rhs) === getindex + @test operation(arguments(rhs)[1]) === solve + + prob1 = ODEProblem(sys1, [], (0.0, 10.0); guesses = [sys1.resistor1.v => 1.0]) + prob2 = ODEProblem(sys2, [], (0.0, 10.0)) + prob3 = ODEProblem(sys3, [], (0.0, 10.0)) + + sol1 = solve(prob1, Rodas5P(); abstol = 1e-8, reltol = 1e-8) + sol2 = solve(prob2, Tsit5(), abstol = 1e-8, reltol = 1e-8) + sol3 = solve(prob3, Tsit5(), abstol = 1e-8, reltol = 1e-8) + + @test SciMLBase.successful_retcode(sol1) + @test SciMLBase.successful_retcode(sol2) + @test SciMLBase.successful_retcode(sol3) + + @test sol2(sol1.t; idxs = unknowns(sys1)).u ≈ sol1.u atol=1e-8 + @test sol3(sol1.t; idxs = unknowns(sys1)).u ≈ sol1.u atol=1e-8 +end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 4a2df411a6..ac448de97e 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -3,10 +3,14 @@ using ModelingToolkit using Graphs using SparseArrays using UnPack -using ModelingToolkit: t_nounits as t, D_nounits as D, default_toterm +using ModelingToolkit: t_nounits as t, D_nounits as D, default_toterm, SymbolicDiscreteCallback using Symbolics: unwrap using DataInterpolations using OrdinaryDiffEq, NonlinearSolve, StochasticDiffEq +import DiffEqNoiseProcess +import SymbolicUtils as SU +import StateSelection +import ModelingToolkitTearing as MTKTearing const ST = StructuralTransformations # Define some variables @@ -21,7 +25,7 @@ eqs = [D(x) ~ w, 0 ~ x^2 + y^2 - L^2] pendulum = System(eqs, t, [x, y, w, z, T], [L, g], name = :pendulum) state = TearingState(pendulum) -StructuralTransformations.find_solvables!(state) +StateSelection.find_solvables!(state) sss = state.structure @unpack graph, solvable_graph, var_to_diff = sss @test sort(graph.fadjlist) == [[1, 7], [2, 8], [3, 5, 9], [4, 6, 9], [5, 6]] @@ -39,8 +43,8 @@ end @testset "observed2graph handles unknowns inside callable parameters" begin @variables x(t) y(t) - @parameters p(..) - g, _ = ModelingToolkit.observed2graph([y ~ p(x), x ~ 0], [y, x]) + @parameters p(::Real) + g, _ = ModelingToolkit.observed2graph([y ~ p(x), x ~ 0], unwrap.([y, x])) @test ModelingToolkit.𝑠neighbors(g, 1) == [2] @test ModelingToolkit.𝑑neighbors(g, 2) == [1] end @@ -59,14 +63,15 @@ end @test_nowarn prob.f(prob.u0, prob.p, 0.0) isys = ModelingToolkit.generate_initializesystem(sys) - @test length(unknowns(isys)) == 5 - @test length(equations(isys)) == 4 + @test length(unknowns(isys)) == 4 + @test length(equations(isys)) == 5 @test !any(equations(isys)) do eq - iscall(eq.rhs) && operation(eq.rhs) in [StructuralTransformations.change_origin] + iscall(eq.rhs) && operation(eq.rhs) in [MTKTearing.change_origin] end end @testset "array hack can be disabled" begin + reassemble_alg = StructuralTransformations.DefaultReassembleAlgorithm(; array_hack = false) @testset "fully_determined = true" begin @variables x(t) y(t)[1:2] z(t)[1:2] @parameters foo(::AbstractVector)[1:2] @@ -74,10 +79,10 @@ end @named sys = System( [D(x) ~ z[1] + z[2] + foo(z)[1], y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) - sys2 = mtkcompile(sys; array_hack = false) + sys2 = mtkcompile(sys; reassemble_alg) @test length(observed(sys2)) == 4 @test !any(observed(sys2)) do eq - iscall(eq.rhs) && operation(eq.rhs) == StructuralTransformations.change_origin + iscall(eq.rhs) && operation(eq.rhs) == MTKTearing.change_origin end end @@ -88,10 +93,10 @@ end @named sys = System( [D(x) ~ z[1] + z[2] + foo(z)[1] + w, y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) - sys2 = mtkcompile(sys; array_hack = false, fully_determined = false) + sys2 = mtkcompile(sys; reassemble_alg, fully_determined = false) @test length(observed(sys2)) == 4 @test !any(observed(sys2)) do eq - iscall(eq.rhs) && operation(eq.rhs) == StructuralTransformations.change_origin + iscall(eq.rhs) && operation(eq.rhs) == MTKTearing.change_origin end end end @@ -220,9 +225,11 @@ end @testset "Issue#3480: Derivatives of time-dependent parameters" begin @component function FilteredInput(; name, x0 = 0, T = 0.1) params = @parameters begin - k(t) = x0 T = T end + discs = @discretes begin + k(t) = x0 + end vars = @variables begin x(t) = k dx(t) = 0 @@ -232,16 +239,19 @@ end eqs = [D(x) ~ dx D(dx) ~ ddx dx ~ (k - x) / T] - return System(eqs, t, vars, params; systems, name) + evt = SymbolicDiscreteCallback([1.0], [k ~ x]; discrete_parameters = [k]) + return System(eqs, t, [vars; discs], params; systems, name, discrete_events = [evt]) end @component function FilteredInputExplicit(; name, x0 = 0, T = 0.1) params = @parameters begin - k(t)[1:1] = [x0] T = T end + discs = @discretes begin + k(t)[1:1] = [x0] + end vars = @variables begin - x(t) = k + x(t) = k[1] dx(t) = 0 ddx(t) end @@ -250,14 +260,17 @@ end D(dx) ~ ddx D(k[1]) ~ 1.0 dx ~ (k[1] - x) / T] - return System(eqs, t, vars, params; systems, name) + evt = SymbolicDiscreteCallback([1.0], [k[1] ~ x]; discrete_parameters = [k]) + return System(eqs, t, [vars; discs], params; systems, name, discrete_events = [evt]) end @component function FilteredInputErr(; name, x0 = 0, T = 0.1) params = @parameters begin - k(t) = x0 T = T end + discs = @discretes begin + k(t) = x0 + end vars = @variables begin x(t) = k dx(t) = 0 @@ -268,7 +281,8 @@ end D(dx) ~ ddx dx ~ (k - x) / T D(k) ~ missing] - return System(eqs, t, vars, params; systems, name) + evt = SymbolicDiscreteCallback([1.0], [k ~ x]; discrete_parameters = [k]) + return System(eqs, t, [vars; discs], params; systems, name, discrete_events = [evt]) end @named sys = FilteredInputErr() @@ -277,10 +291,10 @@ end @mtkcompile sys = FilteredInput() vs = Set() for eq in equations(sys) - ModelingToolkit.vars!(vs, eq) + SU.search_variables!(vs, eq) end for eq in observed(sys) - ModelingToolkit.vars!(vs, eq) + SU.search_variables!(vs, eq) end @test !(D(sys.k) in vs) @@ -315,10 +329,10 @@ end @mtkcompile sys = FilteredInput2() vs = Set() for eq in equations(sys) - ModelingToolkit.vars!(vs, eq) + SU.search_variables!(vs, eq) end for eq in observed(sys) - ModelingToolkit.vars!(vs, eq) + SU.search_variables!(vs, eq) end @test D(sys.k(t)) in vs @@ -328,10 +342,11 @@ end @testset "Don't rely on metadata" begin @testset "ODESystem" begin @variables x(t) p - @parameters y(t) q + @parameters q + @discretes y(t) @mtkcompile sys = System([D(x) ~ x * q, x^2 + y^2 ~ p], t, [x, y], [p, q]; initialization_eqs = [p + q ~ 3], - defaults = [p => missing], guesses = [p => 1.0, y => 1.0]) + bindings = [p => missing], guesses = [p => 1.0, y => 1.0]) @test length(equations(sys)) == 2 @test length(parameters(sys)) == 2 prob = ODEProblem(sys, [x => 1.0, q => 2.0], (0.0, 1.0)) @@ -345,7 +360,7 @@ end @parameters y q @mtkcompile sys = System([0 ~ p * x + y, x^3 + y^3 ~ q], [x, y], [p, q]; initialization_eqs = [p ~ q + 1], - guesses = [p => 1.0], defaults = [p => missing]) + guesses = [p => 1.0], bindings = [p => missing]) @test length(equations(sys)) == length(unknowns(sys)) == 1 @test length(observed(sys)) == 1 @test observed(sys)[1].lhs in Set([x, y]) @@ -357,11 +372,12 @@ end @testset "SDESystem" begin @variables x(t) p a - @parameters y(t) q b + @parameters q b + @discretes y(t) @brownians c @mtkcompile sys = System([D(x) ~ x + q * a, D(y) ~ y + p * b + c], t, [x, y], [p, q], [a, b, c]; initialization_eqs = [p + q ~ 4], - guesses = [p => 1.0], defaults = [p => missing]) + guesses = [p => 1.0], bindings = [p => missing]) @test length(equations(sys)) == 2 @test issetequal(unknowns(sys), [x, y]) @test issetequal(parameters(sys), [p, q]) diff --git a/test/substitute_component.jl b/test/substitute_component.jl index e598d86a06..70f14fbfa1 100644 --- a/test/substitute_component.jl +++ b/test/substitute_component.jl @@ -1,7 +1,7 @@ using ModelingToolkit, ModelingToolkitStandardLibrary, Test using ModelingToolkitStandardLibrary.Blocks using ModelingToolkitStandardLibrary.Electrical -using OrdinaryDiffEq +using OrdinaryDiffEq, SciCompDSL using ModelingToolkit: t_nounits as t, D_nounits as D, renamespace, NAMESPACE_SEPARATOR as NS diff --git a/test/symbolic_parameters.jl b/test/symbolic_parameters.jl deleted file mode 100644 index 5a01c3d645..0000000000 --- a/test/symbolic_parameters.jl +++ /dev/null @@ -1,66 +0,0 @@ -using ModelingToolkit -using NonlinearSolve -using Test -using ModelingToolkit: t_nounits as t, D_nounits as D - -@variables x y z u -@parameters σ ρ β - -eqs = [0 ~ σ * (y - x), - 0 ~ x * (ρ - z) - y, - 0 ~ x * y - β * z] - -par = [ - σ => 1, - ρ => 0.1 + σ, - β => ρ * 1.1 -] -u0 = [ - x => u, - y => σ, # default u0 from default p - z => u - 0.1 -] -ns = System(eqs, [x, y, z], [σ, ρ, β], name = :ns, defaults = [par; u0]) -ModelingToolkit.get_defaults(ns)[y] = u * 1.1 -resolved = ModelingToolkit.varmap_to_vars(defaults(ns), parameters(ns)) -@test resolved == [1, 0.1 + 1, (0.1 + 1) * 1.1] - -prob = NonlinearProblem(complete(ns), [u => 1.0]) -@test prob.u0 == [1.0, 1.1, 0.9] -sol = solve(prob, NewtonRaphson()) - -@variables a -@parameters b -top = System([0 ~ -a + ns.x + b], [a], [b], systems = [ns], name = :top) -ModelingToolkit.get_defaults(top)[b] = ns.σ * 0.5 -ModelingToolkit.get_defaults(top)[ns.x] = unknowns(ns, u) * 0.5 - -res = ModelingToolkit.varmap_to_vars(defaults(top), parameters(top)) -@test res == [0.5, 1, 0.1 + 1, (0.1 + 1) * 1.1] - -top = complete(top) -prob = NonlinearProblem(top, [unknowns(ns, u) => 1.0, a => 1.0]) -@test prob.u0 == [1.0, 0.5, 1.1, 0.9] -sol = solve(prob, NewtonRaphson()) - -# test NullParameters+defaults -prob = NonlinearProblem(top, [unknowns(ns, u) => 1.0, a => 1.0]) -@test prob.u0 == [1.0, 0.5, 1.1, 0.9] -sol = solve(prob, NewtonRaphson()) - -# test initial conditions and parameters at the problem level -pars = @parameters(begin - x0 -end) -vars = @variables(begin - x(ModelingToolkit.t_nounits) -end) -der = Differential(t) -eqs = [der(x) ~ x] -@named sys = System(eqs, t, vars, [x0]) -sys = complete(sys) -initialValues = [x => x0 - x0 => 10.0] -tspan = (0.0, 1.0) -problem = ODEProblem(sys, initialValues, tspan) -@test problem.u0 isa Vector{Float64} diff --git a/test/units.jl b/test/units.jl deleted file mode 100644 index a17dd90575..0000000000 --- a/test/units.jl +++ /dev/null @@ -1,242 +0,0 @@ -using ModelingToolkit, OrdinaryDiffEq, JumpProcesses, Unitful -using Test -MT = ModelingToolkit -UMT = ModelingToolkit.UnitfulUnitCheck -@independent_variables t [unit = u"ms"] -@parameters τ [unit = u"ms"] γ -@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] -D = Differential(t) - -#This is how equivalent works: -@test UMT.equivalent(u"MW", u"kJ/ms") -@test !UMT.equivalent(u"m", u"cm") -@test UMT.equivalent(UMT.get_unit(P^γ), UMT.get_unit((E / τ)^γ)) - -# Basic access -@test UMT.get_unit(t) == u"ms" -@test UMT.get_unit(E) == u"kJ" -@test UMT.get_unit(τ) == u"ms" -@test UMT.get_unit(γ) == UMT.unitless -@test UMT.get_unit(0.5) == UMT.unitless -@test UMT.get_unit(UMT.SciMLBase.NullParameters()) == UMT.unitless - -# Prohibited unit types -@parameters β [unit = u"°"] α [unit = u"°C"] γ [unit = 1u"s"] -@test_throws UMT.ValidationError UMT.get_unit(β) -@test_throws UMT.ValidationError UMT.get_unit(α) -@test_throws UMT.ValidationError UMT.get_unit(γ) - -# Non-trivial equivalence & operators -@test UMT.get_unit(τ^-1) == u"ms^-1" -@test UMT.equivalent(UMT.get_unit(D(E)), u"MW") -@test UMT.equivalent(UMT.get_unit(E / τ), u"MW") -@test UMT.get_unit(2 * P) == u"MW" -@test UMT.get_unit(t / τ) == UMT.unitless -@test UMT.equivalent(UMT.get_unit(P - E / τ), u"MW") -@test UMT.equivalent(UMT.get_unit(D(D(E))), u"MW/ms") -@test UMT.get_unit(ifelse(t > t, P, E / τ)) == u"MW" -@test UMT.get_unit(1.0^(t / τ)) == UMT.unitless -@test UMT.get_unit(exp(t / τ)) == UMT.unitless -@test UMT.get_unit(sin(t / τ)) == UMT.unitless -@test UMT.get_unit(sin(1 * u"rad")) == UMT.unitless -@test UMT.get_unit(t^2) == u"ms^2" - -eqs = [D(E) ~ P - E / τ - 0 ~ P] -@test UMT.validate(eqs) -@named sys = System(eqs, t) - -@test !UMT.validate(D(D(E)) ~ P) -@test !UMT.validate(0 ~ P + E * τ) - -# Disabling unit validation/checks selectively -@test_throws MT.ArgumentError System(eqs, t, [E, P, t], [τ], name = :sys) -System(eqs, t, [E, P, t], [τ], name = :sys, checks = MT.CheckUnits) -eqs = [D(E) ~ P - E / τ - 0 ~ P + E * τ] -@test_throws MT.ValidationError System(eqs, t, name = :sys, checks = MT.CheckAll) -@test_throws MT.ValidationError System(eqs, t, name = :sys, checks = true) -System(eqs, t, name = :sys, checks = MT.CheckNone) -System(eqs, t, name = :sys, checks = false) -@test_throws MT.ValidationError System(eqs, t, name = :sys, - checks = MT.CheckComponents | MT.CheckUnits) -@named sys = System(eqs, t, checks = MT.CheckComponents) -@test_throws MT.ValidationError System(eqs, t, [E, P, t], [τ], name = :sys, - checks = MT.CheckUnits) - -# connection validation -@connector function Pin(; name) - sts = @variables(v(t)=1.0, [unit=u"V"], - i(t)=1.0, [unit=u"A", connect=Flow]) - System(Equation[], t, sts, []; name = name) -end -@connector function OtherPin(; name) - sts = @variables(v(t)=1.0, [unit=u"mV"], - i(t)=1.0, [unit=u"mA", connect=Flow]) - System(Equation[], t, sts, []; name = name) -end -@connector function LongPin(; name) - sts = @variables(v(t)=1.0, [unit=u"V"], - i(t)=1.0, [unit=u"A", connect=Flow], - x(t)=1.0, [unit=NoUnits]) - System(Equation[], t, sts, []; name = name) -end -@named p1 = Pin() -@named p2 = Pin() -@named op = OtherPin() -@named lp = LongPin() -good_eqs = [connect(p1, p2)] -bad_eqs = [connect(p1, p2, op)] -bad_length_eqs = [connect(op, lp)] -@test UMT.validate(good_eqs) -@test !UMT.validate(bad_eqs) -@test !UMT.validate(bad_length_eqs) -@named sys = System(good_eqs, t, [], []) -@test_throws MT.ValidationError System(bad_eqs, t, [], []; name = :sys) - -# Array variables -@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] -System(eqs, t, name = :sys) - -# Nonlinear system -@parameters a [unit = u"kg"^-1] -@variables x [unit = u"kg"] -eqs = [ - 0 ~ a * x -] -@named nls = System(eqs, [x], [a]) - -# SDE test w/ noise vector -@independent_variables t [unit = u"ms"] -@parameters τ [unit = u"ms"] Q [unit = u"MW"] -@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"] -D = Differential(t) -eqs = [D(E) ~ P - E / τ - P ~ Q] - -noiseeqs = [0.1u"MW", - 0.1u"MW"] -@named sys = SDESystem(eqs, noiseeqs, t, [P, E], [τ, Q]) - -# With noise matrix -noiseeqs = [0.1u"MW" 0.1u"MW" - 0.1u"MW" 0.1u"MW"] -@named sys = SDESystem(eqs, noiseeqs, t, [P, E], [τ, Q]) - -# Invalid noise matrix -noiseeqs = [0.1u"MW" 0.1u"MW" - 0.1u"MW" 0.1u"s"] -@test !UMT.validate(eqs, noiseeqs) - -# Non-trivial simplifications -@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] -@named sys = System(eqs, t) -sys_simple = mtkcompile(sys) - -eqs = [D(V) ~ r, - V ~ L^3] -@named sys = System(eqs, t) -sys_simple = mtkcompile(sys) - -@variables V [unit = u"m"^3] L [unit = u"m"] -@parameters v [unit = u"m/s"] r [unit = u"m"^3 / u"s"] t [unit = u"s"] -eqs = [V ~ r * t, - V ~ L^3] -@named sys = System(eqs, [V, L], [t, r]) -sys_simple = mtkcompile(sys) - -eqs = [L ~ v * t, - V ~ L^3] -@named sys = System(eqs, [V, L], [v, t, r]) -sys_simple = mtkcompile(sys) - -#Jump System -@parameters β [unit = u"(mol^2*s)^-1"] γ [unit = u"(mol*s)^-1"] t [unit = u"s"] jumpmol [ - unit = u"mol" -] -@variables S(t) [unit = u"mol"] I(t) [unit = u"mol"] R(t) [unit = u"mol"] -rate₁ = β * S * I -affect₁ = [S ~ S - 1 * jumpmol, I ~ I + 1 * jumpmol] -rate₂ = γ * I -affect₂ = [I ~ I - 1 * jumpmol, R ~ R + 1 * jumpmol] -j₁ = ConstantRateJump(rate₁, affect₁) -j₂ = VariableRateJump(rate₂, affect₂) -js = JumpSystem([j₁, j₂], t, [S, I, R], [β, γ], name = :sys) - -affect_wrong = [S ~ S - jumpmol, I ~ I + 1] -j_wrong = ConstantRateJump(rate₁, affect_wrong) -@test_throws MT.ValidationError JumpSystem([j_wrong, j₂], t, [S, I, R], [β, γ], name = :sys) - -rate_wrong = γ^2 * I -j_wrong = ConstantRateJump(rate_wrong, affect₂) -@test_throws MT.ValidationError JumpSystem([j₁, j_wrong], t, [S, I, R], [β, γ], name = :sys) - -# mass action jump tests for SIR model -maj1 = MassActionJump(2 * β / 2, [S => 1, I => 1], [S => -1, I => 1]) -maj2 = MassActionJump(γ, [I => 1], [I => -1, R => 1]) -@named js3 = JumpSystem([maj1, maj2], t, [S, I, R], [β, γ]) - -#Test unusual jump system -@parameters β γ t -@variables S(t) I(t) R(t) - -maj1 = MassActionJump(2.0, [0 => 1], [S => 1]) -maj2 = MassActionJump(γ, [S => 1], [S => -1]) -@named js4 = JumpSystem([maj1, maj2], t, [S], [β, γ]) - -@mtkmodel ParamTest begin - @parameters begin - a, [unit = u"m"] - end - @variables begin - b(t), [unit = u"kg"] - end -end - -@named sys = ParamTest() - -@named sys = ParamTest(a = 3.0u"cm") -@test ModelingToolkit.getdefault(sys.a) ≈ 0.03 - -@test_throws ErrorException ParamTest(; name = :t, a = 1.0) -@test_throws ErrorException ParamTest(; name = :t, a = 1.0u"s") - -@mtkmodel ArrayParamTest begin - @parameters begin - a[1:2], [unit = u"m"] - end -end - -@named sys = ArrayParamTest() - -@named sys = ArrayParamTest(a = [1.0, 3.0]u"cm") -@test ModelingToolkit.getdefault(sys.a) ≈ [0.01, 0.03] - -@variables x(t) -@test ModelingToolkit.get_unit(sin(x)) == ModelingToolkit.unitless - -@mtkmodel ExpressionParametersTest begin - @parameters begin - v = 1.0, [unit = u"m/s"] - τ = 1.0, [unit = u"s"] - end - @components begin - pt = ParamTest(; a = v * τ) - end -end - -@named sys = ExpressionParametersTest(; v = 2.0u"m/s", τ = 3.0u"s") -sys = complete(sys) -# TODO: Is there a way to evaluate this expression and compare to 6.0? -@test isequal(ModelingToolkit.getdefault(sys.pt.a), sys.v * sys.τ) -@test ModelingToolkit.getdefault(sys.v) ≈ 2.0 -@test ModelingToolkit.getdefault(sys.τ) ≈ 3.0