Base Solvex Development Cortix Tech 03Oct2025

1. Use-Case 02: TBP-Diluent-H\(_2\)O Mixing#

Cortix Tech Logo

Developer: Valmor F. de Almeida, PhD

Cortix Tech, Lowell, MA 01854, USA

Revision date: 02Dec25

1.1. Objectives#

  • Develop usecase scenario for water extraction by TBP without a vapor phase.

  • Test implementation and present results.

  • Use AI assistants to help with information and reporting.

    • AI requests below may need to be executed multiple times if the result is not satisfactory or incorrect.

'''AI Assistance options'''
# Set all to False if you do not have access to OpenAI API and/or AI codes below
cortix_ai = False
stage_ai = False
'''Generate proprietary knowledge database?'''
db_save = False # set this to false if going public (online) with this notebook
'''Other Helpers'''
fig_count = 0
tbl_count = 0

1.2. Base System#

Single stage mixing of water, and TBP with inert diluent.

'''Setup the Base System'''
from cortix import Cortix
from cortix import Network
from cortix import Units as unit
from cortix import ReactionMechanism
from cortix import Quantity

system = Cortix(use_mpi=False, splash=True) # System top level
system_net = system.network = Network() # Network
[13917] 2025-12-02 13:51:51,807 - cortix - INFO - Created Cortix object 
_____________________________________________________________________________
                             L A U N C H I N G                               
_____________________________________________________________________________
      ...                                        s       .     (TAAG Fraktur)
   xH88"`~ .x8X                                 :8      @88>
 :8888   .f"8888Hf        u.      .u    .      .88      %8P      uL   ..
:8888>  X8L  ^""`   ...ue888b   .d88B :@8c    :888ooo    .     .@88b  @88R
X8888  X888h        888R Y888r ="8888f8888r -*8888888  .@88u  ""Y888k/"*P
88888  !88888.      888R I888>   4888>"88"    8888    888E`    Y888L
88888   %88888      888R I888>   4888> "      8888      888E      8888
88888 `> `8888>     888R I888>   4888>        8888      888E      `888N
`8888L %  ?888   ! u8888cJ888   .d888L .+    .8888Lu=   888E   .u./"888&
 `8888  `-*""   /   "*888*P"    ^"8888*"     ^%888*     888&  d888" Y888*"
   "888.      :"      "Y"          "Y"         "Y"      R888" ` "Y   Y"
     `""***~"`                                           ""
                             https://cortix.org                              
_____________________________________________________________________________

1.2.1. Stage#

Instantiate a single stage.

'''Import Stage'''
from solvex import Stage
'''Create Base Stage and add to Base System'''
from solvex import Stage

# Initialization
mixing_volume = 1*unit.L
# Aqueous phase
mixing_vol_flowrate_aqu = 500*unit.mL/unit.min
# Organic phase
mixing_vol_flowrate_org = 600*unit.mL/unit.min
# Vapor phase
mixing_vol_flowrate_vap = (0.0*mixing_vol_flowrate_org/100, 0.0*mixing_vol_flowrate_aqu/100) # percentage of (org, aqu)

mixing_vol_flowrates = [mixing_vol_flowrate_org, mixing_vol_flowrate_aqu, mixing_vol_flowrate_vap]

stg_temperature = unit.convert_temperature(40, 'C', 'K')

stg = Stage(mixing_volume, mixing_vol_flowrates, stg_temperature) # Create solvent extraction module

system_net.module(stg)
print('Flow residence time [s]: average = %5.3e'%stg.flow_residence_time_avg)
print('Aqueous volume fraction = %5.3e'%stg.volume_frac_aqu) 
print('Organic volume fraction = %5.3e'%stg.volume_frac_org) 
Flow residence time [s]: average = 5.455e+01
Aqueous volume fraction = 4.545e-01
Organic volume fraction = 5.455e-01
'''Draw system'''
system_net.draw(engine='circo', node_shape='folder', ports=True)
../../_images/955457756fd09a8d757eb56107eb2c7ef544d7e34aee257aed96129bbb43b74f.svg
'''For help purposes'''
import solvex.stage

Documentation options:

  • Live in this notebook run on code cell: help(solvex_ustc.stage)

  • On the web: source

#help(solvex.stage)

1.2.2. Reaction Mechanisms#

1.2.2.1. Water extraction example#

args_dict = {'water_activity': 1.0}
file_name = 'tbp-h2o.txt'
rxn_mech = ReactionMechanism(file_name=file_name, order_species=True, args_dict=args_dict)
WARNING: ReactionMechanism: user must implement a H2O*[C4H9O]3PO(o) product partition function with signature <product>(rxn_mech, temperature, spc_molar_cc, arg_dict) function for [C4H9O]3PO(o) + H2O(a) <-> H2O*[C4H9O]3PO(o) 
WARNING: ReactionMechanism: user must implement a [H2O]2*[[C4H9O]3PO]2(o) product partition function with signature <product>(rxn_mech, temperature, spc_molar_cc, arg_dict) function for 2 [C4H9O]3PO(o) + 2 H2O(a) <-> [H2O]2*[[C4H9O]3PO]2(o) 
WARNING: ReactionMechanism: user must implement a [H2O]6*[[C4H9O]3PO]3(o) product partition function with signature <product>(rxn_mech, temperature, spc_molar_cc, arg_dict) function for 3 [C4H9O]3PO(o) + 6 H2O(a) <-> [H2O]6*[[C4H9O]3PO]3(o) 
#'''User input'''
#rxn_mech.cat_input()
#'''Show Mechanism'''
# Jupyter Book does not render LaTeX through IPython.display(Markdown)
#rxn_mech.md_print()

5 Species

\[\begin{align*} &{\mathrm{H}_{2}\mathrm{O}}_\mathrm{(a)}, {\mathrm{H}_{2}\mathrm{O}\bullet[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}, {[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}, {[\mathrm{H}_{2}\mathrm{O}]_{2}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{2}}_{\mathrm{(o)}}, {[\mathrm{H}_{2}\mathrm{O}]_{6}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{3}}_{\mathrm{(o)}}\end{align*}\]

3 Reactions

\[\begin{align*} {\mathrm{H}_{2}\mathrm{O}}_\mathrm{(a)}\ + \ {[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}\ &\longleftrightarrow \ {\mathrm{H}_{2}\mathrm{O}\bullet[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}\\ 2.0\,{\mathrm{H}_{2}\mathrm{O}}_\mathrm{(a)}\ + \ 2.0\,{[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}\ &\longleftrightarrow \ {[\mathrm{H}_{2}\mathrm{O}]_{2}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{2}}_{\mathrm{(o)}}\\ 6.0\,{\mathrm{H}_{2}\mathrm{O}}_\mathrm{(a)}\ + \ 3.0\,{[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}\ &\longleftrightarrow \ {[\mathrm{H}_{2}\mathrm{O}]_{6}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{3}}_{\mathrm{(o)}}\\ \end{align*}\]
#'''Species and Reactions Manual Output'''
#print(len(rxn_mech.species_names), ' **Species**\n', rxn_mech.latex_species)
#print(len(rxn_mech.reactions), ' **Reactions**\n', rxn_mech.latex_rxn)

1.2.2.2. Sanity check#

'''Data Check'''
print('Is mass conserved?', rxn_mech.is_mass_conserved())
rxn_mech.rank_analysis(verbose=True, tol=1e-8)
print('S=\n', rxn_mech.stoic_mtrx)
Is mass conserved? True
# reactions =  3
# species   =  5
rank of S =  3
S is full rank.
S=
 [[-1.  1. -1.  0.  0.]
 [-2.  0. -2.  1.  0.]
 [-6.  0. -3.  0.  1.]]

1.2.2.3. User-provided partition functions#

'''Equilibrium constants involved in the reaction mechanism'''

from solvex.partition_func_local import partition_h2o_tbp_org
from solvex.partition_func_local import partition_2h2o_2tbp_org
from solvex.partition_func_local import partition_6h2o_3tbp_org

# Equilibrium constant for H2O*TBP complexation
rxn_mech.data[0]['tau-partition-function'] = partition_h2o_tbp_org

# Equilibrium constant for 2H2O*2TBP complexation
rxn_mech.data[1]['tau-partition-function'] = partition_2h2o_2tbp_org

# Equilibrium constant for 6H2O*3TBP complexation
rxn_mech.data[2]['tau-partition-function'] = partition_6h2o_3tbp_org

1.2.2.4. Add reaction mechanism to stage#

stg.add_reaction_mechanism(rxn_mech)

1.2.2.5. Verify Species Groups#

#'''Aqueous phase'''
#str = stg.rxn_mech.md_print('(a)')
#'''Show aqueous phase'''
# Jupyter Book does not render LaTeX through IPython.display(Markdown)
#print(str)
\[\begin{align*} &{\mathrm{H}_{2}\mathrm{O}}_\mathrm{(a)}, \ \end{align*}\]
#'''Organic phase'''
#str = stg.rxn_mech.md_print('(o)', n_species_line=5)
#'''Show organic phase'''
# Jupyter Book does not render LaTeX through IPython.display(Markdown)
#print(str)
\[\begin{align*} &{\mathrm{H}_{2}\mathrm{O}\bullet[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}, \ {[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}}_\mathrm{(o)}, \ {[\mathrm{H}_{2}\mathrm{O}]_{2}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{2}}_{\mathrm{(o)}}, \ {[\mathrm{H}_{2}\mathrm{O}]_{6}\bullet[[\mathrm{C}_{4}\mathrm{H}_{9}\mathrm{O}]_{3}\mathrm{P}\mathrm{O}]_{3}}_{\mathrm{(o)}}, \ \end{align*}\]

1.2.2.6. Mass transfer data#

'''Adjust relaxation times for mass transfer'''
stg.rxn_mech.data[0]['tau'] = 1.0e-0 * stg.flow_residence_time_avg
stg.rxn_mech.data[1]['tau'] = 1.0e-0 * stg.flow_residence_time_avg
stg.rxn_mech.data[2]['tau'] = 1.0e-0 * stg.flow_residence_time_avg

1.2.2.7. Meta data#

'''Names and info of interest for species'''
  
tbp_org_name = '[C4H9O]3PO(o)'
tbp_org = stg.organic_phase.get_species(tbp_org_name)
tbp_org.info = 'Free TBP'

tbp_monomer_org_name = 'H2O*[C4H9O]3PO(o)'
tbp_monomer_org = stg.organic_phase.get_species(tbp_monomer_org_name)
tbp_monomer_org.info = 'TBP Monomer Hydrate'

tbp_dimer_org_name = '[H2O]2*[[C4H9O]3PO]2(o)'
tbp_dimer_org = stg.organic_phase.get_species(tbp_dimer_org_name)
tbp_dimer_org.info = 'TBP Dimer'

tbp_trimer_hexahydrate_org_name = '[H2O]6*[[C4H9O]3PO]3(o)'
tbp_trimer_hexahydrate_org = stg.organic_phase.get_species(tbp_trimer_hexahydrate_org_name)
tbp_trimer_hexahydrate_org.info = 'TBP Trimer Hexahydrate'

1.3. Initial Conditions#

'''Organic phase in the mixer (diluent is inert)'''

vol_frac_tbp_org = 30/100 # free tbp
#TODO: look this up at 40 C # W: TODO: look this up at 40 C
rho_tbp = 972.5 * unit.gram/unit.L # pure liquid TBP
stg.rxn_mech.args_dict['rho-tbp'] = rho_tbp # for future use in the reaction mechanism
tbp_mass_cc_org = rho_tbp * vol_frac_tbp_org # per volume of organic phase in the mixture
stg.organic_phase.set_value(tbp_org_name, tbp_mass_cc_org)
print('mass_cc_tbp_org [g/L]  =', tbp_mass_cc_org)
print('molar_cc_tbp_org [M] = %1.5e'%(tbp_mass_cc_org/tbp_org.molar_mass/unit.molar))
mass_cc_tbp_org [g/L]  = 291.75
molar_cc_tbp_org [M] = 1.09551e+00
'''Aqueous phase in the mixer'''
 
h2o_aqu = stg.aqueous_phase.get_species('H2O(a)')
h2o_aqu.info = 'Water'
#TODO look this up at 40 C # W: TODO look this up at 40 C
rho_h2o_aqu = 992 * unit.gram/unit.L # per volume of aqueous phase in the mixture
stg.aqueous_phase.set_value(h2o_aqu.name, rho_h2o_aqu)

1.4. Inflow Parameters#

'''Aqueous phase in the inflow'''
#TODO here the concentration must be larger than in the initial condition in the mixer for lower temp #   W: Line too long (105/100)
# look this up later, 1.01 factor may be incorrect
stg.inflow_aqueous_phase.set_value('H2O(a)', 1.0 * rho_h2o_aqu)
'''Organic phase in the inflow'''
stg.inflow_organic_phase.set_value(tbp_org_name, tbp_mass_cc_org)

1.5. Start-up Simulation#

Define the start-up simulation period as one flow residence time.

'''Getting ready to run'''

end_time = 1 * stg.flow_residence_time_avg
import numpy as np
ave_tau = np.mean([data['tau'] for data in stg.rxn_mech.data])
time_step = ave_tau / 15
show_time = (True, 5*time_step)

stg.name = 'Stg-1'
stg.save = True
stg.verbose = True

stg.perturb_flowrates = False

stg.time_step = time_step
stg.end_time = end_time
stg.show_time = show_time
'''Run system in parallel'''
stg.monitor_mass_flowrates = False
stg.monitor_mass_conservation_residual = False
stg.mass_bal_rate_dens_res_tol = 1.e-8 * unit.micro*unit.gram/unit.L/unit.second

system.run()
system.close() # Shutdown Cortix
[13917] 2025-12-02 13:51:52,390 - cortix - INFO - Launching Module <solvex.stage.Stage object at 0x7f7a380b56a0>
[13958] 2025-12-02 13:51:53,759 - cortix - INFO - Stg-1::run():time[m]=0.0
[13958] 2025-12-02 13:51:53,837 - cortix - INFO - Stg-1::run():time[m]=0.3
[13958] 2025-12-02 13:51:53,900 - cortix - INFO - Stg-1::run():time[m]=0.6
Total mass rate density (mixture volume) residual [g/L-s]= -1.38778e-17
total mass inflow rate [g/min]   = 6.710e+02
total mass outflow rate [g/min]  =  6.711e+02
	 net total mass flow rate [g/min] = 1.041e-13
[13958] 2025-12-02 13:51:53,977 - cortix - INFO - Stg-1::run():time[m]=1.0 (et[s]=0.2)
[13917] 2025-12-02 13:51:54,169 - cortix - INFO - run()::Elapsed wall clock time [s]: 2.36
[13917] 2025-12-02 13:51:54,170 - cortix - INFO - Closed Cortix object.
_____________________________________________________________________________
                           T E R M I N A T I N G                             
_____________________________________________________________________________
      ...                                        s       .     (TAAG Fraktur)
   xH88"`~ .x8X                                 :8      @88>
 :8888   .f"8888Hf        u.      .u    .      .88      %8P      uL   ..
:8888>  X8L  ^""`   ...ue888b   .d88B :@8c    :888ooo    .     .@88b  @88R
X8888  X888h        888R Y888r ="8888f8888r -*8888888  .@88u  ""Y888k/"*P
88888  !88888.      888R I888>   4888>"88"    8888    888E`    Y888L
88888   %88888      888R I888>   4888> "      8888      888E      8888
88888 `> `8888>     888R I888>   4888>        8888      888E      `888N
`8888L %  ?888   ! u8888cJ888   .d888L .+    .8888Lu=   888E   .u./"888&
 `8888  `-*""   /   "*888*P"    ^"8888*"     ^%888*     888&  d888" Y888*"
   "888.      :"      "Y"          "Y"         "Y"      R888" ` "Y   Y"
     `""***~"`                                           ""
                             https://cortix.org                              
_____________________________________________________________________________
[13917] 2025-12-02 13:51:54,171 - cortix - INFO - close()::Elapsed wall clock time [s]: 2.36
'''Recover stage'''
stg = system_net.modules[0]

1.5.1. Organic Phase Results#

'''Plot organic phase'''
# TODO: time axis normalized by phase flow residence time.
stg.organic_phase.plot(title='Organic Phase Start-Up', legend='Organic Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Organic phase species history dashboard at start-up.')
../../_images/bf1d3e013ec760ff0e996e63056166b2de66ad2b19802368c4914d9572189ab7.png
Figure 1: Organic phase species history dashboard at start-up.
'''Compute molar concentration of TBP species'''
import numpy as np

tbp_mass_cc_org_history = stg.organic_phase.get_column(tbp_org_name)
tbp_molar_cc_org_history = np.array(tbp_mass_cc_org_history)/tbp_org.molar_mass

tbp_monomer_mass_cc_org_history = stg.organic_phase.get_column(tbp_monomer_org_name)
tbp_monomer_molar_cc_org_history = np.array(tbp_monomer_mass_cc_org_history)/tbp_monomer_org.molar_mass

tbp_dimer_mass_cc_org_history = stg.organic_phase.get_column(tbp_dimer_org_name)
tbp_dimer_molar_cc_org_history = np.array(tbp_dimer_mass_cc_org_history)/tbp_dimer_org.molar_mass

tbp_trimer_hexahydrate_mass_cc_org_history = stg.organic_phase.get_column(tbp_trimer_hexahydrate_org_name)
tbp_trimer_hexahydrate_molar_cc_org_history = np.array(tbp_trimer_hexahydrate_mass_cc_org_history)/tbp_trimer_hexahydrate_org.molar_mass

time_stamps = np.array(stg.organic_phase.time_stamps)
n_startup = len(time_stamps)

tbl_count += 1
print(f'Table {tbl_count}: Molarity history of TBP in the organic phase at start-up.')
print('Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for t,a,b,c,d in zip(time_stamps[::5]/unit.min, 
                 tbp_molar_cc_org_history[::5]/unit.molar, 
                 tbp_monomer_molar_cc_org_history[::5]/unit.molar,
                 tbp_dimer_molar_cc_org_history[::5]/unit.molar,
                 tbp_trimer_hexahydrate_molar_cc_org_history[::5]/unit.molar):
    balance = a+b+2*c+3*d - tbp_mass_cc_org/tbp_org.molar_mass/unit.molar
    print('  %1.2e      %1.3f         %1.3e        %1.3e               %1.3e          %+1.3e'%(t, a, b, c, d, balance))
Table 1: Molarity history of TBP in the organic phase at start-up.
Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |
  0.00e+00      1.096         0.000e+00        0.000e+00               0.000e+00          +0.000e+00
  3.03e-01      0.939         2.419e-02        5.303e-02               8.904e-03          -4.441e-16
  6.06e-01      0.885         3.443e-02        7.103e-02               1.124e-02          +0.000e+00
  9.09e-01      0.867         3.892e-02        7.730e-02               1.177e-02          +0.000e+00
'''Volume fraction of TBP at start-up'''
tbl_count += 1
print(f'Table {tbl_count}: Volume fractions of free and total TBP in the organic phase at start-up.')
print('Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
total_tbp_molar_cc_org_history = tbp_molar_cc_org_history + tbp_monomer_molar_cc_org_history + \
                                 2*tbp_dimer_molar_cc_org_history + 3*tbp_trimer_hexahydrate_molar_cc_org_history
total_tbp_mass_cc_org_history = total_tbp_molar_cc_org_history * tbp_org.molar_mass
for t,a,b in zip(time_stamps[::5]/unit.min, 
               np.array(tbp_mass_cc_org_history[::5])/rho_tbp*100, total_tbp_mass_cc_org_history[::5]/rho_tbp*100):
    print('  %1.2e         %1.3f                  %1.3f'%(t, a, b))
Table 2: Volume fractions of free and total TBP in the organic phase at start-up.
Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |
  0.00e+00         30.000                  30.000
  3.03e-01         25.701                  30.000
  6.06e-01         24.244                  30.000
  9.09e-01         23.734                  30.000
'''Total water extracted'''
water_molar_cc_org_history = tbp_monomer_molar_cc_org_history \
                           + 2*tbp_dimer_molar_cc_org_history \
                           + 6*tbp_trimer_hexahydrate_molar_cc_org_history

water_mass_cc_org_history = water_molar_cc_org_history * h2o_aqu.molar_mass

tbl_count += 1
print(f'Table {tbl_count}: Mass concentration and molarity history of water in the organic phase at start-up.')
print('Time [s] |   [M]   |  [g/L]  |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for a,b,c in zip(time_stamps[::5], water_molar_cc_org_history[::5]/unit.molar, water_mass_cc_org_history[::5]):
    print('%1.2e  %1.3e %1.3e'%(a, b, c))
Table 3: Mass concentration and molarity history of water in the organic phase at start-up.
Time [s] |   [M]   |  [g/L]  |
0.00e+00  0.000e+00 0.000e+00
1.82e+01  1.837e-01 3.309e+00
3.64e+01  2.439e-01 4.394e+00
5.45e+01  2.641e-01 4.758e+00

1.5.2. Aqueous Phase Results#

'''Plot aqueous phase'''
# TODO: time axis normalized by phase flow residence time.
stg.aqueous_phase.plot(title='Aqueous Phase Start-Up', legend='Aqueous Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Aqueous phase species history dashboard at start-up.')
../../_images/0873236403dbfd0800dc3435112543fea2dc317eef38e79c4d1327e7081ab359.png
Figure 2: Aqueous phase species history dashboard at start-up.

1.5.3. Overall Stage Efficiency#

Stage efficiency measures how close to chemical equilibrium the system is as a whole. This is a direct result of the reaction relaxation time which is dependent on the mass transfer coefficients of the system. Much more needs to be investigated in this project with various degrees of theory but these results represent the beginning of a solid development.

'''Stage overall efficiency'''

quant = stg.efficiency_history(mean=True)
quant.plot(title='Stage Efficiency @ %2.1f C Start-Up'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', show=True, figsize=[10,3], error_data=True)

fig_count += 1
print(f'Figure {fig_count}: Stage efficiency history at start-up.')
../../_images/c1c9df6c8267c7eb0844803b734f5c061046262b82bc6923f2c5fe6849e8153d.png
Figure 3: Stage efficiency history at start-up.
tbl_count += 1
print(f'Table {tbl_count}: Stage efficiency history at start-up.')
print('Time [min]  Stage. Eff.(+-std) [%]')
time_name = ''
import pandas as pd
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(['',''], axis=1).rename_axis(time_name)
       .round(3))
print(df.to_string(max_rows=20, min_rows=20))
Table 4: Stage efficiency history at start-up.
Time [min]  Stage. Eff.(+-std) [%]
                   
                   
0.00   0.000  0.000
0.06   6.507  0.111
0.12  12.589  0.377
0.18  18.131  0.711
0.24  23.077  1.052
0.30  27.417  1.359
0.36  31.171  1.611
0.42  34.380  1.797
0.48  37.096  1.919
0.55  39.378  1.982
0.61  41.282  1.994
0.67  42.862  1.965
0.73  44.167  1.904
0.79  45.242  1.820
0.85  46.125  1.720
0.91  46.848  1.610
0.97  47.439  1.495
'''Individual reaction efficiency'''
quant = stg.efficiency_history()
tbl_count += 1
print(f'Table {tbl_count}: Reaction efficiency history at start-up.')
print('Time [min]    Rxn Eff. [%]')

col_names = [f'r{i}' for i in range(len(stg.rxn_mech.reactions))]
time_name = ''
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(3))
print(df.to_string(max_rows=20, min_rows=20))
Table 5: Reaction efficiency history at start-up.
Time [min]    Rxn Eff. [%]
          r0      r1      r2
                            
0.00   0.000   0.000   0.000
0.06   6.372   6.506   6.644
0.12  12.131  12.581  13.054
0.18  17.270  18.111  19.012
0.24  21.807  23.042  24.383
0.30  25.779  27.366  29.107
0.36  29.232  31.105  33.176
0.42  32.218  34.303  36.618
0.48  34.790  37.011  39.488
0.55  36.997  39.288  41.849
0.61  38.886  41.190  43.768
0.67  40.501  42.772  45.312
0.73  41.880  44.080  46.542
0.79  43.055  45.160  47.512
0.85  44.058  46.048  48.270
0.91  44.912  46.776  48.855
0.97  45.641  47.373  49.302

1.5.3.1. Total water extracted in equilibrium#

From reference: 1994 Naganawa and Tachimori Anal. Sci. 10 p 607

[H2O]_o,E = 0.4265 M

h2o_e_molar = water_molar_cc_org_history[-1]/unit.molar

print('Total H2O extracted [M] = %1.4f'%h2o_e_molar)
print('Error (compared to measurement) [%%] = %1.2f'%((h2o_e_molar-0.4265)/0.4265*100))
Total H2O extracted [M] = 0.2661
Error (compared to measurement) [%] = -37.60

1.5.4. Reaction Rate Density#

This quantity only makes sense per volume of the mixture.

'''Reaction rate density'''
import matplotlib.pyplot as plt

quant = stg.r_vec_history() # mole/m^3-s

quant.plot(title='Reaction Rate Density @ %2.1f C Start-Up'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.reactions, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Reaction rate density history at start-up.')
../../_images/9d1ee5aa2183e0bb157c740f2e26d49b220776bbcb1d2963da05cafa8a1e68be.png
Figure 4: Reaction rate density history at start-up.
tbl_count += 1
print(f'Table {tbl_count}: Reaction rate density history at start-up.')
print('Time [min]      r [mM/s]')
import pandas as pd
col_names = [f'r{i}' for i in range(len(stg.rxn_mech.reactions))]
time_name = 't [min]'
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(4))
print(df.to_string(max_rows=20, min_rows=20))
Table 6: Reaction rate density history at start-up.
Time [min]      r [mM/s]
             r0      r1      r2
t [min]                        
0.00     1.0955  2.6403  0.4865
0.06     0.9820  2.2626  0.3985
0.12     0.8889  1.9681  0.3330
0.18     0.8122  1.7365  0.2836
0.24     0.7490  1.5533  0.2459
0.30     0.6966  1.4076  0.2169
0.36     0.6532  1.2912  0.1944
0.42     0.6171  1.1980  0.1770
0.48     0.5870  1.1231  0.1633
0.55     0.5620  1.0627  0.1527
0.61     0.5411  1.0141  0.1444
0.67     0.5235  0.9748  0.1378
0.73     0.5088  0.9430  0.1327
0.79     0.4965  0.9173  0.1287
0.85     0.4862  0.8964  0.1256
0.91     0.4774  0.8795  0.1232
0.97     0.4701  0.8658  0.1213

1.5.5. Species Generation Rate Density#

This quantity is on the basis of mixing volume so all species generation values can be compared.

'''Species generation rate density'''
import matplotlib.pyplot as plt

quant = stg.g_vec_history() # mole/m^3-s

quant.plot(title='Species Generation Rate Density @ %2.1f C Start-Up'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.species_names, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Species generation rate density history at start-up.')
../../_images/82f59bb9eb30985f853d5ff03d115a016f94be61860843025e6d885e0ce1ac19.png
Figure 5: Species generation rate density history at start-up.
tbl_count += 1
print(f'Table {tbl_count}: Species generation rate density history at start-up.')
print('Time [min]        g [mM/s]')
import pandas as pd
col_names = [f'a{i}' for i in range(len(stg.rxn_mech.species_names))]
time_name = 't [min]'
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(4))
print(df.to_string(max_rows=20, min_rows=20))
Table 7: Species generation rate density history at start-up.
Time [min]        g [mM/s]
             a0      a1      a2      a3      a4
t [min]                                        
0.00    -9.2949  1.0955 -7.8355  2.6403  0.4865
0.06    -7.8984  0.9820 -6.7028  2.2626  0.3985
0.12    -6.8231  0.8889 -5.8240  1.9681  0.3330
0.18    -5.9866  0.8122 -5.1359  1.7365  0.2836
0.24    -5.3306  0.7490 -4.5931  1.5533  0.2459
0.30    -4.8128  0.6966 -4.1623  1.4076  0.2169
0.36    -4.4020  0.6532 -3.8188  1.2912  0.1944
0.42    -4.0748  0.6171 -3.5439  1.1980  0.1770
0.48    -3.8132  0.5870 -3.3232  1.1231  0.1633
0.55    -3.6037  0.5620 -3.1456  1.0627  0.1527
0.61    -3.4354  0.5411 -3.0023  1.0141  0.1444
0.67    -3.3002  0.5235 -2.8866  0.9748  0.1378
0.73    -3.1912  0.5088 -2.7930  0.9430  0.1327
0.79    -3.1035  0.4965 -2.7173  0.9173  0.1287
0.85    -3.0327  0.4862 -2.6559  0.8964  0.1256
0.91    -2.9756  0.4774 -2.6061  0.8795  0.1232
0.97    -2.9296  0.4701 -2.5657  0.8658  0.1213

1.6. Steady-State Simulation#

Pick up from where it left from the past run() and continue to the fully developed state of 5 flow residence time. This demonstrates how to continue a simulation from where it was interrupted. Note that the state of the system is automatically used as the initial condition for the next run().

end_time += 4 * stg.flow_residence_time_avg
time_step = 4 * stg.flow_residence_time_avg / 10
show_time = (True, 10*time_step)

stg.time_step = time_step
stg.initial_time = stg.end_time
stg.end_time = end_time
stg.show_time = show_time
'''Run system in parallel'''
stg.monitor_mass_flowrates = False
stg.monitor_mass_conservation_residual = False
system.run()
system.close() # Shutdown Cortix
[13917] 2025-12-02 13:51:56,791 - cortix - INFO - Launching Module <solvex.stage.Stage object at 0x7f79dd878a50>
[13985] 2025-12-02 13:51:58,019 - cortix - INFO - Stg-1::run():time[m]=1.0
Total mass rate density (mixture volume) residual [g/L-s]= -9.02056e-17
total mass inflow rate [g/min]   = 6.710e+02
total mass outflow rate [g/min]  =  6.711e+02
	 net total mass flow rate [g/min] = 1.041e-13
[13985] 2025-12-02 13:51:58,127 - cortix - INFO - Stg-1::run():time[m]=4.6 (et[s]=0.1)
[13917] 2025-12-02 13:51:58,300 - cortix - INFO - run()::Elapsed wall clock time [s]: 6.49
[13917] 2025-12-02 13:51:58,301 - cortix - INFO - Closed Cortix object.
_____________________________________________________________________________
                           T E R M I N A T I N G                             
_____________________________________________________________________________
      ...                                        s       .     (TAAG Fraktur)
   xH88"`~ .x8X                                 :8      @88>
 :8888   .f"8888Hf        u.      .u    .      .88      %8P      uL   ..
:8888>  X8L  ^""`   ...ue888b   .d88B :@8c    :888ooo    .     .@88b  @88R
X8888  X888h        888R Y888r ="8888f8888r -*8888888  .@88u  ""Y888k/"*P
88888  !88888.      888R I888>   4888>"88"    8888    888E`    Y888L
88888   %88888      888R I888>   4888> "      8888      888E      8888
88888 `> `8888>     888R I888>   4888>        8888      888E      `888N
`8888L %  ?888   ! u8888cJ888   .d888L .+    .8888Lu=   888E   .u./"888&
 `8888  `-*""   /   "*888*P"    ^"8888*"     ^%888*     888&  d888" Y888*"
   "888.      :"      "Y"          "Y"         "Y"      R888" ` "Y   Y"
     `""***~"`                                           ""
                             https://cortix.org                              
_____________________________________________________________________________
[13917] 2025-12-02 13:51:58,301 - cortix - INFO - close()::Elapsed wall clock time [s]: 6.49
'''Recover stage'''
stg = system_net.modules[0]

1.6.1. Organic Phase Results#

'''Plot organic phase'''
# TODO: time axis normalized by phase flow residence time.
stg.organic_phase.plot(title='Organic Phase Steady-State', legend='Organic Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Organic phase species history dashboard at steady-state.')
../../_images/5fa345687ea619edd38707f0a7904db44e453439a0b9525a88dbb5a5a9171f19.png
Figure 6: Organic phase species history dashboard at steady-state.
'''Compute molar concentration of TBP species'''
import numpy as np

tbp_mass_cc_org_history = stg.organic_phase.get_column(tbp_org_name)
tbp_molar_cc_org_history = np.array(tbp_mass_cc_org_history)/tbp_org.molar_mass

tbp_monomer_mass_cc_org_history = stg.organic_phase.get_column(tbp_monomer_org_name)
tbp_monomer_molar_cc_org_history = np.array(tbp_monomer_mass_cc_org_history)/tbp_monomer_org.molar_mass

tbp_dimer_mass_cc_org_history = stg.organic_phase.get_column(tbp_dimer_org_name)
tbp_dimer_molar_cc_org_history = np.array(tbp_dimer_mass_cc_org_history)/tbp_dimer_org.molar_mass

tbp_trimer_hexahydrate_mass_cc_org_history = stg.organic_phase.get_column(tbp_trimer_hexahydrate_org_name)
tbp_trimer_hexahydrate_molar_cc_org_history = np.array(tbp_trimer_hexahydrate_mass_cc_org_history)/tbp_trimer_hexahydrate_org.molar_mass

time_stamps = np.array(stg.organic_phase.time_stamps)

tbl_count += 1
print(f'Table {tbl_count}: Molarity history of TBP in the organic phase at steady-state.')
print('Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for t,a,b,c,d in zip(time_stamps[n_startup::5]/unit.min, 
                 tbp_molar_cc_org_history[n_startup::5]/unit.molar, 
                 tbp_monomer_molar_cc_org_history[n_startup::5]/unit.molar,
                 tbp_dimer_molar_cc_org_history[n_startup::5]/unit.molar,
                 tbp_trimer_hexahydrate_molar_cc_org_history[n_startup::5]/unit.molar):
    balance = a+b+2*c+3*d - tbp_mass_cc_org/tbp_org.molar_mass/unit.molar
    print('  %1.2e      %1.3f         %1.3e        %1.3e               %1.3e          %+1.3e'%(t, a, b, c, d, balance))
Table 8: Molarity history of TBP in the organic phase at steady-state.
Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |
  1.33e+00      0.859         4.144e-02        7.991e-02               1.180e-02          +2.220e-16
  3.15e+00      0.856         4.280e-02        8.068e-02               1.163e-02          +0.000e+00
'''Volume fraction of TBP at start-up'''
tbl_count += 1
print(f'Table {tbl_count}: Volume fractions of free and total TBP in the organic phase at steady-state.')
print('Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
total_tbp_molar_cc_org_history = tbp_molar_cc_org_history + tbp_monomer_molar_cc_org_history + \
                                 2*tbp_dimer_molar_cc_org_history + 3*tbp_trimer_hexahydrate_molar_cc_org_history
total_tbp_mass_cc_org_history = total_tbp_molar_cc_org_history * tbp_org.molar_mass
for t,a,b in zip(time_stamps[::5]/unit.min, 
               np.array(tbp_mass_cc_org_history[::5])/rho_tbp*100, total_tbp_mass_cc_org_history[::5]/rho_tbp*100):
    print('  %1.2e         %1.3f                  %1.3f'%(t, a, b))
Table 9: Volume fractions of free and total TBP in the organic phase at steady-state.
Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |
  0.00e+00         30.000                  30.000
  3.03e-01         25.701                  30.000
  6.06e-01         24.244                  30.000
  9.09e-01         23.734                  30.000
  2.42e+00         23.455                  30.000
  4.24e+00         23.453                  30.000
'''Total water extracted'''
water_molar_cc_org_history = tbp_monomer_molar_cc_org_history \
                           + 2*tbp_dimer_molar_cc_org_history \
                           + 6*tbp_trimer_hexahydrate_molar_cc_org_history

water_mass_cc_org_history = water_molar_cc_org_history * h2o_aqu.molar_mass

tbl_count += 1
print(f'Table {tbl_count}: Mass concentration and molarity history of water in the organic phase at steady-state.')
print('Time [s] |   [M]   |  [g/L]  |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for a,b,c in zip(time_stamps[n_startup::5], water_molar_cc_org_history[n_startup::5]/unit.molar, water_mass_cc_org_history[n_startup::5]):
    print('%1.2e  %1.3e %1.3e'%(a, b, c))
Table 10: Mass concentration and molarity history of water in the organic phase at steady-state.
Time [s] |   [M]   |  [g/L]  |
8.00e+01  2.721e-01 4.901e+00
1.89e+02  2.739e-01 4.935e+00

1.6.2. Aqueous Phase Results#

'''Plot aqueous phase'''
# TODO: time axis normalized by phase flow residence time.
stg.aqueous_phase.plot(title='Aqueous Phase Steady-State', legend='Aqueous Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Aqueous phase species history dashboard at steady-state.')
../../_images/61028ff39b38cdcea556d856836f45a6b0f7a20b44deee011e5ce4eedcbed63f.png
Figure 7: Aqueous phase species history dashboard at steady-state.

1.6.3. Overall Stage Efficiency#

'''Stage overall efficiency'''

quant = stg.efficiency_history(mean=True)
quant.plot(title='Stage Efficiency @ %2.1f C Steady-State'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', show=True, figsize=[10,3], error_data=True)

fig_count += 1
print(f'Figure {fig_count}: Stage efficiency history steady-state.')
../../_images/7c5daadbd3d7e570c35a53a8678a86ea9441da269e4a1d5cd415adab4cd78a1a.png
Figure 8: Stage efficiency history steady-state.
tbl_count += 1
print(f'Table {tbl_count}: Stage efficiency history at steady-state.')
print('Time [min]  Stage. Eff.(+-std) [%]')
time_name = ''
import pandas as pd
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(['',''], axis=1).rename_axis(time_name)
       .round(3))
print(df.to_string(max_rows=20, min_rows=20))
Table 11: Stage efficiency history at steady-state.
Time [min]  Stage. Eff.(+-std) [%]
                   
                   
0.00   0.000  0.000
0.06   6.507  0.111
0.12  12.589  0.377
0.18  18.131  0.711
0.24  23.077  1.052
0.30  27.417  1.359
0.36  31.171  1.611
0.42  34.380  1.797
0.48  37.096  1.919
0.55  39.378  1.982
...      ...    ...
1.33  49.276  0.855
1.70  49.800  0.437
2.06  49.947  0.211
2.42  49.987  0.099
2.79  49.997  0.046
3.15  50.000  0.021
3.52  50.000  0.010
3.88  50.000  0.004
4.24  50.000  0.002
4.61  50.000  0.001
'''Individual reaction efficiency'''
quant = stg.efficiency_history()
tbl_count += 1
print(f'Table {tbl_count}: Reaction efficiency history at steady-state.')
print('Time [min]    Rxn Eff. [%]')

col_names = [f'r{i}' for i in range(len(stg.rxn_mech.reactions))]
time_name = ''
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(3))
print(df.to_string(max_rows=20, min_rows=20))
Table 12: Reaction efficiency history at steady-state.
Time [min]    Rxn Eff. [%]
          r0      r1      r2
                            
0.00   0.000   0.000   0.000
0.06   6.372   6.506   6.644
0.12  12.131  12.581  13.054
0.18  17.270  18.111  19.012
0.24  21.807  23.042  24.383
0.30  25.779  27.366  29.107
0.36  29.232  31.105  33.176
0.42  32.218  34.303  36.618
0.48  34.790  37.011  39.488
0.55  36.997  39.288  41.849
...      ...     ...     ...
1.33  48.246  49.241  50.339
1.70  49.274  49.784  50.343
2.06  49.692  49.940  50.210
2.42  49.867  49.984  50.110
2.79  49.942  49.996  50.054
3.15  49.974  49.999  50.026
3.52  49.989  50.000  50.012
3.88  49.995  50.000  50.005
4.24  49.998  50.000  50.002
4.61  49.999  50.000  50.001

1.6.3.1. Total water extracted in equilibrium#

From reference: 1994 Naganawa and Tachimori Anal. Sci. 10 p 607

[H2O]_o,E = 0.4265 M

Comparison to steady state:

h2o_e_molar = water_molar_cc_org_history[-1]/unit.molar

print('Total H2O extracted [M] = %1.4f'%h2o_e_molar)
print('Error (compared to measurement) [%%] = %1.2f'%((h2o_e_molar-0.4265)/0.4265*100))
Total H2O extracted [M] = 0.2739
Error (compared to measurement) [%] = -35.77

1.6.4. Reaction Rate Density#

This quantity only makes sense per volume of the mixture.

'''Reaction rate density'''
import matplotlib.pyplot as plt

quant = stg.r_vec_history()# mole/m^3-s

quant.plot(title='Reaction Rate Density @ %2.1f C Steady-State'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.reactions, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Reaction rate density history at steady-state.')
../../_images/5f1b3a6d2cce9ae629c4a1e8d27d646fbf8962594613ecf9fe3ebd1df1a8a675.png
Figure 9: Reaction rate density history at steady-state.
tbl_count += 1
print(f'Table {tbl_count}: Reaction rate density history at steady-state.')
print('Time [min]      r [mM/s]')
import pandas as pd
col_names = [f'r{i}' for i in range(len(stg.rxn_mech.reactions))]
time_name = 't [min]'
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(4))
print(df.iloc[n_startup:].to_string(max_rows=20, min_rows=20))
Table 13: Reaction rate density history at steady-state.
Time [min]      r [mM/s]
             r0      r1      r2
t [min]                        
1.33     0.4445  0.8237  0.1164
1.70     0.4348  0.8117  0.1157
2.06     0.4310  0.8082  0.1158
2.42     0.4294  0.8072  0.1160
2.79     0.4287  0.8070  0.1161
3.15     0.4284  0.8069  0.1162
3.52     0.4283  0.8069  0.1162
3.88     0.4283  0.8069  0.1162
4.24     0.4282  0.8069  0.1162
4.61     0.4282  0.8069  0.1162

1.6.5. Species Generation Rate Density#

This quantity is on the basis of mixing volume so all species generation values can be compared.

'''Species generation rate density'''
import matplotlib.pyplot as plt

quant = stg.g_vec_history() # mole/m^3-s

quant.plot(title='Species Generation Rate Density @ %2.1f C Steady-State'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.species_names, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Species generation rate density history at steady-state.')
../../_images/b61d44abb30e8db41754e8aa71dee0c9ecfa4802ea29b63c01a6bcac79369591.png
Figure 10: Species generation rate density history at steady-state.
tbl_count += 1
print(f'Table {tbl_count}: Species generation rate density history at steady-state.')
print('Time [min]        g [mM/s]')
import pandas as pd
col_names = [f'a{i}' for i in range(len(stg.rxn_mech.species_names))]
time_name = 't [min]'
df = (quant.value.apply(pd.Series).mul(1)
       .rename(index=lambda i: round(i/unit.min,2))
       .set_axis(col_names, axis=1).rename_axis(time_name)
       .round(4))
print(df.iloc[n_startup:].to_string(max_rows=20, min_rows=20))
Table 14: Species generation rate density history at steady-state.
Time [min]        g [mM/s]
             a0      a1      a2      a3      a4
t [min]                                        
1.33    -2.7903  0.4445 -2.4411  0.8237  0.1164
1.70    -2.7523  0.4348 -2.4052  0.8117  0.1157
2.06    -2.7423  0.4310 -2.3948  0.8082  0.1158
2.42    -2.7398  0.4294 -2.3918  0.8072  0.1160
2.79    -2.7392  0.4287 -2.3909  0.8070  0.1161
3.15    -2.7392  0.4284 -2.3907  0.8069  0.1162
3.52    -2.7392  0.4283 -2.3906  0.8069  0.1162
3.88    -2.7392  0.4283 -2.3906  0.8069  0.1162
4.24    -2.7392  0.4282 -2.3906  0.8069  0.1162
4.61    -2.7392  0.4282 -2.3906  0.8069  0.1162

1.6.5.1. Species generation rate density at steady-state:#

g_vec = stg.g_vec()
columns = stg.rxn_mech.species_names
print('Generation sources [mM/s]')
print(pd.DataFrame([list(g_vec)], columns=columns).T
        .rename(columns={0:'g [mM/s]'}).to_string(float_format='{:.6f}'.format))
Generation sources [mM/s]
                         g [mM/s]
H2O(a)                  -2.739244
H2O*[C4H9O]3PO(o)        0.428233
[C4H9O]3PO(o)           -2.390595
[H2O]2*[[C4H9O]3PO]2(o)  0.806856
[H2O]6*[[C4H9O]3PO]3(o)  0.116216

1.7. Shut-down Simulation#

From steady-state, shutdown in two flow residence times.

end_time += 2 * stg.flow_residence_time_avg
time_step = 2 * stg.flow_residence_time_avg / 20
show_time = (True, 10*time_step)

stg.time_step = time_step
stg.initial_time = stg.end_time
stg.end_time = end_time
stg.show_time = show_time
'''Organic phase in the inflow'''
tbp_mass_cc_org = 0.0
stg.inflow_organic_phase.set_value(tbp_org_name, tbp_mass_cc_org)
'''Run system in parallel'''
stg.monitor_mass_flowrates = False
stg.monitor_mass_conservation_residual = False
system.run()
system.close() # Shutdown Cortix
[13917] 2025-12-02 13:52:00,870 - cortix - INFO - Launching Module <solvex.stage.Stage object at 0x7f79d9bf9090>
[14007] 2025-12-02 13:52:02,142 - cortix - INFO - Stg-1::run():time[m]=4.6
[14007] 2025-12-02 13:52:02,296 - cortix - INFO - Stg-1::run():time[m]=5.6
Total mass rate density (mixture volume) residual [g/L-s]= 1.73472e-18
total mass inflow rate [g/min]   = 4.960e+02
total mass outflow rate [g/min]  =  5.196e+02
	 net total mass flow rate [g/min] = 2.364e+01
[14007] 2025-12-02 13:52:02,423 - cortix - INFO - Stg-1::run():time[m]=6.4 (et[s]=0.3)
[13917] 2025-12-02 13:52:02,636 - cortix - INFO - run()::Elapsed wall clock time [s]: 10.83
[13917] 2025-12-02 13:52:02,637 - cortix - INFO - Closed Cortix object.
_____________________________________________________________________________
                           T E R M I N A T I N G                             
_____________________________________________________________________________
      ...                                        s       .     (TAAG Fraktur)
   xH88"`~ .x8X                                 :8      @88>
 :8888   .f"8888Hf        u.      .u    .      .88      %8P      uL   ..
:8888>  X8L  ^""`   ...ue888b   .d88B :@8c    :888ooo    .     .@88b  @88R
X8888  X888h        888R Y888r ="8888f8888r -*8888888  .@88u  ""Y888k/"*P
88888  !88888.      888R I888>   4888>"88"    8888    888E`    Y888L
88888   %88888      888R I888>   4888> "      8888      888E      8888
88888 `> `8888>     888R I888>   4888>        8888      888E      `888N
`8888L %  ?888   ! u8888cJ888   .d888L .+    .8888Lu=   888E   .u./"888&
 `8888  `-*""   /   "*888*P"    ^"8888*"     ^%888*     888&  d888" Y888*"
   "888.      :"      "Y"          "Y"         "Y"      R888" ` "Y   Y"
     `""***~"`                                           ""
                             https://cortix.org                              
_____________________________________________________________________________
[13917] 2025-12-02 13:52:02,638 - cortix - INFO - close()::Elapsed wall clock time [s]: 10.83
'''Recover stage'''
stg = system_net.modules[0]

1.7.1. Organic Phase Results#

'''Plot organic phase'''
# TODO: time axis normalized by phase flow residence time.
stg.organic_phase.plot(title='Organic Phase Shut-Down', legend='Organic Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Organic phase species history dashboard at shut-down.')
../../_images/e4af7cc64bf783e93ca570544f7633672969d9bc5d6306bee3a946ab22b39f65.png
Figure 11: Organic phase species history dashboard at shut-down.
'''Compute molar concentration of TBP species'''
import numpy as np

tbp_mass_cc_org_history = stg.organic_phase.get_column(tbp_org_name)
tbp_molar_cc_org_history = np.array(tbp_mass_cc_org_history)/tbp_org.molar_mass

tbp_monomer_mass_cc_org_history = stg.organic_phase.get_column(tbp_monomer_org_name)
tbp_monomer_molar_cc_org_history = np.array(tbp_monomer_mass_cc_org_history)/tbp_monomer_org.molar_mass

tbp_dimer_mass_cc_org_history = stg.organic_phase.get_column(tbp_dimer_org_name)
tbp_dimer_molar_cc_org_history = np.array(tbp_dimer_mass_cc_org_history)/tbp_dimer_org.molar_mass

tbp_trimer_hexahydrate_mass_cc_org_history = stg.organic_phase.get_column(tbp_trimer_hexahydrate_org_name)
tbp_trimer_hexahydrate_molar_cc_org_history = np.array(tbp_trimer_hexahydrate_mass_cc_org_history)/tbp_trimer_hexahydrate_org.molar_mass

time_stamps = np.array(stg.organic_phase.time_stamps)

tbl_count += 1
print(f'Table {tbl_count}: Molarity history of TBP in the organic phase at shut-down.')
print('Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for t,a,b,c,d in zip(time_stamps[n_startup::5]/unit.min, 
                 tbp_molar_cc_org_history[n_startup::5]/unit.molar, 
                 tbp_monomer_molar_cc_org_history[n_startup::5]/unit.molar,
                 tbp_dimer_molar_cc_org_history[n_startup::5]/unit.molar,
                 tbp_trimer_hexahydrate_molar_cc_org_history[n_startup::5]/unit.molar):
    balance = a+b+2*c+3*d - tbp_mass_cc_org/tbp_org.molar_mass/unit.molar
    print('  %1.2e      %1.3f         %1.3e        %1.3e               %1.3e          %+1.3e'%(t, a, b, c, d, balance))
Table 15: Molarity history of TBP in the organic phase at shut-down.
Time [min] | Free TBP [M] | TBP Monomer [M] | TBP Dimer [M] | TBP Trimer Hexahydrate [M] | balance |
  1.33e+00      0.859         4.144e-02        7.991e-02               1.180e-02          +1.096e+00
  3.15e+00      0.856         4.280e-02        8.068e-02               1.163e-02          +1.096e+00
  4.70e+00      0.754         4.251e-02        8.015e-02               1.142e-02          +9.913e-01
  5.15e+00      0.420         3.526e-02        6.210e-02               7.264e-03          +6.012e-01
  5.61e+00      0.253         2.577e-02        3.783e-02               3.492e-03          +3.647e-01
  6.06e+00      0.158         1.787e-02        2.049e-02               1.506e-03          +2.212e-01
'''Volume fraction of TBP at start-up'''
tbl_count += 1
print(f'Table {tbl_count}: Volume fractions of free and total TBP in the organic phase at start-up.')
print('Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
total_tbp_molar_cc_org_history = tbp_molar_cc_org_history + tbp_monomer_molar_cc_org_history + \
                                 2*tbp_dimer_molar_cc_org_history + 3*tbp_trimer_hexahydrate_molar_cc_org_history
total_tbp_mass_cc_org_history = total_tbp_molar_cc_org_history * tbp_org.molar_mass
for t,a,b in zip(time_stamps[::5]/unit.min, 
               np.array(tbp_mass_cc_org_history[::5])/rho_tbp*100, total_tbp_mass_cc_org_history[::5]/rho_tbp*100):
    print('  %1.2e         %1.3f                  %1.3f'%(t, a, b))
Table 16: Volume fractions of free and total TBP in the organic phase at start-up.
Time [min] | Free TBP vol frac [%] | Total TBP vol frac [%] |
  0.00e+00         30.000                  30.000
  3.03e-01         25.701                  30.000
  6.06e-01         24.244                  30.000
  9.09e-01         23.734                  30.000
  2.42e+00         23.455                  30.000
  4.24e+00         23.453                  30.000
  4.97e+00         14.373                  20.110
  5.42e+00         8.425                  12.197
  5.88e+00         5.206                  7.398
  6.33e+00         3.277                  4.487
'''Total water extracted'''
water_molar_cc_org_history = tbp_monomer_molar_cc_org_history \
                           + 2*tbp_dimer_molar_cc_org_history \
                           + 6*tbp_trimer_hexahydrate_molar_cc_org_history

water_mass_cc_org_history = water_molar_cc_org_history * h2o_aqu.molar_mass

tbl_count += 1
print(f'Table {tbl_count}: Mass concentration and molarity history of water in the organic phase at shut-down.')
print('Time [s] |   [M]   |  [g/L]  |')
np.set_printoptions(precision=3, suppress=True, linewidth=100)
for a,b,c in zip(time_stamps[n_startup::5], water_molar_cc_org_history[n_startup::5]/unit.molar, water_mass_cc_org_history[n_startup::5]):
    print('%1.2e  %1.3e %1.3e'%(a, b, c))
Table 17: Mass concentration and molarity history of water in the organic phase at shut-down.
Time [s] |   [M]   |  [g/L]  |
8.00e+01  2.721e-01 4.901e+00
1.89e+02  2.739e-01 4.935e+00
2.82e+02  2.713e-01 4.888e+00
3.09e+02  2.030e-01 3.658e+00
3.36e+02  1.224e-01 2.205e+00
3.64e+02  6.788e-02 1.223e+00

1.7.2. Aqueous Phase Results#

'''Plot aqueous phase'''
# TODO: time axis normalized by phase flow residence time.
stg.aqueous_phase.plot(title='Aqueous Phase Shut-Down', legend='Aqueous Phase', nrows=2,ncols=3, show=True, figsize=[12,6])

fig_count += 1
print(f'Figure {fig_count}: Aqueous phase species history dashboard at shut-down.')
../../_images/d5600704b8f0d78da5d54981bc23e13d9b9c9273d6280fb21c07037b7d36ffa9.png
Figure 12: Aqueous phase species history dashboard at shut-down.

1.7.3. Reaction Rate Density#

This quantity only makes sense per volume of the mixture.

'''Reaction rate density'''
import matplotlib.pyplot as plt

quant = stg.r_vec_history()
quant.plot(title='Reaction Rate Density @ %2.1f C Shut-Down'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.reactions, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Reaction rate density history at shut-down.')
../../_images/9614104339116fd7b16b39adeb2ca528bec9fa553b3c52b8a8b3f5e29438df41.png
Figure 13: Reaction rate density history at shut-down.

1.7.4. Species Generation Rate Density#

This quantity is on the basis of mixing volume so all species generation values can be compared.

'''Species generation rate density'''
import matplotlib.pyplot as plt

quant = stg.g_vec_history()
quant.plot(title='Species Generation Rate Density @ %2.1f C Shut-Down'%unit.convert_temperature(stg_temperature,
           'K','C'), x_scaling=1/stg.flow_residence_time_avg, x_label=r'Time [$\bar{\tau}$]', y_label=quant.latex_name+
           ' ['+quant.unit+']', legend=stg.rxn_mech.species_names, show=True, figsize=[10,3], error_data=False)

fig_count += 1
print(f'Figure {fig_count}: Species generation rate density history at shut-down.')
../../_images/11d91d080d47ea458e1864347985e6bd25c9ba92573ca5c9125a47501ecea03d.png
Figure 14: Species generation rate density history at shut-down.

1.8. References#

  • [1] V. F. de Almeida, Cortix, Network Dynamics Simulation, Cortix Tech, Lowell, MA.