From 8bd3b5485a279e07bb236cbb27cff3f3616bfe8b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 28 Oct 2025 19:38:41 +0530 Subject: [PATCH 1/6] FEAT: move ModelingToolkitStandardLibrary to subdirectory --- .../.JuliaFormatter.toml | 2 + .../.github/dependabot.yml | 10 + .../.github/workflows/CompatHelper.yml | 26 + .../.github/workflows/Documentation.yml | 18 + .../.github/workflows/Downgrade.yml | 32 + .../.github/workflows/Downstream.yml | 55 + .../.github/workflows/FormatCheck.yml | 13 + .../.github/workflows/SpellCheck.yml | 13 + .../.github/workflows/TagBot.yml | 15 + .../.github/workflows/Tests.yml | 34 + lib/ModelingToolkitStandardLibrary/.gitignore | 24 + .../.typos.toml | 14 + lib/ModelingToolkitStandardLibrary/LICENSE | 21 + .../Project.toml | 53 + lib/ModelingToolkitStandardLibrary/README.md | 83 ++ .../docs/Project.toml | 20 + .../docs/make.jl | 41 + .../docs/pages.jl | 23 + .../docs/src/API/blocks.md | 96 ++ .../docs/src/API/electrical.md | 90 ++ .../docs/src/API/hydraulic.md | 51 + .../docs/src/API/linear_analysis.md | 154 +++ .../docs/src/API/magnetic.md | 45 + .../docs/src/API/mechanical.md | 96 ++ .../docs/src/API/thermal.md | 51 + .../docs/src/assets/favicon.ico | Bin 0 -> 1394 bytes .../docs/src/assets/logo.png | Bin 0 -> 26575 bytes .../docs/src/connectors/connections.md | 360 +++++++ .../docs/src/connectors/damper.svg | 237 +++++ .../docs/src/connectors/force_input.svg | 155 +++ .../docs/src/connectors/mass.svg | 164 +++ .../docs/src/connectors/model.png | Bin 0 -> 12022 bytes .../docs/src/connectors/model.svg | 201 ++++ .../docs/src/connectors/sign_convention.md | 189 ++++ .../docs/src/connectors/through_across.png | Bin 0 -> 30457 bytes .../docs/src/connectors/through_across.svg | 317 ++++++ .../docs/src/index.md | 105 ++ .../docs/src/tutorials/MOSFET_calibration.md | 79 ++ .../docs/src/tutorials/custom_component.md | 127 +++ .../docs/src/tutorials/dc_motor_pi.md | 141 +++ .../docs/src/tutorials/input_component.md | 295 ++++++ .../docs/src/tutorials/rc_circuit.md | 43 + .../docs/src/tutorials/thermal_model.md | 43 + .../src/Blocks/Blocks.jl | 34 + .../src/Blocks/continuous.jl | 670 ++++++++++++ .../src/Blocks/math.jl | 523 ++++++++++ .../src/Blocks/nonlinear.jl | 122 +++ .../src/Blocks/sources.jl | 876 ++++++++++++++++ .../src/Blocks/utils.jl | 169 +++ .../src/Electrical/Analog/ideal_components.jl | 393 +++++++ .../src/Electrical/Analog/mosfets.jl | 166 +++ .../src/Electrical/Analog/sensors.jl | 164 +++ .../src/Electrical/Analog/sources.jl | 49 + .../src/Electrical/Analog/transistors.jl | 333 ++++++ .../src/Electrical/Digital/components.jl | 285 ++++++ .../src/Electrical/Digital/gates.jl | 178 ++++ .../src/Electrical/Digital/logic.jl | 54 + .../src/Electrical/Digital/logic_vectors.jl | 43 + .../src/Electrical/Digital/sources.jl | 88 ++ .../src/Electrical/Digital/tables.jl | 134 +++ .../src/Electrical/Electrical.jl | 53 + .../src/Electrical/utils.jl | 107 ++ .../src/Hydraulic/Hydraulic.jl | 10 + .../IsothermalCompressible.jl | 24 + .../IsothermalCompressible/components.jl | 968 ++++++++++++++++++ .../IsothermalCompressible/sources.jl | 67 ++ .../Hydraulic/IsothermalCompressible/utils.jl | 172 ++++ .../src/Magnetic/FluxTubes/FluxTubes.jl | 16 + .../src/Magnetic/FluxTubes/basic.jl | 158 +++ .../src/Magnetic/FluxTubes/sources.jl | 53 + .../src/Magnetic/FluxTubes/utils.jl | 41 + .../src/Magnetic/Magnetic.jl | 12 + .../src/Mechanical/Mechanical.jl | 14 + .../src/Mechanical/MultiBody2D/MultiBody2D.jl | 10 + .../src/Mechanical/MultiBody2D/components.jl | 88 ++ .../src/Mechanical/Rotational/Rotational.jl | 23 + .../src/Mechanical/Rotational/components.jl | 269 +++++ .../src/Mechanical/Rotational/sensors.jl | 92 ++ .../src/Mechanical/Rotational/sources.jl | 181 ++++ .../src/Mechanical/Rotational/utils.jl | 189 ++++ .../Mechanical/Translational/Translational.jl | 24 + .../Mechanical/Translational/components.jl | 185 ++++ .../src/Mechanical/Translational/sensors.jl | 85 ++ .../src/Mechanical/Translational/sources.jl | 99 ++ .../src/Mechanical/Translational/utils.jl | 13 + .../TranslationalModelica.jl | 19 + .../TranslationalModelica/components.jl | 149 +++ .../TranslationalModelica/sources.jl | 58 ++ .../Mechanical/TranslationalModelica/utils.jl | 161 +++ .../TranslationalPosition.jl | 22 + .../TranslationalPosition/components.jl | 175 ++++ .../TranslationalPosition/sensors.jl | 81 ++ .../TranslationalPosition/sources.jl | 18 + .../Mechanical/TranslationalPosition/utils.jl | 149 +++ .../src/ModelingToolkitStandardLibrary.jl | 26 + .../Thermal/HeatTransfer/ideal_components.jl | 219 ++++ .../src/Thermal/HeatTransfer/sensors.jl | 80 ++ .../src/Thermal/HeatTransfer/sources.jl | 121 +++ .../src/Thermal/Thermal.jl | 27 + .../src/Thermal/utils.jl | 90 ++ .../test/Blocks/continuous.jl | 492 +++++++++ .../test/Blocks/math.jl | 387 +++++++ .../test/Blocks/nonlinear.jl | 121 +++ .../test/Blocks/sources.jl | 655 ++++++++++++ .../test/Blocks/test_analysis_points.jl | 390 +++++++ .../test/Blocks/utils.jl | 49 + .../test/Electrical/analog.jl | 875 ++++++++++++++++ .../test/Electrical/digital.jl | 473 +++++++++ .../test/Hydraulic/isothermal_compressible.jl | 426 ++++++++ .../test/Magnetic/magnetic.jl | 60 ++ .../test/Mechanical/multibody.jl | 100 ++ .../test/Mechanical/rotational.jl | 322 ++++++ .../test/Mechanical/translational.jl | 239 +++++ .../test/Mechanical/translational_modelica.jl | 109 ++ .../test/Thermal/demo.jl | 26 + .../test/Thermal/motor.jl | 56 + .../test/Thermal/piston.jl | 46 + .../test/Thermal/thermal.jl | 179 ++++ .../test/aqua.jl | 11 + .../test/chua_circuit.jl | 50 + .../test/multi_domain.jl | 204 ++++ .../test/runtests.jl | 81 ++ 122 files changed, 17516 insertions(+) create mode 100644 lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/dependabot.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml create mode 100644 lib/ModelingToolkitStandardLibrary/.gitignore create mode 100644 lib/ModelingToolkitStandardLibrary/.typos.toml create mode 100644 lib/ModelingToolkitStandardLibrary/LICENSE create mode 100644 lib/ModelingToolkitStandardLibrary/Project.toml create mode 100644 lib/ModelingToolkitStandardLibrary/README.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/Project.toml create mode 100644 lib/ModelingToolkitStandardLibrary/docs/make.jl create mode 100644 lib/ModelingToolkitStandardLibrary/docs/pages.jl create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/blocks.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/electrical.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/hydraulic.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/linear_analysis.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/magnetic.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/mechanical.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/API/thermal.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/assets/favicon.ico create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/assets/logo.png create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/connections.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/damper.svg create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/force_input.svg create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/mass.svg create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.png create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.svg create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/sign_convention.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.png create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.svg create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/index.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/MOSFET_calibration.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/custom_component.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/dc_motor_pi.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/input_component.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/rc_circuit.md create mode 100644 lib/ModelingToolkitStandardLibrary/docs/src/tutorials/thermal_model.md create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/Blocks.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/continuous.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/math.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/nonlinear.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Blocks/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/ideal_components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/mosfets.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sensors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/transistors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/gates.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic_vectors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/tables.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/Electrical.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Electrical/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Hydraulic/Hydraulic.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/IsothermalCompressible.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/FluxTubes.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/basic.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Magnetic/Magnetic.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Mechanical.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/MultiBody2D.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/Rotational.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sensors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/Translational.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sensors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/TranslationalModelica.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/TranslationalPosition.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sensors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/ModelingToolkitStandardLibrary.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/ideal_components.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sensors.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Thermal/Thermal.jl create mode 100644 lib/ModelingToolkitStandardLibrary/src/Thermal/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/continuous.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/math.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/nonlinear.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/sources.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/test_analysis_points.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Blocks/utils.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Electrical/analog.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Electrical/digital.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Hydraulic/isothermal_compressible.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Magnetic/magnetic.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Mechanical/multibody.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Mechanical/rotational.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Mechanical/translational.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Mechanical/translational_modelica.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Thermal/demo.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Thermal/motor.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Thermal/piston.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/Thermal/thermal.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/aqua.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/chua_circuit.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/multi_domain.jl create mode 100644 lib/ModelingToolkitStandardLibrary/test/runtests.jl diff --git a/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml b/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml new file mode 100644 index 0000000000..9c79359112 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml @@ -0,0 +1,2 @@ +style = "sciml" +format_markdown = true \ No newline at end of file diff --git a/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml b/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml new file mode 100644 index 0000000000..ec3b005a0e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml @@ -0,0 +1,10 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + ignore: + - dependency-name: "crate-ci/typos" + update-types: ["version-update:semver-patch", "version-update:semver-minor"] diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000000..73494545f2 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml @@ -0,0 +1,26 @@ +name: CompatHelper + +on: + schedule: + - cron: '00 * * * *' + issues: + types: [opened, reopened] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1] + julia-arch: [x86] + os: [ubuntu-latest] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia -e 'using CompatHelper; CompatHelper.main(;subdirs=["", "docs"])' diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml new file mode 100644 index 0000000000..785116ee5a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml @@ -0,0 +1,18 @@ +name: "Documentation" + +on: + push: + branches: + - main + tags: '*' + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} + +jobs: + build-and-deploy-docs: + name: "Documentation" + uses: "SciML/.github/.github/workflows/documentation.yml@v1" + secrets: "inherit" diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml new file mode 100644 index 0000000000..178f6c35d7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml @@ -0,0 +1,32 @@ +name: Downgrade +on: + pull_request: + branches: + - main + paths-ignore: + - 'docs/**' + push: + branches: + - master + paths-ignore: + - 'docs/**' +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + downgrade_mode: ['alldeps'] + julia-version: ['1.10'] + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-downgrade-compat@v2 +# if: ${{ matrix.version == '1.6' }} + with: + skip: Pkg,TOML + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + with: + ALLOW_RERESOLVE: false diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml new file mode 100644 index 0000000000..7cc4149120 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml @@ -0,0 +1,55 @@ +name: IntegrationTest +on: + push: + branches: [main] + tags: [v*] + pull_request: + +jobs: + test: + name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} + runs-on: ${{ matrix.os }} + env: + GROUP: ${{ matrix.package.group }} + strategy: + fail-fast: false + matrix: + julia-version: [1] + os: [ubuntu-latest] + package: + - {user: SciML, repo: ModelingToolkit.jl} + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + arch: x64 + - uses: julia-actions/julia-buildpkg@latest + - name: Clone Downstream + uses: actions/checkout@v4 + with: + repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} + path: downstream + - name: Load this and run the downstream tests + shell: julia --color=yes --project=downstream {0} + run: | + using Pkg + try + # force it to use this PR's version of the package + Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps + Pkg.update() + Pkg.test(coverage=true) # resolver may fail with test time deps + catch err + err isa Pkg.Resolve.ResolverError || rethrow() + # If we can't resolve that means this is incompatible by SemVer and this is fine + # It means we marked this as a breaking change, so we don't need to worry about + # Mistakenly introducing a breaking change, as we have intentionally made one + @info "Not compatible with this release. No problem." exception=err + exit(0) # Exit immediately, as a success + end + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + file: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml new file mode 100644 index 0000000000..7e46c8db90 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml @@ -0,0 +1,13 @@ +name: "Format Check" + +on: + push: + branches: + - 'main' + tags: '*' + pull_request: + +jobs: + format-check: + name: "Format Check" + uses: "SciML/.github/.github/workflows/format-check.yml@v1" diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml new file mode 100644 index 0000000000..9246edd2af --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml @@ -0,0 +1,13 @@ +name: Spell Check + +on: [pull_request] + +jobs: + typos-check: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Check spelling + uses: crate-ci/typos@v1.18.0 \ No newline at end of file diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml new file mode 100644 index 0000000000..f49313b662 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml @@ -0,0 +1,15 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml new file mode 100644 index 0000000000..29b66299f5 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml @@ -0,0 +1,34 @@ +name: "Tests" + +on: + pull_request: + paths-ignore: + - 'docs/**' + push: + branches: + - main + paths-ignore: + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} + +jobs: + tests: + name: "Tests" + strategy: + fail-fast: false + matrix: + version: + - "1" + - "lts" + - "pre" + group: + - "Core" + - "QA" + uses: "SciML/.github/.github/workflows/tests.yml@v1" + with: + julia-version: "${{ matrix.version }}" + group: "${{ matrix.group }}" + secrets: "inherit" diff --git a/lib/ModelingToolkitStandardLibrary/.gitignore b/lib/ModelingToolkitStandardLibrary/.gitignore new file mode 100644 index 0000000000..29126e47b0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.gitignore @@ -0,0 +1,24 @@ +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov + +# Files generated by invoking Julia with --track-allocation +*.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest.toml diff --git a/lib/ModelingToolkitStandardLibrary/.typos.toml b/lib/ModelingToolkitStandardLibrary/.typos.toml new file mode 100644 index 0000000000..906888d1c3 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/.typos.toml @@ -0,0 +1,14 @@ +[default.extend-words] +Nd = "Nd" +nin = "nin" +coul = "coul" +isconnection = "isconnection" +# Additional SciML terms +setp = "setp" +getp = "getp" +indexin = "indexin" +ists = "ists" +ispcs = "ispcs" +eqs = "eqs" +rhs = "rhs" +MTK = "MTK" \ No newline at end of file diff --git a/lib/ModelingToolkitStandardLibrary/LICENSE b/lib/ModelingToolkitStandardLibrary/LICENSE new file mode 100644 index 0000000000..84dfd7edec --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Julia Computing + +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/ModelingToolkitStandardLibrary/Project.toml b/lib/ModelingToolkitStandardLibrary/Project.toml new file mode 100644 index 0000000000..89dd78f93d --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/Project.toml @@ -0,0 +1,53 @@ +name = "ModelingToolkitStandardLibrary" +uuid = "16a59e39-deab-5bd0-87e4-056b12336739" +authors = ["Chris Rackauckas and Julia Computing"] +version = "2.25.0" + +[deps] +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" +IfElse = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" + +[compat] +ADTypes = "1" +Aqua = "0.8" +ChainRulesCore = "1.24" +ControlSystemsBase = "1.4" +DataFrames = "1.7" +DataInterpolations = "6" +DiffEqBase = "6.152" +ForwardDiff = "0.10" +IfElse = "0.1" +LinearAlgebra = "1.10" +ModelingToolkit = "10" +OrdinaryDiffEq = "6.87" +OrdinaryDiffEqDefault = "1.1" +PreallocationTools = "0.4.23" +SafeTestsets = "0.1" +SciMLStructures = "1.4.2" +SymbolicIndexingInterface = "0.3.28" +Symbolics = "6.14" +Test = "1" +julia = "1.10" + +[extras] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["ADTypes", "Aqua", "LinearAlgebra", "OrdinaryDiffEqDefault", "OrdinaryDiffEq", "SafeTestsets", "Test", "ControlSystemsBase", "DataFrames", "DataInterpolations", "SciMLStructures", "SymbolicIndexingInterface", "ForwardDiff"] diff --git a/lib/ModelingToolkitStandardLibrary/README.md b/lib/ModelingToolkitStandardLibrary/README.md new file mode 100644 index 0000000000..b0b04b9f21 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/README.md @@ -0,0 +1,83 @@ +# ModelingToolkitStandardLibrary.jl + +[![Join the chat at https://julialang.zulipchat.com #sciml-bridged](https://img.shields.io/static/v1?label=Zulip&message=chat&color=9558b2&labelColor=389826)](https://julialang.zulipchat.com/#narrow/stream/279055-sciml-bridged) +[![Global Docs](https://img.shields.io/badge/docs-SciML-blue.svg)](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/) + +[![codecov](https://codecov.io/gh/SciML/ModelingToolkitStandardLibrary.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/SciML/ModelingToolkitStandardLibrary.jl) +[![Build Status](https://github.com/SciML/ModelingToolkitStandardLibrary.jl/workflows/CI/badge.svg)](https://github.com/SciML/ModelingToolkitStandardLibrary.jl/actions?query=workflow%3ACI) + +[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor%27s%20Guide-blueviolet)](https://github.com/SciML/ColPrac) +[![SciML Code Style](https://img.shields.io/static/v1?label=code%20style&message=SciML&color=9558b2&labelColor=389826)](https://github.com/SciML/SciMLStyle) + +The ModelingToolkit Standard Library is a standard library of components to model the world and beyond. + +![](https://user-images.githubusercontent.com/1814174/172000112-3579f5cf-c370-48c2-8047-558fbc46aeb6.png) + +## Installation + +Assuming that you already have Julia correctly installed, it suffices to import +ModelingToolkitStandardLibrary.jl in the standard way: + +```julia +import Pkg; +Pkg.add("ModelingToolkitStandardLibrary"); +``` + +## Tutorials and Documentation + +For information on using the package, +[see the stable documentation](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/). Use the +[in-development documentation](https://docs.sciml.ai/ModelingToolkitStandardLibrary/dev/) for the version of +the documentation, which contains the unreleased features. + +## Libraries + +The following are the constituent libraries of the ModelingToolkit Standard Library. + + - [Basic Blocks](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/blocks/) + - [Mechanical Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/mechanical/) + - [Electrical Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/electrical/) + - [Magnetic Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/magnetic/) + - [Thermal Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/thermal/) + +## Example + +The following is the [RC Circuit Demonstration](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/tutorials/rc_circuit/): + +```julia +using ModelingToolkit, OrdinaryDiffEq, Plots +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Blocks: Constant +using ModelingToolkit: t_nounits as t + +@mtkmodel RC begin + @parameters begin + R = 1.0 + C = 1.0 + V = 1.0 + end + @components begin + resistor = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + @equations begin + connect(constant.output, source.V) + connect(source.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + end +end + +@mtkcompile sys = RC() +prob = ODEProblem(sys, Pair[], (0, 10.0)) +sol = solve(prob) + +plot(sol, idxs = [sys.capacitor.v, sys.resistor.i], + title = "RC Circuit Demonstration", + labels = ["Capacitor Voltage" "Resistor Current"]) +``` + +![](https://user-images.githubusercontent.com/1814174/164912983-c3f73628-0e19-4e42-b085-4f62ba6f23d1.png) diff --git a/lib/ModelingToolkitStandardLibrary/docs/Project.toml b/lib/ModelingToolkitStandardLibrary/docs/Project.toml new file mode 100644 index 0000000000..e176c2e6c6 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/Project.toml @@ -0,0 +1,20 @@ +[deps] +ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +IfElse = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173" +ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +ControlSystemsBase = "1.1" +DataFrames = "1.7" +DataInterpolations = "6.4, 7, 8" +Documenter = "1" +IfElse = "0.1" +ModelingToolkit = "10" +OrdinaryDiffEq = "6.31" +Plots = "1.36" diff --git a/lib/ModelingToolkitStandardLibrary/docs/make.jl b/lib/ModelingToolkitStandardLibrary/docs/make.jl new file mode 100644 index 0000000000..a0cac1b6d6 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/make.jl @@ -0,0 +1,41 @@ +using Documenter, ModelingToolkitStandardLibrary +using ModelingToolkit +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkitStandardLibrary.Mechanical +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Magnetic +using ModelingToolkitStandardLibrary.Magnetic.FluxTubes +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Thermal +using ModelingToolkitStandardLibrary.Hydraulic +using ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible + +cp("./docs/Manifest.toml", "./docs/src/assets/Manifest.toml", force = true) +cp("./docs/Project.toml", "./docs/src/assets/Project.toml", force = true) + +ENV["GKSwstype"] = "100" + +include("pages.jl") + +makedocs(sitename = "ModelingToolkitStandardLibrary.jl", + authors = "Julia Computing", + modules = [ModelingToolkit, + ModelingToolkitStandardLibrary, + ModelingToolkitStandardLibrary.Blocks, + ModelingToolkitStandardLibrary.Mechanical, + ModelingToolkitStandardLibrary.Mechanical.Rotational, + ModelingToolkitStandardLibrary.Magnetic, + ModelingToolkitStandardLibrary.Magnetic.FluxTubes, + ModelingToolkitStandardLibrary.Electrical, + ModelingToolkitStandardLibrary.Thermal, + ModelingToolkitStandardLibrary.Hydraulic, + ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible], + clean = true, doctest = false, linkcheck = true, + linkcheck_ignore = ["https://www.mathworks.com/help/simscape/ug/basic-principles-of-modeling-physical-networks.html#bq89sba-6"], + warnonly = [:docs_block, :missing_docs, :cross_references], + format = Documenter.HTML(assets = ["assets/favicon.ico"], + canonical = "https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/"), + pages = pages) + +deploydocs(repo = "github.com/SciML/ModelingToolkitStandardLibrary.jl"; + push_preview = true) diff --git a/lib/ModelingToolkitStandardLibrary/docs/pages.jl b/lib/ModelingToolkitStandardLibrary/docs/pages.jl new file mode 100644 index 0000000000..d2331982bc --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/pages.jl @@ -0,0 +1,23 @@ +pages = [ + "ModelingToolkitStandardLibrary.jl: A Standard Library for ModelingToolkit" => "index.md", + "Tutorials" => [ + "RC Circuit" => "tutorials/rc_circuit.md", + "Custom Components" => "tutorials/custom_component.md", + "Thermal Conduction Model" => "tutorials/thermal_model.md", + "DC Motor with Speed Controller" => "tutorials/dc_motor_pi.md", + "SampledData Component" => "tutorials/input_component.md" + ], + "About Acausal Connections" => [ + "Theory" => "connectors/connections.md", + "Sign Convention" => "connectors/sign_convention.md" + ], + "API" => [ + "Basic Blocks" => "API/blocks.md", + "Electrical Components" => "API/electrical.md", + "Magnetic Components" => "API/magnetic.md", + "Mechanical Components" => "API/mechanical.md", + "Thermal Components" => "API/thermal.md", + "Hydraulic Components" => "API/hydraulic.md", + "Linear Analysis" => "API/linear_analysis.md" + ] +] diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/blocks.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/blocks.md new file mode 100644 index 0000000000..ebeb90c19e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/blocks.md @@ -0,0 +1,96 @@ +# ModelingToolkitStandardLibrary: Blocks + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Blocks +``` + +```@contents +Pages = ["blocks.md"] +``` + +## Index + +```@index +Pages = ["blocks.md"] +``` + +## Utility Blocks + +```@docs +RealInput +RealOutput +RealInputArray +RealOutputArray +SISO +MIMO +``` + +## Math Blocks + +```@docs +Gain +MatrixGain +Sum +Feedback +Add +Add3 +Product +Division +UnaryMinus +Power +Modulo +Floor +Ceil +StaticNonLinearity +Abs +Sign +Sqrt +Sin +Cos +Tan +Asin +Acos +Atan +Atan2 +Sinh +Cosh +Tanh +Exp +Log +Log10 +``` + +## Source Blocks + +```@docs +Constant +Sine +Cosine +ContinuousClock +Ramp +Step +ExpSine +``` + +## Nonlinear Blocks + +```@docs +Limiter +DeadZone +SlewRateLimiter +``` + +## Continuous Blocks + +```@docs +Integrator +Derivative +FirstOrder +SecondOrder +StateSpace +TransferFunction +PI +LimPI +PID +LimPID +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/electrical.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/electrical.md new file mode 100644 index 0000000000..788796abfa --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/electrical.md @@ -0,0 +1,90 @@ +# ModelingToolkitStandardLibrary: Electrical Components + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Electrical +``` + +```@contents +Pages = ["electrical.md"] +``` + +## Index + +```@index +Pages = ["electrical.md"] +``` + +## Electrical Utilities + +```@docs +Pin +OnePort +DigitalPin +``` + +## Analog Components + +```@docs +Ground +Resistor +Conductor +Capacitor +Inductor +IdealOpAmp +Diode +HeatingDiode +VariableResistor +NMOS +PMOS +PNP +NPN +``` + +## Analog Sensors + +```@docs +CurrentSensor +PotentialSensor +VoltageSensor +PowerSensor +MultiSensor +``` + +## Analog Sources + +```@docs +Voltage +Current +``` + +## Digital Gates + +```@docs +Not +And +Nand +Or +Nor +Xor +Xnor +``` + +## Digital Components + +```@docs +HalfAdder +FullAdder +MUX +DEMUX +Encoder +Decoder +``` + +## Digital Sources + +```@docs +PulseDiff +Set +Reset +Pulse +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/hydraulic.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/hydraulic.md new file mode 100644 index 0000000000..f1063548b9 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/hydraulic.md @@ -0,0 +1,51 @@ +# [ModelingToolkit Standard Library: Hydraulic Components](@id hydraulic) + +```@contents +Pages = ["hydraulic.md"] +Depth = 3 +``` + +## Index + +```@index +Pages = ["hydraulic.md"] +``` + +## IsothermalCompressible Components + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible +``` + +### IsothermalCompressible Utils + +```@docs +HydraulicPort +HydraulicFluid +friction_factor +``` + +### IsothermalCompressible Components + +```@docs +Cap +Open +TubeBase +Tube +FlowDivider +Valve +FixedVolume +Volume +DynamicVolume +SpoolValve +SpoolValve2Way +Actuator +``` + +### IsothermalCompressible Sources + +```@docs +MassFlow +Pressure +FixedPressure +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/linear_analysis.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/linear_analysis.md new file mode 100644 index 0000000000..af2369be6a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/linear_analysis.md @@ -0,0 +1,154 @@ +# Linear Analysis + +!!! danger "Experimental" + + The interface described here is currently experimental and at any time subject to breaking changes not respecting semantic versioning. + +Linear analysis refers to the process of linearizing a nonlinear model and analysing the resulting linear dynamical system. To facilitate linear analysis, ModelingToolkitStandardLibrary provides the concept of an [`AnalysisPoint`](@ref), which can be inserted in-between two causal blocks (such as those from the `Blocks` sub module). Once a model containing analysis points is built, several operations are available: + + - [`get_sensitivity`](@ref) get the [sensitivity function (wiki)](https://en.wikipedia.org/wiki/Sensitivity_(control_systems)), $S(s)$, as defined in the field of control theory. + - [`get_comp_sensitivity`](@ref) get the complementary sensitivity function $T(s) : S(s)+T(s)=1$. + - [`get_looptransfer`](@ref) get the (open) loop-transfer function where the loop starts and ends in the analysis point. For a typical simple feedback connection with a plant $P(s)$ and a controller $C(s)$, the loop-transfer function at the plant output is $P(s)C(s)$. + - [`linearize`](@ref) can be called with two analysis points denoting the input and output of the linearized system. + - [`open_loop`](@ref) return a new (nonlinear) system where the loop has been broken in the analysis point, i.e., the connection the analysis point usually implies has been removed. + +An analysis point can be created explicitly using the constructor [`AnalysisPoint`](@ref), or automatically when connecting two causal components using `connect`: + +```julia +connect(comp1.output, :analysis_point_name, comp2.input) +``` + +!!! warning "Causality" + + Analysis points are *causal*, i.e., they imply a directionality for the flow of information. The order of the connections in the connect statement is thus important, i.e., `connect(out, :name, in)` is different from `connect(in, :name, out)`. + +The directionality of an analysis point can be thought of as an arrow in a block diagram, where the name of the analysis point applies to the arrow itself. + +``` +┌─────┐ ┌─────┐ +│ │ name │ │ +│ out├────────►│in │ +│ │ │ │ +└─────┘ └─────┘ +``` + +This is signified by the name being the middle argument to `connect`. + +Of the above mentioned functions, all except for [`open_loop`](@ref) return the output of [`ModelingToolkit.linearize`](@ref), which is + +```julia +matrices, simplified_sys = linearize(...) +# matrices = (; A, B, C, D) +``` + +i.e., `matrices` is a named tuple containing the matrices of a linear state-space system on the form + +```math +\begin{aligned} +\dot x &= Ax + Bu\\ +y &= Cx + Du +\end{aligned} +``` + +## Example + +The following example builds a simple closed-loop system with a plant $P$ and a controller $C$. Two analysis points are inserted, one before and one after $P$. We then derive a number of sensitivity functions and show the corresponding code using the package ControlSystemBase.jl + +```@example LINEAR_ANALYSIS +using ModelingToolkitStandardLibrary.Blocks, ModelingToolkit +@named P = FirstOrder(k = 1, T = 1) # A first-order system with pole in -1 +@named C = Gain(-1) # A P controller +t = ModelingToolkit.get_iv(P) +eqs = [connect(P.output, :plant_output, C.input) # Connect with an automatically created analysis point called :plant_output + connect(C.output, :plant_input, P.input)] +sys = System(eqs, t, systems = [P, C], name = :feedback_system) + +matrices_S = get_sensitivity(sys, :plant_input)[1] # Compute the matrices of a state-space representation of the (input)sensitivity function. +matrices_T = get_comp_sensitivity(sys, :plant_input)[1] +``` + +Continued linear analysis and design can be performed using ControlSystemsBase.jl. +We create `ControlSystemsBase.StateSpace` objects using + +```@example LINEAR_ANALYSIS +using ControlSystemsBase, Plots +S = ss(matrices_S...) +T = ss(matrices_T...) +bodeplot([S, T], lab = ["S" "" "T" ""], plot_title = "Bode plot of sensitivity functions", + margin = 5Plots.mm) +``` + +The sensitivity functions obtained this way should be equivalent to the ones obtained with the code below + +```@example LINEAR_ANALYSIS_CS +using ControlSystemsBase +P = tf(1.0, [1, 1]) |> ss +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) +``` + +We may also derive the loop-transfer function $L(s) = P(s)C(s)$ using + +```@example LINEAR_ANALYSIS +matrices_L = get_looptransfer(sys, :plant_output)[1] +L = ss(matrices_L...) +``` + +which is equivalent to the following with ControlSystems + +```@example LINEAR_ANALYSIS_CS +L = P * (-C) # Add the minus sign to build the negative feedback into the controller +``` + +To obtain the transfer function between two analysis points, we call `linearize` + +```@example LINEAR_ANALYSIS +using ModelingToolkit # hide +matrices_PS = linearize(sys, :plant_input, :plant_output)[1] +``` + +this particular transfer function should be equivalent to the linear system `P(s)S(s)`, i.e., equivalent to + +```@example LINEAR_ANALYSIS_CS +feedback(P, C) +``` + +### Obtaining transfer functions + +A statespace system from [ControlSystemsBase](https://juliacontrol.github.io/ControlSystems.jl/stable/man/creating_systems/) can be converted to a transfer function using the function `tf`: + +```@example LINEAR_ANALYSIS_CS +tf(S) +``` + +## Gain and phase margins + +Further linear analysis can be performed using the [analysis methods from ControlSystemsBase](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/analysis/). For example, calculating the gain and phase margins of a system can be done using + +```@example LINEAR_ANALYSIS_CS +margin(P) +``` + +(they are infinite for this system). A Nyquist plot can be produced using + +```@example LINEAR_ANALYSIS_CS +nyquistplot(P) +``` + +## Index + +```@index +Pages = ["linear_analysis.md"] +``` + +```@autodocs +Modules = [ModelingToolkitStandardLibrary.Blocks] +Pages = ["Blocks/analysis_points.jl"] +Order = [:function, :type] +Private = false +``` + +```@docs +ModelingToolkit.linearize +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/magnetic.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/magnetic.md new file mode 100644 index 0000000000..dd099490b5 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/magnetic.md @@ -0,0 +1,45 @@ +# ModelingToolkitStandardLibrary: Magnetic Components + +```@contents +Pages = ["magnetic.md"] +``` + +## Index + +```@index +Pages = ["magnetic.md"] +``` + +## Flux Tubes + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Magnetic.FluxTubes +``` + +### Flux Tube Utilities + +```@docs +PositiveMagneticPort +NegativeMagneticPort +TwoPort +``` + +### Basic Flux Tube Blocks + +```@docs +Ground +Idle +Short +Crossing +ConstantPermeance +ConstantReluctance +EddyCurrent +ElectroMagneticConverter +``` + +### Flux Tube Sources + +```@docs +ConstantMagneticPotentialDifference +ConstantMagneticFlux +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/mechanical.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/mechanical.md new file mode 100644 index 0000000000..ac0cddafc0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/mechanical.md @@ -0,0 +1,96 @@ +# ModelingToolkit Standard Library: Mechanical Components + +```@contents +Pages = ["mechanical.md"] +Depth = 3 +``` + +## Index + +```@index +Pages = ["mechanical.md"] +``` + +## Rotational Components + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Mechanical.Rotational +``` + +### Rotational Utils + +```@docs +Flange +Support +PartialCompliantWithRelativeStates +PartialElementaryOneFlangeAndSupport2 +PartialElementaryTwoFlangesAndSupport2 +PartialCompliant +``` + +### Rotational Core Components + +```@docs +Fixed +Inertia +Spring +Damper +SpringDamper +IdealGear +RotationalFriction +``` + +### Rotational Sources + +```@docs +Torque +Speed +Position +``` + +### Rotational Sensors + +```@docs +AngleSensor +SpeedSensor +TorqueSensor +RelSpeedSensor +``` + +## Translational Components + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Mechanical.Translational +``` + +### Translational Utils + +```@docs +MechanicalPort +``` + +### Translational Core Components + +```@docs +Mass +Spring +Damper +Fixed +``` + +### Translational Sources + +```@docs +Force +Position +Velocity +Acceleration +``` + +### Translational Sensors + +```@docs +ForceSensor +PositionSensor +AccelerationSensor +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/API/thermal.md b/lib/ModelingToolkitStandardLibrary/docs/src/API/thermal.md new file mode 100644 index 0000000000..d3b9849063 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/API/thermal.md @@ -0,0 +1,51 @@ +# ModelingToolkitStandardLibrary: Thermal Components + +```@meta +CurrentModule = ModelingToolkitStandardLibrary.Thermal +``` + +```@contents +Pages = ["thermal.md"] +``` + +## Index + +```@index +Pages = ["thermal.md"] +``` + +## Thermal Utilities + +```@docs +HeatPort +Element1D +``` + +## Thermal Components + +```@docs +BodyRadiation +ConvectiveConductor +ConvectiveResistor +HeatCapacitor +ThermalConductor +ThermalResistor +ThermalCollector +``` + +## Thermal Sensors + +```@docs +RelativeTemperatureSensor +HeatFlowSensor +TemperatureSensor +``` + +## Thermal Sources + +```@docs +FixedHeatFlow +FixedTemperature +PrescribedHeatFlow +PrescribedTemperature +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/assets/favicon.ico b/lib/ModelingToolkitStandardLibrary/docs/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3c6bd470373d2cccea2154d70907b0e02ab202c8 GIT binary patch literal 1394 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 literal 0 HcmV?d00001 diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/connections.md b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/connections.md new file mode 100644 index 0000000000..be93f9302a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/connections.md @@ -0,0 +1,360 @@ +# Introduction + +In Physical Network Acausal modeling, each physical domain must define a **connector** to combine model components. Each physical domain **connector** defines a minimum of 2 variables, one which is called a *Through* variable, and one which is called an *Across* variable. Both Modelica and SimScape define these variables in the same way: + + - [Modelica Connectors](https://mbe.modelica.university/components/connectors/#acausal-connection) + - [SimScape Connectors](https://www.mathworks.com/help/simscape/ug/basic-principles-of-modeling-physical-networks.html#bq89sba-6) + +However, the standard libraries differ on the selection of the Across variable for the Mechanical Translation and Rotation libraries, Modelica choosing position and angle and SimScape choosing velocity and angular velocity, respectively for Translation and Rotation. Modelica describes their decision [here](https://mbe.modelica.university/components/connectors/simple_domains/). In summary, they would like to provide less integration in the model to avoid lossy numerical behavior, but this decision assumes the lowest order derivative is needed by the model. Numerically it is possible to define the connector either way, but there are some consequences of this decision, and therefore we will study them in detail here as they relate to ModelingToolkit. + +# Through and Across Variable Theory + +### General + +The idea behind the selection of the **through** variable is that it should be a time derivative of some conserved quantity. The conserved quantity should be expressed by the **across** variable. In general terms, the physical system is given by + + - Energy Dissipation & Flow: + +```math +\begin{aligned} + \partial {\color{blue}{across}} / \partial t \cdot c_1 = {\color{green}{through}} \\ + {\color{green}{through}} \cdot c_2 = {\color{blue}{across}} +\end{aligned} +``` + +### Electrical + +For the Electrical domain, the across variable is *voltage* and the through variable *current*. Therefore + + - Energy Dissipation: + +```math +\partial {\color{blue}{voltage}} / \partial t \cdot capacitance = {\color{green}{current}} +``` + + - Flow: + +```math +{\color{green}{current}} \cdot resistance = {\color{blue}{voltage}} +``` + +### Translational + +For the translation domain, choosing *velocity* for the across variable and *force* for the through gives + + - Energy Dissipation: + +```math +\partial {\color{blue}{velocity}} / \partial t \cdot mass = {\color{green}{force}} +``` + + - Flow: + +```math +{\color{green}{force}} \cdot (1/damping) = {\color{blue}{velocity}} +``` + +The diagram here shows the similarity of problems in different physical domains. + +![Through and Across Variables](through_across.png) + +### Translational Connector using *Position* Across Variable + +Now, if we choose *position* for the across variable, a similar relationship can be established, but the pattern must be broken. + + - Energy Dissipation: + +```math +\partial^2 {\color{blue}{position}} / \partial t^2 \cdot mass = {\color{green}{force}} +``` + + - Flow: + +```math +{\color{green}{force}} \cdot (1/damping) = \partial {\color{blue}{position}} / \partial t +``` + +As can be seen, we must now establish a higher order derivative to define the Energy Dissipation and Flow equations, requiring an extra equation, as will be shown in the example below. + +# Examples + +### Electrical Domain + +We can generate the above relationship with ModelingToolkit and the ModelingToolkitStandardLibrary using 3 blocks: + + - Capacitor: for energy storage with initial voltage = 1V + - Resistor: for energy flow + - Ground: for energy sink + +As can be seen, this will give a 1 equation model matching our energy dissipation relationship + +```@example connections +using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit: t_nounits as t +using Plots + +systems = @named begin + resistor = Resistor(R = 1) + capacitor = Capacitor(C = 1) + ground = Ground() +end + +eqs = [connect(capacitor.p, resistor.p) + connect(resistor.n, ground.g, capacitor.n)] + +@named model = System(eqs, t; systems) + +sys = mtkcompile(model) + +println.(equations(sys)) +nothing # hide +``` + +The solution shows what we would expect, a non-linear dissipation of voltage and related decrease in current flow… + +```@example connections +prob = ODEProblem(sys, [1.0], (0, 10.0)) +sol = solve(prob) + +p1 = plot(sol, idxs = [capacitor.v]) +p2 = plot(sol, idxs = [resistor.i]) +plot(p1, p2) +``` + +### Mechanical Translational Domain + +#### Across Variable = velocity + +Now using the Translational library based on velocity, we can see the same relationship with a system reduced to a single equation, using the components: + + - Body (i.e. moving mass): for kinetic energy storage with an initial velocity = 1m/s + - Damper: for energy flow + - Fixed: for energy sink + +```@example connections +using ModelingToolkitStandardLibrary +const TV = ModelingToolkitStandardLibrary.Mechanical.Translational + +systems = @named begin + damping = TV.Damper(d = 1) + body = TV.Mass(m = 1) + ground = TV.Fixed() +end + +eqs = [connect(damping.flange_a, body.flange) + connect(ground.flange, damping.flange_b)] + +@named model = System(eqs, t; systems) + +sys = mtkcompile(model) + +println.(full_equations(sys)) +nothing # hide +``` + +As expected, we have a similar solution… + +```@example connections +prob = ODEProblem( + sys, [], (0, 10.0); initialization_eqs = [sys.body.s ~ 0, sys.body.v ~ 1]) +sol_v = solve(prob) + +p1 = plot(sol_v, idxs = [body.v]) +p2 = plot(sol_v, idxs = [damping.f]) +plot(p1, p2) +``` + +#### Across Variable = position + +Now, let's consider the position-based approach. We can build the same model with the same components. As can be seen, we now end of up with 2 equations, because we need to relate the lower derivative (position) to force (with acceleration). + +```@example connections +const TP = ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition + +systems = @named begin + damping = TP.Damper(d = 1) + body = TP.Mass(m = 1, v = 1) + ground = TP.Fixed(s_0 = 0) +end + +eqs = [connect(damping.flange_a, body.flange) + connect(ground.flange, damping.flange_b)] + +@named model = System(eqs, t; systems) + +sys = mtkcompile(model) + +println.(full_equations(sys)) +nothing # hide +``` + +As can be seen, we get exactly the same result. The only difference here is that we are solving an extra equation, which allows us to plot the body position as well. + +```@example connections +prob = ODEProblem(sys, [], (0, 10.0), fully_determined = true) +sol_p = solve(prob) + +p1 = plot(sol_p, idxs = [body.v]) +p2 = plot(sol_p, idxs = [damping.f]) +p3 = plot(sol_p, idxs = [body.s]) + +plot(p1, p2, p3) +``` + +The question then arises, can the position be plotted when using the Mechanical Translational Domain based on the Velocity Across variable? Yes, we can! There are 2 solutions: + + 1. the `Mass` component will add the position variable when the `s` parameter is used to set an initial position. Otherwise, the component does not track the position. + +```julia +@named body = TV.Mass(m = 1, v = 1, s = 0) +``` + + 2. implement a `PositionSensor` + TODO: Implement Translation Sensors + +Either option will produce the same result regardless of which across variable is used. If the same result is given, why are both options included in the Standard Library, what are the differences? These differences will be discussed next so that an informed decision can be made about which domain is best for your model. + +# Mechanical/Translational Library Differences (Velocity vs. Position Connectors) + +## Initialization + +The main difference between `ModelingToolkitStandardLibrary.Mechanical.Translational` and `ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition` is how they are initialized. In the `ModelingToolkitStandardLibrary` initialization, parameters are defined at the component level, so we simply need to be careful to set the correct initial conditions for the domain that it used. Let's use the following example problem to explain the differences. + +![Example Mechanical Model](model.png) + +In this problem, we have a mass, spring, and damper which are connected to a fixed point. Let's see how each component is defined. + +#### Damper + +The damper will connect the flange/flange 1 (`flange_a`) to the mass, and flange/flange 2 (`flange_b`) to the fixed point. For both position- and velocity-based domains, we set the damping constant `d=1` and `va=1` and leave the default for `v_b_0` at 0. + +```@example connections +@named dv = TV.Damper(d = 1) +@named dp = TP.Damper(d = 1) +nothing # hide +``` + +#### Spring + +The spring will connect the flange/flange 1 (`flange_a`) to the mass, and flange/flange 2 (`flange_b`) to the fixed point. For both position- and velocity-based domains, we set the spring constant `k=1`. The velocity domain then requires the initial velocity `va` and initial spring stretch `delta_s`. The position domain instead needs the natural spring length `l`. + +```@example connections +@named sv = TV.Spring(k = 1) +@named sp = TP.Spring(k = 1, l = 1) +nothing # hide +``` + +#### Mass + +For both position- and velocity-based domains, we set the mass `m=1` and initial velocity `v=1`. Like the damper, the position domain requires the position initial conditions set as well. + +```@example connections +@named bv = TV.Mass(m = 1) +@named bp = TP.Mass(m = 1, v = 1, s = 3) +nothing # hide +``` + +#### Fixed + +Here the velocity domain requires no initial condition, but for our model to work as defined we must set the position domain component to the correct initial position. + +```@example connections +@named gv = TV.Fixed() +@named gp = TP.Fixed(s_0 = 1) +nothing # hide +``` + +### Comparison + +As can be seen, the position-based domain requires more initial condition information to be properly defined, since the absolute position information is required. Therefore, based on the model being described, it may be more natural to choose one domain over the other. + +Let's define a quick function to simplify and solve the 2 different systems. Note, we will solve with a fixed time step and a set tolerance to compare the numerical differences. + +```@example connections +function simplify_and_solve(damping, spring, body, ground; initialization_eqs = Equation[]) + eqs = [connect(spring.flange_a, body.flange, damping.flange_a) + connect(spring.flange_b, damping.flange_b, ground.flange)] + + @named model = System(eqs, t; systems = [ground, body, spring, damping]) + + sys = mtkcompile(model) + + println.(full_equations(sys)) + + prob = ODEProblem(sys, [], (0, 10.0); initialization_eqs, fully_determined = true) + sol = solve(prob; abstol = 1e-9, reltol = 1e-9) + + return sol +end +nothing # hide +``` + +Now let's solve the velocity domain model + +```@example connections +initialization_eqs = [bv.s ~ 3 + bv.v ~ 1 + sv.delta_s ~ 1] +solv = simplify_and_solve(dv, sv, bv, gv; initialization_eqs); +nothing # hide +``` + +And the position domain model + +```@example connections +solp = simplify_and_solve(dp, sp, bp, gp); +nothing # hide +``` + +Now we can plot the comparison of the 2 models and see they give the same result. + +```@example connections +plot(ylabel = "mass velocity [m/s]") +plot!(solv, idxs = [bv.v]) +plot!(solp, idxs = [bp.v]) +``` + +But, what if we wanted to plot the mass position? This is easy for the position-based domain, we have the state `bp₊s(t)`, but for the velocity-based domain we have `sv₊delta_s(t)` which is the spring stretch. To get the absolute position, we add the spring natural length (1m) and the fixed position (1m). As can be seen, we then get the same result. + +```@example connections +plot(ylabel = "mass position [m]") +plot!(solv, idxs = [sv.delta_s + 1 + 1]) +plot!(solp, idxs = [bp.s]) +``` + +So in conclusion, the position based domain gives easier access to absolute position information, but requires more initial condition information. + +## Accuracy + +One may then ask, what the trade-off in terms of numerical accuracy is. When we look at the simplified equations, we can see that actually both systems solve the same equations. The differential equations of the velocity domain are + +```math +\begin{aligned} +m \cdot \dot{v} + d \cdot v + k \cdot \Delta s = 0 \\ +\dot{\Delta s} = v +\end{aligned} +``` + +And for the position domain are + +```math +\begin{aligned} +m \cdot \dot{v} + d \cdot v + k \cdot (s - s_{b_0} - l) = 0 \\ +\dot{s} = v +\end{aligned} +``` + +By definition, the spring stretch is + +```math +\Delta s = s - s_{b_0} - l +``` + +Which means both systems are actually solving the same exact system. We can plot the numerical difference between the 2 systems and see the result is negligible (much less than the tolerance of 1e-9). + +```@example connections +plot(title = "numerical difference: vel. vs. pos. domain", xlabel = "time [s]", + ylabel = "solv[bv.v] .- solp[bp.v]") +time = 0:0.1:10 +plot!(time, (solv(time)[bv.v] .- solp(time)[bp.v]), label = "") +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/damper.svg b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/damper.svg new file mode 100644 index 0000000000..6323394caa --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/damper.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + b + f + + + + + v + v + + diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/force_input.svg b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/force_input.svg new file mode 100644 index 0000000000..f5969363ff --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/force_input.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + v + + f + + + + diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/mass.svg b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/mass.svg new file mode 100644 index 0000000000..b8d2a94c72 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/mass.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + v + + f + port orientation + component orientation + + diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.png b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.png new file mode 100644 index 0000000000000000000000000000000000000000..ac1f6f68a4db586093c2af1cda63ba59a4d540e8 GIT binary patch literal 12022 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 abCuhBl17+?{s+{oljv)kXw_*rJ^vp~v0a(~ literal 0 HcmV?d00001 diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.svg b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.svg new file mode 100644 index 0000000000..e7000210a9 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/model.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + s0 = 3 mv0 = 1 m/s + k=1 N/ml =1 m + d=1 N/(m/s) + 1m + 3m + + + + diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/sign_convention.md b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/sign_convention.md new file mode 100644 index 0000000000..c71e0c08b8 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/sign_convention.md @@ -0,0 +1,189 @@ +# Sign Convention + +A sign convention is recommended for this library that implements the following rule: + +> An input component that specifies the *through* variable should be such that an oppossite equality is written such that when connected to a conserved quantity component (i.e. a mass or capacitor component) a positive value for a flow variable represents the accumulation of that conserved quantity over time in the component. + +Note: that this implements the same convention as applied in Modelica Standard Library. + +For example, the following would be the *correct* sign convention for the `Mechanical.Translational` force variable `f` + +```julia +@mtkmodel ConstantForce begin + @parameters begin + f = 0 + end + @components begin + flange = MechanicalPort() + end + @equations begin + # connectors + flange.f ~ -f # <-- force is leaving + end +end +``` + +And writing the following would be the *incorrect* sign convention. + +```julia +@equations begin + # connectors + flange.f ~ f # <-- wrong through variable input sign! +end +``` + + +# Discussion + +The energy dissipation equation that governs the acausal connection definitions should be such that a positive *through* variable input will lead to an increasing *across* variable value. + +```math +\frac{\partial \blue across}{\partial t} = \text{ {\green through} input} +``` + +This is demonstrated in the following domains of `Mechanical`, `Electrical`, and `Hydraulic`. + +## Mechanical + +The flow variable (i.e. force) input component for the `Mechanical` domain is + +```@example sign_convention +using ModelingToolkit +using ModelingToolkitStandardLibrary.Mechanical.Translational +using ModelingToolkit: t_nounits as t + +@mtkmodel ConstantForce begin + @parameters begin + f + end + @components begin + flange = MechanicalPort() + end + @equations begin + # connectors + flange.f ~ -f + end +end +``` + +Here we can see that a positive input force results in an increasing velocity. + +```@example sign_convention +@mtkmodel Model begin + @components begin + mass = Mass(; m = 10) + force = ConstantForce(; f = 1) + end + @equations begin + connect(mass.flange, force.flange) + end +end +@mtkcompile sys = Model() +full_equations(sys) +``` + +## Electrical + +The flow variable (i.e. current) input component for the `Electrical` domain is + +```@example sign_convention +using ModelingToolkitStandardLibrary.Electrical + +@mtkmodel ConstantCurrent begin + @parameters begin + i + end + @components begin + p = Pin() + n = Pin() + end + @equations begin + 0 ~ p.i + n.i + i ~ -n.i # can also be written as i ~ p.i + end +end +``` + +Here we can see that a positive input current results in an increasing voltage. Note that the electrical domain uses pins `p` and `n` at each side of the source and energy storage components. The direction of connection is not important here, only that a positive connector `p` connects with a negative connector `n`. + +```@example sign_convention +@mtkmodel Model begin + @components begin + capacitor = Capacitor(; C = 10) + current = ConstantCurrent(; i = 1) + ground = Ground() + end + @equations begin + connect(current.n, capacitor.p) + connect(capacitor.n, current.p, ground.g) + end +end +@mtkcompile sys = Model() +full_equations(sys) +``` + +Reversing the pins gives the same result + +```@example sign_convention +@mtkmodel Model begin + @components begin + capacitor = Capacitor(; C = 10) + current = ConstantCurrent(; i = 1) + ground = Ground() + end + @equations begin + connect(current.p, capacitor.n) + connect(capacitor.p, current.n, ground.g) + end +end +@mtkcompile sys = Model() +full_equations(sys) +``` + +## Hydraulic + +The flow variable (i.e. mass flow) input component for the `Hydraulic` domain is + +```@example sign_convention +using ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible + +@mtkmodel ConstantMassFlow begin + @parameters begin + dm + end + @components begin + port = HydraulicPort() + end + @equations begin + port.dm ~ -dm + end +end +``` + +A positive input mass flow leads to an increasing pressure (in this case we get increasing density (`rho`), which is directly related to an increasing pressure). + +```@example sign_convention +@mtkmodel Model begin + @components begin + volume = FixedVolume(; vol = 10.0, p_int = 1e5) + flow = ConstantMassFlow(; dm = 1) + fluid = HydraulicFluid() + end + @equations begin + connect(flow.port, volume.port) + connect(fluid, flow.port) + end +end +@mtkcompile sys = Model() +full_equations(sys) |> first +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.png b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.png new file mode 100644 index 0000000000000000000000000000000000000000..758704af8137c536caf0aef2816fb351c54143a4 GIT binary patch literal 30457 zcmbrm2{hDS_&;te8B`3ihV1(~)~wlgm9l0FBOydaWSJTzI|*Y+vK2x`wy{iPDT7d$ z7)uhuP+~BK-+TIeKEL1h{LcS>&i|bMJvub*z3+RU=XIawxv%GWdwkK>>=ZK}GYt*R zDGPJs%QQ6fE;KYCdhiM06Q27Q(ZDyR5Oe1+8XBW2>OT-jO-=>)kT=}KDcn9777q8i z?MnlP!xj8*1%!Efh4?B2-}WmY>hRIfoTITYzHlY7aGd}zympn)|7%2%=@nblikoHD zsS??je?-*6IWx>SFF@p2bwosv7t3Vt-&f_=jsIf{zE3+g2s66mA_bx?DVHQiA89Vb zB%f$^Y^>j#tDEeEEzgE+>O#>U*6)!D=ak`Q#$^t;!07o{&n65hAUw|aZ@c-*u~hq< z$Ukk&Ug`$q-}Wj?FO?W?j#frrz^Y^O4dUj5<}bnHM5_sH*cVs~@yI@A2AuR(~h z?At|PKSh$9;hQPbximR+6}{ULuxrZ#@Zz)6J0J&f1*{S_emP#75M%o>GyqO`n~Jv8?Ip}vV_@=AJkx@j&{^F{GxD8n zQju>m^hr60r>~&1cqF<153K@&I*lG^cX#y2x#K`iWUL5hfNIPVnOY6Gp7$vYqGTPMGoQm3h*%AN7qj4Fx=A^@efW5+8y|+CwD83na306 zp@t5{nqjT5VxCJ=yGc{Ti%{1w-0+E6#v_(?=9CYSCtk7&Ev|aPvrR0JXr08{aJWghDzV%K&*jFuh%tO1!YF*oPcCHJbz^*VxolJWRHEb zN`E?(RE^$MJax#PmwP+1JWYgN-VbC)hX(h=>kr&)<`$OII^Dvsm!ZU53SVEmg&jqD z40rw{j`-<5>|!^})Wrqz!-a~+a(lt09=72mJC!s68*ovCI{N7hMSuo=9NJf96h?@7 zbh{X7IVIhsU9Y>v5VLn{*W>s_z)G(ujaw4wXNT>p>ZFAU$i~dCAm&QdA z5yE6G^3#zs{Mff|H|oTSd~a+39k_&xr=M)GIvx`mkv`}4LsEW={P79P+l~HA-1PuY zy4l5x-B+9;kXIR|fYz24OXbpTVA(3duB5gB$M2*=a`VQi4HMb@ zLLnMiTE&HHg_XXMK`GoUMPqRb_R6+NaJzbz*B-qTP6#CR21NQjZh|(ebueyta)RJD z`J5$A_ICbY%-tlS0w_>$^9TPJ(1yN6z#@5HN4t0UeJk_wabOB%*i4Z#@^RuHr(cxsa`OS9<9)CQ z3)3h41j-2S2ANKuW|ywhc#p7cGjBWJOgswxsP+(~5BkwUz7#rbioB#MOKXo9MKMe* zDnFw0V9XJ3_pqVA#n9JA=60S`^7QDw@g)_^FsCnW{ip*=5Gv(=qdbxOJzcL4ADcDe z_HyV|lXTJ96E&dtZF&ADm$YlSOX~X%fz2`txqn8Oy5^T} z!s$Zv>Q8Q+>QCp0z)lr86Sje1@Vz-)L@3G6>66Sa zJ}7SeO$nZyjZ5 zy^teXUy2t89sg z9mKxH?qExit5tb;JEjt=U_->N&(k6`#Ag5E6VfjqvB9Uv3}4!k49YpUhq@qK#EWk| zcqnjutoT{}zG-`h_OiL9pU#3>B=lBmJZ`eDReKp99U68>td3}s&p{U@CUTmHx)?K) zg3S>P|1tCveDkBS$JF$a6Tu|+$x0R zjHQD$InJo!2}0)n2R5G2?y7s!7;+qRSC@;Wyo;vBHjjNfPI`I8Mr!^@0Nw?Su0)*z z#R_abBNDqlPU)U$nZebb`+VN2VxD+%-2$8vVVTt2->-f~a|rh}o(^2f&uSYZ@^e$x zAoBRbJ2SG6XG7{6#p~0#hh?89VGG`Nf0lz%V!(2P*6I&tCLxf_?zSFAgG{2rc5aX2 z(~xU^?Hi}#cn=iaD0n%K9zOHfR=4*_dFU%wi?%CcI@a~Kd9P~MJ>ldyx=avyl4u^L zDK(om#OzEHLK^XkCDdH=>XY+~bQxe50G~XEl&Gvw+nQ(PeLH+QxtSaGP|mqFso8aT zY7V?WVqM1bxr?F%-9B!f7ZJ%aqzW{IYlja!rWde7iW#rgEUv0LwpmamT zz^#gmA#*~b+4odJQc$`Xb=X?ePIBDanup)_mAdQK6LM^8dHoXh+7i}vTV?f(vIv*Cs;**R=8?F`=*q;zYzttGNn!5ox}R~KxgF!U94K0!{h_4fv$SUI zeEg@H-D${=)Vj;b!#_fz+6hd14^XEDZm|jVqmVD|fB7xrR_d}_{q1hDveK-BHh4ef zyYi6F#it|kB4eMw39;v-z~_d?|GZAw%#~fdJXl#M>Ctt-<{(C^$=o8=Kl%8LjiO=b zT;I=vUwY`3!&ThdjKb(qK6fMIGVvS6$>yO75}Z%?P4c!WM)Qzvsnil6RujRFK-2bR zw^+I#Q6z`fg}blq>@byHtLU#od`4jA1RaAl1^0yOi%|qKo{cwBN?A&b?W|h`GVUZJ zD5m&g_AV`y|4dyg^Hx&y!R3!vy&_YLssv<6BJI2N<|J8Kz{-1~j+cLs^;N<=w2tj0 z`~S!{#0X^_V-}q9^iQc4|LI2We9=0_w5m}={TpsRnVmE?mMME~QU)f@`oqZVhDqM> zsf&)+Pls@X1otTiQzN!HaP`r8{^${?+hd&gJT0t*K*@#eo(*9ZUe{8p{vg+#bpEPy ziDVBc-W8F$;-UrmeO!*_%M!sZ$vGCedT{ye9&UCGOgk8t^|p}Zpya}frGZO2HjYDk zM?ESEYT~oC?mk_xd^|T&U!lFAG%=Wjmh#vNJv@SPRR&nzdYeG994F@|nwL}KpAUp( z+RA)07EHQ(Lm**T!yKZ`k)XVif#Fl>1YbAJgARsh<#-a-V z!CyJ)XdES{|89((;GOR9u^ljj z(}>hEE%AswdmdAd5( z7WlZ2P)QdJ=_abrOGBgQa1s#HKq9?0-L9SUL@TGh@aHpYHZS#eV{3YTGlX#0^OvN# zjMZsjpCK>3qAV%{(mH8a)NssZ+IO;4`%J=y+HpsqwsucW?ZLh_UOyIIm%fX?I}KQ@ z3xK-HD#`Zkgr2^sDlJ!Dd$388(8uT|=d;WwY23@7#9ggS77?*o_U%4KZM5>WL^FC- zE#1ZPW7!NtXQYu3MNtA}Hrq%)Y1y`D;>n#<6!QG_X!iR%ACSkTm5`rr4r!gf5VQgF zz&52I2xlpoa1k0p)^0Q5*3yWV^8O(An*FA&nbR72<_f>yz=fYzxM+~a6cwa z0h2BwqEh@+LmW6}cc*wT59)u8FX??{q1?~k)JbN4*j*PNT3`I#(XkrnsOwfUIHsKz zk;#tdnHFjh%rlJZyz;uuTfMK3MiOKfrc#jOtY~IztOA&^#K!wiXc$kwRc>-lSat^G z4P30Pwj2QzkHm~z9=M#zkKm5?&HEtzMYE&`769shkCBx20m6&Z41g6~+`kcbq9spB z?(n0NU%6OB(uTpFZ@6Gc*{0)Xl^4Lp_Ub}u2ge>niCgol?He~)tzyc=38nOE#0Bh- z-q+14Lk$ySdg??nIp5+Ceuz7Hy@ZI`Q!=|3`MuW`Q^XI8@8+lPKDznY3UHf+_Zp8m z7JaZGeP3Vgez`sDt>F}`w`e&(h!s2Fsv!eU6TkRLL`42w`mQrp17U?&MhQBemcjNp zDSGOA%7t+CXugf;0AgQhtk#2ivE8fT4avYUu+r-{ZW7}Z=;WN5OedvRO$8IsGzyIFYUtuT=Po{y z1+W-tKo6G#l5;;rM9n%;hKW0&7%} z3IWXACF;4+o)(O}h5#O^`bI3|t_kpyc>q-zLk538#9X~J53HUY5SclY`lHB@EwvA8 zB_PlIOj>SWo(Md`>ev)+Zfsm-bRjuW6)hIhI*3!h8SV!N@nZ(Pf+7k}8nW&x27ad% zMlvN`D05Xpt1)%0nBUnr`R7&7ogcJw5u~-BAwVR4YY?X(#_9R=CoibRy)uA6yS_*) zy3_V7G4Toti1i0>>h#b2!}ZHVglJ`3$%UqHV7r%aH3B@dE3xJ#5pa?=AK~$J6bf?W zq}EHY3!+jse6I!Ld~;|OI{u8kor!tG&KkXwD4NFO_;R!eK8ycB^pIV858Lgo#Sjl} zJf=WaPv3@IYi*W8i(40&f27ullbctWr-v6iF9uu{^A@vm8Td_GUu4Jy@%N|cal{%4+7bX*q)fTt+@h5NX4?A_%_#5kf0K*85Lz&R)r z8@IZl{iNP{+nIP|n|fd$MxFcPWc8>3Tc+UV2a&Jsr-4i58qrY-FoT)5D)LATC~N}O zg?O|m$kuQz5fDLU!{3vqjZ(UIekcG-x=4LAVR{Zxd3(LUf@eRIUT$-u&h<+!q~TD? zx0yOZggPSsHmNdR0I{1gB!G z<-hyj{6ZZVMv9{m#RY0T&&702uIN1mLg#LR{)y8n=pKYHc3O9pmBHkpoY0qMM?jVq z!14N=is_nL(E|+Fm2N^M1FoW}ciRJh+JvwU3Ymb}9Yf=0CQsQGqJ_ zs}=uP!7{ZSBdVSN`2Hm>{cV!V0_~~76$XS$ZGZJrW!M`6=))xsRm2)Nz7pG54+#5i zDiI0ZI$3XYt*^}oHo=&;OmrvQdYq^#^mpj%pi`a{0LKZhr&dlUvY9rlJ6#wU<8NW# z>&B&pkcO*vZH>!505YRCf432K(}rv{w*ed%sFrSbc&eh_#MmYOzeQKr%CI`uN{ae` zPSYtULt}>7GIPrR8g&(qV!aqAzoI*QgjB%~1e9jz^-_#*mVeY4!<>%G5p_&|7+?;c zG$5H3RBRa=|KQh&R06s?lww9jho0#c#O&fLM+t1NoDdmHUY^+)Kw7}u!ZCk$e>q#k zFvpERNj})MG%kz21lU7rW1#v^>9mT|fR+GByDR(fvaF*T$X=e@@M4F*r^1eJ^q;<3 zfBU8aXs0%daw?p|0XS2ekMLh)mj0s*V=hlv%m;ynLprL8wEk5|=Kl48+q$d5gB0vA zwfAlNe-~kJNqA5SunyG6sdCG20%L%N8c#4SUw5FYPz+#*sLen0FD}gf#Rbb>{kr|T z1eYtozV$d80JvHI+hOe^cfV`#5TkD@Fh8B6A|UiS`v0^US~^K}yiCUx%;n zZPjVC$;x}ZPh7eYK$Z=7{Wh``LtWAK-(cf@Bd2TG+dST$hehe+bVTWmfa%oT-CwCh zhjg{u-`UP<3(UpgAQs< z;kX!J81nkh2rJ{VeW@Fn6I_Nlb0D*RdAiUpBQLAel(SmM*06q1Q0Sm(%Um6Rr80i# z+5?Q~xeET7mD5)&RIfbHOdD|#_4TN>tQ$G=7tAqs|SJa*b7 zMaJ%NHkt?gi?$66BAb_e_(ETBCuSW?hCnVI` z=+5LjCuI0mXA_D!49^0{1#XHi$tdOUl>}`1<=sx^niGG9rqqGG@SYEMl(0QCb32%N z()d?Xh`0|C)vo`xBdDB$=|$NT^ej;`w~VXGG%6u|^}UmifI=e7GUSr24|z$L8r zGP{8znn{!v^}(z7l6J7~sDs)7a6PM@$q+B8FIU|+%Gthbl!N@+@s z8#Ba5&oo`jZgBJhjJr#?=Kvmrbphr2=ScY=I~>tKTXTd*^` zaG;ju-w`oqAPx6isXK@TI{QgU_@Fd@l_u4d){DRN1=e-8=_QOh^O^r{3S);q&oqb` zPBlor=4<-S>OB0~av*C!s44I{aJY=i&f@|@|C)$|`&|CFV0~K)U3IPlSvP{an#Fr^ z0KptzrR4ur%1xPva-`?^R3}vO+$7(Ty`e=2)zBOP=;h*+O5oJ48YV(iRm8_aDiE+; zuO5pS<=oxbcnX{eKz~E8Mun|JJf_B~9Y>g4`FI<|{(|C4K?zmNKLKTIuPOjN9H+kN zJvT$j6NLYpr{ahz*;nTo(&>JHaY=jkGqLr%VU;xIwOQMj!MoQQDr0znc{}|EKVXIA@0^v;g{;ja3vi zO?iRdcIfso^%P$j6I>beD7H8aWNe5+lXYzQD)VF0&j}>#H^(WeE4C{my+cIA52)RcB5l)1<=#`R3DnVZx8{PC% zJP032x$gN#Fn`RmcY%x&wlzzSetv7;`xevqb|Zm2q#8EWFdHR1h9O0I7G-yIRdi60 zQE-1@05$nuJqI*qKE(64e}CUOKKLM6H`&6-4f3ycdz{D_-Y-*KKlJ&lVMFkIl~R{gozbM^x$y=Ss-ixAJzIv#c3h4S7X9NTyU)XLh2kSwCy&vC)qo ztwg-OBw$&xrafNj`0z67+<;j*puNXtaDiq}?P1*Rjc|`PeyFR2o<@H!p8GNbOkHa6 zu3J^gB5QL0t&0}=F2Hq6wbf2r8-J9lp_kt_8x_pX+^lQmw|x3kkwW%hvf}Jy_IP4< zcnKq-gk>Y(Pk70-tW0^MN9>glo0uD48{4cwFIhS&zQ_#K22HW-;RwyI$NafV*Y0-kO6{=z z?x$G;71NA7on`{?eMN6|JsdWCc!F|JWK^uzuk2+=is4`#PuzV^;mQ{dbcPe2u8jNX z)bARSakiBm6h{D(nOXI|dt3fkwM|?R8Ep!;FfKD&d-!&yOfKxDoR700kKNwItSO$B zL0r=vQdLYy;O(CHx`~yUw};Iw(5-TnblvKFfy}HSx?Q>^fZ>e_E$ukwo;uM|D=2K$ zD6?>!LWF9USh`5xXl06Q9W63PAO0R>r8E6PZi^ z*icA#ZIomDa{b;}8dx*kro%pSzvKQrk+JUDqd)e7#mWy9cQ(lHV;?Z419^OAC(xel zM3aSD%P_B+W~EPL&~PCE(Uf9?mO1<3Hx0`}xyNQ_8vcMKvjo4dgVvebSqrllr!)e- zNK1cwiahYJ0%?PqmjeP}vUIM!7{@P70W2-tdCb;zi)w2xeD8C?yDCOw29_EI704Bs zN!=U!KQy{f0h6Vy`iEPJYxrD2d27+H98OAsh}b;B=dF72-c z6pRp#Pak%;PgYmvFu54>{p{u5^q#7}1h}YxW@gtYPavuk5qS%^G#M`#9n+mZ>Y~=K zt7qKy|6(f0gb%Wq} zEqHNOLzjk^6}9~NciRXtgu{MS59>K}aDcuUliEY%MH@zct(KO3?2RM`@m&-|`tardk%Z482& zK%uk*F?C;Qyo-&Sp@czQQTA3!*$fNCne6W#8MJ*FT9+~8 z$ylX_V$N#L^r-G%zILjR`Q$myLP~&(C^SG6$;(!8R(zWE!yW5C&rkwCi2fsZr|z}p zE|f0oVLqFo_Ls_V6*zF~uEeA3&(s47@a5q)$(Dsauw}ab z?1T1vJSjd5T<71!jSB>_bm;)^67^HTYj7Y_77DKgoDLAHBpGtO`Xtr;C)*#nN~nV{ zm!yfLfLkVT6X7Iht+ZpckaH}Iyfk@urS6gC>&UjkxU@+pI?ZgQDxX4B(53kYcP+lX zOm;8MjfP?zPLNSr;^`&Z$I?E`4H>eSpBf-w4+VVDj^$Iaosmd;eS zvw%IocEFlSsJoZtY)avIqo~*!X*N___gC$f07FsbNwYvl3Pp`0ff}Li8<}UAO0{mw zgJRgc5~}SH{WCEY3He4sy_8&tVUAgRkxH_1zi-v1F#TyqwhH;HgREfT!9Y((xQFM3 zOrH1F=VG#LDw+{O7;*=1|E_q-A07*EvL&<23t6W0Xuz4hBg_r{EQZk5mjzXXVm}f) zKhbi)B6Nh?ow)b(TFsuVpK3Rhs=1eriG#tco`@<9-ew9(!o+2D^xzyA-OnA)tlRA2 zEyqEJ?+HR5H6tMNriPc6M}fqCt@K6b$w1MRfuHmMfktlF^#HYlYnTq}9ZNWp%uj`f z&)E4bOW_)1f0Ihl+oq-|Q?Df^hqxPBimBIHfQ>!_b{@VuAY#%sN?^5l)vJUgRTGe9EqQQhH_7o0h4mZ3W=JkN$x=Kke&POlhPZpQpfFO} zry7?bE~s5l8{%t>qF^#F4pwNmAuKEEI80xLXPx=}ep%zr89VqfZHw@%4Y^A=lvint z&@WMHJ3}dLbkUTajMS3%c}$)?M<1L}+vNcpU{5bH0>_@SKE`)m^mtLsZrXn>N1);T zcI*Y74O%5jAW6w5J~xoV)^r8#V^xw-Ub@JA6_W#8I8-|T3gEapEVu0Lkj?Z8@fHjK z`G7*^^4|3J=dmgE+0G{cE*Y3E&N?(K@OblETajxGFB;Y!9hOjO6%!kO+&A4jsPzgH zcN5kOVq`OAph#3(s06JD!;6iXp=%~QSJ8S4cU(?5j>@MpT3~n(Kx7j7T8s z{SzWL$`Ll+PcD={8z*=up(mN_2i0kl_Hl2$+hYcBV?WUblS%iCfZLhsWw2`}0`Dfb zS8S|*UOV{u`^1)&wxF{^f96iu!Cx0wgjb-0-I_nhQ!va*~SYnJgEOoUW_!tbA>UZ#x8z|78N z!{=pn^rRaPLJ<)by1Dm6l|HsA82seSm3jH671WYmf4Ifg4bJiC-N4XTfyS8_lF*dY z+Er5^v+~oLe)gQsBqONbu_TV=v8-0AXo?GpamGdpMph|#He`n7MSjWtsHQf%EBj*O zI`7<^9i*-qIJU&+7pKN1niMxSnse{XvNfbIPPp7C{%gNJT!#&E=*?|C3dr)5IjLWJZ6PErScJfq_q+jATiLe9vf>osClQs-0G-aN%~&#rcWIK_ zVGh+HO!F9^LPEWy<(HB4WU>2#TGpzQ&{EJDA3gm_3|XI9;5m?zAeaE+Y1uS%+EuXk zm9Yo3+uKdT4uo>lC6LlWGG$2CwJoqbO$H~q|IP^}D8v3i`kKXBCDVrZzef&=& z13Sd+A573RDWQ82goa(;1DEy~ae1Ft556BXmnXvOHR83n@xmkDE{CB zoL1YD+WMvB8r|{)`F%q!O=sZlmze1D5$yc1VqLGF5a+(E@R)Ng7Rxm588I*l3+PVd zlC&=WkZrBS_iS;hd|bVa{0KY#i)c4yCiLV4&^VbD?!Ou0m*C(lhYq?Eb@M#$K^m!_?T%`t3>lQ*fyU zmni2Y%7u?_x+H5l(d=7EC|!U!Zcr{s>v#R`EsSWW-P791Q=R( zKEzYoaNxFVeK}@XCZ(|2qnSNckSInk;|h8NiJetv{LvRyTnP}s1ZS$X5$a?K;ob+B zqrb*(?9@43AFXLZdNtgy%)bzHm{Tj#uz%JHtOHy2-wx`2w2E?`2BKa!#EdM%5Vuh_ zP?~tvlf~1bTctg{mzB7m*6|*9OXMD#N0L(1^>$3$S(a4^iuoz48Op70_V+Bx=%-LY zyNX=sa6te=ahy-%tA}o@Rj;!))!q?~yVQtwo_xLI;6GZ&q-aUiWzhcQ4uQJCOEu)9&9-iru(Qw)MPs z_;FU_NkY#XACF6-CcleBiN&Cq9Pc^R%l8Y{i}~Tj%rn=lFd#= zms#U8l=rt;)zNq26=x^}nO%!A-gT>&zwV=KB&~g&vy}vx=b7gB`oeAaOn?+ezC|?o z0kZqbDJdi`)Q)Rc9*$_4-Q3t;ebf{7kY_IMGApaEtEc_2qc)_}8~> z(zBjf0Wn0r4G+G7kqFPwZ7>maz9}7DTQ{o&FjJD6gM_$C#CN>HgGr&|klbKscZRQS_wP@#K7sE1k)Lqti3xJ9&w!d>h=X;epaPfq86aUX)+k{e z>2owT0`sFLf->{-)YeWN>{2cO2@snYDjEKAy{n}F--bM+;V|%Z3A=fwA<niL0G+sbE*(jujl)D` zzOyE%8N{vrTyg2(`KUm~tu>$dZ`9;q$Lz{!bE^VZB7%23fot%q8l&b{g5Q-fWncZ-1t#l&TK5^%55XWzhc3Ix|nei83oPfj5bB=>f>n&D$l#)N!-~t!-&faqAR;GhK zBbVRAcclc>-hz>P>(=AQPa{w7@$5P9_Pg_>Z{&#L)Lu|=ZEVb(q~?6vhWqls*!kTP zwZkoOG)heuV*DebX}f4E3h=*)M|UCp>x_pCc}z7Idpdo;p4hWLjCFgS7u%%zZ%!_= z9Y7RW*2iCZF0Fa{+lFxJPjl7~-*y4;`ql5sjD6GEGn6Mk;nH8^JZ~Q6J3fE?{NEhm zZz&O<*7Bo{EK?=)QvAju0s3vjHWZURK=1$^32X}H)yc=BUeg3}Z@Q!Eg49~cYQ)Yf zhX=tOwbQu&urHG!m$hVIxc`_5pd=)J9f8UV-{THYcxIS-%U1za^wLnE0MxbZ{UT5g z_7OJGjP%Y?K|jUSWQ}~W@NDQn$$LEC02u^(GB3>e*6O|XgmuWS*gMNP4dWhO2}wi1 znwn6roqA*CIpQQ7hTjaB{FhzMOaQ`c&JT?x)iW^?A>I?ag}6Wnl#D>>p2E@0GU%zH zCx_NGVCp)h8zInWnb@7w{@8xpEoC%Nhxrb7PMm8+_Z@DQewNPtL`9zSBUFsOBO5zi zad82j-jI-KfwhK4_v30c&>aowslt6ixOKOA(fJ_Hhp3L5Fz)QjQ|fJ}^K8HuJDs0z zT=<|2Fh{$7&eSrWBX1b`dhsj+tN!zvh`7|D0d(s0=Me#mKdKQu@iw@?JPiE1 zW<=;bm*-l`Do6LDIp6p%h+>E#(uaBMoaf>m1%_QhiqxP^ybn{f+;gV5%pCD;f$C+H zI9oWUzT!GJh()|Hd^9tx_tb7hJz%>mM^|%VU8swTQ7^f_hDGP(N#V~jtLHe&-A?VX zhlHL=jGUutj)Cl|fX-tmilQn}L15gNa;xwZv3Mi5} zikk4B6ww(uRzX7%-cCxQ=I$dVt5LcQuUUK^3U6}UFZD&dMR?F+^6^|mJ>7n-v~xPa zAEXNJ{vzLrFBn=kfOqNjzhQa&Zg>NmgT3}#ATqhO_^}7AYd(ILs8>C;okJ~arZ%5W z9e@|%Y-nuo`TS9ocmz8<^6HiX#Xs0SOLjCI_$@8W>C{J<6h_i}oTz2g>3_4xD!_Xu&a;f_aFW_g39oWT8=H3}XxVH?dQuUQAdsACy8L zo40(nfNq4NyW#?ImT^k;)*8d1@WVU3Z7FOGI&?LQMA4ZTN4O9u(12&>k6QPLz0JRt z7V_M4;C8%W&IIRLjM%ut55gFJ zqy9$>Dd8l#8Hx`4_%%j|-|T)3=$>Q~xfY|JH>v?A$uc3Bu)kpHN{j8<@2B}^D8Fzl zZDdzs?D={@CEZ1>DG7D0Ny6Mv+r&6AmcHIDtv_=_0GTQEX-8*-do4+9np^7r8o=pL z7>TiuFmX9=VxD2r>1@*qnufwCc;qLq7L3p)u?tQD;;@l~^F-ot2&uS#8pS*9*G5)D zj+%eE`d(uwINpXpY4Z7;jL~OUw*t@Sq|I^rzzQIyA~DSCPj!>}3DbzhMS^IOmY>$Y zHVp9HAN=?|#k*HB_|d!K?*^-|4$}P+<1T^#Zx!YSyh}3lCcqCKr`6wi2`McR%+Z~s znhP;S$(c-k7GL3mpfgeBIiEvF(!^&e7gxn}V!`9rF1M1&t-DKFFfOn6r?CiW23HNn z2{GhpNBelxsl)^ijdLl1KwYwAu6`d|b>XO{|D?xfoJ0D~&8$^}!5>=R6L9`+Oz>k? zx6+$qu}x$5WM^l;6N8s-gxR&b_2L|4ez%KXO8pzdWODhzJm`o(fK+A$unDU~dnmkx z%mUjLQ4(q5FTN9a+41{=N?lQaB?Rezj;5|9`ldGg_%m*n&O!KdUaKsz?THcr#_}8} zRuoTpcK0Je-$E_2#FJ z@^3*UtRI6p?&sjdi|d|OM+~%u+q?;d21o)W8v3#ZT6|8KY{P+8li`J0uw8J-8;oE2 zp7ca1Hje4S;Dz@02Op<#$a+y zC-ox#(kVkip&HZM>mQ${uzfr_md^G0jvLs!YTlIi!Fh0kHK1PKSVPz;v+Ql0YR-U5W-aHqin6*FRnyf26 zw~)W@1q1xZO6HLFv`ON-WK_`tu}f|T@g4$o1eGFyqHVuWrsX2AaNo&>ilAO;Qw@Oi zwLqR*8OBK)yfL-4xz7c4rETGLqK)L+W7@j$V+F|M=6}4O<0EEWdnUO-+&&{sl9Nf{ z`}Y4>_v=?0B@%-#oTtw;m{<6G{Zxt-z58|J^vUE6=!2XONZ;7Wmlx(F`lqilH@d3L z0&iM~PQ>w^VoTnywR2+Me}p`@M8hRVvyNQ7w795DiLc{GBHm&+oWUK<6PK*bTA4K; zE6I49b^oEx9Yj3xcOaiy;d}cgF5=SM`Kc#t4c3gE3q<>yA*o-!ZIrdYZfiG_y54TM ztJ)m(A13k-IBs(KTpkeFM&XmfUvq_B|52qU9dv9YrGt5ZlHVj+fPX6*vb{m0c5EHa zR%P{r5d&bT>t{lj`RXKq3f?HW=(q}}j2*3R^IQPLMJZAtK#3v<8l9tbwL(KUL`LgI zDCsy4?~QzA!HB~5V1f4t496d?Fy|F2y(pW_TI+I7&JaK0@nOV6*~-SqDJq)ZGZ?C^ z5GGz0HCAQxSN*SnstwND=Y%yQxg%d1Oc#KwfBz{7SmKWFVy_6RVJ@?MrHt;y1zPh` zFbVZ3o9E_{+1q?uYmv=#ks{u6(y$x{Rduaf7u&VPZ}DG;l`evFR8gWmdU8t_d@N|6 zUcw3^`DBG?NEMAXrhz>5p}V$F*N1GzUo3A^zI=PzP}imIWqh&qD0eFiS|sA&npAtv z8sf^^a!z5egAK~>0~5FVC6fctGUn+V$+yqAqS{~DR|N9kf<1El#Ydxoi53sW0ozyt zgzTT?Z!}ph-&OE|P}z6&?rusyMg3OX2V5-6S-L`Yv%JHnY_oE9FXuv~7D%s0%fp8W z`Wom(L~^`G9^SM2WxfS+)v~|$D4bS**adaNV;*?t;zY}t8u};T&%q|;pZ|9kX@>VI8+?aIHf81{=|eHf9Ori z3ngiI$KO7^72{cka-(lI0DESAYiI21q@15@l>zERjH6y0gF5XO0|EuQs#bYr$Sl+w zjTVfK+(H&eB?4tZF!ij9b@W^{tyv>#NKbDgXh0Zgw?)_a)rudI^%GOpt4=$Fk4kNtPTq9w} zalcZCQ}aueK`q2)8MK@;RTnlCC?Wjvt6XgP-8b1lm~+8O;}agQHH4d$TgtnB{Bwg? z{4|u9Zso5?#j4GMQ8#0qFE|eIkgZ|8GNZggkH+#Krk(z6js6?@5H2MGp;+d3p7uB8 zPjCOIyipi@1B=k|5_>0syu@0}o+Bc-Mnqj0jOJao;Q#Q31}@|cYo>VwD(DOAvU@oh z`Ua!;=MY1-g*EYdohjcz2qQBUgE^9pj^ww)GBI`l$AxeM%Yt5;IhEYt@{(S{APT-& zUU+(*B~mR`e~1t^;Sow=918TFUq%{u32vP)$sm3~jta-Dx}-{u&mnRLJihZ7NuMujCszh2G%VVp!O3$*dO{bp+RixeVp&(^~b#-U6?iIn7213e&fK2GUWZl^F1G z)8#mI0)+&y?hDHsYsgW13Rs_egtobb3}q06<7@8M+ZdO*s-X4l$q@Os_5Ms<0{|Sd zdvOP}*QDOr{8tMw$)Tr2TqZWFwO9f58A-soqPE5iO~O2lXXyz!Mp@wf2kJ%rk0z{1 zuHH$_&Vq*)wQ6a2q48#vqyj|hugS2XCWp%UAFHKQIb2a!_?5c>B#>XbOtMjVjLc^x z|8IW*fc;;O159%D#lN>VAO-S&zjQ#I1(-+XzuHoua8vwW&IAa7tBf!F!;AoEr;-5w zwpafjCjW5U44++y;`}0Xd@s!Y920`g5c>Hdz4RFEWlrfa&Wl*^%X7W7Qbz{#S5)KA zNhL}NTwutq04G{LxoG0D(ve^*#Z+SSai0a{qvvF|489W8b8+-S6^gFX62o|xrDFk}c zsqweVJe{eW05i4ozh5hC1UVNm`4cDU-GMxv_SK6j2zO=;j%O!zykO(nPvdF}g|Xj& zKLp_n4Y)~ZF6+cLL>B0b`!6?VwQZ4$xg+T!Yo3zrR&(DKZR`2M>Qqs+b~5nJE5t>Z z`s?1G8ZzFuJjIkw@dU#*!&Q!{Yh=kfk%y1&$Oz_EqK_QMMGbwuGG^czkFvR`Ot@y? z0gXoI{`|t7#~6tz+I{+2h=;y?)=~&Re!D=<`y4S_|6qc;@V)p?j0R(eS+S!2v|FlM zY)V>MRT%TJU1wU0IEG={${2B7cW~DL_c{OgG9l0iD4njEE}d4O#KOq+JEu%Q zi~w$bq#Yb*H@2}F<9_q4%lmV#Qzab{y~t5MD5+(x%H5{EsdQ)U%5 zPsomaQD!eTH2!=>SB!93iX>S&>ncC+qe8)GQJ*(~V zf|b2~eq2y{HDNrgfiAxKb|`Dbc68X}Et(U){4rwQZ8U^T6HALZ%cB1j6&x%p=jiCFzFicYw_Fhu9sb1Z>HB8wmk?V{dTGq ztXp{SZvD{q-ott);tMRrnPAUf%yP&|s`kddHRv*Iqu9>`VVmv0-qY5}eByQ@Qi7Yr z8N$7FS$HTYK}Er4vaV+QR<#6b70bNQ7=r{uL~z7-*GKNT(L%QGCC|=&bc@!P)Nd-Q z8+?Sm-9T$Q*xQkLkZjQ!zzw2U{k-G&zbgCcsHXluZWFYy(>U{rAb8gh%Xr4%IJmoem(4o)-&F?-Lhw}gB zxF79zQiSf~sw36kFM#_n(wZ$TR>vy!A4OufUfct?v&o*)L40K)K;lEu&&1pNprQKmv=CxOwE}Z|Q6bc>pPJSqT zu+m;weCt=;RgyCyofT#Tp3;3FE5+Nl5En5!a^E>sxRF8qll+xmK+xwd#=;mwZ`Oc25u}#)~>!Ee=tARgfH|#&MDsd<< z#xl4WYfUqnRhV;KW|sH{qiMV^Yv2CgO;e{9gv?TG`<2r>fu4(>ub56qFQMeTE-uGq zU_XmJ%_x7>Z0!6Y!THB#V6F-UU(qxFv`@6Hwe_o>CJCl z5l*=73KvF&Wf*+9QC$2gnBmt~{dWAw&)F(?-G8))2ht;?`s~>HJjxlKg zs&|kNMYy-$OIQ!HN*U0^XCiNSJb2OZB^q5>Dh7UQn${c$Z)*2aGp^I657+ZBv`OaH za~JVFx`f~ymj(F4gnQ1ut~Wlx!dIBw>q71R3}2XF5(-`8m!j;Ezm;kxdrm55EQQnJ zt{sS^{|E9k#bKqm_zwALG^VPF^!H&jTKDHJB*ZKbm7wbb)AHdk`9|uNdUoYdfAjc` zOI%`<1kU4eP8_@S(^Rn__VVNbR<$1W+iQWMRBIVX<3FJ0{ikRjjF+Q!y5J&Xn#2P+ zjd*JE2c)M=-Jl-<$R>AUlJ-*rYgDY;InHmV@7A&DeqLOHgBE17NHiGR{-gNE@3tRY z3B%oIvZrk_ZuBq@O?(cDVPU{`IoR~afuV`Bda%5H;+Torg!vSM!#5l)bBlM;(7}Q(X z{FXy1u( zy}su1V=`ZDfUZd+!Go~<8sxAVp$*b8NLHa3eUoiQ<`oSN%ZqgmGPwu(o^li%xY zx|j@H8lnn+i{E2LcS*h6@;hyHD-~1_(7{8t=U1a_@gL^iyd2^5WKHOgcXD_#FTzY>Sb+b}4 z{kot8$ydzptMzBK>4vExjIDmEq+Q?1gfEqFjxi<;vA!~}VD)S%LWPf4@dv6jS2>(GXyffbsn*?Wq8|w&RxD?$sXVn-A+!msQB^wOAQ>c$&wr zOeRek<%O6?$+%=jZRbUE{nUrS_vd{ZQxq2|>+OnxDfu*cXdFEOt~{8*B}i1YJ{lK4 z(I9jEqwusWSuG%{FG8r+7c4=8?*qM6>STOl#)=CZ8#syfldSTVt_@OdsC6qg@4W@) z2cn*g1A}e#`l8pg>xcKy%Y8nrX|Prwj?HDCf}X~A*0r;jG3y>+rG-R(68x`V4Ol}7S6SYF@yeeE9T`2>}4yZPE;+;U|EFCg#f7vWDV%irTNabG6S{U4U^ z4;2v^TU;R!_Qy;7nDHb29;A0j6Mg?Y8~ z5Ts_lanH~{NoQp@N4kBYD8xdb)mW~0gHWS_#!n^ahCh)&sS}Oozp)XD{j7CyMvgRa z_QK+;zgN<#F7`g%li3|(ADPQbT+{h8fO*O`QL>mg@co5pO8QB<_-Zfjtx!{y(cp7z zmaY?XLVJSe^I7@?1=LB1VWZbuZn{pMctq=S2)Va$0ed_#<|`P?&-ZNWr`3q}b8r#= z5}birf_=$q-UG@hD&HutWN^T2XKbW0Xw03m|6sZ_(YL`?lB5Hl`ts-1+jR?fiPG;2 z4T#X8IcJ?m zgkNP35Xfi%?^KjeT<&WL?hNX1tD>cpx(~?iDal2a(gga55n*iRV4yR&Z+_^I%ln#o zs1iqw=GXC0lZw9iuYkJF)(tD^V0QU;-x;T!eAZn_Y>`bfCO&lb!$NoOIuo*ja<8J`TSr|*`F~x zT)x`vl72)``}xJPXBNj?6Oq%-vxH%?Hw z^wig|xdfRi{GKjZ{vqz#sCq9Y2Y$_c9Cq0w_6}$p1dh?N)WgxhGg9lq{`B#Qtp1(F zY|>QemWURg3?H}AM#g*=o3QG|cyx;bJJPRlfRA&LNnlG890Q!WuTMPvQi|gDR_?p% zdFQ$~fb)84db4fz-QZkHGTw!AF=7NqWQiyN*hmu@ti62%^$tB{QGumm$YVm-Zozb^ z@joR?x8V(}x?FlGiKgHVaF-gdF`sw+XoShft{a;)xEs#+3zZe9O)`WZK`fvT0`8ti z+HR?f(5kTGuq-oA1wefKw>h^VpOj5Tbq?>JV3qf?nXOH<0x7xGU-aktL=j993+?g4 z4&xG}+K1WyydAwceA{7%Ou^R6L%Ax+>NW1Aoi8RFwBf(>RW#pS73j*n=|%szHJqqm zkAnNDESN@V9HB1xFq-x|&fmNbIKrndv`>FiOQ;wzSUE}E;=sM{VB>Y%^(1_!qb;tR zYzT{d$vx9f9ms~;nD|^k8;)?0mWo}YNpvZkVZ1~<08eZ=FU)<#wd#p29kF|v14+rd z7IJ38cGyjF%ztLv)%j1X4iGI%-d|kioM!5}J#1GdXkZF>#Aj}JMqi~-h7RrP5?sg9 z+t)J}>;s{$ozKyqT5=+RSU0G~TxjuEYY+AK(W_urg38^B1gnLXq+v-L#?uk#+mb8@ zRh!={9jF*)ucLj(U5TT!(z`fovO1^qkw)v=BItN$BW68b2GK-&*vu9{m0ZWX%}KZi zR4EZ0*7GWOAWpOc^}vHl|Sc&l}X_dWYJDGm>Med&XqCixgbT=E{q&Fu#1CnLX*Tk7V^+jH&e zk)-O?91A6Wo>G?MYjZL(Z%Eadw0-k~*$1?gtdBshv5)$*lv4vv=sEuFKAG@7+#OxR zO1S}#^tQuy#DFTbg2NlzSEwj`cy35IWauqd#gMAcyquaWnkEpFb-AZb-b@Rtn2J)z zW;lCC-<3z(07o(qr35&myUT;_!YEd!hO^B~49VMD07GjL|7o3DzwgPRCT(H}kKQ3+ z)K*k6Sr$bX{q;9Vknw@~zyBT~K6jr46Upl|A{@Rn+vLRXapV3J-I_ZD= z!XE)=z_YtgGwOue>fDq!ldHv(*iw1?I>2`Scn10-rS6@0fGfOCG1(&4)1(OKH12PNDu@emA+H&o(D9yE{9T=KFXAQHSKUd-Q-?Q!? zGS^fiUA2Id?MJxR=n?C8lMVkwZ9kBXv|(($3*;pUGzLC<2#$*IEE%~WIsT53^4gt}qR=`z zcOhp^>w(&Z*3D*cB6h!R7RFC%>hgV;VaVV!;AE(-J+g@Sj=cZ~9`S|80 z73m8DQIlqx@-)nPrEP`Bf06_6rV#M|puP*QKRkK^ocXtp|NCRU7MJn=E+~KgzpsA= z2AEM$%U4=ctaxSL7R1?$fC(r42QU2XNBoWrfSYUg^C@LcMI$=cpqi3kh6h*wvcnsw zOsO}4DA>^EG4yIm7R#Zja@daq9hKzOJN8L_AqAW}ZvS>@3y`!FPphz{NgkF&9p5%V0`m& z+rJYw)VOU~Mx?#0_q^)ijA-O^!ZC9nd;7)zNxlA;f`)@(vG`ca}E5mHj* zMP9EmxQdC@@}manm4K8MiF3C_k3x_1l+^?+e*RN5fUHS#pvvuNmN$j;cW>Iw`BrkS z9z3BQ(Do_tJ9dw~*7{WcA6a3RE}P+?Xnd?f#B=nszS)i*$bpf570c$jyA-skqw8=t zJ2Rprvb-$q2K-5p{J(BN@Qfa(a`v#M)*7Er@hOa!Z zyL2;WfmxA*p_n>}hLWe#%v5Vf^EHASv)<$9{>VNm_tybjGVnYNF`MncNc$;bEJ7o4 zzQcrIK>!o319Qxu4sRHRT6;on2v;WMj7ZbAyqEcTcLX!%;??d-P?yfoChbakUAZ%k zDbDO?(OQPGE2)KZp!x3(V=VJ+;B^XjsDdUi=T9{~eC_!N&w$Ov11+dk-z18suIUkc zN;*zN*qB}?-?NuE4ZKGmbn~zrF*~NKX2i!taBuMXTK5{cz;!l=U4Jj!&Qqg!t{Stb zGb%zQv(Vm{!$uvvy-29oc{b_NO(HkmNiHepx2QzhP^%Tf2T|H8h8D2M&R%%#=Q zY}22T4;))yVYf0N9;c*M1pnEXm1 zC4P@tDlexmc4_i5HT36Ixg0_CXlBo8C>}3QM7YN$g|2iKjKMJL2O_wG?!H^)^NV$6 z5#B-VvQnZ-tma%zOfvN%+fiK-&tR9;xZpc0S^nvVzQMTfrR^`28QdJPx4< zwGm>HxI+fmKXf*MB9j%U%*FE5U-eEO1p%v4DeYKzysj$r{1?XYV+IPnXFKR%lQ6wL zN2}jIAZ;X&8BOdU-fx%GlinzRW9%YQy?P$Wo(bEZ!Rrd#3^z??bWMBe#LmRgzSy$A zjkHWlOL+YWr9m6+=?~0K%MR`M`v(g4ei}A-J-KFTGZSPx$PWMTn0}k(XIX6@?HVht zB1$6S`%($@%kSUIIJS$-Ecoyh$((r$B~%6O0$VVoU#3#>i5Xl4g?wL@zv>4)3&F!r z!;>9625hE3bfOgPn{rPjt~gZFt(2n+u60PU5$A@^0s_Rr)AXm=yJQtwK1NfwbIxCS z_6z~9coD9t7(4DI|8SvuH8#4VlM+u0g6>1YQ1xOoliTs+6}dIT0F^Y9cisBB?6b;$o1* z0MCzyb53md5jLMblY#lOiEnWyXZ&{xg|S=4r~DG%x%2X!Eb=pHyVpkjU0b%KbxwF{ zJU{Z#+Vl&;=io@mjbW@ zSQ`PTw#a{A=HuVs-%PiNCmr5*C#Y5zP$vA{j4@UOmDnZ3TiCiyJS^9s7i6uZ z6BVh3iy&P1J}Jw~Pb1yy^evOREs}*w242_VUm$>>nTcTeO{t~)<}}uzpLfo(!`T7c zjTH`~L0>CS3*j=`Red+SQk}%&HKDayZ8FN);-25_wA(px!%DjCt^`T>{HH2&5CcD2 zG7ndMAy7m&_+`l$xW)*P_?HUE zJ?%EvQHs{+D~4y(z$Z<>@tv0o_)1o)NPUd;u7u%}n*_*dw!}GfQjzUP8&X7nZ|Kr7~}7CR)_KM*`9vvefXLg9639B<@@_=Q|WU zZ~nX>Zyij!em>m_SVqy*fYgo848Y~)wQZ$>l_g{o5b-E`<`wA7)DpGJ)?4g0spsi{i??e_(g}x$Ma`?P4OH8r;PPyX zw)SftM_Cp~wO9iaObjW;bzxA}woj@{HN{HMI)QB07AFDN?7<63EN+dZpxOGy#ISIB2B7kL^|;9*5^XiH*XO*4j+$*9X9(2&Q)*5S_#_nH+r!`yn+X;IF>prOhY_Hbyv@QPJ@h{sp75CLU4J5J7!Ilsa zXGeAnOgDg(68nwSKC+p( zl*d0@<%3N|4`ToW&$Ht9os85aD?=wwpMz5tlJt+%Y_Dfp)V0I~5^SOm$3A2G$>^&} zSF;-Rn=IzYQq~+^nh$83C{qR4U;LX95Z7EO$pWnjx?bb3X{i*S;TIO>f&$G#F8NX# z8Jx3u`_5~N&?ZX84o?wMUL1*?zhMP6naB9?zh!C5lmSn(9D?j~tWO*=)Z42*Z&8b# z6BmH4tT^9hl_J=Mo4Kx0DkJ+StFh|~+Q}tG$Fgj4F~~#j=(UrQwQB0Y4CJ&3DPcn5 zn?LH+K#xuSLipT=&2$tDZ6uxp;4ykP2(hyhaS||29)Nrc6t~99*}z&C@)=jAeDNXs zfx*)P*bD08_dTe74T29CDS&+Op=PJH6b^1}mR%%YbH(Zm`}o}*30k04U5oiH=-xX- z<%MdRC+rRlgFV&O@7B6NhdL($P?{c`%7dmg3x37c!ld6hbB{jxPV^$>S!iY@skYVs zzN-z?iBil~nP5@PhOItb8`+{^sHe;YXb*sDKp{=C7jI@Yw<)WO8M!#d%3z_9*L8Tr zB`u9kS~3>G&ETs>O{z&^-`>;BfxSz9gv}uTs@wwwR_18vNN@gS4K$UN7TsGE22$TN zyxx9SVX)s|f1Uj8ZG@iA);=y=N;K3Keqx|;x@$jT{QF>9y8j%o?C)prhyQ;OwY2Ed z`|I;BR%fGhC&0n_pKAUJDtR4;<^6RMK}=gp22Q7KE_dyp)+Ro)>aW&}%MjTeo;vS> zQ{p1E&KgsvYEt>l>cUF5By7rz;S;@+5fyIt(;XNHz)%Kg?fXz0MWudKQ5%(edH4qd z9jyaUSoUsE*`ee%qFvPJloDfNDOro5Ae7&0J8$zE*2$wwI-8uDwKqdx)?l2wmz@^* zG9`fKc5&)~KaMZUq3V^j0e31x>8hf@NxATeAwrOTy^0m@D$$L#zGkCRUb&`)Ohw)9q=NG1KmZWDot|0@Dks)yh!p%?NdCU#E;M1S`tg`fz zXQ}IsdFOinFbaxl3G*(gp@N}uh3X;6qtQ8+t0QGjINz!qoSI)JMp;Ry`<|Y1pU!`*MmX*vYGmQ(y?MU zu&I4@;H*`8ZiV5rexRlHpo!mePn^;sgj*(R_>SUxgj%+40hwVvmxnv4SFfPmK`+}j z(~H5QzV>e0Q`$Tp_u9`7R|J1s3VdYk_sr7J%>VZVOJchX!p`7RVAeKW<-YFOAKBKUo~qu;c=oJcm%AGJ3nAzmDMH1s$alYD)j>8}5)W z#WshsX`>rpep*F?k|W2W!iE_?Hy?4UC*yn)@)nsD1-z6PhiSh9b6X5yFtnDm1R(}h zA@oYcKNt@SEh{|0MIXwowr&@_=(t>R6p9?d`+q>M2Oif<>gs$MycsW%n?V~uA8u4W zaal)lTfO8OB8vQ5=bvAgIFdQ8o&mY#tOTuGFU+fAt>2&Zo#)R;HAaMaF`__SjBsqN zgK~pkk{VzG;>D_d^X8BF^}VQt7Km_YzKm5*;uC;mdLs8Cb0oJcascYog*lfdn$HM2 z$e+*4PFc>B7ACLy!lk<_5dlsR`fnb@!__X$Z!enKB?3Q1a9Xj+h=3vEuc&^>!xg*p zaN~O=M#3lk*}a&!28el9_QNd$z5d}5bBEro5A*srAgbcB?Brj+-scuZPlX(}Bc2Av zq&*CFKVzn7RiWv7RdX532wnV(H%No|wy)~%4Lsz#uXMn?drql4NW!ymGL@}tfp^rK zl2_yjp@-MA_9)349{SE643vhi;*Lg-FzZHO&BYAClcGV!?8$Vs{yhaS2O(^G6SF?@ zk^@Q{d#|`B9s8(MtXamRFe^BxLVtWXW0Nj3h|jKSRAvC5#R{6N(n?pM+P^Nk5SQT( z2*bIksBc6Vc(lGb`9Tz$Y(A}j@*0x$jo48gF)Q4gg*ANP5l!-!8JR&1_Gcw$a2 zy!zHEwPdqoG6T}(s4{H+#2DqQk+p%k1Sd!J` z0s1<)ppP-q25s=#*UPmqW+!sZ;d_Cd>6VySUDt;he2F2sEwVTl1K|(DHVl@c3zrA4 zC7Cv9f9VN=r?4GBivg6XUPv#H$`1^X0#p3S*dK{b&UuGSFlCzu=iT5z~NoO z-{S>6Z}F;ElUg_&5bms4%{x%BMe_4_yN}*w`a}bPxQW%uaPSyUt0leGOC=0|6WEF z=j;3h{DT>)e{vK&xS7b3%>@roJ91W5yLRZ|T?|4j9lW6*@zyT0YY4@VaQg*_$>#w7 z^6xS^0E-sw4~!;kU)8D{5N}U{IH#qa`-g=-45)r&RnygvQ+0qa?msD_62?Xkhx^ZK zSNBAKw5Fj6@RIkP4y`evtaA)z z1a%IGQ5g~t?C3D;EDas6w}=O19pxKHIj08nXM|piRFOyFb5jY%#jW?Be4Y8| z8&c}1Jg%VF+gQEATJY+2xDreSI#mzFW3WLE<%52A0ND1+t}(E=a+cCY#OAa1ExjKO<6o<3vnHG2bA9R5Sj%B~9bi;)s#iNuMdw&x z&ZD5K&q*}!GoREx0Ma7peQhpjo!MIC%bm8DAD;g5uVwxp=d?cIcPakts)T?9yX4J3 z&9l}|d70-~$d=E1n>tOX zXn|yK(s#zB_moP-pLstWz)3<5D42*7gi^k-;T=B;EBak+h79h(NGqToVcdjf?0S6< zvtFzQP@r0a)7EYAwy2?#4i2wq1A~EzPqO# z#=J~{Y~XCQ3PA-^6~W6;cYlc;;Eyz*&lAnBNmAs%v|9( z1wk)*k4s>zk|=i0s1qgzq6PhnfSXi#UWOZK&z^(xpYJ! zF|<_Xvl$6;((PukDKa4VIl+x{S?B(oH<^uyMj^G!lN`KfE+gnQyJfZh$dHru+heR! z?mOiYAP!r*jzx&${*ajn8Q!h7H*9Jx?p(|n z4vsIWpQ21huyq-yidkV4fWOsN&YcRG45<)wE}j$w^Y^6-c6&P&9PLjoZbzm>bOZK> zi>$P8o~6IOe8cd4YFNYUz}wQ2GUab$i)VFsQFA;=hI=2sn5hej#eG-_b->m=dSnX$ zfHOz#m;PjSo`@70A#TFMa~l^@Mgh(eGm-Oe#jxF@JV-tnA{kdjbEGVD%h_(`MKMD0 zdkJrJ4m!&7Ufz|m@MxA>Cp4?etRShE5E(;bVV;~sCrZjfb;_r0{KyJC9L^)plye<2 zDit`W^Lei44+mQCl{di!T)rZ-Tp>lYhN}F0;vllWriHy1sLXmd@xqI z7+c{uKCD;h7@TfVpPwMnuesnJSb~@}7o3nzS_pHub7}wdRhX&Y&FN!Qsl_t|;5nwh zLPe!sRq`K+xFFL(YxC#)m+gkUCDcFp%CwpEZ6_+E4yzkU6*Wg?)AVlZtH$%^;tjaS z9~!NL6pF%B1KqFJ1bEB1K<}p-1ahCxgX#@8a{$gU!bg&4i$p$hJPZn%$Q&vPR+}z7EE5P zW_p)|Pb>f(C-?JwoL73CW@^y`;PZk*(;;JDZ=H;>(!%`ikydUmAMokx(zDC>*kU_W zaOP6I^L%6d3)-WCWe5K5Mw-@J^_fLZ+tF}8kk(FP+Bd1#VRi^{t26zLC+#2rQN8>< zjJu$TC&n|%_LOWjW*g+~cSIRd{3NaEdI6jUsZ|#i)o5RTbdTt(Bl( z!gpw=%Z^qZ?6jS0e{ArrA!ikeIPsfX!VlkAuzAJzuEShrFCkiuPQ(0n2Sm6Iq^v5C>m5XPpR+f1E@AvbyMV#b_i`7+KrrHaDYbbOh(QvuQtG~!_a z{zKSTub%SxuvhI5PidM$n>c^tqnu>Rw%cYtMxN@A_R4Q9q~52r0BB9IEC_NInB5xL z2W2cZ$=0`>iCIVPj{Kb|My^e8ObZm9xm01yb_PD(7&%el?zS#S<K5D&u7$w z$ix$O2wy_WdsswOX6i;4==YpNzdAFNTd6NMtLp>eQiT{*Rku^nZm@2r(^95XWyNq% z9tOjI_Ey3wpes-waY5B8C0zP+@i_Y7F%ixANBW4Gxr!?5f}06^;{MG1RN6z^cI-d` zRWJKO$vo{KrBF(Yyr;bj6X#aL`8NgK8Y^+Z9ki!m*rr8~Bg9yfK&Ie#e zYnyBBvI+o8_BpzY?*q`BY#$)gjR;vZ3vzw3%HnD7ZRiiOCxOx<=vIN2_YI@)95bX1 z1KB(%Z<>{&M|Xa-OKcu`j$dR$Ch?j$r5j}n8Nt&wE8PmP#NQ0{m#tQ%bs7tYQZ_N9oO09 z3W~>9$yG7KLau!ruO5~roxD0S(BN*3Pcl=KL6(}-ArG(I-pVW?T&j-r|ATtE$rtxB z>r02?y-95iojAPUgxS)g^g_o1RZZIHR-Dg&3LG=@2r-)P!b(=EIGK?x56+g5&2` z>I@J$hvNW*baf)^G^ugQD6Kd}{(4}GB7M*(Cfo9>A$(XBeQ(2r&{PW*u8hdJW$owzyw}{($+FaVEm04kyc2&)DUiBnIm8EUZ`W z%(Wt3mkPMiE%lpW1xd%2amR@|4&JUm!D5|Q_Aojpz1sN`Sex#6VhH~Y;_8?%QtDMaN891BpcCjxFa#fwP_D&6IGNEYseun=7P@6eVA7)XkpwwGv|wjO!`ko^hJEi`UGSh&%ON5vjhr> z8T%OYWSWtJ1Pfbu+Guj)t9)&|++lQR7{l}o(zi)qL-#9K#G0&SyG-Zs2{|li1mO38VyhO1r<+y2bMq|lL*bT1HYnULT7Y0y~ zU58jF48+ZG>ZNd7#1_)AV-teG6$MiE#Q#iKiry_xU!|$YR|%6P{Y9v)v+LmcOU%uY z(o?__DoPDNs8#D7x^v;F}dGrlQo&3$3_({ vvXiI3Bq0lGLoHnI$UA&X_1|f^o`Q6>UrDP&N_;3|YpK8*`j4v~+Q0rk`_A?E literal 0 HcmV?d00001 diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.svg b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.svg new file mode 100644 index 0000000000..82edb64974 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/connectors/through_across.svg @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + current + force + stored energy =capacitance x voltage + stored energy = mass x velocity + (f) + (i) + (v) + (v) + + + Through Variable + Across Variable + + diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/index.md b/lib/ModelingToolkitStandardLibrary/docs/src/index.md new file mode 100644 index 0000000000..b0319ca602 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/index.md @@ -0,0 +1,105 @@ +# ModelingToolkitStandardLibrary.jl + +ModelingToolkitStandardLibrary.jl is a standard library for the +[ModelingToolkit](https://docs.sciml.ai/ModelingToolkit/stable/) acausal modeling system. + +## Installation + +To install ModelingToolkitStandardLibrary.jl, use the Julia package manager: + +```julia +using Pkg +Pkg.add("ModelingToolkitStandardLibrary") +``` + +## Tutorials + + - [RC Circuit](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/tutorials/rc_circuit/) + - [Custom Component](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/tutorials/custom_component/) + - [Thermal Model](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/tutorials/thermal_model/) + - [DC Motor with PI-controller](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/tutorials/dc_motor_pi/) + +## Libraries + +The following are the constituent libraries of the ModelingToolkit Standard Library. + + - [Basic Blocks](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/blocks/) + - [Mechanical Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/mechanical/) + - [Electrical Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/electrical/) + - [Magnetic Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/magnetic/) + - [Thermal Components](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/thermal/) + - [Hydraulic Components](@ref hydraulic) + +## Contributing + + - Please refer to the + [SciML ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://github.com/SciML/ColPrac/blob/master/README.md) + for guidance on PRs, issues, and other matters relating to contributing to SciML. + + - See the [SciML Style Guide](https://github.com/SciML/SciMLStyle) for common coding practices and other style decisions. + - There are a few community forums: + + + The #diffeq-bridged and #sciml-bridged channels in the + [Julia Slack](https://julialang.org/slack/) + + The #diffeq-bridged and #sciml-bridged channels in the + [Julia Zulip](https://julialang.zulipchat.com/#narrow/stream/279055-sciml-bridged) + + On the [Julia Discourse forums](https://discourse.julialang.org) + + See also [SciML Community page](https://sciml.ai/community/) + +## Reproducibility + +```@raw html +
The documentation of this SciML package was built using these direct dependencies, +``` + +```@example +using Pkg # hide +Pkg.status() # hide +``` + +```@raw html +
+``` + +```@raw html +
and using this machine and Julia version. +``` + +```@example +using InteractiveUtils # hide +versioninfo() # hide +``` + +```@raw html +
+``` + +```@raw html +
A more complete overview of all dependencies and their versions is also provided. +``` + +```@example +using Pkg # hide +Pkg.status(; mode = PKGMODE_MANIFEST) # hide +``` + +```@raw html +
+``` + +```@eval +using TOML +using Markdown +version = TOML.parse(read("../../Project.toml", String))["version"] +name = TOML.parse(read("../../Project.toml", String))["name"] +link_manifest = "https://github.com/SciML/" * name * ".jl/tree/gh-pages/v" * version * + "/assets/Manifest.toml" +link_project = "https://github.com/SciML/" * name * ".jl/tree/gh-pages/v" * version * + "/assets/Project.toml" +Markdown.parse("""You can also download the +[manifest]($link_manifest) +file and the +[project]($link_project) +file. +""") +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/MOSFET_calibration.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/MOSFET_calibration.md new file mode 100644 index 0000000000..e4e5b4eb6c --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/MOSFET_calibration.md @@ -0,0 +1,79 @@ +# MOSFET I-V Curves + +In this example, first we'll demonstrate the I-V curves of the NMOS transistor model. +First of all, we construct a circuit using the NMOS transistor. We'll need to import ModelingToolkit and the Electrical standard library that holds the transistor models. + +```@example NMOS +using ModelingToolkit +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Blocks: Constant +using OrdinaryDiffEq +using Plots +``` + +Here we just connect the source pin to ground, the drain pin to a voltage source named `Vcc`, and the gate pin to a voltage source named `Vb`. + +```@example NMOS +@mtkmodel SimpleNMOSCircuit begin + @components begin + Q1 = NMOS() + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + + Vcc_const = Constant(k = V_cc) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 5.0 + V_b = 3.5 + end + @equations begin + #voltage sources + connect(Vcc_const.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.s) + + #other stuff + connect(Vcc.p, Q1.d) + connect(Vb.p, Q1.g) + end +end + +@mtkbuild sys = SimpleNMOSCircuit(V_cc = 5.0, V_b = 3.5) + +prob = ODEProblem(sys, Pair[], (0.0, 10.0)) +sol = solve(prob) +``` + +Now to make sure that the transistor model is working like it's supposed to, we can examine the plots of the drain-source voltage vs. the drain current, otherwise knowns as the I-V curve of the transistor. + +```@example NMOS +v_cc_list = collect(0.05:0.1:10.0) + +I_D_list = [] +I_D_lists = [] + +for V_b in [1.0, 1.4, 1.8, 2.2, 2.6] + I_D_list = [] + for V_cc in v_cc_list + @mtkbuild sys = SimpleNMOSCircuit(V_cc = V_cc, V_b = V_b) + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob) + push!(I_D_list, sol[sys.Q1.d.i][1]) + end + push!(I_D_lists, I_D_list) +end + +reduce(hcat, I_D_lists) +plot(v_cc_list, I_D_lists, title = "NMOS IV Curves", + label = ["V_GS: 1.0 V" "V_GS: 1.4 V" "V_GS: 1.8 V" "V_GS: 2.2 V" "V_GS: 2.6 V"], + xlabel = "Drain-Source Voltage (V)", ylabel = "Drain Current (A)") +``` + +We can see that we get exactly what we would expect: as the drain-source voltage increases, the drain current increases, until the the transistor gets in to the saturation region of operation. +Then the only increase in drain current is due to the channel-length modulation effect. Additionally, we can see that the maximum current reached increases as the gate voltage increases. diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/custom_component.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/custom_component.md new file mode 100644 index 0000000000..0cbab9eaad --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/custom_component.md @@ -0,0 +1,127 @@ +# Custom Component + +In this tutorial, the creation of a custom component is demonstrated via the [Chua's circuit](https://en.wikipedia.org/wiki/Chua%27s_circuit). +The circuit is a simple circuit that shows chaotic behavior. +Except for a non-linear resistor, every other component already is part of `ModelingToolkitStandardLibrary.Electrical`. + +First, we need to make some imports. + +```@example components +using ModelingToolkit +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Electrical +using OrdinaryDiffEq +using Plots +``` + +## Custom Component + +Now the custom component can be defined. +The Modelica implementation of the `NonlinearResistor` looks as follows: + +```Modelica +model NonlinearResistor "Chua's resistor" + extends Interfaces.OnePort; + + parameter SI.Conductance Ga "conductance in inner voltage range"; + parameter SI.Conductance Gb "conductance in outer voltage range"; + parameter SI.Voltage Ve "inner voltage range limit"; +equation + i = if (v < -Ve) then Gb*(v + Ve) - Ga*Ve else if (v > Ve) then Gb*(v - Ve) + Ga*Ve else Ga*v; +end NonlinearResistor; +``` + +this can almost be directly translated to the syntax of `ModelingToolkit`. + +```@example components +@mtkmodel NonlinearResistor begin + @extend OnePort() + @parameters begin + Ga + Gb + Ve + end + @equations begin + i ~ ifelse(v < -Ve, + Gb * (v + Ve) - Ga * Ve, + ifelse(v > Ve, + Gb * (v - Ve) + Ga * Ve, + Ga * v)) + end +end +nothing # hide +``` + +### Explanation + +Since the non-linear resistor is essentially a standard electrical component with two ports, we can extend from the `OnePort` component of the library. + +```julia +@extend OnePort() +``` + +This extends `OnePort` and unpacks `v` and `i` variables. + +It might be a good idea to create parameters for the constants of the `NonlinearResistor`. + +```julia +@parameters begin + Ga + Gb + Ve +end +``` + +This creates symbolic parameters with the name `Ga`, `Gb` and `Ve` whose default values are set from the function's arguments `Ga`, `Gb` and `Ve`, respectively. +This allows the user to `remake` the problem easily with different parameters or allow for auto-tuning or parameter optimization without having to do all the costly steps that may be involved with building and simplifying a model. +The non-linear (in this case piece-wise constant) equation for the current can be implemented using `ifelse`. + +## Building the Model + +The final model can now be created with the components from the library and the new custom component. + +```@example components +@mtkmodel ChaoticAttractor begin + @components begin + inductor = Inductor(L = 18, i = 0) + resistor = Resistor(R = 12.5e-3) + conductor = Conductor(G = 0.565) + capacitor1 = Capacitor(C = 10, v = 4) + capacitor2 = Capacitor(C = 100, v = 0) + non_linear_resistor = NonlinearResistor( + Ga = -0.757576, + Gb = -0.409091, + Ve = 1 + ) + ground = Ground() + end + @equations begin + connect(inductor.p, conductor.p) + connect(conductor.n, non_linear_resistor.p) + connect(capacitor1.p, conductor.n) + connect(inductor.n, resistor.p) + connect(conductor.p, capacitor2.p) + connect(capacitor1.n, capacitor2.n, non_linear_resistor.n, resistor.n, ground.g) + end +end +nothing # hide +``` + +## Simulating the Model + +`@mtkcompile` builds a structurally simplified `ChaoticAttractor` model. +Since the initial voltage of the capacitors was already specified via `v` and the initial current of inductor via `i`, no initial condition is given and an empty pair is supplied. + +```@example components +@mtkcompile sys = ChaoticAttractor() +prob = ODEProblem(sys, Pair[], (0, 5e4)) +sol = solve(prob; saveat = 1.0) + +plot(sol[sys.capacitor1.v], sol[sys.capacitor2.v], title = "Chaotic Attractor", label = "", + ylabel = "C1 Voltage in V", xlabel = "C2 Voltage in V") +``` + +```@example components +plot(sol; idxs = [sys.capacitor1.v, sys.capacitor2.v, sys.inductor.i], + labels = ["C1 Voltage in V" "C2 Voltage in V" "Inductor Current in A"]) +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/dc_motor_pi.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/dc_motor_pi.md new file mode 100644 index 0000000000..1ebc59f4d5 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/dc_motor_pi.md @@ -0,0 +1,141 @@ +# DC Motor with PI-controller + +In this example, a PI-controller is set up for speed control of a DC-motor. An equivalent circuit diagram is depicted below. + +![DC-motor](https://user-images.githubusercontent.com/50108075/196108356-0e8605e3-61a9-4006-8559-786252e55928.png) + +## Modeling and simulation + +The electrical part consists of a resistance and inductance. The coupling between the electrical and rotational domain is done via an electro-motive force (EMF) component. The voltage across the EMF is proportional to the angular velocity and the current is proportional to the torque. On the mechanical side, viscous friction in, e.g., a bearing and the inertia of the shaft is modelled. + +A PI-controller with anti-windup measure should be used as a speed controller. A simulation is performed to verify the tracking performance of the controller and the disturbance rejection capabilities. + +First, the needed packages are imported and the parameters of the model defined. + +```@example dc_motor_pi +using ModelingToolkit +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq +using Plots +``` + +The actual model can now be composed. + +```@example dc_motor_pi +@mtkmodel DCMotor begin + @parameters begin + R = 0.5, [description = "Armature resistance"] # Ohm + L = 4.5e-3, [description = "Armature inductance"] # H + k = 0.5, [description = "Motor constant"] # N.m/A + J = 0.02, [description = "Inertia"] # kg.m² + f = 0.01, [description = "Friction factor"] # N.m.s/rad + tau_L_step = -0.3, [description = "Amplitude of the load torque step"] # N.m + end + @components begin + ground = Ground() + source = Voltage() + ref = Blocks.Step(height = 1, start_time = 0) + pi_controller = Blocks.LimPI(k = 1.1, T = 0.035, u_max = 10, Ta = 0.035) + feedback = Blocks.Feedback() + R1 = Resistor(R = R) + L1 = Inductor(L = L) + emf = EMF(k = k) + fixed = Fixed() + load = Torque() + load_step = Blocks.Step(height = tau_L_step, start_time = 3) + inertia = Inertia(J = J) + friction = Damper(d = f) + speed_sensor = SpeedSensor() + end + @equations begin + 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) + end +end + +@named model = DCMotor() +nothing # hide +``` + +Now the model can be simulated. Typical rotational mechanical systems are described via `DAE` +(differential algebraic equations), however in this case, ModelingToolkit can simplify the model enough +so that it can be represented as a system of `ODEs` (ordinary differential equations). + +```@example dc_motor_pi +sys = mtkcompile(model) +# Provide complete initial conditions for all state variables +u0 = Dict( + sys.L1.i => 0.0, # Initial inductor current + sys.inertia.w => 0.0, # Initial angular velocity + sys.inertia.phi => 0.0, # Initial angle + sys.pi_controller.int.x => 0.0 # Initial PI integrator state +) +prob = ODEProblem(sys, u0, (0, 6.0)) +sol = solve(prob) + +p1 = plot(sol.t, sol[sys.inertia.w], ylabel = "Angular Vel. in rad/s", + label = "Measurement", title = "DC Motor with Speed Controller") +plot!(sol.t, sol[sys.ref.output.u], label = "Reference") +p2 = plot(sol.t, sol[sys.load.tau.u], ylabel = "Disturbance in Nm", label = "") +plot(p1, p2, layout = (2, 1)) +``` + +## Closed-loop analysis + +When implementing and tuning a control system in simulation, it is a good practice to analyze the closed-loop properties and verify robustness of the closed-loop with respect to, e.g., modeling errors. To facilitate this, we added two analysis points to the set of connections above, more specifically, we added the analysis points named `:y` and `:u` to the connections (for more details on analysis points, see [Linear Analysis](@ref)) + +```julia +connect(sys.speed_sensor.w, :y, sys.feedback.input2) +connect(sys.pi_controller.ctr_output, :u, sys.source.V) +``` + +one at the plant output (`:y`) and one at the plant input (`:u`). We may use these analysis points to calculate, e.g., sensitivity functions, illustrated below. Here, we calculate the sensitivity function $S(s)$ and the complimentary sensitivity function $T(s) = I - S(s)$, defined as + +```math +\begin{aligned} +S(s) &= \dfrac{1}{I + P(s)C(s)} \\ +T(s) &= \dfrac{P(s)C(s)}{I + P(s)C(s)} +\end{aligned} +``` + +```@example dc_motor_pi +using ControlSystemsBase +# Get sensitivity function +matrices_S, +simplified_sys_S = Blocks.get_sensitivity( + model, :y, op = Dict(unknowns(sys) .=> 0.0)) +So = ss(matrices_S...) |> minreal # The output-sensitivity function as a StateSpace system +# Get complementary sensitivity function +matrices_T, +simplified_sys_T = Blocks.get_comp_sensitivity( + model, :y, op = Dict(unknowns(sys) .=> 0.0)) +To = ss(matrices_T...)# The output complementary sensitivity function as a StateSpace system +bodeplot([So, To], label = ["S" "T"], plot_title = "Sensitivity functions", + plotphase = false) +``` + +Similarly, we may compute the loop-transfer function and plot its Nyquist curve + +```@example dc_motor_pi +matrices_L, +simplified_sys_L = Blocks.get_looptransfer( + model, :y, op = Dict(unknowns(sys) .=> 0.0)) +L = -ss(matrices_L...) # The loop-transfer function as a StateSpace system. The negative sign is to negate the built-in negative feedback +Ms, ωMs = hinfnorm(So) # Compute the peak of the sensitivity function to draw a circle in the Nyquist plot +nyquistplot(L, label = "\$L(s)\$", ylims = (-2.5, 0.5), xlims = (-1.2, 0.1), + Ms_circles = Ms) +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/input_component.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/input_component.md new file mode 100644 index 0000000000..dbab21ff41 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/input_component.md @@ -0,0 +1,295 @@ +# Building Models with Discrete Data, Interpolations, and Lookup Tables + +There are 4 ways to include data as part of a model. + + 1. using `ModelingToolkitStandardLibrary.Blocks.Interpolation` + 2. using `ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation` + 3. using a custom component with external data (not recommended) + 4. using `ModelingToolkitStandardLibrary.Blocks.SampledData` (legacy) + +This tutorial demonstrate each case and explain the pros and cons of each. + +## `Interpolation` Block + +The `ModelingToolkitStandardLibrary.Blocks.Interpolation` component is easy to use and is performant. +It is similar to using callable parameters, but it provides a block interface with `RealInput` and `RealOutput` connectors. +The `Interpolation` is compatible with interpolation types from `DataInterpolation`. + +```@docs +ModelingToolkitStandardLibrary.Blocks.Interpolation +``` + +Here is an example on how to use it. Let's consider a mass-spring-damper system, where +we have an external force as an input. We then generate some example data in a `DataFrame` +that would represent a measurement of the input. In a more realistic case, this `DataFrame` +would be read from a file. + +```@example interpolation_block +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks +using DataInterpolations +using OrdinaryDiffEq +using DataFrames +using Plots + +function MassSpringDamper(; name) + @named input = RealInput() + @variables f(t) x(t)=0 dx(t)=0 ddx(t) + @parameters m=10 k=1000 d=1 + + eqs = [f ~ input.u + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t; name, systems = [input]) +end + +function MassSpringDamperSystem(data, time; name) + @named src = Interpolation(LinearInterpolation, data, time) + @named clk = ContinuousClock() + @named model = MassSpringDamper() + + eqs = [connect(clk.output, src.input) + connect(src.output, model.input)] + + System(eqs, t, [], []; name, systems = [src, clk, model]) +end + +function generate_data() + dt = 4e-4 + time = 0:dt:0.1 + data = sin.(2 * pi * time * 100) + + return DataFrame(; time, data) +end + +df = generate_data() # example data + +@named system = MassSpringDamperSystem(df.data, df.time) +sys = mtkcompile(system) +prob = ODEProblem(sys, [], (0, df.time[end])) +sol = solve(prob) +plot(sol) +``` + +Note that in the case of the `Interpolation` block, the `data` and the `time` act like +structural parameters. + +As such, we can also build the interpolation object outside of the model + +```@example interpolation_block +my_interpolation = LinearInterpolation(df.data, df.time) + +@mtkmodel MassSpringDamperSystem2 begin + @components begin + src = Interpolation(itp = my_interpolation) + clk = ContinuousClock() + model = MassSpringDamper() + end + @equations begin + connect(src.input, clk.output) + connect(src.output, model.input) + end +end; +@mtkcompile sys = MassSpringDamperSystem2() + +prob = ODEProblem(sys, [], (0, df.time[end])) +sol = solve(prob, Tsit5()) +plot(sol) +``` + +Note that the interpolation is constructed outside of the model, so we cannot use `remake` to change the +data. For that usecase, see the `ParametrizedInterpolation`. + +## `ParametrizedInterpolation` Block + +The `ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation` component is similar to `Interpolation`, but as the name suggests, it is parametrized by the data, allowing one to change the underlying data without rebuilding the model as the data is represented via vector parameters. +The main advantage of this block over the [`Interpolation`](@ref) one is that one can use it for optimization problems. Currently, this supports forward mode AD via ForwardDiff, but due to the increased flexibility of the types in the component, this is not as fast as the `Interpolation` block, +so it is recommended to use only when the added flexibility is required. + +```@docs +ModelingToolkitStandardLibrary.Blocks.ParametrizedInterpolation +``` + +Here is an example on how to use it + +```@example parametrized_interpolation +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks +using DataInterpolations +using OrdinaryDiffEq +using DataFrames +using Plots + +function MassSpringDamper(; name) + @named input = RealInput() + vars = @variables f(t) x(t)=0 dx(t) [guess = 0] ddx(t) + pars = @parameters m=10 k=1000 d=1 + + eqs = [f ~ input.u + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t, vars, pars; name, systems = [input]) +end + +function MassSpringDamperSystem(data, time; name) + @named src = ParametrizedInterpolation(LinearInterpolation, data, time) + @named clk = ContinuousClock() + @named model = MassSpringDamper() + + eqs = [connect(model.input, src.output) + connect(clk.output, src.input)] + + System(eqs, t; name, systems = [src, clk, model]) +end + +function generate_data() + dt = 4e-4 + time = 0:dt:0.1 + data = sin.(2 * pi * time * 100) + + return DataFrame(; time, data) +end + +df = generate_data() # example data + +@named system = MassSpringDamperSystem(df.data, df.time) +sys = mtkcompile(system) +prob = ODEProblem(sys, [], (0, df.time[end])) +sol = solve(prob) +plot(sol) +``` + +If we want to run a new data set, this requires only remaking the problem and solving again + +```@example parametrized_interpolation +prob2 = remake(prob, p = [sys.src.data => ones(length(df.data))]) +sol2 = solve(prob2) +plot(sol2) +``` + +!!! note + + Note that when changing the data, the length of the new data must be the same as the length of the original data. + +## Custom Component with External Data + +The below code shows how to include data using a `Ref` and registered `get_sampled_data` function. This example uses a very basic function which requires non-adaptive solving and sampled data. As can be seen, the data can easily be set and changed before solving. + +```@example custom_component_external_data +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq + +const rdata = Ref{Vector{Float64}}() + +dt = 4e-4 +time = 0:dt:0.1 +# Data Sets +data1 = sin.(2 * pi * time * 100) +data2 = cos.(2 * pi * time * 50) + +function get_sampled_data(t) + i = floor(Int, t / dt) + 1 + x = rdata[][i] + + return x +end + +Symbolics.@register_symbolic get_sampled_data(t) + +function CustomSystem(; name) + vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0 + pars = @parameters m=10 k=1000 d=1 + + eqs = [f ~ get_sampled_data(t) + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t, vars, pars; name) +end + +@named system = CustomSystem() +sys = mtkcompile(system) +prob = ODEProblem(sys, [], (0, time[end])) + +rdata[] = data1 +sol1 = solve(prob, ImplicitEuler(); dt, adaptive = false) +ddx1 = sol1[sys.ddx] + +rdata[] = data2 +sol2 = solve(prob, ImplicitEuler(); dt, adaptive = false) +ddx2 = sol2[sys.ddx] +``` + +The drawback of this method is that the solution observables can be linked to the data `Ref`, which means that if the data changes then the observables are no longer valid. In this case `ddx` is an observable that is derived directly from the data. Therefore, `sol1[sys.ddx]` is no longer correct after the data is changed for `sol2`. + +```julia +# the following test will fail +@test all(ddx1 .== sol1[sys.ddx]) #returns false +``` + +Additional code could be added to resolve this issue, for example by using a `Ref{Dict}` that could link a parameter of the model to the data source. This would also be necessary for parallel processing. + +## `SampledData` Component + +To resolve the issues presented above, the `ModelingToolkitStandardLibrary.Blocks.SampledData` component can be used which allows for a resusable `System` and self contained data which ensures a solution which remains valid for it's lifetime. Now it's possible to also parallelize the call to `solve()`. + +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq + +function SampledDataSystem(; name) + src = SampledData(Float64, name = :src) + + vars = @variables f(t)=0 x(t)=0 dx(t)=0 ddx(t)=0 + pars = @parameters m=10 k=1000 d=1 + + eqs = [f ~ src.output.u + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t, vars, pars; systems = [src], name) +end + +@named system = SampledDataSystem() +sys = mtkcompile(system, split = false) +s = complete(system) + +dt = 4e-4 +time = 0:dt:0.1 +data1 = sin.(2 * pi * time * 100) +data2 = cos.(2 * pi * time * 50) + +prob = ODEProblem(sys, [], (0, time[end]); split = false, tofloat = false, use_union = true) +defs = ModelingToolkit.defaults(sys) + +function get_prob(data) + defs[s.src.buffer] = Parameter(data, dt) + # ensure p is a uniform type of Vector{Parameter{Float64}} (converting from Vector{Any}) + p = Parameter.(ModelingToolkit.varmap_to_vars(defs, parameters(sys); tofloat = false)) + remake(prob; p, build_initializeprob = false) +end + +prob1 = get_prob(data1) +prob2 = get_prob(data2) + +sol1 = Ref{ODESolution}() +sol2 = Ref{ODESolution}() +@sync begin + @async sol1[] = solve(prob1, ImplicitEuler()) + @async sol2[] = solve(prob2, ImplicitEuler()) +end +``` + +Note, in the above example, we can build the system with an empty `SampledData` component, only setting the expected data type: `@named src = SampledData(Float64)`. It's also possible to initialize the component with real sampled data: `@named src = SampledData(data, dt)`. Additionally note that before running an `ODEProblem` using the `SampledData` component, one must be careful about the parameter vector Type. The `SampledData` component contains a `buffer` parameter of type `Parameter`, therefore we must generate the problem using `tofloat=false`. This will initially give a parameter vector of type `Vector{Any}` with a mix of numbers and `Parameter` type. We can convert the vector to a uniform `Parameter` type by running `p = Parameter.(p)`. This will wrap all the single values in a `Parameter` which will be mathematically equivalent to a `Number`. diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/rc_circuit.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/rc_circuit.md new file mode 100644 index 0000000000..d1249e51b4 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/rc_circuit.md @@ -0,0 +1,43 @@ +# RC Circuit Model + +This tutorial is a simplified version of the [RC circuit tutorial in the +`ModelingToolkit.jl` documentation](https://docs.sciml.ai/ModelingToolkit/stable/tutorials/acausal_components/). +In that tutorial, the full RC circuit is built from scratch. Here, we will use the +components of the `Electrical` model in the ModelingToolkit Standard Library to simply +connect pre-made components and simulate the model. + +```@example +using ModelingToolkit, OrdinaryDiffEq, Plots +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Blocks: Constant +using ModelingToolkit: t_nounits as t + +@mtkmodel RC begin + @parameters begin + R = 1.0 + C = 1.0 + V = 1.0 + end + @components begin + resistor = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + @equations begin + connect(constant.output, source.V) + connect(source.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + end +end + +@mtkcompile sys = RC() +prob = ODEProblem(sys, Pair[], (0, 10.0)) +sol = solve(prob) + +plot(sol, idxs = [sys.capacitor.v, sys.resistor.i], + title = "RC Circuit Demonstration", + labels = ["Capacitor Voltage" "Resistor Current"]) +``` diff --git a/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/thermal_model.md b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/thermal_model.md new file mode 100644 index 0000000000..e47c7cb1c5 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/docs/src/tutorials/thermal_model.md @@ -0,0 +1,43 @@ +# Heat Conduction Model + +This example demonstrates the thermal response of two masses connected by a conducting element. +The two masses have the same heat capacity but different initial temperatures (`T1=100 [°C]`, `T2=0 [°C]`). +The mass with the higher temperature will cool off, while the mass with the lower temperature heats up. +They will each asymptotically approach the calculated temperature T_final_K that results +from dividing the total initial energy in the system by the sum of the heat capacities of each element. + +```@example +using ModelingToolkitStandardLibrary.Thermal, ModelingToolkit, OrdinaryDiffEq, Plots +using ModelingToolkit: t_nounits as t + +@mtkmodel HeatConductionModel begin + @parameters begin + C1 = 15 + C2 = 15 + end + @components begin + mass1 = HeatCapacitor(C = C1, T = 373.15) + mass2 = HeatCapacitor(C = C2, T = 273.15) + conduction = ThermalConductor(G = 10) + Tsensor1 = TemperatureSensor() + Tsensor2 = TemperatureSensor() + end + @equations begin + connect(mass1.port, conduction.port_a) + connect(conduction.port_b, mass2.port) + connect(mass1.port, Tsensor1.port) + connect(mass2.port, Tsensor2.port) + end +end + +@mtkcompile sys = HeatConductionModel() +prob = ODEProblem(sys, Pair[], (0, 5.0)) +sol = solve(prob) + +T_final_K = sol[(sys.mass1.T * sys.C1 + sys.mass2.T * sys.C2) / (sys.C1 + sys.C2)] + +plot(title = "Thermal Conduction Demonstration") +plot!(sol, idxs = [sys.mass1.T, sys.mass2.T], + labels = ["Mass 1 Temperature" "Mass 2 Temperature"]) +plot!(sol.t, T_final_K, label = "Steady-State Temperature") +``` diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/Blocks.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/Blocks.jl new file mode 100644 index 0000000000..be434fba76 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/Blocks.jl @@ -0,0 +1,34 @@ +""" +The module `Blocks` contains common input-output components, referred to as blocks. +""" +module Blocks +using ModelingToolkit, Symbolics +import IfElse: ifelse +import ..@symcheck +using ModelingToolkit: getdefault, t_nounits as t, D_nounits as D + +export RealInput, RealInputArray, RealOutput, RealOutputArray, SISO +include("utils.jl") + +export Gain, Sum, MatrixGain, Feedback, Add, Add3, Product, Division, Power, Modulo, + UnaryMinus, Floor, Ceil +export Abs, Sign, Sqrt, Sin, Cos, Tan, Asin, Acos, Atan, Atan2, Sinh, Cosh, Tanh, Exp +export Log, Log10 +include("math.jl") + +export Constant, TimeVaryingFunction, Sine, Cosine, ContinuousClock, Ramp, Step, ExpSine, + Square, Triangular, Parameter, SampledData, + Interpolation, ParametrizedInterpolation +include("sources.jl") + +export Limiter, DeadZone, SlewRateLimiter +include("nonlinear.jl") + +export Integrator, Derivative, FirstOrder, SecondOrder, StateSpace, TransferFunction +export PI, LimPI, PID, LimPID +include("continuous.jl") + +export AnalysisPoint, get_sensitivity, get_comp_sensitivity, + get_looptransfer, open_loop + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/continuous.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/continuous.jl new file mode 100644 index 0000000000..9f40b7de1d --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/continuous.jl @@ -0,0 +1,670 @@ +""" + Integrator(;name, k = 1, x = 0.0) + +Outputs `y = ∫k*u dt`, corresponding to the transfer function ``1/s``. +Initial value of integrator state ``x`` can be set with `x` + +# Connectors: + + - `input` + - `output` + +# Parameters: + + - `k`: Gain of integrator + +# Unknowns: + + - `x`: State of Integrator. Defaults to 0.0. +""" +@mtkmodel Integrator begin + @extend u, y = siso = SISO() + @variables begin + x(t) = 0.0, [description = "State of Integrator"] + end + @parameters begin + k = 1, [description = "Gain"] + end + @equations begin + D(x) ~ k * u + y ~ x + end +end + +""" + Derivative(; name, k = 1, T, x = 0.0) + +Outputs an approximate derivative of the input. The transfer function of this block is + +``` +k k ks +─ - ─────── = ────── +T sT² + T sT + 1 +``` + +and a state-space realization is given by `ss(-1/T, 1/T, -k/T, k/T)` +where `T` is the time constant of the filter. +A smaller `T` leads to a more ideal approximation of the derivative. + +Initial value of the state ``x`` can be set with `x`. + +# Parameters: + + - `k`: Gain + - `T`: [s] Time constant (T>0 required; T=0 is ideal derivative block) + +# Unknowns: + + - `x`: Unknown of Derivative. Defaults to 0.0. + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel Derivative begin + @extend u, y = siso = SISO() + @variables begin + x(t) = 0.0, [description = "Derivative-filter state"] + end + @parameters begin + T = T, [description = "Time constant"] + k = 1, [description = "Gain"] + end + begin + @symcheck T > 0 || + throw(ArgumentError("Time constant `T` has to be strictly positive")) + end + @equations begin + D(x) ~ (u - x) / T + y ~ (k / T) * (u - x) + end +end + +""" + FirstOrder(; name, k = 1.0, T, x = 0.0, lowpass = true) + +A first-order filter with a single real pole at `s = -1/T` and gain `k`. If `lowpass=true` (default), the transfer function +is given by ``Y(s)/U(s) = `` + + +``` + k +─────── +sT + 1 +``` + +and if `lowpass=false`, by + +``` +sT + 1 - k +────────── + sT + 1 +``` + +Initial value of the state `x` can be set with `x` + +# Parameters: + + - `k`: Gain + - `T`: [s] Time constant (T>0 required) + +# Connectors: + + - `input` + - `output` + +See also [`SecondOrder`](@ref) +""" +@mtkmodel FirstOrder begin + @extend u, y = siso = SISO() + @structural_parameters begin + lowpass = true + end + @variables begin + x(t) = 0.0, [description = "State of FirstOrder filter"] + end + @parameters begin + T = T, [description = "Time constant"] + k = 1.0, [description = "Gain"] + end + begin + @symcheck T > 0 || + throw(ArgumentError("Time constant `T` has to be strictly positive")) + end + @equations begin + D(x) ~ (k * u - x) / T + lowpass ? y ~ x : y ~ k * u - x + end +end + +""" + SecondOrder(; name, k = 1.0, w = 1.0, d = 1.0, x = 0.0, xd = 0.0) + +A second-order filter with gain `k`, a bandwidth of `w` rad/s and relative damping `d`. The transfer function +is given by `Y(s)/U(s) = ` + +``` + k*w^2 +───────────────── +s² + 2d*w*s + w^2 +``` + +Critical damping corresponds to `d=1`, which yields the fastest step response without overshoot, `d < 1` results in an underdamped filter while `d > 1` results in an overdamped filter. +`d = 1/√2` corresponds to a Butterworth filter of order 2 (maximally flat frequency response). +Initial value of the state `x` can be set with `x`, and of derivative state `xd` with `xd`. + +# Parameters: + + - `k`: Gain + - `w`: [`rad/s`] Angular frequency + - `d`: Damping + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel SecondOrder begin + @extend u, y = siso = SISO() + @variables begin + x(t), [description = "State of SecondOrder filter", guess = 0.0] + xd(t), [description = "Derivative state of SecondOrder filter", guess = 0.0] + end + @parameters begin + k = 1.0, [description = "Gain"] + w = 1.0, [description = "Bandwidth (angular frequency)"] + d = 1.0, [description = "Relative damping"] + end + @equations begin + D(x) ~ xd + D(xd) ~ w * (w * (k * u - x) - 2 * d * xd) + y ~ x + end +end + +""" + PI(;name, k = 1.0, T = 1.0, int.x = 0.0) + +Textbook version of a PI-controller without actuator saturation and anti-windup measure. +The proportional gain can be set with `k` +Initial value of integrator state `x` can be set with `int.x` + +The PI controller is implemented on standard form: +```math +U(s) = k (1 + \\dfrac{1}{sT}) E(S) +``` + +# Parameters: + - `k`: Proportional gain + - `T`: [s] Integrator time constant (T>0 required) + +# Connectors: + + - `err_input` + - `ctr_output` + +See also [`LimPI`](@ref) +""" +@mtkmodel PI begin + @parameters begin + k = 1.0, [description = "Proportional gain"] + T = 1.0, [description = "Integrator time constant"] + end + begin + @symcheck T > 0 || + throw(ArgumentError("Time constant `T` has to be strictly positive")) + end + @components begin + err_input = RealInput() # control error + ctr_output = RealOutput() # control signal + gainPI = Gain(; k) + addPI = Add() + int = Integrator(k = 1 / T, x = 0.0) + end + @equations begin + connect(err_input, addPI.input1) + connect(addPI.output, gainPI.input) + connect(gainPI.output, ctr_output) + connect(err_input, int.input) + connect(int.output, addPI.input2) + end +end + +""" + PID(;name, k=1, Ti=false, Td=false, Nd=10, int__x=0, der__x=0) + +Text-book version of a PID-controller without actuator saturation and anti-windup measure. + +# Parameters: + + - `k`: Gain + - `Ti`: [s] Integrator time constant (Ti>0 required). If set to false, no integral action is used. + - `Td`: [s] Derivative time constant (Td>0 required). If set to false, no derivative action is used. + - `Nd`: [s] Time constant for the derivative approximation (Nd>0 required; Nd=0 is ideal derivative). + - `int__x`: Initial value for the integrator. + - `der__x`: Initial value for the derivative state. + +# Connectors: + + - `err_input` + - `ctr_output` + +See also [`LimPID`](@ref) +""" +@component function PID(; name, k = 1, Ti = false, Td = false, Nd = 10, int__x = 0, + der__x = 0) + with_I = !isequal(Ti, false) + with_D = !isequal(Td, false) + @named err_input = RealInput() # control error + @named ctr_output = RealOutput() # control signal + @symcheck Ti ≥ 0 || + throw(ArgumentError("Ti out of bounds, got $(Ti) but expected Ti ≥ 0")) + @symcheck Td ≥ 0 || + throw(ArgumentError("Td out of bounds, got $(Td) but expected Td ≥ 0")) + @symcheck Nd > 0 || + throw(ArgumentError("Nd out of bounds, got $(Nd) but expected Nd > 0")) + + pars = @parameters begin + k = k, [description = "Proportional gain"] + Ti = Ti, [description = "Integrator time constant"] + Td = Td, [description = "Derivative time constant"] + Nd = Nd, [description = "Derivative limit"] + end + + @named gainPID = Gain(; k) + @named addPID = Add3() + if with_I + @named int = Integrator(k = 1 / Ti, x = int__x) + else + @named Izero = Constant(k = 0) + end + if with_D + @named der = Derivative(k = Td, T = 1 / Nd, x = der__x) + else + @named Dzero = Constant(k = 0) + end + sys = [err_input, ctr_output, gainPID, addPID] + if with_I + push!(sys, int) + else + push!(sys, Izero) + end + if with_D + push!(sys, der) + else + push!(sys, Dzero) + end + eqs = [ + connect(err_input, addPID.input1), + connect(addPID.output, gainPID.input), + connect(gainPID.output, ctr_output) + ] + if with_I + push!(eqs, connect(err_input, int.input)) + push!(eqs, connect(int.output, addPID.input2)) + else + push!(eqs, connect(Izero.output, addPID.input2)) + end + if with_D + push!(eqs, connect(err_input, der.input)) + push!(eqs, connect(der.output, addPID.input3)) + else + push!(eqs, connect(Dzero.output, addPID.input3)) + end + System(eqs, t, [], pars; name = name, systems = sys) +end + +""" + LimPI(; name, k = 1.0, T, Ta, int__x = 0.0, u_max = 1.0, u_min = -u_max) + +Text-book version of a PI-controller with actuator saturation and anti-windup measure. + +The PI controller is implemented on standard form +```math +u(t) = sat(k (e(t) + ∫\\dfrac{1}{T}e(t) dt) ) +``` +The simplified expression above is given without the anti-windup protection. + +# Parameters: + + - `k`: Proportional gain + - `T`: [s] Integrator time constant (T>0 required) + - `Ta`: [s] Tracking time constant (Ta>0 required) + +# Connectors: + + - `err_input` + - `ctr_output` +""" +@component function LimPI(; name, k = 1, T, u_max, u_min = -u_max, Ta, int__x = 0.0) + @symcheck Ta > 0 || + throw(ArgumentError("Time constant `Ta` has to be strictly positive")) + @symcheck T > 0 || throw(ArgumentError("Time constant `T` has to be strictly positive")) + @symcheck u_max ≥ u_min || throw(ArgumentError("u_min must be smaller than u_max")) + pars = @parameters begin + k = k, [description = "Proportional gain"] + T = T, [description = "Integrator time constant"] + Ta = Ta, [description = "Tracking time constant"] + u_max = u_max, [description = "Upper saturation limit"] + u_min = u_min, [description = "Lower saturation limit"] + end + @named err_input = RealInput() # control error + @named ctr_output = RealOutput() # control signal + @named gainPI = Gain(; k) + @named addPI = Add() + @named addTrack = Add() + @named int = Integrator(k = 1 / T, x = int__x) + @named limiter = Limiter(y_max = u_max, y_min = u_min) + @named addSat = Add(k1 = 1, k2 = -1) + @named gainTrack = Gain(k = 1 / Ta) + sys = [err_input, ctr_output, gainPI, addPI, int, addTrack, limiter, addSat, gainTrack] + eqs = [ + connect(err_input, addPI.input1), + connect(addPI.output, gainPI.input), + connect(gainPI.output, limiter.input), + connect(limiter.output, ctr_output), + connect(limiter.input, addSat.input2), + connect(limiter.output, addSat.input1), + connect(addSat.output, gainTrack.input), + connect(err_input, addTrack.input1), + connect(gainTrack.output, addTrack.input2), + connect(addTrack.output, int.input), + connect(int.output, addPI.input2) + ] + System(eqs, t, [], pars; name = name, systems = sys) +end + +""" + LimPID(; k, Ti=false, Td=false, wp=1, wd=1, Ni, Nd=12, u_max=Inf, u_min=-u_max, gains = false, name) + +Proportional-Integral-Derivative (PID) controller with output saturation, set-point weighting and integrator anti-windup. + +The equation for the control signal is roughly + +``` +k(ep + 1/Ti * ∫e + Td * d/dt(ed)) +e = u_r - u_y +ep = wp*u_r - u_y +ed = wd*u_r - u_y +``` + +where the transfer function for the derivative includes additional filtering, see `? Derivative` for more details. + +# Parameters: + + - `k`: Proportional gain + - `Ti`: [s] Integrator time constant. Set to `false` to turn off integral action. + - `Td`: [s] Derivative time constant. Set to `false` to turn off derivative action. + - `wp`: [0,1] Set-point weighting in the proportional part. + - `wd`: [0,1] Set-point weighting in the derivative part. + - `Nd`: [1/s] Derivative limit, limits the derivative gain to Nd/Td. Reasonable values are ∈ [8, 20]. A higher value gives a better approximation of an ideal derivative at the expense of higher noise amplification. + - `Ni`: `Ni*Ti` controls the time constant `Ta` of anti-windup tracking. A common (default) choice is `Ta = √(Ti*Td)` which is realized by `Ni = √(Td / Ti)`. Anti-windup can be effectively turned off by setting `Ni = Inf`. + - `gains`: If `gains = true`, `Ti` and `Td` will be interpreted as gains with a fundamental PID transfer function on parallel form `ki=Ti, kd=Td, k + ki/s + kd*s`. + +# Connectors: + + - `reference` + - `measurement` + - `ctr_output` +""" +@component function LimPID(; name, k = 1, Ti = false, Td = false, wp = 1, wd = 1, + Ni = Ti == 0 ? Inf : √(max(Td / Ti, 1e-6)), + Nd = 10, + u_max = Inf, + u_min = u_max > 0 ? -u_max : -Inf, + gains = false, + int__x = 0.0, + der__x = 0.0) + with_I = !isequal(Ti, false) + with_D = !isequal(Td, false) + with_AWM = Ni != Inf + if gains + Ti = k / Ti + Td = Td / k + end + @symcheck Ti ≥ 0 || + throw(ArgumentError("Ti out of bounds, got $(Ti) but expected Ti ≥ 0")) + @symcheck Td ≥ 0 || + throw(ArgumentError("Td out of bounds, got $(Td) but expected Td ≥ 0")) + @symcheck u_max ≥ u_min || throw(ArgumentError("u_min must be smaller than u_max")) + @symcheck Nd > 0 || + throw(ArgumentError("Nd out of bounds, got $(Nd) but expected Nd > 0")) + + pars = @parameters begin + k = k, [description = "Proportional gain"] + Ti = Ti, [description = "Integrator time constant"] + Td = Td, [description = "Derivative time constant"] + wp = wp, [description = "Set-point weighting in the proportional part"] + wd = wd, [description = "Set-point weighting in the derivative part"] + Ni = Ni, [description = "Anti-windup tracking gain"] + Nd = Nd, [description = "Derivative limit"] + u_max = u_max, [description = "Upper saturation limit"] + u_min = u_min, [description = "Lower saturation limit"] + end + @named reference = RealInput() + @named measurement = RealInput() + @named ctr_output = RealOutput() # control signal + @named addP = Add(k1 = wp, k2 = -1) + @named gainPID = Gain(; k) + @named addPID = Add3() + @named limiter = Limiter(y_max = u_max, y_min = u_min) + if with_I + if with_AWM + @named addI = Add3(k1 = 1, k2 = -1, k3 = 1) + @named addSat = Add(k1 = 1, k2 = -1) + @named gainTrack = Gain(k = 1 / (k * Ni)) + else + @named addI = Add(k1 = 1, k2 = -1) + end + @named int = Integrator(k = 1 / Ti, x = int__x) + else + @named Izero = Constant(k = 0) + end + if with_D + @named der = Derivative(k = Td, T = 1 / Nd, x = der__x) + @named addD = Add(k1 = wd, k2 = -1) + else + @named Dzero = Constant(k = 0) + end + + sys = [reference, measurement, ctr_output, addP, gainPID, addPID, limiter] + if with_I + if with_AWM + push!(sys, [addSat, gainTrack]...) + end + push!(sys, [addI, int]...) + else + push!(sys, Izero) + end + if with_D + push!(sys, [addD, der]...) + else + push!(sys, Dzero) + end + + eqs = [ + connect(reference, addP.input1), + connect(measurement, addP.input2), + connect(addP.output, addPID.input1), + connect(addPID.output, gainPID.input), + connect(gainPID.output, limiter.input), + connect(limiter.output, ctr_output) + ] + if with_I + push!(eqs, connect(reference, addI.input1)) + push!(eqs, connect(measurement, addI.input2)) + if with_AWM + push!(eqs, connect(limiter.input, addSat.input2)) + push!(eqs, connect(limiter.output, addSat.input1)) + push!(eqs, connect(addSat.output, gainTrack.input)) + push!(eqs, connect(gainTrack.output, addI.input3)) + end + push!(eqs, connect(addI.output, int.input)) + push!(eqs, connect(int.output, addPID.input3)) + else + push!(eqs, connect(Izero.output, addPID.input3)) + end + if with_D + push!(eqs, connect(reference, addD.input1)) + push!(eqs, connect(measurement, addD.input2)) + push!(eqs, connect(addD.output, der.input)) + push!(eqs, connect(der.output, addPID.input2)) + else + push!(eqs, connect(Dzero.output, addPID.input2)) + end + + System(eqs, t, [], pars; name = name, systems = sys) +end + +""" + StateSpace(A, B, C, D = 0; x = zeros(size(A,1)), u0 = zeros(size(B,2)), y0 = zeros(size(C,1)), name) + +A linear, time-invariant state-space system on the form. + +```math +\\begin{aligned} +ẋ &= Ax + Bu \\\\ +y &= Cx + Du +\\end{aligned} +``` + +Transfer functions can also be simulated by converting them to a StateSpace form. + +`y0` and `u0` can be used to set an operating point, providing them changes the dynamics from an LTI system to the affine system + +```math +\\begin{aligned} +ẋ &= Ax + B(u - u0) \\\\ +y &= Cx + D(u - u0) + y0 +\\end{aligned} +``` + +For a nonlinear system + +```math +\\begin{aligned} +ẋ &= f(x, u) \\\\ +y &= h(x, u) +\\end{aligned} +``` + +linearized around the operating point `x₀, u₀`, we have `y0, u0 = h(x₀, u₀), u₀`. +""" +@component function StateSpace(; A, B, C, D = nothing, x = zeros(size(A, 1)), name, + u0 = zeros(size(B, 2)), y0 = zeros(size(C, 1))) + nx, nu, ny = size(A, 1), size(B, 2), size(C, 1) + size(A, 2) == nx || error("`A` has to be a square matrix.") + size(B, 1) == nx || error("`B` has to be of dimension ($nx x $nu).") + size(C, 2) == nx || error("`C` has to be of dimension ($ny x $nx).") + if B isa AbstractVector + B = reshape(B, length(B), 1) + end + if isnothing(D) || iszero(D) + D = zeros(ny, nu) + else + size(D) == (ny, nu) || error("`D` has to be of dimension ($ny x $nu).") + end + @named input = RealInput(nin = nu) + @named output = RealOutput(nout = ny) + @variables x(t)[1:nx]=x [ + description = "State variables of StateSpace system $name" + ] + # pars = @parameters A=A B=B C=C D=D # This is buggy + eqs = [ # FIXME: if array equations work + [Differential(t)(x[i]) ~ + sum(A[i, k] * x[k] for k in 1:nx) + + sum(B[i, j] * (input.u[j] - u0[j]) for j in 1:nu) + for i in 1:nx]..., # cannot use D here + [output.u[j] ~ + sum(C[j, i] * x[i] for i in 1:nx) + + sum(D[j, k] * (input.u[k] - u0[k]) for k in 1:nu) + y0[j] + for j in 1:ny]... + ] + compose(System(eqs, t, vcat(x...), [], name = name), [input, output]) +end + +StateSpace(A, B, C, D = nothing; kwargs...) = StateSpace(; A, B, C, D, kwargs...) + +symbolic_eps(t) = eps(t) +@register_symbolic symbolic_eps(t) + +""" + TransferFunction(; b, a, name) + +A single input, single output, linear time-invariant system provided as a transfer-function. +``` +Y(s) = b(s) / a(s) U(s) +``` +where `b` and `a` are vectors of coefficients of the numerator and denominator polynomials, respectively, ordered such that the coefficient of the highest power of `s` is first. + +The internal state realization is on controller canonical form, with state variable `x`, output variable `y` and input variable `u`. For numerical robustness, the realization used by the integrator is scaled by the last entry of the `a` parameter. The internally scaled state variable is available as `x_scaled`. + +To set the initial state, it's recommended to set the initial condition for `x`, and let that of `x_scaled` be computed automatically. + +# Parameters: +- `b`: Numerator polynomial coefficients, e.g., `2s + 3` is specified as `[2, 3]` +- `a`: Denominator polynomial coefficients, e.g., `s² + 2ωs + ω^2` is specified as `[1, 2ω, ω^2]` + +# Connectors: + - `input` + - `output` + +See also [`StateSpace`](@ref) which handles MIMO systems, as well as [ControlSystemsMTK.jl](https://juliacontrol.github.io/ControlSystemsMTK.jl/stable/) for an interface between [ControlSystems.jl](https://juliacontrol.github.io/ControlSystems.jl/stable/) and ModelingToolkit.jl for advanced manipulation of transfer functions and linear statespace systems. For linearization, see [`linearize`](@ref) and [Linear Analysis](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/linear_analysis/). +""" +@component function TransferFunction(; b = [1], a = [1, 1], name) + nb = length(b) + na = length(a) + nb <= na || + error("Transfer function is not proper, the numerator must not be longer than the denominator") + nx = na - 1 + nbb = max(0, na - nb) + + @named begin + input = RealInput() + output = RealOutput() + end + + @parameters begin + b[1:nb] = b, + [ + description = "Numerator coefficients of transfer function (e.g., 2s + 3 is specified as [2,3])" + ] + a[1:na] = a, + [ + description = "Denominator coefficients of transfer function (e.g., `s² + 2ωs + ω^2` is specified as [1, 2ω, ω^2])" + ] + bb[1:(nbb + nb)] = [zeros(nbb); b] + end + d = bb[1] / a[1]# , [description = "Direct feedthrough gain"] + + a = collect(a) + a_end = ifelse(a[end] > 100 * symbolic_eps(sqrt(a' * a)), a[end], 1.0) + + pars = [collect(b); a; collect(bb)] + @variables begin + x(t)[1:nx] = zeros(nx), + [description = "State of transfer function on controller canonical form"] + x_scaled(t)[1:nx] = collect(x) * a_end, [description = "Scaled vector x"] + u(t), [description = "Input of transfer function"] + y(t), [description = "Output of transfer function"] + end + + x = collect(x) + x_scaled = collect(x_scaled) + bb = collect(bb) + + sts = [x; x_scaled; y; u] + + if nx == 0 + eqs = [y ~ d * u] + else + eqs = Equation[D(x_scaled[1]) ~ (-a[2:na]'x_scaled + a_end * u) / a[1] + D.(x_scaled[2:nx]) .~ x_scaled[1:(nx - 1)] + y ~ ((bb[2:na] - d * a[2:na])'x_scaled) / a_end + d * u + x .~ x_scaled ./ a_end] + end + push!(eqs, input.u ~ u) + push!(eqs, output.u ~ y) + compose(System(eqs, t, sts, pars; name = name), input, output) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/math.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/math.jl new file mode 100644 index 0000000000..d2ad76fc0e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/math.jl @@ -0,0 +1,523 @@ +""" + Gain(; name, k) + +Output the product of a gain value with the input signal. + +# Parameters: + + - `k`: Scalar gain + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel Gain begin + @extend u, y = siso = SISO() + @parameters begin + k, [description = "Gain"] + end + @equations begin + y ~ k * u + end +end +Gain.f(k; name) = Gain.f(; k, name) + +""" + MatrixGain(; K::AbstractArray, name) + +Output the product of a gain matrix with the input signal vector. + +# Structural parameters: + + - `K`: Matrix gain + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel MatrixGain begin + @structural_parameters begin + K + end + begin + nout = size(K, 1) + nin = size(K, 2) + end + @components begin + input = RealInput(; nin = nin) + output = RealOutput(; nout = nout) + end + @equations begin + [output.u[i] ~ sum(K[i, j] * input.u[j] for j in 1:nin) + for i in 1:nout]... + end +end + +""" + Sum(; input__nin::Int, name) + +Output the sum of the elements of the input port vector. +Input port dimension can be set with `input__nin` + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel Sum begin + @components begin + input = RealInput(; nin) + output = RealOutput() + end + @equations begin + output.u ~ sum(input.u) + end +end + +""" + Feedback(; name) + +Output difference between reference input (input1) and feedback input (input2). + +# Connectors: + + - `input1` + - `input2` + - `output` +""" +@mtkmodel Feedback begin + @components begin + input1 = RealInput() + input2 = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ input1.u - input2.u + end +end + +""" + Add(; name, k1 = 1.0, k2 = 1.0) + +Output the sum of the two scalar inputs. + +# Parameters: + + - `k1`: Gain for first input + - `k2`: Gain for second input + +# Connectors: + + - `input1` + - `input2` + - `output` +""" +@mtkmodel Add begin + @components begin + input1 = RealInput() + input2 = RealInput() + output = RealOutput() + end + @parameters begin + k1 = 1.0, [description = "Gain of Add input1"] + k2 = 1.0, [description = "Gain of Add input2"] + end + @equations begin + output.u ~ k1 * input1.u + k2 * input2.u + end +end + +""" + Add(; name, k1 = 1.0, k2 = 1.0, k3 = 1.0) + +Output the sum of the three scalar inputs. + +# Parameters: + + - `k1`: Gain for first input + - `k2`: Gain for second input + - `k3`: Gain for third input + +# Connectors: + + - `input1` + - `input2` + - `input3` + - `output` +""" +@mtkmodel Add3 begin + @components begin + input1 = RealInput() + input2 = RealInput() + input3 = RealInput() + output = RealOutput() + end + @parameters begin + k1 = 1.0, [description = "Gain of Add input1"] + k2 = 1.0, [description = "Gain of Add input2"] + k3 = 1.0, [description = "Gain of Add input3"] + end + @equations begin + output.u ~ k1 * input1.u + k2 * input2.u + k3 * input3.u + end +end + +""" + Product(; name) + +Output product of the two inputs. + +# Connectors: + + - `input1` + - `input2` + - `output` +""" +@mtkmodel Product begin + @components begin + input1 = RealInput() + input2 = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ input1.u * input2.u + end +end + +""" + Division(; name) + +Output first input divided by second input. + +# Connectors: + + - `input1` + - `input2` + - `output` +""" +@mtkmodel Division begin + @components begin + input1 = RealInput() + input2 = RealInput(guess = 1.0) # denominator can not be zero + output = RealOutput() + end + @equations begin + output.u ~ input1.u / input2.u + end +end + +""" + Power(; name) + +Output the exponential with base as the first input and exponent as second input i.e u1^u2 + +# Connectors: + + - `base` + - `exponent` + - `output` +""" +@mtkmodel Power begin + @components begin + base = RealInput() + exponent = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ base.u^exponent.u + end +end + +""" + Modulo(; name) + +Output the remainder when the first input is divided by second input. + +# Connectors: + + - `dividend` + - `divisor` + - `remainder` +""" +@mtkmodel Modulo begin + @components begin + dividend = RealInput() + divisor = RealInput(guess = 1.0) # denominator can not be zero + remainder = RealOutput() + end + @equations begin + remainder.u ~ mod(dividend.u, divisor.u) + end +end + +""" + UnaryMinus(; name) + +Output the product of -1 and the input. + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel UnaryMinus begin + @components begin + input = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ -(input.u) + end +end + +""" + Floor(; name) + +Output the floor rounding of the input. + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel Floor begin + @components begin + input = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ floor(input.u) + end +end + +""" + Ceil(; name) + +Output the ceiling rounding of the input. + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel Ceil begin + @components begin + input = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ ceil(input.u) + end +end + +""" + StaticNonLinearity(func; name) + +Applies the given function to the input. + +If the given function is not composed of simple core methods (e.g. sin, abs, ...), it has to be registered via `@register_symbolic func(u)` + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel StaticNonLinearity begin + @structural_parameters begin + func + end + @extend u, y = siso = SISO() + @equations begin + y ~ func(u) + end +end +StaticNonLinearity.f(func; name) = StaticNonLinearity(; func, name) + +""" + Abs(; name) + +Output the absolute value of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Abs(; name) = StaticNonLinearity(abs; name) + +""" + Sign(; name) + +Output the sign of the input + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Sign(; name) = StaticNonLinearity(sign; name) + +""" + Sqrt(; name) + +Output the square root of the input (input >= 0 required). + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Sqrt(; name) = StaticNonLinearity(sqrt; name) + +""" + Sin(; name) + +Output the sine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Sin(; name) = StaticNonLinearity(sin; name) + +""" + Cos(; name) + +Output the cosine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Cos(; name) = StaticNonLinearity(cos; name) + +""" + Tan(; name) + +Output the tangent of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Tan(; name) = StaticNonLinearity(tan; name) + +""" + Asin(; name) + +Output the arc sine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Asin(; name) = StaticNonLinearity(asin; name) + +""" + Acos(; name) + +Output the arc cosine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Acos(; name) = StaticNonLinearity(acos; name) + +""" + Atan(; name) + +Output the arc tangent of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Atan(; name) = StaticNonLinearity(atan; name) + +""" + Atan2(; name) + +Output the arc tangent of the input. + +# Connectors: + + - `input1` + - `input2` + - `output` +""" +@mtkmodel Atan2 begin + @components begin + input1 = RealInput() + input2 = RealInput() + output = RealOutput() + end + @equations begin + output.u ~ atan(input1.u, input2.u) + end +end + +""" + Sinh(; name) + +Output the hyperbolic sine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Sinh(; name) = StaticNonLinearity(sinh; name) + +""" + Cosh(; name) + +Output the hyperbolic cosine of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Cosh(; name) = StaticNonLinearity(cosh; name) + +""" + Tanh(; name) + +Output the hyperbolic tangent of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Tanh(; name) = StaticNonLinearity(tanh; name) + +""" + Exp(; name) + +Output the exponential (base e) of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Exp(; name) = StaticNonLinearity(exp; name) + +""" + Log(; name) + +Output the natural (base e) logarithm of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Log(; name) = StaticNonLinearity(log; name) + +""" + Log10(; name) + +Output the base 10 logarithm of the input. + +# Connectors: + +See [`StaticNonLinearity`](@ref) +""" +@component Log10(; name) = StaticNonLinearity(log10; name) diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/nonlinear.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/nonlinear.jl new file mode 100644 index 0000000000..b8350669d8 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/nonlinear.jl @@ -0,0 +1,122 @@ +_clamp(u, u_min, u_max) = max(min(u, u_max), u_min) +_dead_zone(u, u_min, u_max) = ifelse(u > u_max, u - u_max, ifelse(u < u_min, u - u_min, 0)) + +""" + Limiter(;name, y_max, y_min = y_max > 0 ? -y_max : -Inf) + +Limit the range of a signal. + +# Parameters: + + - `y_max`: Maximum of output signal + - `y_min`: Minimum of output signal + +# Connectors: + + - `input` + - `output` +""" +@component function Limiter(; name, y_max, y_min = y_max > 0 ? -y_max : -Inf) + @symcheck y_max ≥ y_min || throw(ArgumentError("`y_min` must be smaller than `y_max`")) + m = (y_max + y_min) / 2 + siso = SISO(u_start = m, y_start = m, name = :siso) # Default signals to center of saturation to minimize risk of saturation while linearizing etc. + @unpack u, y = siso + pars = @parameters y_max=y_max [description="Maximum allowed output of Limiter $name"] y_min=y_min [ + description="Minimum allowed output of Limiter $name" + ] + eqs = [ + y ~ _clamp(u, y_min, y_max) + ] + extend(System(eqs, t, [], pars; name = name), siso) +end + +""" + DeadZone(; name, u_max, u_min = -u_max) + +The DeadZone block defines a region of zero output. +If the input is within `u_min` ... `u_max`, the output is zero. Outside of this zone, the output is a linear function of the input with a slope of 1. + +``` + y▲ + │ / + │ / + u_min │ / +─────|──┼──|───────► u + / │ u_max + / │ + / │ +``` + +# Parameters: + + - `u_max`: Upper limit of dead zone + - `u_min`: Lower limit of dead zone + +# Connectors: + + - `input` + - `output` +""" +@mtkmodel DeadZone begin + @parameters begin + u_max, [description = "Upper limit of dead zone of DeadZone"] + u_min = -u_max, [description = "Lower limit of dead zone of DeadZone"] + end + begin + if !ModelingToolkit.isvariable(u_max) + u_max ≥ u_min || throw(ArgumentError("`u_min` must be smaller than `u_max`")) + end + end + + @extend u, y = siso = SISO() + + @equations begin + y ~ _dead_zone(u, u_min, u_max) + end +end + +""" + SlewRateLimiter(; name, y_start, rising = 1.0, falling = -rising, Td = 0.001) + +Limits the slew rate of a signal. +Initial value of state `Y` can be set with `int.y` + +# Parameters: + + - `rising`: Maximum rising slew rate + - `falling`: Maximum falling slew rate + - `Td`: [s] Derivative time constant + - `y_start`: Initial value of `y` state of SISO + +# Connectors: + + - `input` + - `output` +""" +@component function SlewRateLimiter(; + name, y_start = 0.0, rising = 1.0, falling = -rising, Td = 0.001) + pars = @parameters begin + rising = rising, [description = "Maximum rising slew rate of SlewRateLimiter"] + falling = falling, [description = "Derivative time constant of SlewRateLimiter"] + Td = Td, [description = "Derivative time constant"] + y_start = y_start + end + + getdefault(rising) ≥ getdefault(falling) || + throw(ArgumentError("`rising` must be smaller than `falling`")) + getdefault(Td) > 0 || + throw(ArgumentError("Time constant `Td` must be strictly positive")) + + @named siso = SISO(; y_start) + @unpack y, u = siso + + eqs = [ + D(y) ~ max(min((u - y) / Td, rising), falling) + ] + + initialization_eqs = [ + y ~ y_start + ] + + return extend(System(eqs, t, [], pars; name, initialization_eqs), siso) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/sources.jl new file mode 100644 index 0000000000..61a2e41161 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/sources.jl @@ -0,0 +1,876 @@ +using DiffEqBase +import ChainRulesCore +using PreallocationTools + +# Define and register smooth functions +# These are "smooth" aka differentiable and avoid Gibbs effect +# These follow: `offset` + `smooth_wave` * `smooth_step` with zero output for `t < start_time` +function smooth_cos(x, δ, f, amplitude, ϕ, offset, start_time) + offset + + amplitude * cos(2 * π * f * (x - start_time) + ϕ) * + smooth_step(x, δ, one(x), zero(x), start_time) +end + +function smooth_damped_sin(x, δ, f, amplitude, damping, ϕ, offset, start_time) + offset + + exp((start_time - x) * damping) * amplitude * sin(2 * π * f * (x - start_time) + ϕ) * + smooth_step(x, δ, one(x), zero(x), start_time) +end + +function smooth_ramp(x, δ, height, duration, offset, start_time) + offset + + height / (duration) * + (smooth_xH(x, δ, start_time) - smooth_xH(x, δ, start_time + duration)) +end + +function smooth_sin(x, δ, f, amplitude, ϕ, offset, start_time) + offset + + amplitude * sin(2 * pi * f * (x - start_time) + ϕ) * + smooth_step(x, δ, one(x), zero(x), start_time) +end + +function smooth_square(x, δ, f, amplitude, offset, start_time) + offset + + amplitude * 2atan(sin(2π * (x - start_time) * f) / δ) / π * + smooth_step(x, δ, one(x), zero(x), start_time) +end + +function smooth_step(x, δ, height, offset, start_time) + offset + height * (atan((x - start_time) / δ) / π + 0.5) +end + +function smooth_triangular(x, δ, f, amplitude, offset, start_time) + offset + + amplitude * (1 - 2acos((1 - δ)sin(2π * (x - start_time) * f)) / π) * + smooth_step(x, δ, one(x), zero(x), start_time) +end + +function smooth_xH(x, δ, tₒ) + 0.5 * (x - tₒ) * (1 + ((x - tₒ) / sqrt((x - tₒ)^2 + δ^2))) +end + +function square(x, f, amplitude, offset, start_time) + offset + + (x > start_time) * (amplitude * + (4 * floor(f * (x - start_time)) - 2 * floor(2 * (x - start_time) * f) + 1)) +end + +function triangular(x, f, amplitude, offset, start_time) + p = 1 / f # period + offset + + (x > start_time) * + (4 * amplitude * f * abs(abs((x - p / 4 - start_time) % p) - p / 2) - amplitude) +end + +""" + Constant(; name, k = 0.0) + +Generate constant signal. + +# Parameters: + + - `k`: Constant output value + +# Connectors: + + - `output` +""" +@mtkmodel Constant begin + @components begin + output = RealOutput() + end + @parameters begin + k = 0.0, [description = "Constant output value of block"] + end + @equations begin + output.u ~ k + end +end + +""" + TimeVaryingFunction(f; name) + +Outputs ``f(t)``. + +The input variable `t` can be changed by passing a different variable as the keyword argument `t`. + +# Connectors: +- `output` +""" +@mtkmodel TimeVaryingFunction begin + @structural_parameters begin + f + end + @components begin + output = RealOutput() + end + @equations begin + output.u ~ f(t) + end +end +TimeVaryingFunction.f(f; name) = TimeVaryingFunction(; f, name) + +""" + Sine(; name, frequency, amplitude = 1, phase = 0, offset = 0, start_time = 0, + smooth = false) + +Generate sine signal. + +# Parameters: + + - `frequency`: [Hz] Frequency of sine wave + - `amplitude`: Amplitude of sine wave + - `phase`: [rad] Phase of sine wave + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` + - `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function Sine(; name, + frequency, + amplitude = 1, + phase = 0, + offset = 0, + start_time = 0, + smooth = false) + @named output = RealOutput() + pars = @parameters offset=offset start_time=start_time amplitude=amplitude frequency=frequency phase=phase + equation = if smooth == false + offset + ifelse(t < start_time, 0, + amplitude * sin(2 * pi * frequency * (t - start_time) + phase)) + else + smooth === true && (smooth = 1e-5) + smooth_sin(t, smooth, frequency, amplitude, phase, offset, start_time) + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + Cosine(; name, frequency, amplitude = 1, phase = 0, offset = 0, start_time = 0, + smooth = false) + +Generate cosine signal. + +# Parameters: +- `frequency`: [Hz] Frequency of cosine wave +- `amplitude`: Amplitude of cosine wave +- `phase`: [rad] Phase of cosine wave +- `offset`: Offset of output signal +- `start_time`: [s] Output `y = offset` for `t < start_time` +- `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: +- `output` +""" +@component function Cosine(; name, + frequency, + amplitude = 1, + phase = 0, + offset = 0, + start_time = 0, + smooth = false) + @named output = RealOutput() + pars = @parameters offset=offset start_time=start_time amplitude=amplitude frequency=frequency phase=phase + equation = if smooth == false + offset + ifelse(t < start_time, zero(t), + amplitude * cos(2 * pi * frequency * (t - start_time) + phase)) + else + smooth === true && (smooth = 1e-5) + smooth_cos(t, smooth, frequency, amplitude, phase, offset, start_time) + end + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + ContinuousClock(; name, offset = 0, start_time = 0) + +Generate current time signal. + +# Parameters: + + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` + +# Connectors: + + - `output` +""" +@component function ContinuousClock(; name, offset = 0, start_time = 0) + @named output = RealOutput() + pars = @parameters offset=offset start_time=start_time + eqs = [ + output.u ~ offset + ifelse(t < start_time, zero(t), t - start_time) + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" +Ramp(; name, height = 1, duration = 1, offset = 0, start_time = 0, smooth = false) + +Generate ramp signal. + +# Parameters: + + - `height`: Height of ramp + - `duration`: [s] Duration of ramp (= 0.0 gives a Step) + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` + - `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function Ramp(; name, + height = 1.0, + duration = 1.0, + offset = 0.0, + start_time = 0.0, + smooth = false) + @named output = RealOutput() + pars = @parameters offset=offset start_time=start_time height=height duration=duration + equation = if smooth == false + offset + ifelse(t < start_time, zero(height), + ifelse(t < (start_time + duration), (t - start_time) * height / duration, + height)) + else + smooth === true && (smooth = 1e-5) + smooth_ramp(t, smooth, height, duration, offset, start_time) + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + Square(; name, frequency = 1.0, amplitude = 1.0, offset = 0.0, start_time = 0.0, + smooth = false) +Generate smooth square signal. + +# Parameters: + + - `frequency`: [Hz] Frequency of square wave + - `amplitude`: Amplitude of square wave + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` + - `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function Square(; name, frequency = 1.0, amplitude = 1.0, + offset = 0.0, start_time = 0.0, smooth = false) + @named output = RealOutput() + pars = @parameters begin + frequency = frequency + amplitude = amplitude + offset = offset + start_time = start_time + end + + equation = if smooth == false + square(t, frequency, amplitude, offset, start_time) + else + smooth === true && (smooth = 1e-5) + smooth_square(t, smooth, frequency, amplitude, offset, start_time) + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + Step(;name, height=1, offset=0, start_time=0, duration=Inf, smooth=true) + +Generate step signal. + +# Parameters: + + - `height`: Height of step + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` and thereafter `offset+height`. + - `duration`: [s] If `duration < Inf` is supplied, the output will revert to `offset` after `duration` seconds. + - `smooth`: If `true`, returns a smooth wave. Defaults to `true` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function Step(; + name, height = 1.0, offset = 0.0, start_time = 0.0, duration = Inf, + smooth = 1e-5) + @named output = RealOutput() + duration_numeric = duration + pars = @parameters offset=offset start_time=start_time height=height duration=duration + equation = if smooth == false # use comparison in case smooth is a float + offset + + ifelse((start_time <= t) & (t < start_time + duration), height, zero(height)) + else + smooth === true && (smooth = 1e-5) + if duration_numeric == Inf + smooth_step(t, smooth, height, offset, start_time) + else + smooth_step(t, smooth, height, offset, start_time) - + smooth_step(t, smooth, height, zero(start_time), start_time + duration) + end + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + ExpSine(; name, frequency, amplitude = 1, damping = 0.1, phase = 0, offset = 0, start_time = 0, smooth = false) + +Exponentially damped sine signal. + +# Parameters: + + - `frequency`: [Hz] Frequency of sine wave + - `amplitude`: Amplitude of sine wave + - `damping`: [1/s] Damping coefficient of sine wave + - `phase`: [rad] Phase of sine wave + - `offset`: Offset of output signal + - `start_time`: [s] Output `y = offset` for `t < start_time` + - `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function ExpSine(; name, + frequency, + amplitude = 1.0, + damping = 0.1, + phase = 0.0, + offset = 0.0, + start_time = 0.0, + smooth = false) + @named output = RealOutput() + pars = @parameters offset=offset start_time=start_time amplitude=amplitude frequency=frequency phase=phase damping=damping + + equation = if smooth == false + offset + ifelse(t < start_time, zero(amplitude), + amplitude * exp(-damping * (t - start_time)) * + sin(2 * pi * frequency * (t - start_time) + phase)) + else + smooth === true && (smooth = 1e-5) + smooth_damped_sin(t, smooth, frequency, amplitude, damping, phase, offset, + start_time) + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +""" + Triangular(; name, amplitude = 1.0, frequency = 1.0, offset = 0.0, + start_time = 0.0, smooth = false) + +Generate smooth triangular signal for frequencies less than or equal to 25 Hz + +# Parameters: + + - `frequency`: [Hz] Frequency of square wave + - `amplitude`: Amplitude of square wave + - `offset`: Offset of output signal. + - `start_time`: [s] Output `y = offset` for `t < start_time` + - `smooth`: If `true`, returns a smooth wave. Defaults to `false` + It uses a default smoothing factor of `δ=1e-5`, but this can be changed by supplying `smooth=δ`. + +# Connectors: + + - `output` +""" +@component function Triangular(; name, amplitude = 1.0, frequency = 1.0, + offset = 0.0, start_time = 0.0, smooth = false) + @named output = RealOutput() + pars = @parameters begin + amplitude = amplitude + frequency = frequency + offset = offset + start_time = start_time + end + + equation = if smooth == false + triangular(t, frequency, amplitude, offset, start_time) + else + smooth === true && (smooth = 1e-5) + smooth_triangular(t, smooth, frequency, amplitude, offset, start_time) + end + + eqs = [ + output.u ~ equation + ] + + compose(System(eqs, t, [], pars; name = name), [output]) +end + +# TODO: +# - Exponentials Generate a rising and falling exponential signal +# - Pulse Generate pulse signal of type Real +# - SawTooth Generate saw tooth signal +# - Trapezoid Generate trapezoidal signal of type Real + +# SampledData Parameter struct ---------------- + +struct Parameter{T <: Real} + data::Vector{T} + ref::T + circular_buffer::Bool +end + +Parameter(data::Vector{T}, ref::T) where {T <: Real} = Parameter(data, ref, true) +Parameter(x::Parameter) = x +function Parameter(x::T; tofloat = true) where {T <: Real} + if tofloat + x = float(x) + P = typeof(x) + else + P = T + end + + return Parameter(P[], x) +end + +function Base.isequal(x::Parameter, y::Parameter) + b0 = length(x.data) == length(y.data) + if b0 + b1 = all(x.data .== y.data) + b2 = x.ref == y.ref + return b1 & b2 + else + return false + end +end + +Base.:*(x::Number, y::Parameter) = x * y.ref +Base.:*(y::Parameter, x::Number) = Base.:*(x, y) +Base.:*(x::Parameter, y::Parameter) = x.ref * y.ref + +Base.:/(x::Number, y::Parameter) = x / y.ref +Base.:/(y::Parameter, x::Number) = y.ref / x +Base.:/(x::Parameter, y::Parameter) = x.ref / y.ref + +Base.:+(x::Number, y::Parameter) = x + y.ref +Base.:+(y::Parameter, x::Number) = Base.:+(x, y) +Base.:+(x::Parameter, y::Parameter) = x.ref + y.ref + +Base.:-(y::Parameter) = -y.ref +Base.:-(x::Number, y::Parameter) = x - y.ref +Base.:-(y::Parameter, x::Number) = y.ref - x +Base.:-(x::Parameter, y::Parameter) = x.ref - y.ref + +Base.:^(x::Number, y::Parameter) = Base.:^(x, y.ref) +Base.:^(y::Parameter, x::Number) = Base.:^(y.ref, x) +Base.:^(x::Parameter, y::Parameter) = Base.:^(x.ref, y.ref) + +Base.isless(x::Parameter, y::Number) = Base.isless(x.ref, y) +Base.isless(y::Number, x::Parameter) = Base.isless(y, x.ref) + +Base.copy(x::Parameter{T}) where {T} = Parameter{T}(copy(x.data), x.ref) + +ifelse(c::Bool, x::Parameter, y::Parameter) = ifelse(c, x.ref, y.ref) +ifelse(c::Bool, x::Parameter, y::Number) = ifelse(c, x.ref, y) +ifelse(c::Bool, x::Number, y::Parameter) = ifelse(c, x, y.ref) + +Base.max(x::Number, y::Parameter) = max(x, y.ref) +Base.max(x::Parameter, y::Number) = max(x.ref, y) +Base.max(x::Parameter, y::Parameter) = max(x.ref, y.ref) + +Base.min(x::Number, y::Parameter) = min(x, y.ref) +Base.min(x::Parameter, y::Number) = min(x.ref, y) +Base.min(x::Parameter, y::Parameter) = min(x.ref, y.ref) + +function Base.show(io::IO, m::MIME"text/plain", p::Parameter) + if !isempty(p.data) + print(io, p.data) + else + print(io, p.ref) + end +end + +get_sample_time(memory::Parameter) = memory.ref +Symbolics.@register_symbolic get_sample_time(memory::Parameter) + +Base.convert(::Type{T}, x::Parameter{T}) where {T <: Real} = x.ref +function Base.convert(::Type{<:Parameter{T}}, x::Number) where {T <: Real} + Parameter{T}(T[], x, true) +end + +# SampledData utilities ---------------- + +function linear_interpolation(x1::Real, x2::Real, t1::Real, t2::Real, t) + if t1 != t2 + slope = (x2 - x1) / (t2 - t1) + intercept = x1 - slope * t1 + + return slope * t + intercept + else + @assert x1==x2 "x1 ($x1) and x2 ($x2) should be equal if t1 == t2" + + return x2 + end +end + +function first_order_backwards_difference(t, memory) + Δt = get_sample_time(memory) + x1 = get_sampled_data(t, memory) + x0 = get_sampled_data(t - Δt, memory) + + return (x1 - x0) / Δt +end + +function first_order_backwards_difference(t, buffer, Δt, circular_buffer) + x1 = get_sampled_data(t, buffer, Δt, circular_buffer) + x0 = get_sampled_data(t - Δt, buffer, Δt, circular_buffer) + + return (x1 - x0) / Δt +end + +function get_sampled_data(t, + buffer::Vector{T}, + dt::T, + circular_buffer = true) where {T <: Real} + if t < 0 + t = zero(t) + end + + if isempty(buffer) + if T <: AbstractFloat + return T(NaN) + else + return zero(T) + end + end + + i1 = floor(Int, t / dt) + 1 #expensive + i2 = i1 + 1 + + t1 = (i1 - 1) * dt + x1 = @inbounds buffer[i1] + + if t == t1 + return x1 + else + n = length(buffer) + + if circular_buffer + i1 = (i1 - 1) % n + 1 + i2 = (i2 - 1) % n + 1 + else + if i2 > n + i2 = n + i1 = i2 - 1 + end + end + + t2 = (i2 - 1) * dt + x2 = @inbounds buffer[i2] + return linear_interpolation(x1, x2, t1, t2, t) + end +end +function get_sampled_data(t, buffer) + get_sampled_data(t, buffer.data, buffer.ref, buffer.circular_buffer) +end +Symbolics.@register_symbolic Parameter(data::Vector, ref, circular_buffer::Bool) +Symbolics.@register_symbolic get_sampled_data(t, buffer::Parameter) +Symbolics.@register_symbolic get_sampled_data(t, buffer::Vector, dt, circular_buffer) false + +function Symbolics.derivative(::typeof(get_sampled_data), args::NTuple{2, Any}, ::Val{1}) + t = @inbounds args[1] + buffer = @inbounds args[2] + first_order_backwards_difference(t, buffer) +end +function ChainRulesCore.frule((_, ẋ, _), ::typeof(get_sampled_data), t, buffer) + first_order_backwards_difference(t, buffer) * ẋ +end + +function Symbolics.derivative(::typeof(get_sampled_data), args::NTuple{4, Any}, ::Val{1}) + t = @inbounds args[1] + buffer = @inbounds args[2] + sample_time = @inbounds args[3] + circular_buffer = @inbounds args[4] + first_order_backwards_difference(t, buffer, sample_time, circular_buffer) +end +function ChainRulesCore.frule((_, ẋ, _), + ::typeof(get_sampled_data), + t, + buffer, + sample_time, + circular_buffer) + first_order_backwards_difference(t, buffer, sample_time, circular_buffer) * ẋ +end + +# SampledData component ---------------- + +module SampledDataType +@enum Option vector_based struct_based +end + +""" + SampledData(; name, buffer, sample_time, circular_buffer=true) + +data input component. + +# Parameters: + - `buffer::Vector{Real}`: holds the data sampled at `sample_time` + - `sample_time::Real` + - `circular_buffer::Bool = true`: how to handle `t > length(buffer)*sample_time`. If true data is considered circular, otherwise last data point is held. + +# Connectors: + - `output` +""" +@component function SampledData(::Val{SampledDataType.vector_based}; + name, + buffer, + sample_time, + circular_buffer = true) + T = eltype(buffer) + pars = @parameters begin + buffer::Vector{T} = buffer #::Vector{Real} + sample_time::T = sample_time #::Real + circular_buffer::Bool = circular_buffer #::Bool + end + @parameters p::Parameter{T} = Parameter(buffer, sample_time, circular_buffer) + vars = [] + systems = @named begin + output = RealOutput() + end + eqs = [ + output.u ~ get_sampled_data(t, p) + ] + return System(eqs, t, vars, [pars; p]; name, systems) +end + +""" + SampledData(; name, buffer) + +data input component. + +# Parameters: + - `buffer`: a `Parameter` type which holds the data and sample time + +# Connectors: + - `output` +""" +@component function SampledData( + ::Val{SampledDataType.struct_based}; name, buffer::Parameter) + pars = @parameters begin + buffer::typeof(buffer) = buffer #::Parameter + end + vars = [] + systems = @named begin + output = RealOutput() + end + eqs = [ + output.u ~ get_sampled_data(t, buffer) + ] + return System(eqs, t, vars, pars; name, systems) +end + +SampledData(x::SampledDataType.Option; kwargs...) = SampledData(Val(x); kwargs...) + +# struct_based +function SampledData(T::Type, circular_buffer = true; name) + SampledData(SampledDataType.struct_based; + name, + buffer = Parameter(T[], zero(T), circular_buffer)) +end + +# vector_based +function SampledData(sample_time::T, circular_buffer = true; name) where {T <: Real} + SampledData(SampledDataType.vector_based; + name, + buffer = T[], + sample_time, + circular_buffer) +end +function SampledData(buffer::Vector{<:Real}, + sample_time::Real, + circular_buffer = true; + name) + SampledData(SampledDataType.vector_based; name, buffer, sample_time, circular_buffer) +end +function SampledData(; name, buffer, sample_time, circular_buffer) + SampledData(SampledDataType.vector_based; name, buffer, sample_time, circular_buffer) +end + +""" + Interpolation(interp_type, u, x, args...; name) + +Represent function interpolation symbolically as a block component. +By default interpolation types from [`DataInterpolations.jl`](https://github.com/SciML/DataInterpolations.jl) are supported, +but in general any callable type that builds the interpolation object via `itp = interpolation_type(u, x, args...)` and calls +the interpolation with `itp(t)` should work. This does not need to represent an interpolation, it can be any type that satisfies +the interface, such as lookup tables. +# Arguments: + - `interp_type`: the type of the interpolation. For `DataInterpolations`, +these would be any of [the available interpolations](https://github.com/SciML/DataInterpolations.jl?tab=readme-ov-file#available-interpolations), +such as `LinearInterpolation`, `ConstantInterpolation` or `CubicSpline`. + - `u`: the data used for interpolation. For `DataInterpolations` this will be an `AbstractVector` + - `x`: the values that each data points correspond to, usually the times corresponding to each value in `u`. + - `args`: any other arguments needed to build the interpolation +# Keyword arguments: + - `name`: the name of the component + +# Parameters: + - `interpolator`: the symbolic representation of the interpolation object, callable as `interpolator(t)` + +# Connectors: + - `input`: a [`RealInput`](@ref) connector corresponding to the input variable + - `output`: a [`RealOutput`](@ref) connector corresponding to the interpolated value +""" +function Interpolation(interp_type, u, x, args...; name) + itp = interp_type(u, x, args...) + Interpolation(; itp, name) +end + +@deprecate Interpolation(itp; name) Interpolation(; itp, name) + +function Interpolation(; itp, name) + @parameters (interpolator::typeof(itp))(..) = itp + @named input = RealInput() + @named output = RealOutput() + + eqs = [output.u ~ interpolator(input.u)] + + System( + eqs, t, [], [interpolator]; name, systems = [input, output]) +end + +""" + CachedInterpolation + +This callable struct caches the calls to an interpolation object via PreallocationTools. +""" +struct CachedInterpolation{T, I, U, X, C} + interpolation_type::I + prev_u::U + prev_x::X + cache::C + + function CachedInterpolation(interpolation_type, u, x, args) + # we need to copy the inputs to avoid aliasing + prev_u = DiffCache(copy(u)) + # Interpolation points can be a range, but we want to be able + # to update the cache if needed (and setindex! is not defined on ranges) + # with a view from MTKParameters, so we collect to get a vector + prev_x = DiffCache(collect(copy(x))) + cache = GeneralLazyBufferCache() do (u, x) + interpolation_type(get_tmp(prev_u, u), get_tmp(prev_x, x), args...) + end + T = typeof(cache[(get_tmp(prev_u, u), get_tmp(prev_x, x))]) + I = typeof(interpolation_type) + U = typeof(prev_u) + X = typeof(prev_x) + C = typeof(cache) + + new{T, I, U, X, C}(interpolation_type, prev_u, prev_x, cache) + end +end + +function (f::CachedInterpolation{T})(u, x, args) where {T} + (; prev_u, prev_x, cache, interpolation_type) = f + + interp = @inbounds if (u, x) ≠ (get_tmp(prev_u, u), get_tmp(prev_x, x)) + get_tmp(prev_u, u) .= u + get_tmp(prev_x, x) .= x + cache.bufs[(u, x)] = interpolation_type( + get_tmp(prev_u, u), get_tmp(prev_x, x), args...) + else + cache[(u, x)] + end + + return interp +end + +Base.nameof(::CachedInterpolation) = :CachedInterpolation + +@register_symbolic (f::CachedInterpolation)(u::AbstractArray, x::AbstractArray, args::Tuple) + +""" + ParametrizedInterpolation(interp_type, u, x, args...; name, t = ModelingToolkit.t_nounits) + +Represent function interpolation symbolically as a block component, with the interpolation data represented parametrically. +By default interpolation types from [`DataInterpolations.jl`](https://github.com/SciML/DataInterpolations.jl) are supported, +but in general any callable type that builds the interpolation object via `itp = interpolation_type(u, x, args...)` and calls +the interpolation with `itp(t)` should work. This does not need to represent an interpolation, it can be any type that satisfies +the interface, such as lookup tables. +# Arguments: + - `interp_type`: the type of the interpolation. For `DataInterpolations`, +these would be any of [the available interpolations](https://github.com/SciML/DataInterpolations.jl?tab=readme-ov-file#available-interpolations), +such as `LinearInterpolation`, `ConstantInterpolation` or `CubicSpline`. + - `u`: the data used for interpolation. For `DataInterpolations` this will be an `AbstractVector` + - `x`: the values that each data points correspond to, usually the times corresponding to each value in `u`. + - `args`: any other arguments beeded to build the interpolation +# Keyword arguments: + - `name`: the name of the component + +# Parameters: + - `data`: the symbolic representation of the data passed at construction time via `u`. + - `ts`: the symbolic representation of times corresponding to the data passed at construction time via `x`. + +# Connectors: + - `input`: a [`RealInput`](@ref) connector corresponding to the independent variable + - `output`: a [`RealOutput`](@ref) connector corresponding to the interpolated value +""" +function ParametrizedInterpolation( + interp_type::T, u::AbstractVector, x::AbstractVector, args...; + name) where {T} + build_interpolation = CachedInterpolation(interp_type, u, x, args) + + @parameters data[1:length(x)] = u + @parameters ts[1:length(x)] = x + @parameters interpolation_type::T=interp_type [tunable = false] + @parameters (interpolator::interp_type)(..)::eltype(u) + + @named input = RealInput() + @named output = RealOutput() + + eqs = [output.u ~ interpolator(input.u)] + + System(eqs, ModelingToolkit.t_nounits, [], + [data, ts, interpolation_type, interpolator]; + parameter_dependencies = [ + interpolator ~ build_interpolation(data, ts, args) + ], + systems = [input, output], + name) +end + +function ParametrizedInterpolation(; interp_type, u::AbstractVector, x::AbstractVector, name) + ParametrizedInterpolation(interp_type, u, x; name) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Blocks/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Blocks/utils.jl new file mode 100644 index 0000000000..61cbb484e3 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Blocks/utils.jl @@ -0,0 +1,169 @@ +@connector function RealInput(; + name, nin = 1, u_start = nothing, guess = nin > 1 ? zeros(nin) : 0.0) + if u_start !== nothing + Base.depwarn( + "The keyword argument `u_start` is deprecated. Use `guess` instead.", :u_start) + guess = u_start + end + if nin == 1 + @variables u(t) [ + input = true, + description = "Inner variable in RealInput $name" + ] + else + @variables u(t)[1:nin] [ + input = true, + description = "Inner variable in RealInput $name" + ] + u = collect(u) + end + System(Equation[], t, [u;], []; name = name, guesses = [(u .=> guess);]) +end +@doc """ + RealInput(;name, guess) + +Connector with one input signal of type Real. + +# Parameters: +- `guess=0`: Guess value for `u`. + +# States: +- `u`: Value of the connector which is a scalar. +""" RealInput + +@connector function RealInputArray(; name, nin, u_start = nothing, guess = zeros(nin)) + if u_start !== nothing + Base.depwarn( + "The keyword argument `u_start` is deprecated. Use `guess` instead.", :u_start) + guess = u_start + end + @variables u(t)[1:nin] [ + input = true, + description = "Inner variable in RealInputArray $name" + ] + System(Equation[], t, [u], []; name = name, guesses = [u => guess]) +end +@doc """ + RealInputArray(;name, nin, guess) + +Connector with an array of input signals of type Real. + +# Parameters: +- `nin`: Number of inputs. +- `guess=zeros(nin)`: Guess value for `u`. + +# States: +- `u`: Value of the connector which is an array. +""" RealInputArray + +@connector function RealOutput(; + name, nout = 1, u_start = nothing, guess = nout > 1 ? zeros(nout) : 0.0) + if u_start !== nothing + Base.depwarn( + "The keyword argument `u_start` is deprecated. Use `guess` instead.", :u_start) + guess = u_start + end + if nout == 1 + @variables u(t) [ + output = true, + description = "Inner variable in RealOutput $name" + ] + else + @variables u(t)[1:nout] [ + output = true, + description = "Inner variable in RealOutput $name" + ] + u = collect(u) + end + System(Equation[], t, [u;], []; name = name, guesses = [(u .=> guess);]) +end +@doc """ + RealOutput(;name, guess) + +Connector with one output signal of type Real. + +# Parameters: +- `guess=0`: Guess value for `u`. + +# States: +- `u`: Value of the connector which is a scalar. +""" RealOutput + +@connector function RealOutputArray(; name, nout, u_start = nothing, guess = zeros(nout)) + if u_start !== nothing + Base.depwarn( + "The keyword argument `u_start` is deprecated. Use `guess` instead.", :u_start) + guess = u_start + end + @variables u(t)[1:nout] [ + output = true, + description = "Inner variable in RealOutputArray $name" + ] + System(Equation[], t, [u], []; name = name, guesses = [u => guess]) +end +@doc """ + RealOutputArray(;name, nout, guess) + +Connector with an array of output signals of type Real. + +# Parameters: +- `nout`: Number of outputs. +- `guess=zeros(nout)`: Guess value for `u`. + +# States: +- `u`: Value of the connector which is an array. +""" RealOutputArray + +""" + SISO(;name, u_start = 0.0, y_start = 0.0) + +Single input single output (SISO) continuous system block. + +# Parameters: + + - `u_start`: Initial value for the input + - `y_start`: Initial value for the output +""" +@mtkmodel SISO begin + @parameters begin + u_start = 0.0 + y_start = 0.0 + end + @variables begin + u(t), [guess = u_start, description = "Input of SISO system"] + y(t), [guess = y_start, description = "Output of SISO system"] + end + @components begin + input = RealInput(guess = u_start) + output = RealOutput(guess = y_start) + end + @equations begin + u ~ input.u + y ~ output.u + end +end + +""" + MIMO(; name, nin = 1, nout = 1, u_start = zeros(nin), y_start = zeros(nout)) + +Base class for a multiple input multiple output (MIMO) continuous system block. + +# Parameters: + + - `nin`: Input dimension + - `nout`: Output dimension + - `u_start`: Initial value for the input + - `y_start`: Initial value for the output +""" +@component function MIMO(; name, nin = 1, nout = 1, u_start = zeros(nin), + y_start = zeros(nout)) + @named input = RealInput(nin = nin, guess = u_start) + @named output = RealOutput(nout = nout, guess = y_start) + @variables(u(t)[1:nin]=u_start, [description="Input of MIMO system $name"], + y(t)[1:nout]=y_start, [description="Output of MIMO system $name"],) + eqs = [ + [u[i] ~ input.u[i] for i in 1:nin]..., + [y[i] ~ output.u[i] for i in 1:nout]... + ] + return System(eqs, t, vcat(u..., y...), []; name = name, systems = [input, output]) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/ideal_components.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/ideal_components.jl new file mode 100644 index 0000000000..46ee0358ca --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/ideal_components.jl @@ -0,0 +1,393 @@ +""" + Ground(; name) + +Ground node with the potential of zero and connector `g`. Every circuit must have one ground +node. + +# Connectors: + + - `g` +""" +@mtkmodel Ground begin + @components begin + g = Pin() + end + @equations begin + g.v ~ 0 + end +end + +""" + Resistor(; name, R = 1.0, T_ref = 300.15, alpha = 0, T_dep = false) + +Generic resistor with optional temperature dependency. + +# States: + + - See [OnePort](@ref) + - `R(t)`: [`Ω`] Resistance (temperature dependent if `T_dep = true`) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + - `heat_port` [HeatPort](@ref) (only if `T_dep = true`) Heat port to model the temperature dependency + +# Parameters: + + - `R`: [`Ω`] Reference resistance + - `T_ref`: [K] Reference temperature + - `alpha`: [K⁻¹] Temperature coefficient of resistance + - `T_dep`: [bool] Temperature dependency +""" +@mtkmodel Resistor begin + @extend v, i = oneport = OnePort() + + @structural_parameters begin + T_dep = false + end + + @parameters begin + R = 1.0, [description = "Reference resistance"] + T_ref = 300.15, [description = "Reference temperature"] + alpha = 0.0, [description = "Temperature coefficient of resistance"] + end + + if T_dep + @components begin + heat_port = HeatPort() + end + @variables begin + R_T(t), [description = "Temperature-dependent resistance"] + end + @equations begin + R_T ~ R * (1 + alpha * (heat_port.T - T_ref)) # Temperature-dependent resistance + heat_port.Q_flow ~ -v * i # -LossPower + v ~ i * R_T # Ohm's Law + end + else + @equations begin + v ~ i * R # Ohm's Law for constant resistance + end + end +end + +""" + Conductor(; name, G) + +Creates an ideal conductor. + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + +# Parameters: + + - `G`: [`S`] Conductance +""" +@mtkmodel Conductor begin + @extend v, i = oneport = OnePort() + @parameters begin + G, [description = "Conductance"] + end + @equations begin + i ~ v * G + end +end + +""" + Capacitor(; name, C, v) + +Creates an ideal capacitor. +Initial voltage of capacitor can be set with `v` ([`V`]) + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + +# Parameters: + + - `C`: [`F`] Capacitance +""" +@mtkmodel Capacitor begin + @parameters begin + C, [description = "Capacitance"] + end + @extend v, i = oneport = OnePort(; v) + @equations begin + D(v) ~ i / C + end +end + +""" + Inductor(; name, L, i) + +Creates an ideal Inductor. +Initial current through inductor can be set with `i` ([`A`]). + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + +# Parameters: + + - `L`: [`H`] Inductance +""" +@mtkmodel Inductor begin + @parameters begin + L, [description = "Inductance"] + end + @extend v, i = oneport = OnePort(; i) + @equations begin + D(i) ~ 1 / L * v + end +end + +""" + IdealOpAmp(; name) + +Ideal operational amplifier (norator-nullator pair). +The ideal OpAmp is a two-port. The left port is fixed to `v1 = 0` and `i1 = 0` (nullator). +At the right port both any voltage `v2` and any current `i2` are possible (norator). + +# States: + +See [TwoPort](@ref) + +# Connectors: + + - `p1` Positive pin (left port) + - `p2` Positive pin (right port) + - `n1` Negative pin (left port) + - `n2` Negative pin (right port) +""" +@mtkmodel IdealOpAmp begin + @extend v1, v2, i1, i2 = twoport = TwoPort() + @equations begin + v1 ~ 0 + i1 ~ 0 + end +end + +""" + Short(; name) + +Short is a simple short cut branch. That means the voltage drop between both pins is zero. + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin +""" +@mtkmodel Short begin + @extend v, i = oneport = OnePort() + @equations begin + v ~ 0 + end +end + +""" + EMF(; name, k) + +Electromotoric force (electric/mechanic transformer) + +# States + + - `v(t)`: [`V`] The voltage across component `p.v - n.v` + - `i(t)`: [`A`] The current passing through positive pin + - `phi`: [`rad`] Rotation angle (=flange.phi - support.phi) + - `w`: [`rad/s`] Angular velocity (= der(phi)) + +# Connectors + + - `p` [Pin](@ref) Positive pin + - `n` [Pin](@ref) Negative pin + - `flange` [Flange](@ref) Shaft of EMF shaft + - `support` [Support](@ref) Support/housing of emf shaft + +# Parameters: + + - `k`: [`N⋅m/A`] Transformation coefficient +""" +@mtkmodel EMF begin + @parameters begin + k, [description = "Transformation coefficient"] + end + @variables begin + phi(t), [guess = 0.0, description = "Rotation Angle"] + w(t), [guess = 0.0] + end + @extend v, i = oneport = OnePort() + @components begin + flange = Flange() + support = Support() + end + @equations begin + phi ~ flange.phi - support.phi + D(phi) ~ w + k * w ~ v + flange.tau ~ -k * i + end +end + +""" + Diode(; name, Is = 1e-6, n = 1, T = 300.15, T_dep = false) + +Generic diode with optional temperature dependency. + +# States + + - See [OnePort](@ref) + +# Connectors + + - `p` Positive pin + - `n` Negative pin + - `port` [HeatPort](@ref) (only if `T_dep = true`) Heat port to model variable temperature dependency + +# Parameters: + + - `Is`: [`A`] Saturation current + - `n`: Ideality factor + - `T`: [K] Constant ambient temperature - only used if T_dep=false + - `T_dep`: [bool] Temperature dependency +""" +@mtkmodel Diode begin + @constants begin + k = 1.380649e-23 # Boltzmann constant (J/K) + q = 1.602176634e-19 # Elementary charge (C) + end + @extend v, i = oneport = OnePort(; v = 0.0) + + @structural_parameters begin + T_dep = false + end + + @parameters begin + Is = 1e-6, [description = "Saturation current (A)"] + n = 1, [description = "Ideality factor"] + T = 300.15, [description = "Ambient temperature"] + end + + if T_dep + @components begin + port = HeatPort() + end + @variables begin + Vt(t), [description = "Thermal voltage"] + end + @equations begin + Vt ~ k * port.T / q # Thermal voltage equation + i ~ Is * (exp(v / (n * Vt)) - 1) # Shockley diode equation with temperature dependence + port.Q_flow ~ -v * i # -LossPower + end + else + @equations begin + i ~ Is * (exp(v * q / (n * k * T)) - 1) # Shockley diode equation + end + end +end + +""" + VariableResistor(; name, R_ref = 1.0, T_ref = 300.15, R_const = 1e-3, T_dep = false) + +Variable resistor with optional temperature dependency. + +The total resistance R ∈ [R_const, R_const + R_ref], where pos is the +position of the wiper and R_ref is the variable resistance between p and n. +The total resistance is then: + +R = R_const + pos * R_ref + +If T_dep is true, then R also depends on the temperature of the heat port with +temperature coefficient alpha. The total resistance is then: + +R = R_const + pos * R_ref * (1 + alpha * (port.T - T_ref)) + +# States + + - See [OnePort](@ref) + - `pos(t)`: Position of the wiper (normally 0-1) + - `R(t)`: Resistance + +# Connectors + + - `p` Positive pin + - `n` Negative pin + - `position` RealInput to set the position of the wiper + - `port` [HeatPort](@ref) Heat port to model the temperature dependency + +# Parameters + + - `R_ref`: [`Ω`] Resistance at temperature T_ref when fully closed (pos=1.0) + - `T_ref`: [K] Reference temperature + - `R_const`: [`Ω`] Constant resistance between p and n + - `T_dep`: Temperature dependency + - `alpha`: [K⁻¹] Temperature coefficient of resistance + - `enforce_bounds`: Enforce bounds for the position of the wiper (0-1) +""" +@mtkmodel VariableResistor begin + @extend v, i = oneport = OnePort() + + @structural_parameters begin + T_dep = false + enforce_bounds = true + end + + @parameters begin + R_ref = 1.0, + [description = "Resistance at temperature T_ref when fully closed (pos=1.0) (Ω)"] + T_ref = 300.15, [description = "Reference temperature (K)"] + R_const = 1e-3, [description = "Constant resistance between p and n (Ω)"] + end + + @components begin + position = RealInput() + end + + @variables begin + pos(t), [description = "Position of the wiper (normally 0-1)"] + R(t), [description = "Resistance (Ω)"] + end + + if T_dep + @parameters begin + alpha = 1e-3, [description = "Temperature coefficient of resistance (K^-1)"] + end + @components begin + port = HeatPort() + end + @equations begin + port.Q_flow ~ -v * i # -LossPower + R ~ R_const + pos * R_ref * (1 + alpha * (port.T - T_ref)) + end + else + @equations begin + R ~ R_const + pos * R_ref + end + end + + @equations begin + pos ~ (enforce_bounds ? clamp(position.u, 0, 1) : position.u) + v ~ i * R + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/mosfets.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/mosfets.jl new file mode 100644 index 0000000000..1b12a6afe7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/mosfets.jl @@ -0,0 +1,166 @@ +""" + NMOS(;name, V_tn, R_DS, lambda) + +Creates an N-type MOSFET transistor + + # Structural Parameters + - `use_transconductance`: If `true` the parameter `k_n` needs to be provided, and is used in the calculation of the current + through the transistor. Otherwise, `mu_n`, `C_ox`, `W`, and `L` need to be provided and are used to calculate the transconductance. + + - `use_channel_length_modulation`: If `true` the channel length modulation effect is taken in to account. In essence this gives + the drain-source current has a small dependency on the drains-source voltage in the saturation region of operation. + + # Connectors + - `d` Drain Pin + - `g` Gate Pin + - `s` Source Pin + + # Parameters + - `mu_n`: Electron mobility + - `C_ox`: Oxide capacitance (F/m^2) + - `W`: Channel width (m) + - `L`: Channel length + - `k_n`: MOSFET transconductance parameter + +Based on the MOSFET models in (Sedra, A. S., Smith, K. C., Carusone, T. C., & Gaudet, V. C. (2021). Microelectronic circuits (8th ed.). Oxford University Press.) +""" +@mtkmodel NMOS begin + @variables begin + V_GS(t) + V_DS(t) + V_OV(t) + end + + @components begin + d = Pin() + g = Pin() + s = Pin() + end + + @parameters begin + V_tn = 0.8, [description = "Threshold voltage (V)"] + R_DS = 1e7, [description = "Drain to source resistance (Ω)"] + + lambda = 0.04, [description = "Channel length modulation coefficient (V^(-1))"] + + if !use_transconductance + mu_n, [description = "Electron mobility"] + C_ox, [description = "Oxide capacitance (F/m^2)"] + W, [description = "Channel width (m)"] + L, [description = "Channel length (m)"] + else + k_n = 20e-3, [description = "MOSFET transconductance parameter"] + end + end + + @structural_parameters begin + use_transconductance = true + end + + begin + if !use_transconductance + k_n = mu_n * C_ox * (W / L) + end + end + + @equations begin + V_DS ~ ifelse(d.v < s.v, s.v - d.v, d.v - s.v) + V_GS ~ g.v - ifelse(d.v < s.v, d.v, s.v) + V_OV ~ V_GS - V_tn + + d.i ~ + ifelse(d.v < s.v, -1, 1) * ifelse(V_GS < V_tn, + V_DS / R_DS, + ifelse(V_DS < V_OV, + k_n * (1 + lambda * V_DS) * (V_OV - V_DS / 2) * V_DS + V_DS / R_DS, + ((k_n * V_OV^2) / 2) * (1 + lambda * V_DS) + V_DS / R_DS + ) + ) + + g.i ~ 0 + s.i ~ -d.i + end +end + +""" + PMOS(;name, V_tp, R_DS, lambda) + +Creates an N-type MOSFET transistor + + # Structural Parameters + - `use_transconductance`: If `true` the parameter `k_p` needs to be provided, and is used in the calculation of the current + through the transistor. Otherwise, `mu_n`, `C_ox`, `W`, and `L` need to be provided and are used to calculate the transconductance. + + - `use_channel_length_modulation`: If `true` the channel length modulation effect is taken in to account. In essence this gives + the drain-source current has a small dependency on the drains-source voltage in the saturation region of operation. + + # Connectors + - `d` Drain Pin + - `g` Gate Pin + - `s` Source Pin + + # Parameters + - `mu_p`: Electron mobility + - `C_ox`: Oxide capacitance (F/m^2) + - `W`: Channel width (m) + - `L`: Channel length + - `k_p`: MOSFET transconductance parameter + +Based on the MOSFET models in (Sedra, A. S., Smith, K. C., Carusone, T. C., & Gaudet, V. C. (2021). Microelectronic circuits (8th ed.). Oxford University Press.) +""" +@mtkmodel PMOS begin + @variables begin + V_GS(t) + V_DS(t) + end + + @components begin + d = Pin() + g = Pin() + s = Pin() + end + + @parameters begin + V_tp = -1.5, [description = "Threshold voltage (V)"] + R_DS = 1e7, [description = "Drain-source resistance (Ω)"] + + lambda = 1 / 25, [description = "Channel length modulation coefficient (V^(-1))"] + + if !use_transconductance + mu_p, [description = "Hole mobility"] + C_ox, [description = "Oxide capacitance (F/m^2)"] + W, [description = "Channel width (m)"] + L, [description = "Channel length (m)"] + else + k_p = 20e-3 + end + end + + @structural_parameters begin + use_transconductance = true + end + + begin + if !use_transconductance + k_p = mu_p * C_ox * (W / L) + end + end + + @equations begin + V_DS ~ ifelse(d.v > s.v, s.v - d.v, d.v - s.v) + V_GS ~ g.v - ifelse(d.v > s.v, d.v, s.v) + + d.i ~ + -ifelse(d.v > s.v, -1.0, 1.0) * ifelse(V_GS > V_tp, + V_DS / R_DS, + ifelse(V_DS > (V_GS - V_tp), + k_p * (1 + lambda * V_DS) * ((V_GS - V_tp) - V_DS / 2) * V_DS + + V_DS / R_DS, + ((k_p * (V_GS - V_tp)^2) / 2) * (1 + lambda * V_DS) + V_DS / R_DS + ) + ) + + g.i ~ 0 + s.i ~ -d.i + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sensors.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sensors.jl new file mode 100644 index 0000000000..5c6e7f16db --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sensors.jl @@ -0,0 +1,164 @@ +""" + CurrentSensor(; name) + +Creates a circuit component that measures the current flowing through it. Analogous to +an ideal ammeter. + +# States: + + - `i(t)`: [`A`] Current through the sensor + +# Connectors: + + - `p` Positive pin + - `n` Negative pin +""" +@mtkmodel CurrentSensor begin + @components begin + p = Pin() + n = Pin() + end + @variables begin + i(t) + end + @equations begin + p.v ~ n.v + i ~ p.i + i ~ -n.i + end +end + +""" +PotentialSensor(; name) + +Creates a circuit component which measures the potential at a pin. + +# States: + + - `phi(t)`: [`V`] The measured potential at this point + +# Connectors: + + - `p` Pin at which potential is to be measured +""" +@mtkmodel PotentialSensor begin + @components begin + p = Pin() + end + @variables begin + phi(t) + end + @equations begin + p.i ~ 0 + phi ~ p.v + end +end + +""" +VoltageSensor(; name) + +Creates a circuit component that measures the voltage across it. Analogous to an ideal voltmeter. + +# States: + + - `v(t)`: [`V`] The voltage difference from positive to negative pin `p.v - n.v` + +# Connectors: + + - `p` Positive pin + - `n` Negative pin +""" +@mtkmodel VoltageSensor begin + @components begin + p = Pin() + n = Pin() + end + @variables begin + v(t) + end + @equations begin + p.i ~ 0 + n.i ~ 0 + v ~ p.v - n.v + end +end + +""" +PowerSensor(; name) + +Combines a [`VoltageSensor`](@ref) and a [`CurrentSensor`](@ref) to measure the power being +consumed by a circuit. + +# States: + + - `power(t)`: [`W`] The power being consumed, given by the product of voltage and current + - See [VoltageSensor](@ref) + - See [CurrentSensor](@ref) + +# Connectors: + + - `pc` Corresponds to the `p` pin of the [`CurrentSensor`](@ref) + - `nc` Corresponds to the `n` pin of the [`CurrentSensor`](@ref) + - `pv` Corresponds to the `p` pin of the [`VoltageSensor`](@ref) + - `nv` Corresponds to the `n` pin of the [`VoltageSensor`](@ref) +""" +@mtkmodel PowerSensor begin + @components begin + pc = Pin() + nc = Pin() + pv = Pin() + nv = Pin() + voltage_sensor = VoltageSensor() + current_sensor = CurrentSensor() + end + @variables begin + power(t) + end + @equations begin + connect(voltage_sensor.p, pv) + connect(voltage_sensor.n, nv) + connect(current_sensor.p, pc) + connect(current_sensor.n, nc) + power ~ current_sensor.i * voltage_sensor.v + end +end + +""" +MultiSensor(; name) + +Combines a [`VoltageSensor`](@ref) and a [`CurrentSensor`](@ref). + +# States: + + - `v(t)`: [`V`] The voltage across the [`VoltageSensor`](@ref). Defaults to 1.0. + - `i(t)`: [`A`] The current across the [`CurrentSensor`](@ref). Defaults to 1.0. + +# Connectors: + + - `pc` Corresponds to the `p` pin of the [`CurrentSensor`](@ref) + - `nc` Corresponds to the `n` pin of the [`CurrentSensor`](@ref) + - `pv` Corresponds to the `p` pin of the [`VoltageSensor`](@ref) + - `nv` Corresponds to the `n` pin of the [`VoltageSensor`](@ref) +""" +@mtkmodel MultiSensor begin + @components begin + pc = Pin() + nc = Pin() + pv = Pin() + nv = Pin() + voltage_sensor = VoltageSensor() + current_sensor = CurrentSensor() + end + @variables begin + i(t) = 1.0 + v(t) = 1.0 + end + @equations begin + connect(voltage_sensor.p, pv) + connect(voltage_sensor.n, nv) + connect(current_sensor.p, pc) + connect(current_sensor.n, nc) + i ~ current_sensor.i + v ~ voltage_sensor.v + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sources.jl new file mode 100644 index 0000000000..807fa91f11 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/sources.jl @@ -0,0 +1,49 @@ +""" + Voltage(; name) + +Acts as an ideal voltage source with no internal resistance. + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + - `V` [RealInput](@ref) Input for the voltage control signal, i.e. `V ~ p.v - n.v` +""" +@mtkmodel Voltage begin + @extend v, i = oneport = OnePort() + @components begin + V = RealInput() + end + @equations begin + v ~ V.u + end +end + +""" + Current(; name) + +Acts as an ideal current source with no internal resistance. + +# States: + +See [OnePort](@ref) + +# Connectors: + + - `p` Positive pin + - `n` Negative pin + - `I` [RealInput](@ref) Input for the current control signal, i.e. `I ~ p.i +""" +@mtkmodel Current begin + @extend v, i = oneport = OnePort() + @components begin + I = RealInput() + end + @equations begin + i ~ I.u + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/transistors.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/transistors.jl new file mode 100644 index 0000000000..3dad46c456 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Analog/transistors.jl @@ -0,0 +1,333 @@ +""" + NPN(;name, B_F, B_R, Is, V_T, V_A, phi_C, phi_E, Z_C, Z_E, Tau_f, Tau_r, C_jC0, C_jE0, C_CS, gamma_C, gamma_E, NF, NR) + +Creates an NPN Bipolar Junction Transistor following a modified Ebers-Moll model. Includes an optional substrate pin and optional +Early voltage effect. + + # Structural Parameters + - `use_substrate`: If `true`, a substrate pin connector is available. If `false` it is + assumed the substrate is connected to the collector pin. + + - `use_Early`: If `true`, the Early effect is modeled, which takes in to account the effect + collector-base voltage variations have on the collector-base depletion region. In many cases this + effectively means that the collector current has a dependency on the collector-emitter voltage. + + - `use_advanced_continuation`: When false, the `C_jC` and `C_jE` non-linear capacitance curves use + a simplified linear continuation starting when `V_BC` and `V_BE` are 0, respectively. If `true`, the `Z_C` and `Z_E` parameters + are used to start the linear continuation at `Phi_C - Z_C` and `Phi_E - Z_E`. + + # Connectors + - `b` Base Pin + - `c` Collector Pin + - `e` Emitter Pin + - `s` Substrate Pin, only available when `use_substrate = true` + + # Parameters + - `B_F`: Forward beta + - `B_R`: Reverse beta + - `Is`: Saturation current + - `V_T`: Thermal voltage at 300K + - `V_A`: Inverse Early voltage + - `phi_C`: Collector junction exponent + - `phi_E`: Emitter junction exponent + - `Z_C`: Collector junction offset + - `Z_E`: Emitter junction offset + - `Tau_f`: Forward transit time + - `Tau_r`: Reverse transit time + - `C_jC0`: Collector junction capacitance coefficient + - `C_jE0`: Emitter junction capacitance coefficient + - `C_CS`: Collector-substrate capacitance + - `gamma_C`: Collector junction exponent + - `gamma_E`: Emitter junction exponent + - `NF`: Forward emission coefficient + - `NR`: Reverse emission coefficient +""" +@mtkmodel NPN begin + @variables begin + V_BE(t) + V_BC(t) + ICC(t) + IEC(t) + + C_jC(t) + C_jE(t) + C_DC(t) + C_DE(t) + + I_sub(t) + V_sub(t) + V_CS(t) + end + + @structural_parameters begin + use_substrate = false + use_Early = true + use_advanced_continuation = false + end + + @components begin + b = Pin() + e = Pin() + c = Pin() + + if use_substrate + s = Pin() + end + end + + @parameters begin + B_F = 50.0, [description = "Forward beta"] + B_R = 0.1, [description = "Reverse beta"] + Is = 1e-16, [description = "Saturation current"] + V_T = 0.026, [description = "Thermal voltage at 300K"] + + if use_Early + V_A = 0.02, [description = "Inverse Early voltage"] + end + + phi_C = 0.8, [description = "Collector junction scaling factor"] + phi_E = 0.6, [description = "Emitter junction scaling factor"] + + if use_advanced_continuation + Z_C = 0.1, [description = "Collector junction offset"] + Z_E = 0.1, [description = "Emitter junction offset"] + end + + Tau_f = 0.12e-9, [description = "Forward transit time"] + Tau_r = 5e-9, [description = "Reverse transit time"] + + C_jC0 = 0.5e-12, [description = "Collector-junction capacitance coefficient"] + C_jE0 = 0.4e-12, [description = "Emitter-junction capacitance coefficient"] + + C_CS = 1e-12, [description = "Collector-substrate capacitance"] + + gamma_C = 0.5, [description = "Collector junction exponent"] + gamma_E = 1.0 / 3.0, [description = "Emitter junction exponent"] + + NF = 1.0, [description = "Forward ideality exponent"] + NR = 1.0, [description = "Reverse ideality exponent"] + end + + @equations begin + V_BE ~ b.v - e.v + V_BC ~ b.v - c.v + + ICC ~ Is * (exp(V_BE / V_T) - 1) + IEC ~ Is * (exp(V_BC / V_T) - 1) + + if !use_advanced_continuation + C_jC ~ ifelse(V_BC / phi_C > 0.0, 1 + gamma_C * V_BC / phi_C, + (C_jC0) / (1 - V_BC / phi_C)^gamma_C) + C_jE ~ ifelse(V_BE / phi_E > 0.0, 1 + gamma_E * V_BE / phi_E, + (C_jE0) / (1 - V_BE / phi_E)^gamma_E) + end + + if use_advanced_continuation + C_jC ~ if V_BC > phi_C - Z_C + ((C_jC0 * gamma_C * (1 - ((phi_C - Z_C) / phi_C))^(-gamma_C - 1)) / phi_C) * + V_BC - + ((C_jC0 * gamma_C * (1 - ((phi_C - Z_C) / phi_C))^(-gamma_C - 1)) / phi_C) * + (phi_C - Z_C) + (C_jC0) / (1 - (phi_C - Z_C) / phi_C)^gamma_C + else + (C_jC0) / (1 - V_BC / phi_C)^gamma_C + end + + C_jE ~ if V_BE > phi_E - Z_E + ((C_jE0 * gamma_E * (1 - ((phi_E - Z_E) / phi_E))^(-gamma_E - 1)) / phi_E) * + V_BE - + ((C_jE0 * gamma_E * (1 - ((phi_E - Z_E) / phi_E))^(-gamma_E - 1)) / phi_E) * + (phi_E - Z_E) + (C_jE0) / (1 - (phi_E - Z_E) / phi_E)^gamma_E + else + (C_jE0) / (1 - V_BE / phi_E)^gamma_E + end + end + + C_DE ~ Tau_f * (Is / (NF * V_T)) * exp(V_BE / (NF * V_T)) + C_DC ~ Tau_r * (Is / (NR * V_T)) * exp(V_BC / (NR * V_T)) + + if use_substrate + s.i ~ I_sub + s.v ~ V_sub + V_CS ~ c.v - V_sub + end + + if !use_substrate + V_sub ~ c.v + end + + I_sub ~ ifelse(use_substrate, -C_CS * D(V_CS), -C_CS * D(V_sub)) + + c.i ~ + (ICC - IEC) * ifelse(use_Early, (1 - V_BC * V_A), 1.0) - IEC / B_R - + (C_jC + C_DC) * D(V_BC) - I_sub + b.i ~ IEC / B_R + ICC / B_F + (C_jC + C_DC) * D(V_BC) + (C_jE + C_DE) * D(V_BE) + e.i ~ -c.i - b.i - I_sub + end +end + +""" + PNP(;name, B_F, B_R, Is, V_T, V_A, phi_C, phi_E, Z_C, Z_E, Tau_f, Tau_r, C_jC0, C_jE0, C_CS, gamma_C, gamma_E, NF, NR) + +Creates a PNP Bipolar Junction Transistor following a modified Ebers-Moll model. Includes an optional substrate pin and optional +Early voltage effect. + + # Structural Parameters + - `use_substrate`: If `true`, a substrate pin connector is available. If `false` it is + assumed the substrate is connected to the collector pin. + + - `use_Early`: If `true`, the Early effect is modeled, which takes in to account the effect + collector-base voltage variations have on the collector-base depletion region. In many cases this + effectively means that the collector current has a dependency on the collector-emitter voltage. + + - `use_advanced_continuation`: When false, the `C_jC` and `C_jE` non-linear capacitance curves use + a simplified linear continuation starting when `V_CB` and `V_EB` are 0, respectively. If `true`, the `Z_C` and `Z_E` parameters + are used to start the linear continuation at `Phi_C - Z_C` and `Phi_E - Z_E`. + + # Connectors + - `b` Base Pin + - `c` Collector Pin + - `e` Emitter Pin + - `s` Substrate Pin, only available when `use_substrate = true` + + # Parameters + - `B_F`: Forward beta + - `B_R`: Reverse beta + - `Is`: Saturation current + - `V_T`: Thermal voltage at 300K + - `V_A`: Inverse Early voltage + - `phi_C`: Collector junction exponent + - `phi_E`: Emitter junction exponent + - `Z_C`: Collector junction offset + - `Z_E`: Emitter junction offset + - `Tau_f`: Forward transit time + - `Tau_r`: Reverse transit time + - `C_jC0`: Collector junction capacitance coefficient + - `C_jE0`: Emitter junction capacitance coefficient + - `C_CS`: Collector-substrate capacitance + - `gamma_C`: Collector junction exponent + - `gamma_E`: Emitter junction exponent + - `NF`: Forward emission coefficient + - `NR`: Reverse emission coefficient +""" +@mtkmodel PNP begin + @variables begin + V_EB(t) + V_CB(t) + ICC(t) + IEC(t) + + C_jC(t) + C_jE(t) + C_DC(t) + C_DE(t) + + I_sub(t) + V_sub(t) + V_CS(t) + end + + @structural_parameters begin + use_substrate = false + use_Early = true + use_advanced_continuation = false + end + + @components begin + b = Pin() + e = Pin() + c = Pin() + + if use_substrate + s = Pin() + end + end + + @parameters begin + B_F = 50.0, [description = "Forward beta"] + B_R = 0.1, [description = "Reverse beta"] + Is = 1e-16, [description = "Saturation current"] + V_T = 0.026, [description = "Thermal voltage at 300K"] + + if use_Early + V_A = 0.02, [description = "Inverse Early voltage"] + end + + phi_C = 0.8, [description = "Collector junction scaling factor"] + phi_E = 0.6, [description = "Emitter junction scaling factor"] + + if use_advanced_continuation + Z_C = 0.1, [description = "Collector junction offset"] + Z_E = 0.1, [description = "Emitter junction offset"] + end + + Tau_f = 0.12e-9, [description = "Forward transit time"] + Tau_r = 5e-9, [description = "Reverse transit time"] + + C_jC0 = 0.5e-12, [description = "Collector-junction capacitance coefficient"] + C_jE0 = 0.4e-12, [description = "Emitter-junction capacitance coefficient"] + + C_CS = 1e-12, [description = "Collector-substrate capacitance"] + + gamma_C = 0.5, [description = "Collector junction exponent"] + gamma_E = 1.0 / 3.0, [description = "Emitter junction exponent"] + + NF = 1.0, [description = "Forward ideality exponent"] + NR = 1.0, [description = "Reverse ideality exponent"] + end + + @equations begin + V_EB ~ e.v - b.v + V_CB ~ c.v - b.v + + ICC ~ Is * (exp(V_EB / V_T) - 1) + IEC ~ Is * (exp(V_CB / V_T) - 1) + + if !use_advanced_continuation + C_jC ~ ifelse(V_CB / phi_C > 0.0, 1 + gamma_C * V_CB / phi_C, + (C_jC0) / (1 - V_CB / phi_C)^gamma_C) + C_jE ~ ifelse(V_EB / phi_E > 0.0, 1 + gamma_E * V_EB / phi_E, + (C_jE0) / (1 - V_EB / phi_E)^gamma_E) + end + + if use_advanced_continuation + C_jC ~ if V_CB > phi_C - Z_C + ((C_jC0 * gamma_C * (1 - ((phi_C - Z_C) / phi_C))^(-gamma_C - 1)) / phi_C) * + V_CB - + ((C_jC0 * gamma_C * (1 - ((phi_C - Z_C) / phi_C))^(-gamma_C - 1)) / phi_C) * + (phi_C - Z_C) + (C_jC0) / (1 - (phi_C - Z_C) / phi_C)^gamma_C + else + (C_jC0) / (1 - V_CB / phi_C)^gamma_C + end + + C_jE ~ if V_EB > phi_E - Z_E + ((C_jE0 * gamma_E * (1 - ((phi_E - Z_E) / phi_E))^(-gamma_E - 1)) / phi_E) * + V_EB - + ((C_jE0 * gamma_E * (1 - ((phi_E - Z_E) / phi_E))^(-gamma_E - 1)) / phi_E) * + (phi_E - Z_E) + (C_jE0) / (1 - (phi_E - Z_E) / phi_E)^gamma_E + else + (C_jE0) / (1 - V_EB / phi_E)^gamma_E + end + end + + C_DE ~ Tau_f * (Is / (NF * V_T)) * exp(V_EB / (NF * V_T)) + C_DC ~ Tau_r * (Is / (NR * V_T)) * exp(V_CB / (NR * V_T)) + + if use_substrate + s.i ~ I_sub + s.v ~ V_sub + V_CS ~ c.v - V_sub + end + + if !use_substrate + V_sub ~ c.v + end + + I_sub ~ ifelse(use_substrate, -C_CS * D(V_CS), -C_CS * D(V_sub)) + + c.i ~ + IEC / B_R - (ICC - IEC) * ifelse(use_Early, (1 - V_CB * V_A), 1.0) + + (C_jC + C_DC) * D(V_CB) - I_sub + b.i ~ -IEC / B_R - ICC / B_F - (C_jC + C_DC) * D(V_CB) - (C_jE + C_DE) * D(V_EB) + e.i ~ -c.i - b.i - I_sub + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/components.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/components.jl new file mode 100644 index 0000000000..023a0d0c9d --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/components.jl @@ -0,0 +1,285 @@ +# Adders +""" +```julia +HalfAdder(; name) +``` + +Takes two bits as input, and outputs the sum and the carry + +# States + + - `sum(t)` + The sum of the input bits + - `carry(t)` + The carry generated by the input bits + +# Connectors + + - `x1`, `x2` + The two inputs to add + - `y1` + Output [`DigitalPin`](@ref) corresponding to the sum + - `y2` + Output [`DigitalPin`](@ref) corresponding to the carry +""" +@component function HalfAdder(; name) + @named x1 = DigitalPin() + @named x2 = DigitalPin() + @named y1 = DigitalPin() + @named y2 = DigitalPin() + @variables sum(t), carry(t) + + eqs = [y1.val ~ _xor(x1.val, x2.val) + y2.val ~ _and(x1.val, x2.val) + sum ~ y1.val + carry ~ y2.val] + System(eqs, t, [sum, carry], [], systems = [x1, x2, y1, y2], name = name) +end + +""" +```julia +FullAdder(; name) +``` + +Takes three bits as input, and outputs the sum and the carry + +# States + + - `sum(t)` + The sum of the input bits + - `carry(t)` + The carry generated by the input bits + +# Connectors + + - `x1`, `x2`, `x3` + The three inputs to add + - `y1` + Output [`DigitalPin`](@ref) corresponding to the sum + - `y2` + Output [`DigitalPin`](@ref) corresponding to the carry +""" +@component function FullAdder(; name) + @named x1 = DigitalPin() + @named x2 = DigitalPin() + @named x3 = DigitalPin() + @named y1 = DigitalPin() + @named y2 = DigitalPin() + @variables sum(t), carry(t) + + eqs = [y1.val ~ _xor(x1.val, x2.val, x3.val) + y2.val ~ _or(_and(x3.val, _xor(x1.val, x2.val)), _and(x1.val, x2.val)) + sum ~ y1.val + carry ~ y2.val] + System(eqs, t, [sum, carry], [], systems = [x1, x2, x3, y1, y2], name = name) +end + +# Multiplexers + +# This selects data from the `N` input ports (`d₀` to `dₙ₋₁`) +# using values of `n` select lines, where `N = 2^n` +""" +```julia +MUX(; name, N = 4) +``` + +Standard Multiplexer. Selects data from `N` input ports using the values +of `n` select lines, where `N=2ⁿ`. For the `i`th input port to be selected, +the values of the select lines should correspond to the binary representation +of `i`. + +# Connectors + + - `d1`, `d2`, ... + The `N` input lines + - `s1`, `s2`, ... + The `n` select lines + - `y` + The output, selected from one of the `N` input lines +""" +@component function MUX(; name, N = 4) + n = log2(N) + try + n = Int(n) + catch + (e) + throw("`N` must be a power of 2") + end + s = map(0:(n - 1)) do i + DigitalPin(; name = Symbol(:s, i)) + end + d = map(0:(N - 1)) do i + DigitalPin(; name = Symbol(:d, i)) + end + @named y = DigitalPin() + + nodes = Num[] + for i in 1:N + bin = digits!(zeros(Int64, n), i - 1, base = 2) + statelist = Term{Real, Nothing}[] + for j in 1:n + varstate = bin[j] == 0 ? _not(s[j].val) : s[j].val + push!(statelist, varstate) + end + push!(nodes, _and(statelist..., d[i].val)) + end + + eqs = Equation[y.val ~ _or(nodes...)] + + System(eqs, t, [], [], systems = [d..., s..., y], name = name) +end + +# This selects one of the `N` output ports (`y₀` to `yₙ₋₁`) +# to transmit data `d` using values of `n` select lines, where `N = 2^n` +""" +```julia +DEMUX(; name, N = 4) +``` + +Standard Demultiplexer. Performs the reverse operation of a [`MUX`](@ref). +Selects one of the `N` output ports to transmit the input `d` using the +values of `n` select lines, where `N=2ⁿ`. For the `i`th output port to be +selected, the values of the select lines should correspond to the binary +representation of `i`. + +# Connectors + + - `d` + The input to be transmitted to one of the output lines + - `s1`, `s2`, ... + The `n` select lines + - `y1`, `y2`, ... + The `N` output lines +""" +@component function DEMUX(; name, N = 4) + n = log2(N) + try + n = Int(n) + catch + (e) + throw("`N` must be a power of 2") + end + @named d = DigitalPin() + s = map(0:(n - 1)) do i + DigitalPin(; name = Symbol(:s, i)) + end + y = map(0:(N - 1)) do i + DigitalPin(; name = Symbol(:y, i)) + end + + eqs = Equation[] + for i in 1:N + bin = digits!(zeros(Int64, n), i - 1, base = 2) + statelist = Term{Real, Nothing}[] + for j in 1:n + varstate = bin[j] == 0 ? _not(s[j].val) : s[j].val + push!(statelist, varstate) + end + push!(eqs, y[i].val ~ _and(statelist..., d.val)) + end + + System(eqs, t, [], [], systems = [d, s..., y...], name = name) +end + +# Encoder-Decoder + +# Encodes `N` inputs to `n` outputs, where `N = 2^n` +""" +```julia +Encoder(; name, N = 4) +``` + +Encodes `N` inputs to `n` outputs, where `N=2ⁿ`. Exactly one of the inputs should be `1`. +If the `i`th input is `1`, then the output corresponds to the binary representation of `i`. + +# Connectors + + - `d1`, `d2`, ... + The `N` input lines + - `y1`, `y2`, ... + The `n` output lines +""" +@component function Encoder(; name, N = 4) + n = log2(N) + try + n = Int(n) + catch + (e) + throw("`N` must be a power of 2") + end + d = map(0:(N - 1)) do i + DigitalPin(; name = Symbol(:d, i)) + end + y = map(0:(n - 1)) do i + DigitalPin(; name = Symbol(:y, i)) + end + + nodes = Vector{Term{Real, Nothing}}[] + i = 0 + for j in 1:n + counter = 1 + statelist = Term{Real, Nothing}[] + while i < N + while counter <= 2^j + counter > 2^(j - 1) && push!(statelist, d[i + 1].val) + counter += 1 + i = i + 1 + end + counter = 1 + end + i = 0 + push!(nodes, statelist) + end + + eqs = Equation[] + for i in n:-1:1 + push!(eqs, y[i].val ~ _or(nodes[i]...)) + end + + System(eqs, t, [], [], systems = [d..., y...], name = name) +end + +# Decodes `n` inputs to `N` outputs, where `N = 2^n` +""" +```julia +Decoder(; name, n = 2) +``` + +Performs the reverse operation of an [`Encoder`](@ref). Decodes `n` inputs +to `N` outputs, where `N=2ⁿ`. The `i`th output is `1` if the values of +the select lines correspond to the binary representation of `1`. + +# Connectors + + - `d1`, `d2`, ... + The `n` input lines + - `y1`, `y2`, ... + The `N` output lines +""" +@component function Decoder(; name, n = 2) + N = 2^n + d = map(0:(n - 1)) do i + DigitalPin(; name = Symbol(:d, i)) + end + y = map(0:(N - 1)) do i + DigitalPin(; name = Symbol(:y, i)) + end + + nodes = Vector{Term{Real, Nothing}}[] + for i in 1:N + bin = digits!(zeros(Int64, n), i - 1, base = 2) + statelist = Term{Real, Nothing}[] + for j in 1:n + varst = bin[j] == 0 ? _not(d[j].val) : d[j].val + push!(statelist, varst) + end + push!(nodes, statelist) + end + + eqs = Equation[] + for i in N:-1:1 + push!(eqs, y[i].val ~ _and(nodes[i]...)) + end + + System(eqs, t, [], [], systems = [d..., y...], name = name) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/gates.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/gates.jl new file mode 100644 index 0000000000..9f5e9e6e31 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/gates.jl @@ -0,0 +1,178 @@ +""" +```julia +Not(; name) +``` + +NOT gate in 9-level logic. + +# Connectors + + - `x` + Input [`DigitalPin`](@ref) + - `y` + Output [`DigitalPin`](@ref) +""" +function Not(; name) + @named x = DigitalPin() + @named y = DigitalPin() + + eqs = [x.i ~ y.i + y.val ~ _not(x.val)] + System(eqs, t, [], [], systems = [x, y], name = name) +end + +""" +```julia +And(; name, N = 2) +``` + +AND gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function And(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vals = [k.val for k in x] + eqs = [y.val ~ _and(vals...) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end + +""" +```julia +Nand(; name, N = 2) +``` + +NAND gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function Nand(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vlist = [k.val for k in x] + eqs = [y.val ~ _not(_and(vlist...)) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end + +""" +```julia +Or(; name, N = 2) +``` + +OR gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function Or(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vals = [k.val for k in x] + eqs = [y.val ~ _or(vals...) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end + +""" +```julia +Nor(; name, N = 2) +``` + +NOR gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function Nor(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vlist = [k.val for k in x] + eqs = [y.val ~ _not(_or(vlist...)) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end + +""" +```julia +Xor(; name, N = 2) +``` + +XOR gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function Xor(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vals = [k.val for k in x] + eqs = [y.val ~ _xor(vals...) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end + +""" +```julia +Xnor(; name, N = 2) +``` + +XNOR gate in 9-level logic, with `N` inputs + +# Connectors + + - `x1`, `x2`, ... + `N` input [`DigitalPin`](@ref)s + - `y` + Output [`DigitalPin`](@ref) +""" +function Xnor(; name, N = 2) + x = map(1:N) do i + DigitalPin(name = Symbol(:x, i)) + end + @named y = DigitalPin() + + vlist = [k.val for k in x] + eqs = [y.val ~ _not(_xor(vlist...)) + y.i ~ sum(k -> k.i, x)] + System(eqs, t, [], [], systems = [x..., y], name = name) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic.jl new file mode 100644 index 0000000000..ed6f52ffa0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic.jl @@ -0,0 +1,54 @@ +@enum Logic Uninitialized=1 ForcingUnknown ForcingZero ForcingOne HighImpedance WeakUnknown WeakZero WeakOne DontCare + +const U = Uninitialized +const X = ForcingUnknown +const F0 = ForcingZero +const F1 = ForcingOne +const Z = HighImpedance +const W = WeakUnknown +const L = WeakZero +const H = WeakOne +const DC = DontCare + +function Base.show(io::IO, ::MIME"text/plain", l::Logic) + if Int(l) == 1 + print(io, "U") + elseif Int(l) == 2 + print(io, "X") + elseif Int(l) == 3 + print(io, "F0") + elseif Int(l) == 4 + print(io, "F1") + elseif Int(l) == 5 + print(io, "Z") + elseif Int(l) == 6 + print(io, "W") + elseif Int(l) == 7 + print(io, "L") + elseif Int(l) == 8 + print(io, "H") + elseif Int(l) == 9 + print(io, "DC") + else + print(io, "Invalid logic level: $l") + end +end + +Base.zero(::Logic) = F0 +Base.zero(::Type{Logic}) = F0 +Base.one(::Logic) = F1 +Base.one(::Type{Logic}) = F1 + +# Helpers to convert 1 and 0 to their `Logic` counterparts +function Base.convert(l::Type{Logic}, i::Number) + if i == zero(i) + zero(l) + elseif i == one(i) + one(l) + else + throw("$i isn't a valid `Logic` value") + end +end +Base.convert(l::Type{Logic}, i::Logic) = i + +get_logic_level(l::Logic) = Int(l) diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic_vectors.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic_vectors.jl new file mode 100644 index 0000000000..590621cd11 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/logic_vectors.jl @@ -0,0 +1,43 @@ +import Base: size, axes, getindex, setindex! + +const LogicOrNumber = Union{Logic, Number} + +struct StdULogicVector{N} <: AbstractArray{Logic, N} + logic::Array{Logic, N} + function StdULogicVector(l::Array) + new{ndims(l)}(Array{Logic}(convert.(Logic, l))) + end +end + +struct StdLogicVector{N} <: AbstractArray{Logic, N} + logic::Array{Logic, N} + function StdLogicVector(l::Array) + new{ndims(l)}(Array{Logic}(convert.(Logic, l))) + end +end + +const LogicVector = Union{StdULogicVector, StdLogicVector} + +size(l::LogicVector) = size(l.logic) + +axes(l::LogicVector) = axes(l.logic) + +getindex(s::LogicVector, i::Int) = getindex(s.logic, i) +function Base.getindex(s::LogicVector, i1::Int, i2::Int, + I::Int...) + getindex(s.logic, i1, i2, I...) +end + +setindex!(A::LogicVector, x::Logic, i1::Int) = setindex!(A.logic, x, i1) +function Base.setindex!(A::LogicVector, x::Logic, i1::Int, i2::Int, I::Int...) + setindex!(A.logic, x, i1, i2, I...) +end + +get_logic_level(s::LogicVector) = Int.(s.logic) + +# predefined vectors +const std_ulogic = StdULogicVector([U, X, F0, F1, Z, W, L, H, DC]) +const UX01 = StdULogicVector([U, X, F0, F1]) +const UX01Z = StdULogicVector([U, X, F0, F1, Z]) +const X01 = StdULogicVector([X, F0, F1]) +const X01Z = StdULogicVector([X, F0, F1, Z]) diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/sources.jl new file mode 100644 index 0000000000..0e4160b0f3 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/sources.jl @@ -0,0 +1,88 @@ +""" +```julia +PulseDiff(; name, Val = 1, dt = 0.1) +``` + +# States + + - `val(t)` + Output value of the source + +# Connectors + + - `d` + Output [`DigitalPin`](@ref) +""" +function PulseDiff(; name, Val = 1, dt = 0.1) + @named d = DigitalPin() + @variables val(t) + D = ModelingToolkit.Difference(t; dt = dt) + + eqs = [D(val) ~ Val + val ~ d.val] + + System(eqs, t, [val], [], systems = [d], defaults = Dict(Val => 0), name = name) +end + +""" +```julia +Set(; name) +``` + +Source that outputs a constant signal of `1`. + +# Connectors + + - `d` + Output [`DigitalPin`](@ref) +""" +function Set(; name) + @named d = DigitalPin() + + eqs = [ + d.val ~ 1 + ] + System(eqs, t, [], [], systems = [d], name = name) +end + +""" +```julia +Reset(; name) +``` + +Source that outputs a constant signal of `1` + +# Connectors + + - `d` + Output [`DigitalPin`](@ref) +""" +function Reset(; name) + @named d = DigitalPin() + + eqs = [ + d.val ~ 0 + ] + System(eqs, t, [], [], systems = [d], name = name) +end + +""" +```julia +Pulse(; name, duty_cycle = 0.5, T = 1.0) +``` + +Pulse output with specified `duty_cycle` and time period (`T`) + +# Connectors + + - `d` + Output [`DigitalPin`](@ref) +""" +function Pulse(; name, duty_cycle = 0.5, T = 1.0) + @named d = DigitalPin() + + eqs = [ + d.val ~ IfElse.ifelse(t % T > duty_cycle * T, 1, 0) + ] + System(eqs, t, [], [], systems = [d], name = name) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/tables.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/tables.jl new file mode 100644 index 0000000000..beb1717306 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Digital/tables.jl @@ -0,0 +1,134 @@ +struct LogicTable{N} <: AbstractArray{Logic, N} + logic::Array{Logic, N} + function LogicTable(l::Array{Logic}) + any(i -> i != 9, size(l)) && + throw(ArgumentError("Incorrect number of logic values are passed. A variable of type +`LogicTable` must have nine entries corresponding to 9 logic levels")) + new{ndims(l)}(l) + end +end + +Base.size(l::LogicTable) = size(l.logic) + +Base.axes(l::LogicTable) = axes(l.logic) + +getindex(s::LogicTable, l::Logic) = getindex(s.logic, get_logic_level(l)) +function Base.getindex(s::LogicTable, i1::Logic, i2::Logic) + getindex(s.logic, get_logic_level(i1), get_logic_level(i2)) +end +function Base.getindex(s::LogicTable, i1::Logic, i2::Logic, + I::Logic...) + getindex(s.logic, get_logic_level(i1), get_logic_level(i2), get_logic_level(I...)...) +end + +getindex(s::LogicTable, l::Int) = getindex(s.logic, l) +function getindex(s::LogicTable, i1::Int, i2::Int, I::Int...) + getindex(s.logic, i1, i2, I...) +end + +function Base.setindex!(A::LogicTable, x::Logic, i1::Int) + setindex!(A.logic, x, i1) +end +function Base.setindex!(A::LogicTable, x::Logic, i1::Int, i2::Int, I::Int...) + setindex!(A.logic, x, i1, i2, I...) +end + +get_logic_level(l::LogicTable) = Int.(l.logic) + +# AND gate +const AndTable = LogicTable([ + # U X F0 F1 Z W L H DC + U U F0 U U U F0 U U # U + U X F0 X X X F0 X X # X + F0 F0 F0 F0 F0 F0 F0 F0 F0 # F0 + U X F0 F1 X X F0 F1 X # F1 + U X F0 X X X F0 X X # Z + U X F0 X X X F0 X X # W + F0 F0 F0 F0 F0 F0 F0 F0 F0 # L + U X F0 F1 X X F0 F1 X # H + U X F0 X X X F0 X X]) # DC + +function _and2(a::Logic, b::Logic) + AndTable[a, b] +end +_and2(a::Number, b::Logic) = _and2(convert(Logic, a), b) +_and2(a::Logic, b::Number) = _and2(a, convert(Logic, b)) +_and2(a::Number, b::Number) = _and2(convert(Logic, a), convert(Logic, b)) + +function _and(x...) + y = x[1] + for i in 2:lastindex(x) + y = _and2(y, x[i]) + end + return y +end + +@register_symbolic _and(a, b) + +# NOT gate +const NotTable = LogicTable([U, X, F1, F0, X, X, F1, F0, X]) + +_not(x::Logic) = NotTable[x] +_not(x::Number) = _not(convert(Logic, x)) + +@register_symbolic _not(x) + +# OR gate +const OrTable = LogicTable([ + # U X F0 F1 Z W L H DC + U U U F1 U U U F1 U # U + U X X F1 X X X F1 X # X + U X F0 F1 X X F0 F1 X # F0 + F1 F1 F1 F1 F1 F1 F1 F1 F1 # F1 + U X X F1 X X X F1 X # Z + U X X F1 X X X F1 X # W + U X F0 F1 X X F0 F1 X # L + F1 F1 F1 F1 F1 F1 F1 F1 F1 # H + U X X F1 X X X F1 X]) # DC + +function _or2(a::Logic, b::Logic) + OrTable[a, b] +end +_or2(a::Number, b::Logic) = _or2(convert(Logic, a), b) +_or2(a::Logic, b::Number) = _or2(a, convert(Logic, b)) +_or2(a::Number, b::Number) = _or2(convert(Logic, a), convert(Logic, b)) + +function _or(x...) + y = x[1] + for i in 2:lastindex(x) + y = _or2(y, x[i]) + end + return y +end + +@register_symbolic _or(a, b) + +# XOR gate +const XorTable = LogicTable([ + # U X F0 F1 Z W L H DC + U U U U U U U U U # U + U X X X X X X X X # X + U X F0 F1 X X F0 F1 X # F0 + U X F1 F0 X X F1 F0 X # F1 + U X X X X X X X X # Z + U X X X X X X X X # W + U X F0 F1 X X F0 F1 X # L + U X F1 F0 X X F1 F0 X # H + U X X X X X X X X]) # DC + +function _xor2(a::Logic, b::Logic) + XorTable[a, b] +end +_xor2(a::Number, b::Logic) = _xor2(convert(Logic, a), b) +_xor2(a::Logic, b::Number) = _xor2(a, convert(Logic, b)) +_xor2(a::Number, b::Number) = _xor2(convert(Logic, a), convert(Logic, b)) + +function _xor(x...) + y = x[1] + for i in 2:lastindex(x) + y = _xor2(y, x[i]) + end + return y +end + +@register_symbolic _xor(a, b) diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/Electrical.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/Electrical.jl new file mode 100644 index 0000000000..712e4b172d --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/Electrical.jl @@ -0,0 +1,53 @@ +""" +Library of electrical models. +This library contains electrical components to build up analog circuits. +""" +module Electrical + +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ..Thermal: HeatPort +using ..Mechanical.Rotational: Flange, Support +using ..Blocks: RealInput, RealOutput + +export Pin, OnePort +include("utils.jl") + +export Capacitor, + Ground, Inductor, Resistor, Conductor, Short, IdealOpAmp, EMF, + Diode, VariableResistor +include("Analog/ideal_components.jl") + +export CurrentSensor, PotentialSensor, VoltageSensor, PowerSensor, MultiSensor +include("Analog/sensors.jl") + +export Voltage, Current +include("Analog/sources.jl") + +export NMOS, PMOS +include("Analog/mosfets.jl") + +export NPN, PNP +include("Analog/transistors.jl") + +# include("Digital/gates.jl") +# include("Digital/sources.jl") + +# TODO: +# - digital +# - machines +# - multi-phase + +export Logic +include("Digital/logic.jl") + +export StdLogicVector, StdULogicVector, + std_ulogic, UX01, UX01Z, X01, X01Z, + get_logic_level +include("Digital/logic_vectors.jl") + +export LogicTable, + AndTable, OrTable, NotTable, XorTable +include("Digital/tables.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Electrical/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Electrical/utils.jl new file mode 100644 index 0000000000..2fea22c277 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Electrical/utils.jl @@ -0,0 +1,107 @@ +@connector Pin begin + v(t) # Potential at the pin [V] + i(t), [connect = Flow] # Current flowing into the pin [A] +end +@doc """ + Pin(; name) + +A pin in an analog circuit. + +# States: +- `v(t)`: [`V`] The voltage at this pin +- `i(t)`: [`A`] The current passing through this pin +""" Pin + +""" + OnePort(; name, v = 0.0, i = 0.0) + +Component with two electrical pins `p` and `n` and current `i` flows from `p` to `n`. + +# States: + + - `v(t)`: [`V`] The voltage across component `p.v - n.v` + - `i(t)`: [`A`] The current passing through positive pin + +# Connectors: + + - `p` Positive pin + - `n` Negative pin +""" +@mtkmodel OnePort begin + @components begin + p = Pin() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + v ~ p.v - n.v + 0 ~ p.i + n.i + i ~ p.i + end +end + +""" + TwoPort(; name, v1 = 0.0, v2 = 0.0, i1 = 0.0, i2 = 0.0) + +Component with four electrical pins `p1`, `n1`, `p2` and `n2` +Current `i1` flows from `p1` to `n1` and `i2` from `p2` to `n2`. + +# States: +- `v1(t)`: [`V`] The voltage across first ports `p1.v - n1.v` +- `v2(t)`: [`V`] The voltage across second ports `p2.v - n2.v` +- `i1(t)`: [`A`] The current passing through positive pin `p1` +- `i2(t)`: [`A`] The current passing through positive pin `p2` + +# Connectors: +- `p1` First positive pin +- `p2` Second positive pin +- `n1` First negative pin +- `n2` Second Negative pin +""" + +@mtkmodel TwoPort begin + @components begin + p1 = Pin() + n1 = Pin() + p2 = Pin() + n2 = Pin() + end + @variables begin + v1(t) + i1(t) + v2(t) + i2(t) + end + @equations begin + v1 ~ p1.v - n1.v + 0 ~ p1.i + n1.i + i1 ~ p1.i + v2 ~ p2.v - n2.v + 0 ~ p2.i + n2.i + i2 ~ p2.i + end +end + +@connector function DigitalPin(; name) + @variables val(t) v(t) i(t) + eqs = [ + val ~ IfElse.ifelse((0.0 <= v) & (v <= 0.8) | (2.0 <= v) & (v <= 5.0), + IfElse.ifelse(v > 2.0, 1, 0), X) + ] + System(Equation[], t, [val, v, i], [], guesses = Dict(val => 0, i => 0), + name = name) +end +@doc """ + DigitalPin(; name) + +A pin in a digital circuit. + +# States: +- `v(t)`: [`V`] The voltage at this pin +- `i(t)`: [`A`] The current passing through this pin +- `val(t)`: The binary value of the pin at this point. A voltage from `0V` to `0.8V` is a binary value of `0`. +A voltage in the range `2.0V` to `5.0V` is `1`. Any other value is `X`. +""" DigitalPin diff --git a/lib/ModelingToolkitStandardLibrary/src/Hydraulic/Hydraulic.jl b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/Hydraulic.jl new file mode 100644 index 0000000000..cb3cb1deb8 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/Hydraulic.jl @@ -0,0 +1,10 @@ +""" +Library of hydraulic models. +""" +module Hydraulic + +using ModelingToolkit + +include("IsothermalCompressible/IsothermalCompressible.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/IsothermalCompressible.jl b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/IsothermalCompressible.jl new file mode 100644 index 0000000000..3403ee022e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/IsothermalCompressible.jl @@ -0,0 +1,24 @@ +""" +Library to model iso-thermal compressible liquid fluid flow +""" +module IsothermalCompressible + +using ModelingToolkit, Symbolics +using ModelingToolkit: t_nounits as t, D_nounits as D + +using ...Blocks: RealInput, RealOutput +using ...Mechanical.Translational: MechanicalPort, Mass + +using IfElse: ifelse + +export HydraulicPort, HydraulicFluid +include("utils.jl") + +export Cap, Tube, FixedVolume, DynamicVolume, Open, FlowDivider, Valve, Volume, SpoolValve, + SpoolValve2Way, Actuator +include("components.jl") + +export MassFlow, Pressure, FixedPressure +include("sources.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/components.jl b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/components.jl new file mode 100644 index 0000000000..305e96105e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/components.jl @@ -0,0 +1,968 @@ + +""" + Cap(; name) + +Caps a hydraulic port to prevent mass flow in or out. + +# Connectors: +- `port`: hydraulic port +""" +@mtkmodel Cap begin + @variables begin + p(t), [guess = 0] + end + + @components begin + port = HydraulicPort() + end + + @equations begin + port.p ~ p + port.dm ~ 0 + end +end + +""" + Open(; name) + +Provides an "open" boundary condition for a hydraulic port such that mass flow `dm` is non-zero. This is opposite from an un-connected hydraulic port or the `Cap` boundary component which sets the mass flow `dm` to zero. + +# Connectors: +- `port`: hydraulic port +""" +@mtkmodel Open begin + @variables begin + p(t), [guess = 0] + dm(t), [guess = 0] + end + + @components begin + port = HydraulicPort() + end + + @equations begin + port.p ~ p + port.dm ~ dm + end +end + +""" + TubeBase(add_inertia = true, variable_length = true; area, length_int, head_factor = 1, perimeter = 2 * sqrt(area * pi), shape_factor = 64, name) + +Variable length internal flow model of the fully developed incompressible flow friction. Includes optional inertia term when `add_inertia = true` to model wave propagation. Hydraulic ports have equal flow but variable pressure. Density is averaged over the pressures, used to calculated average flow velocity and flow friction. + +# States: +- `x`: [m] length of the pipe +- `ddm`: [kg/s^2] Rate of change of mass flow rate in control volume. + +# Parameters: +- `area`: [m^2] tube cross sectional area +- `length_int`: [m] initial tube length +- `perimeter`: [m] perimeter of the pipe cross section (needed only for non-circular pipes) +- `shape_factor`: shape factor, see `friction_factor` function +- `head_factor`: effective length multiplier, used to account for addition friction from flow development and additional friction such as pipe bends, entrance/exit lossses, etc. + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +""" +@component function TubeBase(add_inertia = true, variable_length = true; + area, + length_int, + head_factor = 1, + perimeter = 2 * sqrt(area * pi), + shape_factor = 64, + name) + pars = @parameters begin + area = area + length_int = length_int + perimeter = perimeter + shape_factor = shape_factor + head_factor = head_factor + end + + @variables begin + x(t), [guess = length_int] + ddm(t) = 0 + end + + vars = [] + if variable_length + push!(vars, x) + c = x + else + c = length_int + end + add_inertia && push!(vars, ddm) + + systems = @named begin + port_a = HydraulicPort() + port_b = HydraulicPort() + end + + # let ---------------------- + Δp = port_a.p - port_b.p + dm = port_a.dm + + d_h = 4 * area / perimeter + + # Opting for a more numerically stable constant density (use head factor to compensate if needed) + ρ = density_ref(port_a) # (full_density(port_a) + full_density(port_b)) / 2 + μ = viscosity(port_a) + + f = friction_factor(dm, area, d_h, μ, shape_factor) + u = dm / (ρ * area) + + shear = (1 / 2) * ρ * regPow(u, 2) * f * head_factor * (c / d_h) + inertia = if add_inertia + (c / area) * ddm + else + 0 + end + + eqs = [0 ~ port_a.dm + port_b.dm + domain_connect(port_a, port_b)] + + if variable_length + push!(eqs, Δp ~ ifelse(c > 0, shear + inertia, zero(c))) + else + push!(eqs, Δp ~ shear + inertia) + end + + if add_inertia + push!(eqs, D(dm) ~ ddm) + end + + System(eqs, t, vars, pars; name, systems) +end + +""" + Tube(N, add_inertia=true; p_int, area, length, head_factor=1, perimeter = 2 * sqrt(area * pi), shape_factor = 64, name) + +Constant length internal flow model discretized by `N` (`FixedVolume`: `N`, `TubeBase`:`N-1`) which models the fully developed flow friction, compressibility (when `N>1`), and inertia effects when `add_inertia = true`. See `TubeBase` and `FixedVolume` for more information. + +# Parameters: +- `p_int`: [Pa] initial pressure +- `area`: [m^2] tube cross sectional area +- `length`: [m] real length of the tube +- `perimeter`: [m] perimeter of the pipe cross section (needed only for non-circular pipes) +- `shape_factor`: shape factor, see `friction_factor` function +- `head_factor`: effective length multiplier, used to account for addition friction from flow development and additional friction such as pipe bends, entrance/exit lossses, etc. + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +""" +@component function Tube(N, add_inertia = true; area, length, head_factor = 1, + perimeter = 2 * sqrt(area * pi), + shape_factor = 64, p_int, name) + @assert(N>0, + "the Tube component must be defined with at least 1 segment (i.e. N>0), found N=$N") + + #TODO: How to set an assert effective_length >= length ?? + pars = @parameters begin + area = area + length = length + head_factor = head_factor + perimeter = perimeter + shape_factor = shape_factor + p_int = p_int + end + + vars = [] + + ports = @named begin + port_a = HydraulicPort() + port_b = HydraulicPort() + end + + pipe_bases = [] + for i in 1:N + x = TubeBase(add_inertia, false; name = Symbol("p$i"), + shape_factor = ParentScope(shape_factor), + area = ParentScope(area), + length_int = N > 1 ? ParentScope(length) / (N - 1) : ParentScope(length), + head_factor = ParentScope(head_factor), + perimeter = ParentScope(perimeter)) + push!(pipe_bases, x) + end + + eqs = [connect(pipe_bases[1].port_a, port_a) + connect(pipe_bases[end].port_b, port_b)] + + volumes = [] + for i in 1:(N - 1) + x = FixedVolume(; name = Symbol("v$i"), + vol = ParentScope(area) * ParentScope(length) / (N - 1), + p_int = ParentScope(p_int)) + push!(volumes, x) + push!(eqs, + connect(x.port, pipe_bases[i].port_b, pipe_bases[i + 1].port_a)) + end + + return System(eqs, t, vars, pars; name, systems = [ports; pipe_bases; volumes]) +end + +""" + FlowDivider(; n, name) + +Reduces the flow from `port_a` to `port_b` by `n`. Useful for modeling parallel tubes efficiently by placing a `FlowDivider` on each end of a tube. + +# Parameters: +- `n`: divide flow from `port_a` to `port_b` by `n` + +# Connectors: +- `port_a`: full flow hydraulic port +- `port_b`: part flow hydraulic port +""" +@mtkmodel FlowDivider begin + + #TODO: assert n >= 1 + + @parameters begin + n = n + end + + @variables begin + dm_a(t), [guess = 0] + dm_b(t), [guess = 0] + end + + @components begin + port_a = HydraulicPort() + port_b = HydraulicPort() + open = Open() + end + + @equations begin + connect(port_a, port_b, open.port) + dm_a ~ port_a.dm + dm_b ~ dm_a / n + open.dm ~ dm_a - dm_b # extra flow dumps into an open port + # port_b.dm ~ dm_b # divided flow goes to port_b + end +end + +@component function ValveBase( + reversible = false; minimum_area = 0, Cd, Cd_reverse = Cd, name) + pars = @parameters begin + Cd = Cd + Cd_reverse = Cd_reverse + minimum_area = minimum_area + end + + systems = @named begin + port_a = HydraulicPort() + port_b = HydraulicPort() + end + + vars = @variables begin + area(t), [guess = 0] + y(t), [guess = 0] + end + + # let + # Opting for a more numerically stable constant density (use head factor to compensate if needed) + ρ = density_ref(port_a) #(full_density(port_a) + full_density(port_b)) / 2 + + x = if reversible + area + else + ifelse(area > minimum_area, area, minimum_area) + end + + # let ------ + Δp = port_a.p - port_b.p + dm = port_a.dm + c = if reversible + Cd + else + ifelse(Δp > 0, Cd, Cd_reverse) + end + + eqs = [0 ~ port_a.dm + port_b.dm + domain_connect(port_a, port_b) + dm ~ regRoot(2 * Δp * ρ / c) * x # I think this should be reformulated as: regRoot(2 DP rho) c x + y ~ x] + + System(eqs, t, vars, pars; name, systems) +end + +""" + Valve(reversible = false; p_a_int, p_b_int, area_int, Cd, Cd_reverse = Cd, minimum_area = 0, name) + +Valve with `area` input and discharge coefficient `Cd` defined by https://en.wikipedia.org/wiki/Discharge_coefficient. The `Cd_reverse` parameter allows for directional flow restriction, making it possible to define a check valve. + +# Parameters: +- `p_a_int`: [Pa] initial pressure for `port_a` +- `p_b_int`: [Pa] initial pressure for `port_b` +- `area_int`: [m^2] initial valve opening +- `Cd`: discharge coefficient flowing from `a → b` +- `Cd_reverse`: discharge coefficient flowing from `b → a` +- `minimum_area`: when `reversible = false` applies a forced minimum area + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +- `area`: real input setting the valve `area`. When `reversible = true`, negative input reverses flow direction, otherwise a floor of `minimum_area` is enforced. +""" +@component function Valve(reversible = false; + Cd, Cd_reverse = Cd, + minimum_area = 0, + name) + pars = @parameters begin + Cd = Cd + Cd_reverse = Cd_reverse + minimum_area = minimum_area + end + + systems = @named begin + port_a = HydraulicPort() + port_b = HydraulicPort() + area = RealInput() + base = ValveBase(reversible; Cd, Cd_reverse, + minimum_area) + end + + vars = [] + + eqs = [connect(base.port_a, port_a) + connect(base.port_b, port_b) + base.area ~ area.u] + + System(eqs, t, vars, pars; name, systems) +end + +@component function VolumeBase(; area, dead_volume = 0, p_int, x_int, + name) + pars = @parameters begin + area = area + dead_volume = dead_volume + p_int = p_int + end + + systems = @named begin + port = HydraulicPort() + end + + vars = @variables begin + x(t) = x_int + dx(t), [guess = 0] + rho(t), [guess = liquid_density(port)] + m(t), [guess = 0] + vol(t) + end + + # let + dm = port.dm + p = port.p + + eqs = [vol ~ dead_volume + area * x + D(x) ~ dx + D(m) ~ dm + #rho ~ full_density(port, p) + p ~ full_pressure(port, rho) # see https://github.com/SciML/OrdinaryDiffEq.jl/issues/2561 + m ~ rho * vol] + + initialization_eqs = [p ~ p_int] + + System(eqs, t, vars, pars; name, systems, initialization_eqs) +end + +""" + FixedVolume(; p_int, vol, name) + +Fixed fluid volume. + +# Parameters: +- `p_int`: [Pa] initial pressure +- `vol`: [m^3] fixed volume + +# Connectors: +- `port`: hydraulic port +""" +@component function FixedVolume(; vol, name, p_int) + pars = @parameters begin + vol = vol + p_int = p_int + end + + systems = @named begin + port = HydraulicPort(;) + end + + vars = @variables begin + rho(t), [guess = liquid_density(port)] + m(t), [guess = vol * liquid_density(port)] + p(t) = p_int + end + + # let + dm = port.dm + + eqs = [D(m) ~ dm + # rho ~ full_density(port, p) + p ~ full_pressure(port, rho) # see https://github.com/SciML/OrdinaryDiffEq.jl/issues/2561 + p ~ port.p + m ~ rho * vol] + + System(eqs, t, vars, pars; name, systems) +end + +""" + Volume(; x, dx=0, p, drho=0, dm=0, area, direction = 1, name) + +Volume with moving wall with `flange` connector for converting hydraulic energy to 1D mechanical. The `direction` argument aligns the mechanical port with the hydraulic port, useful when connecting two dynamic volumes together in oppsing directions to create an actuator. + +``` + ┌─────────────────┐ ─── + │ │ ▲ + │ │ +dm ────► │ │ area + │ │ + │ │ ▼ + └─────────────────┤ ─── + │ + └─► x (= ∫ flange.v * direction) +``` + +# Features: +- volume discretization with flow resistance and inertia: use `N` to control number of volume and resistance elements. Set `N=0` to turn off volume discretization. See `TubeBase` for more information about flow resistance. +- minimum volume flow shutoff with damping and directional resistance. Use `reversible=false` when problem defines volume position `x` and solves for `dm` to prevent numerical instability. + +# Parameters: +## volume +- `p`: [Pa] initial pressure +- `area`: [m^2] moving wall area +- `x`: [m] initial wall position +- `dx=0`: [m/s] initial wall velocity +- `drho=0`: [kg/m^3/s] initial density derivative +- `dm=0`: [kg/s] initial flow + +- `direction`: [+/-1] applies the direction conversion from the `flange` to `x` + +# Connectors: +- `port`: hydraulic port +- `flange`: mechanical translational port + +See also [`FixedVolume`](@ref), [`DynamicVolume`](@ref) +""" +@component function Volume(; + #parameters + area, + direction = +1, + x_int, + name) + pars = @parameters begin + area = area + x_int = x_int + end + + vars = @variables begin + x(t) = x_int + dx(t), [guess = 0] + p(t), [guess = 0] + f(t), [guess = 0] + rho(t), [guess = 0] + m(t), [guess = 0] + dm(t), [guess = 0] + end + + systems = @named begin + port = HydraulicPort() + flange = MechanicalPort() + damper = ValveBase(reversible; + Cd, + Cd_reverse, + minimum_area) + end + + systems = @named begin + port = HydraulicPort() + flange = MechanicalPort() + damper = ValveBase(reversible; + Cd, + Cd_reverse, + minimum_area) + end + + eqs = [ + # connectors + port.p ~ p + port.dm ~ dm + flange.v * direction ~ dx + flange.f * direction ~ -f + # differentials + D(x) ~ dx + D(m) ~ dm + + # physics + # rho ~ full_density(port, p) + p ~ full_pressure(port, rho) # see https://github.com/SciML/OrdinaryDiffEq.jl/issues/2561 + f ~ p * area + m ~ rho * x * area] + + System(eqs, t, vars, pars; name, systems) +end + +""" + DynamicVolume(reversible = false; p_int, area, x_int = 0, x_max, x_min = 0, x_damp = x_min, direction = +1, perimeter = 2 * sqrt(area * pi), shape_factor = 64, head_factor = 1, Cd = 1e2, Cd_reverse = Cd, name) + +Volume with moving wall with `flange` connector for converting hydraulic energy to 1D mechanical. The `direction` argument aligns the mechanical port with the hydraulic port, useful when connecting two dynamic volumes together in oppsing directions to create an actuator. + +``` + ┌─────────────────┐ ─── + │ │ ▲ + │ │ +dm ────► │ │ area + │ │ + │ │ ▼ + └─────────────────┤ ─── + │ + └─► x (= ∫ flange.v * direction) +``` + +# Features: +- minimum volume flow shutoff with damping and directional resistance. Use `reversible=false` when problem defines volume position `x` and solves for `dm` to prevent numerical instability. + +# Parameters: +## volume +- `p_int`: [Pa] initial pressure +- `area`: [m^2] moving wall area +- `x_max`: [m] max wall position, needed for volume discretization to apply the correct volume sizing as a function of `x` +- `x_min`: [m] wall position that shuts off flow and prevents negative volume. +- `x_damp`: [m] wall position that initiates a linear damping region before reaching full flow shut off. Helps provide a smooth end stop. + +- `direction`: [+/-1] applies the direction conversion from the `flange` to `x` + +## flow resistance +- `perimeter`: [m] perimeter of the cross section (needed only for non-circular volumes) +- `shape_factor`: shape factor, see `friction_factor` function +- `head_factor`: effective length multiplier, used to account for addition friction from flow development and additional friction such as pipe bends, entrance/exit lossses, etc. + +## flow shut off and damping +- `Cd`: discharge coefficient for flow out of the volume. *Note: area is 1m² when valve is fully open. Ensure this does not induce unwanted flow resistance.* +- `Cd_reverse`: discharge coefficient for flow into the volume. Use a lower value to allow easy wall release, in some cases the wall can "stick". + + +# Connectors: +- `port`: hydraulic port +- `flange`: mechanical translational port +""" +@component function DynamicVolume(reversible = false; + area, + x_int = 0, + x_max, + x_min = 0, + x_damp = x_min, + direction = +1, + + # Tube + perimeter = 2 * sqrt(area * pi), + shape_factor = 64, + head_factor = 1, + p_int, + + # Valve + Cd = 1e2, + Cd_reverse = Cd, + minimum_area = 0, + + # Damping + d = 0, name) + @assert (direction == +1)||(direction == -1) "direction argument must be +/-1, found $direction" + + #TODO: How to set an assert effective_length >= length ?? + pars = @parameters begin + area = area + + x_int = x_int + x_max = x_max + x_min = x_min + x_damp = x_damp + + perimeter = perimeter + shape_factor = shape_factor + head_factor = head_factor + p_int = p_int + + Cd = Cd + Cd_reverse = Cd_reverse + minimum_area = minimum_area + + d = d + end + + vars = @variables begin + x(t) = x_int + vol(t), [guess = x_int * area] + end + + systems = @named begin + port = HydraulicPort(;) + flange = MechanicalPort(;) + damper = ValveBase(reversible; + Cd, + Cd_reverse, + minimum_area) + moving_volume = VolumeBase(; + area, + dead_volume = area * x_int, + p_int, + x_int = 0) + end + + ratio = (x - x_min) / (x_damp - x_min) + + damper_area = if reversible + one(x) + else + ifelse(x >= x_damp, one(x), ifelse((x < x_damp) & (x > x_min), ratio, zero(x))) + end + + dx = moving_volume.dx + p = moving_volume.port.p + + eqs = [vol ~ x * area + D(x) ~ flange.v * direction + damper.area ~ damper_area + connect(port, damper.port_b) + connect(moving_volume.port, damper.port_a) + dx ~ flange.v * direction + p * area - dx * d ~ -flange.f * direction] + + return System(eqs, t, vars, pars; name, systems) +end + +""" + SpoolValve(reversible = false; x_int, Cd, d, name) + +Spool valve with `x` valve opening input as mechanical flange port and `d` diameter of orifice. See `Valve` for more information. + +# Parameters: +- `x_int`: [m] initial valve opening +- `d`: [m] orifice diameter +- `Cd`: discharge coefficient flowing from `a → b` + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +- `flange`: mechanical translational port + +See [`Valve`](@ref) for more information. +""" +@component function SpoolValve(reversible = false; Cd, d, x_int, name) + pars = @parameters begin + d = d + Cd = Cd + x_int = x_int + end + + systems = @named begin + port_a = HydraulicPort(;) + port_b = HydraulicPort(;) + flange = MechanicalPort() + valve = ValveBase(reversible; Cd) + end + + vars = @variables begin + x(t) = x_int + dx(t), [guess = 0] + end + + eqs = [D(x) ~ dx + flange.v ~ dx + flange.f ~ 0 #TODO: model flow force + connect(valve.port_a, port_a) + connect(valve.port_b, port_b) + valve.area ~ x * 2π * d] + + System(eqs, t, vars, pars; name, systems) +end + +""" + SpoolValve2Way(reversible = false; m, g, x_int, Cd, d, name) + +2-ways spool valve with 4 ports and spool mass. Fluid flow direction S → A and B → R when `x` is positive and S → B and A → R when `x` is negative. + +# Parameters: +- `m`: [kg] mass of the spool +- `g`: [m/s²] gravity field acting on the spool, positive value acts in the positive direction +- `x_int`: [m] initial valve opening +- `d`: [m] orifice diameter +- `Cd`: discharge coefficient flowing from `s → a` and `b → r` + +# Connectors: +- `port_s`: hydraulic port +- `port_a`: hydraulic port +- `port_b`: hydraulic port +- `port_r`: hydraulic port +- `flange`: mechanical translational port + +See [`SpoolValve`](@ref) for more information. +""" +@component function SpoolValve2Way(reversible = false; m, g, Cd, d, x_int, name) + pars = @parameters begin + m = m + g = g + + d = d + + Cd = Cd + + x_int = x_int + # dx_int = dx_int + end + + vars = [] + + systems = @named begin + vSA = SpoolValve(reversible; Cd, d, x_int) + vBR = SpoolValve(reversible; Cd, d, x_int) + + port_s = HydraulicPort(;) + port_a = HydraulicPort(;) + port_b = HydraulicPort(;) + port_r = HydraulicPort(;) + + mass = Mass(; m = m, g = g) + + flange = MechanicalPort() + end + + eqs = [connect(vSA.port_a, port_s) + connect(vSA.port_b, port_a) + connect(vBR.port_a, port_b) + connect(vBR.port_b, port_r) + connect(vSA.flange, vBR.flange, mass.flange, flange)] + + initialization_eqs = [ + mass.s ~ x_int + # mass.v ~ dx_int + ] + + System(eqs, t, vars, pars; name, systems, initialization_eqs) +end + +""" + Actuator(N, add_inertia = true, reversible = false; + p_a_int, + p_b_int, + area_a, + area_b, + perimeter_a = 2 * sqrt(area_a * pi), + perimeter_b = 2 * sqrt(area_b * pi), + length_a_int, + length_b_int, + shape_factor_a = 64, + shape_factor_b = 64, + head_factor_a = 1, + head_factor_b = 1, + m, + g, + x_int = 0, + minimum_volume_a = 0, + minimum_volume_b = 0, + damping_volume_a = minimum_volume_a, + damping_volume_b = minimum_volume_b, + Cd = 1e4, + Cd_reverse = Cd, + p_a_int, + p_b_int, + name) + +Actuator made of two DynamicVolumes connected in opposite direction with body mass attached. + +# Features: +- volume discretization with flow resistance and inertia: use `N` to control number of volume and resistance elements. Set `N=0` to turn off volume discretization. See `TubeBase` for more information about flow resistance. +- minimum volume flow shutoff with damping and directional resistance. Use `reversible=false` when problem defines volume position `x` and solves for `dm` to prevent numerical instability. + +# Parameters: +## volume +- `p_a_int`: [Pa] initial pressure for `port_a` +- `p_b_int`: [Pa] initial pressure for `port_b` +- `area_a`: [m^2] moving wall area of volume `A` +- `area_b`: [m^2] moving wall area of volume `B` +- `length_a_int`: [m] initial wall position for `A` +- `length_b_int`: [m] initial wall position for `b` + +## mass +- `m`: [kg] mass of the body +- `g`: [m/s²] gravity field acting on the mass, positive value acts in the positive direction +- `x_int`: [m] initial flange position + +## flow resistance +- `perimeter_a`: [m] perimeter of the cross section `A` (needed only for non-circular volumes) +- `perimeter_b`: [m] perimeter of the cross section `B` (needed only for non-circular volumes) +- `shape_factor_a`: shape factor of `A`, see `friction_factor` function +- `shape_factor_b`: shape factor of `B`, see `friction_factor` function +- `head_factor_a`: effective length multiplier for `A`, used to account for addition friction from flow development and additional friction such as pipe bends, entrance/exit lossses, etc. +- `head_factor_b`: effective length multiplier for `B`, used to account for addition friction from flow development and additional friction such as pipe bends, entrance/exit lossses, etc. + +## flow shut off and damping +- `minimum_volume_a`: [m^3] minimum volume `A` that shuts off flow and prevents negative volume. +- `minimum_volume_b`: [m^3] minimum volume `B` that shuts off flow and prevents negative volume. +- `damping_volume_a`: [m^3] volume of `A` that initiates a linear damping region before reaching full flow shut off. Helps provide a smooth end stop. +- `damping_volume_b`: [m^3] volume of `B` that initiates a linear damping region before reaching full flow shut off. Helps provide a smooth end stop. +- `Cd`: discharge coefficient for flow out of the volume. *Note: area is 1m² when valve is fully open. Ensure this does not induce unwanted flow resistance.* +- `Cd_reverse`: discharge coefficient for flow into the volume. Use a lower value to allow easy wall release, in some cases the wall can "stick". + + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +- `flange`: mechanical translational port +""" +@component function Actuator(reversible = false; + area_a, + area_b, + perimeter_a = 2 * sqrt(area_a * pi), + perimeter_b = 2 * sqrt(area_b * pi), + length_a_int, + length_b_int, + shape_factor_a = 64, + shape_factor_b = 64, + head_factor_a = 1, + head_factor_b = 1, + m, + g, + x_int = 0, + dx_int = 0, + minimum_volume_a = 0, + minimum_volume_b = 0, + damping_volume_a = minimum_volume_a, + damping_volume_b = minimum_volume_b, + Cd = 1e4, + Cd_reverse = Cd, + d = 0, + p_a_int, + p_b_int, + name) + pars = @parameters begin + area_a = area_a + area_b = area_b + perimeter_a = perimeter_a + perimeter_b = perimeter_b + shape_factor_a = shape_factor_a + shape_factor_b = shape_factor_b + head_factor_a = head_factor_a + head_factor_b = head_factor_b + x_int = x_int + dx_int = dx_int + length_a_int = length_a_int + length_b_int = length_b_int + minimum_volume_a = minimum_volume_a + minimum_volume_b = minimum_volume_b + damping_volume_a = damping_volume_a + damping_volume_b = damping_volume_b + Cd = Cd + Cd_reverse = Cd_reverse + m = m + g = g + d = d + p_a_int = p_a_int + p_b_int = p_b_int + end + + vars = @variables begin + x(t) = x_int + dx(t) = dx_int + end + + total_length = length_a_int + length_b_int + + #TODO: include effective_length + systems = @named begin + vol_a = DynamicVolume(reversible; direction = +1, + area = area_a, + x_int = length_a_int, + x_max = total_length, + x_min = minimum_volume_a / area_a, + x_damp = damping_volume_a / area_a, + perimeter = perimeter_a, + shape_factor = shape_factor_a, + head_factor = head_factor_a, + Cd, + Cd_reverse, + d, + p_int = p_a_int) + + vol_b = DynamicVolume(reversible; direction = -1, + area = area_b, + x_int = length_b_int, + x_max = total_length, + x_min = minimum_volume_b / area_b, + x_damp = damping_volume_b / area_b, + perimeter = perimeter_b, + shape_factor = shape_factor_b, + head_factor = head_factor_b, + Cd, + Cd_reverse, + d, + p_int = p_b_int) + mass = Mass(; m, g) + port_a = HydraulicPort() + port_b = HydraulicPort() + flange = MechanicalPort() + end + + eqs = [connect(vol_a.port, port_a) + connect(vol_b.port, port_b) + connect(vol_a.flange, vol_b.flange, mass.flange, flange) + D(x) ~ dx + dx ~ vol_a.flange.v] + + initialization_eqs = [ + mass.s ~ x_int + ] + + System(eqs, t, vars, pars; name, systems, initialization_eqs) +end + +""" + Orifice() + +A valve in fixed position, with parameters for area and the discharge coefficient (fitting the form Effective Area = area x Cd) + +``` + ┌ + │ + ▲ +dm ────► effective area + ▼ + │ + └ +``` + +# Features: +- + +# Parameters: +## volume +- `area`: [m^2] physical area +- `cd`: [unitless] discharge coefficient + +# Connectors: +- `port_a`: hydraulic port +- `port_b`: hydraulic port +""" + +@mtkmodel Orifice begin + @parameters begin + orifice_area = 0.00094 + Cd = 0.6 # TODO Cd here is defined differently from Valve(). + # Here it follows the form Effective Orifice Area = Cd x Physical Orifice Area + # The Valve component should be updated too. + end + @components begin + area = Constant(k = orifice_area) + valve = Valve(Cd = 1 / (Cd * Cd)) + port_a = HydraulicPort() + port_b = HydraulicPort() + end + @equations begin + connect(valve.area, area.output) + connect(valve.port_a, port_a) + connect(valve.port_b, port_b) + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/sources.jl new file mode 100644 index 0000000000..cb032a1099 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/sources.jl @@ -0,0 +1,67 @@ +""" + MassFlow(; name) + +Hydraulic mass flow input source + +# Connectors: + + - `port`: hydraulic port + - `dm`: real input +""" +@mtkmodel MassFlow begin + @components begin + port = HydraulicPort() + dm = RealInput() + end + + @equations begin + port.dm ~ -dm.u + end +end + +""" + FixedPressure(; p, name) + +Fixed pressure source + +# Parameters: +- `p`: [Pa] set pressure (set by `p` argument) + +# Connectors: +- `port`: hydraulic port +""" +@mtkmodel FixedPressure begin + @parameters begin + p + end + + @components begin + port = HydraulicPort() + end + + @equations begin + port.p ~ p + end +end +@deprecate Source FixedPressure + +""" + Pressure(; name) + +input pressure source + +# Connectors: +- `port`: hydraulic port +- `p`: real input +""" +@mtkmodel Pressure begin + @components begin + port = HydraulicPort() + p = RealInput() + end + + @equations begin + port.p ~ p.u + end +end +@deprecate InputSource Pressure diff --git a/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/utils.jl new file mode 100644 index 0000000000..98e269ccf1 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Hydraulic/IsothermalCompressible/utils.jl @@ -0,0 +1,172 @@ +# regPow(x, a, delta = 0.01) = x * (x * x + delta * delta)^((a - 1) / 2); +function regPow(x, a, delta = 0.01) + ifelse(abs(x / delta) >= 1, sign(x) * abs(x / delta)^a * delta^a, (delta^a * x) / delta) +end +regRoot(x, delta = 0.01) = regPow(x, 0.5, delta) + +""" + HydraulicPort(; name) + +Connector port for hydraulic components. + +# States: +- `p`: [Pa] gauge total pressure +- `dm`: [kg/s] mass flow +""" +@connector function HydraulicPort(; name) + pars = @parameters begin + ρ + β + μ + n + let_gas + ρ_gas + p_gas + end + + vars = @variables begin + p(t), [guess = 0] + dm(t), [guess = 0, connect = Flow] + end + + System(Equation[], t, vars, pars; name) +end + +""" + HydraulicFluid(; density = 997, bulk_modulus = 2.09e9, viscosity = 0.0010016, gas_density = 0.0073955, gas_pressure = -1000, n = 1, let_gas = 1, name) + +Fluid parameter setter for isothermal compressible fluid domain. Defaults given for water at 20°C and 0Pa gage (1atm absolute) reference pressure. Density is modeled using the Tait equation of state. For pressures below the reference pressure, density is linearly interpolated to the gas state (when `let_gas` is set to 1), this helps prevent pressures from going below the reference pressure. + +# Parameters: + +- `ρ`: [kg/m^3] fluid density at 0Pa reference gage pressure (set by `density` argument) +- `Β`: [Pa] fluid bulk modulus describing the compressibility (set by `bulk_modulus` argument) +- `μ`: [Pa*s] or [kg/m-s] fluid dynamic viscosity (set by `viscosity` argument) +- `n`: density exponent +- `let_gas`: set to 1 to allow fluid to transition from liquid to gas (for density calculation only) +- `ρ_gas`: [kg/m^3] density of fluid in gas state at reference gage pressure `p_gas` (set by `gas_density` argument) +- `p_gas`: [Pa] reference pressure (set by `gas_pressure` argument) +""" +@connector function HydraulicFluid(; density = 997, bulk_modulus = 2.09e9, + viscosity = 0.0010016, gas_density = 0.0073955, + gas_pressure = -1000, n = 1, let_gas = 1, name) + pars = @parameters begin + ρ = density + β = bulk_modulus + μ = viscosity + n = n + let_gas = let_gas + ρ_gas = gas_density + p_gas = gas_pressure + end + + vars = @variables begin + dm(t), [guess = 0, connect = Flow] + end + + eqs = [ + dm ~ 0 + ] + + System(eqs, t, vars, pars; name) +end + +function transition(x1, x2, y1, y2, x) + u = (x - x1) / (x2 - x1) + blend = u^2 * (3 - 2 * u) + return (1 - blend) * y1 + blend * y2 +end + +f_laminar(shape_factor, Re) = shape_factor * regPow(Re, -1, 0.1) #regPow used to avoid dividing by 0, min value is 0.1 +f_turbulent(shape_factor, Re) = (shape_factor / 64) / (0.79 * log(Re) - 1.64)^2 + +""" + friction_factor(dm, area, d_h, viscosity, shape_factor) + +Calculates the friction factor ``f`` for fully developed flow in a tube such that ``Δp = f \\cdot \\rho \\frac{u^2}{2} \\frac{l}{d_h}`` where + +- ``Δp``: [Pa] is the pressure difference over the tube length ``l`` +- ``\\rho``: [kg/m^3] is the average fluid density +- ``u``: [m/s] is the average fluid velocity +- ``l``: [m] is the tube length + +The friction factor is calculated for laminar and turbulent flow with a transition region between Reynolds number 2000 to 3000. Turbulent flow equation is for smooth tubes, valid for the Reynolds number range up to 5e6. + +# Arguments: + +- `dm`: [kg/s] mass flow +- `area`: [m^2] tube cross sectional area +- `d_h`: [m] tube hydraulic diameter. For circular tubes d_h is the tube diameter, otherwise it can be found from `4*area/perimeter` +- `density`: [kg/m^3] fluid density +- `viscosity`: [Pa*s] or [kg/m-s] fluid dynamic viscosity +- `shape_factor`: the constant defining the laminar fully developed constant f*Re related to the shape of the tube cross section + +Reference: Introduction to Fluid Mechanics, Fox & McDonald, 5th Edition, equations 8.19 and 8.21 +""" +function friction_factor(dm, area, d_h, viscosity, shape_factor) + # u = abs(dm) / (density * area) + # Re = density * u * d_h / viscosity + + Re = abs(dm) * d_h / (area * viscosity) + + if Re <= 2000 + return f_laminar(shape_factor, Re) + elseif 2000 < Re < 3000 + return transition(2000, 3000, f_laminar(shape_factor, Re), + f_turbulent(shape_factor, Re), Re) + else + return f_turbulent(shape_factor, Re) + end +end +@register_symbolic friction_factor(dm, area, d_h, viscosity, shape_factor) +Symbolics.derivative(::typeof(friction_factor), args, ::Val{1}) = 0 +Symbolics.derivative(::typeof(friction_factor), args, ::Val{4}) = 0 + +density_ref(port) = port.ρ +density_exp(port) = port.n +gas_density_ref(port) = port.ρ_gas +gas_pressure_ref(port) = port.p_gas +bulk_modulus(port) = port.β +viscosity(port) = port.μ + +function liquid_density(port, p) + density_ref(port) * + regPow(1 + density_exp(port) * p / bulk_modulus(port), 1 / density_exp(port)) +end #Tait-Murnaghan equation of state +liquid_density(port) = liquid_density(port, port.p) + +# p = beta*(rho/rho_0 - 1) +# (p/beta + 1)*rho_0 = rho + +function liquid_pressure(port, rho) + (rho / density_ref(port) - 1) * bulk_modulus(port) +end + +function gas_density(port, p) + slope = (density_ref(port) - gas_density_ref(port)) / (0 - gas_pressure_ref(port)) + b = density_ref(port) + + return b + p * slope +end + +function gas_pressure(port, rho) + slope = (0 - gas_pressure_ref(port)) / (density_ref(port) - gas_density_ref(port)) + b = 0 + + return b + rho * slope +end + +function full_density(port, p) + ifelse(port.let_gas == 1, + ifelse(p >= 0, liquid_density(port, p), gas_density(port, p)), + liquid_density(port, p)) +end +full_density(port) = full_density(port, port.p) + +function full_pressure(port, rho) + ifelse(port.let_gas == 1, + ifelse( + rho >= density_ref(port), liquid_pressure(port, rho), gas_pressure(port, rho)), + liquid_pressure(port, rho) + ) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/FluxTubes.jl b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/FluxTubes.jl new file mode 100644 index 0000000000..08dd00120f --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/FluxTubes.jl @@ -0,0 +1,16 @@ +module FluxTubes +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +using ...Electrical: Pin + +export PositiveMagneticPort, NegativeMagneticPort, TwoPort +include("utils.jl") + +export Ground, Idle, Short, Crossing, ConstantPermeance, ConstantReluctance, EddyCurrent, + ElectroMagneticConverter +include("basic.jl") + +export ConstantMagneticPotentialDifference, ConstantMagneticFlux +include("sources.jl") + +end #module diff --git a/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/basic.jl b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/basic.jl new file mode 100644 index 0000000000..3c5c4b0c1b --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/basic.jl @@ -0,0 +1,158 @@ +""" + Ground(; name) + +Zero magnetic potential. +""" +@mtkmodel Ground begin + @components begin + port = PositiveMagneticPort() + end + @equations begin + port.V_m ~ 0 + end +end + +""" + Idle(;name) + +Idle running branch. +""" +@mtkmodel Idle begin + @extend (Phi,) = two_port = TwoPort() + @equations begin + Phi ~ 0 + end +end + +""" + Short(;name) + +Short cut branch. +""" +@mtkmodel Short begin + @extend (V_m,) = two_port = TwoPort() + @equations begin + V_m ~ 0 + end +end + +""" + Crossing(;name) + +Crossing of two branches. + +This is a simple crossing of two branches. The ports port_p1 and port_p2 are connected, as well as port_n1 and port_n2. +""" +@mtkmodel Crossing begin + @components begin + port_p1 = PositiveMagneticPort() + port_p2 = PositiveMagneticPort() + port_n1 = NegativeMagneticPort() + port_n2 = NegativeMagneticPort() + end + @equations begin + connect(port_p1, port_p2) + connect(port_n1, port_n2) + end +end + +""" + ConstantPermeance(; name, G_m = 1.0) + +Constant permeance. + +# Parameters: + + - `G_m`: [H] Magnetic permeance +""" +@mtkmodel ConstantPermeance begin + @extend V_m, Phi = two_port = TwoPort() + @parameters begin + G_m = 1.0, [description = "Magnetic permeance"] + end + @equations begin + Phi ~ G_m * V_m + end +end + +""" + ConstantReluctance(; name, R_m = 1.0) + +Constant reluctance. + +# Parameters: + + - `R_m`: [H^-1] Magnetic reluctance +""" +@mtkmodel ConstantReluctance begin + @extend V_m, Phi = two_port = TwoPort(; Phi = 0.0) + @parameters begin + R_m = 1.0, [description = "Magnetic reluctance"] + end + @equations begin + V_m ~ Phi * R_m + end +end + +""" + ElectroMagneticConverter(; name, N, Phi) + +Ideal electromagnetic energy conversion. + +The electromagnetic energy conversion is given by Ampere's law and Faraday's law respectively +V_m = N * i +N * dΦ/dt = -v + +Initial magnetic flux flowing into the port_p can be set with `Phi` ([Wb]) + +# Parameters: + + - `N`: Number of turns +""" +@mtkmodel ElectroMagneticConverter begin + @parameters begin + N, [description = "Number of turns"] + end + @variables begin + v(t) + i(t) + end + @extend V_m, Phi = two_port = TwoPort(; Phi) + @components begin + p = Pin() + n = Pin() + end + @equations begin + v ~ p.v - n.v + 0 ~ p.i + n.i + i ~ p.i + #converter equations: + V_m ~ i * N # Ampere's law + D(Phi) ~ -v / N + end +end + +""" + EddyCurrent(;name, Phi, rho = 0.098e-6, l = 1, A = 1) + +For modelling of eddy current in a conductive magnetic flux tube. +Initial magnetic flux flowing into the port_p can be set with `Phi` ([`Wb`]) + +# Parameters: + + - `rho`: [ohm * m] Resistivity of flux tube material (default: Iron at 20degC) + - `l`: [m] Average length of eddy current path + - `A`: [m^2] Cross sectional area of eddy current path +""" +@mtkmodel EddyCurrent begin + @parameters begin + rho = 0.098e-6, [description = "Resistivity of flux tube material"] + l = 1, [description = "Average length of eddy current path"] + A = 1, [description = "Cross sectional area of eddy current path"] + R = rho * l / A # Electrical resistance of eddy current path + end + @extend (V_m, Phi) = two_port = TwoPort(; Phi) + @equations begin + D(Phi) ~ V_m * R + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/sources.jl new file mode 100644 index 0000000000..d2c5f8dfe7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/sources.jl @@ -0,0 +1,53 @@ +""" + ConstantMagneticPotentialDifference(; name, V_m = 0.0) + +Constant magnetomotive force. + +Parameters: + + - `V_m`: [A] Magnetic potential difference +""" +@mtkmodel ConstantMagneticPotentialDifference begin + @components begin + port_p = PositiveMagneticPort() + port_n = NegativeMagneticPort() + end + @parameters begin + V_m = 0.0, [description = "Magnetic potential difference"] + end + @variables begin + Phi(t) + end + @equations begin + V_m ~ port_p.V_m - port_n.V_m + Phi ~ port_p.Phi + 0 ~ port_p.Phi + port_n.Phi + end +end + +""" + ConstantMagneticFlux(; name, Phi = 0.0) + +Source of constant magnetic flux. + +Parameters: + + - `Phi`: [Wb] Magnetic flux +""" +@mtkmodel ConstantMagneticFlux begin + @components begin + port_p = PositiveMagneticPort() + port_n = NegativeMagneticPort() + end + @parameters begin + Phi = 0.0, [description = "Magnetic flux"] + end + @variables begin + V_m(t) + end + @equations begin + V_m ~ port_p.V_m - port_n.V_m + Phi ~ port_p.Phi + 0 ~ port_p.Phi + port_n.Phi + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/utils.jl new file mode 100644 index 0000000000..a7d1027be0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Magnetic/FluxTubes/utils.jl @@ -0,0 +1,41 @@ +@connector MagneticPort begin + V_m(t), [description = "Magnetic potential at the port"] + Phi(t), [connect = Flow, description = "Magnetic flux flowing into the port"] +end +Base.@doc "Port for a Magnetic system." MagneticPort + +""" +Positive magnetic port +""" +const PositiveMagneticPort = MagneticPort + +""" +Negative magnetic port +""" +const NegativeMagneticPort = MagneticPort + +""" + TwoPort(; name, V_m = 0.0, Phi = 0.0) + +Partial component with magnetic potential difference between two magnetic ports p and n and magnetic flux Phi from p to n. + +# Parameters: + + - `V_m`: Initial magnetic potential difference between both ports + - `Phi`: Initial magnetic flux from port_p to port_n +""" +@mtkmodel TwoPort begin + @components begin + port_p = PositiveMagneticPort() + port_n = NegativeMagneticPort() + end + @variables begin + V_m(t) + Phi(t) + end + @equations begin + V_m ~ port_p.V_m - port_n.V_m + Phi ~ port_p.Phi + 0 ~ port_p.Phi + port_n.Phi + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Magnetic/Magnetic.jl b/lib/ModelingToolkitStandardLibrary/src/Magnetic/Magnetic.jl new file mode 100644 index 0000000000..4eefda950c --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Magnetic/Magnetic.jl @@ -0,0 +1,12 @@ +module Magnetic + +using ModelingToolkit + +# FluxTubes +include("FluxTubes/FluxTubes.jl") + +# QuasiStatic + +# FundamentalWave + +end #module diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Mechanical.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Mechanical.jl new file mode 100644 index 0000000000..95c9e0204c --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Mechanical.jl @@ -0,0 +1,14 @@ +""" +Library of mechanical models. +""" +module Mechanical + +using ModelingToolkit + +include("Rotational/Rotational.jl") +include("Translational/Translational.jl") +include("TranslationalPosition/TranslationalPosition.jl") +include("TranslationalModelica/TranslationalModelica.jl") +include("MultiBody2D/MultiBody2D.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/MultiBody2D.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/MultiBody2D.jl new file mode 100644 index 0000000000..ab0e3e6c07 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/MultiBody2D.jl @@ -0,0 +1,10 @@ +module MultiBody2D + +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ..TranslationalPosition + +export Link +include("components.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/components.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/components.jl new file mode 100644 index 0000000000..2e45de98fe --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/MultiBody2D/components.jl @@ -0,0 +1,88 @@ +@mtkmodel Link begin + @parameters begin + m + l + I + g + x1_0 = 0.0 + y1_0 = 0.0 + end + + @variables begin + (A(t)), [state_priority = 10] + (dA(t)), [state_priority = 10] + (ddA(t)), [state_priority = 10] + + fx1(t) + fy1(t) + + fx2(t) + fy2(t) + + x1(t) = x1_0 + dx1(t) + + y1(t) = y1_0 + dy1(t) + + x2(t) = l + x1_0 + dx2(t) + + y2(t) + dy2(t) + + x_cm(t) = l / 2 + x1_0 + dx_cm(t) + ddx_cm(t) + + y_cm(t) + dy_cm(t) + ddy_cm(t) + end + + @components begin + TX1 = Flange() + TY1 = Flange() + + TX2 = Flange() + TY2 = Flange() + end + + @equations begin + D(A) ~ dA + D(dA) ~ ddA + D(x1) ~ dx1 + D(y1) ~ dy1 + D(x2) ~ dx2 + D(y2) ~ dy2 + D(x_cm) ~ dx_cm + D(dx_cm) ~ ddx_cm + D(y_cm) ~ dy_cm + D(dy_cm) ~ ddy_cm + + # x forces + m * ddx_cm ~ fx1 + fx2 + + # y forces + m * ddy_cm ~ m * g + fy1 + fy2 + + # torques + I * ddA ~ + -fy1 * (x2 - x1) / 2 + fy2 * (x2 - x1) / 2 + fx1 * (y2 - y1) / 2 - + fx2 * (y2 - y1) / 2 + + # geometry + x2 ~ l * cos(A) + x1 + y2 ~ l * sin(A) + y1 + x_cm ~ l * cos(A) / 2 + x1 + y_cm ~ l * sin(A) / 2 + y1 + TX1.f ~ fx1 + TX1.s ~ x1 + TY1.f ~ fy1 + TY1.s ~ y1 + TX2.f ~ fx2 + TX2.s ~ x2 + TY2.f ~ fy2 + TY2.s ~ y2 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/Rotational.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/Rotational.jl new file mode 100644 index 0000000000..0483c0654b --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/Rotational.jl @@ -0,0 +1,23 @@ +""" +Library to model 1-dimensional, rotational mechanical systems +""" +module Rotational + +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ...Blocks: RealInput, RealOutput +import ...@symcheck + +export Flange, Support +include("utils.jl") + +export Fixed, Inertia, Spring, Damper, SpringDamper, IdealGear, RotationalFriction +include("components.jl") + +export Torque, ConstantTorque, Speed, Position +include("sources.jl") + +export AngleSensor, SpeedSensor, TorqueSensor, RelSpeedSensor +include("sensors.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/components.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/components.jl new file mode 100644 index 0000000000..f734c1862c --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/components.jl @@ -0,0 +1,269 @@ +""" + Fixed(;name, phi0 = 0.0) + +Flange fixed in housing at a given angle. + +# Connectors: + + - `flange` [Flange](@ref) + +# Parameters: + + - `phi0`: [`rad`] Fixed offset angle of housing +""" +@mtkmodel Fixed begin + @components begin + flange = Flange() + end + @parameters begin + phi0 = 0.0, [description = "Fixed offset angle of flange"] + end + @equations begin + flange.phi ~ phi0 + end +end + +""" + Inertia(;name, J, phi = 0.0, w = 0.0, a = 0.0) + +1D-rotational component with inertia. + +# States: + + - `phi`: [`rad`] Absolute rotation angle of component + - `w`: [`rad/s`] Absolute angular velocity of component (= D(phi)) + - `a`: [`rad/s²`] Absolute angular acceleration of component (= D(w)) + +# Connectors: + + - `flange_a` [Flange](@ref) Left flange + - `flange_b` [Flange](@ref) Right flange + +# Parameters: + + - `J`: [`kg·m²`] Moment of inertia +""" +@mtkmodel Inertia begin + @parameters begin + J, [description = "Moment of inertia"] + end + @components begin + flange_a = Flange() + flange_b = Flange() + end + begin + @symcheck J > 0 || throw(ArgumentError("Expected `J` to be positive")) + end + @variables begin + phi(t), [description = "Absolute rotation angle", guess = 0.0] + w(t), [description = "Absolute angular velocity", guess = 0.0] + a(t), [description = "Absolute angular acceleration", guess = 0.0] + end + @equations begin + phi ~ flange_a.phi + phi ~ flange_b.phi + D(phi) ~ w + D(w) ~ a + J * a ~ flange_a.tau + flange_b.tau + end +end + +""" + Spring(; name, c, phi_rel0 = 0.0) + +Linear 1D rotational spring + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle (`flange_b.phi - flange_a.phi`) + - `tau(t)`: [`N.m`] Torque between flanges (`flange_b.tau`) + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + +# Parameters: + + - `c`: [`N.m/rad`] Spring constant + - `phi_rel0`: [`rad`] Unstretched spring angle. Defaults to 0.0. +""" +@mtkmodel Spring begin + @extend phi_rel, tau = partial_comp = PartialCompliant() + begin + @symcheck c > 0 || throw(ArgumentError("Expected `c` to be positive")) + end + @parameters begin + c, [description = "Spring constant"] + phi_rel0 = 0.0, [description = "Unstretched spring angle"] + end + @equations begin + tau ~ c * (phi_rel - phi_rel0) + end +end + +""" + Damper(; name, d) + +Linear 1D rotational damper + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle (= flange_b.phi - flange_a.phi) + - `w_rel(t)`: [`rad/s`] Relative angular velocity (= D(phi_rel)) + - `a_rel(t)`: [`rad/s²`] Relative angular acceleration (= D(w_rel)) + - `tau(t)`: [`N.m`] Torque between flanges (= flange_b.tau) + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + +# Parameters: + + - `d`: [`N.m.s/rad`] Damping constant +""" +@mtkmodel Damper begin + @extend w_rel, tau = partial_comp = PartialCompliantWithRelativeStates() + begin + @symcheck d > 0 || throw(ArgumentError("Expected `d` to be positive")) + end + @parameters begin + d, [description = "Damping constant"] + end + @equations begin + tau ~ d * w_rel + end +end +""" + SpringDamper(; name, d) + +Linear 1D rotational spring and damper + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle (= flange_b.phi - flange_a.phi) + - `w_rel(t)`: [`rad/s`] Relative angular velocity (= D(phi_rel)) + - `a_rel(t)`: [`rad/s²`] Relative angular acceleration (= D(w_rel)) + - `tau(t)`: [`N.m`] Torque between flanges (= flange_b.tau) + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + +# Parameters: + + - `d`: [`N.m.s/rad`] Damping constant + - `c`: [`N.m/rad`] Spring constant + - `phi_rel0`: [`rad`] Unstretched spring angle. Defaults to 0.0 +""" +@mtkmodel SpringDamper begin + @extend phi_rel, w_rel, tau = partial_comp = PartialCompliantWithRelativeStates() + @variables begin + tau_c(t), [description = "Spring torque"] + tau_d(t), [description = "Damper torque"] + end + @parameters begin + d, [description = "Damping constant"] + c, [description = "Spring constant"] + phi_rel0 = 0.0, [description = "Unstretched spring angle"] + end + @equations begin + tau_c ~ c * (phi_rel - phi_rel0) + tau_d ~ d * w_rel + tau ~ tau_c + tau_d + end +end + +""" + IdealGear(; name, ratio, use_support = false) + +Ideal gear without inertia. + +This element characterizes any type of gear box which is fixed in the ground and which has one driving shaft and one driven shaft. + +# States: + + - `phi_a(t)`: [`rad`] Relative angle between shaft a and the support + - `phi_b(t)`: [`rad`] Relative angle between shaft b and the support + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + - `support` [Support](@ref) if `use_support == true` + +# Parameters: + + - `ratio`: Transmission ratio (flange_a.phi/flange_b.phi) + - `use_support`: If support flange enabled, otherwise implicitly grounded. By default it is `false` +""" +@mtkmodel IdealGear begin + @extend phi_support, flange_a, + flange_b = partial_element = PartialElementaryTwoFlangesAndSupport2(; + use_support = false) + @parameters begin + ratio, [description = "Transmission ratio"] + end + @variables begin + phi_a(t), + [description = "Relative angle between shaft a and the support", guess = 0.0] + phi_b(t), + [description = "Relative angle between shaft b and the support", guess = 0.0] + end + @equations begin + phi_a ~ flange_a.phi - phi_support + phi_b ~ flange_b.phi - phi_support + phi_a ~ ratio * phi_b + 0 ~ ratio * flange_a.tau + flange_b.tau + end +end + +""" + RotationalFriction(; name, f, tau_c, w_brk, tau_brk) + +Models rotational friction with Stribeck effect, Coulomb friction and viscous friction between the two flanges. +The friction torque is a function of the relative angular velocity between `flange_a` and `flange_b`. + +Friction model: "Armstrong, B. and C.C. de Wit, Friction Modeling and Compensation, The Control Handbook, CRC Press, 1995." + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle `(= flange_b.phi - flange_a.phi)` + - `w_rel(t)`: [`rad/s`] Relative angular velocity `(= D(phi_rel))` + - `a_rel(t)`: [`rad/s²`] Relative angular acceleration `(= D(w_rel))` + - `tau(t)`: [`N.m`] Torque between flanges `(= flange_b.tau)` + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + +# Parameters: + + - `f`: [`N⋅m/(rad/s)`] Viscous friction coefficient + - `tau_c`: [`N⋅m`] Coulomb friction torque + - `w_brk`: [`rad/s`] Breakaway friction velocity + - `tau_brk`: [`N⋅m`] Breakaway friction torque +""" +@mtkmodel RotationalFriction begin + @extend w_rel, tau = partial_comp = PartialCompliantWithRelativeStates() + @parameters begin + f, [description = "Viscous friction coefficient"] + tau_c, [description = "Coulomb friction torque"] + w_brk, [description = "Breakaway friction velocity"] + tau_brk, [description = "Breakaway friction torque"] + end + + begin + str_scale = sqrt(2 * exp(1)) * (tau_brk - tau_c) + w_st = w_brk * sqrt(2) + w_coul = w_brk / 10 + end + @equations begin + tau ~ + str_scale * (exp(-(w_rel / w_st)^2) * w_rel / w_st) + + tau_c * tanh(w_rel / w_coul) + f * w_rel # Stribeck friction + Coulomb friction + Viscous friction + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sensors.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sensors.jl new file mode 100644 index 0000000000..258552e8d5 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sensors.jl @@ -0,0 +1,92 @@ +""" + AngleSensor(; name) + +Ideal sensor to measure the absolute flange angle + +# Connectors: + + - `flange`: [Flange](@ref) Flange of shaft from which sensor information shall be measured + - `phi`: [RealOutput](@ref) Absolute angle of flange +""" +@mtkmodel AngleSensor begin + @components begin + flange = Flange() + phi = RealOutput() + end + @equations begin + phi.u ~ flange.phi + flange.tau ~ 0 + end +end + +""" + SpeedSensor(; name) + +Ideal sensor to measure the absolute flange angular velocity + +# Connectors: + + - `flange`: [Flange](@ref) Flange of shaft from which sensor information shall be measured + - `w`: [RealOutput](@ref) Absolute angular velocity of flange +""" +@mtkmodel SpeedSensor begin + @components begin + flange = Flange() + w = RealOutput() + end + @equations begin + D(flange.phi) ~ w.u + flange.tau ~ 0 + end +end + +""" + TorqueSensor(;name) + +Ideal sensor to measure the torque between two flanges (`= flange_a.tau`) + +# Connectors: + + - `flange_a`: [Flange](@ref) Left flange of shaft + - `flange_b`: [Flange](@ref) Left flange of shaft + - `tau`: [RealOutput](@ref) Torque in flange flange_a and flange_b (`tau = flange_a.tau = -flange_b.tau`) +""" +@mtkmodel TorqueSensor begin + @components begin + flange_a = Flange() + flange_b = Flange() + tau = RealOutput() + end + @equations begin + flange_a.phi ~ flange_b.phi + tau.u ~ flange_a.tau + end +end + +""" + RelSpeedSensor(; name) + +Ideal sensor to measure the relative angular velocity + +# Connectors: + + - `flange_a`: [Flange](@ref) Flange of shaft from which sensor information shall be measured + - `flange_b`: [Flange](@ref) Flange of shaft from which sensor information shall be measured + - `w`: [RealOutput](@ref) Absolute angular velocity of flange +""" +@mtkmodel RelSpeedSensor begin + @components begin + flange_a = Flange() + flange_b = Flange() + w_rel = RealOutput() + end + @variables begin + phi_rel(t), [guess = 0.0] + end + @equations begin + 0 ~ flange_a.tau + flange_b.tau + phi_rel ~ flange_b.phi - flange_a.phi + D(phi_rel) ~ w_rel.u + 0 ~ flange_a.tau + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sources.jl new file mode 100644 index 0000000000..a93496292f --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/sources.jl @@ -0,0 +1,181 @@ +@mtkmodel PartialTorque begin + @extend flange, + phi_support = partial_element = PartialElementaryOneFlangeAndSupport2(; + use_support = false) + @variables begin + phi(t), + [ + description = "Angle of flange with respect to support (= flange.phi - support.phi)" + ] + end + @equations begin + phi ~ flange.phi - phi_support + end +end + +""" + Torque(; name, use_support = false) + +Input signal acting as external torque on a flange + +# States: + + - `phi_support(t)`: [`rad`] Absolute angle of support flange + +# Connectors: + + - `flange` [Flange](@ref) + - `tau` [RealInput](@ref) Accelerating torque acting at flange `-flange.tau` + +# Parameters: + + - `use_support` +""" +@mtkmodel Torque begin + @extend (flange,) = partial_element = PartialElementaryOneFlangeAndSupport2(; + use_support = false) + @components begin + tau = RealInput() + end + @equations begin + flange.tau ~ -tau.u + end +end + +""" + ConstantTorque(; name, tau_constant, use_support = false) + +Constant torque source + +# State variables: + +- `phi_support(t)`: [`rad`] Absolute angle of support flange, only available if `use_support = true` +- `tau`: Accelerating torque acting at flange (= -flange.tau) +- `w`: Angular velocity of flange with respect to support (= der(phi)) + +# Connectors: +- `flange` [Flange](@ref) + +# Arguments: +- `tau_constant`: The constant torque applied by the source +- `use_support`: Whether or not an internal support flange is added. By default, it is `false` +""" +@mtkmodel ConstantTorque begin + @parameters begin + tau_constant, + [ + description = "Constant torque (if negative, torque is acting as load in positive direction of rotation)" + ] + end + @extend flange, phi = partial_element = PartialTorque(; use_support = false) + @variables begin + tau(t), [description = "Accelerating torque acting at flange (= -flange.tau)"] + w(t), + [description = "Angular velocity of flange with respect to support (= der(phi))"] + end + @equations begin + w ~ D(phi) + tau ~ -flange.tau + tau ~ tau_constant + end +end + +""" + Speed(; name, use_support = false, exact = false, f_crit = 50) + +Forced movement of a flange according to a reference angular velocity signal + +# States: + + - `phi_support(t)`: [`rad`] Absolute angle of support flange" + +# Connectors: + + - `flange` [Flange](@ref) + - `w_ref` [RealInput](@ref) Reference angular velocity of flange with respect to support as input signal needs to be continuously differential + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded + - `exact`: true/false exact treatment/filtering the input signal + - `tau_filt`: [`rad/s`] if exact=false, Time constant of low-pass filter to filter input signal +""" +@component function Speed(; name, use_support = false, exact = false, tau_filt = 50) + @named partial_element = PartialElementaryOneFlangeAndSupport2(use_support = use_support) + @unpack flange, phi_support = partial_element + @named w_ref = RealInput() + @variables phi(t) [guess = 0.0] w(t) [guess = 0.0] a(t) [guess = 0.0] + eqs = [phi ~ flange.phi - phi_support + D(phi) ~ w] + if exact + pars = [] + push!(eqs, w ~ w_ref.u) + push!(eqs, a ~ 0) + else + pars = @parameters tau_filt = tau_filt + push!(eqs, D(w) ~ a) + push!(eqs, a ~ (w_ref.u - w) * tau_filt) + end + return extend(System(eqs, t, [phi, w, a], pars; name = name, systems = [w_ref]), + partial_element) +end + +""" + Position(; name, exact = false, f_crit = 50, use_support = false) + +Forced movement of a flange according to a reference angle signal. + +The input signal `phi_ref` defines the reference angle in [rad]. Flange is forced to move according to this reference motion relative to flange support. According to parameter `exact` (default = `false`), this is done in the following way: + +- `exact=true`: The reference angle is treated exactly. This is only possible if the input signal is defined by an analytical function that can be differentiated at least twice in order to compute the acceleration. +- `exact=false`: The reference angle is filtered and the second derivative of the filtered curve is used to compute the reference acceleration of the flange. This second derivative is not computed by numerical differentiation but by an appropriate realization of the filter. For filtering, a second-order Bessel filter is used. The critical frequency (also called cut-off frequency) of the filter is defined via parameter `f_crit` in [Hz]. This value should be selected in such a way that it is higher than the essential low frequencies in the signal. + +# Connectors +- `flange::Flange`: Flange to be moved +- `phi_ref::RealInput`: Reference angle of flange with respect to support + +# Variables +- `phi(t)`: Rotation angle of flange with respect to support +- `w(t)`: If `exact=false`, Angular velocity of flange with respect to support +- `a(t)`: If `exact=false`, Angular acceleration of flange with respect to support + +# Parameters +- `exact`: (structural) true/false exact treatment/filtering the input signal +- `f_crit`: [Hz] if `exact=false`, Critical frequency of filter to filter input signal +""" +@component function Position(; name, exact = false, f_crit = 50, use_support = false) + systems = @named begin + partial_element = PartialElementaryOneFlangeAndSupport2(; use_support) + phi_ref = RealInput() + end + @unpack flange, phi_support = partial_element + + pars = @parameters begin + f_crit = f_crit, [description = "Critical frequency of input-signal filter"] + end + + w_crit = 2 * π * f_crit + af = 1.3617 # s coefficient of Bessel filter + bf = 0.6180 # s*s coefficient of Bessel filter + + vars = @variables begin + phi(t), + [guess = 0.0, description = "Rotation angle of flange with respect to support"] + w(t), + [guess = 0.0, description = "Angular velocity of flange with respect to support"] + a(t), + [guess = 0.0, + description = "Angular acceleration of flange with respect to support"] + end + + equations = if exact + [phi ~ flange.phi - phi_support + phi ~ phi_ref.u] + else + [phi ~ flange.phi - phi_support + D(phi) ~ w + D(w) ~ a + a ~ ((phi_ref.u - phi) * w_crit - af * w) * (w_crit / bf)] + end + extend(System(equations, t; name, systems = [phi_ref]), partial_element) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/utils.jl new file mode 100644 index 0000000000..f3f8342758 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Rotational/utils.jl @@ -0,0 +1,189 @@ +@connector Flange begin + phi(t), [description = "Rotation angle of flange"] + tau(t), [connect = Flow, description = "Cut torque in flange"] +end + +Base.@doc """ + Support(;name) + +1-dim. rotational flange of a shaft. + +# States: +- `phi(t)`: [`rad`] Absolute rotation angle of flange +- `tau(t)`: [`N.m`] Cut torque in the flange +""" Flange + +@connector Support begin + phi(t), [description = "Rotation angle of flange"] + tau(t), [connect = Flow, description = "Cut torque in flange"] +end + +# Base.@doc """ +# InternalSupport(;name, tau) + +# 1-dim. rotational flange of a shaft. + +# - `tau`: External support torque (must be computed via torque balance in model where InternalSupport is used; = flange.tau) + +# # States: +# - `phi(t)`: [`rad`] Absolute rotation angle of flange +# - `tau(t)`: [`N.m`] Cut torque in the flange +# """ Flange + +# @connector function InternalSupport(; name, tau) +# @named flange = Flange() +# @variables phi(t)=0 [description = "Rotation angle of support $name"] +# # tau(t), [connect = Flow, description = "Cut torque in support $name"],) +# equations = [flange.tau ~ tau +# flange.phi ~ phi] +# System(equations, t, [phi], [], name = name, systems = [flange]) # NOTE: tau not included since it belongs elsewhere +# end + +Base.@doc """ + Support(;name) + +Support/housing of a 1-dim. rotational shaft + +# States: +- `phi(t)`: [`rad`] Absolute rotation angle of the support/housing +- `tau(t)`: [`N.m`] Cut torque in the support/housing +""" Support + +""" + PartialCompliant(; name, phi_rel = 0.0, tau = 0.0) + +Partial model for the compliant connection of two rotational 1-dim. shaft flanges. + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle (`flange_b.phi - flange_a.phi`). It accepts an initial value, which defaults to 0.0. + - `tau(t)`: [`N.m`] Torque between flanges (`flange_b.tau`). It accepts an initial value, which defaults to 0.0. + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + +""" +@mtkmodel PartialCompliant begin + @components begin + flange_a = Flange() + flange_b = Flange() + end + @variables begin + phi_rel(t), [description = "Relative rotation angle between flanges", guess = 0.0] + tau(t), [description = "Torque between flanges", guess = 0.0] + end + @equations begin + phi_rel ~ flange_b.phi - flange_a.phi + flange_b.tau ~ tau + flange_a.tau ~ -tau + end +end + +""" + PartialCompliantWithRelativeStates(; name, phi_rel = 0.0, tau = 0.0) + +Partial model for the compliant connection of two rotational 1-dim. shaft flanges where the relative angle and speed are used as preferred states + +# States: + + - `phi_rel(t)`: [`rad`] Relative rotation angle (= flange_b.phi - flange_a.phi). It accepts an initial value, which defaults to 0.0. + - `w_rel(t)`: [`rad/s`] Relative angular velocity (= D(phi_rel)). It accepts an initial value, which defaults to 0.0. + - `a_rel(t)`: [`rad/s²`] Relative angular acceleration (= D(w_rel)). It accepts an initial value, which defaults to 0.0. + - `tau(t)`: [`N.m`] Torque between flanges (= flange_b.tau). It accepts an initial value, which defaults to 0.0. + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) +""" +@mtkmodel PartialCompliantWithRelativeStates begin + @components begin + flange_a = Flange() + flange_b = Flange() + end + @variables begin + phi_rel(t), [description = "Relative rotation angle between flanges", guess = 0.0] + w_rel(t), [description = "Relative angular velocity between flanges", guess = 0.0] + a_rel(t), + [description = "Relative angular acceleration between flanges", guess = 0.0] + tau(t), [description = "Torque between flanges", guess = 0.0] + end + @equations begin + phi_rel ~ flange_b.phi - flange_a.phi + D(phi_rel) ~ w_rel + D(w_rel) ~ a_rel + flange_b.tau ~ tau + flange_a.tau ~ -tau + end +end + +""" + PartialElementaryOneFlangeAndSupport2(; name, use_support = false) + +Partial model for a component with one rotational 1-dim. shaft flange and a support used for textual modeling, i.e., for elementary models + +# States: + + - `phi_support(t)`: [`rad`] Absolute angle of support flange" + +# Connectors: + + - `flange` [Flange](@ref) + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded +""" +@component function PartialElementaryOneFlangeAndSupport2(; name, use_support = false) + @named flange = Flange() + sys = [flange] + @variables phi_support(t) [ + description = "Absolute angle of support flange", guess = 0.0] + if use_support + @named support = Support() + eqs = [support.phi ~ phi_support + support.tau ~ -flange.tau] + push!(sys, support) + else + eqs = [phi_support ~ 0] + end + return compose(System(eqs, t, [phi_support], []; name = name), sys) +end + +""" + PartialElementaryTwoFlangesAndSupport2(;name, use_support=false) + +Partial model for a component with two rotational 1-dim. shaft flanges and a support used for textual modeling, i.e., for elementary models + +# States: + + - `phi_support(t)`: [`rad`] Absolute angle of support flange + +# Connectors: + + - `flange_a` [Flange](@ref) + - `flange_b` [Flange](@ref) + - `support` [Support](@ref) if `use_support == true` + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded +""" +@component function PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + @named flange_a = Flange() + @named flange_b = Flange() + sys = [flange_a, flange_b] + @variables phi_support(t) [ + description = "Absolute angle of support flange", guess = 0.0] + if use_support + @named support = Support() + eqs = [support.phi ~ phi_support + support.tau ~ -flange_a.tau - flange_b.tau] + push!(sys, support) + else + eqs = [phi_support ~ 0] + end + return compose(System(eqs, t, [phi_support], []; name = name), sys) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/Translational.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/Translational.jl new file mode 100644 index 0000000000..4db9168ad2 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/Translational.jl @@ -0,0 +1,24 @@ +""" +Library to model 1-dimensional, translational mechanical systems +""" +module Translational + +using ModelingToolkit, Symbolics +using ModelingToolkit: getdefault, t_nounits as t, D_nounits as D + +using ModelingToolkitStandardLibrary.Blocks: RealInput, RealOutput +using IfElse: ifelse + +export MechanicalPort +include("utils.jl") + +export Mass, Spring, Damper, Fixed +include("components.jl") + +export Force, Position, Velocity, Acceleration +include("sources.jl") + +export ForceSensor, PositionSensor, AccelerationSensor +include("sensors.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/components.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/components.jl new file mode 100644 index 0000000000..0f7551d82f --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/components.jl @@ -0,0 +1,185 @@ +""" + Free(; name) + +Use to close a system that has un-connected `MechanicalPort`'s where the force should not be zero (i.e. you want to solve for the force to produce the given movement of the port) + +# Connectors: + + - `flange`: 1-dim. translational flange +""" +@mtkmodel Free begin + @components begin + flange = MechanicalPort() + end + @variables begin + f(t) + end + @equations begin + flange.f ~ f + end +end + +""" + Fixed(; name) + +Fixes a flange position (velocity = 0) + +# Connectors: + + - `flange`: 1-dim. translational flange +""" +@mtkmodel Fixed begin + @components begin + flange = MechanicalPort() + end + @equations begin + flange.v ~ 0 + end +end + +""" + Mass(; name, m, g = 0) + +Sliding mass with inertia + +# Parameters: + + - `m`: [kg] mass of sliding body + - `g = 0`: [m/s^2] [m/s²] gravity field acting on the mass, positive value acts in the positive direction + + +# States: + + - `v`: [m/s] absolute linear velocity of sliding mass + - `s`: [m] absolute position of sliding mass (optional with parameter s) + +# Connectors: + + - `flange`: 1-dim. translational flange +""" +@component function Mass(; name, m, g = 0) + pars = @parameters begin + m = m + g = g + end + @named flange = MechanicalPort() + + vars = @variables begin + s(t), [guess = 0] + v(t), [guess = 0] + f(t), [guess = 0] + end + + eqs = [flange.v ~ v + flange.f ~ f + D(s) ~ v + D(v) ~ f / m + g] + + return compose(System(eqs, t, vars, pars; name = name), + flange) +end + +const REL = Val(:relative) + +""" + Spring(; name, k, delta_s = 0.0, va=0.0, v_b_0=0.0) + +Linear 1D translational spring + +# Parameters: + + - `k`: [N/m] Spring constant + - `delta_s`: initial spring stretch + - `va`: [m/s] Initial value of absolute linear velocity at flange_a (default 0 m/s) + - `v_b_0`: [m/s] Initial value of absolute linear velocity at flange_b (default 0 m/s) + +# Connectors: + + - `flange_a`: 1-dim. translational flange on one side of spring + - `flange_b`: 1-dim. translational flange on opposite side of spring +""" +@component function Spring(; name, k) + Spring(REL; name, k) +end # default + +@component function Spring(::Val{:relative}; name, k) + pars = @parameters begin + k = k + end + vars = @variables begin + delta_s(t), [guess = 0] + f(t), [guess = 0] + end + + @named flange_a = MechanicalPort() + @named flange_b = MechanicalPort() + + eqs = [D(delta_s) ~ flange_a.v - flange_b.v + f ~ k * delta_s + flange_a.f ~ +f + flange_b.f ~ -f] + return compose(System(eqs, t, vars, pars; name = name), + flange_a, + flange_b) #flange_a.f => +k*delta_s, flange_b.f => -k*delta_s +end + +const ABS = Val(:absolute) +@component function Spring(::Val{:absolute}; name, k, l = 0) + pars = @parameters begin + k = k + l = l + end + vars = @variables begin + sa(t), [guess = 0] + sb(t), [guess = 0] + f(t), [guess = 0] + end + + @named flange_a = MechanicalPort() + @named flange_b = MechanicalPort() + + eqs = [D(sa) ~ flange_a.v + D(sb) ~ flange_b.v + f ~ k * (sa - sb - l) #delta_s + flange_a.f ~ +f + flange_b.f ~ -f] + return compose(System(eqs, t, vars, pars; name = name), + flange_a, + flange_b) #, flange_a.f => k * (flange_a__s - flange_b__s - l) +end + +""" + Damper(; name, d, flange_a.v = 0.0, flange_b.v = 0.0) + +Linear 1D translational damper + +# Parameters: + + - `d`: [N.s/m] Damping constant + +# Connectors: + + - `flange_a`: 1-dim. translational flange on one side of damper. Initial value of state `v` is set to 0.0 m/s. + - `flange_b`: 1-dim. translational flange on opposite side of damper. Initial value of state `v` is set to 0.0 m/s. +""" +@mtkmodel Damper begin + @parameters begin + d + end + @variables begin + v(t), [guess = 0] + f(t), [guess = 0] + end + + @components begin + flange_a = MechanicalPort() + flange_b = MechanicalPort() + end + + @equations begin + v ~ flange_a.v - flange_b.v + f ~ v * d + flange_a.f ~ +f + flange_b.f ~ -f + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sensors.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sensors.jl new file mode 100644 index 0000000000..2a61d42db0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sensors.jl @@ -0,0 +1,85 @@ +""" + ForceSensor(; name) + +Linear 1D force sensor, measures the force between two flanges. + +# Connectors: + +- `flange`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel ForceSensor begin + @components begin + flange_a = MechanicalPort() + flange_b = MechanicalPort() + output = RealOutput() + end + + @equations begin + flange_a.v ~ flange_b.v + flange_a.f + flange_b.f ~ 0.0 + output.u ~ flange_a.f + end +end + +""" + PositionSensor(; s = 0, name) + +Linear 1D position sensor. + +# States: + +- `s`: [m] absolute position (with initial value of 0.0) + +# Connectors: + +- `flange`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel PositionSensor begin + @components begin + flange = MechanicalPort() + output = RealOutput() + end + + @variables begin + s(t) + end + + @equations begin + D(s) ~ flange.v + output.u ~ s + flange.f ~ 0.0 + end +end + +""" + AccelerationSensor(; name) + +Linear 1D acceleration sensor. + +# States: + +- `a`: [m/s^2] measured acceleration + +# Connectors: + +- `flange`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel AccelerationSensor begin + @components begin + flange = MechanicalPort() + output = RealOutput() + end + + @variables begin + a(t) + end + + @equations begin + a ~ D(flange.v) + output.u ~ a + flange.f ~ 0.0 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sources.jl new file mode 100644 index 0000000000..1f1eed02b3 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/sources.jl @@ -0,0 +1,99 @@ +""" + Force(; name) + +Linear 1D force input source + +# Connectors: + + - `flange`: 1-dim. translational flange + - `f`: real input +""" +@mtkmodel Force begin + @components begin + flange = MechanicalPort(; v = 0.0) + f = RealInput() + end + + @equations begin + flange.f ~ -f.u + end +end + +""" + Position(solves_force = true; name) + +Linear 1D position input source. Set `solves_force=false` to force input force to 0 (i.e. only the position is given, the respective force needed is already provided elsewhere in the model). + +# Connectors: + + - `flange`: 1-dim. translational flange + - `s`: real input +""" +@component function Position(solves_force = true; name) + vars = [] + + systems = @named begin + flange = MechanicalPort(; v = 0) + s = RealInput() + end + + eqs = [ + D(s.u) ~ flange.v + ] + + !solves_force && push!(eqs, 0 ~ flange.f) + + System(eqs, t, vars, []; + name, systems) +end + +""" + Velocity(solves_force = true; name) + +Linear 1D position input source. Set `solves_force=false` to force input force to 0 (i.e. only the velocity is given, the respective force needed is already provided elsewhere in the model). + +# Connectors: + + - `flange`: 1-dim. translational flange + - `v`: real input +""" +@component function Velocity(solves_force = true; name) + systems = @named begin + flange = MechanicalPort(; v = 0) + v = RealInput() + end + + eqs = [ + v.u ~ flange.v + ] + + !solves_force && push!(eqs, 0 ~ flange.f) + + System(eqs, t, [], []; name, systems) +end + +""" +Acceleration(solves_force = true; name) + +Linear 1D position input source. Set `solves_force=false` to force input force to 0 (i.e. only the acceleration is given, the respective force needed is already provided elsewhere in the model). + +# Connectors: + + - `flange`: 1-dim. translational flange + - `a`: real input +""" +@component function Acceleration(solves_force = true; name) + systems = @named begin + flange = MechanicalPort(; v = 0) + a = RealInput() + end + + vars = @variables v(t) + + eqs = [v ~ flange.v + D(v) ~ a.u] + + !solves_force && push!(eqs, 0 ~ flange.f) + + System(eqs, t, vars, []; name, systems) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/utils.jl new file mode 100644 index 0000000000..012cd2c0d1 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/Translational/utils.jl @@ -0,0 +1,13 @@ +@connector MechanicalPort begin + v(t) + f(t), [connect = Flow] +end +Base.@doc """ + MechanicalPort(;name) + +1-dim. rotational flange of a shaft. + +# States: +- `v`: [m/s] velocity of the node +- `f`: [N] force entering the node +""" MechanicalPort diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/TranslationalModelica.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/TranslationalModelica.jl new file mode 100644 index 0000000000..fd3ad41a10 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/TranslationalModelica.jl @@ -0,0 +1,19 @@ +""" +Library to model 1-dimensional, translational mechanical components. +""" +module TranslationalModelica + +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ...Blocks: RealInput, RealOutput + +export Flange +include("utils.jl") + +export Fixed, Mass, Spring, Damper, SpringDamper +include("components.jl") + +export Force, Position +include("sources.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/components.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/components.jl new file mode 100644 index 0000000000..e401270abd --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/components.jl @@ -0,0 +1,149 @@ +""" + Fixed(; name, s0 = 0.0) + +Flange fixed in housing at a given position. + +# Parameters: + + - `s0`: [m] Fixed offset position of housing + +# Connectors: + + - `flange: 1-dim. translational flange` +""" +@mtkmodel Fixed begin + @parameters begin + s0 = 0 + end + + @components begin + flange = Flange() + end + + @equations begin + flange.s ~ s0 + end +end + +""" + Mass(; name, m, s, v = 0.0) + +Sliding mass with inertia + +# Parameters: + + - `m`: [kg] Mass of sliding mass + +# States: + + - `s`: [m] Absolute position of sliding mass. It accepts an initial value, which defaults to 0.0. + - `v`: [m/s] Absolute linear velocity of sliding mass (= D(s)). It accepts an initial value, which defaults to 0.0. + +# Connectors: + + - `flange: 1-dim. translational flange of mass` +""" +@mtkmodel Mass begin + @parameters begin + m = 0.0, [description = "Mass of sliding mass [kg]"] + end + @variables begin + v(t), [description = "Absolute linear velocity of sliding mass [m/s]"] + a(t), [description = "Absolute linear acceleration of sliding mass [m/s^2]"] + end + @extend flange_a, flange_b, s = pr = PartialRigid(; L = 0.0, s) + @equations begin + v ~ D(s) + a ~ D(v) + m * a ~ flange_a.f + flange_b.f + end +end + +""" + Spring(; c= 0.0, name, s_rel0 = 0) + +Linear 1D translational spring + +# Parameters: + + - `c`: [N/m] Spring constant + - `s_rel0`: Unstretched spring length + +# Connectors: + + - `flange_a: 1-dim. translational flange on one side of spring` + - `flange_b: 1-dim. translational flange on opposite side of spring` #default function +""" +@mtkmodel Spring begin + @extend flange_a, flange_b, s_rel, f = pc = PartialCompliant() + @parameters begin + c = 0.0, [description = "Spring constant [N/m]"] + s_rel0 = 0.0, [description = "Unstretched spring length [m]"] + end + + @equations begin + f ~ c * (s_rel - s_rel0) + end +end + +""" + Damper(; name, d = 0.0) + +Linear 1D translational damper + +# Parameters: + + - `d`: [N.s/m] Damping constant + +# Connectors: + + - `flange_a: 1-dim. translational flange on one side of damper` + - `flange_b: 1-dim. translational flange on opposite side of damper` +""" +@mtkmodel Damper begin + @extend flange_a, flange_b, v_rel, f = pc = PartialCompliantWithRelativeStates() + @parameters begin + d = 0.0, [description = "Damping constant [Ns/m]"] + end + @variables begin + lossPower(t), [description = "Power dissipated by the damper [W]"] + end + @equations begin + f ~ d * v_rel + lossPower ~ f * v_rel + end +end + +""" + SpringDamper(; name, c = 0.0, d = 0.0, s_rel0 = 0.0) + +Linear 1D translational spring and damper in parallel + +# Parameters: +- `c`: [N/m] Spring constant +- `d`: [N.s/m] Damping constant +- `s_rel0`: Unstretched spring length + +# Connectors: +- `flange_a: 1-dim. translational flange on one side of spring` +- `flange_b: 1-dim. translational flange on opposite side of spring` + +# Variables: +- `lossPower`: [W] Power dissipated by the damper +- `f`: [N] Total force +""" +@mtkmodel SpringDamper begin + @extend flange_a, flange_b, s_rel, v_rel, f = pc = PartialCompliantWithRelativeStates() + @parameters begin + d = 0.0, [description = "Damping constant [Ns/m]"] + c = 0.0, [description = "Spring constant [N/m]"] + s_rel0 = 0.0, [description = "Unstretched spring length [m]"] + end + @variables begin + lossPower(t), [description = "Power dissipated by the damper [W]"] + end + @equations begin + f ~ c * (s_rel - s_rel0) + d * v_rel + lossPower ~ d * v_rel^2 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/sources.jl new file mode 100644 index 0000000000..f3ce5645ca --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/sources.jl @@ -0,0 +1,58 @@ +""" + Force(; name, use_support = false) + +Input signal acting as external force on a flange +""" +@mtkmodel Force begin + @extend (flange,) = partial_element = PartialElementaryOneFlangeAndSupport2(; + use_support = false) + @components begin + f = RealInput() # Accelerating force acting at flange (= -flange.tau) + end + @equations begin + flange.f ~ -f.u + end +end + +""" + Position(; name, exact = false, f_crit = 50) + +Forced movement of a flange according to a reference position + +The input signal `s_ref` defines the reference position in [m]. Flange flange is forced to move relative to the support connector according to this reference motion. According to parameter `exact`, this is done in the following way: + +- `exact=true`: The reference position is treated exactly. This is only possible, if the input signal is defined by an analytical function which can be differentiated at least twice. If this prerequisite is fulfilled, the Modelica translator will differentiate the input signal twice in order to compute the reference acceleration of the flange. +- `exact=false`: The reference position is filtered and the second derivative of the filtered curve is used to compute the reference acceleration of the flange. This second derivative is not computed by numerical differentiation but by an appropriate realization of the filter. For filtering, a second order Bessel filter is used. The critical frequency (also called cut-off frequency) of the filter is defined via parameter `f_crit` in [Hz]. This value should be selected in such a way that it is higher as the essential low frequencies in the signal. + +The input signal can be provided from one of the signal generator blocks of the block library `Blocks.Sources`. +""" +@mtkmodel Position begin + @extend (s,) = ptf = PartialElementaryOneFlangeAndSupport2() + @structural_parameters begin + exact = false + end + @parameters begin + f_crit = 50 + end + @variables begin + v(t) + a(t) + end + @components begin + s_ref = RealInput() + end + begin + w_crit = 2π * f_crit + af = 1.3617 + bf = 0.6180 + end + @equations begin + if exact + s ~ s_ref.u + else + a ~ ((s_ref.u - s) * w_crit - af * v) * (w_crit / bf) + end + v ~ D(s) + a ~ D(v) + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/utils.jl new file mode 100644 index 0000000000..6911485cf7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalModelica/utils.jl @@ -0,0 +1,161 @@ +@connector Flange begin + s(t) + f(t), [connect = Flow] +end +Base.@doc """ + Flange(;name) + +1-dim. translational flange. + +# States: +- `s`: [m] Absolute position of flange +- `f`: [N] Cut force into the flange +""" Flange + +@connector Support begin + s(t) + f(t), [connect = Flow] +end +Base.@doc """ + Support(;name) + +Support/housing 1-dim. translational flange. + +# States: +- `s`: [m] Absolute position of the support/housing +- `f`: [N] Cut force into the flange +""" Support + +@mtkmodel PartialTwoFlanges begin + @components begin + flange_a = Flange() # (left) driving flange (flange axis directed into cut plane, e. g. from left to right) + flange_b = Flange() # (right) driven flange (flange axis directed out of cut plane) + end +end + +""" + PartialCompliant(; name, s_rel = 0.0, f = 0.0) + +Partial model for the compliant connection of two translational 1-dim. flanges. + +# States: + + - `s_rel`: [m] Relative distance (= flange_b.s - flange_a.s). It accepts an initial value, which defaults to 0.0. + - `f`: [N] Force between flanges (= flange_b.f). It accepts an initial value, which defaults to 0.0. +""" +@mtkmodel PartialCompliant begin + @extend (flange_a, flange_b) = pt = PartialTwoFlanges() + @variables begin + s_rel(t), [description = "Relative distance between flanges", guess = 0.0] + f(t), [description = "Force between flanges", guess = 0.0] + end + + @equations begin + s_rel ~ flange_b.s - flange_a.s + flange_b.f ~ +f + flange_a.f ~ -f + end +end + +""" + PartialCompliantWithRelativeStates(;name, s_rel = 0.0, v_rel = 0.0, f = 0.0) + +Partial model for the compliant connection of two translational 1-dim. flanges. + + # States: + + - `s_rel`: [m] Relative distance (= flange_b.phi - flange_a.phi). It accepts an initial value, which defaults to 0.0. + - `v_rel`: [m/s] Relative linear velocity (= der(s_rel)). It accepts an initial value, which defaults to 0.0. + - `f`: [N] Force between flanges (= flange_b.f). It accepts an initial value, which defaults to 0.0. +""" +@mtkmodel PartialCompliantWithRelativeStates begin + @extend flange_a, flange_b = pt = PartialTwoFlanges() + @variables begin + s_rel(t), [description = "Relative distance between flanges"] + v_rel(t), [description = "Relative linear velocity))"] + f(t), [description = "Forces between flanges"] + end + + @equations begin + s_rel ~ flange_b.s - flange_a.s + v_rel ~ D(s_rel) + flange_b.f ~ f + flange_a.f ~ -f + end +end + +""" + PartialElementaryOneFlangeAndSupport2(; name, use_support = false) + +Partial model for a component with one translational 1-dim. shaft flange and a support used for textual modeling, i.e., for elementary models + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded + +# States: + + - `s_support`: [m] Absolute position of support flange" +""" +function PartialElementaryOneFlangeAndSupport2(; name, use_support = false) + @named flange = Flange() + @variables s_support(t) [description = "Absolute position of support flange"] + @variables s(t) [ + description = "Distance between flange and support (= flange.s - support.s)" + ] + eqs = [s ~ flange.s - s_support] + if use_support + @named support = Support() + push!(eqs, support.f ~ -flange.f) + compose(System(eqs, t; name = name), flange, support) + else + push!(eqs, s_support ~ 0) + compose(System(eqs, t; name = name), flange) + end +end + +""" + PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + +Partial model for a component with two translational 1-dim. flanges and a support used for textual modeling, i.e., for elementary models + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded + +# States: + + - `s_support`: [m] Absolute position of support flange" +""" +function PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + @named flange = Flange() + + @variables s_a(t) [description = "Distance between left flange and support"] + @variables s_b(t) [description = "Distance between right flange and support"] + @variables s_support(t) [description = "Absolute position of support flange"] + + eqs = [s_a ~ flange_a.s - s_support + s_b ~ flange_b.s - s_support] + if use_support + @named support = Support() + push!(eqs, support.f ~ -flange_a.f - flange_b.f) + compose(System(eqs, t; name = name), flange, support) + else + push!(eqs, s_support ~ 0) + compose(System(eqs, t; name = name), flange) + end +end + +@mtkmodel PartialRigid begin + @extend flange_a, flange_b = ptf = PartialTwoFlanges() + @variables begin + s(t), [description = "Absolute position of center of component"] + end + @parameters begin + L = 0.0, [description = "Length of component, from left flange to right flange"] + end + @equations begin + flange_a.s ~ s - L / 2 + flange_b.s ~ s + L / 2 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/TranslationalPosition.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/TranslationalPosition.jl new file mode 100644 index 0000000000..c304d6cece --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/TranslationalPosition.jl @@ -0,0 +1,22 @@ +""" +Library to model 1-dimensional, translational mechanical components. +""" +module TranslationalPosition + +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ...Blocks: RealInput, RealOutput + +export Flange +include("utils.jl") + +export Fixed, Mass, Spring, Damper +include("components.jl") + +export Force +include("sources.jl") + +export PositionSensor, ForceSensor, AccelerationSensor +include("sensors.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/components.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/components.jl new file mode 100644 index 0000000000..4ed4bdf9eb --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/components.jl @@ -0,0 +1,175 @@ +""" + Fixed(;name, s_0=0.0) + +Flange fixed in housing at a given position. + +# Parameters: + + - `s_0`: [m] Fixed offset position of housing + +# Connectors: + + - `flange: 1-dim. translational flange` +""" +@mtkmodel Fixed begin + @parameters begin + s_0 = 0 + end + @components begin + flange = Flange() + end + @equations begin + flange.s ~ s_0 + end +end + +""" + Mass(; name, m, s = 0.0, v = 0.0) + +Sliding mass with inertia + +# Parameters: + + - `m`: [kg] Mass of sliding mass + - `s_0`: [m] Initial value of absolute position of sliding mass + - `v_0`: [m/s] Initial value of absolute linear velocity of sliding mass + +# States: + + - `s`: [m] Absolute position of sliding mass + - `v`: [m/s] Absolute linear velocity of sliding mass (= der(s)) + +# Connectors: + + - `flange: 1-dim. translational flange of mass` +""" +@mtkmodel Mass begin + @parameters begin + m + end + @variables begin + s(t) + v(t) + f(t) + end + @components begin + flange = Flange() + end + @equations begin + flange.s ~ s + flange.f ~ f + D(s) ~ v + D(v) ~ f / m + end +end + +const REL = Val(:relative) +@component function Spring(::Val{:relative}; name, k, va = 0.0, vb = 0.0, + delta_s = 0) + pars = @parameters begin + k = k + end + vars = @variables begin + va(t) = va + vb(t) = vb + delta_s(t) = delta_s + f(t) = delta_s * k + end + + @named flange_a = Flange() + @named flange_b = Flange() + + eqs = [D(flange_a.s) ~ va + D(flange_b.s) ~ vb + D(delta_s) ~ va - vb + f ~ k * delta_s + flange_a.f ~ +f + flange_b.f ~ -f] + + return compose( + System(eqs, t, vars, pars; name = name), + flange_a, + flange_b) +end + +const ABS = Val(:absolute) + +""" + Spring(; name, k, l=0) + +Linear 1D translational spring + +# Parameters: + + - `k`: [N/m] Spring constant + - `l`: Unstretched spring length + +# Connectors: + + - `flange_a: 1-dim. translational flange on one side of spring` + - `flange_b: 1-dim. translational flange on opposite side of spring` #default function +""" +function Spring(; name, k, l = 0) + Spring(ABS; name, k, l) +end #default function + +@component function Spring(::Val{:absolute}; + name, k, l = 0) + pars = @parameters begin + k = k + l = l + end + vars = @variables begin + f(t) + end + + @named flange_a = Flange() + @named flange_b = Flange() + + eqs = [ + # delta_s ~ flange_a.s - flange_b.s + f ~ k * (flange_a.s - flange_b.s - l) #delta_s + flange_a.f ~ +f + flange_b.f ~ -f] + return compose(System(eqs, t, vars, pars; name = name), flange_a, flange_b) +end + +""" + Damper(; name, d, va =0.0, vb = 0.0, flange_a.s = 0, flange_b.s = 0) + +Linear 1D translational damper + +# Parameters: + + - `d`: [N.s/m] Damping constant + - `flange_a__s`: [m] Initial value of absolute position of flange_a + - `flange_b__s`: [m] Initial value of absolute position of flange_b + +# Connectors: + + - `flange_a: 1-dim. translational flange on one side of damper` + - `flange_b: 1-dim. translational flange on opposite side of damper` +""" +@mtkmodel Damper begin + @parameters begin + d + end + @variables begin + va(t) + vb(t) + f(t) + end + + @components begin + flange_a = Flange() + flange_b = Flange() + end + + @equations begin + D(flange_a.s) ~ va + D(flange_b.s) ~ vb + f ~ (va - vb) * d + flange_a.f ~ +f + flange_b.f ~ -f + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sensors.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sensors.jl new file mode 100644 index 0000000000..8e264dbb1e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sensors.jl @@ -0,0 +1,81 @@ +""" + ForceSensor(; name) + +Linear 1D force sensor, measures the force between two flanges. + +# Connectors: + +- `flange_a`: 1-dim. translational flange +- `flange_b`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel ForceSensor begin + @components begin + flange_a = Flange() + flange_b = Flange() + output = RealOutput() + end + + @equations begin + flange_a.s ~ flange_b.s + flange_a.f + flange_b.f ~ 0.0 + output.u ~ flange_a.f + end +end + +""" + PositionSensor(; s = 0, name) + +Linear 1D position sensor. + +# States: + +- `s`: [m] absolute position (with initial value of 0.0) + +# Connectors: + +- `flange`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel PositionSensor begin + @components begin + flange = Flange() + output = RealOutput() + end + + @equations begin + output.u ~ flange.s + flange.f ~ 0.0 + end +end + +""" + AccelerationSensor(; name) + +Linear 1D acceleration sensor. + +# States: + +- `a`: [m/s^2] measured acceleration + +# Connectors: + +- `flange`: 1-dim. translational flange +- `output`: real output +""" +@mtkmodel AccelerationSensor begin + @components begin + flange = Flange() + output = RealOutput() + end + + @variables begin + a(t) + end + + @equations begin + a ~ D(D(flange.s)) + output.u ~ a + flange.f ~ 0.0 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sources.jl new file mode 100644 index 0000000000..7c7dacaa8a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/sources.jl @@ -0,0 +1,18 @@ +""" + Force(; name, use_support = false) + +Input signal acting as external force on a flange +""" +@mtkmodel Force begin + @extend (flange,) = partial_element = PartialElementaryOneFlangeAndSupport2(; + use_support = false) + @parameters begin + s = 0 + end + @components begin + f = RealInput() + end + @equations begin + flange.f ~ -f.u + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/utils.jl new file mode 100644 index 0000000000..897a7005fc --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Mechanical/TranslationalPosition/utils.jl @@ -0,0 +1,149 @@ +@connector Flange begin + s(t) + f(t), [connect = Flow] +end +Base.@doc """ + Flange(;name) + +1-dim. translational flange. + +# States: +- `s`: [m] Absolute position of flange +- `f`: [N] Cut force into the flange +""" Flange + +@connector Support begin + s(t) + f(t), [connect = Flow] +end +Base.@doc """ + Support(;name) + +Support/housing 1-dim. translational flange. + +# States: +- `s`: [m] Absolute position of the support/housing +- `f`: [N] Cut force into the flange +""" Support + +""" + PartialCompliant(;name, s_rel_start=0.0, f_start=0.0) + +Partial model for the compliant connection of two translational 1-dim. flanges. + +# States: + + - `s_rel`: [m] Relative distance (= flange_b.s - flange_a.s). It accepts an initial value, which defaults to 0.0. + - `f`: [N] Force between flanges (= flange_b.f). It accepts an initial value, which defaults to 0.0. +""" +@mtkmodel PartialCompliant begin#(; name, s_rel_start = 0.0, f_start = 0.0) + @components begin + flange_a = Flange() + flange_b = Flange() + end + @variables begin + v_a(t) + v_b(t) + s_rel(t) + f(t) + end + @equations begin + D(flange_a.s) ~ v_a + D(flange_b.s) ~ v_b + D(s_rel) ~ v_b - v_a + flange_b.f ~ +f + flange_a.f ~ -f + end +end + +""" + PartialCompliantWithRelativeStates(;name, s_rel_start=0.0, v_rel_start=0.0, a_rel_start=0.0, f_start=0.0) + +Partial model for the compliant connection of two translational 1-dim. flanges. + +# Parameters: + + - `s_rel_start`: [m] Initial relative distance + - `v_rel_start`: [m/s] Initial relative linear velocity (= der(s_rel)) + - `a_rel_start`: [m/s²] Initial relative linear acceleration (= der(v_rel)) + - `f_start`: [N] Initial force between flanges + +# States: + + - `s_rel`: [m] Relative distance (= flange_b.phi - flange_a.phi) + - `v_rel`: [m/s] Relative linear velocity (= der(s_rel)) + - `a_rel`: [m/s²] Relative linear acceleration (= der(v_rel)) + - `f`: [N] Force between flanges (= flange_b.f) +""" +@mtkmodel PartialCompliantWithRelativeStates begin + @components begin + flange_a = Flange() + flange_b = Flange() + end + @variables begin + delta_s(t) + f(t) + end + @equations begin + delta_s ~ flange_a.s - flange_b.s + flange_a.f ~ +f + flange_b.f ~ -f + end +end + +""" + PartialElementaryOneFlangeAndSupport2(;name, use_support=false) + +Partial model for a component with one translational 1-dim. shaft flange and a support used for textual modeling, i.e., for elementary models + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded + +# States: + + - `s_support`: [m] Absolute position of support flange" +""" +@component function PartialElementaryOneFlangeAndSupport2(; name, use_support = false) + @named flange = Flange() + sys = [flange] + @variables s_support(t) + if use_support + @named support = Support() + eqs = [support.s ~ s_support + support.f ~ -flange.f] + push!(sys, support) + else + eqs = [s_support ~ 0] + end + return compose(System(eqs, t, [s_support], []; name = name), sys) +end + +""" + PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + +Partial model for a component with two translational 1-dim. flanges and a support used for textual modeling, i.e., for elementary models + +# Parameters: + + - `use_support`: If support flange enabled, otherwise implicitly grounded. By default it is `false` + +# States: + + - `s_support`: [m] Absolute position of support flange" +""" +@component function PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + @named flange_a = Flange() + @named flange_b = Flange() + sys = [flange_a, flange_b] + @variables s_support(t) + if use_support + @named support = Support() + eqs = [support.s ~ s_support + support.f ~ -flange_a.f - flange_b.f] + push!(sys, support) + else + eqs = [s_support ~ 0] + end + return compose(System(eqs, t, [s_support], []; name = name), sys) +end diff --git a/lib/ModelingToolkitStandardLibrary/src/ModelingToolkitStandardLibrary.jl b/lib/ModelingToolkitStandardLibrary/src/ModelingToolkitStandardLibrary.jl new file mode 100644 index 0000000000..07885fee42 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/ModelingToolkitStandardLibrary.jl @@ -0,0 +1,26 @@ +module ModelingToolkitStandardLibrary +import Symbolics: unwrap + +""" + @symcheck J > 0 || throw(ArgumentError("Expected `J` to be positive")) + +Omits the check expression if the argument `J` is symbolic. +""" +macro symcheck(ex) + ex.args[1].head === :call || + error("Expected an expression on the form sym > val || error()") + sym = ex.args[1].args[2] + quote + _issymbolic(x) = !(unwrap(x) isa Real) + _issymbolic($(esc(sym))) || ($(esc(ex))) + end +end + +include("Blocks/Blocks.jl") +include("Mechanical/Mechanical.jl") +include("Thermal/Thermal.jl") +include("Electrical/Electrical.jl") +include("Magnetic/Magnetic.jl") +include("Hydraulic/Hydraulic.jl") + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/ideal_components.jl b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/ideal_components.jl new file mode 100644 index 0000000000..890382ca36 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/ideal_components.jl @@ -0,0 +1,219 @@ +""" + HeatCapacitor(; name, C, T = 273.15 + 20) + +Lumped thermal element storing heat + +# States: + + - `T`: [`K`] Temperature of element. It accepts an initial value, which defaults to 273.15 + 20. + - `der_T`: [`K/s`] Time derivative of temperature + +# Connectors: + + - `port` + +# Parameters: + + - `C`: [`J/K`] Heat capacity of element (= cp*m) +""" +@mtkmodel HeatCapacitor begin + @components begin + port = HeatPort() + end + @parameters begin + C, [description = "Heat capacity of element"] + end + @variables begin + T(t), [guess = 273.15 + 20] + der_T(t), [guess = 0.0] + end + + @equations begin + T ~ port.T + der_T ~ port.Q_flow / C + D(T) ~ der_T + end +end + +""" + ThermalConductor(; name, G) + +Lumped thermal element transporting heat without storing it. + +# States: + +see [`Element1D`](@ref) + +# Connectors: + +`port_a` +`port_b` + +# Parameters: + + - `G`: [`W/K`] Constant thermal conductance of material +""" +@mtkmodel ThermalConductor begin + @extend Q_flow, dT = element1d = Element1D() + @parameters begin + G + end + @equations begin + Q_flow ~ G * dT + end +end + +""" + ThermalResistor(; name, R) + +Lumped thermal element transporting heat without storing it. + +# States: + + - `dT`: [`K`] Temperature difference across the component a.T - b.T + - `Q_flow`: [`W`] Heat flow rate from port a -> port b + +# Connectors: + + - `port_a` + - `port_b` + +# Parameters: + + - `R`: [`K/W`] Constant thermal resistance of material +""" +@mtkmodel ThermalResistor begin + @extend Q_flow, dT = element1d = Element1D() + @parameters begin + R + end + @equations begin + dT ~ R * Q_flow + end +end + +""" + ConvectiveConductor(; name, G) + +Lumped thermal element for heat convection. + +# States: + + - `dT`: [`K`] Temperature difference across the component `solid.T` - `fluid.T` + - `Q_flow`: [`W`] Heat flow rate from `solid` -> `fluid` + +# Connectors: + + - `solid` + - `fluid` + +# Parameters: + + - `G`: [W/K] Convective thermal conductance +""" +@mtkmodel ConvectiveConductor begin + @extend Q_flow, dT = convective_element1d = ConvectiveElement1D() + @parameters begin + G + end + @equations begin + Q_flow ~ G * dT + end +end + +""" + ConvectiveResistor(; name, R) + +Lumped thermal element for heat convection. + +# States: + + - `dT`: [`K`] Temperature difference across the component `solid.T` - `fluid.T` + - `Q_flow`: [`W`] Heat flow rate from `solid` -> `fluid` + +# Connectors: + + - `solid` + - `fluid` + +# Parameters: + + - `R`: [`K/W`] Constant thermal resistance of material +""" +@mtkmodel ConvectiveResistor begin + @extend Q_flow, dT = convective_element1d = ConvectiveElement1D() + @parameters begin + R + end + @equations begin + dT ~ R * Q_flow + end +end + +""" + BodyRadiation(; name, G) + +Lumped thermal element for radiation heat transfer. + +# States: + + - `dT`: [`K`] Temperature difference across the component a.T - b.T + - `Q_flow`: [`W`] Heat flow rate from port a -> port b + +# Connectors: + + - `port_a` + - `port_b` + +# Parameters: + + - `G`: [m^2] Net radiation conductance between two surfaces # Stefan-Boltzmann constant TODO: extract into physical constants module or use existing one +""" +@mtkmodel BodyRadiation begin + begin + sigma = 5.6703744191844294e-8 # Stefan-Boltzmann constant TODO: extract into physical constants module or use existing one + end + + @extend Q_flow, dT, port_a, port_b = element1d = Element1D() + @parameters begin + G + end + @equations begin + Q_flow ~ G * sigma * (port_a.T^4 - port_b.T^4) + end +end + +""" + ThermalCollector(; name, m = 1) + +Collects `m` heat flows + +This is a model to collect the heat flows from `m` heatports to one single heatport. + +# States: + +# Connectors: + + - `port_a1` to `port_am` + - `port_b` + +# Parameters: + + - `m`: Number of heat ports (e.g. m=2: `port_a1`, `port_a2`) +""" +@mtkmodel ThermalCollector begin + @structural_parameters begin + m::Integer = 1 + end + + @components begin + port_a = [HeatPort(name = Symbol(:port_a, i)) for i in 1:m] + port_b = HeatPort() + end + + @equations begin + port_b.Q_flow + sum(k -> k.Q_flow, port_a) ~ 0 + port_b.T ~ port_a[1].T + [port_a[i].T ~ port_a[i + 1].T for i in 1:(m - 1)] + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sensors.jl b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sensors.jl new file mode 100644 index 0000000000..afc4ed441a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sensors.jl @@ -0,0 +1,80 @@ +""" + TemperatureSensor(; name) + +Absolute temperature sensor in kelvin. + +This is an ideal absolute temperature sensor which returns the temperature of the connected port in kelvin as an output +signal. The sensor itself has no thermal interaction with whatever it is connected to. Furthermore, no thermocouple-like +lags are associated with this sensor model. + +# Connectors: + + - `port`: [HeatPort](@ref) Thermal port from which sensor information shall be measured + - `T`: [RealOutput](@ref) [K] Absolute temperature of port +""" +@mtkmodel TemperatureSensor begin + @components begin + port = HeatPort() + T = RealOutput() + end + @equations begin + T.u ~ port.T + port.Q_flow ~ 0 + end +end + +""" + RelativeTemperatureSensor(; name) + +Relative Temperature sensor. + +The relative temperature `port_a.T - port_b.T` is determined between the two ports of this component and is provided as +output signal in kelvin. + +# Connectors: + + - `port_a`: [HeatPort](@ref) Thermal port from which sensor information shall be measured + - `port_b`: [HeatPort](@ref) Thermal port from which sensor information shall be measured + - `T`: [RealOutput](@ref) [K] Relative temperature `a.T - b.T` +""" +@mtkmodel RelativeTemperatureSensor begin + @components begin + port_a = HeatPort() + port_b = HeatPort() + T = RealOutput() + end + @equations begin + T.u ~ port_a.T - port_b.T + port_a.Q_flow ~ 0 + port_b.Q_flow ~ 0 + end +end + +""" + HeatFlowSensor(; name) + +Heat flow rate sensor. + +This model is capable of monitoring the heat flow rate flowing through this component. The sensed value of heat flow rate +is the amount that passes through this sensor while keeping the temperature drop across the sensor zero. This is an ideal +model, so it does not absorb any energy, and it has no direct effect on the thermal response of a system it is included in. +The output signal is positive, if the heat flows from `port_a` to `port_b`. + +# Connectors: + + - `port_a`: [HeatPort](@ref) Thermal port from which sensor information shall be measured + - `port_b`: [HeatPort](@ref) Thermal port from which sensor information shall be measured + - `Q_flow`: [RealOutput](@ref) [W] Heat flow from `port_a` to `port_b` +""" +@mtkmodel HeatFlowSensor begin + @components begin + port_a = HeatPort() + port_b = HeatPort() + Q_flow = RealOutput() + end + @equations begin + port_a.T ~ port_b.T + port_a.Q_flow + port_b.Q_flow ~ 0 + Q_flow.u ~ port_a.Q_flow + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sources.jl b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sources.jl new file mode 100644 index 0000000000..cdd279479a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Thermal/HeatTransfer/sources.jl @@ -0,0 +1,121 @@ +""" + FixedHeatFlow(; name, Q_flow = 1.0, T_ref = 293.15, alpha = 0.0) + +Fixed heat flow boundary condition. + +This model allows a specified amount of heat flow rate to be "injected" into a thermal system at a given port. +The constant amount of heat flow rate `Q_flow` is given as a parameter. The heat flows into the component to which +the component FixedHeatFlow is connected, if parameter `Q_flow` is positive. + +# Connectors: + + - `port` + +# Parameters: + + - `Q_flow`: [W] Fixed heat flow rate at port + - `T_ref`: [K] Reference temperature + - `alpha`: [1/K] Temperature coefficient of heat flow rate +""" +@mtkmodel FixedHeatFlow begin + @parameters begin + Q_flow = 1.0, [description = "Fixed heat flow rate at port"] + T_ref = 293.15, [description = "Reference temperature"] + alpha = 0.0, [description = "Temperature coefficient of heat flow rate"] + end + @components begin + port = HeatPort() + end + + @equations begin + port.Q_flow ~ ifelse(alpha == 0.0, + -Q_flow, # Simplified equation when alpha is 0 + -Q_flow * (1 + alpha * (port.T - T_ref))) + end +end + +""" + FixedTemperature(; name, T) + +Fixed temperature boundary condition in kelvin. + +This model defines a fixed temperature `T` at its port in kelvin, i.e., it defines a fixed temperature as a boundary condition. + +# Connectors: + + - `port` + +# Parameters: + + - `T`: [K] Fixed temperature boundary condition +""" +@mtkmodel FixedTemperature begin + @components begin + port = HeatPort() + end + @parameters begin + T, [description = "Fixed temperature boundary condition"] + end + @equations begin + port.T ~ T + end +end + +""" + PrescribedHeatFlow(; name, T_ref = 293.15, alpha = 0.0) + +Prescribed heat flow boundary condition. + +This model allows a specified amount of heat flow rate to be "injected" into a thermal system at a given port. +The amount of heat is given by the input signal `Q_flow` into the model. The heat flows into the component to which +the component `PrescribedHeatFlow` is connected, if the input signal is positive. +If parameter alpha is > 0, the heat flow is multiplied by `1 + alpha*(port.T - T_ref`) in order to simulate temperature +dependent losses (which are given a reference temperature T_ref). + +# Connectors: + + - `port` + - `RealInput` `Q_flow` Input for the heat flow + +# Parameters: + + - `T_ref`: [K] Reference temperature + - `alpha`: [1/K] Temperature coefficient of heat flow rate +""" +@mtkmodel PrescribedHeatFlow begin + @parameters begin + T_ref = 293.15, [description = "Reference temperature"] + alpha = 0.0, [description = "Temperature coefficient of heat flow rate"] + end + @components begin + port = HeatPort() + Q_flow = RealInput() + end + @equations begin + port.Q_flow ~ -Q_flow.u * (1 + alpha * (port.T - T_ref)) + end +end + +""" + PrescribedTemperature(; name) + +This model represents a variable temperature boundary condition. + +The temperature in kelvin is given as input signal to the `RealInput` `T`. The effect is that an instance of +this model acts as an infinite reservoir, able to absorb or generate as much energy as required to keep +the temperature at the specified value. + +# Connectors: + + - `port` + - `RealInput` `T` input for the temperature +""" +@mtkmodel PrescribedTemperature begin + @components begin + port = HeatPort() + T = RealInput() + end + @equations begin + port.T ~ T.u + end +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Thermal/Thermal.jl b/lib/ModelingToolkitStandardLibrary/src/Thermal/Thermal.jl new file mode 100644 index 0000000000..a7ad12c074 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Thermal/Thermal.jl @@ -0,0 +1,27 @@ +""" +Library of thermal system components to model heat transfer. +""" +module Thermal +using ModelingToolkit, Symbolics, IfElse +using ModelingToolkit: t_nounits as t, D_nounits as D +using ...Blocks: RealInput, RealOutput + +export HeatPort, Element1D +include("utils.jl") + +export BodyRadiation, ConvectiveConductor, ConvectiveResistor, HeatCapacitor, + ThermalConductor, + ThermalResistor, ThermalCollector +include("HeatTransfer/ideal_components.jl") + +export RelativeTemperatureSensor, HeatFlowSensor, TemperatureSensor +include("HeatTransfer/sensors.jl") + +export FixedHeatFlow, FixedTemperature, PrescribedHeatFlow, PrescribedTemperature +include("HeatTransfer/sources.jl") + +# Simple components for 1-dimensional incompressible thermo-fluid flow models +# TODO: +# - FluidHeatFlow + +end diff --git a/lib/ModelingToolkitStandardLibrary/src/Thermal/utils.jl b/lib/ModelingToolkitStandardLibrary/src/Thermal/utils.jl new file mode 100644 index 0000000000..74e9f3b42c --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/src/Thermal/utils.jl @@ -0,0 +1,90 @@ +@connector HeatPort begin + @parameters begin + T_guess = 273.15 + 20 + Q_flow_guess = 0.0 + end + + @variables begin + T(t), [guess = T_guess] + Q_flow(t), [guess = Q_flow_guess, connect = Flow] + end +end +Base.@doc """ + HeatPort(; T = nothing, T_guess = 273.15 + 20, Q_flow = nothing, Q_flow_guess = 0.0, name) + +Port for a thermal system. +# Parameters: +- `T_guess`: [K] Initial guess for the temperature of the port (set to 273.15 + 20). +- `Q_flow_guess`: [W] Initial guess for the heat flow rate at the port (set to 0.0). + +# States: +- `T`: [K] Temperature of the port. Guess set to `T_guess`. Passing a value for `T` will set its default. +- `Q_flow`: [W] Heat flow rate at the port. Guess set to `Q_flow_guess`. Passing a value for `Q_flow` will set its default. +""" HeatPort + +""" + Element1D(; name, dT = 0.0, Q_flow = 0.0) + +This partial model contains the basic connectors and variables to allow heat transfer models to be created that do not +store energy. This model defines and includes equations for the temperature drop across the element, `dT`, and the heat +flow rate through the element from `port_a` to `port_b`, `Q_flow`. + +# States: + + - `dT`: [`K`] Temperature difference across the component a.T - b.T. It accepts an initial value, which defaults to 0.0. + - `Q_flow`: [`W`] Heat flow rate from port a -> port b. It accepts an initial value, which defaults to 0.0. + +# Connectors: + +`port_a` +`port_b` +""" +@mtkmodel Element1D begin + @components begin + port_a = HeatPort() + port_b = HeatPort() + end + @variables begin + dT(t), [guess = 0.0] + Q_flow(t), [guess = 0.0] + end + @equations begin + dT ~ port_a.T - port_b.T + port_a.Q_flow ~ Q_flow + port_a.Q_flow + port_b.Q_flow ~ 0 + end +end + +""" + ConvectiveElement1D(; name, dT = 0.0, Q_flow = 0.0) + +This partial model contains the basic connectors and variables to allow heat +transfer models to be created that do not store energy. This model defines and +includes equations for the temperature drop across the element, `dT`, and the heat +flow rate through the element from `solid` to `fluid`, `Q_flow`. + +# States: + + - `dT`: [`K`] Temperature difference across the component `solid.T` - `fluid.T`. It accepts an initial value, which defaults to 0.0. + - `Q_flow`: [`W`] Heat flow rate from `solid` -> `fluid`. It accepts an initial value, which defaults to 0.0. + +# Connectors: + +`solid` +`fluid` +""" +@mtkmodel ConvectiveElement1D begin + @components begin + solid = HeatPort() + fluid = HeatPort() + end + @variables begin + dT(t), [guess = 0.0] + Q_flow(t), [guess = 0.0] + end + @equations begin + dT ~ solid.T - fluid.T + solid.Q_flow ~ Q_flow + solid.Q_flow + fluid.Q_flow ~ 0 + end +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/continuous.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/continuous.jl new file mode 100644 index 0000000000..78b7eff500 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/continuous.jl @@ -0,0 +1,492 @@ +using ModelingToolkit, ModelingToolkitStandardLibrary, OrdinaryDiffEq +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: t_nounits as t +using OrdinaryDiffEq: ReturnCode.Success +using Test + +#= +Testing strategy: +The general strategy is to test systems using simple inputs where the solution +is known on closed form. For algebraic systems (without differential variables), +an integrator with a constant input is often used together with the system under test. +=# + +@testset "Constant" begin + @named c = Constant(; k = 1) + @named int = Integrator(x = 1) + @named iosys = System(connect(c.output, int.input), t, systems = [int, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(sol[c.output.u] .≈ 1) + @test sol[int.output.u][end] .≈ 2 # expected solution +end + +@testset "Derivative" begin + @named source = Sine(; frequency = 1) + @named int = Integrator(; k = 1) + @named der = Derivative(; k = 1, T = 0.001) + @named iosys = System( + [ + connect(source.output, der.input), + connect(der.output, int.input) + ], + t, + systems = [int, source, der]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(isapprox.(sol[source.output.u], sol[int.output.u], atol = 1e-1)) +end + +@testset "PT1" begin + pt1_func(t, k, T) = k * (1 - exp(-t / T)) # Known solution to first-order system + + k, T = 1.2, 0.1 + @named c = Constant(; k = 1) + @named pt1 = FirstOrder(; k = k, T = T) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u]≈pt1_func.(sol.t, k, T) atol=1e-3 + + # Test highpass feature + @named pt1 = FirstOrder(; k = k, T = T, lowpass = false) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u]≈k .- pt1_func.(sol.t, k, T) atol=1e-3 +end + +@testset "PT2" begin + # Known solution to second-order system + function pt2_func(t, k, w, d) + y = if d == 0 + -k * (-1 + cos(t * w)) + else + d = complex(d) + real(k * (1 + + (-cosh(sqrt(-1 + d^2) * t * w) - + (d * sinh(sqrt(-1 + d^2) * t * w)) / sqrt(-1 + d^2)) / exp(d * t * w))) + end + end + + k, w, d = 1.0, 1.0, 0.5 + @named c = Constant(; k = 1) + @named pt2 = SecondOrder(; k = k, w = w, d = d) + @named iosys = System(connect(c.output, pt2.input), t, systems = [pt2, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, [unknowns(sys) .=> 0.0...; pt2.xd => 0.0], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt2.output.u]≈pt2_func.(sol.t, k, w, d) atol=1e-3 +end + +@testset "StateSpace" begin + A = [0 1; -1 -0.5] + B = [0, 1] + C = [0.9 1;] + D = [0;;] + @named ss = StateSpace(; A, B, C, D, x = zeros(2)) + @named c = Constant(; k = 1) + @named model = System([ + connect(c.output, ss.input) + ], + t, + systems = [ss, c]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + # initial condition + @test sol[ss.x[1]][1]≈0 atol=1e-3 + @test sol[ss.x[2]][1]≈0 atol=1e-3 + # equilibrium point is at [1, 0] + @test sol[ss.x[1]][end]≈1 atol=1e-3 + @test sol[ss.x[2]][end]≈0 atol=1e-3 + + # non-zero operating point + u0 = [1] # This causes no effective input to the system since c.k = 1 + y0 = [2] + @named ss = StateSpace(; A, B, C, D, x = zeros(2), u0, y0) + @named model = System([ + connect(c.output, ss.input) + ], + t, + systems = [ss, c]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + + @test sol[ss.x[1]][end ÷ 2]≈0 atol=1e-3 # Test that x did not move + @test sol[ss.x[1]][end]≈0 atol=1e-3 # Test that x did not move + @test sol[ss.x[2]][end]≈0 atol=1e-3 + @test sol[ss.output.u[1]][end]≈y0[] atol=1e-3 # Test that the output equals the operating point +end + +""" +Second order demo plant +""" +@component function Plant(; name, x = zeros(2)) + @named input = RealInput() + @named output = RealOutput() + D = Differential(t) + sts = @variables x1(t)=x[1] x2(t)=x[2] + eqs = [D(x1) ~ x2 + D(x2) ~ -x1 - 0.5 * x2 + input.u + output.u ~ 0.9 * x1 + x2] + compose(System(eqs, t, sts, []; name), [input, output]) +end + +@testset "PI" begin + re_val = 2 + @named ref = Constant(; k = re_val) + @named pi_controller = PI(k = 1, T = 1) + @named plant = Plant() + @named fb = Feedback() + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pi_controller.err_input), + connect(pi_controller.ctr_output, plant.input) + ], + t, + systems = [pi_controller, plant, ref, fb]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s +end + +@testset "PID" begin + re_val = 2 + @named ref = Constant(; k = re_val) + @named pid_controller = PID(k = 3, Ti = 0.5, Td = 1 / 100) + @named plant = Plant() + @named fb = Feedback() + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pid_controller.err_input), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref, fb]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + + @testset "PI" begin + @named pid_controller = PID(k = 3, Ti = 0.5, Td = false) + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pid_controller.err_input), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref, fb]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + end + + @testset "PD" begin + @named pid_controller = PID(k = 10, Ti = false, Td = 1) + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pid_controller.err_input), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref, fb]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end] > 1 # without I there will be a steady-state error + end +end + +@testset "LimPI" begin + re_val = 1 + @named ref = Constant(; k = re_val) + @named pi_controller_lim = LimPI(k = 3, + T = 0.5, + u_max = 1.5, + u_min = -1.5, + Ta = 0.1) + @named pi_controller = PI(gainPI.k = 3, T = 0.5) + @named sat = Limiter(y_max = 1.5, y_min = -1.5) + @named plant = Plant() + @named fb = Feedback() + + # without anti-windup measure + sol = let + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pi_controller.err_input), + connect(pi_controller.ctr_output, sat.input), + connect(sat.output, plant.input) + ], + t, + systems = [pi_controller, plant, ref, fb, sat]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 20.0)) + sol = solve(prob, Rodas4()) + end + + # with anti-windup measure + sol_lim = let + @named model = System( + [ + connect(ref.output, fb.input1), + connect(plant.output, fb.input2), + connect(fb.output, pi_controller_lim.err_input), + connect(pi_controller_lim.ctr_output, sat.input), + connect(sat.output, plant.input) + ], + t, + systems = [pi_controller_lim, plant, ref, fb, sat]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 20.0)) + sol = solve(prob, Rodas4()) + end + + @test sol.retcode == Success + @test sol_lim.retcode == ReturnCode.Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test all(isapprox.(sol_lim[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test sol_lim[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test all(-1.5 .<= sol_lim[pi_controller_lim.ctr_output.u] .<= 1.5) # test limit + + # Plots.plot(sol; vars=[plant.output.u]) # without anti-windup measure + # Plots.plot!(sol_lim; vars=[plant.output.u]) # with anti-windup measure +end + +@testset "LimPID" begin + re_val = 1 + @named ref = Constant(; k = re_val) + @named pid_controller = LimPID( + k = 3, Ti = 0.5, Td = 1 / 100, u_max = 1.5, u_min = -1.5, + Ni = 0.1 / 0.5) + @named plant = Plant() + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + + @testset "PI" begin + @named pid_controller = LimPID(k = 3, Ti = 0.5, Td = false, u_max = 1.5, + u_min = -1.5, Ni = 0.1 / 0.5) + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + end + @testset "PD" begin + @named pid_controller = LimPID(k = 10, Ti = false, Td = 1, u_max = 1.5, + u_min = -1.5) + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end] > 0.5 # without I there will be a steady-state error + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + end + @testset "set-point weights" begin + @testset "wp" begin + @named pid_controller = LimPID(k = 3, Ti = 0.5, Td = 1 / 100, u_max = 1.5, + u_min = -1.5, Ni = 0.1 / 0.5, wp = 0, wd = 1) + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + sol[pid_controller.addP.output.u] == -sol[pid_controller.measurement.u] + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + end + @testset "wd" begin + @named pid_controller = LimPID(k = 3, Ti = 0.5, Td = 1 / 100, u_max = 1.5, + u_min = -1.5, Ni = 0.1 / 0.5, wp = 1, wd = 0) + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + sol[pid_controller.addD.output.u] == -sol[pid_controller.measurement.u] + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + end + end + @testset "PI without AWM" begin + @named pid_controller = LimPID(k = 3, Ti = 0.5, Td = false, u_max = 1.5, + u_min = -1.5, Ni = Inf) + @named model = System( + [ + connect(ref.output, pid_controller.reference), + connect(plant.output, pid_controller.measurement), + connect(pid_controller.ctr_output, plant.input) + ], + t, + systems = [pid_controller, plant, ref]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol, vars=[plant.output.u, plant.input.u]) + @test sol.retcode == Success + @test all(isapprox.(sol[ref.output.u], re_val, atol = 1e-3)) # check reference + @test sol[plant.output.u][end]≈re_val atol=1e-3 # zero control error after 100s + @test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit + end + + @testset "TransferFunction" begin + pt1_func(t, k, T) = k * (1 - exp(-t / T)) # Known solution to first-order system + + @named c = Constant(; k = 1) + @named pt1 = TransferFunction(b = [1.2], a = [3.14, 1]) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u]≈pt1_func.(sol.t, 1.2, 3.14) atol=1e-3 + + # Test logic for a_end by constructing an integrator + @named c = Constant(; k = 1) + @named pt1 = TransferFunction(b = [1.2], a = [3.14, 0]) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u] ≈ sol.t .* (1.2 / 3.14) + @test sol[pt1.x[1]] ≈ sol.t .* (1 / 3.14) # Test that scaling of state works properly + + # Test higher order + + function pt2_func(t, k, w, d) + y = if d == 0 + -k * (-1 + cos(t * w)) + else + d = complex(d) + real(k * (1 + + (-cosh(sqrt(-1 + d^2) * t * w) - + (d * sinh(sqrt(-1 + d^2) * t * w)) / sqrt(-1 + d^2)) / + exp(d * t * w))) + end + end + + k, w, d = 1.0, 1.0, 0.5 + @named pt1 = TransferFunction(b = [w^2], a = [1, 2d * w, w^2]) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u]≈pt2_func.(sol.t, k, w, d) atol=1e-3 + + # test zeros (high-pass version of first test) + @named c = Constant(; k = 1) + @named pt1 = TransferFunction(b = [1, 0], a = [1, 1]) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[pt1.output.u]≈1 .- pt1_func.(sol.t, 1, 1) atol=1e-3 + @test sol[pt1.x[1]]≈pt1_func.(sol.t, 1, 1) atol=1e-3 # Test that scaling of state works properly + + # Test with no state + @named pt1 = TransferFunction(b = [2.7], a = [pi]) + @named iosys = System(connect(c.output, pt1.input), t, systems = [pt1, c]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[], (0.0, 100.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test all(==(2.7 / pi), sol[pt1.output.u]) + end +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/math.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/math.jl new file mode 100644 index 0000000000..f37ebf9cb7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/math.jl @@ -0,0 +1,387 @@ +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkitStandardLibrary.Blocks: _clamp, _dead_zone +using ModelingToolkit: inputs, unbound_inputs, bound_inputs, t_nounits as t +using OrdinaryDiffEq: ReturnCode.Success + +@testset "Gain" begin + @named c = Constant(; k = 1) + @named gain = Gain(; k = 1) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c.output, gain.input), + connect(gain.output, int.input) + ], + t, systems = [int, gain, c]) + + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 1.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test all(sol[c.output.u] .≈ 1) + @test sol[int.output.u][end] ≈ 2 # expected solution after 1s +end + +@testset "Feedback loop" begin + @named c = Constant(; k = 2) + @named gain = Gain(; k = 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) + + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 100.0)) + + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[int.output.u][end] ≈ 2 # expected solution after 1s +end + +@testset "Add" begin + @named c1 = Constant(; k = 1) + @named c2 = Sine(; frequency = 1) + @named add = Add(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, add.input1), + connect(c2.output, add.input2), + connect(add.output, int.input) + ], + t, + systems = [int, add, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[add.output.u] ≈ 1 .+ sin.(2 * pi * sol.t) + + @testset "weights" begin + k1 = -1 + k2 = 2 + @named add = Add(; k1 = k1, k2 = k2) + @named model = System( + [ + connect(c1.output, add.input1), + connect(c2.output, add.input2), + connect(add.output, int.input) + ], + t, + systems = [int, add, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[add.output.u] ≈ k1 .* 1 .+ k2 .* sin.(2 * pi * sol.t) + end +end + +@testset "Add3" begin + @named c1 = Constant(; k = 1) + @named c2 = Sine(; frequency = 1) + @named c3 = Sine(; frequency = 2) + @named add = Add3(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, add.input1), + connect(c2.output, add.input2), + connect(c3.output, add.input3), + connect(add.output, int.input) + ], + t, + systems = [int, add, c1, c2, c3]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[add.output.u] ≈ 1 .+ sin.(2 * pi * sol.t) .+ sin.(2 * pi * 2 * sol.t) + + @testset "weights" begin + k1 = -1 + k2 = 2 + k3 = -pi + @named add = Add3(; k1 = k1, k2 = k2, k3 = k3) + @named model = System( + [ + connect(c1.output, add.input1), + connect(c2.output, add.input2), + connect(c3.output, add.input3), + connect(add.output, int.input) + ], + t, + systems = [int, add, c1, c2, c3]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[add.output.u] ≈ + k1 .* 1 .+ k2 .* sin.(2 * pi * sol.t) .+ k3 .* sin.(2 * pi * 2 * sol.t) + end +end + +@testset "Product" begin + @named c1 = Constant(; k = 2) + @named c2 = Sine(; frequency = 1) + @named prod = Product(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, prod.input1), + connect(c2.output, prod.input2), + connect(prod.output, int.input) + ], + t, + systems = [int, prod, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[prod.output.u] ≈ 2 * sin.(2 * pi * sol.t) +end + +@testset "Power" begin + @named c1 = Sine(; frequency = 1) + @named c2 = Constant(; k = 2) + @named pow = Power(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, pow.base), + connect(c2.output, pow.exponent), + connect(pow.output, int.input) + ], + t, + systems = [int, pow, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[pow.output.u] ≈ sin.(2 * pi * sol.t) .^ 2 +end + +@testset "Modulo" begin + @named c1 = Ramp(height = 2, duration = 1, offset = 1, start_time = 0, smooth = false) + @named c2 = Constant(; k = 1) + @named modl = Modulo(;) + @named model = System( + [ + connect(c1.output, modl.dividend), + connect(c2.output, modl.divisor) + ], + t, + systems = [modl, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[modl.remainder.u] ≈ mod.(2 * sol.t, 1) +end + +@testset "UnaryMinus" begin + @named c1 = Sine(; frequency = 1) + @named minu = UnaryMinus(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, minu.input), + connect(minu.output, int.input) + ], + t, + systems = [int, minu, c1]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[minu.output.u] ≈ -sin.(2 * pi * sol.t) +end + +@testset "Floor" begin + @named c1 = Sine(; frequency = 1) + @named flr = Floor(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, flr.input), + connect(flr.output, int.input) + ], + t, + systems = [int, flr, c1]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[flr.output.u] ≈ floor.(sin.(2 * pi * sol.t)) +end + +@testset "Ceil" begin + @named c1 = Sine(; frequency = 1) + @named cel = Ceil(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, cel.input), + connect(cel.output, int.input) + ], + t, + systems = [int, cel, c1]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[cel.output.u] ≈ ceil.(sin.(2 * pi * sol.t)) +end + +@testset "Division" begin + @named c1 = Sine(; frequency = 1) + @named c2 = Constant(; k = 2) + @named div = Division(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, div.input1), + connect(c2.output, div.input2), + connect(div.output, int.input) + ], + t, + systems = [int, div, c1, c2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[div.output.u] ≈ sin.(2 * pi * sol.t) ./ 2 +end + +@testset "Abs" begin + @named c = Sine(; frequency = 1) + @named absb = Abs(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c.output, absb.input), + connect(absb.output, int.input) + ], + t, + systems = [int, absb, c]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[absb.output.u] ≈ abs.(sin.(2 * pi * sol.t)) +end + +@testset "MatrixGain" begin + K = [1 2; 3 4] + @named gain = MatrixGain(; K) + K = [1, 2] + @named gain = MatrixGain(; K) + # TODO: +end + +@testset "Sum" begin + @named s = Sum(; input.nin = 2) + # TODO: +end + +@testset "Math" begin + for (block, func) in [ + (Abs, abs), + (Sign, sign), + (Sin, sin), + (Cos, cos), + (Tan, tan), + (Asin, asin), + (Acos, acos), + (Atan, atan), + (Sinh, sinh), + (Cosh, cosh), + (Tanh, tanh), + (Exp, exp) + ] + @info "testing $block" + @named source = Sine(frequency = 1, amplitude = 0.5) + @named b = block() + @named int = Integrator() + @named model = System( + [ + connect(source.output, b.input), + connect(b.output, int.input) + ], + t, systems = [int, b, source]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[b.output.u] ≈ func.(sol[source.output.u]) + end + + # input must be positive + for (block, func) in [(Sqrt, sqrt), (Log, log), (Log10, log10)] + @info "testing $block" + @named source = Sine(; frequency = 1, offset = 2, amplitude = 0.5) + @named b = block() + @named int = Integrator() + @named model = System( + [ + connect(source.output, b.input), + connect(b.output, int.input) + ], + t, systems = [int, b, source]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0, b.input.u => 2.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + @test isequal(unbound_inputs(sys), []) + @test sol.retcode == Success + @test sol[b.output.u] ≈ func.(sol[source.output.u]) + end +end + +@testset "Atan2" begin + @named c1 = Sine(; frequency = 1, offset = 2) + @named c2 = Sine(; frequency = 1, offset = 1) + @named b = Atan2(;) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(c1.output, b.input1), + connect(c2.output, b.input2), + connect(b.output, int.input) + ], + t, + systems = [int, b, c1, c2]) + + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[int.x => 0.0, b.input1.u => 2, b.input2.u => 1], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + + @test isequal(unbound_inputs(sys), []) + @test all(map(u -> u in Set([b.input1.u, b.input2.u, int.input.u]), bound_inputs(sys))) + @test all(map(u -> u in Set([b.input1.u, b.input2.u, int.input.u]), inputs(sys))) + @test sol.retcode == Success + @test sol[int.input.u] ≈ atan.(sol[c1.output.u], sol[c2.output.u]) +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/nonlinear.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/nonlinear.jl new file mode 100644 index 0000000000..bfc02c1d0b --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/nonlinear.jl @@ -0,0 +1,121 @@ +using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Blocks: _clamp, _dead_zone +using OrdinaryDiffEq: ReturnCode.Success + +@testset "Limiter" begin + @testset "Constant" begin + @named c = Constant(; k = 1) + @named int = Integrator(; k = 1) + @named sat = Limiter(; y_min = -0.6, y_max = 0.8) + @named model = System( + [ + connect(c.output, int.input), + connect(int.output, sat.input) + ], + t, + systems = [int, c, sat]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [int.x => 1.0], (0.0, 1.0)) + + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test sol[int.output.u][end] ≈ 2 + @test sol[sat.output.u][end] ≈ 0.8 + end + + @testset "Sine" begin + y_min, y_max = -0.3, 0.5 + @named source = Sine(; frequency = 1 / 2) + @named lim = Limiter(; y_max = y_max, y_min = y_min) + @named int = Integrator(; k = 1) + @named iosys = System( + [ + connect(source.output, lim.input), + connect(lim.output, int.input) + ], + t, + systems = [source, lim, int]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 10.0)) + + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test all(abs.(sol[lim.output.u]) .<= 0.5) + @test all(isapprox.(sol[lim.output.u], _clamp.(sol[source.output.u], y_min, y_max), + atol = 1e-2)) + + # Plots.plot(sol; vars=[source.output.u, lim.output.u]) + # Plots.scatter(sol[source.output.u], sol[lim.output.u]) + # Plots.scatter!(sol[source.output.u], _clamp.(sol[source.output.u], y_min, y_max)) + end +end + +@testset "DeadZone" begin + @testset "Constant" begin + @named c = Constant(; k = 1) + @named int = Integrator(; k = 1) + @named dz = DeadZone(; u_min = -2, u_max = 1) + @named model = System( + [ + connect(c.output, int.input), + connect(int.output, dz.input) + ], + t, + systems = [int, c, dz]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [int.x => 1.0], (0.0, 1.0)) + sol = solve(prob, Rodas4()) + + @test SciMLBase.successful_retcode(sol) + @test all(sol[int.output.u][end] .≈ 2) + end + + @testset "Sine" begin + u_min, u_max = -2, 1 + @named source = Sine(; amplitude = 3, frequency = 1 / 2) + @named dz = DeadZone(; u_min = u_min, u_max = u_max) + @named int = Integrator(; k = 1) + @named model = System( + [ + connect(source.output, dz.input), + connect(dz.output, int.input) + ], + t, + systems = [int, source, dz]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [int.x => 1.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + + @test SciMLBase.successful_retcode(sol) + @test all(sol[dz.output.u] .<= 2) + @test all(sol[dz.output.u] .>= -1) + @test all(isapprox.(sol[dz.output.u], + _dead_zone.(sol[source.output.u], u_min, u_max), atol = 1e-2)) + + # Plots.plot(sol; vars=[source.output.u, dz.output.u]) + # Plots.scatter(sol[source.output.u], sol[dz.output.u]) + # Plots.scatter!(sol[source.output.u], _dead_zone.(sol[source.output.u], u_min, u_max)) + end +end + +@testset "SlewRateLimiter" begin + @named source = Sine(; frequency = 1 / 2) + @named rl = SlewRateLimiter(; rising = 1, falling = -1, Td = 0.001, y_start = -1 / 3) + @named iosys = System([ + connect(source.output, rl.input) + ], + t, + systems = [source, rl]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + + tS = 0.01 + sol = solve(prob, Rodas4(), saveat = tS, abstol = 1e-10, reltol = 1e-10) + @test SciMLBase.successful_retcode(sol) + @test all(abs.(sol[rl.output.u]) .<= 0.51) + @test all(-1 - 1e-5 .<= diff(sol[rl.output.u]) ./ tS .<= 1 + 1e-5) # just an approximation +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/sources.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/sources.jl new file mode 100644 index 0000000000..752ad08ab4 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/sources.jl @@ -0,0 +1,655 @@ +using ModelingToolkit, ModelingToolkitStandardLibrary, OrdinaryDiffEq +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks: smooth_sin, smooth_cos, smooth_damped_sin, + smooth_square, smooth_step, smooth_ramp, + smooth_triangular, triangular, square +using OrdinaryDiffEq: ReturnCode.Success +using DataInterpolations +using DataFrames +using SymbolicIndexingInterface +using SciMLStructures: SciMLStructures, Tunable +using ForwardDiff +using ADTypes + +@testset "Constant" begin + @named src = Constant(k = 2) + @named int = Integrator() + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u][end]≈2 atol=1e-3 +end + +@testset "TimeVaryingFunction" begin + f(t) = t^2 + 1 + vars = @variables y(t) dy(t) ddy(t) + @named src = TimeVaryingFunction(f) + @named int = Integrator() + @named iosys = System( + [y ~ src.output.u + D(y) ~ dy + D(dy) ~ ddy + connect(src.output, int.input)], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 10.0)) + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈f.(sol.t) atol=1e-3 + @test sol[int.output.u][end]≈1 / 3 * 10^3 + 10 atol=1e-3 # closed-form solution to integral +end + +@testset "Sine" begin + function sine(t, frequency, amplitude, phase, offset, start_time) + offset + ifelse(t < start_time, 0, + amplitude * sin(2 * pi * frequency * (t - start_time) + phase)) + end + + frequency = 1 + amplitude = 2 + phase = 0 + offset = 1 + start_time = 2 + δ = 1e-5 + @named int = Integrator() + + @named src = Sine(frequency = frequency, amplitude = amplitude, phase = phase, + offset = offset, start_time = start_time) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈sine.(sol.t, frequency, amplitude, phase, offset, start_time) atol=1e-3 + + @named smooth_src = Sine(frequency = frequency, + amplitude = amplitude, + phase = phase, + offset = offset, + start_time = start_time, + smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_sin.( + smooth_sol.t, δ, frequency, amplitude, + phase, offset, start_time) atol=1e-3 +end + +@testset "Cosine" begin + function cosine(t, frequency, amplitude, phase, offset, start_time) + offset + ifelse(t < start_time, 0, + amplitude * cos(2 * pi * frequency * (t - start_time) + phase)) + end + + frequency = 1 + amplitude = 2 + phase = 0 + offset = 1 + start_time = 2 + δ = 1e-5 + @named int = Integrator() + + @named src = Cosine(frequency = frequency, + amplitude = amplitude, + phase = phase, + offset = offset, + start_time = start_time, + smooth = false) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈cosine.(sol.t, frequency, amplitude, phase, offset, start_time) atol=1e-3 + + @named smooth_src = Cosine(frequency = frequency, + amplitude = amplitude, + phase = phase, + offset = offset, + start_time = start_time, + smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_cos.( + smooth_sol.t, δ, frequency, amplitude, + phase, offset, start_time) atol=1e-3 +end + +@testset "ContinuousClock" begin + cont_clock(t, offset, start_time) = offset + ifelse(t < start_time, 0, t - start_time) + + offset, start_time = 1, 0 + + @named src = ContinuousClock(offset = offset, start_time = start_time) + @named int = Integrator() + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈cont_clock.(sol.t, offset, start_time) atol=1e-3 +end + +@testset "Ramp" begin + function ramp(t, offset, height, duration, start_time) + offset + ifelse(t < start_time, 0, + ifelse(t < (start_time + duration), (t - start_time) * height / duration, + height)) + end + + offset, height, duration, start_time, δ = 1, 2, 2, 0, 1e-5 + @named int = Integrator() + + @named src = Ramp(offset = offset, height = height, duration = duration, + start_time = start_time) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈ramp.(sol.t, offset, height, duration, start_time) atol=1e-3 + + start_time = 2 + @named smooth_src = Ramp(offset = offset, height = height, duration = duration, + start_time = start_time, smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_ramp.(smooth_sol.t, δ, height, duration, + offset, start_time) atol=1e-3 +end + +@testset "Step" begin + step(t, offset, height, start_time) = offset + ifelse(t < start_time, 0, height) + + offset, height, start_time, δ = 1, 2, 5, 1e-5 + @named int = Integrator() + + @named src = Step(offset = offset, height = height, start_time = start_time, + smooth = false) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + + @test sol.retcode == Success + @test sol[src.output.u]≈step.(sol.t, offset, height, start_time) atol=1e-2 + @test sol(start_time, idxs = src.output.u) == height + offset # Test that the step is applied at the start time + + # test with duration + duration = 1.2 + @named src = Step(offset = offset, height = height, start_time = start_time, + duration = duration, smooth = false) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4(), dtmax = 0.1) # set dtmax to prevent the solver from overstepping the entire step disturbance + + @test sol.retcode == Success + @test sol[src.output.u]≈step.(sol.t, offset, height, start_time) - + step.(sol.t, 0, height, start_time + duration) atol=1e-2 + + @named smooth_src = Step(offset = offset, height = height, start_time = start_time, + smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4(), dtmax = 0.1) # set dtmax to prevent the solver from overstepping the entire step disturbance) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u] ≈ + smooth_step.(smooth_sol.t, δ, height, offset, start_time) + + # with duration + @named smooth_src = Step(offset = offset, height = height, start_time = start_time, + smooth = true, duration = duration) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u] ≈ + smooth_step.(smooth_sol.t, δ, height, offset, start_time) - + smooth_step.(smooth_sol.t, δ, height, 0, start_time + duration) +end + +@testset "Square" begin + frequency = 1 + amplitude = 2 + offset = 1 + start_time = 2.5 + δ = 1e-5 + @named int = Integrator() + + @named src = Square(frequency = frequency, amplitude = amplitude, + offset = offset, start_time = start_time) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + + @test sol.retcode == Success + @test sol[src.output.u]≈square.(sol.t, frequency, amplitude, offset, start_time) atol=1e-3 + + @named smooth_src = Square(frequency = frequency, amplitude = amplitude, + offset = offset, start_time = start_time, smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_square.(smooth_sol.t, δ, frequency, + amplitude, offset, start_time) atol=1e-3 +end + +@testset "Triangular" begin + frequency = 5 + amplitude = 1 + offset = 2 + start_time = 1 + δ = 1e-5 + @named int = Integrator() + + @named src = Triangular(frequency = frequency, amplitude = amplitude, + offset = offset, start_time = start_time) + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 4.0)) + sol = solve(prob, Rodas4(), saveat = 0.01) + + @test sol.retcode == Success + @test sol[src.output.u]≈triangular.(sol.t, frequency, amplitude, offset, start_time) atol=1e-3 + + @named smooth_src = Triangular(frequency = frequency, amplitude = amplitude, + offset = offset, start_time = start_time, smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 4.0)) + smooth_sol = solve(smooth_prob, Rodas4(), saveat = 0.01) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_triangular.(smooth_sol.t, δ, frequency, + amplitude, offset, start_time) atol=1e-3 +end + +@testset "ExpSine" begin + function exp_sine(t, amplitude, frequency, damping, phase, start_time) + offset + ifelse(t < start_time, 0, + amplitude * exp(-damping * (t - start_time)) * + sin(2 * pi * frequency * (t - start_time) + phase)) + end + + frequency, amplitude, damping = 3, 2, 0.10 + phase, offset, start_time, δ = 0, 0, 0, 1e-5 + @named src = ExpSine(frequency = frequency, amplitude = amplitude, damping = damping, + phase = phase, offset = offset, start_time = start_time) + @named int = Integrator() + @named iosys = System([ + connect(src.output, int.input) + ], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + prob = ODEProblem(sys, Pair[int.x => 0.0], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u]≈exp_sine.(sol.t, amplitude, frequency, damping, phase, + start_time) atol=1e-3 + + offset, start_time = 1, 2 + @named smooth_src = ExpSine(frequency = frequency, amplitude = amplitude, + damping = damping, phase = phase, offset = offset, + start_time = start_time, smooth = true) + @named smooth_iosys = System([ + connect(smooth_src.output, int.input) + ], + t, + systems = [int, smooth_src]) + smooth_sys = mtkcompile(smooth_iosys) + smooth_prob = ODEProblem(smooth_sys, Pair[int.x => 0.0], (0.0, 10.0)) + smooth_sol = solve(smooth_prob, Rodas4()) + + @test smooth_sol.retcode == Success + @test smooth_sol[smooth_src.output.u]≈smooth_damped_sin.(smooth_sol.t, δ, frequency, + amplitude, damping, phase, + offset, start_time) atol=1e-3 +end + +@testset "SampledData" begin + dt = 4e-4 + t_end = 10.0 + time = 0:dt:t_end + x = @. time^2 + 1.0 + + @testset "using Parameter type" begin + vars = @variables y(t) dy(t) ddy(t) + @named src = SampledData(Float64) + @named int = Integrator() + @named iosys = System( + [y ~ src.output.u + D(y) ~ dy + D(dy) ~ ddy + connect(src.output, int.input)], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + s = complete(iosys) + prob = ODEProblem(sys, + [s.src.buffer => Parameter(x, dt)], + (0.0, t_end); + tofloat = false) + # prob = remake(prob; p = Parameter.(prob.p)) #<-- no longer needed with ModelingToolkit.jl PR #2231 + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u][1] == 1.0 #check correct initial condition + + @test sol(time)[src.output.u]≈x atol=1e-3 + @test sol[int.output.u][end]≈1 / 3 * 10^3 + 10.0 atol=1e-3 # closed-form solution to integral + @test sol[dy][end]≈2 * time[end] atol=1e-3 + @test sol[ddy][end]≈2 atol=1e-3 + end + + @testset "using Vector Based" begin + vars = @variables y(t) dy(t) ddy(t) + @named src = SampledData(dt) + @named int = Integrator() + @named iosys = System( + [y ~ src.output.u + D(y) ~ dy + D(dy) ~ ddy + connect(src.output, int.input)], + t, + systems = [int, src]) + sys = mtkcompile(iosys) + s = complete(iosys) + prob = ODEProblem(sys, + [s.src.buffer => x, s.src.sample_time => dt], + (0.0, t_end); + tofloat = false) + + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[src.output.u][1] == 1.0 #check correct initial condition + + @test sol(time)[src.output.u]≈x atol=1e-3 + @test sol[int.output.u][end]≈1 / 3 * 10^3 + 10.0 atol=1e-3 # closed-form solution to integral + @test sol[dy][end]≈2 * time[end] atol=1e-3 + @test sol[ddy][end]≈2 atol=1e-3 + end +end + +@testset "Interpolation" begin + @variables y(t) = 0 + u = rand(15) + x = 0:14.0 + + @named i = Interpolation(LinearInterpolation, u, x) + eqs = [i.input.u ~ t, D(y) ~ i.output.u] + + @named model = System(eqs, t, systems = [i]) + sys = mtkcompile(model) + + prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys, [], (0.0, 4)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) +end + +@testset "Interpolation in model macro" begin + function MassSpringDamper(; name) + @named input = RealInput() + @variables f(t) x(t)=0 dx(t)=0 ddx(t) + @parameters m=10 k=1000 d=1 + + eqs = [f ~ input.u + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t; name, systems = [input]) + end + + table_data = [1.0, 2.0, 3.0] + table_bkp = [0.0, 0.5, 1.0] + itp = LinearInterpolation(table_data, table_bkp) + + @mtkmodel model_with_lut begin + @components begin + src = Interpolation(itp) + clk = ContinuousClock() + model = MassSpringDamper() + end + @equations begin + connect(src.input, clk.output) + connect(src.output, model.input) + end + end; + @mtkcompile sys = model_with_lut() + + prob = ODEProblem(sys, [], (0.0, 1)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) +end + +@testset "ParametrizedInterpolation" begin + @variables y(t) = 0 + u = rand(15) + x = 0:14.0 + + @testset "LinearInterpolation" begin + @named i = ParametrizedInterpolation(LinearInterpolation, u, x) + eqs = [i.input.u ~ t, D(y) ~ i.output.u] + + @named model = System(eqs, t, systems = [i]) + sys = mtkcompile(model) + + prob = ODEProblem{true, SciMLBase.FullSpecialize}(sys, [], (0.0, 4)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) + + prob2 = remake(prob, p = [i.data => ones(15)]) + sol2 = solve(prob2) + + @test SciMLBase.successful_retcode(sol2) + @test all(only.(sol2.u) .≈ sol2.t) # the solution for y' = 1 is y(t) = t + + set_data! = setp(prob2, i.data) + set_data!(prob2, zeros(15)) + sol3 = solve(prob2) + @test SciMLBase.successful_retcode(sol3) + @test iszero(sol3) + + function loss(x, p) + prob0, set_data! = p + ps = parameter_values(prob0) + arr, repack, alias = SciMLStructures.canonicalize(Tunable(), ps) + T = promote_type(eltype(x), eltype(arr)) + promoted_ps = SciMLStructures.replace(Tunable(), ps, T.(arr)) + prob = remake(prob0; p = promoted_ps) + + set_data!(prob, x) + sol = solve(prob) + sum(abs2.(only.(sol.u) .- sol.t)) + end + + set_data! = setp(prob, i.data) + of = OptimizationFunction(loss, AutoForwardDiff()) + op = OptimizationProblem( + of, u, (prob, set_data!), lb = zeros(15), ub = fill(2.0, 15)) + + # check that type changing works + @test length(ForwardDiff.gradient(x -> of(x, (prob, set_data!)), u)) == 15 + + @test_skip begin + r = solve(op, Optimization.LBFGS(), maxiters = 1000) + @test of(r.u, (prob, set_data!)) < of(u, (prob, set_data!)) + end + end + + @testset "BSplineInterpolation" begin + @named i = ParametrizedInterpolation( + BSplineInterpolation, u, x, 3, :Uniform, :Uniform) + eqs = [i.input.u ~ t, D(y) ~ i.output.u] + + @named model = System(eqs, t, systems = [i]) + sys = mtkcompile(model) + + prob = ODEProblem(sys, [], (0.0, 4)) + sol = solve(prob) + + @test SciMLBase.successful_retcode(sol) + end + + @testset "Initialization" begin + function MassSpringDamper(; name) + @named input = RealInput() + vars = @variables f(t) x(t)=0 dx(t) [guess = 0] ddx(t) + pars = @parameters m=10 k=1000 d=1 + + eqs = [f ~ input.u + ddx * 10 ~ k * x + d * dx + f + D(x) ~ dx + D(dx) ~ ddx] + + System(eqs, t, vars, pars; name, systems = [input]) + end + + function MassSpringDamperSystem(data, time; name) + @named src = ParametrizedInterpolation(LinearInterpolation, data, time) + @named clk = ContinuousClock() + @named model = MassSpringDamper() + + eqs = [connect(model.input, src.output) + connect(src.input, clk.output)] + + System(eqs, t; name, systems = [src, clk, model]) + end + + function generate_data() + dt = 4e-4 + time = 0:dt:0.1 + data = sin.(2 * pi * time * 100) + + return DataFrame(; time, data) + end + + df = generate_data() # example data + + @named system = MassSpringDamperSystem(df.data, df.time) + sys = mtkcompile(system) + prob = ODEProblem(sys, [], (0, df.time[end])) + sol = solve(prob) + + @test SciMLBase.successful_retcode(sol) + + prob2 = remake(prob, p = [sys.src.data => ones(length(df.data))]) + sol2 = solve(prob2) + + @test SciMLBase.successful_retcode(sol2) + end +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/test_analysis_points.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/test_analysis_points.jl new file mode 100644 index 0000000000..7244c7e4b0 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/test_analysis_points.jl @@ -0,0 +1,390 @@ +using Test, LinearAlgebra +using ModelingToolkit +using ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq +using ModelingToolkit: get_eqs, vars, @set!, t_nounits as t +using ControlSystemsBase + +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) + +@test_logs (:warn,) (:warn,) connect(P.input, :bad_connection, C.output) + +# Test with explicitly created AnalysisPoint +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) + +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 +# plot(sol) + +matrices, _ = get_sensitivity(sys, ap) +@test matrices.A[] == -2 +@test matrices.B[] * matrices.C[] == -1 # either one negative +@test matrices.D[] == 1 + +matrices, _ = get_comp_sensitivity(sys, ap) +@test matrices.A[] == -2 +@test matrices.B[] * matrices.C[] == 1 # both positive or negative +@test matrices.D[] == 0 + +#= +# 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) +=# + +# Test with automatically created analysis point +eqs = [connect(P.output, C.input) + connect(C.output, :plant_input, P.input)] +sys = System(eqs, t, systems = [P, C], name = :hej) + +matrices, _ = get_sensitivity(sys, :plant_input) +@test matrices.A[] == -2 +@test matrices.B[] * matrices.C[] == -1 # either one negative +@test matrices.D[] == 1 + +matrices, _ = get_comp_sensitivity(sys, :plant_input) +@test matrices.A[] == -2 +@test matrices.B[] * matrices.C[] == 1 # both positive +@test matrices.D[] == 0 + +## get_looptransfer + +matrices, _ = Blocks.get_looptransfer(sys, :plant_input) +@test matrices.A[] == -1 +@test matrices.B[] * matrices.C[] == -1 # either one negative +@test matrices.D[] == 0 +#= +# 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 +=# + +# Open loop +open_sys, (u, y) = Blocks.open_loop(sys, :plant_input) + +# Linearizing the open-loop system should yield the same system as get_looptransfer +matrices, _ = linearize(open_sys, [u], [y]) +@test matrices.A[] == -1 +@test matrices.B[] * matrices.C[] == -1 # either one negative +@test matrices.D[] == 0 + +# Test with more than one AnalysisPoint +eqs = [connect(P.output, :plant_output, C.input) + connect(C.output, :plant_input, P.input)] +sys = System(eqs, t, systems = [P, C], name = :hej) + +matrices, _ = get_sensitivity(sys, :plant_input) +@test matrices.A[] == -2 +@test matrices.B[] * matrices.C[] == -1 # either one negative +@test matrices.D[] == 1 + +## Test linearize between analysis points +matrices, _ = linearize(sys, :plant_input, :plant_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 + +# Test with output given by symbolic variable instead of analysis point +matrices2, _ = linearize(sys, :plant_input, [P.output.u]) +@test matrices2 == matrices + +## Test with subsystems + +@named P = FirstOrder(k = 1, T = 1) +@named C = 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)] + +# eqs = [connect(P.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(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)) +# sol = solve(prob, Rodas5()) +# plot(sol) + +matrices, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_input) + +using ControlSystemsBase # This is required to simplify the results to test against known solution +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 + +## A more complicated test case +using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks: Sine, PID, SecondOrder, Step, RealOutput +using ModelingToolkit: connect + +# Parameters +m1 = 1 +m2 = 1 +k = 1000 # Spring stiffness +c = 10 # Damping coefficient +@named inertia1 = Inertia(; J = m1) +@named inertia2 = Inertia(; J = m2) +@named spring = Spring(; c = k) +@named damper = Damper(; d = c) +@named torque = Torque() + +function SystemModel(u = nothing; name = :model) + eqs = [connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] + if u !== nothing + push!(eqs, connect(torque.tau, u.output)) + return 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) +# plot( +# plot(sol, vars = [filt.y, model.inertia1.phi, model.inertia2.phi]), +# plot(sol, vars = [pid.ctr_output.u], title = "Control signal"), +# legend = :bottomright, +# ) + +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 + +# plot(stepres, plotx=true, ploty=true, size=(800, 1200), leftmargin=5Plots.mm) +# plot!(sol, vars = [model.inertia2.phi], sp=1, l=:dash) + +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) + +## A simple multi-level system with loop openings +@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, _ = linearize(sys_inner, :u, :y, loop_openings = [:u]) +@test P_broken.A[] == -1 +P_broken, _ = linearize(sys_inner, :u, :y, loop_openings = [:y]) +@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 + +## Sensitivities in multivariate signals +import ControlSystemsBase as CS +import ModelingToolkitStandardLibrary.Blocks +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, _ = Blocks.get_sensitivity(sys, :plant_input) +S = CS.feedback(I(2), Kss * Pss, pos_feedback = true) + +# bodeplot([ss(matrices...), S]) +@test CS.tf(CS.ss(matrices...)) ≈ CS.tf(S) + +matrices, _ = Blocks.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, _ = Blocks.get_looptransfer( + sys, :plant_input) +L = Kss * Pss +@test CS.tf(CS.ss(matrices...)) ≈ CS.tf(L) + +matrices, _ = linearize(sys, :plant_input, :plant_output) +G = CS.feedback(Pss, Kss, pos_feedback = true) +@test CS.tf(CS.ss(matrices...)) ≈ CS.tf(G) + +## Multiple analysis points ==================================================== +@named P = FirstOrder(k = 1, T = 1) +@named C = 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], [sys_outer.inner.plant_output]) +G = CS.ss(matrices...) |> sminreal +@test tf(G) ≈ tf(CS.feedback(Ps, Cs)) diff --git a/lib/ModelingToolkitStandardLibrary/test/Blocks/utils.jl b/lib/ModelingToolkitStandardLibrary/test/Blocks/utils.jl new file mode 100644 index 0000000000..9880fe0915 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Blocks/utils.jl @@ -0,0 +1,49 @@ +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit +using OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "Array Guesses" begin + for (block, guess) in [ + (RealInputArray(; nin = 3, name = :a), zeros(3)), + (RealOutputArray(; nout = 3, name = :a), zeros(3)) + ] + guesses = ModelingToolkit.guesses(block) + @test guesses[@nonamespace block.u] == guess + end +end + +@testset "Scalarized Guesses" begin + for (block, guess) in [ + (RealInput(; name = :a), 0.0), + (RealInput(; nin = 3, name = :a), zeros(3)), + (RealOutput(; name = :a), 0.0), + (RealOutput(; nout = 3, name = :a), zeros(3)) + ] + guesses = ModelingToolkit.guesses(block) + @test guesses[@nonamespace block.u[1]] == guess[1] + end +end + +@testset "SISO Check" begin + k, w, d = 1.0, 1.0, 0.5 + @named c = Constant(; k = 1) + @named so = SecondOrder(; k = k, w = w, d = d, xd = 1) + @named iosys = System(connect(c.output, so.input), t, systems = [so, c]) + sys = mtkcompile(iosys) + + initsys = ModelingToolkit.generate_initializesystem(sys) + initsys = mtkcompile(initsys) + initprob = NonlinearProblem(initsys, [t => 0]) + initsol = solve(initprob) + + @test initsol[sys.so.xd] == 1.0 + @test initsol[sys.so.u] == 1.0 +end + +@test_deprecated RealInput(; name = :a, u_start = 1.0) +@test_deprecated RealInput(; name = :a, nin = 2, u_start = ones(2)) +@test_deprecated RealOutput(; name = :a, u_start = 1.0) +@test_deprecated RealOutput(; name = :a, nout = 2, u_start = ones(2)) +@test_deprecated RealInputArray(; name = :a, nin = 2, u_start = ones(2)) +@test_deprecated RealOutputArray(; name = :a, nout = 2, u_start = ones(2)) diff --git a/lib/ModelingToolkitStandardLibrary/test/Electrical/analog.jl b/lib/ModelingToolkitStandardLibrary/test/Electrical/analog.jl new file mode 100644 index 0000000000..cac7a1dd7a --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Electrical/analog.jl @@ -0,0 +1,875 @@ +using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks: Step, + Constant, Sine, Cosine, ExpSine, Ramp, + Square, Triangular +using ModelingToolkitStandardLibrary.Blocks: square, triangular +using ModelingToolkitStandardLibrary.Thermal: FixedTemperature +using OrdinaryDiffEq: ReturnCode.Success + +# using Plots + +@testset "sensors" begin + @named source = Sine(offset = 1, amplitude = 10, frequency = 5) + @named voltage = Voltage() + @named resistor = Resistor(R = 1) + @named capacitor = Capacitor(C = 1, v = 0.0) + @named ground = Ground() + + @named voltage_sensor = VoltageSensor() + @named current_sensor = CurrentSensor() + @named power_sensor = PowerSensor() + + connections = [connect(source.output, voltage.V) + connect(voltage.p, resistor.p) + connect(resistor.n, current_sensor.p) + connect(current_sensor.n, power_sensor.pc) + connect(power_sensor.nc, capacitor.p) + connect(capacitor.n, voltage.n, ground.g) + connect(capacitor.p, voltage_sensor.p) + connect(capacitor.n, voltage_sensor.n) + connect(capacitor.p, power_sensor.pv) + connect(capacitor.n, power_sensor.nv)] + + @named model = System(connections, t; + systems = [ + resistor, + capacitor, + source, + voltage, + ground, + voltage_sensor, + current_sensor, + power_sensor + ]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [], (0.0, 10.0)) + sol = solve(prob, Tsit5()) + + # Plots.plot(sol; vars=[capacitor.v, voltage_sensor.v]) + # Plots.plot(sol; vars=[power_sensor.power, capacitor.i * capacitor.v]) + # Plots.plot(sol; vars=[resistor.i, current_sensor.i]) + @test SciMLBase.successful_retcode(sol) + @test sol[capacitor.v]≈sol[voltage_sensor.v] atol=1e-3 + @test sol[power_sensor.power]≈sol[capacitor.i * capacitor.v] atol=1e-3 + @test sol[resistor.i]≈sol[current_sensor.i] atol=1e-3 +end + +# simple voltage divider +@testset "voltage divider with a short branch" begin + @named source = Constant(k = 10) + @named voltage = Voltage() + @named R0 = Resistor(R = 1e3) + @named R1 = Resistor(R = 1e3) + @named R2 = Resistor(R = 1e3) + @named ground = Ground() + @named short = Short() + + connections = [connect(source.output, voltage.V) + connect(voltage.p, R1.p) + connect(R1.n, short.p, R0.p) + connect(short.n, R0.n, R2.p) + connect(R2.n, voltage.n, ground.g)] + + @named model = System(connections, t, + systems = [R0, R1, R2, source, short, voltage, ground]; guesses = [ + R2.v => 0.0, R1.v => 0.0]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [], (0, 2.0)) + sol = solve(prob, Rodas4()) # has no state; does not work with Tsit5 + @test SciMLBase.successful_retcode(sol) + @test sol[short.v] == sol[R0.v] == zeros(length(sol.t)) + @test sol[R0.i] == zeros(length(sol.t)) + @test sol[R1.p.v][end]≈10 atol=1e-3 + @test sol[R1.n.v][end]≈5 atol=1e-3 + @test sol[R2.n.v][end]≈0 atol=1e-3 +end + +# simple RC +@testset "RC" begin + @named source = Constant(k = 10) + @named voltage = Voltage() + @named resistor = Resistor(R = 1) + @named capacitor = Capacitor(C = 1, v = 0.0) + @named ground = Ground() + + connections = [connect(source.output, voltage.V) + connect(voltage.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, voltage.n, ground.g)] + + @named model = System(connections, t; + systems = [resistor, capacitor, source, voltage, ground]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Tsit5()) + + # Plots.plot(sol; vars=[source.v, capacitor.v]) + @test SciMLBase.successful_retcode(sol) + @test sol[capacitor.v][end]≈10 atol=1e-3 +end + +# simple RL +@testset "RL" begin + @named source = Constant(k = 10) + @named voltage = Voltage() + @named resistor = Resistor(R = 1) + @named inductor = Inductor(L = 1.0, i = 0.0) + @named ground = Ground() + + connections = [connect(source.output, voltage.V) + connect(voltage.p, resistor.p) + connect(resistor.n, inductor.p) + connect(inductor.n, voltage.n, ground.g)] + + @named model = System(connections, t; + systems = [resistor, inductor, source, voltage, ground]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Tsit5()) + + # Plots.plot(sol; vars=[inductor.i, inductor.i]) + @test SciMLBase.successful_retcode(sol) + @test sol[inductor.i][end]≈10 atol=1e-3 +end + +@testset "RC with voltage sources" begin + R, C = 1, 1 + @named voltage = Voltage() + @named source_const = Constant(k = 10) + @named source_sin = Sine(offset = 1, amplitude = 10, frequency = 2, start_time = 0.5, + phase = 0) + @named source_step = Step(offset = 1, height = 10, start_time = 0.5) + @named source_tri = Triangular(offset = 1, start_time = 0.5, amplitude = 10, + frequency = 2) + @named source_dsin = ExpSine(offset = 1, amplitude = 10, frequency = 2, + start_time = 0.5, phase = 0, damping = 0.5) + @named source_ramp = Ramp(offset = 1, height = 10, start_time = 0.5, duration = 1) + sources = [source_const, source_sin, source_step, source_tri, source_dsin, source_ramp] + + @named resistor = Resistor(; R) + @named capacitor = Capacitor(; C, v = 0.0) + @named ground = Ground() + + for source in sources + connections = [connect(source.output, voltage.V) + connect(voltage.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, voltage.n, ground.g)] + + @named model = System(connections, t; + systems = [resistor, capacitor, source, ground, voltage]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + + # Plots.plot(sol; vars=[voltage.v, capacitor.v]) + end +end + +# RC with current sources +@testset "RC with current sources" begin + start_time = 2 + @named current = Current() + @named source = Step(start_time = 2) + @named resistor = Resistor(R = 1) + @named capacitor = Capacitor(C = 1, v = 0.0) + @named ground = Ground() + + connections = [connect(source.output, current.I) + connect(current.p, resistor.n) + connect(capacitor.n, resistor.p) + connect(capacitor.p, current.n, ground.g)] + + @named model = System(connections, t; + systems = [ground, resistor, current, capacitor, source]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Tsit5()) + y(x, st) = (x .> st) .* abs.(collect(x) .- st) + @test SciMLBase.successful_retcode(sol) + @test sum(reduce(vcat, sol[capacitor.v]) .- y(sol.t, start_time))≈0 atol=1e-2 +end + +@testset "Integrator" begin + R = 1e3 + f = 1 + Vin = 5 + @named ground = Ground() + @named R1 = Resistor(R = R) + @named R2 = Resistor(R = 100 * R) + @named C1 = Capacitor(C = 1 / (2 * pi * f * R), v = 0.0) + @named opamp = IdealOpAmp() + @named square_source = Square(amplitude = Vin) + @named voltage = Voltage() + @named sensor = VoltageSensor() + + connections = [connect(square_source.output, voltage.V) + connect(voltage.p, R1.p) + connect(R1.n, C1.n, R2.p, opamp.n1) + connect(opamp.p2, C1.p, R2.n) + connect(opamp.p1, ground.g, opamp.n2, voltage.n) + connect(opamp.p2, sensor.p) + connect(sensor.n, ground.g)] + @named model = System(connections, t, + systems = [ + R1, + R2, + opamp, + square_source, + voltage, + C1, + ground, + sensor + ]) + sys = mtkcompile(model) + u0 = [C1.v => 0.0 + R1.v => 0.0] + prob = ODEProblem(sys, u0, (0, 100.0)) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test sol[opamp.v2] == sol[C1.v] # Not a great one however. Rely on the plot + @test sol[opamp.p2.v] == sol[sensor.v] + + # plot(sol, vars=[sensor.v, square.v, C1.v]) +end + +_step(x, h, st) = ifelse(x < st, 0, h) +_cos_wave(x, f, A, st, ϕ) = A * cos(2 * π * f * (x - st) + ϕ) +_ramp(x, st, d, h) = ifelse(x < st, 0, + ifelse(x < (st + d), (x - st) * h / d, h)) +_sine_wave(x, f, A, st, ϕ) = A * sin(2 * π * f * (x - st) + ϕ) +_damped_sine_wave(x, f, A, st, ϕ, d) = exp((st - x) * d) * A * sin(2 * π * f * (x - st) + ϕ) + +@testset "Voltage function generators" begin + st, o, h, f, A, et, ϕ, d, δ = 0.7, 1.25, 3, 2, 2.5, 2, π / 4, 0.1, 0.0001 + + @named res = Resistor(R = 1) + @named cap = Capacitor(C = 1, v = 0.0) + @named ground = Ground() + @named voltage = Voltage() + @named voltage_sensor = VoltageSensor() + @named step = Step(start_time = st, offset = o, height = h) + @named cosine = Cosine(offset = o, amplitude = A, frequency = f, start_time = st, + phase = ϕ) + @named sine = Sine(offset = o, amplitude = A, frequency = f, start_time = st, phase = ϕ) + @named damped_sine = ExpSine(offset = o, amplitude = A, frequency = f, start_time = st, + phase = ϕ, damping = d) + @named ramp = Ramp(offset = o, start_time = st, duration = et - st, height = h) + @named vsquare = Square(offset = o, start_time = st, amplitude = A, frequency = f) + @named tri = Triangular(offset = o, start_time = st, amplitude = A, frequency = f) + # @named vsawtooth = SawTooth(amplitude=A, start_time=st, frequency=f, offset=o) + + sources = [step, cosine, sine, damped_sine, ramp, tri, vsquare] #, vsawtooth] + function waveforms(i, x) + getindex( + [o .+ _step.(x, h, st), + o .+ (x .> st) .* _cos_wave.(x, f, A, st, ϕ), + o .+ (x .> st) .* _sine_wave.(x, f, A, st, ϕ), + o .+ (x .> st) .* _damped_sine_wave.(x, f, A, st, ϕ, d), + o .+ _ramp.(x, st, (et - st), h), + triangular.(x, f, A, o, st), + square.(x, f, A, o, st)], + i) + end + # o .+ (x .> st). * _sawtooth_wave.(x, δ, f, A, st), + + for i in 1:lastindex(sources) + source = sources[i] + @info "Testing Voltage with $(nameof(source)) source" + eqs = [connect(source.output, voltage.V) + connect(voltage.p, voltage_sensor.p, res.p) + connect(res.n, cap.p) + connect(ground.g, voltage_sensor.n, voltage.n, cap.n)] + @named vmodel = System(eqs, t, + systems = [ + voltage_sensor, + res, + cap, + source, + voltage, + ground + ]) + vsys = mtkcompile(vmodel) + + u0 = [cap.v => 0.0] + + prob = ODEProblem(vsys, u0, (0, 10.0)) + sol = solve(prob, dt = 0.1, Tsit5()) + + @test SciMLBase.successful_retcode(sol) + @test sol[voltage.V.u]≈waveforms(i, sol.t) atol=1e-1 + @test sol[voltage.p.v] ≈ sol[voltage.V.u] + # For visual inspection + # plt = plot(sol; vars=[voltage.v]) + # savefig(plt, "test_voltage_$(source.name)") + end +end + +@testset "Current function generators" begin + st, o, h, f, A, et, ϕ, d, δ = 0.7, 1.25, 3, 2, 2.5, 2, π / 4, 0.1, 0.0001 + + @named ground = Ground() + @named res = Resistor(R = 1.0) + @named cap = Capacitor(C = 1, v = 0.0) + @named current_sensor = CurrentSensor() + @named current = Current() + @named step = Step(start_time = st, offset = o, height = h) + @named cosine = Cosine(offset = o, amplitude = A, frequency = f, start_time = st, + phase = ϕ) + @named sine = Sine(offset = o, amplitude = A, frequency = f, start_time = st, phase = ϕ) + @named damped_sine = ExpSine(offset = o, amplitude = A, frequency = f, start_time = st, + phase = ϕ, damping = d) + @named ramp = Ramp(offset = o, start_time = st, duration = et - st, height = h) + @named vsquare = Square(offset = o, start_time = st, amplitude = A, frequency = f) + @named tri = Triangular(offset = o, start_time = st, amplitude = A, frequency = f) + # @named isawtooth = SawTooth(amplitude=A, start_time=st, frequency=f, offset=o) + + sources = [step, cosine, sine, damped_sine, ramp, tri, vsquare] #, idamped_sine] + function waveforms(i, x) + getindex( + [o .+ _step.(x, h, st), + o .+ (x .> st) .* _cos_wave.(x, f, A, st, ϕ), + o .+ (x .> st) .* _sine_wave.(x, f, A, st, ϕ), + o .+ (x .> st) .* _damped_sine_wave.(x, f, A, st, ϕ, d), + o .+ _ramp.(x, st, (et - st), h), + triangular.(x, f, A, o, st), + square.(x, f, A, o, st)], + i) + end + # # o .+ (x .> st). * _sawtooth_wave.(x, δ, f, A, st) + + for i in 1:lastindex(sources) + source = sources[i] + @info "Testing Current with $(nameof(source)) source" + eqs = [connect(source.output, current.I) + connect(current.p, current_sensor.n) + connect(current_sensor.p, res.p) + connect(res.n, cap.p) + connect(current.n, ground.g, cap.n)] + @named model = System(eqs, t, + systems = [ + current_sensor, + source, + current, + res, + cap, + ground + ]) + isys = mtkcompile(model) + + u0 = [cap.v => 0.0] + + prob = ODEProblem(isys, u0, (0, 10.0)) + sol = solve(prob, dt = 0.1, Tsit5()) + + @test SciMLBase.successful_retcode(sol) + @test sol[current.I.u]≈waveforms(i, sol.t) atol=1e-1 + @test sol[current.I.u]≈sol[current.p.i] atol=1e-1 + # For visual inspection + # plt = plot(sol) + # savefig(plt, "test_current_$(source.name)") + end +end + +@testset "Diode component test" begin + @mtkmodel DiodeTest begin + @parameters begin + R = 1.0 + C = 1.0 + V = 10.0 + n = 1.0 + Is = 1e-3 + f = 1.0 + end + @components begin + resistor = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + diode = Diode(n = n, Is = Is) + ac = Sine(frequency = f, amplitude = V) + ground = Ground() + end + @equations begin + connect(ac.output, source.V) + connect(source.p, diode.p) + connect(diode.n, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + end + end + + @mtkcompile sys = DiodeTest() + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + + # Extract solutions for testing + diode_voltage = sol[sys.diode.v] + diode_current = sol[sys.diode.i] + resistor_current = sol[sys.resistor.i] + capacitor_voltage = sol[sys.capacitor.v] + + # Tests + @test all(diode_current .>= -1e-3) + @test capacitor_voltage[end] .≈ 8.26 rtol=3e-1 + + # For visual inspection + # plt = plot(sol; idxs = [diode.i, resistor.i, capacitor.v], + # size = (800, 600), dpi = 300, + # labels = ["Diode Current" "Resistor Current" "Capacitor Voltage"], + # title = "Diode Test") + # savefig(plt, "diode_test") +end + +@testset "HeatingDiode component test" begin + @mtkmodel HeatingDiodeTest begin + @parameters begin + R = 1.0 + C = 1.0 + V = 10.0 + T = 300.0 # Ambient temperature in Kelvin + n = 2.0 + Is = 1e-6 + f = 1.0 + end + @components begin + resistor = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + heating_diode = Diode(n = n, Is = Is, T_dep = true) + ac = Sine(frequency = f, amplitude = V) + ground = Ground() + temp = FixedTemperature(T = T) + end + @equations begin + connect(ac.output, source.V) + connect(source.p, heating_diode.p) + connect(heating_diode.n, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, ground.g) + connect(source.n, ground.g) + connect(temp.port, heating_diode.port) + end + end + + @mtkcompile sys = HeatingDiodeTest() + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob, Rodas4()) + + # Extract solutions for testing + diode_voltage = sol[sys.heating_diode.v] + diode_current = sol[sys.heating_diode.i] + resistor_current = sol[sys.resistor.i] + capacitor_voltage = sol[sys.capacitor.v] + + # Expected thermal voltage at given temperature + k = 1.380649e-23 # Boltzmann constant (J/K) + q = 1.602176634e-19 # Elementary charge (C) + + # Tests + @test all(diode_current .>= -1e-6) # Diode current should not exceed reverse saturation + @test capacitor_voltage[end]≈7.75 rtol=3e-1 # Final capacitor voltage close to input voltage + + # For visual inspection + # plt = plot(sol; vars = [heating_diode.i, resistor.i, capacitor.v], + # size = (800, 600), dpi = 300, + # labels = ["HeatingDiode Current" "Resistor Current" "Capacitor Voltage"], + # title = "HeatingDiode Test") + # savefig(plt, "heating_diode_test") + + # Remake model with higher amb. temperature, final capacitor voltage should be lower + T = 400.0 + reprob = remake(prob; p = [sys.T => T]) + sol = solve(reprob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test sol[sys.capacitor.v][end] < capacitor_voltage[end] +end + +@testset "VariableResistor with Temperature Dependency" begin + R_ref = 2.0 + R_const = 1.0 + + # Define the RC model as described + @mtkmodel RC begin + @parameters begin + R = R_ref # Variable resistance reference value + C = 1.0 # Capacitance + k = 10.0 # Voltage source scaling factor + f = 0.2 # Frequency of sine input + T = 300.0 # Ambient temperature in Kelvin + end + @components begin + res_input = Sine(frequency = f, amplitude = 1.0, offset = 0.0) + volt_input = Constant(k = 1.0) + resistor = VariableResistor(R_ref = R_ref, R_const = R_const, T_dep = true) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + temp = FixedTemperature(T = T) + ground = Ground() + end + @equations begin + connect(temp.port, resistor.port) + connect(res_input.output, resistor.position) + connect(volt_input.output, source.V) + connect(source.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + end + end + + # Build and solve the system + @mtkcompile sys = RC() + prob = ODEProblem(sys, [], (0.0, 10.0); guesses = [sys.resistor.i => 0.0]) # No state variables initially + sol = solve(prob) + + # Perform Tests + resistor_resistance = sol[sys.resistor.R] + capacitor_voltage = sol[sys.capacitor.v] + + @test SciMLBase.successful_retcode(sol) # Ensure the simulation is successful + @test all(resistor_resistance .>= R_const) # Resistance should be >= constant value + @test maximum(resistor_resistance) ≤ R_const + R_ref # Maximum resistance when pos=1 (R_const + R_ref) + @test all(capacitor_voltage .>= 0.0) # Capacitor voltage should not be negative + + # For visual inspection + # plt = plot(sol; vars = [sys.resistor.R, sys.capacitor.v], + # size = (800, 600), dpi = 300, + # labels = ["Variable Resistor Resistance" "Capacitor Voltage"], + # title = "RC Circuit Test with VariableResistor") + # savefig(plt, "rc_circuit_test_variable_resistor") +end +@testset "NMOS Transistor" begin + @mtkmodel SimpleNMOSCircuit begin + @components begin + Q1 = NMOS() + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + + Vcc_const = Constant(k = V_cc) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 5.0 + V_b = 3.5 + end + @equations begin + #voltage sources + connect(Vcc_const.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.s) + + #other stuff + connect(Vcc.p, Q1.d) + connect(Vb.p, Q1.g) + end + end + + @mtkbuild sys = SimpleNMOSCircuit(V_cc = 5.0, V_b = 3.5) + + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob) + @test sol[sys.Q1.d.i][1] > 0.0 + @test sol[sys.Q1.s.i][1] < 0.0 + @test sol[sys.Q1.g.i][1] == 0.0 + @test sol[sys.Q1.d.v][1] == 5.0 + @test sol[sys.Q1.s.v] < sol[sys.Q1.d.v] + + # test device symmetry + @mtkmodel FlippedNMOSCircuit begin + @components begin + Q1 = NMOS() + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + + Vcc_const = Constant(k = V_cc) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 5.0 + V_b = 3.5 + end + @equations begin + #voltage sources + connect(Vcc_const.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.d) + + #other stuff + connect(Vcc.p, Q1.s) + connect(Vb.p, Q1.g) + end + end + + @mtkbuild flipped_sys = FlippedNMOSCircuit(V_cc = 5.0, V_b = 3.5) + + flipped_prob = ODEProblem(flipped_sys, Pair[], (0.0, 10.0)) + flipped_sol = solve(flipped_prob) + @test flipped_sol[flipped_sys.Q1.d.i][1] < 0 + @test flipped_sol[flipped_sys.Q1.s.i][1] > 0 + @test flipped_sol[flipped_sys.Q1.s.v] > flipped_sol[flipped_sys.Q1.d.v] +end + +@testset "PMOS Transistor" begin + @mtkmodel SimplePMOSCircuit begin + @components begin + Q1 = PMOS() + Vs = Voltage() + Vb = Voltage() + Vd = Voltage() + ground = Ground() + + Vs_const = Constant(k = V_s) + Vb_const = Constant(k = V_b) + Vd_const = Constant(k = V_d) + end + + @parameters begin + V_s = 5.0 + V_b = 3.5 + V_d = 0.0 + end + @equations begin + #voltage sources + connect(Vs_const.output, Vs.V) + connect(Vb_const.output, Vb.V) + connect(Vd_const.output, Vd.V) + + #ground connections + connect(Vs.n, Vb.n, ground.g, Vd.n) + + connect(Vd.p, Q1.d) + #other stuff + connect(Vs.p, Q1.s) + connect(Vb.p, Q1.g) + end + end + + @mtkbuild sys = SimplePMOSCircuit(V_s = 5.0, V_b = 2.5, V_d = 3) + + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob) + + @test sol[sys.Q1.d.i][1] < 0.0 + @test sol[sys.Q1.s.i][1] > 0.0 + + # device symmetry + @mtkmodel FlippedPMOSCircuit begin + @components begin + Q1 = PMOS() + Vs = Voltage() + Vb = Voltage() + Vd = Voltage() + ground = Ground() + + Vs_const = Constant(k = V_s) + Vb_const = Constant(k = V_b) + Vd_const = Constant(k = V_d) + end + + @parameters begin + V_s = 5.0 + V_b = 3.5 + V_d = 0.0 + end + @equations begin + #voltage sources + connect(Vs_const.output, Vs.V) + connect(Vb_const.output, Vb.V) + connect(Vd_const.output, Vd.V) + + #ground connections + connect(Vs.n, Vb.n, ground.g, Vd.n) + + connect(Vd.p, Q1.s) + #other stuff + connect(Vs.p, Q1.d) + connect(Vb.p, Q1.g) + end + end + + @mtkbuild flipped_sys = FlippedPMOSCircuit(V_s = 5.0, V_b = 2.5, V_d = 3) + + flipped_prob = ODEProblem(flipped_sys, Pair[], (0.0, 10.0)) + flipped_sol = solve(flipped_prob) + + @test flipped_sol[flipped_sys.Q1.d.i][1] > 0.0 + @test flipped_sol[flipped_sys.Q1.s.i][1] < 0.0 +end + +@testset "NPN Tests" begin + @mtkmodel SimpleNPNCircuit begin + @components begin + Q1 = NPN() + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + + Vcc_const = Constant(k = V_cc) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 0.0 + V_b = 0.0 + end + @equations begin + #voltage sources + connect(Vcc_const.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.e) + + #other stuff + connect(Vcc.p, Q1.c) + connect(Vb.p, Q1.b) + end + end + + @mtkcompile sys = SimpleNPNCircuit(V_cc = 3.0, V_b = 0.70) + + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob) + + # make sure KCL is true + @test sol[sys.Q1.b.i][1] + sol[sys.Q1.e.i][1] + sol[sys.Q1.c.i][1] ≈ 0.0 + + # test NPN with substrate + @mtkmodel SimpleNPNCircuitSubstrate begin + @components begin + Q1 = NPN(use_substrate = true) + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + R1 = Resistor(R = 1000) + + Vcc_sine = Sine(frequency = 0.5) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 0.0 + V_b = 0.0 + end + @equations begin + #voltage sources + connect(Vcc_sine.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.e, Q1.s) + + #other stuff + connect(Vcc.p, R1.p) + connect(R1.n, Q1.c) + connect(Vb.p, Q1.b) + end + end + + @mtkcompile sys = SimpleNPNCircuitSubstrate(V_b = 0.70) + + prob = ODEProblem(sys, [sys.Q1.c.i => 0.0], (0.0, 10.0)) + sol = solve(prob) + + @test isapprox( + sol[sys.Q1.b.i][15] + + sol[sys.Q1.e.i][15] + + sol[sys.Q1.c.i][15] + + sol[sys.Q1.s.i][15], + 0.0, atol = 1e-16) +end + +@testset "PNP Tests" begin + @mtkmodel SimplePNPCircuit begin + @components begin + Q1 = PNP() + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + + Vcc_const = Constant(k = V_cc) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 0.0 + V_b = 0.0 + end + @equations begin + #voltage sources + connect(Vcc_const.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.e) + + #other stuff + connect(Vcc.p, Q1.c) + connect(Vb.p, Q1.b) + end + end + + @mtkcompile sys = SimplePNPCircuit(V_cc = 3.0, V_b = 0.70) + + prob = ODEProblem(sys, Pair[], (0.0, 10.0)) + sol = solve(prob) + + # make sure KCL is true + @test sol[sys.Q1.b.i][1] + sol[sys.Q1.e.i][1] + sol[sys.Q1.c.i][1] ≈ 0.0 + + # test PNP with substrate + @mtkmodel SimplePNPCircuitSubstrate begin + @components begin + Q1 = PNP(use_substrate = true) + Vcc = Voltage() + Vb = Voltage() + ground = Ground() + R1 = Resistor(R = 1000) + + Vcc_sine = Sine(frequency = 0.5) + Vb_const = Constant(k = V_b) + end + + @parameters begin + V_cc = 0.0 + V_b = 0.0 + end + @equations begin + #voltage sources + connect(Vcc_sine.output, Vcc.V) + connect(Vb_const.output, Vb.V) + + #ground connections + connect(Vcc.n, Vb.n, ground.g, Q1.e, Q1.s) + + #other stuff + connect(Vcc.p, R1.p) + connect(R1.n, Q1.c) + connect(Vb.p, Q1.b) + end + end + + @mtkcompile sys = SimplePNPCircuitSubstrate(V_b = 0.70) + + prob = ODEProblem(sys, [sys.Q1.c.i => 0.0], (0.0, 10.0); guesses = [sys.Q1.I_sub => 1.0]) + sol = solve(prob) + + @test isapprox( + sol[sys.Q1.b.i][15] + + sol[sys.Q1.e.i][15] + + sol[sys.Q1.c.i][15] + + sol[sys.Q1.s.i][15], + 0.0, + atol = 1e-16) +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Electrical/digital.jl b/lib/ModelingToolkitStandardLibrary/test/Electrical/digital.jl new file mode 100644 index 0000000000..eae00ae2f9 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Electrical/digital.jl @@ -0,0 +1,473 @@ +using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkitStandardLibrary.Electrical: _and, _or, _not, _xor +using ModelingToolkitStandardLibrary.Electrical: U, X, F0, F1, Z, W, L, H, DC, Uninitialized +using ModelingToolkitStandardLibrary.Electrical: AndTable, OrTable, NotTable, XorTable +using ModelingToolkitStandardLibrary.Electrical: get_logic_level +using OrdinaryDiffEq: ReturnCode.Success + +# using ModelingToolkitStandardLibrary.Electrical: Set, Reset + +@testset "Logic, logic-vectors and helpers" begin + # Logic and helper functions + @test length(instances(Logic)) == 9 + @test convert.(Logic, [1, 0]) |> typeof == Vector{Logic} + @test get_logic_level(Z) == 5 + + io = IOBuffer() + show(io, MIME("text/plain"), Uninitialized) + @test String(take!(io)) == "U" + + # Logic zeros and ones + @test zero(Logic) == zero(U) == F0 + @test one(Logic) == one(U) == F1 + @test ones(Logic, 2, 2) == [F1 F1 + F1 F1] + + # Logic vectors + u_logic = StdULogicVector([U, W, X, 1]) + @test typeof(u_logic.logic) == Vector{Logic} + @test get_logic_level(u_logic) == [1, 6, 2, 4] + + logic = StdLogicVector([U, W, X, 1]) + @test typeof(logic.logic) == Vector{Logic} + @test get_logic_level(logic) == [1, 6, 2, 4] + + # Predefined logic vectors + @test std_ulogic.logic == [U, X, F0, F1, Z, W, L, H, DC] + @test UX01.logic == [U, X, F0, F1] + @test UX01Z.logic == [U, X, F0, F1, Z] + @test X01.logic == [X, F0, F1] + @test X01Z.logic == [X, F0, F1, Z] + + # Logic vector helpers + test_logic_matrix = StdULogicVector([U F0 + F1 X]) + test_logic_vector = StdLogicVector([U, F0, F1, X]) + + size(test_logic_matrix) == (2, 2) + axes(test_logic_matrix) == (Base.OneTo(2), Base.OneTo(2)) + + getindex(test_logic_matrix, 1, 1) == U + getindex(test_logic_vector, 1) == U + + setindex!(test_logic_matrix, Z, 1, 1) + @test test_logic_matrix[1, 1] == Z + setindex!(test_logic_vector, Z, 1) + @test test_logic_vector[1] == Z + + # Logic helper functions + @test get_logic_level.([U, X, F0, F1, Z, W, L, H, DC]) == 1:9 + @test convert.(Logic, [1, 0, U]) == [F1, F0, U] + @test_throws "3 isn't a valid `Logic` value" convert(Logic, 3) +end + +@testset "Logic Tables" begin + # LogicTable vec-or-mat and helpers + test_not_logic_table = LogicTable([U, X, F1, F0, X, X, F1, F0, X]) + @test test_not_logic_table[1] == U + @test test_not_logic_table[F1] == F0 + + test_not_logic_table[1] = X + @test test_not_logic_table[1] == X + + @test_throws ArgumentError LogicTable([U; U]) +end + +@testset "Gate tables and logic gate helpers" begin + # logic tables and logic gate helpers + @test size(AndTable) == size(OrTable) == size(XorTable) == (9, 9) + @test size(NotTable) == (9,) + + # tests (Number, Number), (Logic, Logic), (Logic, Number) inputs + @test _and(1, 1, U, W, 1, 0) == F0 + @test _or(0, 1, U, 1) == F1 + @test _xor(0, 1, U, U, 1, 1) == U + # tests (Number, Logic) input + @test _and(1, F1) == F1 + @test _or(0, F0) == F0 + @test _xor(1, F0) == F1 + # tests Number and Logic (via internal convert) + @test _not(1) == F0 + + A = [U, W, Z, F1, F0] + B = [U, W, X, F0, DC] + _xor.(A, B) == _and.(_or.(A, _not.(B)), _or.(_not.(A), B)) +end + +#= + +@named set1 = Set() +@named reset1 = Reset() +@named set2 = Set() +@named reset2 = Reset() +@named out = DigitalPin() + +@testset "Not gate" begin + @named set = Set() + @named reset = Reset() + @named not = Not() + sources = [set, reset] + for source in sources + not_eqs = [connect(source.d, not.x) + connect(out, not.y)] + @named not_model = System(not_eqs, t, systems = [out, not, source]) + sys = alias_elimination(not_model) + u0 = [ + not.y.val => 0.0, + ] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test .!(sol[not.x.val] .> 0.5) == Bool.(sol[not.y.val]) + end +end + +@testset "And, Nand gate" begin + @named and = And() + @named nand = Nand() + for one in [set1, reset1], two in [set2, reset2] + and_eqs = [connect(one.d, and.x1) + connect(two.d, and.x2) + connect(out, and.y)] + @named and_model = System(and_eqs, t, systems = [and, one, two, out]) + sys = alias_elimination(and_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + @test sol.retcode == Success + @test sol[and.y.val] == _and.(sol[one.d.val], sol[two.d.val]) + + nand_eqs = [connect(one.d, nand.x1) + connect(two.d, nand.x2) + connect(out, nand.y)] + @named nand_model = System(nand_eqs, t, systems = [nand, one, two, out]) + sys = alias_elimination(nand_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + nsol = solve(prob, Rosenbrock23()) + @test nsol.retcode == Success + @test nsol[nand.y.val] == _not.(sol[and.y.val]) + end +end + +@testset "Or gate" begin + @named or = Or() + @named nor = Nor() + for one in [set1, reset1], two in [set2, reset2] + or_eqs = [connect(one.d, or.x1) + connect(two.d, or.x2) + connect(out, or.y)] + @named or_model = System(or_eqs, t, systems = [or, one, two, out]) + sys = alias_elimination(or_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + @test sol.retcode == Success + @test sol[or.y.val] == _or.(sol[one.d.val], sol[two.d.val]) + + nor_eqs = [connect(one.d, nor.x1) + connect(two.d, nor.x2) + connect(out, nor.y)] + @named nor_model = System(nor_eqs, t, systems = [nor, one, two, out]) + sys = alias_elimination(nor_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + nsol = solve(prob, Rosenbrock23()) + @test nsol.retcode == Success + @test nsol[nor.y.val] == _not.(sol[or.y.val]) + end +end + +@testset "Xor gate" begin + @named xor = Xor() + @named xnor = Xnor() + for one in [set1, reset1], two in [set2, reset2] + xor_eqs = [connect(one.d, xor.x1) + connect(two.d, xor.x2) + connect(out, xor.y)] + @named xor_model = System(xor_eqs, t, systems = [xor, one, two, out]) + sys = alias_elimination(xor_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + @test sol.retcode == Success + @test sol[xor.y.val] == + _or.(_and.(sol[one.d.val], _not.(sol[two.d.val])), + _and.(sol[two.d.val], _not.(sol[one.d.val]))) + + xnor_eqs = [connect(one.d, xnor.x1) + connect(two.d, xnor.x2) + connect(out, xnor.y)] + @named xnor_model = System(xnor_eqs, t, systems = [xnor, one, two, out]) + sys = alias_elimination(xnor_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + nsol = solve(prob, Rosenbrock23()) + @test nsol.retcode == Success + @test nsol[xnor.y.val] == _not.(sol[xor.y.val]) + end +end + +@testset "Half Adders" begin + @named ha = HalfAdder() + @named out1 = DigitalPin() + @named out2 = DigitalPin() + for one in [set1, reset1], two in [set2, reset2] + ha_eqs = [connect(one.d, ha.x1) + connect(two.d, ha.x2) + connect(out1, ha.y0) + connect(out2, ha.y1)] + @named ha_model = System(ha_eqs, t, systems = [ha, one, two, out1, out2]) + sys = alias_elimination(ha_model) + + u0 = [] + + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test sol[ha.y0.val] == sol[ha.sum] == _xor.(sol[one.d.val], sol[two.d.val]) + end +end + +@testset "Full Adder" begin + @named set3 = Set() + @named reset3 = Reset() + @named out1 = DigitalPin() + @named out2 = DigitalPin() + for one in [set1, reset1], two in [set2, reset2], three in [set3, reset3] + @named fa = FullAdder() + fa_eqs = [connect(one.d, fa.x1) + connect(two.d, fa.x2) + connect(three.d, fa.x3) + connect(out1, fa.y0) + connect(out2, fa.y1)] + @named fa_model = System(fa_eqs, t, + systems = [fa, one, two, three, out1, out2]) + sys = mtkcompile(fa_model) + + u0 = [] + + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test sol[fa.y0.val] == sol[fa.sum] == + _xor(sol[one.d.val], sol[two.d.val], sol[three.d.val]) + end +end + +@testset "Multiplexers" begin + @named mux2x1 = MUX(N = 2) + @named mux4x1 = MUX(N = 4) + @named demux1x2 = DEMUX(N = 2) + out = map(0:7) do i + DigitalPin(; name = Symbol(:out, i)) + end + input = map(0:7) do i + rand([Set(name = Symbol(:s, i)), Reset(name = Symbol(:r, i))]) + end + @named select0 = Set() + @named select1 = Reset() + + @info "Building a 2:1 MUX and 1:2 DEMUX..." + mux_eqs = [connect(input[1].d, mux2x1.d0) + connect(input[2].d, mux2x1.d1) + connect(select0.d, mux2x1.s0) + connect(mux2x1.y, demux1x2.d) + connect(select1.d, demux1x2.s0) + connect(out[1], demux1x2.y0) + connect(out[2], demux1x2.y1)] + @named mux_model = System(mux_eqs, t, + systems = [mux2x1, demux1x2, + out[2], out[1], select0, select1, + input[2], input[1]]) + sys = alias_elimination(mux_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test sol[mux2x1.y.val] == sol[mux2x1.d1.val] + @test sol[mux2x1.y.val] == sol[demux1x2.y0.val] + + @info "Building a 4:1 MUX..." + @named seta = Set() + @named setb = Set() + @named reseta = Reset() + @named resetb = Reset() + select0, select1 = [reseta, seta], [resetb, setb] + for idx1 in 1:2, idx0 in 1:2 + mux_eqs = [connect(input[1].d, mux4x1.d0) + connect(input[2].d, mux4x1.d1) + connect(input[3].d, mux4x1.d2) + connect(input[4].d, mux4x1.d3) + connect(select0[idx0].d, mux4x1.s0) + connect(select1[idx1].d, mux4x1.s1) + connect(out[1], mux4x1.y)] + @named mux_model = System(mux_eqs, t, + systems = [mux4x1, out[1], select0[idx0], + select1[idx1], + input[1], input[2], input[3], input[4]]) + sys = alias_elimination(mux_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + (idx1 == 1 && idx0 == 1) && @test sol[mux4x1.y.val] == sol[mux4x1.d0.val] + (idx1 == 1 && idx0 == 2) && @test sol[mux4x1.y.val] == sol[mux4x1.d1.val] + (idx1 == 2 && idx0 == 1) && @test sol[mux4x1.y.val] == sol[mux4x1.d2.val] + (idx1 == 2 && idx0 == 2) && @test sol[mux4x1.y.val] == sol[mux4x1.d3.val] + end +end + +@testset "Demultiplexers" begin + @named demux1x2 = DEMUX(N = 2) + @named demux1x4 = DEMUX(N = 4) + @named demux1x8 = DEMUX(N = 8) + @named input = Set() + out = map(1:4) do i + DigitalPin(; name = Symbol(:out, i)) + end + + @named seta = Set() + @named setb = Set() + @named reseta = Reset() + @named resetb = Reset() + select0, select1 = [reseta, seta], [resetb, setb] + for idx1 in 1:2, idx0 in 1:2 + @info "Building 1:4 DEMUX..." + eqs = [connect(input.d, demux1x4.d) + connect(select0[idx0].d, demux1x4.s0) + connect(select1[idx1].d, demux1x4.s1) + connect(out[1], demux1x4.y0) + connect(out[2], demux1x4.y1) + connect(out[3], demux1x4.y2) + connect(out[4], demux1x4.y3)] + @named demux_model = System(eqs, t, + systems = [demux1x4, select0[idx0], select1[idx1], + input, + out[1], out[2], out[3], out[4]]) + sys = alias_elimination(demux_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + (idx1 == 1 && idx0 == 1) && @test sol[demux1x4.d.val] == sol[demux1x4.y0.val] + (idx1 == 1 && idx0 == 2) && @test sol[demux1x4.d.val] == sol[demux1x4.y1.val] + (idx1 == 2 && idx0 == 1) && @test sol[demux1x4.d.val] == sol[demux1x4.y2.val] + (idx1 == 2 && idx0 == 2) && @test sol[demux1x4.d.val] == sol[demux1x4.y3.val] + end +end + +@testset "Encoder and Decoder" begin + @named enc4x2 = Encoder(N = 4) + @named dec2x4 = Decoder(n = 2) + out = map(1:4) do i + DigitalPin(; name = Symbol(:out, i)) + end + + input = map(0:7) do i + rand([Set(name = Symbol(:s, i)), Reset(name = Symbol(:r, i))]) + end + + @info "Building a 4:2 Encoder..." + enc_eqs = [connect(input[1].d, enc4x2.d0) + connect(input[2].d, enc4x2.d1) + connect(input[3].d, enc4x2.d2) + connect(input[4].d, enc4x2.d3) + connect(out[1], enc4x2.y0) + connect(out[2], enc4x2.y1)] + @named enc_model = System(enc_eqs, t, + systems = [enc4x2, out[1], out[2], + input[1], input[2], input[3], input[4]]) + sys = alias_elimination(enc_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test sol[enc4x2.y0.val] == _or.(sol[input[4].d.val], sol[input[2].d.val]) + @test sol[enc4x2.y1.val] == _or.(sol[input[4].d.val], sol[input[3].d.val]) + + @info "Building a 2:4 Decoder..." + dec_eqs = [connect(input[1].d, dec2x4.d0) + connect(input[2].d, dec2x4.d1) + connect(out[1], dec2x4.y0) + connect(out[2], dec2x4.y1) + connect(out[3], dec2x4.y2) + connect(out[4], dec2x4.y3)] + @named dec_model = System(dec_eqs, t, + systems = [dec2x4, out[1], out[2], + out[3], out[4], + input[1], input[2]]) + sys = alias_elimination(dec_model) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test sol[dec2x4.y0.val] == + _and.(_not.(sol[input[2].d.val]), + _not.(sol[input[1].d.val])) + @test sol[dec2x4.y1.val] == _and.(_not.(sol[input[2].d.val]), + (sol[input[1].d.val])) + @test sol[dec2x4.y2.val] == _and.((sol[input[2].d.val]), + _not.(sol[input[1].d.val])) + @test sol[dec2x4.y3.val] == _and.((sol[input[2].d.val]), (sol[input[1].d.val])) +end + +@testset "Sources and DigitalPin" begin + @named out = DigitalPin() + @named and = And() + @named setₐ = Set() + @named resetₐ = Reset() + @named setᵦ = Set() + @named resetᵦ = Reset() + + for α in [setₐ, resetₐ], β in [setᵦ, resetᵦ] + eqs = [connect(α.d, and.x1) + connect(β.d, and.x2) + connect(out, and.y)] + + @named pul = System(eqs, t, systems = [α, β, and, out]) + sys = alias_elimination(pul) + # sys = mtkcompile(pul) + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + sol = solve(prob, Rosenbrock23()) + + @test sol.retcode == Success + @test ModelingToolkitStandardLibrary._and.(sol[and.x1.val], sol[and.x2.val]) == + sol[and.y.val] + end + + # Test Pulse + @named pulse = Pulse() + @named pulseD = PulseDiff() + @named not = Not() + eqs = [connect(pulseD.d, not.x) + connect(out, not.y)] + @named pul = System(eqs, t, systems = [pulseD, not, out]) + sys = alias_elimination(pul) + # sys = mtkcompile(pul) + + u0 = [] + prob = ODEProblem(sys, u0, (0, 1.5)) + # sol = solve(prob, Rosenbrock23()) +end + +=# diff --git a/lib/ModelingToolkitStandardLibrary/test/Hydraulic/isothermal_compressible.jl b/lib/ModelingToolkitStandardLibrary/test/Hydraulic/isothermal_compressible.jl new file mode 100644 index 0000000000..407438a8bc --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Hydraulic/isothermal_compressible.jl @@ -0,0 +1,426 @@ +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible as IC +import ModelingToolkitStandardLibrary.Blocks as B +import ModelingToolkitStandardLibrary.Mechanical.Translational as T + +using ModelingToolkitStandardLibrary.Blocks: Parameter + +NEWTON = NLNewton( + check_div = false, always_new = true, max_iter = 100, relax = 9 // 10, κ = 1e-6) + +@testset "Fluid Domain and Tube" begin + function FluidSystem(N; bulk_modulus, name) + pars = @parameters begin + bulk_modulus = bulk_modulus + p_int = 0 + end + + systems = @named begin + fluid = IC.HydraulicFluid(; bulk_modulus) + stp = B.Step(; height = 10e5, offset = 0, start_time = 0.005, + duration = Inf, smooth = true) + src = IC.Pressure() + vol = IC.FixedVolume(; vol = 10.0, p_int) + res = IC.Tube(N; area = 0.01, length = 50.0, p_int) + end + + eqs = [connect(stp.output, src.p) + connect(fluid, src.port) + connect(src.port, res.port_a) + connect(res.port_b, vol.port)] + + System(eqs, t, [], pars; name, systems) + end + + @mtkcompile s1_1 = FluidSystem(1; bulk_modulus = 1e9) + @mtkcompile s1_2 = FluidSystem(1; bulk_modulus = 2e9) + @mtkcompile s5_1 = FluidSystem(5; bulk_modulus = 1e9) + + p1_1 = ODEProblem(s1_1, [], (0, 0.05)) + p1_2 = ODEProblem(s1_2, [], (0, 0.05)) + p5_1 = ODEProblem(s5_1, [], (0, 0.05)) + + sol1_1 = solve(p1_1, Rodas5P()) + sol1_2 = solve(p1_2, Rodas5P()) + sol5_1 = solve(p5_1, Rodas5P()) + + # fig = Figure() + # tm = 0:0.001:0.05 |> collect + # ax = Axis(fig[1,1]) + # lines!(ax, tm, sol1_1.(tm; idxs=s1_2.vol.port.p)); fig + # lines!(ax, tm, sol1_2.(tm; idxs=s1_1.vol.port.p)); fig + # lines!(ax, tm, sol5_1.(tm; idxs=s5_1.vol.port.p)); fig + # fig + + # higher stiffness should compress more quickly and give a higher pressure + @test sol1_2[s1_2.vol.port.p][end] > sol1_1[s1_1.vol.port.p][end] + + # N=5 pipe is compressible, will pressurize more slowly + @test sol1_1[s1_1.vol.port.p][end] > sol5_1[s5_1.vol.port.p][end] +end + +@testset "Valve" begin + function ValveSystem(; name) + pars = [] + + systems = @named begin + fluid = IC.HydraulicFluid() + sink = IC.FixedPressure(; p = 10e5) + vol = IC.FixedVolume(; vol = 5, p_int = 1e5) + valve = IC.Valve(; Cd = 1e5, minimum_area = 0) + ramp = B.Ramp(; + height = 0.1, duration = 0.1, offset = 0, start_time = 0.1, smooth = true) + end + + eqs = [connect(fluid, sink.port) + connect(sink.port, valve.port_a) + connect(valve.port_b, vol.port) + connect(valve.area, ramp.output)] + + System(eqs, t, [], pars; name, systems) + end + + @named valve_system = ValveSystem() + sys = mtkcompile(valve_system) + prob = ODEProblem(sys, [], (0, 1)) + sol = solve(prob, Rodas5P(); abstol = 1e-6, reltol = 1e-9) + s = complete(valve_system) + + # the volume should discharge to 10bar + @test sol[s.vol.port.p][end]≈10e5 atol=1e5 + + # fig = Figure() + # tm = 0:0.01:1 |> collect + # ax = Axis(fig[1,1]) + # lines!(ax, tm, sol.(tm; idxs=sys.vol.port.p)); + # fig +end + +@testset "DynamicVolume and minimum_volume feature" begin # Need help here + function TestSystem(; name, area = 0.01, length = 0.1, damping_volume = length * area * + 0.1) + pars = [] + + # DynamicVolume values + systems = @named begin + fluid = IC.HydraulicFluid(; bulk_modulus = 1e9) + + src1 = IC.Pressure(;) + src2 = IC.Pressure(;) + + vol1 = IC.DynamicVolume(; direction = +1, + area, + x_int = length, + x_max = length * 2, + x_min = length * 0.1, + x_damp = damping_volume / area + length * 0.1, + d = 1e3, + p_int = 10e5) + # vol1 = IC.Volume(;area, direction = +1, x_int=length) + + vol2 = IC.DynamicVolume(; direction = -1, + area, + x_int = length, + x_max = length * 2, + x_min = length * 0.1, + x_damp = damping_volume / area + length * 0.1, + d = 1e3, + p_int = 10e5) + # vol2 = IC.Volume(;area, direction = -1, x_int=length) + + mass = T.Mass(; m = 10) + + sin1 = B.Sine(; frequency = 0.5, amplitude = +1e5, offset = 10e5) + sin2 = B.Sine(; frequency = 0.5, amplitude = -1e5, offset = 10e5) + end + + eqs = [connect(fluid, src1.port) + connect(fluid, src2.port) + connect(src1.port, vol1.port) + connect(src2.port, vol2.port) + connect(vol1.flange, mass.flange, vol2.flange) + connect(src1.p, sin1.output) + connect(src2.p, sin2.output)] + + initialization_eqs = [mass.s ~ 0.0 + mass.v ~ 0.0] + + System(eqs, t, [], pars; name, systems, initialization_eqs) + end + + @named sys = TestSystem() + sys = mtkcompile(sys; allow_symbolic = true) + prob = ODEProblem(sys, [], (0, 5)) + sol = solve(prob, Rodas5P(); abstol = 1e-6, reltol = 1e-9) + # begin + # fig = Figure() + + # ax = Axis(fig[1,1], ylabel="position [m]", xlabel="time [s]") + # lines!(ax, sol.t, sol[sys.vol1.x]; label="vol1") + # lines!(ax, sol.t, sol[sys.vol2.x]; label="vol2") + # Legend(fig[1,2], ax) + + # ax = Axis(fig[2,1], ylabel="pressure [bar]", xlabel="time [s]") + # lines!(ax, sol.t, sol[sys.vol1.damper.port_a.p]/1e5; label="vol1") + # lines!(ax, sol.t, sol[sys.vol2.damper.port_a.p]/1e5; label="vol2") + # ylims!(ax, 10-2, 10+2) + + # ax = Axis(fig[3,1], ylabel="area", xlabel="time [s]") + # lines!(ax, sol.t, sol[sys.vol1.damper.area]; label="area 1") + # lines!(ax, sol.t, sol[sys.vol2.damper.area]; label="area 2") + + # display(fig) + # end + + # volume/mass should stop moving at opposite ends + @test sol(0; idxs = sys.vol1.x) == 0.1 + @test sol(0; idxs = sys.vol2.x) == 0.1 + + @test round(sol(1; idxs = sys.vol1.x); digits = 2) == 0.19 + @test round(sol(1; idxs = sys.vol2.x); digits = 2) == 0.01 + + @test round(sol(2; idxs = sys.vol1.x); digits = 2) == 0.01 + @test round(sol(2; idxs = sys.vol2.x); digits = 2) == 0.19 + + @test round(sol(3; idxs = sys.vol1.x); digits = 2) == 0.19 + @test round(sol(3; idxs = sys.vol2.x); digits = 2) == 0.01 + + @test round(sol(4; idxs = sys.vol1.x); digits = 2) == 0.01 + @test round(sol(4; idxs = sys.vol2.x); digits = 2) == 0.19 +end + +@testset "Actuator System" begin + function ActuatorSystem(use_input; name) + pars = @parameters begin + p_s = 200e5 + p_r = 5e5 + + A_1 = 360e-4 + A_2 = 360e-4 + + p_1 = 45e5 + p_2 = 45e5 + + l_1 = 1.5 + l_2 = 1.5 + m_f = 250 + g = 0 + + d = 100e-3 + + Cd = 0.01 + + m_piston = 880 + end + + vars = @variables begin + ddx(t) = 0 + end + + systems = @named begin + src = IC.FixedPressure(; p = p_s) + valve = IC.SpoolValve2Way(; g, m = m_f, d, Cd, x_int = 0) + piston = IC.Actuator(; + length_a_int = l_1, + length_b_int = l_2, + area_a = A_1, + area_b = A_2, + m = m_piston, + g = 0, + minimum_volume_a = A_1 * 1e-3, + minimum_volume_b = A_2 * 1e-3, + damping_volume_a = A_1 * 5e-3, + damping_volume_b = A_2 * 5e-3, + p_a_int = p_1, + p_b_int = p_2) + # body = T.Mass(; m = 1500) + # pipe = IC.Tube(1; area = A_2, length = 2.0, p_int = p_2) + snk = IC.FixedPressure(; p = p_r) + pos = T.Position() + + # m1 = IC.FlowDivider(; n = 3) + # m2 = IC.FlowDivider(; n = 3) + + fluid = IC.HydraulicFluid() + end + + if use_input + @named input = B.SampledData(Float64) + else + #@named input = B.TimeVaryingFunction(f) + @named input = B.Constant(k = 0) + end + + push!(systems, input) + + eqs = [connect(input.output, pos.s) + connect(valve.flange, pos.flange) + connect(valve.port_a, piston.port_a) + # connect(piston.flange, body.flange) + + connect(piston.port_b, valve.port_b) + + # connect(piston.port_b, pipe.port_b) + # # connect(piston.port_b, m1.port_a) + # # connect(m1.port_b, pipe.port_b) + + # connect(pipe.port_a, valve.port_b) + # # connect(pipe.port_a, m2.port_b) + # # connect(m2.port_a, valve.port_b) + + connect(src.port, valve.port_s) + connect(snk.port, valve.port_r) + connect(fluid, src.port, snk.port) + D(piston.mass.v) ~ ddx] + + initialization_eqs = [ + # body.s ~ 0 + ] + + System(eqs, t, vars, pars; name, systems, initialization_eqs) + end + + @mtkcompile initsys = ActuatorSystem(false) + + initprob = ODEProblem(initsys, [], (0, 0)) + initsol = solve(initprob, Rodas5P()) + + @mtkcompile sys = ActuatorSystem(true) + + dt = 1e-4 + time = 0:dt:0.1 + + x = @. (time - 0.015)^2 - 10 * (time - 0.02)^3 + x[1:150] = zeros(150) + + defs = ModelingToolkit.defaults(sys) + defs[sys.input.buffer] = Parameter(0.5 * x, dt) + + # NOTE: bypassing initialization system: https://github.com/SciML/ModelingToolkit.jl/issues/3312 + prob = ODEProblem(sys, initsol[1], (0, 0.1); build_initializeprob = false) + + #TODO: Implement proper initialization system after issue is resolved + #TODO: How to bring the body back and not have an overdetermined system? + + # check the fluid domain + @test Symbol(defs[sys.src.port.ρ]) == Symbol(sys.fluid.ρ) + @test Symbol(defs[sys.valve.port_s.ρ]) == Symbol(sys.fluid.ρ) + @test Symbol(defs[sys.valve.port_a.ρ]) == Symbol(sys.fluid.ρ) + @test Symbol(defs[sys.valve.port_b.ρ]) == Symbol(sys.fluid.ρ) + @test Symbol(defs[sys.valve.port_r.ρ]) == Symbol(sys.fluid.ρ) + @test Symbol(defs[sys.snk.port.ρ]) == Symbol(sys.fluid.ρ) + + @time sol = solve(prob, Rodas5P(); initializealg = NoInit()) + + @test sol[sys.ddx][1] == 0.0 + @test maximum(sol[sys.ddx]) > 200 + @test sol[sys.piston.x][end] > 0.6 +end + +@testset "Prevent Negative Pressure" begin + @component function HydraulicSystem(; name) + pars = @parameters let_gas = 1 + + systems = @named begin + fluid = IC.HydraulicFluid(; let_gas) + vol = IC.DynamicVolume(; area = 0.001, x_int = 0.05, + x_max = 0.1, x_damp = 0.02, x_min = 0.01, direction = +1, p_int = 100e5) + mass = T.Mass(; m = 100, g = -9.807) # s = 0.05 + cap = IC.Cap() + end + + eqs = [connect(fluid, cap.port, vol.port) + connect(vol.flange, mass.flange)] + + initialization_eqs = [mass.s ~ 0.05 + mass.v ~ 0] + + return System(eqs, t, [], pars; name, systems, initialization_eqs) + end + + @mtkcompile sys = HydraulicSystem() + + prob1 = ODEProblem(sys, [], (0, 0.05)) + # prob1 = remake(prob1; u0 = BigFloat.(prob1.u0)) + prob2 = ODEProblem(sys, [sys.let_gas => 0], (0, 0.05)) + + # @time sol1 = solve(prob1, Rodas5P(); abstol=1e-9, reltol=1e-9) #BUG: Using BigFloat gives... ERROR: MethodError: no method matching getindex(::Missing, ::Int64) + @time sol1 = solve(prob1, Rodas5P(); adaptive = false, dt = 1e-6) #TODO: fix BigFloat to implement abstol=1e-9, reltol=1e-9 + @time sol2 = solve(prob2, Rodas5P()) + + # case 1: no negative pressure will only have gravity pulling mass back down + # case 2: with negative pressure, added force pulling mass back down + # - case 1 should push the mass higher + @test sol1[sys.mass.s][end] > sol2[sys.mass.s][end] + + # case 1 should prevent negative pressure less than -1000 + @test minimum(sol1[sys.vol.port.p]) > -5000 + @test minimum(sol2[sys.vol.port.p]) < -5000 + + # fig = Figure() + # ax = Axis(fig[1,1]) + # lines!(ax, sol1.t, sol1[sys.vol.port.p]); fig + # lines!(ax, sol2.t, sol2[sys.vol.port.p]); fig + + # ax = Axis(fig[1,2]) + # lines!(ax, sol1.t, sol1[sys.mass.s]) + # lines!(ax, sol2.t, sol2[sys.mass.s]) + # fig +end + +#TODO +# @testset "Component Flow Reversals" begin +# # Check Component Flow Reversals +# function System(; name) +# pars = [] + +# systems = @named begin +# fluid = IC.HydraulicFluid() +# source = IC.Pressure() +# sink = IC.FixedPressure(; p = 101325) +# pipe = IC.Tube(1, false; area = 0.1, length =.1, head_factor = 1) +# osc = Sine(; frequency = 0.01, amplitude = 100, offset = 101325) +# end + +# eqs = [connect(fluid, pipe.port_a) +# connect(source.port, pipe.port_a) +# connect(pipe.port_b, sink.port) +# connect(osc.output, source.p)] + +# System(eqs, t, [], []; systems) +# end + +# @named sys = System() + +# syss = mtkcompile.([sys]) +# tspan = (0.0, 1000.0) +# prob = ODEProblem(sys, tspan) # u0 guess can be supplied or not +# @time sol = solve(prob) + +# end + +#TODO +# @testset "Tube Discretization" begin +# # Check Tube Discretization +# end + +#TODO +# @testset "Pressure BC" begin +# # Ensure Pressure Boundary Condition Works +# end + +#TODO +# @testset "Massflow BC" begin +# # Ensure Massflow Boundary Condition Works +# end + +#TODO +# @testset "Splitter Flow Test" begin +# # Ensure FlowDivider Splits Flow Properly +# # 1) Set flow into port A, expect reduction in port B + +# # 2) Set flow into port B, expect increase in port B +# end + +#TODO: Test Valve Inversion diff --git a/lib/ModelingToolkitStandardLibrary/test/Magnetic/magnetic.jl b/lib/ModelingToolkitStandardLibrary/test/Magnetic/magnetic.jl new file mode 100644 index 0000000000..0b356930a2 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Magnetic/magnetic.jl @@ -0,0 +1,60 @@ +using ModelingToolkitStandardLibrary.Magnetic, ModelingToolkit, OrdinaryDiffEq, Test + +import ModelingToolkitStandardLibrary.Electrical +import ModelingToolkitStandardLibrary.Blocks +import ModelingToolkitStandardLibrary.Magnetic +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t +using OrdinaryDiffEq: ReturnCode.Success +# using Plots + +@testset "Inductor" begin + mu_air = 1 + l_air = 0.0001 + mu_Fe = 1000 + l_Fe = 4 * 0.065 + a = b = 0.25 + + @named source = Blocks.Sine(amplitude = 230 * sqrt(2), frequency = 50, phase = pi / 2) + @named voltage = Electrical.Voltage() + @named r = Electrical.Resistor(R = 7.5) + @named ground = Electrical.Ground() + @named coil = Magnetic.FluxTubes.ElectroMagneticConverter(N = 600, Phi = 0.0) + @named ground_m = Magnetic.FluxTubes.Ground() + @named r_mAirPar = Magnetic.FluxTubes.ConstantReluctance(R_m = a * b * l_air * mu_air) + @named r_mFe = Magnetic.FluxTubes.ConstantReluctance(R_m = a * b * l_Fe * mu_Fe) + @named r_mLeak = Magnetic.FluxTubes.ConstantReluctance(R_m = 1.2e6) + connections = [connect(source.output, voltage.V) + connect(voltage.p, r.p) + connect(r.n, coil.p) + connect(voltage.n, coil.n) + connect(coil.port_p, r_mLeak.port_p) + connect(r_mLeak.port_p, r_mAirPar.port_p) + connect(r_mAirPar.port_n, r_mFe.port_p) + connect(r_mFe.port_n, r_mLeak.port_n) + connect(r_mFe.port_n, coil.port_n) + connect(ground.g, voltage.n) + connect(ground_m.port, r_mFe.port_n)] + @named model = System(connections, t, + systems = [ + source, + r, + ground, + coil, + ground_m, + r_mAirPar, + r_mFe, + r_mLeak, + voltage + ]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0, 0.1)) + sol = solve(prob, Rodas4()) + + # Plots.plot(sol; vars=[r.i]) + # Plots.plot(sol; vars=[r_mFe.V_m, r_mFe.Phi]) + + @test SciMLBase.successful_retcode(sol) + @test sol[r_mFe.Phi] == sol[r_mAirPar.Phi] + @test all(sol[coil.port_p.Phi] + sol[r_mLeak.Phi] + sol[r_mAirPar.Phi] .== 0) +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Mechanical/multibody.jl b/lib/ModelingToolkitStandardLibrary/test/Mechanical/multibody.jl new file mode 100644 index 0000000000..9c1f470c40 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Mechanical/multibody.jl @@ -0,0 +1,100 @@ +using ModelingToolkit +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Mechanical.MultiBody2D +using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition +using OrdinaryDiffEq +# using Setfield +using Test + +@named link1 = Link(; m = 1, l = 10, I = 84, g = -9.807) +@named link2 = Link(; m = 1, l = 10, I = 84, g = -9.807, x1_0 = 10) +@named cart = Mass(; m = 1, s = 0) +# @named force = SineForce(;amp=3e3, freq=15) +@named fixed = Fixed() +# @named m1 = Mass(;m=0.5) +# @named m2 = Mass(;m=0.5) + +eqs = [connect(link1.TX1, cart.flange) #, force.flange) + connect(link1.TY1, fixed.flange) + connect(link1.TX2, link2.TX1) + connect(link1.TY2, link2.TY1)] + +@named model = System(eqs, t, [], []; systems = [link1, link2, cart, fixed]) + +sys = mtkcompile(model) +@test length(unknowns(sys)) == 6 + +# The below code does work... +#= +unset_vars = setdiff(unknowns(sys), keys(ModelingToolkit.defaults(sys))) +prob = ODEProblem(sys, unset_vars .=> 0.0, (0.0, 20), []; jac = true) +sol = solve(prob, Rodas5P()) + +@test sol[cart.s][end] ≈ 4.767 atol=1e-3 + +plot(sol, idxs = [cart.s]) +=# + +#= +using CairoMakie +f = Figure() +a = Axis(f[1,1],xlabel="time [s]", ylabel="cart x pos. [m]") +lines!(a, sol.t, sol[cart.s]) +f + +function plot_link(sol, sys, tmax) + tm = Observable(0.0) + idx = Dict(reverse.(enumerate(unknowns(sys)))) + + fig = Figure() + a = Axis(fig[1,1], aspect=DataAspect(), ) + hidedecorations!(a) + s = @lift(sol($tm, idxs=[link1.x1, link1.x2, link2.x1, link2.x2, link1.y1, link1.y2, link2.y1, link2.y2])) + + m1x1 = @lift($s[1]) + m1x2 = @lift($s[2]) + m2x1 = @lift($s[3]) + m2x2 = @lift($s[4]) + + m1y1 = @lift($s[5]) + m1y2 = @lift($s[6]) + m2y1 = @lift($s[7]) + m2y2 = @lift($s[8]) + + sz1 = 0.5 + # lines!(a, [-sz1, sz1, sz1, -sz1, -sz1], @lift([$m1x1, $m1x1, $m1x1+sz1*2, $m1x1+sz1*2, $m1x1])) + + lines!(a, @lift([$m1x1, $m1x2]), @lift([$m1y1, $m1y2]), linewidth=10, color=:blue) + lines!(a, @lift([$m2x1, $m2x2]), @lift([$m2y1, $m2y2]), linewidth=10, color=:red) + + CairoMakie.ylims!(a, -40, 20) + CairoMakie.xlims!(a, -20, 40) + + # a = Axis(fig[1, 1], xlabel="time [s]", ylabel="position [m]") + # lines!(a, sol.t, sol[2,:]) + # lines!(a, sol.t, sol[4,:]) + + # scatter!(a, tm, m1x) + # scatter!(a, tm, m2x) + # ylims!(a, -60, 30) + + framerate = 30 + timestamps = range(0, tmax, step=1/framerate) + + record(fig, "links.mp4", timestamps; + framerate = framerate) do t + tm[] = t + end + + #= + CairoMakie.Makie.Record(fig, timestamps; framerate=framerate) do t + tm[] = t + end + =# + + nothing +end + +plot_link(sol, sys, 20) + +=# diff --git a/lib/ModelingToolkitStandardLibrary/test/Mechanical/rotational.jl b/lib/ModelingToolkitStandardLibrary/test/Mechanical/rotational.jl new file mode 100644 index 0000000000..b295e2bf14 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Mechanical/rotational.jl @@ -0,0 +1,322 @@ +using ModelingToolkitStandardLibrary.Mechanical.Rotational, + ModelingToolkit, OrdinaryDiffEq, + Test +using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq: ReturnCode.Success + +# using Plots + +@testset "two inertias" begin + @mtkmodel TwoInertia begin + @components begin + fixed = Fixed() + inertia1 = Inertia(J = 2) # this one is fixed + spring = Spring(c = 1e4) + damper = Damper(d = 10) + inertia2 = Inertia(J = 2, phi = pi / 2) + end + @equations begin + connect(fixed.flange, inertia1.flange_b) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(spring.flange_b, damper.flange_b, inertia2.flange_a) + end + end + + @mtkcompile sys = TwoInertia() + + prob = ODEProblem(sys, [D(D(sys.inertia2.phi)) => 0], (0, 10.0)) + sol1 = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol1) + + prob = DAEProblem( + sys, D.(unknowns(sys)) .=> prob.f(sol1.u[1], prob.p, 0.0), (0, 10.0)) + dae_sol = solve(prob, DFBDF()) + @test SciMLBase.successful_retcode(dae_sol) + @test all(dae_sol[sys.inertia1.w] .== 0) + @test dae_sol[sys.inertia2.w][end]≈0 atol=1e-3 # all energy has dissipated + + @mtkmodel WithSpringDamper begin + @components begin + fixed = Fixed() + inertia1 = Inertia(J = 2) # this one is fixed + springdamper = SpringDamper(; c = 1e4, d = 10) + inertia2 = Inertia(J = 2, phi = pi / 2) + end + @equations begin + connect(fixed.flange, inertia1.flange_b) + connect(inertia1.flange_b, springdamper.flange_a) + connect(springdamper.flange_b, inertia2.flange_a) + end + end + + @mtkcompile sys = WithSpringDamper() + + prob = ODEProblem(sys, [D(D(sys.inertia2.phi)) => 0], (0, 10.0)) + sol2 = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol2) + @test sol2(0:1:10, idxs = sys.inertia2.w).u≈sol1(0:1:10, idxs = sys.inertia2.w).u atol=1e-3 + + # Plots.plot(sol; vars=[inertia1.w, inertia2.w]) +end + +@testset "two inertias with driving torque" begin + @mtkmodel TwoInertiasWithDrivingTorque begin + @structural_parameters begin + amplitude = 10 # Amplitude of driving torque + frequency = 5 # Frequency of driving torque + J_motor = 0.1 # Motor inertia + end + + @components begin + fixed = Fixed() + torque = Torque(; use_support = true) + inertia1 = Inertia(J = 2, phi = pi / 2) + spring = Rotational.Spring(c = 1e4) + damper = Damper(d = 10) + inertia2 = Inertia(J = 4) + sine = Blocks.Sine(amplitude = amplitude, frequency = frequency) + end + + @equations begin + connect(sine.output, torque.tau) + connect(torque.support, fixed.flange) + connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(spring.flange_b, damper.flange_b, inertia2.flange_a) + end + end + + @mtkcompile sys = TwoInertiasWithDrivingTorque() + deqs = [eq.lhs => eq.rhs for eq in equations(sys)] + prob = DAEProblem( + sys, [deqs; + D(D(sys.inertia2.phi)) => 1.0; sys.spring.flange_b.phi => 0.0], (0, 10.0)) + sol = solve(prob, DFBDF()) + @test SciMLBase.successful_retcode(sol) + + prob = ODEProblem( + sys, [D(D(sys.inertia2.phi)) => 0.0, sys.spring.flange_b.phi => 0.0], (0, 1.0)) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + + # exact opposite oscillation with smaller amplitude J2 = 2*J1 and with an offset. + @test all(isapprox.( + sol[sys.inertia1.w], -sol[sys.inertia2.w] * 2 .+ sol[sys.inertia1.w][1], atol = 1)) + @test all(sol[sys.torque.flange.tau] .== -sol[sys.sine.output.u]) # torque source is equal to negative sine + + ## Test with constant torque source + @mtkmodel TwoInertiasWitConstantTorque begin + @components begin + fixed = Fixed() + torque = ConstantTorque(use_support = true, tau_constant = 1) + inertia1 = Inertia(J = 2, phi = pi / 2) + spring = Rotational.Spring(c = 1e4) + damper = Damper(d = 10) + inertia2 = Inertia(J = 4) + end + @equations begin + connect(torque.support, fixed.flange) + connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(spring.flange_b, damper.flange_b, inertia2.flange_a) + end + end + + @mtkcompile sys = TwoInertiasWitConstantTorque() + + prob = ODEProblem( + sys, [D(D(sys.inertia2.phi)) => 1.0, sys.spring.flange_b.phi => 0.0], (0, 10.0)) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test sol(sol.t[end], idxs = sys.inertia1.w)≈sol(sol.t[end], idxs = sys.inertia2.w) rtol=0.1 # both inertias have same angular velocity after initial transient +end + +# see: https://doc.modelica.org/Modelica%204.0.0/Resources/helpWSM/Modelica/Modelica.Mechanics.Rotational.Examples.First.html +@testset "first example" begin + @mtkmodel FirstExample begin + @structural_parameters begin + amplitude = 10 # Amplitude of driving torque + frequency = 5 # Frequency of driving torque + J_motor = 0.1 # Motor inertia + J_load = 2 # Load inertia + ratio = 10 # Gear ratio + damping = 10 # Damping in bearing of gear + end + + @components begin + fixed = Fixed() + torque = Torque(use_support = true) + inertia1 = Inertia(J = J_motor) + idealGear = IdealGear(ratio = ratio, use_support = true) + inertia2 = Inertia(J = 2) + spring = Spring(c = 1e4) + inertia3 = Inertia(J = J_load) + damper = Damper(d = damping) + sine = Blocks.Sine(amplitude = amplitude, frequency = frequency) + end + + @equations begin + connect(inertia1.flange_b, idealGear.flange_a) + connect(idealGear.flange_b, inertia2.flange_a) + connect(inertia2.flange_b, spring.flange_a) + connect(spring.flange_b, inertia3.flange_a) + connect(damper.flange_a, inertia2.flange_b) + connect(damper.flange_b, fixed.flange) + connect(sine.output, torque.tau) + connect(torque.support, fixed.flange) + connect(idealGear.support, fixed.flange) + connect(torque.flange, inertia1.flange_a) + end + end + + @mtkcompile sys = FirstExample() + prob = ODEProblem( + sys, [sys.inertia3.w => 0.0, sys.spring.flange_a.phi => 0.0], (0, 1.0)) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + # Plots.plot(sol; vars=[inertia2.w, inertia3.w]) +end + +@testset "Stick-Slip" begin + @mtkmodel VelocityProfile begin + @components begin + sine = Blocks.Sine(amplitude = 10, frequency = 0.1) + dz = Blocks.DeadZone(u_max = 2) + lim = Blocks.Limiter(y_max = 6) + output = Blocks.RealOutput() + end + @equations begin + connect(sine.output, dz.input) + connect(dz.output, lim.input) + connect(lim.output, output) + end + end + + @mtkmodel StickSlip begin + @components begin + fixed = Fixed() + spring = Spring(c = 6.5) + damper = Damper(d = 0.01) + inertia = Inertia(J = 0.0001) + friction = RotationalFriction(f = 0.001, tau_c = 20, w_brk = 0.06035, + tau_brk = 25) + vel_profile = VelocityProfile() + source = Speed() + angle_sensor = AngleSensor() + end + + @equations begin + connect(vel_profile.output, source.w_ref) + connect(source.flange, friction.flange_a) + connect(friction.flange_b, inertia.flange_a) + connect(inertia.flange_b, spring.flange_a, damper.flange_a) + connect(spring.flange_b, damper.flange_b, fixed.flange) + connect(angle_sensor.flange, inertia.flange_a) + end + end + + @mtkcompile sys = StickSlip() + prob = DAEProblem(sys, + [D.(unknowns(sys)) .=> 0.0; + [sys.inertia.flange_b.tau => 0.0; unknowns(sys) .=> 0.0...]], + (0, 10.0)) + + sol = solve(prob, DFBDF()) + @test SciMLBase.successful_retcode(sol) + @test sol[sys.angle_sensor.phi.u] == sol[sys.inertia.flange_a.phi] + + # p1 = Plots.plot(sol; vars=[inertia.flange_a.phi, source.phi], title="Angular Position", labels=["Inertia" "Source"], ylabel="Angle in rad") + # p2 = Plots.plot(sol; vars=[friction.w_rel], title="Rel. Angular Velocity of Friction", label="", ylabel="Angular Velocity in rad/s") + # Plots.plot(p1, p2, layout=(2, 1)) + # Plots.savefig("stick_slip.png") + + # Plots.scatter(sol[friction.w], sol[friction.tau], label="") +end + +@testset "sensors" begin + @mtkmodel Sensors begin + @components begin + fixed = Fixed() + inertia1 = Inertia(J = 2) # this one is fixed + spring = Spring(c = 1e4) + damper = Damper(d = 10) + inertia2 = Inertia(J = 2, phi = pi / 2) + speed_sensor = SpeedSensor() + torque_sensor = TorqueSensor() + rel_speed_sensor = RelSpeedSensor() + end + + @equations begin + connect(fixed.flange, inertia1.flange_b, rel_speed_sensor.flange_b) + connect(inertia1.flange_b, torque_sensor.flange_a) + connect(torque_sensor.flange_b, spring.flange_a, damper.flange_a, + speed_sensor.flange, rel_speed_sensor.flange_a) + connect(spring.flange_b, damper.flange_b, inertia2.flange_a) + end + end + + @mtkcompile sys = Sensors() + + prob = ODEProblem(sys, [D(D(sys.inertia2.phi)) => 0.0], (0, 10.0)) + sol = solve(prob, Rodas4()) + @test SciMLBase.successful_retcode(sol) + @test all(sol[sys.inertia1.w] .== 0) + @test all(sol[sys.inertia1.w] .== sol[sys.speed_sensor.w.u]) + @test sol[sys.inertia2.w][end]≈0 atol=1e-3 # all energy has dissipated + @test all(sol[sys.rel_speed_sensor.w_rel.u] .== sol[sys.speed_sensor.w.u]) + @test all(sol[sys.torque_sensor.tau.u] .== -sol[sys.inertia1.flange_b.tau]) + + prob = DAEProblem( + sys, D.(unknowns(sys)) .=> prob.f(sol.u[1], prob.p, 0.0), (0, 10.0)) + sol = solve(prob, DFBDF()) + @test SciMLBase.successful_retcode(sol) + @test all(sol[sys.inertia1.w] .== 0) + @test all(sol[sys.inertia1.w] .== sol[sys.speed_sensor.w.u]) + @test sol[sys.inertia2.w][end]≈0 atol=1e-3 # all energy has dissipated + @test all(sol[sys.rel_speed_sensor.w_rel.u] .== sol[sys.speed_sensor.w.u]) + @test all(sol[sys.torque_sensor.tau.u] .== -sol[sys.inertia1.flange_b.tau]) + + # Plots.plot(sol; vars=[inertia1.w, inertia2.w]) +end + +@testset "Position" begin + @mtkmodel TestPosition begin + @components begin + pos = Rotational.Position(exact = true, f_crit = 500) + input = Blocks.Sine(frequency = 1, amplitude = 1) + inertia = Rotational.Inertia(J = 1) + end + @equations begin + connect(input.output, pos.phi_ref) + connect(pos.flange, inertia.flange_a) + end + end + @mtkcompile sys = TestPosition() + prob = ODEProblem(sys, [], (0, 10.0)) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + tv = 0:0.01:10 + @test sol(tv, idxs = sys.inertia.phi).u≈sin.(2pi .* tv) atol=1e-12 + + @mtkmodel TestPosition begin + @components begin + pos = Rotational.Position(exact = false, f_crit = 500) + input = Blocks.Sine(frequency = 1, amplitude = 1) + inertia = Rotational.Inertia(J = 1) + end + @equations begin + connect(input.output, pos.phi_ref) + connect(pos.flange, inertia.flange_a) + end + end + @mtkcompile sys = TestPosition() + prob = ODEProblem(sys, [ + sys.inertia.phi => 0, + sys.inertia.w => 0 + ], (0, 10.0)) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + tv = 0:0.01:10 + @test sol(tv, idxs = sys.inertia.phi).u≈sin.(2pi .* tv) atol=1e-1 +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational.jl b/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational.jl new file mode 100644 index 0000000000..628ec7127f --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational.jl @@ -0,0 +1,239 @@ +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D + +using ModelingToolkitStandardLibrary.Blocks +import ModelingToolkitStandardLibrary.Mechanical.Translational as TV +import ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition as TP + +@testset "Free" begin + function TestSystem(; name) + systems = @named begin + acc = TV.Acceleration(false) + a = Constant(; k = -10) + mass = TV.Mass(; m = 100) + free = TV.Free() + end + + eqs = [connect(a.output, acc.a) + connect(mass.flange, acc.flange, free.flange)] + + System(eqs, t, [], []; name, systems) + end + + @named system = TestSystem() + s = complete(system) + sys = mtkcompile(system) + prob = ODEProblem(sys, [s.mass.s => 0], (0, 0.1)) + sol = solve(prob, Rosenbrock23()) + + @test sol[s.mass.flange.v][end]≈-0.1 * 10 atol=1e-3 + @test sol[s.free.f][end] ≈ 100 * 10 +end + +@testset "Spring, Damper, Mass, Fixed" begin + @named dv = TV.Damper(d = 1) + @named dp = TP.Damper(d = 1) + + @named sv = TV.Spring(k = 1) + @named sp = TP.Spring(k = 1, l = 1) + + @named bv = TV.Mass(m = 1) + @named bp = TP.Mass(m = 1, v = 1, s = 3) + + @named gv = TV.Fixed() + @named gp = TP.Fixed(s_0 = 1) + + function simplify_and_solve( + damping, spring, body, ground; initialization_eqs = Equation[]) + eqs = [connect(spring.flange_a, body.flange, damping.flange_a) + connect(spring.flange_b, damping.flange_b, ground.flange)] + + @named model = System(eqs, t; systems = [ground, body, spring, damping]) + + sys = mtkcompile(model) + + prob = ODEProblem( + sys, [], (0, 20.0); initialization_eqs, fully_determined = true) + sol = solve(prob; abstol = 1e-9, reltol = 1e-9) + + return sol + end + + solv = simplify_and_solve( + dv, sv, bv, gv; initialization_eqs = [bv.s ~ 3, bv.v ~ 1, sv.delta_s ~ 1]) + solp = simplify_and_solve(dp, sp, bp, gp) + + @test solv[bv.v][1] == 1.0 + @test solv[bv.v][end]≈0.0 atol=1e-4 + + @test solp[bp.v][1] == 1.0 + @test solp[bp.v][end]≈0.0 atol=1e-4 +end + +@testset "driven spring damper mass" begin + @named dv = TV.Damper(d = 1) + @named dp = TP.Damper(d = 1) + + @named sv = TV.Spring(k = 1) + @named sp = TP.Spring(k = 1, l = 1) + + @named bv = TV.Mass(m = 1) + @named bp = TP.Mass(m = 1, v = 1, s = 3) + + @named gv = TV.Fixed() + @named gp = TP.Fixed(s_0 = 1) + + @named fv = TV.Force() + @named fp = TP.Force() + + @named source = Sine(frequency = 3, amplitude = 2) + + function TestSystem(damping, spring, body, ground, f, source) + eqs = [connect(f.f, source.output) + connect(f.flange, body.flange) + connect(spring.flange_a, body.flange, damping.flange_a) + connect(spring.flange_b, damping.flange_b, ground.flange)] + + @named model = System(eqs, t; + systems = [ground, body, spring, damping, f, source]) + + return model + end + + model = TestSystem(dv, sv, bv, gv, fv, source) + sys = mtkcompile(model) + prob = ODEProblem( + sys, [bv.s => 0, sv.delta_s => 1], (0, 20.0), fully_determined = true) + solv = solve(prob, Rodas4()) + + model = TestSystem(dp, sp, bp, gp, fp, source) + sys = mtkcompile(model) + prob = ODEProblem(sys, [], (0, 20.0), fully_determined = true) + solp = solve(prob, Rodas4()) + + for sol in (solv, solp) + lb, ub = extrema(solv(15:0.05:20, idxs = bv.v).u) + @test -lb≈ub atol=1e-2 + @test -0.11 < lb < -0.1 + end +end + +@testset "sources & sensors" begin + @testset "Translational" begin + @testset "PositionSensor & ForceSensor" begin + function TestSystem(; name) + systems = @named begin + pos = TV.Position() + pos_sensor = TV.PositionSensor(; s = 1) + force = TV.Force() + force_sensor = TV.ForceSensor() + + spring = TV.Spring(; k = 1000) + + src1 = Sine(frequency = 100, amplitude = 2) + src2 = Sine(frequency = 100, amplitude = -1) + + pos_value = RealInput() + force_output = RealOutput() + end + + eqs = [connect(pos.s, src1.output) + connect(force.f, src2.output) + connect(pos.flange, force_sensor.flange_a) + connect(force_sensor.flange_b, spring.flange_a) + connect(spring.flange_b, force.flange, pos_sensor.flange) + connect(pos_value, pos_sensor.output) + connect(force_output, force_sensor.output)] + + System(eqs, t, [], []; name, systems) + end + + @named system = TestSystem() + s = complete(system) + sys = mtkcompile(system) + prob = ODEProblem(sys, [], (0, 1 / 400)) + sol = solve(prob, Rosenbrock23()) + + delta_s = 1 / 1000 + s_b = 2 - delta_s + 1 + + @test sol[s.pos_value.u][end]≈1.0 atol=1e-3 + @test all(sol[s.spring.flange_a.f] .== sol[s.force_output.u]) + end + + @testset "AccelerationSensor" begin + @named acc = TV.AccelerationSensor() + m = 4 + @named mass = TV.Mass(m = m) + @named force = TV.Force() + @named source = Sine(frequency = 2, amplitude = 1) + @named acc_output = RealOutput() + eqs = [ + connect(force.f, source.output), + connect(force.flange, mass.flange), + connect(acc.flange, mass.flange), + connect(acc_output, acc.output) + ] + @named sys = System( + eqs, t, [], []; systems = [force, source, mass, acc, acc_output]) + s = complete(mtkcompile(sys)) + prob = ODEProblem(s, [mass.s => 0], (0.0, pi)) + sol = solve(prob, Tsit5()) + @test sol[sys.acc_output.u] ≈ (sol[sys.mass.f] ./ m) + end + end + + @testset "TranslationalPosition" begin + @testset "PositionSensor & ForceSensor" begin + function mass_spring(; name) + systems = @named begin + fixed = TP.Fixed() + spring = TP.Spring(; k = 10.0, l = 1.0) + mass = TP.Mass(; m = 100.0, s = 2.0, v = 0.0) + pos_sensor = TP.PositionSensor() + force_sensor = TP.ForceSensor() + pos_value = RealOutput() + force_value = RealOutput() + end + eqs = [ + connect(fixed.flange, force_sensor.flange_a), + connect(force_sensor.flange_b, spring.flange_a), + connect(spring.flange_b, mass.flange, pos_sensor.flange), + connect(pos_sensor.output, pos_value), + connect(force_sensor.output, force_value) + ] + System(eqs, t, [], []; name, systems) + end + + @named model = mass_spring() + sys = mtkcompile(model) + + prob = ODEProblem(sys, [], (0.0, 1.0), fully_determined = true) + sol = solve(prob, Tsit5()) + + @test all(sol[sys.spring.flange_a.f] .== sol[sys.force_value.u]) + @test all(sol[sys.mass.s] .== sol[sys.pos_value.u]) + end + + @testset "AccelerationSensor" begin + @named acc = TP.AccelerationSensor() + m = 4 + @named mass = TP.Mass(m = m) + @named force = TP.Force() + @named source = Sine(frequency = 2, amplitude = 1) + @named acc_output = RealOutput() + eqs = [ + connect(force.f, source.output), + connect(force.flange, mass.flange), + connect(acc.flange, mass.flange), + connect(acc_output, acc.output) + ] + @named sys = System( + eqs, t, [], []; systems = [force, source, mass, acc, acc_output]) + s = complete(mtkcompile(sys)) + prob = ODEProblem(s, [], (0.0, pi), fully_determined = true) + sol = solve(prob, Tsit5()) + @test sol[sys.acc_output.u] ≈ (sol[sys.mass.f] ./ m) + end + end +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational_modelica.jl b/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational_modelica.jl new file mode 100644 index 0000000000..d27902742b --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Mechanical/translational_modelica.jl @@ -0,0 +1,109 @@ +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D + +using ModelingToolkitStandardLibrary.Blocks: Sine +using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Damper, Spring, Mass, + Fixed, Force, + SpringDamper, + Position + +@testset "spring damper mass fixed" begin + @mtkmodel SpringDamperMassFixed begin + @components begin + damper = Damper(; d = 1) + spring = Spring(; c = 1, s_rel0 = 1) + mass = Mass(; m = 1, v = 1, s = 0) + fixed = Fixed(s0 = 1) + end + @equations begin + connect(spring.flange_a, mass.flange_a, damper.flange_a) + connect(spring.flange_b, damper.flange_b, fixed.flange) + end + end + + @mtkcompile sys = SpringDamperMassFixed() + + prob = ODEProblem(sys, [], (0, 20.0)) + sol = solve(prob, ImplicitMidpoint(), dt = 0.01) + + @test sol[sys.mass.v][1] == 1.0 + @test sol[sys.mass.v][end]≈0.0 atol=1e-4 +end + +@testset "driven spring damper mass" begin + @mtkmodel DrivenSpringDamperMass begin + @components begin + damper = Damper(; d = 1) + spring = Spring(; c = 1, s_rel0 = 1) + mass = Mass(; m = 1, v = 1, s = 0) + fixed = Fixed(; s0 = 1) + force = Force() + source = Sine(frequency = 3, amplitude = 2) + end + + @equations begin + connect(force.f, source.output) + connect(force.flange, mass.flange_a) + connect(spring.flange_a, mass.flange_b, damper.flange_a) + connect(spring.flange_b, damper.flange_b, fixed.flange) + end + end + + @mtkcompile sys = DrivenSpringDamperMass() + + prob = ODEProblem(sys, [], (0, 20.0)) + sol = solve(prob, Rodas4()) + + lb, ub = extrema(sol(15:0.05:20, idxs = sys.mass.v).u) + @test -lb≈ub atol=1e-2 + @test -0.11 < lb < -0.1 +end + +@testset "driven SpringDamper mass" begin + @mtkmodel DrivenSpringDamperMass2 begin + @components begin + springdamper = SpringDamper(; d = 1, c = 1, s_rel0 = 1) + mass = Mass(; m = 1, v = 1, s = 0) + fixed = Fixed(; s0 = 1) + force = Force() + source = Sine(frequency = 3, amplitude = 2) + end + + @equations begin + connect(force.f, source.output) + connect(force.flange, mass.flange_a) + connect(springdamper.flange_a, mass.flange_b) + connect(springdamper.flange_b, fixed.flange) + end + end + + @mtkcompile sys = DrivenSpringDamperMass2() + + prob = ODEProblem(sys, [], (0, 20.0)) + sol = solve(prob, Rodas4()) + + lb, ub = extrema(sol(15:0.05:20, idxs = sys.mass.v).u) + @test -lb≈ub atol=1e-2 + @test -0.11 < lb < -0.1 +end + +@testset "Position source" begin + @mtkmodel TestPositionSource begin + @components begin + p1 = Position(exact = true) + source = Sine(frequency = 3, amplitude = 2) + mass = Mass(m = 1, v = 1, s = 0) + end + + @equations begin + connect(source.output, p1.s_ref) + connect(p1.flange, mass.flange_a) + end + end + + @mtkcompile sys = TestPositionSource() + prob = ODEProblem(sys, [], (0, 2pi)) + sol = solve(prob, Rodas4()) + tv = 0:0.1:(2pi) + @test sol(tv, idxs = sys.mass.s).u≈@.(2sin(2pi * tv * 3)) atol=1e-2 +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Thermal/demo.jl b/lib/ModelingToolkitStandardLibrary/test/Thermal/demo.jl new file mode 100644 index 0000000000..be81c878ea --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Thermal/demo.jl @@ -0,0 +1,26 @@ +using ModelingToolkitStandardLibrary.Thermal, ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +using OrdinaryDiffEq: ReturnCode.Success + +# Modelica example +@testset "demo" begin + @named mass1 = HeatCapacitor(C = 15, T = 373.15) + @named mass2 = HeatCapacitor(C = 15, T = 273.15) + @named conduction = ThermalConductor(G = 10) + @named Tsensor1 = TemperatureSensor() + @named Tsensor2 = TemperatureSensor() + + connections = [ + connect(mass1.port, conduction.port_a), + connect(conduction.port_b, mass2.port), + connect(mass1.port, Tsensor1.port), + connect(mass2.port, Tsensor2.port) + ] + + @named model = System(connections, t, + systems = [mass1, mass2, conduction, Tsensor1, Tsensor2]) + sys = mtkcompile(model) + prob = ODEProblem(sys, [], (0, 3.0)) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Thermal/motor.jl b/lib/ModelingToolkitStandardLibrary/test/Thermal/motor.jl new file mode 100644 index 0000000000..3b84ef51d7 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Thermal/motor.jl @@ -0,0 +1,56 @@ +using ModelingToolkit, Test +using ModelingToolkitStandardLibrary.Thermal +using ModelingToolkitStandardLibrary.Blocks + +# https://doc.modelica.org/Modelica%204.0.0/Resources/helpWSM/Modelica/Modelica.Thermal.HeatTransfer.Examples.Motor.html + +@testset "Thermal Motor Demo" begin + k2c(T) = T - 273.15 + + @mtkmodel ThermalMotor begin + @parameters begin + T_amb = 293.15 + end + @components begin + windingLosses = PrescribedHeatFlow(T_ref = k2c(95), alpha = 3.03e-3) + winding = HeatCapacitor(C = 2500, T = T_amb) + T_winding = TemperatureSensor() + winding2core = ThermalConductor(G = 10) + coreLosses = PrescribedHeatFlow() + core = HeatCapacitor(C = 25000, T = T_amb) + T_core = TemperatureSensor() + convection = ConvectiveConductor(G = 25) + environment = PrescribedTemperature() + amb = Constant(k = T_amb) + core_losses_const = Constant(k = 500) + winding_losses = Step(height = 900, offset = 100, start_time = 360, + duration = Inf, smooth = false) + end + @equations begin + connect(windingLosses.port, winding.port) + connect(coreLosses.port, core.port) + connect(winding.port, winding2core.port_a) + connect(winding2core.port_b, core.port) + connect(winding.port, T_winding.port) + connect(core.port, T_core.port) + connect(winding2core.port_b, convection.solid) + connect(convection.fluid, environment.port) + connect(amb.output, environment.T) + connect(winding_losses.output, windingLosses.Q_flow) + connect(core_losses_const.output, coreLosses.Q_flow) + end + end + + @mtkcompile motor = ThermalMotor() + prob = ODEProblem(motor, Pair[], (0, 720.0)) + sol = solve(prob) + + # plot(sol; vars=[T_winding.T, T_core.T]) + @test SciMLBase.successful_retcode(sol) + @test sol[motor.T_winding.T.u] == sol[motor.winding.T] + @test sol[motor.T_core.T.u] == sol[motor.core.T] + @test sol[-motor.core.port.Q_flow] ≈ + sol[motor.coreLosses.port.Q_flow + motor.convection.solid.Q_flow + motor.winding2core.port_b.Q_flow] + @test sol[motor.T_winding.T.u][end] >= 500 # not good but better than nothing + @test sol[motor.T_core.T.u] <= sol[motor.T_winding.T.u] +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Thermal/piston.jl b/lib/ModelingToolkitStandardLibrary/test/Thermal/piston.jl new file mode 100644 index 0000000000..25e24a181b --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Thermal/piston.jl @@ -0,0 +1,46 @@ +using ModelingToolkit, Test +using ModelingToolkitStandardLibrary.Thermal +using ModelingToolkitStandardLibrary.Blocks + +# Tests ConvectiveResistor and includes FixedTemperature and ThermalResistor + +@testset "Piston cylinder wall" begin + @info "Building a piston-cylinder..." + @mtkmodel Piston begin + @parameters begin + # ᵧ -> gas and ᵪ -> coolant + Tᵧ = 1000, [description = "Temperature of gas"] + Tᵪ = 10, [description = "Temperature of coolant"] + # R = 1/h; h is convection co-efficient + Rᵧ = 50e-4, [description = "Thermal resistance of gas"] + Rᵪ = 10e-4, [description = "Thermal resistance of coolant"] + R_wall = 1.5e-4 + end + @components begin + coolant = ConvectiveResistor(R = Rᵪ) + gas = ConvectiveResistor(R = Rᵧ) + wall = ThermalResistor(R = R_wall) + gas_tem = FixedTemperature(T = Tᵧ) + coolant_tem = FixedTemperature(T = Tᵪ) + end + @equations begin + connect(gas_tem.port, gas.solid) + connect(gas.fluid, wall.port_a) + connect(wall.port_b, coolant.fluid) + connect(coolant.solid, coolant_tem.port) + end + end + + @mtkcompile piston = Piston() + + prob = ODEProblem(piston, [], (0, 3.0)) + sol = solve(prob) + + # Heat-flow-rate is equal in magnitude + # and opposite in direction + @test SciMLBase.successful_retcode(sol) + # The initial value doesn't add up to absolute zero, while the rest do. To avoid + # tolerance on the latter, the test is split in two parts. + @test sol[piston.gas.Q_flow][1]≈-sol[piston.coolant.Q_flow][1] rtol=1e-6 + @test sol[piston.gas.Q_flow][2:end]≈-sol[piston.coolant.Q_flow][2:end] rtol=1e-6 +end diff --git a/lib/ModelingToolkitStandardLibrary/test/Thermal/thermal.jl b/lib/ModelingToolkitStandardLibrary/test/Thermal/thermal.jl new file mode 100644 index 0000000000..665bf9a95e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/Thermal/thermal.jl @@ -0,0 +1,179 @@ +using ModelingToolkitStandardLibrary.Thermal, ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks: Constant, Step +using OrdinaryDiffEq: ReturnCode.Success + +# Test HeatCapacitor, TemperatureSensor, RelativeTemperatureSensor, FixedTemperature +@testset "Heat systems" begin + T, C, G = 10.0, 10.0, 10.0 + @variables final_T(t) + @named mass1 = HeatCapacitor(C = C) + @named mass2 = HeatCapacitor(C = C) + @named th_conductor = ThermalConductor(G = G) + @named reltem_sensor = RelativeTemperatureSensor() + @named T_sensor1 = TemperatureSensor() + @named T_sensor2 = TemperatureSensor() + @named tem_src = FixedTemperature(T = T) + + @info "Building a single-body system..." + eqs = [connect(mass1.port, th_conductor.port_a) + connect(th_conductor.port_b, reltem_sensor.port_a) + connect(reltem_sensor.port_b, tem_src.port)] + @named h1 = System(eqs, t, systems = [mass1, reltem_sensor, tem_src, th_conductor]) + sys = mtkcompile(h1) + + u0 = [mass1.T => 2] + prob = ODEProblem(sys, u0, (0, 2.0)) + sol = solve(prob, Tsit5()) + + # Check if Relative temperature sensor reads the temperature of heat capacitor + # when connected to a thermal conductor and a fixed temperature source + @test SciMLBase.successful_retcode(sol) + @test sol[reltem_sensor.T.u] + sol[tem_src.port.T] == + sol[mass1.T] + sol[th_conductor.dT] + + @info "Building a two-body system..." + eqs = [connect(T_sensor1.port, mass1.port, th_conductor.port_a) + connect(th_conductor.port_b, mass2.port, T_sensor2.port) + final_T ~ (mass1.C * mass1.T + mass2.C * mass2.T) / + (mass1.C + mass2.C)] + @named h2 = System(eqs, t, [final_T], [], + systems = [mass1, mass2, T_sensor1, T_sensor2, th_conductor]) + sys = mtkcompile(h2) + + u0 = [mass1.T => 1.0 + mass2.T => 10.0] + prob = ODEProblem(sys, u0, (0, 3.0)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) + m1, m2 = sol.u[end] + @test m1≈m2 atol=1e-1 + @test sol[T_sensor1.T.u] == sol[sys.mass1.T] + @test sol[T_sensor2.T.u] == sol[sys.mass2.T] +end + +# Test HeatFlowSensor, FixedHeatFlow, ThermalResistor, ThermalConductor +@testset "Heat flow system" begin + C, G, R = 10, 10, 10 + @named flow_src = FixedHeatFlow(Q_flow = 50, alpha = 100) + @named mass1 = HeatCapacitor(C = C) + @named hf_sensor1 = HeatFlowSensor() + @named hf_sensor2 = HeatFlowSensor() + @named th_conductor = ThermalConductor(G = G) + @named th_resistor = ThermalResistor(R = R) + @named th_ground = FixedTemperature(T = 0) + + @info "Building a heat-flow system..." + eqs = [connect(mass1.port, th_resistor.port_a, th_conductor.port_a) + connect(th_conductor.port_b, flow_src.port, hf_sensor1.port_a, + hf_sensor2.port_a) + connect(th_resistor.port_b, hf_sensor1.port_b, hf_sensor2.port_b, + th_ground.port)] + @named h2 = System(eqs, t, + systems = [mass1, hf_sensor1, hf_sensor2, + th_resistor, flow_src, th_ground, th_conductor]) + sys = mtkcompile(h2) + + u0 = [mass1.T => 10.0] + prob = ODEProblem(sys, u0, (0, 3.0)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) + @test sol[th_conductor.dT] .* G == sol[th_conductor.Q_flow] + @test sol[th_conductor.Q_flow] ≈ sol[hf_sensor1.Q_flow.u] + sol[flow_src.port.Q_flow] + + @test sol[mass1.T] == sol[th_resistor.port_a.T] + @test sol[th_resistor.dT] ./ R ≈ sol[th_resistor.Q_flow] +end + +# Test ConvectiveConductor, BodyRadiation +@testset "Radiator system" begin + T_gas, T_coolant = 1000, 10 + R_wall = 10 + G = 0.04 + σ = 5.6703744191844294e-8 # Stefan-Boltzmann constant + + @named base = ThermalResistor(R = R_wall) + @named gas_tem = FixedTemperature(T = T_gas) + @named coolant_tem = FixedTemperature(T = T_coolant) + @named radiator = BodyRadiation(G = G) + @named dissipator = ConvectiveConductor(G = 10) + @named mass = HeatCapacitor(C = 10) + + @info "Building a radiator..." + eqs = [connect(gas_tem.port, radiator.port_a, base.port_a, dissipator.solid, mass.port) + connect(coolant_tem.port, base.port_b, radiator.port_b, dissipator.fluid)] + @named rad = System(eqs, t, + systems = [ + base, + gas_tem, + radiator, + dissipator, + coolant_tem, + mass + ]) + sys = mtkcompile(rad) + + u0 = [mass.T => T_gas] + prob = ODEProblem(sys, u0, (0, 3.0)) + sol = solve(prob, Rodas4()) + + @test SciMLBase.successful_retcode(sol) + @test sol[dissipator.dT] == sol[radiator.port_a.T] - sol[radiator.port_b.T] + rad_Q_flow = G * σ * (T_gas^4 - T_coolant^4) + @test sol[radiator.Q_flow] == fill(rad_Q_flow, length(sol[radiator.Q_flow])) +end + +@testset "Thermal Collector" begin + @named flow_src = FixedHeatFlow(Q_flow = 50, alpha = 100) + @named hf_sensor = HeatFlowSensor() + @named collector = ThermalCollector(m = 2) + @named th_resistor = ThermalResistor(R = 10) + @named tem_src = FixedTemperature(T = 10) + @named mass = HeatCapacitor(C = 10) + + @info "Building a heat collector..." + eqs = [connect(flow_src.port, collector.port_a1, th_resistor.port_a) + connect(tem_src.port, collector.port_a2) + connect(hf_sensor.port_a, collector.port_b) + connect(hf_sensor.port_b, mass.port, th_resistor.port_b)] + @named coll = System(eqs, t, + systems = [hf_sensor, flow_src, tem_src, + collector, th_resistor, mass]) + sys = mtkcompile(coll) + + prob = ODEProblem(sys, [], (0, 3.0)) + sol = solve(prob, Rodas4()) + + @test SciMLBase.successful_retcode(sol) + @test sol[collector.port_b.Q_flow] + sol[collector.port_a1.Q_flow] + + sol[collector.port_a2.Q_flow] == + zeros(length(sol[collector.port_b.Q_flow])) + @test sol[collector.port_b.T] == sol[collector.port_a1.T] == sol[collector.port_a2.T] +end + +@testset "FixedHeatFlow with alpha=0.0 test" begin + @mtkmodel TestModel begin + @components begin + temp = FixedTemperature(T = 300) + heatflow = FixedHeatFlow(Q_flow = -1.0) + wall = ThermalResistor(R = 1) + end + + @equations begin + connect(temp.port, wall.port_a) + connect(wall.port_b, heatflow.port) + end + end + + @info "Building a FixedHeatFlow with alpha=0.0" + @mtkcompile test_model = TestModel() + prob = ODEProblem(test_model, Pair[], (0, 10.0)) + sol = solve(prob) + + heat_flow = sol[test_model.heatflow.port.Q_flow] + + @test SciMLBase.successful_retcode(sol) # Ensure the simulation is successful + @test all(isapprox.(heat_flow, 1.0, rtol = 1e-6)) # Heat flow value should be equal to the fixed value defined +end diff --git a/lib/ModelingToolkitStandardLibrary/test/aqua.jl b/lib/ModelingToolkitStandardLibrary/test/aqua.jl new file mode 100644 index 0000000000..5d580adc12 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/aqua.jl @@ -0,0 +1,11 @@ +using ModelingToolkitStandardLibrary, Aqua +@testset "Aqua" begin + Aqua.find_persistent_tasks_deps(ModelingToolkitStandardLibrary) + Aqua.test_ambiguities(ModelingToolkitStandardLibrary, recursive = false) + Aqua.test_deps_compat(ModelingToolkitStandardLibrary) + Aqua.test_piracies(ModelingToolkitStandardLibrary) + Aqua.test_project_extras(ModelingToolkitStandardLibrary) + Aqua.test_stale_deps(ModelingToolkitStandardLibrary) + Aqua.test_unbound_args(ModelingToolkitStandardLibrary) + Aqua.test_undefined_exports(ModelingToolkitStandardLibrary) +end diff --git a/lib/ModelingToolkitStandardLibrary/test/chua_circuit.jl b/lib/ModelingToolkitStandardLibrary/test/chua_circuit.jl new file mode 100644 index 0000000000..552d88966e --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/chua_circuit.jl @@ -0,0 +1,50 @@ +using ModelingToolkit +using ModelingToolkit: t_nounits as t +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Electrical: OnePort +using OrdinaryDiffEq +using OrdinaryDiffEq: ReturnCode.Success +using IfElse: ifelse + +@testset "Chua Circuit" begin + @component function NonlinearResistor(; name, Ga, Gb, Ve) + @named oneport = OnePort() + @unpack v, i = oneport + pars = @parameters Ga=Ga Gb=Gb Ve=Ve + eqs = [ + i ~ ifelse(v < -Ve, + Gb * (v + Ve) - Ga * Ve, + ifelse(v > Ve, + Gb * (v - Ve) + Ga * Ve, + Ga * v)) + ] + extend(System(eqs, t, [], pars; name = name), oneport) + end + + @named L = Inductor(L = 18, i = 0.0) + @named Ro = Resistor(R = 12.5e-3) + @named G = Conductor(G = 0.565) + @named C1 = Capacitor(C = 10, v = 4) + @named C2 = Capacitor(C = 100, v = 0.0) + @named Nr = NonlinearResistor(Ga = -0.757576, + Gb = -0.409091, + Ve = 1) + @named Gnd = Ground() + + connections = [connect(L.p, G.p) + connect(G.n, Nr.p) + connect(Nr.n, Gnd.g) + connect(C1.p, G.n) + connect(L.n, Ro.p) + connect(G.p, C2.p) + connect(C1.n, Gnd.g) + connect(C2.n, Gnd.g) + connect(Ro.n, Gnd.g)] + + @named model = System(connections, t, systems = [L, Ro, G, C1, C2, Nr, Gnd]) + sys = mtkcompile(model) + prob = ODEProblem(sys, Pair[], (0, 5e4), saveat = 0.01) + sol = solve(prob, Rodas4()) + + @test sol.retcode == Success +end diff --git a/lib/ModelingToolkitStandardLibrary/test/multi_domain.jl b/lib/ModelingToolkitStandardLibrary/test/multi_domain.jl new file mode 100644 index 0000000000..afbd40d726 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/multi_domain.jl @@ -0,0 +1,204 @@ +using ModelingToolkitStandardLibrary.Electrical +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkitStandardLibrary.Thermal +import ModelingToolkitStandardLibrary +using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +using OrdinaryDiffEq: ReturnCode.Success +# using Plots + +@testset "DC motor" begin + f = 0.01 + k = 0.5 + R = 0.5 + tau_L_step = -3 + V_step = 10 + + @mtkmodel DCMotor begin + @structural_parameters begin + R = 0.5 + L = 4.5e-3 + k = 0.5 + J = 0.02 + f = 0.01 + V_step = 10 + tau_L_step = -3 + end + @components begin + ground = Ground() + source = Voltage() + voltage_step = Blocks.Step(height = V_step, start_time = 0) + R1 = Resistor(R = R) + L1 = Inductor(L = L, i = 0.0) + emf = EMF(k = k) + fixed = Fixed() + load = Torque() + load_step = Blocks.Step(height = tau_L_step, start_time = 3) + inertia = Inertia(J = J) + friction = Damper(d = f) + end + @equations begin + connect(fixed.flange, emf.support, friction.flange_b) + connect(emf.flange, friction.flange_a, inertia.flange_a) + connect(inertia.flange_b, load.flange) + connect(load_step.output, load.tau) + connect(voltage_step.output, source.V) + connect(source.p, R1.p) + connect(R1.n, L1.p) + connect(L1.n, emf.p) + connect(emf.n, source.n, ground.g) + end + end + + @mtkcompile dc_motor = DCMotor(; f, k, R, V_step) + + prob = ODEProblem(dc_motor, unknowns(dc_motor) .=> 0.0, (0, 6.0)) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + # EMF equations + @test -0.5 .* sol[dc_motor.emf.i] == sol[dc_motor.emf.flange.tau] + @test sol[dc_motor.emf.v] == 0.5 .* sol[dc_motor.emf.w] + # test steady-state values + dc_gain = [f/(k^2 + f * R) k/(k^2 + f * R); k/(k^2 + f * R) -R/(k^2 + f * R)] + idx_t = findfirst(sol.t .> 2.5) + @test sol[dc_motor.inertia.w][idx_t]≈(dc_gain * [V_step; 0])[2] rtol=1e-3 + @test sol[dc_motor.emf.i][idx_t]≈(dc_gain * [V_step; 0])[1] rtol=1e-3 + idx_t = findfirst(sol.t .> 5.5) + @test sol[dc_motor.inertia.w][idx_t]≈(dc_gain * [V_step; -tau_L_step])[2] rtol=1e-3 + @test sol[dc_motor.emf.i][idx_t]≈(dc_gain * [V_step; -tau_L_step])[1] rtol=1e-3 + + @test_skip begin + prob = DAEProblem(dc_motor, D.(unknowns(dc_motor)) .=> 0.0, (0, 6.0)) + sol = solve(prob, DFBDF()) + @test sol.retcode == Success + # EMF equations + @test -0.5 .* sol[dc_motor.emf.i] == sol[dc_motor.emf.flange.tau] + @test sol[dc_motor.emf.v] == 0.5 .* sol[dc_motor.emf.w] + # test steady-state values + dc_gain = [f/(k^2 + f * R) k/(k^2 + f * R); k/(k^2 + f * R) -R/(k^2 + f * R)] + idx_t = findfirst(sol.t .> 2.5) + @test sol[dc_motor.inertia.w][idx_t]≈(dc_gain * [V_step; 0])[2] rtol=1e-3 + @test sol[dc_motor.emf.i][idx_t]≈(dc_gain * [V_step; 0])[1] rtol=1e-3 + idx_t = findfirst(sol.t .> 5.5) + @test sol[dc_motor.inertia.w][idx_t]≈(dc_gain * [V_step; -tau_L_step])[2] rtol=1e-3 + @test sol[dc_motor.emf.i][idx_t]≈(dc_gain * [V_step; -tau_L_step])[1] rtol=1e-3 + end + # p1 = Plots.plot(sol, vars=[inertia.w], ylabel="Angular Vel. in rad/s", label="") + # p2 = Plots.plot(sol, vars=[emf.i], ylabel="Current in A", label="") + # Plots.plot(p1, p2, layout=(2,1)) + # Plots.savefig("dc_motor.png") +end + +@testset "DC motor with speed sensor" begin + R = 0.5 + k = 0.5 + f = 0.01 + V_step = 10 + tau_L_step = -3 + + @mtkmodel DCMotorWithSpeedSensor begin + @structural_parameters begin + R = 0.5 + L = 4.5e-3 + k = 0.5 + J = 0.02 + f = 0.01 + V_step = 10 + tau_L_step = -3 + end + @components begin + ground = Ground() + source = Voltage() + voltage_step = Blocks.Step(height = V_step, start_time = 0) + R1 = Resistor(R = R) + L1 = Inductor(L = L, i = 0.0) + emf = EMF(k = k) + fixed = Fixed() + load = Torque() + load_step = Blocks.Step(height = tau_L_step, start_time = 3) + inertia = Inertia(J = J) + friction = Damper(d = f) + speed_sensor = SpeedSensor() + end + @equations begin + 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(voltage_step.output, source.V) + connect(source.p, R1.p) + connect(R1.n, L1.p) + connect(L1.n, emf.p) + connect(emf.n, source.n, ground.g) + end + end + + @mtkcompile sys = DCMotorWithSpeedSensor(; f, k, R, V_step, tau_L_step) + + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0, 6.0)) + sol = solve(prob, Rodas4()) + + @test sol.retcode == Success + # EMF equations + @test -0.5 .* sol[sys.emf.i] == sol[sys.emf.flange.tau] + @test sol[sys.emf.v] == 0.5 .* sol[sys.emf.w] + + # test steady-state values + dc_gain = [f/(k^2 + f * R) k/(k^2 + f * R); k/(k^2 + f * R) -R/(k^2 + f * R)] + idx_t = findfirst(sol.t .> 2.5) + @test sol[sys.inertia.w][idx_t]≈(dc_gain * [V_step; 0])[2] rtol=1e-3 + @test sol[sys.emf.i][idx_t]≈(dc_gain * [V_step; 0])[1] rtol=1e-3 + idx_t = findfirst(sol.t .> 5.5) + @test sol[sys.inertia.w][idx_t]≈(dc_gain * [V_step; -tau_L_step])[2] rtol=1e-3 + @test sol[sys.emf.i][idx_t]≈(dc_gain * [V_step; -tau_L_step])[1] rtol=1e-3 + @test all(sol[sys.inertia.w] .== sol[sys.speed_sensor.w.u]) + + @test_skip begin + prob = DAEProblem(sys, D.(unknowns(sys)) .=> 0.0, (0, 6.0)) + sol = solve(prob, DFBDF()) + @test sol.retcode == Success + # EMF equations + @test -0.5 .* sol[sys.emf.i] == sol[sys.emf.flange.tau] + @test sol[sys.emf.v] == 0.5 .* sol[sys.emf.w] + # test steady-state values + dc_gain = [f/(k^2 + f * R) k/(k^2 + f * R); k/(k^2 + f * R) -R/(k^2 + f * R)] + idx_t = findfirst(sol.t .> 2.5) + @test sol[sys.inertia.w][idx_t]≈(dc_gain * [V_step; 0])[2] rtol=1e-3 + @test sol[sys.emf.i][idx_t]≈(dc_gain * [V_step; 0])[1] rtol=1e-3 + idx_t = findfirst(sol.t .> 5.5) + @test sol[sys.inertia.w][idx_t]≈(dc_gain * [V_step; -tau_L_step])[2] rtol=1e-3 + @test sol[sys.emf.i][idx_t]≈(dc_gain * [V_step; -tau_L_step])[1] rtol=1e-3 + # + @test all(sol[sys.inertia.w] .== sol[sys.speed_sensor.w.u]) + end +end + +@testset "Electrical Heating Circuit" begin + @mtkmodel ElHeatingCircuit begin + @components begin + ground = Ground() + source = Voltage() + voltage_sine = Blocks.Sine(amplitude = 220, frequency = 1) + heating_resistor = Resistor(R = 100, alpha = 1e-3, + T_ref = 293.15, T_dep = true) + thermal_conductor = ThermalConductor(G = 50) + env = FixedTemperature(T = 273.15 + 20) + end + @equations begin + connect(source.n, ground.g, heating_resistor.n) + connect(source.p, heating_resistor.p) + connect(voltage_sine.output, source.V) + connect(heating_resistor.heat_port, thermal_conductor.port_a) + connect(thermal_conductor.port_b, env.port) + end + end + + @mtkcompile sys = ElHeatingCircuit() + + prob = ODEProblem(sys, [], (0, 6.0); guesses = [sys.heating_resistor.i => 0.0]) + sol = solve(prob, Rodas4()) + @test sol.retcode == Success + @test sol[sys.source.v * sys.source.i] == -sol[sys.env.port.Q_flow] +end diff --git a/lib/ModelingToolkitStandardLibrary/test/runtests.jl b/lib/ModelingToolkitStandardLibrary/test/runtests.jl new file mode 100644 index 0000000000..c65f7b2759 --- /dev/null +++ b/lib/ModelingToolkitStandardLibrary/test/runtests.jl @@ -0,0 +1,81 @@ +using SafeTestsets, Test + +const GROUP = get(ENV, "GROUP", "All") + +@time begin + if GROUP == "QA" || GROUP == "All" + @time @safetestset "Aqua" begin + include("aqua.jl") + end + end + + if GROUP == "Core" || GROUP == "All" + @testset "Core" begin + # Blocks + @safetestset "Blocks: utils" begin + include("Blocks/utils.jl") + end + @safetestset "Blocks: math" begin + include("Blocks/math.jl") + end + @safetestset "Blocks: nonlinear" begin + include("Blocks/nonlinear.jl") + end + @safetestset "Blocks: continuous" begin + include("Blocks/continuous.jl") + end + @safetestset "Blocks: sources" begin + include("Blocks/sources.jl") + end + @safetestset "Blocks: analysis points" begin + include("Blocks/test_analysis_points.jl") + end + + # Electrical + @safetestset "Analog Circuits" begin + include("Electrical/analog.jl") + end + + @safetestset "Digital Circuits" begin + include("Electrical/digital.jl") + end + @safetestset "Chua Circuit Demo" begin + include("chua_circuit.jl") + end + + # Thermal + @safetestset "Thermal Circuits" begin + include("Thermal/thermal.jl") + end + @safetestset "Thermal Demo" begin + include("Thermal/demo.jl") + include("Thermal/motor.jl") + include("Thermal/piston.jl") + end + + # Magnetic + @safetestset "Magnetic" begin + include("Magnetic/magnetic.jl") + end + + # Mechanical + @safetestset "Mechanical Rotation" begin + include("Mechanical/rotational.jl") + end + @safetestset "Mechanical Translation" begin + include("Mechanical/translational.jl") + end + @safetestset "Mechanical Translation Modelica" begin + include("Mechanical/translational_modelica.jl") + end + @safetestset "Multi-Domain" begin + include("multi_domain.jl") + end + + # Hydraulic + @safetestset "Hydraulic IsothermalCompressible" begin + include("Hydraulic/isothermal_compressible.jl") + end + end + end +end From 5b3ab7e0f5ebb285f295f30b0720bf3cbe086d53 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 29 Oct 2025 14:14:17 +0530 Subject: [PATCH 2/6] ci: change CI workflows to handle subpackages --- .github/workflows/CompatHelper.yml | 2 +- .github/workflows/Documentation-MTKStdlib.yml | 23 +++++ .github/workflows/TagBot.yml | 9 +- .github/workflows/Tests.yml | 85 +++++++++++++++---- 4 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/Documentation-MTKStdlib.yml diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index a4be307e55..c85ddcff86 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -13,4 +13,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} - run: julia -e 'using CompatHelper; CompatHelper.main(;subdirs=["", "docs"])' + run: julia -e 'using CompatHelper; CompatHelper.main(;subdirs=["", "docs", "lib/ModelingToolkitStandardLibrary"])' diff --git a/.github/workflows/Documentation-MTKStdlib.yml b/.github/workflows/Documentation-MTKStdlib.yml new file mode 100644 index 0000000000..b064570c40 --- /dev/null +++ b/.github/workflows/Documentation-MTKStdlib.yml @@ -0,0 +1,23 @@ +name: "Documentation - MTKStandardLibrary" + +on: + push: + branches: + - master + paths: + - 'lib/ModelingToolkitStandardLibrary' + tags: '*' + pull_request: + paths: + - 'lib/ModelingToolkitStandardLibrary' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} + +jobs: + build-and-deploy-docs: + name: "Documentation" + uses: "SciML/.github/.github/workflows/documentation.yml@v1" + secrets: "inherit" + working-directory: "lib/ModelingToolkitStandardLibrary" diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b662..a96b9dfe3f 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -9,7 +9,14 @@ jobs: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - - uses: JuliaRegistries/TagBot@v1 + - name: Tag ModelingToolkit + uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} + - name: Tag ModelingToolkitStandardLibrary + uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} + subdir: lib/ModelingToolkitStandardLibrary diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 67c6806708..7728b0eaa9 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -5,14 +5,15 @@ on: branches: - master - 'release-' - - v10 paths-ignore: - 'docs/**' + - 'lib/ModelingToolkitStandardLibrary/docs/**' push: branches: - master paths-ignore: - 'docs/**' + - 'lib/ModelingToolkitStandardLibrary/docs/**' - 'benchmark/**' concurrency: @@ -32,21 +33,69 @@ jobs: - "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' }} + - "ModelingToolkit - InterfaceI" + - "ModelingToolkit - InterfaceII" + - "ModelingToolkit - Initialization" + - "ModelingToolkit - SymbolicIndexingInterface" + - "ModelingToolkit - Extended" + - "ModelingToolkit - Extensions" + - "ModelingToolkit - Downstream" + - "ModelingToolkit - RegressionI" + - "ModelingToolkit - FMI" + - "ModelingToolkitStandardLibrary - QA" + - "ModelingToolkitStandardLibrary - Core" secrets: "inherit" + steps: + - uses: actions/checkout@v4 + + - name: "Setup Julia ${{ matrix.version }}" + uses: julia-actions/setup-julia@v2 + with: + version: "${{ matrix.version }}" + arch: "${{ runner.arch }}" + + - uses: julia-actions/cache@v1 + if: ${{ vars.USE_SELF_HOSTED != 'true' }} + with: + token: "${{ secrets.GITHUB_TOKEN }}" + + - uses: julia-actions/julia-buildpkg@v1 + + - name: "Run tests ${{ vars.USE_SELF_HOSTED == 'true' && '' || format('on {0}', inputs.os) }} with Julia v${{ inputs.julia-version }}" + shell: julia --color=yes --check-bounds=yes --depwarn=yes {0} + run: | + import Pkg + PKG_GROUP = ${{ matrix.group }} + NAMES = split(PKG_GROUP, " - ") + @assert length(NAMES) == 2 """ + Invalid CI configuration. Test group must be of the form 'Package - Group'. \ + Got "$PKG_GROUP". + """ + PKG = NAMES[1] + GROUP = NAMES[2] + ENV["GROUP"] = GROUP + @info "Preparing Environment" + if PKG == "ModelingToolkit" + Pkg.activate(".") + Pkg.develop(; path = "lib/ModelingToolkitStandardLibrary") + else + @assert isdir("lib/$PKG") """ + Package $PKG not found in subdirectory `lib/`. + """ + Pkg.activate("lib/$PKG") + Pkg.develop(; path = ".") + end + + @info "Running tests" + Pkg.test(PKG; coverage = true, julia_args=["--check-bounds=yes", "--compiled-modules=yes", "--depwarn=yes"], force_latest_compatible_version=false, allow_reresolve=true) + + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: "src,lib/ModelingToolkitStandardLibrary/src" + + - name: "Report Coverage with Codecov" + uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: "${{ secrets.CODECOV_TOKEN }}" + fail_ci_if_error: true From df5737258136c9ff449efaf733fb7891f5de3733 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 29 Oct 2025 14:21:49 +0530 Subject: [PATCH 3/6] ci: remove MTKStdlib CI files --- .../.github/dependabot.yml | 10 ---- .../.github/workflows/CompatHelper.yml | 26 --------- .../.github/workflows/Documentation.yml | 18 ------ .../.github/workflows/Downgrade.yml | 32 ----------- .../.github/workflows/Downstream.yml | 55 ------------------- .../.github/workflows/FormatCheck.yml | 13 ----- .../.github/workflows/SpellCheck.yml | 13 ----- .../.github/workflows/TagBot.yml | 15 ----- .../.github/workflows/Tests.yml | 34 ------------ 9 files changed, 216 deletions(-) delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/dependabot.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml delete mode 100644 lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml diff --git a/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml b/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml deleted file mode 100644 index ec3b005a0e..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" # Location of package manifests - schedule: - interval: "weekly" - ignore: - - dependency-name: "crate-ci/typos" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml deleted file mode 100644 index 73494545f2..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: CompatHelper - -on: - schedule: - - cron: '00 * * * *' - issues: - types: [opened, reopened] - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - julia-version: [1] - julia-arch: [x86] - os: [ubuntu-latest] - steps: - - uses: julia-actions/setup-julia@latest - with: - version: ${{ matrix.julia-version }} - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: julia -e 'using CompatHelper; CompatHelper.main(;subdirs=["", "docs"])' diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml deleted file mode 100644 index 785116ee5a..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/Documentation.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "Documentation" - -on: - push: - branches: - - main - tags: '*' - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} - -jobs: - build-and-deploy-docs: - name: "Documentation" - uses: "SciML/.github/.github/workflows/documentation.yml@v1" - secrets: "inherit" diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml deleted file mode 100644 index 178f6c35d7..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downgrade.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Downgrade -on: - pull_request: - branches: - - main - paths-ignore: - - 'docs/**' - push: - branches: - - master - paths-ignore: - - 'docs/**' -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - downgrade_mode: ['alldeps'] - julia-version: ['1.10'] - steps: - - uses: actions/checkout@v4 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.julia-version }} - - uses: julia-actions/julia-downgrade-compat@v2 -# if: ${{ matrix.version == '1.6' }} - with: - skip: Pkg,TOML - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - with: - ALLOW_RERESOLVE: false diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml deleted file mode 100644 index 7cc4149120..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/Downstream.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: IntegrationTest -on: - push: - branches: [main] - tags: [v*] - pull_request: - -jobs: - test: - name: ${{ matrix.package.repo }}/${{ matrix.package.group }}/${{ matrix.julia-version }} - runs-on: ${{ matrix.os }} - env: - GROUP: ${{ matrix.package.group }} - strategy: - fail-fast: false - matrix: - julia-version: [1] - os: [ubuntu-latest] - package: - - {user: SciML, repo: ModelingToolkit.jl} - steps: - - uses: actions/checkout@v4 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.julia-version }} - arch: x64 - - uses: julia-actions/julia-buildpkg@latest - - name: Clone Downstream - uses: actions/checkout@v4 - with: - repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} - path: downstream - - name: Load this and run the downstream tests - shell: julia --color=yes --project=downstream {0} - run: | - using Pkg - try - # force it to use this PR's version of the package - Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps - Pkg.update() - Pkg.test(coverage=true) # resolver may fail with test time deps - catch err - err isa Pkg.Resolve.ResolverError || rethrow() - # If we can't resolve that means this is incompatible by SemVer and this is fine - # It means we marked this as a breaking change, so we don't need to worry about - # Mistakenly introducing a breaking change, as we have intentionally made one - @info "Not compatible with this release. No problem." exception=err - exit(0) # Exit immediately, as a success - end - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v5 - with: - file: lcov.info - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml deleted file mode 100644 index 7e46c8db90..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/FormatCheck.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Format Check" - -on: - push: - branches: - - 'main' - tags: '*' - pull_request: - -jobs: - format-check: - name: "Format Check" - uses: "SciML/.github/.github/workflows/format-check.yml@v1" diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml deleted file mode 100644 index 9246edd2af..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/SpellCheck.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Spell Check - -on: [pull_request] - -jobs: - typos-check: - name: Spell Check with Typos - runs-on: ubuntu-latest - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v4 - - name: Check spelling - uses: crate-ci/typos@v1.18.0 \ No newline at end of file diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml deleted file mode 100644 index f49313b662..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/TagBot.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: -jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml b/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml deleted file mode 100644 index 29b66299f5..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.github/workflows/Tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "Tests" - -on: - pull_request: - paths-ignore: - - 'docs/**' - push: - branches: - - main - paths-ignore: - - 'docs/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} - -jobs: - tests: - name: "Tests" - strategy: - fail-fast: false - matrix: - version: - - "1" - - "lts" - - "pre" - group: - - "Core" - - "QA" - uses: "SciML/.github/.github/workflows/tests.yml@v1" - with: - julia-version: "${{ matrix.version }}" - group: "${{ matrix.group }}" - secrets: "inherit" From 9108729e3361c49cef539fbf6f6dfc0db5a37486 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 29 Oct 2025 14:26:13 +0530 Subject: [PATCH 4/6] refactor: remove redundant files in MTKStdlib --- .../.JuliaFormatter.toml | 2 -- lib/ModelingToolkitStandardLibrary/.gitignore | 24 ------------------- .../.typos.toml | 14 ----------- 3 files changed, 40 deletions(-) delete mode 100644 lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml delete mode 100644 lib/ModelingToolkitStandardLibrary/.gitignore delete mode 100644 lib/ModelingToolkitStandardLibrary/.typos.toml diff --git a/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml b/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml deleted file mode 100644 index 9c79359112..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.JuliaFormatter.toml +++ /dev/null @@ -1,2 +0,0 @@ -style = "sciml" -format_markdown = true \ No newline at end of file diff --git a/lib/ModelingToolkitStandardLibrary/.gitignore b/lib/ModelingToolkitStandardLibrary/.gitignore deleted file mode 100644 index 29126e47b0..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Files generated by invoking Julia with --code-coverage -*.jl.cov -*.jl.*.cov - -# Files generated by invoking Julia with --track-allocation -*.jl.mem - -# System-specific files and directories generated by the BinaryProvider and BinDeps packages -# They contain absolute paths specific to the host computer, and so should not be committed -deps/deps.jl -deps/build.log -deps/downloads/ -deps/usr/ -deps/src/ - -# Build artifacts for creating documentation generated by the Documenter package -docs/build/ -docs/site/ - -# File generated by Pkg, the package manager, based on a corresponding Project.toml -# It records a fixed state of all packages used by the project. As such, it should not be -# committed for packages, but should be committed for applications that require a static -# environment. -Manifest.toml diff --git a/lib/ModelingToolkitStandardLibrary/.typos.toml b/lib/ModelingToolkitStandardLibrary/.typos.toml deleted file mode 100644 index 906888d1c3..0000000000 --- a/lib/ModelingToolkitStandardLibrary/.typos.toml +++ /dev/null @@ -1,14 +0,0 @@ -[default.extend-words] -Nd = "Nd" -nin = "nin" -coul = "coul" -isconnection = "isconnection" -# Additional SciML terms -setp = "setp" -getp = "getp" -indexin = "indexin" -ists = "ists" -ispcs = "ispcs" -eqs = "eqs" -rhs = "rhs" -MTK = "MTK" \ No newline at end of file From bba3afa67e4d0ef79c94ce5c6a3bad80650f4657 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 29 Oct 2025 14:26:23 +0530 Subject: [PATCH 5/6] chore: move MTKStdlib typos config --- .typos.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.typos.toml b/.typos.toml index a933260fb5..5cdd606344 100644 --- a/.typos.toml +++ b/.typos.toml @@ -7,3 +7,13 @@ ser = "ser" isconnection = "isconnection" Ue = "Ue" Derivate = "Derivate" +Nd = "Nd" +coul = "coul" +setp = "setp" +getp = "getp" +indexin = "indexin" +ists = "ists" +ispcs = "ispcs" +eqs = "eqs" +rhs = "rhs" +MTK = "MTK" From 5df2a208ad47ec0fc257edbbca8255b490bd727c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 29 Oct 2025 14:56:31 +0530 Subject: [PATCH 6/6] TEMP COMMIT: debug ci --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 7728b0eaa9..dd76b8c788 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -7,7 +7,7 @@ on: - 'release-' paths-ignore: - 'docs/**' - - 'lib/ModelingToolkitStandardLibrary/docs/**' + # - 'lib/ModelingToolkitStandardLibrary/docs/**' push: branches: - master