Custom components
FONSim provides several ready-to-use components
in its standard library.
However, there’s a good chance you will
now and then want
to define and use your own components.
This tutorial discusses
how to do this
by redefining the Container
component.
Full script, including usage example:
custom_component.py
.
Component definition
Components are defined as Python classes
that inherit from the base class Component
.
The following snippet defines
our new Container
class and initializes the base class,
and also provides some documentation.
n addition to the mandatory label argument,
we have added two custom arguments:
the fluid and the container volume,
as both of these influence the behavior of the component.
11class Container(fons.Component):
12 """
13 A Container object is a (by default, empty) container or tank.
14 It has one terminal named 'a'.
15 It has one state named 'mass',
16 which represents the mass of the fluid inside the container.
17
18 :param label: label
19 :param fluid: fluid, must be compressible
20 :param volume: volume of the container in m^3.
21 """
22 def __init__(self, label=None, fluid=None, volume=None):
23 super().__init__(label)
Next, we need to define the possibilities
for the component to interact with other components
by instantiating and adding a Terminal
.
This is done in the following five lines:
26 self.set_terminals(
27 fons.Terminal('a',
28 [fons.Variable('pressure', 'across', label='p',
29 initial_value=cnorm.pressure_atmospheric,
30 range=(0, np.Inf)),
31 fons.Variable('massflow', 'through', label='mf')]))
The second line gives the terminal its label ‘a’, and the next three lines add the variables that constitute the terminal. Two terminals from two different components that are connected together should have the same variable _keys_. In line with other components in the FONSim standard library, we are working in a simple fluidical domain with the keys ‘pressure’ and ‘massflow’ (we assume the fluid temperature is constant throughout the system). Following classical system modelling theory, the former is an ‘across’ variable while the latter is an ‘through’ variable. Both variables receive labels, respectively ‘p’ and ‘mf’, to refer to them in the further component definition.
Note
At each internal node, which internally is a group of terminals that are connected together, FONSim enforces that at any simulation time step all across variables are equal in value and that all through variables sum to zero. Internal nodes are normally not of relevance to the user and they do not represent physical elements (e.g. T-piece).
The container has a single state: the fluid mass ‘m’ inside. Its initial value is set to the fluid mass that corresponds to standard temperature and pressure (STP) (following the ideal gas law). This is easily added with the following two lines:
32 self.set_states(fons.Variable('mass', 'local', label='m',
After the terminals and states have been defined,
we need to describe the behaviour of the component.
This is done by adding the two class methods
update_state
and evaluate
.
The update_state
method, shown in the excerpt below,
defines how the state should change over time
given particular values at the terminals.
It is formulated as an explicit finite difference equation
(hence the timestep dt
is provided as argument).
The two arguments ‘mf’ and ‘m’ should match
with the variable labels given earlier.
In the case of our container,
this is a simple finite integral
(the mass inside changes over time as there is massflow in- and out).
36 @self.auto_state
37 def update_state(dt, mf, m):
38 m_new = m + mf*dt
39 return {'m': m_new}
40 self.update_state = update_state
The latter (evaluate
) is shown in the following snippet
and defines how the terminal and state variable values relate.
It is formulated as an implicit equation.
In the case of our container,
it is the ideal gas law for a fluid with a constant temperature.
One can add the argument t
which will receive the value
of the current simulation time.
Here it is left out because the behaviour does not depend on time.
This is the end of the component definition. FONSim takes care of estimating the derivatives. Alternatively, we can specify them ourselves, which is discussed in the next section.
Manual derivative definition
Sometimes it is necessary for stability to manually specify the derivative. It also increases the simulation speed. The following example shows the two methods with the derivative definitions:
53 # With derivative specified
54 @self.auto_state
55 def update_state(dt, mf, m):
56 jacobian = {}
57 m_new = m + dt * mf
58 jacobian['m/p'] = 0
59 jacobian['m/mf'] = dt
60 return {'m': m_new}, jacobian
61 self.update_state = update_state
62
63 @self.auto
64 def evaluate(p, m):
65 mass_stp = volume*fluid.rho_stp
66 values, jacobian = np.zeros([1], dtype=float), [{}]
67 values[0] = m*cnorm.pressure_atmospheric - mass_stp*p
68 jacobian[0]['m'] = cnorm.pressure_atmospheric
69 jacobian[0]['p'] = -mass_stp
70 return values, jacobian
71 self.evaluate = evaluate
For the state update,
the derivatives are specified in the form
of a dictionary jacobian
with as keys
strings of the partial derivatives.
For the evaluate
method,
they are formulated
as a list of dictionaries
(also named jacobian
).
Usage
To finish, the component can be used like any other component. For example,
86system.add(Container('container', fluid=fluid, volume=250e-6))
creates and adds a container to the system object with a volume of 250 ml. If desired, you can run the simulation coded in the example file and view the resulting plot.
Conclusion
In this tutorial, we have discussed how to create a custom component in FONSim by redefining the Container component. We have explained the component definition process, including how to define the terminals, states, and behavior of the component. We have also demonstrated how to use the custom component by creating an instance of it, and using it in a system. With this information, you should now be able to create your own custom components and use them in your FONSim simulations.