Skip to content

Commit 72dd1c0

Browse files
committed
Simplify ODE construction
1 parent c84cfe7 commit 72dd1c0

File tree

1 file changed

+56
-106
lines changed

1 file changed

+56
-106
lines changed

micki/model.py

+56-106
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,15 @@ def set_initial_conditions(self, U0):
563563
newspecies.append(species)
564564
self._species = newspecies
565565

566+
# Also obtain a list of species that will be variables in the
567+
# differential equations. This excludes fixed species and empty
568+
# sites.
569+
self._variable_species = []
570+
for species in self._species:
571+
if species.label not in self.fixed + [self.solvent]:
572+
self._variable_species.append(species)
573+
self.nvariables = len(self._variable_species)
574+
566575
# Start with the incomplete user-provided initial conditions
567576
self.U0 = U0.copy()
568577

@@ -614,11 +623,6 @@ def set_initial_conditions(self, U0):
614623
# Normalize the concentration of empty sites to match the
615624
# appropriate site ratio from the lattice.
616625
self.U0[name] = self.vactot[species] - occsites[species]
617-
# if species not in U0:
618-
# self.vactot[species] = 1.
619-
# U0[species] = 1. - occsites[species]
620-
# else:
621-
# self.vactot[species] = U0[species] + occsites[species]
622626

623627
# Populate dictionary of initial conditions for all species
624628
for name, species in self.species.items():
@@ -629,10 +633,6 @@ def set_initial_conditions(self, U0):
629633
# The number of variables that will be in our differential equations
630634
size = len(self._species)
631635

632-
# Initialize "mass matrix", which is a diagonal matrix with 1s for
633-
# differential elements and 0s for algebraic elements (steady-state)
634-
M = np.ones(size, dtype=int)
635-
636636
# This creates a symbol for each species named modelparamX where X
637637
# is a three-digit numerical identifier that corresponds to its
638638
# position in the order of species
@@ -646,8 +646,8 @@ def set_initial_conditions(self, U0):
646646
for species in self._species:
647647
self.symbols_all.append(species.symbol)
648648
self.symbols_dict[species] = species.symbol
649-
if species.label not in self.fixed and species.label != self.solvent:
650-
self.symbols.append(species.symbol)
649+
650+
self.symbols = [species.symbol for species in self._variable_species]
651651

652652
# subs converts a species symbol to either its initial value if
653653
# it is fixed or to a constraint (such as constraining the total
@@ -662,9 +662,6 @@ def set_initial_conditions(self, U0):
662662
vacsymbols -= species.symbol
663663
subs[vacancy.symbol] = vacsymbols
664664

665-
# nsymbols is the size of the differential equations
666-
self.nsymbols = len(self.symbols)
667-
668665
# known_symbols keeps track of user-provided symbols that the
669666
# model has seen, so that symbols referring to species not in
670667
# the model can be later removed.
@@ -673,30 +670,43 @@ def set_initial_conditions(self, U0):
673670
known_symbols.add(species.symbol)
674671

675672
# Create the final mass matrix of the proper dimensions
676-
self.M = np.zeros((self.nsymbols, self.nsymbols), dtype=int)
673+
self.M = np.eye(self.nvariables, dtype=int)
677674
# algvar tells the solver which variables are differential
678675
# and which are algebraic. It is the diagonal of the mass matrix.
679-
algvar = np.zeros(self.nsymbols, dtype=float)
680-
for i, symboli in enumerate(self.symbols_all):
681-
for j, symbolj in enumerate(self.symbols):
682-
if symboli == symbolj:
683-
self.M[j, j] = M[i]
684-
algvar[j] = M[i]
676+
algvar = np.ones(self.nvariables, dtype=float)
677+
678+
# TODO: Make a way to set algebraic variables.
685679

686680
# Initialize all rate expressions based on the above symbols
687-
self._rate_init()
688-
# f_sym is the SYMBOLIC master equation for all species
689-
self.f_sym = []
681+
nrxns = len(self._reactions)
682+
# Array of symbolic rate expressions
683+
self.rates = np.zeros(nrxns, dtype=object)
684+
# Array of rate coefficients.
685+
self.dfdr = np.zeros((self.nvariables, nrxns), dtype=int)
686+
687+
for j, rxn in enumerate(self._reactions):
688+
rate_for = rxn.get_kfor(self.T, self.Asite, self.z)
689+
rate_rev = rxn.get_krev(self.T, self.Asite, self.z)
690+
691+
for i, species in enumerate(self._variable_species):
692+
rcount = rxn.reactants.species.count(species)
693+
pcount = rxn.products.species.count(species)
694+
self.dfdr[i, j] = -rcount + pcount
695+
696+
for species in self._species + self.vacancy:
697+
rcount = rxn.reactants.species.count(species)
698+
pcount = rxn.products.species.count(species)
699+
if not isinstance(species, Electron):
700+
rate_for *= species.symbol**rcount
701+
rate_rev *= species.symbol**pcount
690702

691-
# TODO: Convert this to a matrix multiplication
692-
for species in self._species:
693-
f = 0
694-
# DETAILED BALANCE NOTE: See discussion in _rate_init
695-
if species.label not in self.fixed:
696-
if species not in self.vacancy and species.label != self.solvent:
697-
for i, rate in enumerate(self.rates):
698-
f += self.rate_count[i][species] * rate
699-
self.f_sym.append(f)
703+
# Overall reaction rate (flux)
704+
self.rates[j] = rate_for
705+
if rxn.reversible:
706+
self.rates[j] -= rate_rev
707+
708+
# f_sym is the SYMBOLIC master equation for all species
709+
self.f_sym = np.dot(self.dfdr, self.rates)
700710

701711
# subs is a dictionary whose keys are internal species symbols and
702712
# whose values are the initial concentrations of that species if it
@@ -725,7 +735,7 @@ def set_initial_conditions(self, U0):
725735

726736
# jac_sym is the SYMBOLIC Jacobian matrix, that is df/dc, where f is a
727737
# row of the master equation and c is a species.
728-
self.jac_sym = np.zeros((self.nsymbols, self.nsymbols), dtype=object)
738+
self.jac_sym = np.zeros((self.nvariables, self.nvariables), dtype=object)
729739
for i, f in enumerate(self.f_sym):
730740
for j, symbol in enumerate(self.symbols):
731741
self.jac_sym[i, j] = sym.diff(f, symbol)
@@ -748,70 +758,10 @@ def set_initial_conditions(self, U0):
748758
break
749759

750760
# Pass initial values to the fortran module
751-
self.finitialize(U0, 1e-10, [1e-10]*self.nsymbols, [], [], algvar)
761+
self.finitialize(U0, 1e-10, [1e-10]*self.nvariables, [], [], algvar)
752762

753763
self.initialized = True
754764

755-
def _rate_init(self):
756-
# List of symbolic rate expressions
757-
self.rates = []
758-
# List of dicts. For a given reaction, the change in concentration
759-
# of a species for "one unit" of reaction occuring in the forward
760-
# direction
761-
self.rate_count = []
762-
# Keeps track of adsorption reactions
763-
self.is_rate_ads = []
764-
765-
for reaction in self._reactions:
766-
# Calculate kfor, krev, and keq
767-
rate_for = reaction.get_kfor(self.T, self.Asite, self.z)
768-
rate_rev = reaction.get_krev(self.T, self.Asite, self.z)
769-
770-
# IMPORTANT NOTE PERTAINING TO ABOVE:
771-
# As far as the user can tell, all fluids (Gas, Liquid) are
772-
# represented in units of concentration (i.e. mol/L) and all
773-
# adsorbates are represented in coverage (i.e. N/M, where M is the
774-
# total number of sites). INTERNALLY, this is not the case.
775-
# Internally, ALL species are number fractions relative to the
776-
# default number of catalytic sites (though this can be changed).
777-
# This means that in order to achieve detailed balance, the
778-
# reaction rates must be modulated by the volume. Further sections
779-
# of the code that pertain to this behavior will be highlighted
780-
# with "DETAILED BALANCE NOTE"
781-
782-
# Initialize dictionary for reaction stiochiometry to 0 for all
783-
# species in the model
784-
rate_count = {}
785-
for species in self._species:
786-
rate_count[species] = 0
787-
for vacancy in self.vacancy:
788-
rate_count[vacancy] = 0
789-
790-
# Reactants are consumed in the forward direction
791-
for species in reaction.reactants:
792-
# Multiply the rate by the species' symbol UNLESS it is an
793-
# electron. All reactions are 0th order in electrons, even
794-
# if they consume/produce electrons.
795-
if not isinstance(species, Electron):
796-
rate_for *= species.symbol
797-
rate_count[species] -= 1
798-
799-
# Products are created in the forward direction
800-
for species in reaction.products:
801-
if not isinstance(species, Electron):
802-
rate_rev *= species.symbol
803-
rate_count[species] += 1
804-
805-
# Overall reaction rate (flux)
806-
rate = rate_for
807-
if reaction.reversible:
808-
rate -= rate_rev
809-
810-
# Append all data to global rate lists
811-
self.rates.append(rate)
812-
self.rate_count.append(rate_count)
813-
self.is_rate_ads.append(reaction.adsorption)
814-
815765
def setup_execs(self):
816766
from micki.fortran import f90_template, pyf_template
817767
from numpy import f2py
@@ -820,13 +770,13 @@ def setup_execs(self):
820770
# concentrations provided by the differential equation solver inside
821771
# the Fortran code (that is, y_vec is an INPUT to the functions that
822772
# calculate the residual, Jacobian, and rate)
823-
y_vec = sym.IndexedBase('yin', shape=(self.nsymbols,))
773+
y_vec = sym.IndexedBase('yin', shape=(self.nvariables,))
824774
# Map y_vec elements (1-indexed, of course) onto 'modelparam' symbols
825-
trans = {self.symbols[i]: y_vec[i + 1] for i in range(self.nsymbols)}
775+
trans = {self.symbols[i]: y_vec[i + 1] for i in range(self.nvariables)}
826776
# Map string represntation of 'modelparam' symbols onto string
827777
# representation of y-vec elements
828778
str_trans = {}
829-
for i in range(self.nsymbols):
779+
for i in range(self.nvariables):
830780
str_trans[sym.fcode(self.symbols[i], source_format='free')] = \
831781
sym.fcode(y_vec[i + 1], source_format='free')
832782

@@ -841,7 +791,7 @@ def setup_execs(self):
841791
ratecode = []
842792

843793
# Convert symbolic master equation into a valid Fortran string
844-
for i in range(self.nsymbols):
794+
for i in range(self.nvariables):
845795
fcode = sym.fcode(self.f_sym[i], source_format='free')
846796
# Replace modelparam symbols with their y_vec counterpart
847797
for key in str_list:
@@ -851,8 +801,8 @@ def setup_execs(self):
851801

852802
# Effectively the same as above, except on the two-dimensional Jacobian
853803
# matrix.
854-
for i in range(self.nsymbols):
855-
for j in range(self.nsymbols):
804+
for i in range(self.nvariables):
805+
for j in range(self.nvariables):
856806
expr = self.jac_sym[j, i]
857807
# Unlike the residual, some elements of the Jacobian can be 0.
858808
# We don't need to bother writing 'jac(x,y) = 0' a hundred
@@ -874,7 +824,7 @@ def setup_execs(self):
874824
# We insert all of the parameters of this differential equation into
875825
# the prewritten Fortran template, including the residual, Jacobian,
876826
# and rate expressions we just calculated.
877-
program = f90_template.format(neq=self.nsymbols, nx=1,
827+
program = f90_template.format(neq=self.nvariables, nx=1,
878828
nrates=len(self.rates),
879829
rescalc='\n'.join(rescode),
880830
jaccalc='\n'.join(jaccode),
@@ -893,7 +843,7 @@ def setup_execs(self):
893843

894844
# Write the pertinent data into the temp directory
895845
with open(os.path.join(dname, pyfname), 'w') as f:
896-
f.write(pyf_template.format(modname=modname, neq=self.nsymbols,
846+
f.write(pyf_template.format(modname=modname, neq=self.nvariables,
897847
nrates=len(self.rates)))
898848

899849
# Compile the module with f2py
@@ -956,7 +906,7 @@ def _out_array_to_dict(self, U, dU, r):
956906
return Ui, dUi, ri
957907

958908
def find_steady_state(self, dt=60, maxiter=2000, epsilon=1e-8):
959-
t, U1, dU1, r1 = self.ffind_steady_state(self.nsymbols,
909+
t, U1, dU1, r1 = self.ffind_steady_state(self.nvariables,
960910
len(self.rates),
961911
dt,
962912
maxiter,
@@ -973,7 +923,7 @@ def find_steady_state(self, dt=60, maxiter=2000, epsilon=1e-8):
973923
return t, U, r
974924

975925
def solve(self, t, ncp):
976-
self.t, U1, dU1, r1 = self.fsolve(self.nsymbols,
926+
self.t, U1, dU1, r1 = self.fsolve(self.nvariables,
977927
len(self.rates), ncp, t)
978928
self.U1 = U1.T
979929
self.dU1 = dU1.T
@@ -1017,7 +967,7 @@ def check_rates(self, U, epsilon=1e-6):
1017967
RuntimeWarning, stacklevel=2)
1018968

1019969
def copy(self, initialize=True):
1020-
newmodel = Model(self.T, self.Asite, self.z, self.nz, self.shape, self.lattice)
970+
newmodel = Model(self.T, self.Asite, self.z, self.lattice)
1021971
newmodel.add_reactions(self.reactions)
1022972
newmodel.set_fixed(self.fixed)
1023973
newmodel.set_solvent(self.solvent)

0 commit comments

Comments
 (0)