def __init__(self, tend, dt, tstart=0): """ init function initialize the lab class Arguments: tend {float} -- end time of computation dt {float} -- timestep Keyword Arguments: tstart {float} -- start time of computation (default: {0}) """ self.tend = tend self.dt = dt self.time = np.linspace(tstart, tend, round(tend / dt) + 1) self.species = DotDict({}) self.dynamic_functions = DotDict({}) self.profiles = DotDict({}) self.dcdt = DotDict({}) self.rates = DotDict({}) self.estimated_rates = DotDict({}) self.constants = DotDict({}) self.functions = DotDict({}) self.henry_law_equations = [] self.acid_base_components = [] self.acid_base_system = phcalc.System() self.ode_method = 'scipy'
def add_species(self, theta, element, D, init_C, bc_top, bc_top_type, bc_bot, bc_bot_type, w=False, int_transport=True): self.species[element] = DotDict({}) self.species[element]['bc_top_value'] = bc_top self.species[element]['bc_top_type'] = bc_top_type.lower() self.species[element]['bc_bot_value'] = bc_bot self.species[element]['bc_bot_type'] = bc_bot_type.lower() self.species[element]['theta'] = np.ones((self.N)) * theta self.species[element]['D'] = D self.species[element]['init_C'] = init_C self.species[element]['concentration'] = np.zeros( (self.N, self.time.size)) self.species[element]['rates'] = np.zeros((self.N, self.time.size)) self.species[element]['concentration'][:, 0] = self.species[element][ 'init_C'] self.profiles[element] = self.species[element]['concentration'][:, 0] if w: self.species[element]['w'] = w else: self.species[element]['w'] = self.w self.species[element]['int_transport'] = int_transport if int_transport: self.template_AL_AR(element) self.update_matrices_due_to_bc(element, 0) self.dcdt[element] = '0'
def add_parameter(self, name, lower_boundary, upper_boundary): """ add parameter to calibrate Arguments: name {str} -- name of the parameter matching name in model lb {float} -- lower boundary ub {float} -- upper boundary """ self.parameters[name] = DotDict({}) self.parameters[name]['lower_boundary'] = lower_boundary self.parameters[name]['upper_boundary'] = upper_boundary self.parameters[name]['value'] = self.lab.constants[name]
def add_species(self, theta, name, D, init_conc, bc_top_value, bc_top_type, bc_bot_value, bc_bot_type, w=False, int_transport=True): """add chemical compund to the column model with boundary conditions Arguments: theta {numpy.array} -- porosity or 1 minus porosity name {str} -- name of the element D {float} -- total diffusion init_conc {float or numpy.array} -- initial concentration bc_top_value {float} -- top boundary value bc_top_type {str} -- boundary type (flux, constant) bc_bot_value {float} -- bottom boundary value bc_bot_type {str} -- type of bottom boundary Keyword Arguments: w {float} -- advective term for this element (default: {False}) int_transport {bool} -- integrate transport? (default: {True}) """ self.species[name] = DotDict({}) self.species[name]['bc_top_value'] = bc_top_value self.species[name]['bc_top_type'] = bc_top_type.lower() self.species[name]['bc_bot_value'] = bc_bot_value self.species[name]['bc_bot_type'] = bc_bot_type.lower() self.species[name]['theta'] = np.ones((self.N)) * theta self.species[name]['D'] = D self.species[name]['init_conc'] = init_conc self.species[name]['concentration'] = np.zeros( (self.N, self.time.size)) self.species[name]['rates'] = np.zeros((self.N, self.time.size)) self.species[name]['concentration'][:, 0] = self.species[name][ 'init_conc'] self.profiles[name] = self.species[name]['concentration'][:, 0] if w: self.species[name]['w'] = w else: self.species[name]['w'] = self.w self.species[name]['int_transport'] = int_transport if int_transport: self.template_AL_AR(name) self.update_matrices_due_to_bc(name, 0) self.dcdt[name] = '0'
def __init__(self, lab): """ defines which parameters to calibrate, range of the parameters, optimization function etc. NOTE: self.parameters is ordered dictionary to ensure iteration over the same order and assigning correct x0 values """ self.lab = lab self.parameters = OrderedDict({}) self.measurements = DotDict({}) self.error = np.nan self.verbose = False
def add_measurement(self, name, values, time, depth=0): """add measurment which will be used for calibration. Name of the measurement should match name variable in the model Arguments: name {str} -- name of the measurement values {np.array} -- values of measurement time {np.array} -- when measured (realative to model times) depth {float} -- depth of the measurement for column model (default: {0}) """ self.measurements[name] = DotDict({}) self.measurements[name]['values'] = values self.measurements[name]['time'] = time self.measurements[name]['depth'] = depth
def add_species(self, element, init_conc): """Summary Args: element (string): name of the element init_conc (float): initial concentration """ self.species[element] = DotDict({}) self.species[element]['init_C'] = init_conc self.species[element]['concentration'] = np.zeros( (self.N, self.time.size)) self.species[element]['alpha'] = np.zeros((self.N, self.time.size)) self.species[element]['rates'] = np.zeros((self.N, self.time.size)) self.species[element]['concentration'][:, 0] = self.species[element][ 'init_C'] self.profiles[element] = self.species[element]['concentration'][:, 0] self.species[element]['int_transport'] = False self.dcdt[element] = '0'
class Lab: """The batch experiments simulations""" def __init__(self, tend, dt, tstart=0): """ init function initialize the lab class Arguments: tend {float} -- end time of computation dt {float} -- timestep Keyword Arguments: tstart {float} -- strart time of computation (default: {0}) """ self.tend = tend self.dt = dt self.time = np.linspace(tstart, tend, round(tend / dt) + 1) self.species = DotDict({}) self.dynamic_functions = DotDict({}) self.profiles = DotDict({}) self.dcdt = DotDict({}) self.rates = DotDict({}) self.estimated_rates = DotDict({}) self.constants = DotDict({}) self.henry_law_equations = [] self.acid_base_components = [] self.acid_base_system = phcalc.System() self.ode_method = 'scipy' def __getattr__(self, attr): """dot notation for species you can use lab.element and get species dictionary Arguments: attr {str} -- name of the species Returns: DotDict -- returns DotDict of species """ return self.species[attr] def solve(self, verbose=True): """ solves coupled PDEs Keyword Arguments: verbose {bool} -- if true verbose output (default: {True}) with estimation of computational time etc. """ self.reset() with np.errstate(invalid='raise'): for i in np.arange( 1, len( np.linspace(0, self.tend, round(self.tend / self.dt) + 1))): # try: self.integrate_one_timestep(i) if verbose: self.estimate_time_of_computation(i) # except FloatingPointError as inst: # print( # '\nABORT!!!: Numerical instability... Please, adjust dt and dx manually...') # traceback.print_exc() # sys.exit() def estimate_time_of_computation(self, i): if i == 1: self.tstart = time.time() print("Simulation started:\n\t", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) if i == 100: total_t = len(self.time) * (time.time() - self.tstart) / 100 * self.dt / self.dt m, s = divmod(total_t, 60) h, m = divmod(m, 60) print( "\n\nEstimated time of the code execution:\n\t %dh:%02dm:%02ds" % (h, m, s)) print( "Will finish approx.:\n\t", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() + total_t))) def henry_equilibrium_integrate(self, i): for eq in self.henry_law_equations: self.species[eq['gas']]['concentration'][:, i], self.species[ eq['aq']][ 'concentration'][:, i] = equilibriumsolver.solve_henry_law( self.species[eq['aq']]['concentration'][:, i] + self.species[eq['gas']]['concentration'][:, i], eq['Hcc']) for elem in [eq['gas'], eq['aq']]: self.profiles[elem] = self.species[elem]['concentration'][:, i] if self.species[elem]['int_transport']: self.update_matrices_due_to_bc(elem, i) def acid_base_solve_ph(self, i): # initial guess from previous time-step res = self.species['pH']['concentration'][0, i - 1] for idx_j in range(self.N): for c in self.acid_base_components: init_conc = 0 for element in c['species']: init_conc += self.species[element]['concentration'][idx_j, i] c['pH_object'].conc = init_conc if idx_j == 0: self.acid_base_system.pHsolve(guess=7, tol=1e-4) res = self.acid_base_system.pH else: phs = np.linspace(res - 0.1, res + 0.1, 201) idx = self.acid_base_system._diff_pos_neg(phs).argmin() res = phs[idx] self.species['pH']['concentration'][idx_j, i] = res self.profiles['pH'][idx_j] = res def add_partition_equilibrium(self, aq, gas, Hcc): """ For partition reactions between 2 species Args: aq (string): name of aquatic species gas (string): name of gaseous species Hcc (double): Henry Law Constant """ self.henry_law_equations.append({'aq': aq, 'gas': gas, 'Hcc': Hcc}) def add_ion(self, element, charge): ion = phcalc.Neutral(charge=charge, conc=np.nan) self.acid_base_components.append({ 'species': [element], 'pH_object': ion }) def add_acid(self, species, pKa, charge=0): acid = phcalc.Acid(pKa=pKa, charge=charge, conc=np.nan) self.acid_base_components.append({ 'species': species, 'pH_object': acid }) def acid_base_equilibrium_solve(self, i): self.acid_base_solve_ph(i) self.acid_base_update_concentrations(i) def init_rates_arrays(self): for rate in self.rates: self.estimated_rates[rate] = np.zeros((self.N, self.time.size)) def create_dynamic_functions(self): fun_str = desolver.create_ode_function(self.species, self.constants, self.rates, self.dcdt) exec(fun_str) self.dynamic_functions['dydt_str'] = fun_str self.dynamic_functions['dydt'] = locals()['f'] self.dynamic_functions['solver'] = desolver.create_solver( locals()['f']) def reset(self): """lab.reset() resets the solution for re-run """ for element in self.species: self.profiles[element] = self.species[element]['concentration'][:, 0] def pre_run_methods(self): if len(self.acid_base_components) > 0: self.create_acid_base_system() self.acid_base_equilibrium_solve(0) if self.ode_method is 'scipy': self.create_dynamic_functions() self.init_rates_arrays() def change_concentration_profile(self, element, i, new_profile): self.profiles[element] = new_profile self.update_matrices_due_to_bc(element, i) def reactions_integrate_scipy(self, i): # C_new, rates_per_elem, rates_per_rate = desolver.ode_integrate(self.profiles, self.dcdt, self.rates, self.constants, self.dt, solver='rk4') # C_new, rates_per_elem = desolver.ode_integrate(self.profiles, self.dcdt, self.rates, self.constants, self.dt, solver='rk4') # for idx_j in range(self.N): for idx_j in range(self.N): yinit = np.zeros(len(self.species)) for idx, s in enumerate(self.species): yinit[idx] = self.profiles[s][idx_j] ynew = desolver.ode_integrate_scipy( self.dynamic_functions['solver'], yinit, self.dt) for idx, s in enumerate(self.species): self.species[s]['concentration'][idx_j, i] = ynew[idx] for element in self.species: self.profiles[element] = self.species[element]['concentration'][:, i] if self.species[element]['int_transport']: self.update_matrices_due_to_bc(element, i) def reconstruct_rates(self): for idx_t in range(len(self.time)): for name, rate in self.rates.items(): conc = {} for s in self.species: conc[s] = self.species[s]['concentration'][:, idx_t] r = ne.evaluate(rate, {**self.constants, **conc}) self.estimated_rates[name][:, idx_t] = r * (r > 0) for s in self.species: self.species[s]['rates'] = ( self.species[s]['concentration'][:, 1:] - self.species[s]['concentration'][:, :-1]) / self.dt
class Lab: """The batch experiments simulations""" def __init__(self, tend, dt, tstart=0): """ init function initialize the lab class Arguments: tend {float} -- end time of computation dt {float} -- timestep Keyword Arguments: tstart {float} -- start time of computation (default: {0}) """ self.tend = tend self.dt = dt self.time = np.linspace(tstart, tend, round(tend / dt) + 1) self.species = DotDict({}) self.dynamic_functions = DotDict({}) self.profiles = DotDict({}) self.dcdt = DotDict({}) self.rates = DotDict({}) self.estimated_rates = DotDict({}) self.constants = DotDict({}) self.functions = DotDict({}) self.henry_law_equations = [] self.acid_base_components = [] self.acid_base_system = phcalc.System() self.ode_method = 'scipy' def __getattr__(self, attr): """dot notation for species you can use lab.element and get species dictionary Arguments: attr {str} -- name of the species Returns: DotDict -- returns DotDict of species """ return self.species[attr] def solve(self, verbose=True): """ solves coupled PDEs Keyword Arguments: verbose {bool} -- if true verbose output (default: {True}) with estimation of computational time etc. """ self.reset() with np.errstate(invalid='raise'): for i in np.arange(1, len(self.time)): try: self.integrate_one_timestep(i) if verbose: self.estimate_time_of_computation(i) except FloatingPointError as inst: print( '\nABORT!!!: Numerical instability... Please, adjust dt and dx manually...' ) traceback.print_exc() sys.exit() # temporal hack for time dependent variables if 'TIME' in self.species: self.species.pop('TIME', None) def estimate_time_of_computation(self, i): """ function estimates time required for computation uses first hundread of steps to estimate approximate time for computation of all steps Arguments: i {int} -- index of time """ if i == 1: self.start_computation_time = time.time() print("Simulation started:\n\t", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) if i == 100: total_t = len(self.time) * ( time.time() - self.start_computation_time) / 100 * self.dt / self.dt m, s = divmod(total_t, 60) h, m = divmod(m, 60) print( "\n\nEstimated time of the code execution:\n\t %dh:%02dm:%02ds" % (h, m, s)) print( "Will finish approx.:\n\t", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() + total_t))) def henry_equilibrium_integrate(self, i): """integrates Henry equlibrium reactions Estimates the destribution of species using functions from equilibriumsolver, and, then, updated the profiles with new concentrations Arguments: i {int} -- index of time """ for eq in self.henry_law_equations: self.species[eq['gas']]['concentration'][:, i], self.species[ eq['aq']][ 'concentration'][:, i] = equilibriumsolver.solve_henry_law( self.species[eq['aq']]['concentration'][:, i] + self.species[eq['gas']]['concentration'][:, i], eq['Hcc']) for elem in [eq['gas'], eq['aq']]: self.profiles[elem] = self.species[elem]['concentration'][:, i] if self.species[elem]['int_transport']: self.update_matrices_due_to_bc(elem, i) def acid_base_solve_ph(self, i): """solves acid base reactions solves acid-base using function from phcalc. First, it sums the total concentration for particular species, then, estimates pH. if it idx=0, then it uses "greedy" algorithm, else, uses +-0.1 of previous pH and finds minimum around it. Arguments: i {[type]} -- [description] """ # initial guess from previous time-step res = self.species['pH']['concentration'][0, i - 1] for idx_j in range(self.N): for c in self.acid_base_components: init_conc = 0 for element in c['species']: init_conc += self.species[element]['concentration'][idx_j, i] c['pH_object'].conc = init_conc if idx_j == 0: self.acid_base_system.pHsolve(guess=7, tol=1e-4) res = self.acid_base_system.pH else: phs = np.linspace(res - 0.1, res + 0.1, 201) idx = self.acid_base_system._diff_pos_neg(phs).argmin() res = phs[idx] self.species['pH']['concentration'][idx_j, i] = res self.profiles['pH'][idx_j] = res def add_partition_equilibrium(self, aq, gas, Hcc): """ For partition reactions between 2 species Args: aq (string): name of aquatic species gas (string): name of gaseous species Hcc (double): Henry Law Constant """ self.henry_law_equations.append({'aq': aq, 'gas': gas, 'Hcc': Hcc}) def henry_equilibrium(self, aq, gas, Hcc): """ For partition reactions between 2 species Args: aq (string): name of aquatic species gas (string): name of gaseous species Hcc (double): Henry Law Constant """ self.add_partition_equilibrium(aq, gas, Hcc) def add_ion(self, name, charge): """add non-dissociative ion in acid-base system Arguments: name {str} -- name of the chemical element charge {float} -- charge of chemical element """ ion = phcalc.Neutral(charge=charge, conc=np.nan) self.acid_base_components.append({'species': [name], 'pH_object': ion}) def add_acid(self, species, pKa, charge=0): """add acid in acid-base system Arguments: species {list} -- list of species, e.g. ['H3PO4', 'H2PO4', 'HPO4', 'PO4'] pKa {list} -- list of floats with pKs, e.g. [2.148, 7.198, 12.375] Keyword Arguments: charge {float} -- highest charge in the acid (default: {0}) """ acid = phcalc.Acid(pKa=pKa, charge=charge, conc=np.nan) self.acid_base_components.append({ 'species': species, 'pH_object': acid }) def acid_base_equilibrium_solve(self, i): """solves acid-base equilibrium equations Arguments: i {int} -- step in time """ self.acid_base_solve_ph(i) self.acid_base_update_concentrations(i) def init_rates_arrays(self): """allocates zero matrices for rates """ for rate in self.rates: self.estimated_rates[rate] = np.zeros((self.N, self.time.size)) def create_dynamic_functions(self): """create strings of dynamic functions for scipy solver and later execute them using exec(), potentially not safe but haven't found better approach yet. """ fun_str = desolver.create_ode_function(self.species, self.functions, self.constants, self.rates, self.dcdt) exec(fun_str) self.dynamic_functions['dydt_str'] = fun_str self.dynamic_functions['dydt'] = locals()['f'] self.dynamic_functions['solver'] = desolver.create_solver( locals()['f']) def reset(self): """resets the solution for re-run """ for element in self.species: self.profiles[element] = self.species[element]['concentration'][:, 0] def pre_run_methods(self): """pre-run before solve initiates acid-base system and creates dynamic functions (strings of ODE) for reaction solver """ self.add_time_variable() if len(self.acid_base_components) > 0: self.create_acid_base_system() self.acid_base_equilibrium_solve(0) if self.ode_method is 'scipy': self.create_dynamic_functions() self.init_rates_arrays() def change_concentration_profile(self, element, i, new_profile): """change concentration in profile vectors Arguments: element {str} -- name of the element i {int} -- step in time new_profile {np.array} -- vector of new concetrations """ self.profiles[element] = new_profile self.update_matrices_due_to_bc(element, i) def reactions_integrate_scipy(self, i): """integrates ODE of reactions Arguments: i {int} -- step in time """ # C_new, rates_per_elem, rates_per_rate = desolver.ode_integrate(self.profiles, self.dcdt, self.rates, self.constants, self.dt, solver='rk4') # C_new, rates_per_elem = desolver.ode_integrate(self.profiles, self.dcdt, self.rates, self.constants, self.dt, solver='rk4') # for idx_j in range(self.N): for idx_j in range(self.N): yinit = np.zeros(len(self.species)) for idx, s in enumerate(self.species): yinit[idx] = self.profiles[s][idx_j] ynew = desolver.ode_integrate_scipy( self.dynamic_functions['solver'], yinit, self.dt) for idx, s in enumerate(self.species): self.species[s]['concentration'][idx_j, i] = ynew[idx] for element in self.species: self.profiles[element] = self.species[element]['concentration'][:, i] if self.species[element]['int_transport']: self.update_matrices_due_to_bc(element, i) def reconstruct_rates(self): """reconstructs rates after model run 1. estimates rates; 2. estimates changes of concentrations not sure if it works with dynamic functions of "scipy", only with rk4 and butcher 5? """ if self.ode_method == 'scipy': rates_str = desolver.create_rate_function(self.species, self.functions, self.constants, self.rates, self.dcdt) exec(rates_str, globals()) self.dynamic_functions['rates_str'] = rates_str # from IPython.core.debugger import set_trace # set_trace() self.dynamic_functions['rates'] = globals()['rates'] yinit = np.zeros(len(self.species)) for idx_t in range(len(self.time)): for idx_j in range(self.N): for idx, s in enumerate(self.species): yinit[idx] = self.species[s]['concentration'][idx_j, idx_t] rates = self.dynamic_functions['rates'](yinit) for idx, r in enumerate(self.rates): self.estimated_rates[r][idx_j, idx_t] = rates[idx] else: for idx_t in range(len(self.time)): for name, rate in self.rates.items(): conc = {} for spc in self.species: conc[spc] = self.species[spc]['concentration'][:, idx_t] r = ne.evaluate(rate, {**self.constants, **conc}) self.estimated_rates[name][:, idx_t] = r * (r > 0) for spc in self.species: self.species[spc]['rates'] = ( self.species[spc]['concentration'][:, 1:] - self.species[spc]['concentration'][:, :-1]) / self.dt