diff --git a/docs/connections.html b/docs/connections.html new file mode 100644 index 000000000..d640930bc --- /dev/null +++ b/docs/connections.html @@ -0,0 +1,925 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + +
+ +

Introduction

+

In Physical Network Acausal modeling each physical domain must define a connector to combine model components. This is somewhat analogus to real life connections that are seen in electronics (i.e. battery connected to a wire) or fluid dynamics (i.e. pump connected to a pipe), to name a couple examples. 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:

+ +

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. 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 to 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: $ \partial \color{blue}{across} / \partial t \cdot c_1 = \color{green}{through} $

    +
  • +
  • Flow: $ \color{green}{through} \cdot c_2 = \color{blue}{across} $

    +
  • +
+

Electrical

+

So for the Electrical domain the across variable is voltage and the through variable current. Therefore

+
    +
  • Energy Dissipation: $ \partial \color{blue}{voltage} / \partial t \cdot capacitance = \color{green}{current} $

    +
  • +
  • Flow: $ \color{green}{current} \cdot resistance = \color{blue}{voltage} $

    +
  • +
+

Translational

+

And for the translation domain, choosing velocity for the across variable and force for the through gives

+
    +
  • Energy Dissipation: $ \partial \color{blue}{velocity} / \partial t \cdot mass = \color{green}{force} $

    +
  • +
  • Flow: $ \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

+

Electrical Domain Example

+

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

+ + +
+using ModelingToolkitStandardLibrary
+using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq
+using CairoMakie
+
+@parameters t
+
+@named resistor = Resistor(R = 1)
+@named capacitor = Capacitor(C = 1)
+@named ground = Ground()
+
+eqs = [
+    connect(capacitor.n, resistor.p)
+    connect(resistor.n, ground.g, capacitor.p)
+    ]
+
+@named model = ODESystem(eqs, t; systems=[resistor, capacitor, ground])
+
+sys = structural_simplify(model)
+
+equations(sys)
+
+ + +
+1-element Vector{Equation}:
+ Differential(t)(capacitor₊v(t)) ~ capacitor₊i(t) / capacitor₊C
+
+ + +

The solution shows what we would expect, a non-linear disipation of voltage and releated decrease in current flow...

+ + +
+prob = ODEProblem(sys, [1.0], (0, 10.0), [])
+sol = solve(prob, ImplicitMidpoint(); dt=0.01)
+
+fig = Figure()
+ax = Axis(fig[1,1], ylabel="voltage [V]")
+lines!(ax, sol.t, sol[capacitor.v], label="sol[capacitor.v]")
+axislegend(ax)
+
+ax = Axis(fig[2,1], xlabel="time [s]", ylabel="current [A]")
+lines!(ax, sol.t, -sol[resistor.i], label="sol[resistor.i]")
+axislegend(ax)
+
+fig
+
+ + + + +

Translational Domain Example (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

    +
  • +
+ + +
+module TranslationalVelocity
+    using ModelingToolkit
+    using ModelingToolkitStandardLibrary.Mechanical.Translational
+
+    @parameters t
+
+    @named damping = Damper(d = 1)
+    @named body = Body(m = 1, v0=1)
+    @named ground = Fixed()
+
+    eqs = [
+        connect(damping.port_a, body.port)
+        connect(ground.port, damping.port_b)
+        ]
+
+    @named model = ODESystem(eqs, t; systems=[damping, body, ground])
+
+    sys = structural_simplify(model)
+end
+
+sys = TranslationalVelocity.sys
+full_equations(sys)
+
+ + +
+1-element Vector{Equation}:
+ Differential(t)(body₊v(t)) ~ (-damping₊d*body₊v(t)) / body₊m
+
+ + +

As expected we have a similar solution...

+ + +
+prob = ODEProblem(sys, [1.0], (0, 10.0), [])
+sol_v = solve(prob, ImplicitMidpoint(); dt=0.01)
+
+fig = Figure()
+ax = Axis(fig[1,1], ylabel="velocity [m/s]")
+lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v]")
+axislegend(ax)
+
+ax = Axis(fig[2,1], xlabel="time [s]", ylabel="force [N]")
+lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f]")
+axislegend(ax)
+
+fig
+
+ + + + +

Translational Domain Example (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).

+ + +
+module TranslationalPosition
+    using ModelingToolkit
+    using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition
+
+    @parameters t
+    D = Differential(t)
+
+    # Let's define a simple body that only tracks the across and through variables...
+    function Body(; name, m, v0 = 0.0)
+        @named flange = Flange()
+        pars = @parameters m=m v0=v0
+        vars = @variables begin
+            v(t) = v0
+            f(t) = m*v0
+        end
+        eqs = [
+            D(flange.s) ~ v
+            flange.f ~ f
+
+            D(v) ~ f/m
+            ]
+        return compose(ODESystem(eqs, t, vars, pars; name = name), flange)
+    end
+
+    @named damping = Damper(d = 1)
+    @named body = Body(m = 1, v0=1)
+    @named ground = Fixed()
+
+    eqs = [
+        connect(damping.flange_a, body.flange)
+        connect(ground.flange, damping.flange_b)
+        ]
+
+    @named model = ODESystem(eqs, t; systems=[damping, body, ground])
+
+    sys = structural_simplify(model)
+end
+
+sys = TranslationalPosition.sys
+
+full_equations(sys)
+
+ + +
+2-element Vector{Equation}:
+ Differential(t)(body₊flange₊s(t)) ~ body₊v(t)
+ Differential(t)(body₊v(t)) ~ -((damping₊d*body₊v(t)) / body₊m)
+
+ + +

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.

+ + +
+prob = ODEProblem(sys, [0.0, 1.0], (0, 10.0), [])
+sol_p = solve(prob, ImplicitMidpoint(); dt=0.01)
+
+fig = Figure()
+ax = Axis(fig[1,1], ylabel="velocity [m/s]")
+lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.v], label="sol[body.v] (position based)")
+lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v] (velocity based)", linestyle=:dash)
+axislegend(ax)
+
+ax = Axis(fig[2,1], ylabel="force [N]")
+lines!(ax, sol_p.t, -sol_p[TranslationalVelocity.body.f], label="sol[body.f] (position based)")
+lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f] (velocity based)", linestyle=:dash)
+axislegend(ax)
+
+ax = Axis(fig[3,1], xlabel="time [s]", ylabel="position [m]")
+lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.flange.s], label="sol[body.flange.s]")
+axislegend(ax)
+
+fig
+
+ + + + + +
+ +
+
+
+ + + diff --git a/docs/connections.jmd b/docs/connections.jmd new file mode 100644 index 000000000..ef36bc53d --- /dev/null +++ b/docs/connections.jmd @@ -0,0 +1,203 @@ +# Introduction +In Physical Network Acausal modeling each physical domain must define a **connector** to combine model components. This is somewhat analogus to real life connections that are seen in electronics (i.e. battery connected to a wire) or fluid dynamics (i.e. pump connected to a pipe), to name a couple examples. 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 to 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: $ \partial \color{blue}{across} / \partial t \cdot c_1 = \color{green}{through} $ +- Flow: $ \color{green}{through} \cdot c_2 = \color{blue}{across} $ + +### Electrical +So for the Electrical domain the across variable is *voltage* and the through variable *current*. Therefore + +- Energy Dissipation: $ \partial \color{blue}{voltage} / \partial t \cdot capacitance = \color{green}{current} $ +- Flow: $ \color{green}{current} \cdot resistance = \color{blue}{voltage} $ + +### Translational +And for the translation domain, choosing *velocity* for the across variable and *force* for the through gives + +- Energy Dissipation: $ \partial \color{blue}{velocity} / \partial t \cdot mass = \color{green}{force} $ +- Flow: $ \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) + + +# Electrical Domain Example +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 + +```julia +using ModelingToolkitStandardLibrary +using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq +using CairoMakie + +@parameters t + +@named resistor = Resistor(R = 1) +@named capacitor = Capacitor(C = 1) +@named ground = Ground() + +eqs = [ + connect(capacitor.n, resistor.p) + connect(resistor.n, ground.g, capacitor.p) + ] + +@named model = ODESystem(eqs, t; systems=[resistor, capacitor, ground]) + +sys = structural_simplify(model) + +equations(sys) +``` + +The solution shows what we would expect, a non-linear disipation of voltage and releated decrease in current flow... + +```julia +prob = ODEProblem(sys, [1.0], (0, 10.0), []) +sol = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="voltage [V]") +lines!(ax, sol.t, sol[capacitor.v], label="sol[capacitor.v]") +axislegend(ax) + +ax = Axis(fig[2,1], xlabel="time [s]", ylabel="current [A]") +lines!(ax, sol.t, -sol[resistor.i], label="sol[resistor.i]") +axislegend(ax) + +fig +``` +# Translational Domain Example (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 + +```julia +module TranslationalVelocity + using ModelingToolkit + using ModelingToolkitStandardLibrary.Mechanical.Translational + + @parameters t + + @named damping = Damper(d = 1) + @named body = Body(m = 1, v0=1) + @named ground = Fixed() + + eqs = [ + connect(damping.port_a, body.port) + connect(ground.port, damping.port_b) + ] + + @named model = ODESystem(eqs, t; systems=[damping, body, ground]) + + sys = structural_simplify(model) +end + +sys = TranslationalVelocity.sys +full_equations(sys) +``` + +As expected we have a similar solution... +```julia +prob = ODEProblem(sys, [1.0], (0, 10.0), []) +sol_v = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="velocity [m/s]") +lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v]") +axislegend(ax) + +ax = Axis(fig[2,1], xlabel="time [s]", ylabel="force [N]") +lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f]") +axislegend(ax) + +fig +``` + +# Translational Domain Example (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). + +```julia +module TranslationalPosition + using ModelingToolkit + using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition + + @parameters t + D = Differential(t) + + # Let's define a simple body that only tracks the across and through variables... + function Body(; name, m, v0 = 0.0) + @named flange = Flange() + pars = @parameters m=m v0=v0 + vars = @variables begin + v(t) = v0 + f(t) = m*v0 + end + eqs = [ + D(flange.s) ~ v + flange.f ~ f + + D(v) ~ f/m + ] + return compose(ODESystem(eqs, t, vars, pars; name = name), flange) + end + + @named damping = Damper(d = 1) + @named body = Body(m = 1, v0=1) + @named ground = Fixed() + + eqs = [ + connect(damping.flange_a, body.flange) + connect(ground.flange, damping.flange_b) + ] + + @named model = ODESystem(eqs, t; systems=[damping, body, ground]) + + sys = structural_simplify(model) +end + +sys = TranslationalPosition.sys + +full_equations(sys) +``` + +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. + +```julia +prob = ODEProblem(sys, [0.0, 1.0], (0, 10.0), []) +sol_p = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="velocity [m/s]") +lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.v], label="sol[body.v] (position based)") +lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v] (velocity based)", linestyle=:dash) +axislegend(ax) + +ax = Axis(fig[2,1], ylabel="force [N]") +lines!(ax, sol_p.t, -sol_p[TranslationalVelocity.body.f], label="sol[body.f] (position based)") +lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f] (velocity based)", linestyle=:dash) +axislegend(ax) + +ax = Axis(fig[3,1], xlabel="time [s]", ylabel="position [m]") +lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.flange.s], label="sol[body.flange.s]") +axislegend(ax) + +fig +``` + + diff --git a/docs/connections.md b/docs/connections.md new file mode 100644 index 000000000..0f80bf2cc --- /dev/null +++ b/docs/connections.md @@ -0,0 +1,239 @@ +# Introduction +In Physical Network Acausal modeling each physical domain must define a **connector** to combine model components. This is somewhat analogus to real life connections that are seen in electronics (i.e. battery connected to a wire) or fluid dynamics (i.e. pump connected to a pipe), to name a couple examples. 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 to 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: $ \partial \color{blue}{across} / \partial t \cdot c_1 = \color{green}{through} $ +- Flow: $ \color{green}{through} \cdot c_2 = \color{blue}{across} $ + +### Electrical +So for the Electrical domain the across variable is *voltage* and the through variable *current*. Therefore + +- Energy Dissipation: $ \partial \color{blue}{voltage} / \partial t \cdot capacitance = \color{green}{current} $ +- Flow: $ \color{green}{current} \cdot resistance = \color{blue}{voltage} $ + +### Translational +And for the translation domain, choosing *velocity* for the across variable and *force* for the through gives + +- Energy Dissipation: $ \partial \color{blue}{velocity} / \partial t \cdot mass = \color{green}{force} $ +- Flow: $ \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) + + +# Electrical Domain Example +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 + +```julia +using ModelingToolkitStandardLibrary +using ModelingToolkitStandardLibrary.Electrical, ModelingToolkit, OrdinaryDiffEq +using CairoMakie + +@parameters t + +@named resistor = Resistor(R = 1) +@named capacitor = Capacitor(C = 1) +@named ground = Ground() + +eqs = [ + connect(capacitor.n, resistor.p) + connect(resistor.n, ground.g, capacitor.p) + ] + +@named model = ODESystem(eqs, t; systems=[resistor, capacitor, ground]) + +sys = structural_simplify(model) + +equations(sys) +``` + +``` +1-element Vector{Equation}: + Differential(t)(capacitor₊v(t)) ~ capacitor₊i(t) / capacitor₊C +``` + + + + + +The solution shows what we would expect, a non-linear disipation of voltage and releated decrease in current flow... + +```julia +prob = ODEProblem(sys, [1.0], (0, 10.0), []) +sol = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="voltage [V]") +lines!(ax, sol.t, sol[capacitor.v], label="sol[capacitor.v]") +axislegend(ax) + +ax = Axis(fig[2,1], xlabel="time [s]", ylabel="current [A]") +lines!(ax, sol.t, -sol[resistor.i], label="sol[resistor.i]") +axislegend(ax) + +fig +``` + +![](figures/connections_2_1.png) + + +# Translational Domain Example (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 + +```julia +module TranslationalVelocity + using ModelingToolkit + using ModelingToolkitStandardLibrary.Mechanical.Translational + + @parameters t + + @named damping = Damper(d = 1) + @named body = Body(m = 1, v0=1) + @named ground = Fixed() + + eqs = [ + connect(damping.port_a, body.port) + connect(ground.port, damping.port_b) + ] + + @named model = ODESystem(eqs, t; systems=[damping, body, ground]) + + sys = structural_simplify(model) +end + +sys = TranslationalVelocity.sys +full_equations(sys) +``` + +``` +1-element Vector{Equation}: + Differential(t)(body₊v(t)) ~ (-damping₊d*body₊v(t)) / body₊m +``` + + + + + +As expected we have a similar solution... +```julia +prob = ODEProblem(sys, [1.0], (0, 10.0), []) +sol_v = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="velocity [m/s]") +lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v]") +axislegend(ax) + +ax = Axis(fig[2,1], xlabel="time [s]", ylabel="force [N]") +lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f]") +axislegend(ax) + +fig +``` + +![](figures/connections_4_1.png) + + + +# Translational Domain Example (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). + +```julia +module TranslationalPosition + using ModelingToolkit + using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition + + @parameters t + D = Differential(t) + + # Let's define a simple body that only tracks the across and through variables... + function Body(; name, m, v0 = 0.0) + @named flange = Flange() + pars = @parameters m=m v0=v0 + vars = @variables begin + v(t) = v0 + f(t) = m*v0 + end + eqs = [ + D(flange.s) ~ v + flange.f ~ f + + D(v) ~ f/m + ] + return compose(ODESystem(eqs, t, vars, pars; name = name), flange) + end + + @named damping = Damper(d = 1) + @named body = Body(m = 1, v0=1) + @named ground = Fixed() + + eqs = [ + connect(damping.flange_a, body.flange) + connect(ground.flange, damping.flange_b) + ] + + @named model = ODESystem(eqs, t; systems=[damping, body, ground]) + + sys = structural_simplify(model) +end + +sys = TranslationalPosition.sys + +full_equations(sys) +``` + +``` +2-element Vector{Equation}: + Differential(t)(body₊flange₊s(t)) ~ body₊v(t) + Differential(t)(body₊v(t)) ~ -((damping₊d*body₊v(t)) / body₊m) +``` + + + + + +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. + +```julia +prob = ODEProblem(sys, [0.0, 1.0], (0, 10.0), []) +sol_p = solve(prob, ImplicitMidpoint(); dt=0.01) + +fig = Figure() +ax = Axis(fig[1,1], ylabel="velocity [m/s]") +lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.v], label="sol[body.v] (position based)") +lines!(ax, sol_v.t, sol_v[TranslationalVelocity.body.v], label="sol[body.v] (velocity based)", linestyle=:dash) +axislegend(ax) + +ax = Axis(fig[2,1], ylabel="force [N]") +lines!(ax, sol_p.t, -sol_p[TranslationalVelocity.body.f], label="sol[body.f] (position based)") +lines!(ax, sol_v.t, -sol_v[TranslationalVelocity.body.f], label="sol[body.f] (velocity based)", linestyle=:dash) +axislegend(ax) + +ax = Axis(fig[3,1], xlabel="time [s]", ylabel="position [m]") +lines!(ax, sol_p.t, sol_p[TranslationalPosition.body.flange.s], label="sol[body.flange.s]") +axislegend(ax) + +fig +``` + +![](figures/connections_6_1.png) diff --git a/docs/figures/connections_2_1.png b/docs/figures/connections_2_1.png new file mode 100644 index 000000000..55cbd9241 Binary files /dev/null and b/docs/figures/connections_2_1.png differ diff --git a/docs/figures/connections_4_1.png b/docs/figures/connections_4_1.png new file mode 100644 index 000000000..b6e5c6e5c Binary files /dev/null and b/docs/figures/connections_4_1.png differ diff --git a/docs/figures/connections_6_1.png b/docs/figures/connections_6_1.png new file mode 100644 index 000000000..3fde409d3 Binary files /dev/null and b/docs/figures/connections_6_1.png differ diff --git a/docs/through_across.png b/docs/through_across.png new file mode 100644 index 000000000..758704af8 Binary files /dev/null and b/docs/through_across.png differ diff --git a/docs/through_across.svg b/docs/through_across.svg new file mode 100644 index 000000000..82edb6497 --- /dev/null +++ b/docs/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/src/Mechanical/Mechanical.jl b/src/Mechanical/Mechanical.jl index f58b6e667..aa2f8dc7f 100644 --- a/src/Mechanical/Mechanical.jl +++ b/src/Mechanical/Mechanical.jl @@ -6,5 +6,7 @@ module Mechanical using ModelingToolkit include("Rotational/Rotational.jl") +include("Translational/Translational.jl") +include("TranslationalPosition/TranslationalPosition.jl") end diff --git a/src/Mechanical/Translational/Translational.jl b/src/Mechanical/Translational/Translational.jl new file mode 100644 index 000000000..e16cbb88d --- /dev/null +++ b/src/Mechanical/Translational/Translational.jl @@ -0,0 +1,20 @@ +""" +Library to model 1-dimensional, translational mechanical systems +""" +module Translational + +using ModelingToolkit, Symbolics, IfElse + +@parameters t +D = Differential(t) + +export Port +include("utils.jl") + +export Body, Spring, Damper, Fixed +include("components.jl") + +export PositionSensor +include("sensors.jl") + +end diff --git a/src/Mechanical/Translational/components.jl b/src/Mechanical/Translational/components.jl new file mode 100644 index 000000000..21aca06cf --- /dev/null +++ b/src/Mechanical/Translational/components.jl @@ -0,0 +1,80 @@ +function Fixed(; name) + @named port = Port() + eqs = [port.v ~ 0] + return compose(ODESystem(eqs, t, [], []; name = name), port) +end + +function Body(; name, v0 = 0.0, m = 1.0, s0=nothing, g=nothing) + @named port = Port(v0 = v0) + pars = @parameters m=m + vars = @variables begin + v(t) = v0 + f(t) = m*v0 + end + + eqs = [ + port.v ~ v + port.f ~ f + ] + + # gravity option + if !isnothing(g) + @parameters g=g + push!(pars, g) + push!(eqs, D(v) ~ f/m+g) + else + push!(eqs, D(v) ~ f/m) + end + + # position option + if !isnothing(s0) + @parameters s0=s0 + push!(pars, s0) + + @variables s(t)=s0 + push!(vars, s) + + push!(eqs, D(s) ~ v) + end + + return compose(ODESystem(eqs, t, vars, pars; name = name), port) +end + + +function Spring(;name, k = 1e3, delta0 = 0.0) + pars = @parameters k=k + vars = @variables begin + Δx(t) = delta0 + f(t) = k*delta0 + end + + @named port_a = Port() + @named port_b = Port() + + eqs = [ + D(Δx) ~ port_a.v - port_b.v + f ~ k*Δx + port_a.f ~ +f + port_b.f ~ -f + ] + return compose(ODESystem(eqs, t, vars, pars; name = name), port_a, port_b) +end + +function Damper(; name, d=1e2) + pars = @parameters d=d + vars = @variables v(t)=0.0 f(t)=0.0 + + @named port_a = Port() + @named port_b = Port() + + eqs = [ + v ~ port_a.v - port_b.v + f ~ v*d + + port_a.f ~ +f + port_b.f ~ -f + ] + return compose(ODESystem(eqs, t, vars, pars; name = name), port_a, port_b) +end + + diff --git a/src/Mechanical/Translational/sensors.jl b/src/Mechanical/Translational/sensors.jl new file mode 100644 index 000000000..ddcfe9106 --- /dev/null +++ b/src/Mechanical/Translational/sensors.jl @@ -0,0 +1,11 @@ +function PositionSensor(; name, s0=0.0) + @named port = Port() + + pars=@parameters s0=s0 + vars=@variables s(t)=s0 + eqs = [ + D(s) ~ port.v + 0 ~ p.f + ] + compose(ODESystem(eqs, t, vars, pars; name=name), port) +end \ No newline at end of file diff --git a/src/Mechanical/Translational/utils.jl b/src/Mechanical/Translational/utils.jl new file mode 100644 index 000000000..8865b41c6 --- /dev/null +++ b/src/Mechanical/Translational/utils.jl @@ -0,0 +1,16 @@ +@connector function Port(; name, v0=0.0, f0=0.0) + sts = @variables begin + v(t) + f(t), [connect = Flow] + end + ODESystem(Equation[], t, sts, [], name = name, defaults = Dict(v => v0, f => f0)) +end +Base.@doc """ + Port(;name, v_int=0.0, f_int=0.0) + +1-dim. rotational flange of a shaft. + +# States: +- `v`: [m/s] velocity of the node +- `f`: [N] force entering the node +""" Port diff --git a/src/Mechanical/TranslationalPosition/TranslationalPosition.jl b/src/Mechanical/TranslationalPosition/TranslationalPosition.jl new file mode 100644 index 000000000..9891ddbcc --- /dev/null +++ b/src/Mechanical/TranslationalPosition/TranslationalPosition.jl @@ -0,0 +1,21 @@ +""" +Library to model 1-dimensional, translational mechanical components. +""" +module TranslationalPosition + +using ModelingToolkit, Symbolics, IfElse +using ...Blocks: RealInput, RealOutput + +@parameters t +D = Differential(t) + +export Flange +include("utils.jl") + +export Fixed, Mass, Spring, Damper, IdealGear +include("components.jl") + +export Force +include("sources.jl") + +end diff --git a/src/Mechanical/TranslationalPosition/components.jl b/src/Mechanical/TranslationalPosition/components.jl new file mode 100644 index 000000000..6e9ef0550 --- /dev/null +++ b/src/Mechanical/TranslationalPosition/components.jl @@ -0,0 +1,98 @@ +""" + 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` +""" +function Fixed(; name, s0 = 0.0) + @named flange = Flange() + @parameters s0 = s0 + eqs = [flange.s ~ s0] + return compose(ODESystem(eqs, t, [], [s0]; name = name), flange) +end + +""" + Mass(;name, m, s_start=0.0, v_start=0.0, a_start=0.0) + +Sliding mass with inertia + +# Parameters: +- `m`: [kg] Mass of sliding mass +- `s_start`: [m] Initial value of absolute position of sliding mass +- `v_start`: [m/s] Initial value of absolute linear velocity of sliding mass +- `a_start`: [m/s²] Initial value of absolute linear acceleration of sliding mass + +# States: +- `s`: [m] Absolute position of sliding mass +- `v`: [m/s] Absolute linear velocity of sliding mass (= der(s)) +- `a`: [m/s²] Absolute linear acceleration of sliding mass (= der(v)) + +# Connectors: +- `flange_a: 1-dim. translational flange on one side of mass` +- `flange_b: 1-dim. translational flange on opposite side of mass` +""" +function Mass(; name, m, s_start = 0.0, v_start = 0.0, a_start = 0.0) + @named flange_a = Flange() + @named flange_b = Flange() + @parameters m = m + sts = @variables begin + s(t) = s_start + v(t) = v_start + a(t) = a_start + end + eqs = [s ~ flange_a.s + s ~ flange_b.s + D(s) ~ v + D(v) ~ a + m * a ~ flange_a.f + flange_b.f] + return compose(ODESystem(eqs, t, sts, [m]; name = name), flange_a, flange_b) +end + +""" + Spring(;name, c, s_rel0=0.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` +""" +function Spring(; name, c, s_rel0 = 0.0) + @named partial_comp = PartialCompliant() + @unpack s_rel, f = partial_comp + pars = @parameters begin + c = c + s_rel0 = s_rel0 + end + eqs = [f ~ c * (s_rel - s_rel0)] + extend(ODESystem(eqs, t, [], pars; name = name), partial_comp) +end + +""" + Damper(;name, d) + +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` +""" +function Damper(; name, d) + @named partial_comp = PartialCompliantWithRelativeStates() + @unpack v_rel, f = partial_comp + pars = @parameters d = d + eqs = [f ~ d * v_rel] + extend(ODESystem(eqs, t, [], pars; name = name), partial_comp) +end diff --git a/src/Mechanical/TranslationalPosition/sources.jl b/src/Mechanical/TranslationalPosition/sources.jl new file mode 100644 index 000000000..3854cb82b --- /dev/null +++ b/src/Mechanical/TranslationalPosition/sources.jl @@ -0,0 +1,12 @@ +""" + Force(;name) + +Input signal acting as external force on a flange +""" +function Force(; name, use_support = false) + @named partial_element = PartialElementaryOneFlangeAndSupport2(use_support = use_support) + @unpack flange = partial_element + @named f = RealInput() # Accelerating force acting at flange (= -flange.tau) + eqs = [flange.f ~ -f.u] + return extend(ODESystem(eqs, t, [], []; name = name, systems = [f]), partial_element) +end diff --git a/src/Mechanical/TranslationalPosition/utils.jl b/src/Mechanical/TranslationalPosition/utils.jl new file mode 100644 index 000000000..e419fbfc0 --- /dev/null +++ b/src/Mechanical/TranslationalPosition/utils.jl @@ -0,0 +1,147 @@ +@connector function Flange(; name) + sts = @variables begin + s(t) + f(t), [connect = Flow] + end + ODESystem(Equation[], t, sts, [], name = name, defaults = Dict(s => 0.0, f => 0.0)) +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 function Support(; name) + sts = @variables begin + s(t) + f(t), [connect = Flow] + end + ODESystem(Equation[], t, sts, [], name = name, defaults = Dict(s => 0.0, f => 0.0)) +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. + +# Parameters: +- `s_rel_start`: [m] Initial relative distance between the flanges +- `f_start`: [N] Initial force between flanges + +# States: +- `s_rel`: [m] Relative distance (= flange_b.s - flange_a.s) +- `f`: [N] Force between flanges (= flange_b.f) +""" +function PartialCompliant(; name, s_rel_start = 0.0, f_start = 0.0) + @named flange_a = Flange() + @named flange_b = Flange() + sts = @variables begin + s_rel(t) = s_rel_start + f(t) = f_start + end + eqs = [s_rel ~ flange_b.s - flange_a.s + flange_b.f ~ f + flange_a.f ~ -f] + return compose(ODESystem(eqs, t, sts, []; name = name), flange_a, flange_b) +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) +""" +function PartialCompliantWithRelativeStates(; name, s_rel_start = 0.0, v_rel_start = 0.0, + a_rel_start = 0.0, f_start = 0.0) + @named flange_a = Flange() + @named flange_b = Flange() + sts = @variables begin + s_rel(t) = s_rel_start + v_rel(t) = v_rel_start + a_rel(t) = a_rel_start + f(t) = f_start + end + eqs = [s_rel ~ flange_b.s - flange_a.s + D(s_rel) ~ v_rel + D(v_rel) ~ a_rel + flange_b.f ~ f + flange_a.f ~ -f] + return compose(ODESystem(eqs, t, sts, []; name = name), flange_a, flange_b) +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() + 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(ODESystem(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 + +# States: +- `s_support`: [m] Absolute position of support flange" +""" +function PartialElementaryTwoFlangesAndSupport2(; name, use_support = false) + @named flange_a = Flange() + @named flange_b = Flange() + sys = [flange_a, flange_b] + @variables s_support(t) = 0.0 + 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(ODESystem(eqs, t, [s_support], []; name = name), sys) +end diff --git a/test/Mechanical/translational.jl b/test/Mechanical/translational.jl new file mode 100644 index 000000000..b488c6983 --- /dev/null +++ b/test/Mechanical/translational.jl @@ -0,0 +1,39 @@ +using ModelingToolkitStandardLibrary.Mechanical.Translational, ModelingToolkit, OrdinaryDiffEq, + Test + + + +@parameters t +D = Differential(t) + + +@testset "one falling body" begin + #body starts at 1m, moving up at 2m/s in gravity of 10 + @named body = Body(x0 = 1.0, v0 = 2.0, g=-10.0) + + @named model = ODESystem(body.port.f ~ 0, t, [], []; systems=[body]) + + sys = structural_simplify(model) + prob = ODEProblem(sys, [], (0, 1.0)) + sol = solve(prob, ImplicitMidpoint(), dt=0.1) + + #x = g*t^2/2 + v_int*t + x_int + #dx = g*t + v_int + + @test sol.retcode == :Success + @test sol[body.x, 1] == 1.0 + @test sol[body.dx, 1] == 2.0 + @test sol[body.x, end] ≈ -10/2 + 2.0 + 1.0 +end + + + + + + + + + + + + diff --git a/test/runtests.jl b/test/runtests.jl index 7ecd62366..7587a1744 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,4 +20,6 @@ using SafeTestsets @safetestset "Magnetic" begin include("Magnetic/magnetic.jl") end # Mechanical -@safetestset "Mechanical" begin include("Mechanical/rotational.jl") end +@safetestset "Mechanical Rotation" begin include("Mechanical/rotational.jl") end +@safetestset "Mechanical Translation" begin include("Mechanical/translational.jl") end +