The first process model¶
Recap¶
A brief description on how models are created is given in the Hello World paragraph of the getting started section. The example was
1from simu import Model, NumericHandler, Quantity
2
3
4class Square(Model):
5 """A model of a square"""
6
7 def interface(self):
8 self.parameters.define("length", 10, "m")
9 self.properties.declare("area", "m^2")
10
11 def define(self):
12 self.properties["area"] = self.parameters["length"] ** 2
13
14
15def main():
16 numeric = NumericHandler(Square.top())
17 func = numeric.function
18 print(func.arg_structure)
19 print(func.result_structure)
20
21 args = numeric.arguments
22 print(args)
23
24 args[NumericHandler.MODEL_PARAMS]["length"] = Quantity(20, "cm")
25 result = func(args)
26
27 print(f"{result[NumericHandler.MODEL_PROPS]['area']:.3fP~}")
28
29
30if __name__ == '__main__':
31 main()
32
To recap, the model declares an interface telling other (parent) models that it has a parameter called length and calculates a property called area. Then, the definition implements the relationship.
The attentive reader might at this point have realised that no thermodynamic model was inv olved. The entire example is somehow remote to classical process engineering unless one calculates the cross-section of a square duct. Let us get this rectified in this section, and bring in our ideal gas model from the previous section.
To do so, we first need to create a simu.MaterialDefinition object, and by this follow the proper way to build up a simulation, and in practice, you might soon build up a repository of materials required for your field of application.
Creating a material definition¶
We can reuse much of the previous code, resulting into a simu.ThermoFrame object for a pure methane ideal gas. Only now, we store configuration data in yaml files. Let us have one file with some chemical species and their formulae (species_db.yml):
1Methane: CH4
2Hydrogen: H2
3Nitrogen: N2
4Pentamminerhodium chloride: "[RhCl(NH3)5]:2+"
Further, we store the thermodynamic model structures in another file (thermo_model_structures.yml):
1simple_ideal_gas:
2 state: GibbsState
3 contributions:
4 - H0S0ReferenceState
5 - LinearHeatCapacity
6 - IdealMix
7 - GibbsIdealGas
8simple_ideal_solution:
9 state: GibbsState
10 contributions:
11 - H0S0ReferenceState
12 - LinearHeatCapacity
13 - IdealMix
14 - ConstantGibbsVolume
And finally, the actual thermodynamic parameters (ideal_gas_param.yml):
1H0S0ReferenceState:
2 T_ref: 25 degC
3 dh_form:
4 Methane: -74.87 kJ/mol
5 p_ref: 1 bar,
6 s_0:
7 Methane: 188.66 J/K/mol
8LinearHeatCapacity:
9 cp_a:
10 Methane: 35.69 J/K/mol
11 cp_b:
12 Methane: 50 mJ/K**2/mol
Let’s first import some classes and read in those files:
1from pathlib import Path
2from yaml import safe_load
3
4from simu import (
5 InitialState, MaterialDefinition, ThermoParameterStore,
6 StringDictThermoSource, SpeciesDB)
7from simu.app import RegThermoFactory
8
9CURRENT_DIR = Path(__file__).parent
10
11# load species database
12with open(CURRENT_DIR / "species_db.yml") as file:
13 species = SpeciesDB(safe_load(file))
14
15# load model structure database
16with open(CURRENT_DIR / "thermo_model_structures.yml") as file:
17 model_structures = safe_load(file)
18
19# load thermodynamic parameter database
20with open(CURRENT_DIR / "ideal_gas_param.yml") as file:
21 parameter_source = StringDictThermoSource(safe_load(file))
Now, as before, we can create the factory and from there the frame for our thermodynamic model:
23factory = RegThermoFactory()
24frame = factory.create_frame(species.get_sub_db(["Methane"]),
25 model_structures["simple_ideal_gas"])
Next, we go for the material definition, requiring also the initial state and a simu.ThermoParameterStore object:
27initial_state = InitialState.from_si(400, 2e5, [1.0])
28store = ThermoParameterStore()
29ch4_ideal = MaterialDefinition(frame, initial_state, store)
This parameter store can be shared among multiple – normally all – material definitions, and thus holds a global set of thermodynamic parameters. Multiple stores are only required if two materials containing the same chemical species need to receive distinct values for the same thermodynamic parameter.
So far, we did not provide the parameter values, but the material definition has already told the store which parameters are required. We can query the super-set of the parameter names and units of all missing parameters required:
>>> pprint(missing_symbols)
{'H0S0ReferenceState': {'T_ref': 'K ',
'dh_form': {'Methane': 'J / mol '},
'p_ref': 'Pa ',
's_0': {'Methane': 'J / K / mol '}},
'LinearHeatCapacity': {'cp_a': {'Methane': 'J / K / mol '},
'cp_b': {'Methane': 'J / K ** 2 / mol '}}}
Finally, we provide the already read parameters to the store:
31store.add_source("my_source", parameter_source)
This time, there are no more missing symbols, and the print statement prints an empty dictionary:
>>> pprint(store.get_missing_symbols())
{}
In real applications, storing the meta-data and parameters in yml files is not the most stupid idea, but you might connect to any other file format or database of your choice, as long as the source can provide the nested dictionary of properties as requested by the store.
Note
As multiple parameter sources can be stacked in one store, we recommend to assign one source per bibliographic source of parameters. The models can then easily be queried for the names of the used sources and by that keep these sources traceable.
Using a material in a model¶
This is the big moment, as we now can use the material definition in an actual process model. The above created simu.MaterialDefinition object can be global for the entire project along with all other material definitions that you might need.
The following model is a hello world example for using such material:
1from simu import Model
2from .material_factory import ch4_ideal_gas
3
4class Source(Model):
5 """A model of a methane source"""
6
7 def interface(self):
8 self.parameters.define("T", 25, "degC")
9 self.parameters.define("p", 1, "bar")
10 self.parameters.define("V", 10, "m^3/hr")
11
12 def define(self):
13 src = self.materials.create_flow("source", ch4_ideal_gas)
14 self.residuals.add("T", self.parameters["T"] - src["T"], "K")
15 self.residuals.add("p", self.parameters["p"] - src["p"], "bar")
16 self.residuals.add("V", self.parameters["V"] - src["V"], "m^3/h")
Here we first define the three parameters T, p and V that determine our system. These three parameters also constitute the interface of our model. The definition creates a methane flow simu.Material object from our definition. Finally, lines 14-16 constrain the system to the direct specifications of the parameter variables.
Well, the above syntax is very verbose, but might get into the way with regards to coding efficiency and the ambitions to keep lines short and to the point. For this reason, we define a subclass to simu.Model, namely simu.AModel that does nothing but defining abbreviations. As such, we can reduce the above model to:
1from simu import AModel
2from .material_factory import ch4_ideal_gas
3
4class Source(AModel):
5 """A model of a methane source"""
6
7 def interface(self):
8 self.pad("T", 25, "degC")
9 self.pad("p", 1, "bar")
10 self.pad("V", 10, "m^3/hr")
11
12 def define(self):
13 src = self.mcf("source", ch4_ideal_gas)
14 self.ra("T", self.pa["T"] - src["T"], "K")
15 self.ra("p", self.pa["p"] - src["p"], "bar")
16 self.ra("V", self.pa["V"] - src["V"], "m^3/h")
This is as much as we can do without entirely drowning out the pythonic way of coding.
Either way, here we are with a complete process model. By creating a simu.NumericHandler, we obtain a function object representing our model. The function argument and result is a nested structure of quantities:
>>> from simu import NumericHandler
>>> numeric = NumericHandler(Source.top())
>>> args = numeric.arguments
>>> pprint(args)
{'model_params': {'T': <Quantity(25.0, 'degree_Celsius')>,
'V': <Quantity(10.0, 'meter ** 3 / hour')>,
'p': <Quantity(1.0, 'bar')>},
'thermo_params': {'default': {'H0S0ReferenceState': {'T_ref': <Quantity(25, 'degree_Celsius')>,
'dh_form': {'Methane': <Quantity(-74.87, 'kilojoule / mole')>},
'p_ref': <Quantity(1, 'bar')>,
's_0': {'Methane': <Quantity(188.66, 'joule / kelvin / mole')>}},
'LinearHeatCapacity': {'cp_a': {'Methane': <Quantity(35.69, 'joule / kelvin / mole')>},
'cp_b': {'Methane': <Quantity(50.0, 'millijoule / kelvin ** 2 / mole')>}}}},
'vectors': {'states': <Quantity([400, 200000, 1], 'dimensionless')>}}
Firstly, we can recognize the model parameters, the thermodynamic parameters, and the thermodynamic state of our material. The latter is stored in a dimensionless vector for the purpose of numerical solving. Later-on, we show how this vector, and/or individual parameters can be substituted by CasADi symbols and thus become free variables in a calculation.
Further, we can query the result by calling the function with this argument:
>>> pprint(numeric.function(args))
{'residuals': {'T': <Quantity(-101.85..., 'kelvin')>,
'V': <Quantity(-0.0138..., 'meter ** 3 / second')>,
'p': <Quantity(-100000.0, 'pascal')>},
'thermo_props': {'source': {'S': <Quantity(194.09666..., 'watt / kelvin')>,
'T': <Quantity(400.0, 'kelvin')>,
'T_ref': <Quantity(298.15, 'kelvin')>,
'V': <Quantity(0.01662..., 'meter ** 3 / second')>,
'mu': {'Methane': <Quantity(-148614.30..., 'joule / mole')>},
'n': {'Methane': <Quantity(1.0, 'mole / second')>},
'p': <Quantity(200000.0, 'pascal')>,
'p_ref': <Quantity(100000.0, 'pascal')>}},
'vectors': {'bounds': <Quantity([2.e+05 1.e+00 4.e+02], 'dimensionless')>,
'residuals': <Quantity([-1.018...e+09 -4.986...e+08 -1.00000000e+07], 'dimensionless')>}}
Here we see the residuals as physical quantities, but also converted to a dimensionless vector, representing the quotient of residuals and their tolerances. Thermodynamic properties are included, and model properties would, if there were any.
The volume is calculated to 59.86 m3/hr, but we specified 10 m3/hr, and also the pressure and temperature are not yet as desired. The specifications are only fulfilled once the residuals are brought down to values below their tolerances.
Summary / Outlook¶
Based on the previously defined thermodynamic model, we created a material definition object.
For good house-keeping, we can move most of the static configuration for instance into
yamlfiles.Such material definition can be utilized in process models to initiate a state or flow of that material.
Once instantiated in a model, the properties of the material can be used to derive new properties or to define model constraints (residuals).