@@ -563,6 +563,15 @@ def set_initial_conditions(self, U0):
563
563
newspecies .append (species )
564
564
self ._species = newspecies
565
565
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
+
566
575
# Start with the incomplete user-provided initial conditions
567
576
self .U0 = U0 .copy ()
568
577
@@ -614,11 +623,6 @@ def set_initial_conditions(self, U0):
614
623
# Normalize the concentration of empty sites to match the
615
624
# appropriate site ratio from the lattice.
616
625
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]
622
626
623
627
# Populate dictionary of initial conditions for all species
624
628
for name , species in self .species .items ():
@@ -629,10 +633,6 @@ def set_initial_conditions(self, U0):
629
633
# The number of variables that will be in our differential equations
630
634
size = len (self ._species )
631
635
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
-
636
636
# This creates a symbol for each species named modelparamX where X
637
637
# is a three-digit numerical identifier that corresponds to its
638
638
# position in the order of species
@@ -646,8 +646,8 @@ def set_initial_conditions(self, U0):
646
646
for species in self ._species :
647
647
self .symbols_all .append (species .symbol )
648
648
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 ]
651
651
652
652
# subs converts a species symbol to either its initial value if
653
653
# it is fixed or to a constraint (such as constraining the total
@@ -662,9 +662,6 @@ def set_initial_conditions(self, U0):
662
662
vacsymbols -= species .symbol
663
663
subs [vacancy .symbol ] = vacsymbols
664
664
665
- # nsymbols is the size of the differential equations
666
- self .nsymbols = len (self .symbols )
667
-
668
665
# known_symbols keeps track of user-provided symbols that the
669
666
# model has seen, so that symbols referring to species not in
670
667
# the model can be later removed.
@@ -673,30 +670,43 @@ def set_initial_conditions(self, U0):
673
670
known_symbols .add (species .symbol )
674
671
675
672
# 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 )
677
674
# algvar tells the solver which variables are differential
678
675
# 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.
685
679
686
680
# 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
690
702
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 )
700
710
701
711
# subs is a dictionary whose keys are internal species symbols and
702
712
# whose values are the initial concentrations of that species if it
@@ -725,7 +735,7 @@ def set_initial_conditions(self, U0):
725
735
726
736
# jac_sym is the SYMBOLIC Jacobian matrix, that is df/dc, where f is a
727
737
# 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 )
729
739
for i , f in enumerate (self .f_sym ):
730
740
for j , symbol in enumerate (self .symbols ):
731
741
self .jac_sym [i , j ] = sym .diff (f , symbol )
@@ -748,70 +758,10 @@ def set_initial_conditions(self, U0):
748
758
break
749
759
750
760
# 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 )
752
762
753
763
self .initialized = True
754
764
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
-
815
765
def setup_execs (self ):
816
766
from micki .fortran import f90_template , pyf_template
817
767
from numpy import f2py
@@ -820,13 +770,13 @@ def setup_execs(self):
820
770
# concentrations provided by the differential equation solver inside
821
771
# the Fortran code (that is, y_vec is an INPUT to the functions that
822
772
# calculate the residual, Jacobian, and rate)
823
- y_vec = sym .IndexedBase ('yin' , shape = (self .nsymbols ,))
773
+ y_vec = sym .IndexedBase ('yin' , shape = (self .nvariables ,))
824
774
# 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 )}
826
776
# Map string represntation of 'modelparam' symbols onto string
827
777
# representation of y-vec elements
828
778
str_trans = {}
829
- for i in range (self .nsymbols ):
779
+ for i in range (self .nvariables ):
830
780
str_trans [sym .fcode (self .symbols [i ], source_format = 'free' )] = \
831
781
sym .fcode (y_vec [i + 1 ], source_format = 'free' )
832
782
@@ -841,7 +791,7 @@ def setup_execs(self):
841
791
ratecode = []
842
792
843
793
# Convert symbolic master equation into a valid Fortran string
844
- for i in range (self .nsymbols ):
794
+ for i in range (self .nvariables ):
845
795
fcode = sym .fcode (self .f_sym [i ], source_format = 'free' )
846
796
# Replace modelparam symbols with their y_vec counterpart
847
797
for key in str_list :
@@ -851,8 +801,8 @@ def setup_execs(self):
851
801
852
802
# Effectively the same as above, except on the two-dimensional Jacobian
853
803
# 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 ):
856
806
expr = self .jac_sym [j , i ]
857
807
# Unlike the residual, some elements of the Jacobian can be 0.
858
808
# We don't need to bother writing 'jac(x,y) = 0' a hundred
@@ -874,7 +824,7 @@ def setup_execs(self):
874
824
# We insert all of the parameters of this differential equation into
875
825
# the prewritten Fortran template, including the residual, Jacobian,
876
826
# 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 ,
878
828
nrates = len (self .rates ),
879
829
rescalc = '\n ' .join (rescode ),
880
830
jaccalc = '\n ' .join (jaccode ),
@@ -893,7 +843,7 @@ def setup_execs(self):
893
843
894
844
# Write the pertinent data into the temp directory
895
845
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 ,
897
847
nrates = len (self .rates )))
898
848
899
849
# Compile the module with f2py
@@ -956,7 +906,7 @@ def _out_array_to_dict(self, U, dU, r):
956
906
return Ui , dUi , ri
957
907
958
908
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 ,
960
910
len (self .rates ),
961
911
dt ,
962
912
maxiter ,
@@ -973,7 +923,7 @@ def find_steady_state(self, dt=60, maxiter=2000, epsilon=1e-8):
973
923
return t , U , r
974
924
975
925
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 ,
977
927
len (self .rates ), ncp , t )
978
928
self .U1 = U1 .T
979
929
self .dU1 = dU1 .T
@@ -1017,7 +967,7 @@ def check_rates(self, U, epsilon=1e-6):
1017
967
RuntimeWarning , stacklevel = 2 )
1018
968
1019
969
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 )
1021
971
newmodel .add_reactions (self .reactions )
1022
972
newmodel .set_fixed (self .fixed )
1023
973
newmodel .set_solvent (self .solvent )
0 commit comments