Introduction to the crunchflow.input package
The crunchflow.input package is designed to efficiently open, edit and save text files used to run the CrunchFlow reactive transport code. While it is possible (and often simpler) to perform CrunchFlow simulations by opening input files in a text editor and editing them manually, this workflow can be too time-consuming for tasks that require many simulations, such as sensitivity analyses. Generating several hundred (or several thousand) input files by hand simply isn’t practical. This
package provides classes for creating and editing these files programmatically.
1. Creating an InputFile object
The InputFile class creates objects that represent CrunchFlow input files. These objects have methods for adding, removing, and modifying sections and parameters, as well as saving them to a file.
1a. Keyword Blocks
Each InputFile object contains keyword blocks (KeywordBlock objects) that correspond to individual blocks within a CrunchFlow input file. All the standard blocks that CrunchFlow recognizes (PRIMARY_SPECIES, RUNTIME, FLOW, etc) are available as classes in the crunchflow.input.blocks module. To adhere to Python naming conventions, KeywordBlock objects are always defined in CamelCase (i.e., PrimarySpecies, Runtime, Flow, etc).
[1]:
# Import the Runtime block
from crunchflow.input.blocks import Runtime
# Create an instance of Runtime block
runtime_block = Runtime()
# It's possible to create blocks by defining individual attributes
runtime_block.time_units = "years"
runtime_block.timestep_max = 0.01
runtime_block.time_tolerance = 0.001
runtime_block.gimrt = True
# Or using the set_parameters method, which takes a dictionary
# as input, where the keys are the attribute names and the values
# are the attribute values
runtime_block.set_parameters({"time_units": "years", "timestep_max": 0.01, "time_tolerance": 0.001, "gimrt": True})
print(runtime_block)
gimrt true
timestep_max 0.01
time_tolerance 0.001
time_units years
1b. Adding Blocks to an InputFile
In Python, objects have attributes. For an InputFile these attributes are keyword blocks. While KeywordBlock objects are defined in CamelCase (e.g., PrimarySpecies, Runtime, Flow, etc), the corresponding InputFile attributes are all lower case, using an underscore to separate words (i.e., InputFile.primary_species, . InputFile.runtime, InputFile.flow, etc).
To add a block to an InputFile, simply set the attribute to the block you want to add. For example, to add a Runtime block to an InputFile:
[2]:
# Import the InputFile class
from crunchflow.input import InputFile
# Create an instance of the InputFile class
my_simulation = InputFile()
# Set the runtime attribute for this input file
my_simulation.runtime = runtime_block
# Printing the InputFile shows all defined blocks for that
# individual simulation
print(my_simulation)
RUNTIME
gimrt true
timestep_max 0.01
time_tolerance 0.001
time_units years
END
2. Loading an InputFile object from file
While it’s possible to define an InputFile object from scratch, it’s often more convenient to load an existing input file and modify it. This can be done using the InputFile.load method. As an example, let’s load a sample input file from the CrunchFlow short course:
[3]:
# Load an existing input file
my_simulation = InputFile.load("surface_complexation.in", path="input_files")
# If we print this object, we can see that it contains all the blocks
# and parameters from the input file
print(my_simulation)
TITLE
Problem 3: multi-component surface complexation
END
RUNTIME
correction_max 2.0
database datacom.dbs
database_sweep false
debye-huckel true
gimrt true
screen_output 100
speciate_only false
timestep_max .1
timestep_init 1.0E-14
time_tolerance 0.005
time_units years
END
OUTPUT
spatial_profile 1 4
time_series_print H+ Tracer SiO2(aq) Na+ Ca++ CO2(aq) pH UO2++ Zn++ Pb++ Hg++
time_series totconhistory1.txt 100 1 1
time_series totconhistory2.txt 300 1 1
END
DISCRETIZATION
xzones 400 0.25
END
FLOW
calculate_flow true
distance_units meters
time_units years
pressure 300000 default
pressure 300000 zone 0-0 1-1 1-1 fix
pressure 0 zone 401-401 1-1 1-1 fix
permeability_x 1.0E-13 default
END
TRANSPORT
cementation_exponent 1.0
dispersivity 10
distance_units centimeters
fix_diffusion 0.919E-05
formation_factor 1.0
time_units seconds
END
PRIMARY_SPECIES
H+
CO2(aq)
Mg++
Ca++
Na+
Fe+++
SiO2(aq)
Cl-
UO2++
Zn++
Pb++
Hg++
Tracer
END
SECONDARY_SPECIES
(UO2)2(OH)2++
(UO2)2OH+++
(UO2)3(OH)4++
(UO2)3(OH)5+
(UO2)3(OH)7-
(UO2)4(OH)7+
HCO3-
CaCl+
CaCl2(aq)
CaOH+
Fe(OH)2+
Fe(OH)3(aq)
Fe(OH)4-
Fe2(OH)2++++
Fe3(OH)4(5+)
FeCl++
FeCl2+
FeCl4-
FeOH++
H2SiO4--
H4(H2SiO4)4----
H6(H2SiO4)4--
HCl(aq)
HSiO3-
Mg4(OH)4++++
MgCl+
NaCl(aq)
NaHSiO3(aq)
NaOH(aq)
OH-
Pb(OH)2(aq)
Pb(OH)3-
Pb2OH+++
Pb3(OH)4++
Pb4(OH)4++++
Pb6(OH)8++++
PbCl+
PbCl2(aq)
PbCl3-
PbCl4--
PbOH+
UO2(OH)2(aq)
UO2(OH)3-
UO2(OH)4--
UO2Cl+
UO2Cl2(aq)
UO2OH+
Zn(OH)2(aq)
Zn(OH)3-
Zn(OH)4--
Zn(OH)Cl(aq)
ZnCl+
ZnCl2(aq)
ZnCl3-
ZnCl4--
ZnOH+
(UO2)11(CO3)6(OH)12--
(UO2)2CO3(OH)3-
(UO2)3(CO3)6(6-)
(UO2)3(OH)5CO2+
(UO2)3O(OH)2(HCO3)+
CO3--
CaCO3(aq)
CaHCO3+
FeCO3+
MgCO3(aq)
MgHCO3+
NaCO3-
NaHCO3(aq)
Pb(CO3)2--
PbCO3(aq)
UO2(CO3)2--
UO2(CO3)3----
UO2CO3(aq)
ZnCO3(aq)
ZnHCO3+
END
MINERALS
Fe(OH)3 -rate -55 -!set to -be non-reactive
Quartz -rate -55 -!set to -be non-reactive
END
GASES
CO2(g)
END
INITIAL_CONDITIONS
Groundwater 1-400
END
BOUNDARY_CONDITIONS
x_begin Minewater flux
x_end Groundwater flux
y_begin Groundwater flux
y_end Groundwater flux
END
SURFACE_COMPLEXATION
>FeOH_strong on Fe(OH)3
END
CONDITION Minewater
temperature 25
pH 8.5
CO2(aq) CO2(g) 0.001
Mg++ 1E-4
Ca++ 9E-5
Na+ charge
Fe+++ 1E-19
SiO2(aq) 1E-7
Cl- 1E-4
Tracer 1E-4
UO2++ 1E-4
Zn++ 1E-4
Pb++ 1E-4
Hg++ 1E-4
Fe(OH)3 1E-20 ssa 1e-10
>FeOH_strong 1E-20
END
CONDITION Groundwater
temperature 25
pH 7.5
CO2(aq) CO2(g) 0.0001
Mg++ 1E-4
Ca++ 9E-5
Na+ 1E-4
Fe+++ 1E-19
SiO2(aq) 1E-7
Cl- charge
Tracer 1E-30
UO2++ 1E-9 equilibrate_surface
Zn++ 1E-9 equilibrate_surface
Pb++ 1E-9 equilibrate_surface
Hg++ 1E-9 equilibrate_surface
Quartz 0.700 ssa 1
Fe(OH)3 0.0005 ssa 600.0 surface
>FeOH_strong 9.259259E-08
END
TEMPERATURE
set_temperature 25
END
[4]:
# Now the attributes of the InputFile object are the blocks
# from the input file. We can access these blocks using dot notation
runtime_block = my_simulation.runtime
print(runtime_block)
correction_max 2.0
database datacom.dbs
database_sweep false
debye-huckel true
gimrt true
screen_output 100
speciate_only false
timestep_max .1
timestep_init 1.0E-14
time_tolerance 0.005
time_units years
[5]:
# We can also print individual attributes of a block
print(runtime_block.time_units)
# We can also string these together to access nested attributes
print(my_simulation.runtime.time_units)
years
years
2a. Conditions blocks
Some blocks are a little more complicated than others. For example, an individual CrunchFlow input file can have multiple Conditions blocks. To accommodate this, the InputFile.conditions attribute is a dictionary of multiple KeywordBlock objects, where the keys are the condition names. For example:
[6]:
# Print which conditions are available in this input file
print(my_simulation.conditions)
{'Minewater': Minewater, 'Groundwater': Groundwater}
[7]:
# Print the 'Minewater' condition using dictionary notation
print(my_simulation.conditions["Minewater"])
CONDITION Minewater
temperature 25
pH 8.5
CO2(aq) CO2(g) 0.001
Mg++ 1E-4
Ca++ 9E-5
Na+ charge
Fe+++ 1E-19
SiO2(aq) 1E-7
Cl- 1E-4
Tracer 1E-4
UO2++ 1E-4
Zn++ 1E-4
Pb++ 1E-4
Hg++ 1E-4
Fe(OH)3 1E-20 ssa 1e-10
>FeOH_strong 1E-20
Within a Conditions block, there are some conventional attributes (such as temperature, set_porosity, etc.). The concentrations of individual species are stored in a dictionary called concentrations.
[8]:
# Print the concentrations of each species within the `Minewater` condition
# The nested dictionary notation here is a little verbose, but
# it shows that the attributes of an object can have attributes themselves
for species, concentration in my_simulation.conditions["Minewater"].concentrations.items():
print(f"{species}: {concentration}")
pH: 8.5
CO2(aq): CO2(g) 0.001
Mg++: 1E-4
Ca++: 9E-5
Na+: charge
Fe+++: 1E-19
SiO2(aq): 1E-7
Cl-: 1E-4
Tracer: 1E-4
UO2++: 1E-4
Zn++: 1E-4
Pb++: 1E-4
Hg++: 1E-4
Fe(OH)3: 1E-20 ssa 1e-10
>FeOH_strong: 1E-20
2b. The KineticsBlock sub-class
Another complicated type of block are those that include information on reaction kinetics (MINERALS and AQUEOUS_KINETICS). These are all read into a KineticsBlock sub-class that consists of nested dictionaries. Within the outer dictionary, the key is the species and within the inner dictionary, the key is the reaction label. For example, a MINERALS block can be listed as follows:
MINERALS
Calcite
Barite -label default -rate -4.5 -activation 1.0
Barite -label h+ -rate -2.5 -activation 0.0
END
So, the reaction dictionary for “Barite” would be {'default': {'rate': -4.5, 'activation': 1.0}, 'h+': {'rate': -2.5, 'activation': 0.0}}.
[9]:
# Let's test the above functionality with an example input file
my_simulation = InputFile.load("minerals_example.in", path="input_files")
print(my_simulation.minerals)
Calcite
Barite -rate -4.5 -activation 1.0
Barite -label h+ -rate -2.5 -activation 0.0
[10]:
# If we print the reaction dictionary for Barite, it should be what we showed above
print(my_simulation.minerals.species_dict["Barite"])
{'default': {'rate': '-4.5', 'activation': '1.0'}, 'h+': {'rate': '-2.5', 'activation': '0.0'}}
[11]:
# And for calcite, it's a lot shorter
# Note that if no reaction label is provided, the label is assumed to be 'default'
print(my_simulation.minerals.species_dict["Calcite"])
{'default': {}}
2c. The other attribute
Sometimes, the load method might read in an attribute that is not recognized by the crunchflow package. In this case, load store this information in the other attribute of the block. It will still be written to file and can be modified, but a warning will be issued. For example:
[12]:
# Load an input file
# If you open the file, you'll see that the RUNTIME block
# contains an attribute called 'new_crunch_feature'
my_simulation = InputFile.load("surface_complexation_modified.in", path="input_files")
Warning: Unrecognized attribute 'new_crunch_feature' in block 'runtime'
[13]:
# Despite the warning, new_crunch_feature is still printed in
# the block and can be modified
print(my_simulation.runtime)
correction_max 2.0
database datacom.dbs
database_sweep false
debye-huckel true
gimrt true
screen_output 100
speciate_only false
timestep_max .1
timestep_init 1.0E-14
time_tolerance 0.005
time_units years
new_crunch_feature true
3. Loading an InputFile, modifying it and saving it to file
We can combine all these various methods to load an input file, modify it, and save it to a new file. For example, let’s load the surface complexation example, change the pH of the influent (“Minewater”), and save it to a new file:
[14]:
# Load an existing input file
my_simulation = InputFile.load("surface_complexation.in", path="input_files")
# To limit too many nested dictionaries/attributes, assign
# the minewater condition to a variable and print it
minewater_cond = my_simulation.conditions["Minewater"]
print(minewater_cond)
CONDITION Minewater
temperature 25
pH 8.5
CO2(aq) CO2(g) 0.001
Mg++ 1E-4
Ca++ 9E-5
Na+ charge
Fe+++ 1E-19
SiO2(aq) 1E-7
Cl- 1E-4
Tracer 1E-4
UO2++ 1E-4
Zn++ 1E-4
Pb++ 1E-4
Hg++ 1E-4
Fe(OH)3 1E-20 ssa 1e-10
>FeOH_strong 1E-20
[15]:
# Now change the pH of this variable
# Because pH is stored in the `concentrations` attribute of a `Conditions`
# block, we'll have to set it using dictionary notation
minewater_cond.concentrations["pH"] = 8.0
# Now print the modified condition
# (Note that in Python, variable names are just references to
# objects. You can think of them like aliases. So when we modify
# minewater_cond, we also modify the original object. Thus,
# my_simulation.conditions['Minewater'] should reflect the new pH.)
print(my_simulation.conditions["Minewater"])
CONDITION Minewater
temperature 25
pH 8.0
CO2(aq) CO2(g) 0.001
Mg++ 1E-4
Ca++ 9E-5
Na+ charge
Fe+++ 1E-19
SiO2(aq) 1E-7
Cl- 1E-4
Tracer 1E-4
UO2++ 1E-4
Zn++ 1E-4
Pb++ 1E-4
Hg++ 1E-4
Fe(OH)3 1E-20 ssa 1e-10
>FeOH_strong 1E-20
[16]:
# Save the modified input file to a new file
my_simulation.save("surface_complexation_pH8.in", path="input_files")