Oscillator

In the first part of this example, we consider a one-dimensional mass-spring oscillator, and in the second part, we reuse this system to form a mass-spring-damper system.

Before we start, let's load EPHS.jl and Plots.jl:

using EPHS, Plots

Mass-spring oscillator

Following a bottom-up approach, we first define the primitive systems, and then we combine them into a composite system.

Storage components

We start by defining two storage components that model storage of potential and kinetic energy:

pe = let
  k = Par(:k, 1.5)                 # stiffness parameter with default value `1.5`
  q = XVar(:q)                     # state variable for displacement of the spring
  I = Dtry(                        # interface
    :q => Dtry(displacement)       # power port defined by its quantity
  )
  E = Const(1/2) * k * q^Const(2)  # energy storage function
  StorageComponent(I, E)
end

ke = let
  m = Par(:m, 1.)
  p = XVar(:p)
  I = Dtry(
    :p => Dtry(momentum)
  )
  E = Const(1/2) * p^Const(2) / m
  StorageComponent(I, E)
end
StorageComponent
■
└─ p => Quantity(:momentum, :ℝ, false)
        e = p.x * (m^-1.0)
Energy: 0.5 * (p.x^2.0) * (m^-1.0)

Reversible component

Next, we define a reversible component that models the reversible coupling of the potential and kinetic energy domains:

pkc = ReversibleComponent(
  Dtry(
    :q => Dtry(ReversiblePort(FlowPort(displacement, -EVar(:p)))),
    :p => Dtry(ReversiblePort(FlowPort(momentum, EVar(:q))))
  )
)
ReversibleComponent
■
├─ p => Quantity(:momentum, :ℝ, false)
│       f = q.e
└─ q => Quantity(:displacement, :ℝ, true)
        f = -1.0 * p.e

A reversible component is defined by a skew-symmetric relation among the power variables:

\[\begin{bmatrix} \mathtt{q.f} \\ \mathtt{p.f} \end{bmatrix} \: = \: \begin{bmatrix} 0 & -1 \\ +1 & 0 \end{bmatrix} \, \begin{bmatrix} \mathtt{q.e} \\ \mathtt{p.e} \end{bmatrix}\]

Composite system

We now define a Pattern, which interconnects the three components (in the only possible way):

osc = CompositeSystem(
  Dtry( # directory of junctions (energy domains)
    :q => Dtry(Junction(displacement, Position(1, 2))),
    :p => Dtry(Junction(momentum, Position(1, 4), exposed=true)),
  ),
  Dtry( # directory of inner boxes (subsystems)
    :pe => Dtry(
      InnerBox(
        Dtry( # interface of box `pe`
          :q => Dtry(InnerPort(■.q)), # assignment of port `pe.q` to junction `q`
        ),
        pe, # filling of the box
        Position(1, 1)
      ),
    ),
    :ke => Dtry(
      InnerBox(
        Dtry(
          :p => Dtry(InnerPort(■.p)),
        ),
        ke,
        Position(1, 5)
      ),
    ),
    :pkc => Dtry(
      InnerBox(
        Dtry(
          :q => Dtry(InnerPort(■.q)),
          :p => Dtry(InnerPort(■.p))
        ),
        pkc,
        Position(1, 3)
      ),
    ),
  )
)

To make the system extensible, the kinetic energy domain is exposed via an outer port.

The semantics of the composite system is the relation represented by the following equations:

assemble(osc)
Flows:
ke.p.f = -1.0 * pe.q.e + p.f
pe.q.f = ke.p.e
Efforts:
ke.p.e = ke.p.x * (ke.m^-1.0)
pe.q.e = pe.k * pe.q.x

In the output, the state variable of a port, say pe.q, is shown as pe.q.x, while the flow and effort variables are shown as pe.q.f and pe.q.e, respectively.

Simulation

We define an initial condition as a directory:

ic = Dtry(
  :pe => Dtry(
    :q => Dtry(0.0),
  ),
  :ke => Dtry(
    :p => Dtry(3.0),
  ),
)
■
├─ ke
│  └─ p => 3.0
└─ pe
   └─ q => 0.0

We use the variational midpoint rule to simulate the dynamics of the oscillator:

h = 0.01 # time step size
t = 20.0 # duration of the simulation
sim = simulate(osc, midpoint_rule, ic, h, t);

Plots

We can plot the evolution of the displacement state variable pe.q.x:

q = XVar(DtryPath(:pe), DtryPath(:q))
plot_evolution(sim, q)
Example block output

We can also plot the evolution of the total energy, the potential energy, and the kinetic energy:

plot_evolution(sim,
  "total energy" => total_energy(osc),
  "potential energy" => total_energy(pe; box_path=DtryPath(:pe)),
  "kinetic energy" => total_energy(ke; box_path=DtryPath(:ke));
  ylims=(0, Inf), # Plots.jl attribute to set the y axis limits
)
Example block output

Mass-spring-damper system

We first define the additionally required primitive systems, and then we combine them with the mass-spring oscillator.

Storage component

We define a 'thermal capacity', i.e. a storage component that models storage of thermal energy:

tc = let
  c₁ = Par(:c₁, 0.5)
  c₂ = Par(:c₂, 2.0)
  s = XVar(:s)
  E = c₁ * exp(s / c₂)
  StorageComponent(
    Dtry(
      :s => Dtry(entropy)
    ),
    E
  )
end
StorageComponent
■
└─ s => Quantity(:entropy, :ℝ, true)
        e = c₁ * (exp(s.x * (c₂^-1.0))) * (c₂^-1.0) + -1.0 * ENV.θ
Energy: c₁ * (exp(s.x * (c₂^-1.0)))

Irreversible component

Next, we define an irreversible component that models the irreversible process of mechanical friction:

mf = let
  d = Par(:d, 0.02)                     # friction coefficient
  p₊e = EVar(:p)                        # effort (velocity)
  s₊e = EVar(:s)                        # effort (temperature (wrt reference environment))
  p₊f = d * p₊e                         # flow (damping force)
  s₊f = -((d * p₊e * p₊e) / (θ₀ + s₊e)) # flow (entropy production)
  IrreversibleComponent(
    Dtry(
      :p => Dtry(IrreversiblePort(momentum, p₊f)),
      :s => Dtry(IrreversiblePort(entropy, s₊f))
    )
  )
end
IrreversibleComponent
■
├─ p => Quantity(:momentum, :ℝ, false)
│       f = d * p.e
└─ s => Quantity(:entropy, :ℝ, true)
        f = -1.0 * d * (p.e^2.0) * ((ENV.θ + s.e)^-1.0)

An irreversible component is defined by a symmetric, non-negative definite relation among the power variables:

\[\begin{bmatrix} \mathtt{p.f} \\ \mathtt{s.f} \end{bmatrix} \: = \: \frac{1}{\theta_0} \, d \, \begin{bmatrix} \theta & -\upsilon \\ - \upsilon & \frac{\upsilon^2}{\theta} \end{bmatrix} \, \begin{bmatrix} \mathtt{p.e} \\ \mathtt{s.e} \end{bmatrix}\]

Here, $\upsilon = \mathtt{p.e}$ is the velocity and $\theta = \theta_{0} + \mathtt{s.e}$ is the absolute temperature at which kinetic energy is dissipated into the thermal energy domain.

The condition for conservation of energy is satisfied:

\[\begin{bmatrix} 0 \\ 0 \end{bmatrix} \: = \: \begin{bmatrix} \theta & \upsilon \\ - \upsilon & \frac{\upsilon^2}{\theta} \end{bmatrix} \, \begin{bmatrix} \upsilon \\ \theta \end{bmatrix}\]

Composite system

We can now define a Pattern, which interconnects the mass-spring oscillator and the two primitive systems that we have just defined:

damped_osc = CompositeSystem(
  Dtry(
    :p => Dtry(Junction(momentum, Position(1,2))),
    :s => Dtry(Junction(entropy, Position(1,4))),
  ),
  Dtry(
    :osc => Dtry(
      InnerBox(
        Dtry(
          :p => Dtry(InnerPort(■.p)),
        ),
        osc,
        Position(1,1)
      ),
    ),
    :mf => Dtry(
      InnerBox(
        Dtry(
          :p => Dtry(InnerPort(■.p)),
          :s => Dtry(InnerPort(■.s)),
        ),
        mf,
        Position(1,3)
      ),
    ),
    :tc => Dtry(
      InnerBox(
        Dtry(
          :s => Dtry(InnerPort(■.s)),
        ),
        tc,
        Position(1,5)
      ),
    ),
  )
)

The semantics of the composite system is the relation represented by the following equations:

assemble(damped_osc)
Flows:
osc.ke.p.f = -1.0 * mf.d * osc.ke.p.e + -1.0 * osc.pe.q.e
osc.pe.q.f = osc.ke.p.e
tc.s.f = mf.d * (osc.ke.p.e^2.0) * ((ENV.θ + tc.s.e)^-1.0)
Efforts:
osc.ke.p.e = osc.ke.p.x * (osc.ke.m^-1.0)
osc.pe.q.e = osc.pe.k * osc.pe.q.x
tc.s.e = tc.c₁ * (exp(tc.s.x * (tc.c₂^-1.0))) * (tc.c₂^-1.0) + -1.0 * ENV.θ

Simulation

ic2 = Dtry(
  :osc => ic,
  :tc => Dtry(
    :s => Dtry(1.0)
  ),
)

sim = simulate(damped_osc, midpoint_rule, ic2, h, t);

Plots

q = XVar(DtryPath(:osc, :pe), DtryPath(:q))
plot_evolution(sim, q)
Example block output
plot_evolution(sim,
  "total energy" => total_energy(damped_osc),
  "potential energy" => total_energy(pe; box_path=DtryPath(:osc, :pe)),
  "kinetic energy" => total_energy(ke; box_path=DtryPath(:osc, :ke)),
  "thermal energy" => total_energy(tc; box_path=DtryPath(:tc));
  ylims=(0, Inf),
  legend=:topright,
)
Example block output