Advanced functionalities of ChemApp for Python: Streams

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 or els_exclude, be aware that you need to supply the stoichiometries of the phase constituents. This can be achieved by adding the phsconfigs=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 a PhaseMatterState 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-manipulated StreamState 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 as PhaseConstituentIncomingAmountState 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

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.