In this article, some of the features and recent additions to the ChemApp for Python package are introduced and explained.
As a brief summary, ChemApp for Python is a Python module that encapsulates the ChemApp thermodynamic calculation library. It does provide different ways to access it.
To users that are accustomed to ChemApp from other languages, the basic
module delivers a relatively pure interface very similar to the ones from other
languages, despite some minor changes due to adaptions to the Python development
world, which includes moving from 1-based indexing to 0-based indexing, as well
as replacing string literals with enumerations, and properly raising Python
exceptions instead of the error mechanisms that are (that are = used/implemented) in the other language
interfaces.
The friendly
module takes a much more elaborate approach to exposing the ChemApp
interface. It mostly avoids the addressing-by-index of the underlying library
and allows to use e.g. system component names and phase names directly. It also
has much more expressive routines to get and set certain properties, e.g.
get_eq_A_pcs_in_ph(...)
which is a routine to get the equilibrium amount of a
phase constituent in a phase. It does make consistent use of certain
abbreviations such as pc
for phase constituent (and s
appended for all
of them), which are quick to learn and reasonably easy to understand.
However, the friendly module also introduces some Python object factories,
which are very useful if a less sequential and more complex programming attempt
is made, e.g. in process modelling.
In this article, some examplary use of these objects and the ways to interact and manipulate them is shown, including newly added routines and concepts.
Basics of a stream calculation
A thermodynamic equilibrium calculated with ChemApp fundamentally minimizes the
total Gibbs energy of a given system with a given set of boundary conditions.
However, the total Gibbs energy and the derived intensive properties per phase are rarely the technically interesting or relevant results. A stream calculation
, as it is called in FactSage/ChemApp, allows to set initial conditions in addition to the general boundary conditions. As a consequence, the total Gibbs energies of that reference state is also calculated and as a result the difference between the total state of the initial conditions and the calculated total minimized state is obtained. This allows, for example, for quick and easy calculation of heat balances and the concept of mixing different inputs in a reactor, such as
hot air mixing with a cold aqueous solution, can be expressed relatively
straightforward.
Stream calculations with initial conditions can be understood as a shorthand for a series of consecutive calculations, as that the same result can be obtained by calculation of each stream separately, storing the respective data and subsequently correcting the total Gibbs energy results of the following calculations with the respective results from the streams.
Streams in ChemApp for Python
The friendly
module of ChemApp for Python contains a number of classes that
are set to enable stream calculations and the handling of the results and
inputs. namely IncomingAmountState
, SystemComponentIncomingAmountState
,
PhaseConstituentIncomingAmountState
for the incoming amounts, the
SystemComponentState
, PhaseState
and PhaseConstituentState
to control the
states of the respective components of the thermodynamic system, as well as the
EquilibriumCalculationResult
and StreamCalculationResult
to manage the
results of calculations. Lastly, but most important for this article, the
StreamState
class can be used to combine and interact with all of the above
pieces to allow quick and comprehensive process modelling.
Singleton concept
To completely understand the inner workings of the ChemApp for Python module, it helps to understand the architecture. Even though Python is a typically object oriented programming language, the wrapped ChemApp library is a state bound single instance. This means that all classes and routines that are loaded from the ChemApp for Python module are accessing the same single instance of ChemApp.
This allows to freely mix e.g. condition setting commands from the
chemapp.basic
module, e.g. using tqsetc
, and using higher level routines
from the chemapp.friendly
module such as
EquilibriumCalculation.calculate_eq()
.
It also means that instantiating classes such as EquilibriumCalculation
will
not yield two separate instances of that class, which means that these two
instance's properties are fully linked and updated when the underlying ChemApp
library changes.
In contrast with this singleton architecture of the classes that are directly
linked to ChemApp, the XXState
classes are regularly instantiable Python
classes, which means that they behave like any other class.
To link these two concepts, a small and powerful option exists: the
get_result_object()
routine that can be called for both the
EquilibriumCalculationResult
as well as the StreamCalculationResult
classes,
which allows to store the state of a successful ChemApp calculation into a
Python object.
from chemapp.friendly import (
ThermochemicalSystem as cats,
EquilibriumCalculation as caec
)
cats.load("FeOH.cst")
caec.set_IA_sc("Fe", 5.0)
caec.set_IA_sc("O", 1.0)
caec.set_eq_P(1.0)
caec.set_eq_T(540.0)
caec.calculate_eq(print_results=True)
equilibrium_result = caec.get_result_object()
This EquilibriumCalculationResult
object is already pretty capable in itself,
as it contains all relevant thermodynamic results, such as .Cp
, .SM
etc.
# fetch the total heat capacity of the system
print(equilibrium_result.Cp)
#>> 5.967754945626497e-05
# fetch the molar entropy of the system
print(equilibrium_result.SM)
#>> 1.8985688975406527e-05
# fetch the equilibrium temperature
print(equilibrium_result.T)
#>> 540.0
# note: these values are in the units when calculated - of which the state is saved into the .units property of the equilibrium_result object.
print(equilibrium_result.units)
#>> {'P': <PressureUnit.bar: 0>, 'V': <VolumeUnit.m3: 2>, 'T': <TemperatureUnit.C: 1>, 'E': <EnergyUnit.kWh: 3>, 'A': <AmountUnit.mol: 0>}
These are class instance properties and are thereby not bound to the ChemApp
library state, but rather to the equilibrium_result
object.
Creating a stream object from a calculation
Besides these thermodynamic results values, the EquilibriumCalculationResult
and StreamCalculationResult
have more interesting features, such as the
ability to provide the results in such a way that they can be used as input for
subsequent calculations, by means of using the XXState
classes.
The routine that is used for this is create_stream
. Even though it was already part
of ChemApp for Python, it has been significantly expanded to
be even more useful in version 8.2.5.
def create_stream(
self,
str name,
bint include_zeros=False,
list phs_include=None,
list phs_exclude=None,
list els_include=None, # new in 8.2.5
list els_exclude=None, # new in 8.2.5
double el_threshold=0.0 # new in 8.2.5
):
"""
Create a stream state object from this result object.
:param name: stream name (str)
:param include_zeros: include zero values (bool)
:param phs_include: phases to include (list)
:param phs_exclude: phases to ignore (list)
:param els_include: elements to include (list)
:param els_exclude: elements to ignore (list)
:param el_threshold: minimum threshold value to be considered (double). Defaults to 0.0.
:return: new stream object
"""
The already present options allow generation a stream object that can be used as
input to the next calculation - You can choose to ignore those phases that have
an amount of zero by setting the include_zeros
flag to True
.
It also allows to explicitly include or exclude certain phases by using the
phs_include
or phs_exclude
lists. If you have detailed knowledge of your
process, you'll be able to determine e.g. different material flows or separate
the reaction system by the formed phases.
However, in the extended version of create_stream
, we support even more
functionality to create streams from equilibrium calculations, by filtering the
resulting stream contents by means of presence of certain elements. Analogous to
the option to include or exclude phases, you can choose to add elements to
els_include
or els_exclude
to separate the phases by their constituents.
NOTE: If you are using
els_include
orels_exclude
, be aware that you need to supply the stoichiometries of the phase constituents. This can be achieved by adding thephsconfigs=ThermochemicalSystem.get_config_phs()
parameter. This is explicitly needed, since the StreamState object is not directly associated with the thermodynamic system being used.
The option el_threshold
defaults to 0.0
so that typically even miniscule
amounts of each phase are considered. Setting this value to a significant value
larger than 0.0 can be helpful to remove residual solutes or gaseous remnants.
The create_stream
routine will always return a StreamState
object.
Splitting by state of matter
One very common occurence in process modelling using ChemApp (for Python) is the
necessity to split material flow by the states of matter. We have added
functionality to allow this. It is of note that the ChemApp calculations
themselves are completely inconsiderate of the state of matter - there are some
phase models that are clearly only reasonable for certain state of matters (such
as e.g. the IDMX
model used for ideal gasses, or the PIXX
models for Pitzer
model liquids), but others, such as RKMP
are frequently used for both liquid
or solid solutions.
Therefore, we added a helper function find_state
to ChemApp for Python, that
will make a best guess at the state of matter of a phase by the phase name
only. There are conventions around certain phase name structures when exporting
from FactSage, so that it should be able to guess the correct state of matter in
many if not all cases you typically encounter. Still, if you use manually
generated database files, you should be cautiously aware of these assumptions.
NOTE: The
find_state
routine can be imported from chemapp.core. It returns aPhaseMatterState
enum with the state of matter. It does not depend on reading a database file, as it only guesses based on the phase name. You can not set the reported state of a phase explicitly, nor is it currently possible to edit the list of associations. If you think the results should be different, feel free to contact us.
To provide more comfort in using these routines, you can use the
split_by_states()
routine of the StreamState
class. This yields a tuple
of three streams:
stream1 = equilibrium_result.create_stream(name='Output')
solids,liquids,gasses = s1.split_by_states()
print(solids)
# >> ===============================================================
# Stream State
# ---------------------------------------------------------------
# Class: chemapp.core.StreamState
#
# Name: Output_solid
# T: 5.4000E+02 C
# P: 1.0000E+00 bar
# Cp: NAN kWh/C
# H: NAN kWh
# S: NAN kWh/C
# G: NAN kWh
# V: NAN m3
# ---------------------------------------------------------------
# Phase Constituent A
# mol
# ---------------------------------------------------------------
# Fe_bcc_a2(s) 4.0000E+00
# FeO_Wustite(s) 1.0000E+00
# ---------------------------------------------------------------
# Total 5.0000E+00
# ===============================================================
The three separated streams will have appended _solid
, _liquid
and
_gas
.
If you only want to acquire one of the stream objects, the routines
create_gas_stream()
, create_liquid_stream()
andcreate_solid_stream()
are shorthands to yield these streams, with their signatures being identical to
the create_stream
routine, therefore requiring a name to be set.
Combining streams
The routine combine_with(self, other, name="")
allows to add two StreamState
objects into a single stream. It will add the phase constituent amounts of both
streams to each other, and return a new object.
Other than the phase constituent amounts, the temperature and pressure of
self
will be kept, as well as the name if the keyword argument is omitted
from the call.
from chemapp.core import StreamState, PhaseConstituentIncomingAmountState
stream2 = StreamState(P=300, T=500, name="Stream2", units=Units.get())
# add one phase constituent that already exists in 'solids' above
stream2._A_pcs.append(PhaseConstituentIncomingAmountState('FeO_Wustite(s)','FeO_Wustite(s)', 4.5))
print(stream2)
Further stream manipulations
There certainly are more relevant modifications to be done to stream objects, namely e.g. to change pressure or temperature of a stream to reflect the changes in a process.
By the virtue of trying to enable it in a simple and user-friendly fashion, this can be done by
directly using the respective properties of the StreamState
object, .T
and .P
. A tiny bit of caution should be taken when setting these values,
since there is no type checking or input validation taking place (e.g. negative
temperatures etc). After all, we trust users to value simplicity of use over
protective layers.
Add your own functionality
This being implemented in Python, the user is free to monkey patch or add
functionality to any of these classes and routines. There are many ways to mix,
create and separate StreamState
objects. There are very few things to obey:
- On initialization, a units object (typically
Units.get()
) is attached to the object - this is not updated when e.g. units used in ChemApp are changed. It is strictly recommended to keep to one set of consistent units throughout a complete calculation. - Just as well, the
.Cp
,.V
,.H
and.G
are values set at appropriate times, e.g. when the object is created, but can not be updated when e.g. combining or splitting stream objects. They may only be valid for a non-manipulatedStreamState
object. - Most properties that the objects in ChemApp for Python are exposing are stored
as with the
_
prefix as object attributes. - The
.A
property is generated from the sum of the phase constituent amounts. The phase constituent incoming amounts are stored in list._A_pcs
asPhaseConstituentIncomingAmountState
objects.
The current implementations should make it easy to add your own StreamState
manipulation routines, such as the following example:
from copy import copy
def scale_stream(stream, factor):
"""
Scale a StreamState object's incoming amounts by a factor of `factor`.
"""
copy_stream = copy(stream)
# clear the existing phase constituent list
copy_stream._A_pcs = []
for A_pc in stream.A_pcs:
# creating new PhaseConstituentIncomingAmountState instances
# with the multiplied amounts
copy_stream._A_pcs.append(
PhaseConstituentIncomingAmountState(
A_pc.ph,
A_pc.pc,
A_pc.A*factor
)
)
return copy_stream