"Processor" is a very general term within V-HAB. It basically refers to any component that changes the properties of matter, like temperature, pressure, mass, or even the type of substance. Since that is a very broad scope, processors are divided into several specific types of processors, which each have even more specific subtypes. The general types of processors within V-HAB are:

  • extract merge processors (ExMes), (already encountered in 1.4.3 Branches and Solvers) that allow you to remove mass from a phase or add it to a phase
  • Phase to Phase processors (P2P) that define the movement of mass from one phase to another within the same store (basically in case of a phase change or adsorption processes etc.)
  • Flow to Flow (F2F) processors that allow you to change matter properties within a branch
  • Manipulators that allow you to transforms one substance into another (which is required for the simulation of chemical reactions)

1.4.4.1 Phase to Phase Processors

A phase to phase (P2P) processor is required to define if and how much matter should be moved from one phase to another within the same store. Aside from phase changes, there are other physical and chemical processes where something is "moved" from one phase to another. One example for this is the current CO2 removal technology used on the ISS where zeolites (solid, porous pellets) are used to absorb CO2. The gaseous CO2 is absorbed into the solid zeolite, which can be be simulated by using a P2P. For this example, there are two P2Ps required. One to remove the CO2 from the CabinAir stream passing through the CO2_Removal store and another one to remove the oxygen made in O2_Generation from the liquid Water phase to pass it to the Cabin. P2Ps also require ExMes and which can either be defined by the user to set specific names. As with branches, the phase object can also used as input in which case V-HAB automatically creates the required ExMe.

There are two different types of P2Ps: flow and stationary P2Ps. The flow P2P is assumed to act on the flow entering the phase and therefore only works with flow phases, which model a flow as a phase to split matter off from the flow. The stationary P2P is assumed to be less dependent on the current in flows of the phase. It works with both flow and normal phases, as long as the P2P can be assumed to remain constant over one tick and not be dependent on changes in the in-flow rate of the connected phases. However, both P2Ps act on the phases they are connected to and not on the actual flows. That is because it is not possible, from a programming point of view, to remove a part of the mass from a branch as that would make it difficult to ensure a closed mass balance. For that reason, the only location where mass is actually stored within V-HAB are the phases and in order to remove some mass from a mass flow you have to put a phase in between the initial and ending phases.The primary difference between the two P2P types is, that the stationary P2P is calculated in a seperate P2P post tick of the timer (if you want to learn more about post ticks, please read 5. Timer and Execution Order) and uses the update function to calculate the flowrate of the P2P. The flow P2Ps should only be used with flow phases and therefore also with a matter multibranch solver. Since the flow phase is modelled as beeing infinitessimal small and not contain any mass, the multibranch solver ensures a closed mass balance over the phase, which also includes the P2P flowrates. Therefore, flow P2Ps have a seperate function called calculateFlowRate which is provided with the inflows and inflow matter composition of the flow phase by the matter multibranch solver. This ensures that the P2P receives the current branch flowrates of the adjacent flow phase and can calculate the respective P2P flowrate. However, this also means that the P2P calculation may be called multiple times in the same V-HAB time step.

Both types of P2Ps will be used in this example. The flow P2P is used for the CO2 absorption while the stationary P2P is used to remove the oxygen from the water phase in the O2_Generation store. 

In order to create a new P2P you have to create the empty class definition for it. You can create the new folder +components in your +introduction folder and put the P2P there. At first we will define the CO2 P2P as stationary, in which case the LiOH_Absorber sub-class (main-class being the stationary P2P), the empty class definition should look like this: 

Empty P2P
classdef LiOH_Absorber < matter.procs.p2ps.stationary
    properties (SetAccess = public, GetAccess = public)
        arExtractPartials;
    end
    methods
        function this = LiOH_Absorber(oStore, sName, sPhaseIn, sPhaseOut)
            this@matter.procs.p2ps.stationary(oStore, sName, sPhaseIn, sPhaseOut);

        end
	end
	methods (Access = protected)
        function update(this)

        end
    end
end

As before, the class has persistent properties that can be set, a class definition, and an update function to calculate the values for the current time step. In this case, the P2P will be a very simple representation of a LiOH absorber that simply absorbs 90% of the CO2 flowing through it. This is not a physically correct representation of LiOH, but it is used here to illustrate how a P2P processor works. In the sub-class definition you first have to define which substance the P2P is supposed to move from one phase to another by using the following code lines:

this.arExtractPartials = zeros(1, this.oMT.iSubstances);
this.arExtractPartials(this.oMT.tiN2I.CO2) = 1;

It is also possible to tell the P2P to move multiple substances at once by setting more than one value of arExtractPartials larger than zero. However, the sum over all entries of arExtractPartials should always be equal to 1 because it is a ratio representation of the total flow rate. For example, if two values are set to 0.5 and the total flow rate is 1 kg/s then the partial flow rate for the specific substance would be 0.5 kg/s. It is also possible to change these values over the course of the simulation, but for now it is sufficient to set the value for CO2 to 1 and leave it at 1 for the whole simulation. Now the content of the update function has to be defined in a way that 90% of the CO2 is absorbed. For this purpose, it is first necessary to get the current ingoing flow rates and their matter composition by using the following code:

[ afFlowRate, mrPartials ] = this.getInFlows();

The function getInFlows() returns a vector afFlowRate that contains the current TOTAL flow rate of all incoming flows and a matrix mrPartials that contains the partial mass ratio (the mass composition) for each of the flows. The partial flow rate matrix can be calculated from this through element-wise multiplication and since we are only interested in the flow rate of CO2 we directly specify this at the multiplication:

afCO2InFlows = afFlowRate .* mrPartials(:, this.oMT.tiN2I.CO2);

In order to calculate the current flow rate of the P2P the sum over all ingoing CO2 flows has to be multiplied with 0.9:

fFlowRate = sum(afCO2InFlows) * 0.9;

And since this is a good location to talk about how to make code efficient, we will discuss an alternative solution (which produces identical results)

fFlowRate= sum(afCO2InFlows .* 0.9);

While both options produce the same results you should use the first option (where the sum is performed first and then the multiplication) because in this case MATLAB only has to perform the multiplication once. On the other hand, if you perform the multiplication first, as shown in the second option, MATLAB has to multiply each entry of the vector afCO2InFlows with 0.9. This might seem insignificant for this example especially since the number of inflows is very limited. However, with larger systems and many small inefficiencies like this, the inefficiencies add up to have a noticeable effect on the simulation speed, which is completely unnecessary because the second option does not even provide better results.

So far we have calculated the flow rate the P2P has to move but we have not yet "told" the P2P to actually do this. For this purpose, the function setMatterProperties() of the P2P has to be used to set the actual value. The whole code line for this function within the P2P will look like this:

this.setMatterProperties(fFlowRate, this.arExtractPartials);

The P2P will work as intended after implementing this line of code. Of course, this is only a very simple representation of a CO2 absorber. You can now add the LiOH_Absorber P2P to the system by using the following code:

xxyy.introduction.components.LiOH_Absorber(this.toStores.CO2_Removal, 'LiOH_P2P', this.toStores.CO2_Removal.toPhases.Air, this.toStores.CO2_Removal.toPhases.LiOH);

For Phase to Phase processes, the definition of the connected phases might be confusing at first. If you look at the definition of the LiOH_Absorber the inputs are:

LiOH_Absorber(oStore, sName, sPhaseIn, sPhaseOut)

where sPhaseIn and sPhaseOut are either string with the format 'StoreName.ExMeName' or a phase object. In and Out are meant with regard to the ExMe, which means that the first phase defined (sPhaseIn) is the one the matter is removed from if the P2P flowrate is positive.

The next P2P that has to be written are the phase to phase processors that are used to remove the oxygen and hydrogen that were generated by water electrolysis and putting them into their respective gas phases. You can simply copy the LiOH_Absorber P2P classdef and rename it to General_P2P to get started. For this purpose, a P2P processor that has an additional input to define the substance it is supposed to remove has to be defined (otherwise two new P2P .m files (class definitions) would be necessary, one for oxygen and one for hydrogen). This can be achieved by adding a string as input to the P2P definition like this:

General_P2P(oStore, sName, sPhaseIn, sPhaseOut, sSubstance)

Then the substance the absorber has to remove has to depend on the new input string.

this.arExtractPartials = zeros(1, this.oMT.iSubstances);
this.arExtractPartials(this.oMT.tiN2I.(sSubstance)) = 1;

You should also save the string as a property for the P2P processor to be able to reference it later on as well. For this you first have to add it to the property list at the beginning of the code and then add the following code line to function this = General_P2P().

this.sSubstance = sSubstance;

At this point, the P2P processor would remove either oxygen and hydrogen based on how much of it is flowing into the water phase. However, O2 and H2 do not flow into the water phase. They are created in the water phase. Therefore, the update function for the P2P has to be rewritten, simply remove all the code from the update function that was copied from the LiOH_Absorber processor to get started.

For this P2P we want to remove all matter of the specified substance that currently is in the phase. For this purpose, we have to divide the current mass of the substance with the time step to calculate the flow rate. Therefore, we first need a way to calculate the time step of the phase to phase processors. To do this, the P2P has a property called fLastUpdate which stores the absolute time at which the processors was last updated.

At the beginning of the update function the elapsed time can now be calculated by using the code:

fElapsedTime = this.oTimer.fTime - this.fLastUpdate;

In order to ensure that no negative or zero time steps are used we add an if query that aborts the update function if the time step is equal to or smaller than zero.

if fElapsedTime <= 0
   return 
end

Now the necessary framework for the calculation exists and you can calculate and set the required flow rate after the if query and above the definition of fLastExec.

fFlowRate = this.oIn.oPhase.afMass(this.oMT.tiN2I.(this.sSubstance))/fElapsedTime;

this.setMatterProperties(fFlowRate, this.arExtractPartials);

You now have a general P2P processor that removes all mater of the input-specified substance from its input phase within one time step. Now add the two required P2Ps for oxygen and hydrogen to the system, passing the correct input paramters to the General_P2P() function call with a unique sName for each.

However, try to consider what your P2P might do with this calculation in different cases inside the simulation. While the calculations are correct, there exist two possible issues with this P2P that might come back to haunt you later on. First, the time step we use for the calculation is not actually the next time step the P2P will make, but rather it is the previous time step! This can lead to problems if the time steps that the phases (and therefore the P2Ps) make are very different. For example if the mass you want to convert is always 1 kg but your timestep changes from 1s to 100s in a single step then you transfer way too much mass in the 100s timestep (as it uses the calculation 1kg/1s for 100s). The second possible issue is that small timesteps can lead to unrealistically high P2P flowrates. For example the smallest time step in V-HAB is 1e-8 seconds, if you divide 1 kg with this time you get a flowrate of 1e8 kg/s! When you create your own components you should stop to think about what calculation you implement, and what possible issues might occur later on. While this will slow you down a little in the beginning, it will greatly reduce the effort spent on debugging later and also help you to find possible issues while debugging your simulation!

Since this P2P is not a very good solution to our problem in this case, we will adjust it in 1.4.5 Events after we create the manipulator using events to solve our current problem. Therefore, if you receive the following error trying to execute the simulation with this P2P, do not worry and keep on till after 1.4.5 Events:

Error using base/throw (line 171)
Error using findProperty. No valid value for isobaric Isobaric Heat Capacity of O2 (liquid) found in matter table.


Now you have learned how to use stationary P2Ps and while the LiOH Absorber P2P we programmed would already function it would not react to flowrate changes directly. Since the flowrate in our example is constant that is not an issue, however if we had a rapidly changing flowrate the better solution would be to use a flow phase for the CO2 in the absorber and use a flow P2P. These P2Ps only work if they are used together with the multibranch solver, which is the case in this system. The multibranch solver then hands in the flowrates entering the two phases to which the P2P is connected and the user can decide how to handle the calculation based on these flowrates. Adjust the CO2 adsorption calculation which was written for the stationary P2P to the flow P2P with the following code and then use the flow P2P for this system!

Empty P2P
classdef LiOH_Absorber < matter.procs.p2ps.flow
    properties (SetAccess = protected, GetAccess = public)
        arExtractPartials;
    end
    methods
        function this = LiOH_Absorber(oStore, sName, sPhaseIn, sPhaseOut)
            this@matter.procs.p2ps.flow(oStore, sName, sPhaseIn, sPhaseOut);

        end
		
		function calculateFlowRate(this, afInFlowRates, aarInPartials, afOutFlowRates, aarOutPartials)
			% afInFlowRates: the in going flow rates in kg/s for this.oIn.oPhase
			% aarInPartials: the in going partial mass ratios for this.oIn.oPhase
			% afOutFlowRates: the in going flow rates in kg/s for this.oOut.oPhase
			% aarOutPartials: the in going partial mass ratios for this.oOut.oPhase
			%
			% You can calculate the total partial in flow rates for this.oIn.oPhase or
			% this.oOut.oPhase by using:
			% afPartialInFlowsForInside = sum((afInFlowRates .* aarInPartials),1);
			% afPartialInFlowsForOutside = sum((afOutFlowRates.* aarOutPartials),1);
			%
			% Note: These P2Ps only work if one of the branches for the flow phase is 
			% an iterative multibranch solver!
		end
	end
	methods (Access = protected)
        function update(~)

        end
    end
end

1.4.4.2 Flow to Flow Processors

In general, a F2F can be anything that you can built into a plumbing in the real world. The most general example of this would be a pipe but it can also represent a pump, a heat exchanger or anything else that has an impact on the properties of the matter flowing through the branch. Not all branches will require F2Fs to work correctly. This depends on the type of solver that is used to solve the branch. In general, any physics based solver will require the branch to contain F2F in order to calculate it correctly while on the other hand the manual and residual solver are not based on physical calculations and will therefore also work without any F2F. Now we want to write a fan F2F proc that generates the necessary pressure difference to move the air through the two heat exchanger loops. Create a new empty Matlab File called fan.m and save it to the components folder you also used for the p2p procs. The basic framework for a flow to flow processor that supports the iterative solver looks like this:

Empty F2F
classdef fan < matter.procs.f2f
	properties (SetAccess = protected, GetAccess = public)
        
    end
    methods
        function this = fan(oContainer, sName)
			this@matter.procs.f2f(oContainer, sName);         
			this.supportSolver('callback',  @this.solverDeltas); 
			
			this.bActive = true;
        end

        function fDeltaPress = solverDeltas(this, ~)

        end
    end
end

The f2f processor requires the property bActive since the solver uses this to differentiate between active and inactive components. If a component is set to inactive (bActive = false) the solver will simply ignore it. For flow to flow processors the calculation they have to perform depend on the type of solver for which they are used. Therefore, it is necessary to define which solvers are supported and which function contains the necessary calculation for this solver type. The iterative solver is a callback solver which means that it calls the various f2f processors within the branch and tells them to calculate their pressure (and if applicable temperature) difference. For this introduction a very simple representation of a fan that has a constant pressure difference with a linear startup over 1000 seconds will be sufficient. First we add the property fDeltaPressure and make it dependent of an input to the f2f processor to make it possible to define different values for the pressure difference. In order to do so, add a new input parameter called fDeltaPressure to the f2f definition of the fan and store it in a property that is also called this.fDeltaPressure! The paramter fDeltaPressure does not need to be added to the properties of this sub-class definition because the parent class already contains it. Now you only have to add the linear startup behavior to the solverDeltas() function by using the following code.

if this.oTimer.fTime < 1000
    fDeltaPress = (this.oTimer.fTime/1000)*this.fDeltaPressure;
else
    fDeltaPress = this.fDeltaPressure;
end

Again this code is way to simple for an actual simulation because the way this calculation works the fan only has a startup behavior at the beginning of the simulation. In a better implementation the fan would have another function to turn it on/off and the startup behavior would occur whenever it is switched from off to on. However, we do not need that for our simple showcase.

Now you can add the fans to the system by using the following code. (They have to be added before the definition of the branches where they are used)

xxyy.introduction.components.fan(this, 'HX_Fan', -1e5);

Because of the internal calculation logic pressure rises have to be negative values and therefore the input values for the pressure difference to the fan f2f for the HX air loop has to be -1e5 Pa. In order to add the f2f procs to a branch you have to add them to the definition of the branch inside the curved brackets {}. The order in which you put down the f2f proc names is also the physical order in which the components would be in the actual plumbing. Since a pressure rise without any component that produces pressure losses would result in infinite flowrates, also add two pipes from the V-HAB library using the following code:

fLength     = 1;    % Pipe length in m
fDiameter   = 0.01; % Pipe Diameter in m
fRoughness  = 1e-5; % Pipe surface roughness in m. If roughness is not provided, a smooth pipe is assumed
components.matter.pipe(this, 'Pipe_1', fLength, fDiameter, fRoughness);
components.matter.pipe(this, 'Pipe_2', fLength, fDiameter, fRoughness);

The branch for the HX air loop the definition should then look like this.

matter.branch(this, 'Cabin.Cabin_to_HX', {'Pipe_1','HX_Fan','Pipe_2'}, 'Cabin.Cabin_from_HX', 'Cabin_HX_Loop');

Since the branch now contains pipes and a fan, we can solve it numerically and no longer have to manually provide a flowrate for it. Therefore, change the solver of this branch into the interval type:

solver.matter.interval.branch(this.toBranches.Cabin_HX_Loop);

Also remove the line where the flowrate for this branch was manually defined!

1.4.4.3 Manipulators

The last type of processor in V-HAB are the Manipulators (manip). So far the processors covered moving matter from one place to another or changing the properties (pressure, temperature, state etc.) of the matter but not changing one type of matter into a completely different type. However, this is exactly what is necessary to model chemical reactions where substances react and transform into other substances. In this example a manipulator is required to model the electrolysis of water where water is broken down into H2 and O2. To begin programming the manip first create an empty Matlab file again and save it under the name O2_Generation_Manip into the component folder which was also used for the other components. The basic code framework for the manipulator looks like this:

Empty Manip
classdef O2_Generation_Manip < matter.manips.substance.stationary 
    methods
        function this = O2_Generation_Manip(sName, oPhase)
			this@matter.manips.substance.stationary(sName, oPhase); 

        end
	end
	
    methods (Access = protected)
        function update(this)
        end
    end
end

As for the P2P procs the manipulators are also divided into flow and stationary processors. In this case a stationary processor is used since the phase in which the reaction takes place is not a flow phase! The first calculation in the update function is the calculation of the incoming partial flow rates.

[ afFlowRate, mrPartials ] = this.getInFlows();
if isempty(afFlowRate)
	afFlowRateIn = zeros(1, this.oPhase.oMT.iSubstances);
else
	afFlowRateIn = sum(afFlowRate .* mrPartials(1, :),1);
end

The electrolyzer model used here will neglect most electrochemical effects and simply transforms the inflow of water into hydrogen and oxygen based on the stoichiometric reaction. This can be achieved by using the following code:

fH2Production = (this.oMT.afMolarMass(this.oMT.tiN2I.H2)/this.oMT.afMolarMass(this.oMT.tiN2I.H2O)) 		* afFlowRateIn(this.oMT.tiN2I.H2O);

fO2Production = (1-(this.oMT.afMolarMass(this.oMT.tiN2I.H2)/this.oMT.afMolarMass(this.oMT.tiN2I.H2O))) 	* afFlowRateIn(this.oMT.tiN2I.H2O);

For manipulators the value that has to be calculated and set are the partial flow rates for all substances. In this case the substances are hydrogen, oxygen and water. The flow rate of hydrogen and oxygen was already calculated and the flow rate for water is simply the sum of these two values multiplied with -1 since water is the substance that is consumed in the reaction. For all other substances the flow rate is zero and therefore a vector containing only zeros is created at first and then the specific values for water, hydrogen and oxygen are set to the respective flow rates.

afPartialFlowRates = zeros(1, this.oPhase.oMT.iSubstances);
afPartialFlowRates(this.oMT.tiN2I.H2O) = - (fO2Production + fH2Production);
afPartialFlowRates(this.oMT.tiN2I.O2) = fO2Production;
afPartialFlowRates(this.oMT.tiN2I.H2) = fH2Production;

Now these values have to be set for the manipulator by using the following line of code.

update@matter.manips.substance.stationary(this, afPartialFlowRates);

With this the oxygen generation manipulator is also finished and can be added to the system. The required inputs for the manipulator are only the name of the manipulator and the phase in which it is located. For this case the water phase in the O2_Generation store is used.

You should note that you can only add one manipulator per phase. This may seem like an unnecessary limitation at first, however it prevents problems later on. For multiple manipulators V-HAB would not be able to decide on an update order and would therefore update them in an random order. However, usually manips depend on the content of the phase for their calculation. If the phase content depends on an random update order it would not make a lot of sense. Rather if you have multiple reactions taking place in the phase at once the suggested solution is to split the calculations up into function files (to have better overview over them, if they are all inside one manips update function this could be confusing) and then call these functions in the manip update to calculate overall flowrates that the manip can then set. This way the user can also decide on an update order of the function (if he wants) or decide if all equations should use the same initial state.

Updates to Plots for Code Verification

The changes made in this chapter will result in new system output results, which will be visible in the plots of parameters that are already being logged from previous chapters. If everything above was programmed correctly, then the plots should now appear as follows:

  • Keine Stichwörter