Esempio n. 1
0
'''
density_dict,calc_dict = initial_conditions(initial_conditions_gas,M,species,\
                                            rate_numba,calc_dict,particles,\
                                                initials_from_run,t0,path)
density_dict['RO2'] = ppool_density_calc(density_dict, ppool)

#calculating t0 summations
summations_dict = {}
if summations == 1:
    summations_dict = summations_compile(sums)
    sums_dict = {}
    sums_dict = summations_eval(summations_dict, density_dict, calc_dict)
    density_dict.update(sums_dict)

if particles == 1:
    particle_dict = particle_calcs(part_calc_dict, density_dict)
else:
    particle_dict = {}
'''    
saving initial conditions to an output directory for reference
'''
f = open("%s/%s/initial_concentrations.txt" % (path, output_folder), "w")
for i in species:
    f.write('%s=%s ;\n' % (i, density_dict[i]))
f.close()
'''
Calculating the reaction rates and compiling the master array of ODEs
'''
num_species = len(species)  #some calculations ask for the number of species
#better to calculate it once rather than every time
Esempio n. 2
0
def dydy(t, y0):
    '''
    The function to calculate the jacobian that is fed to the integrator

    inputs:
    	t = time
    	y0 = concentration of species

    returns:
    	dydy = array of change in species concentration with change 
    		   in other species concentrations
    '''
    #Update density dictionary
    for i in range(num_species):
        density_dict[species[i]] = y0[i]
    density_dict['RO2'] = ppool_density_calc(density_dict, ppool)

    #if summations are included they need to be updated to the density dictionary also
    if summations == 1:
        sums_dict = summations_eval(summations_dict, density_dict, calc_dict)
        density_dict.update(sums_dict)

    #n = time in seconds from midnight for each day
    n = t - ((int(t / 86400)) * 86400)

    # recalculate photolysis
    # COSX and SECX involve local hour angles dependant on position and time
    # of year (latitude of location and declination of sun)

    lha = (1 + ((n - time_correct) /
                4.32E+4)) * pi  # local hour angle, radians. Midnight start
    cosx = ((numba_cos(lha) * cosld) + sinld)  # solar zenith angle

    # Set negative cosx to zero and calculate the inverse
    # (secx=1/cosx). The MCM photolysis parameterisation
    # (http://mcm.leeds.ac.uk/MCM/parameters/photolysis_param.htt)
    # requires cosx and secx to calculate the photolysis rates.
    if cosx <= 1E-30:
        cosx = 0.0
        secx = 1.0E+30
    else:
        secx = 1.0 / (
            ((cosx + numba_abs(cosx)) / 2) + 1.0E-30)  # no division by 0

    #updates the relevant dictionary whether lights are on or off
    for i in light_on_times:
        if i[0] <= t <= i[1]:
            indoor_photo_dict["cosx"] = cosx
            indoor_photo_dict["secx"] = secx
            photolysis_J(indoor_photo_dict, photo_dict, J_dict)
            break
        else:
            indoor_photo_dict_off["cosx"] = cosx
            indoor_photo_dict_off["secx"] = secx
            photolysis_J(indoor_photo_dict_off, photo_dict, J_dict)

    #diurnal outdoor rates
    if diurnal == 1:
        out_calc_dict["n"] = n
        out_calc_dict["cosx"] = cosx
        out_calc_dict["secx"] = secx
        outdoor_rates_calc(outdoor_dict, outdoor_dict_diurnal, out_calc_dict)

    #recalculate temp,humidity,water
    h2o, rh = h2o_rh(t, temp, rel_humidity, numba_exp)
    calc_dict['h2o'] = h2o

    #recalculate particle sums
    global particle_dict
    if particles == 1:
        particle_dict = particle_calcs(part_calc_dict, density_dict)
    else:
        particle_dict = {}

    #checks time, if between times set for a forced density change the rate
    #is applied to the specific species
    if timed_emissions == 1:
        for key in timed_inputs:
            for i in timed_inputs[key]:
                if i[0] <= t <= i[1]:
                    timed_dict["%s_timed" % key] = i[2]
                    break
                else:
                    timed_dict["%s_timed" % key] = 0

    #recalculate reaction rates
    reaction_rate_dict=reaction_eval(reaction_number,J_dict,calc_dict,density_dict,\
                                     dt,reaction_compiled_dict,outdoor_dict,\
                                         surface_dict,particle_dict,timed_dict)

    #recalculate jacobian
    dydy_jac=dy_dy_calc(dy_dy_dict,J_dict,calc_dict,density_dict,species,\
                        outdoor_dict,surface_dict,particle_dict,num_species,\
                            timed_dict,reaction_rate_dict)
    return dydy_jac
Esempio n. 3
0
def run_inchem(filename, particles, INCHEM_additional, custom, temp, rel_humidity,
               M, const_dict, AER, diurnal, city, date, lat, light_type, 
               light_on_times, glass, HMIX, initials_from_run,
               initial_conditions_gas, timed_emissions, timed_inputs, dt, t0,
               seconds_to_integrate, custom_name, output_graph, output_species):
  
    '''
    import all modules
    '''
    import sys
    import pickle
    from modules.inchem_import import import_all, custom_import 
    from modules.particle_input import particle_import, particle_calcs, reactions_check
    from modules.photolysis import photolysis_J, Zixu_photolysis, Zixu_photolysis_compiled
    from modules.initial_dictionaries import initial_conditions, master_calc, master_compiler,\
        reaction_rate_compile, reaction_eval, write_jacobian_build, INCHEM_species_calc
    from modules.outdoor_concentrations import outdoor_rates, outdoor_rates_diurnal, outdoor_rates_calc
    import numpy as np
    import numba as nb
    from scipy.integrate import ode
    import time
    from modules.surface_dictionary import surface_deposition
    from threadpoolctl import threadpool_limits
    import importlib.util
    import pandas as pd
    import os
    import datetime
    import math
    import time as timing
    from modules.reactivity import reactivity_summation, reactivity_calc, production_calc
    
    sys.setrecursionlimit(4000) #to compile the master array the recursion limit
    #must be increased as some of the evaluated ODEs are greater than the usual
    #system limit
    
    save_rate = 1 #sets the rate at which outputs are saved within the integrator.
    #Useful if the timestep has to be reduced but an output at a specific interval
    #is still required. A save rate of 1 will save every dt, a save rate of 2 will
    #save every 2*dt    
    
    def summations_compile(sums):
        '''
        compiles any custom summations
        
        inputs:
            sums = list of summations [name of sum, calculation]
            
        returns:
            summation_dict = dictionary of summations {name of sum : compiled calculation}
        '''
        summation_dict={}
        for i in sums:
            summation_dict[i[0]]=compile(i[1],'<string>','eval')
        return summation_dict
    
    
    def summations_eval(summation_dict,density_dict,calc_dict):
        '''
        evaluates custom summations
        
        inputs:
            summation_dict = dictionary of summations {name of sum : compiled calculation}
            density_dict = dictionary of current species concentrations {species : concentration}
            calc_dict = dictionary of constants and variables for use in calculations
            
        returns:
            sums_dict = dictionary of evaluated sum calculations {name of sum : value}
        '''
        full_dict={**density_dict,**calc_dict}
        for spec in summation_dict.keys():
            sums_dict[spec]=(eval(summation_dict[spec],{},full_dict)) 
        return sums_dict
    
    
    def h2o_rh(time,temp,rel_humidity,numba_exp):
        '''
        calculates relative humidity and water at a given time and temperature
    
        inputs:
            time = current simulation time (s)
            temp = current temperature value (C)
            rel_humidity = current relative humidity (%), can be calculation
            numba_exp = exponent
            
        returns:
            h20 = water vapour
            rh = relative humidity (%)
        '''
        rh = rel_humidity 
        h2o=6.1078*numba_exp(-1.0e+0*(597.3-0.57*(temp-273.16))*18.0/1.986*
                       (1.0/temp-1.0/273.16))*10./(1.38e-16*temp)*rh               
        return h2o,rh
    
    
    def ppool_density_calc(density_dict,ppool):
        '''
        peroxy radical summation evaluation
        
        inputs:
            density_dict = dictionary of current concentrations
            ppool = list of species to be summed for RO2
        
        returns:
            RO2 = summed peroxy radical species
        '''
        RO2 = 0.
        for p in ppool:
            RO2 += eval(p,{},density_dict)
        return RO2
    
    
    def dy_calc(master_compiled,reaction_rate_dict,calc_dict,density_dict,outdoor_dict,\
                surface_dict,species,timed_dict):
        '''
        evaluating the master array of species
        
        inputs:
            master_compiled = dictionary of compiled ODEs
            reaction_rate_dict = dictionary of reaction rate values at current time
            calc_dict = dictionary of constants and variables for use in calculations
            density_dict = dictionary of current concentrations
            outdoor_dict = dictionary of outdoor species concentrations
            surface_dict = dictionary of surface deposition rates for each species
            species = list of species
            timed_dict = dictionary of rates for any timed inputs for current time
            
        returns:
            dy_dict = dictionary of changes in concentration for each species per second
        '''
        dy_dict={}    
        full_dict={**reaction_rate_dict,**calc_dict,**density_dict,**outdoor_dict,\
                   **surface_dict,**timed_dict}
        
        for specie in species:
            dy_dict[specie]=(eval(master_compiled[specie],{},full_dict))
        return dy_dict
    
    
    def dy_dy_calc(dy_dy_dict,J_dict,calc_dict,density_dict,species,outdoor_dict,\
                   surface_dict,num_species,timed_dict,reaction_rate_dict):
        '''
        Evaluating the Jacobian during integration
        
        inputs:
            dy_dy_dict = dictionary jacobian {index:[x,y,compiled calculation]}
            J_dict = dictionary of current photolysys values
            reaction_rate_dict = dictionary of reaction rate values at current time
            calc_dict = dictionary of constants and variables for use in calculations
            density_dict = dictionary of current concentrations
            outdoor_dict = dictionary of outdoor species concentrations
            surface_dict = dictionary of surface deposition rates for each species
            num_species = number of species
            
        returns:
            dy_dy = dictionary of values of species change with species per second
        '''
        dy_dy=np.zeros((num_species,num_species),dtype=np.float32) #twice as fast as lil_matrix
        
        full_dict={**reaction_rate_dict,**density_dict,**outdoor_dict,**surface_dict,\
                   **calc_dict,**timed_dict}
        for k,v in dy_dy_dict.items():
                dy_dy[v[0],v[1]]=eval(v[2],{},full_dict)
        return dy_dy
    
    
    def dydt(t,y0):
        '''
        The function to calculate dydt that is fed to the integrator
    
        inputs:
        	t = time
        	y0 = concentration of species
    
        returns:
        	dy = dictionary of change in species concentration with change in time
        '''
        #Update density dictionary
        for i in range(num_species):
            density_dict[species[i]]=y0[i]  
        density_dict['RO2']=ppool_density_calc(density_dict,ppool)
        
        #if summations are included they need to be updated to the density dictionary also           
        if summations == True:
            sums_dict = summations_eval(summations_dict,density_dict,calc_dict)
            density_dict.update(sums_dict)
            
        #n = time in seconds from midnight for each day
        n = t-((int(t/86400))*86400)
        
        # recalculate photolysis
        # COSX and SECX involve local hour angles dependant on position and time
        # of year (latitude of location and declination of sun)
    
        lha = (1+((n-time_correct)/4.32E+4))*pi     # local hour angle, radians. Midnight start
        cosx = ((numba_cos(lha)*cosld)+sinld)       # solar zenith angle 
        
        # Set negative cosx to zero and calculate the inverse  
        # (secx=1/cosx). The MCM photolysis parameterisation  
        # (http://mcm.leeds.ac.uk/MCM/parameters/photolysis_param.htt)  
        # requires cosx and secx to calculate the photolysis rates.
        if cosx <= 1E-30:
            cosx = 0.0  
            secx = 1.0E+30  
        else:  
            secx = 1.0 / (((cosx + numba_abs(cosx))/2)+1.0E-30) #no divison by 0
        
        #updates the relevant dictionary whether lights are on or off
        for i in light_on_times:
            if i[0] <= t <= i[1]:
                indoor_photo_dict["cosx"] = cosx
                indoor_photo_dict["secx"] = secx
                photolysis_J(indoor_photo_dict,photo_dict,J_dict)
                break
            else:
                indoor_photo_dict_off["cosx"] = cosx
                indoor_photo_dict_off["secx"] = secx
                photolysis_J(indoor_photo_dict_off,photo_dict,J_dict)
        
        #diurnal outdoor rates
        if diurnal == True:
            out_calc_dict["n"] = n
            out_calc_dict["cosx"] = cosx
            out_calc_dict["secx"] = secx
            outdoor_rates_calc(outdoor_dict,outdoor_dict_diurnal,out_calc_dict)
        
        #recalculate humidity,water
        h2o,rh = h2o_rh(t,temp,rel_humidity,numba_exp)
        calc_dict['h2o']=h2o
     
        #recalculate particle sums
        if particles == True:
            particle_dict = particle_calcs(part_calc_dict,density_dict)
            density_dict.update(particle_dict)
            
        #checks time, if between times set for a forced density change the rate
        #is applied to the specific species
        if timed_emissions == True:
            for key in timed_inputs:
                for i in timed_inputs[key]:
                    if i[0] <= t <= i[1]:
                        timed_dict["%s_timed" % key] = i[2]
                        break
                    else:
                        timed_dict["%s_timed" % key] = 0
        
        #recalculate reaction rates
        reaction_rate_dict=reaction_eval(reaction_number,J_dict,calc_dict,\
                                         density_dict,dt,reaction_compiled_dict,\
                                             outdoor_dict,surface_dict,\
                                                 timed_dict)
        
        #evaluate the mater array to recalculate concentrations
        dy_dict=dy_calc(master_compiled,reaction_rate_dict,calc_dict,density_dict,\
                        outdoor_dict,surface_dict,species,timed_dict)
        
        #output the new concentration values
        dy = [dy_dict[i] for i in species]
        return dy
    
    
    def dydy(t,y0): 
        '''
        The function to calculate the jacobian that is fed to the integrator
    
        inputs:
        	t = time
        	y0 = concentration of species
    
        returns:
        	dydy = array of change in species concentration with change 
        		   in other species concentrations
        '''
        #Update density dictionary
        for i in range(num_species):
            density_dict[species[i]]=y0[i]            
        density_dict['RO2']=ppool_density_calc(density_dict,ppool)
        
        #if summations are included they need to be updated to the density dictionary also            
        if summations == True:
            sums_dict = summations_eval(summations_dict,density_dict,calc_dict)
            density_dict.update(sums_dict)
        
        #n = time in seconds from midnight for each day
        n = t-((int(t/86400))*86400)
        
        # recalculate photolysis
        # COSX and SECX involve local hour angles dependant on position and time
        # of year (latitude of location and declination of sun)
    
        lha = (1+((n-time_correct)/4.32E+4))*pi     # local hour angle, radians. Midnight start
        cosx = ((numba_cos(lha)*cosld)+sinld)       # solar zenith angle 
        
        # Set negative cosx to zero and calculate the inverse  
        # (secx=1/cosx). The MCM photolysis parameterisation  
        # (http://mcm.leeds.ac.uk/MCM/parameters/photolysis_param.htt)  
        # requires cosx and secx to calculate the photolysis rates.
        if cosx <= 1E-30:
            cosx = 0.0  
            secx = 1.0E+30  
        else:  
            secx = 1.0 / (((cosx + numba_abs(cosx))/2)+1.0E-30) # no division by 0
        
        #updates the relevant dictionary whether lights are on or off
        for i in light_on_times:
            if i[0] <= t <= i[1]:
                indoor_photo_dict["cosx"] = cosx
                indoor_photo_dict["secx"] = secx
                photolysis_J(indoor_photo_dict,photo_dict,J_dict)
                break
            else:
                indoor_photo_dict_off["cosx"] = cosx
                indoor_photo_dict_off["secx"] = secx
                photolysis_J(indoor_photo_dict_off,photo_dict,J_dict)
        
        #diurnal outdoor rates
        if diurnal == True:
            out_calc_dict["n"] = n
            out_calc_dict["cosx"] = cosx
            out_calc_dict["secx"] = secx
            outdoor_rates_calc(outdoor_dict,outdoor_dict_diurnal,out_calc_dict)
        
        #recalculate temp,humidity,water
        h2o,rh = h2o_rh(t,temp,rel_humidity,numba_exp)
        calc_dict['h2o']=h2o
     
        #recalculate particle sums
        if particles == True:
            particle_dict = particle_calcs(part_calc_dict,density_dict)
            density_dict.update(particle_dict)
        
        #checks time, if between times set for a forced density change the rate
        #is applied to the specific species
        if timed_emissions == True:
            for key in timed_inputs:
                for i in timed_inputs[key]:
                    if i[0] <= t <= i[1]:
                        timed_dict["%s_timed" % key] = i[2]
                        break
                    else:
                        timed_dict["%s_timed" % key] = 0
                    
        #recalculate reaction rates
        reaction_rate_dict=reaction_eval(reaction_number,J_dict,calc_dict,density_dict,\
                                         dt,reaction_compiled_dict,outdoor_dict,\
                                             surface_dict,timed_dict)
        
        #recalculate jacobian
        dydy_jac=dy_dy_calc(dy_dy_dict,J_dict,calc_dict,density_dict,species,\
                            outdoor_dict,surface_dict,num_species,\
                                timed_dict,reaction_rate_dict)
        return dydy_jac
    
    def integrate_function(iters,t_bound_internal,y0,t0,ret,save_rate,num_species,\
                           total_iter,dt):
        '''
        Using lsoda to calculate density evolution and output as n_new
    
        inputs:
        	iters = number of iterations that have already been performed 
        			through the integrator, used to calculate when an output
        			is saved
        	t_bound_internal = the time to integrate to within this function (s)
        	y0 = initial species concentration
        	t0 = start time (s)
        	ret = integrator return code from scipy.integrate.ode.get_return_code
        		  set as 1 prior to calling the integrator
        	save_rate = how many iterations to perform before saving an output
        	num_species = the total number of species 
        	total_iter = the total number of iterations to be completed
        	dt = time step (s)
    
        returns:
        	dt_out = list of output times
        	n_new = array of output concentrations for each species at each time
        	iters = total number of iterations completed
        	ret = integrator return code from scipy.integrate.ode.get_return_code
        	iter_time = list of time stamps for the completion of each integration
        				over dt
        	calculated_output = dictionary of lists of calculated values for each
        						iteration. These are calculations done using the species
        						concentrations and are only for output purposes
        '''
        #assign arrays to populate
        n_new=[[] for i in range(num_species)]
        dt_out=[]
        iter_time=[]
        
        calculated_output = {}
        calculated_output['RO2'] = []
        if particles == True:
            calculated_output['tsp'] = []
            calculated_output['acidsum'] = []
            calculated_output['tspx'] = []
            calculated_output['mwomv'] = []
        for i in reactivity_dict:
            calculated_output[i] = []
        for i in production_dict:
            calculated_output[i] = []
        for i in J_dict:
            calculated_output[i] = []
        for i in outdoor_dict:
            calculated_output[i] = []
        if summations == True:
            for i in sums_dict:
                calculated_output[i] = []
        
        #set integrator args
        atol = [1e-6]*num_species     #Default 1e-6
        rtol = 1e-6                  #Default 1e-6
        first_step = 1e-10              #size of first integration step to try (s)
        nsteps = 5000                   #max number of internal timesteps
        max_step = dt                   #maximum time step allowed by integrator
        
        #set the integrator and arguments
        r=ode(dydt,dydy).set_integrator('lsoda',atol=atol,rtol=rtol,first_step=\
                                        first_step,nsteps=nsteps,max_step=max_step)
        r.set_initial_value(y0,t0)
        
        #integrate
        while r.successful() and r.t<t_bound_internal:
            print('Iteration ', iters+1,'/', total_iter,'¦',r.t,' to ',r.t+dt)
            r.integrate(r.t+dt)
            iters=iters+1
            ret=r.get_return_code()
            if iters % save_rate == 0: #output every save_rate iterations
                dt_out.append(int(r.t))
                iter_time.append(timing.time()-start_time)
                calculated_output['RO2'].append(density_dict["RO2"])
                reactivity_calc(reactivity_dict,reactivity_compiled,reaction_rate_dict,\
                                calc_dict,density_dict)
                production_calc(production_dict,production_compiled,reaction_rate_dict,\
                                calc_dict,density_dict)
                for i in reactivity_dict:
                    calculated_output[i].append(reactivity_dict[i])
                for i in production_dict:
                    calculated_output[i].append(production_dict[i])
                if particles == True:
                    calculated_output['tsp'].append(density_dict['tsp'])
                    calculated_output['acidsum'].append(density_dict['acidsum'])
                    calculated_output['tspx'].append(density_dict['tspx'])
                    calculated_output['mwomv'].append(density_dict['mwomv'])
                for i in range(num_species):
                    n_new[i].append(r.y[i])
                for i in J_dict:
                    calculated_output[i].append(J_dict[i])
                for i in outdoor_dict:
                    calculated_output[i].append(outdoor_dict[i])
                if summations == True:
                    for i in sums_dict:
                        calculated_output[i].append(density_dict[i])
        return dt_out,n_new,iters,ret,iter_time,calculated_output
       
    '''
    numba functions to increase speed of mathamatical operations
    '''
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_exp(x):
        return np.exp(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_cos(x):
        return np.cos(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_arctan(x):
        return np.arctan(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_sin(x):
        return np.sin(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_arccos(x):
        return np.arccos(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_sqrt(x):
        return np.sqrt(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_abs(x):
        return np.abs(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_log(x):
        return np.log(x)
    
    @nb.jit(nb.f8(nb.f8), nopython=True)
    def numba_log10(x):
        return np.log10(x)
        
    start_time=timing.time() #program start time
    
    '''
    setting the output folder in current working directory
    '''
    path=os.getcwd()
    now = datetime.datetime.now()
    output_folder = ("%s_%s" % (now.strftime("%Y%m%d_%H%M%S"), custom_name))
    os.mkdir('%s/%s' % (path,output_folder))
    with open('%s/__init__.py' % output_folder,'w') as f:
        pass
    
    print('Creating folder:', output_folder)
    
    '''
    Saving a copy of the settings and MCM files to the output folder
    '''
    from shutil import copyfile
    copyfile("settings.py", "%s/%s/settings.py" % (path,output_folder))
    copyfile(filename, "%s/%s/mcm.fac" % (path,output_folder))
    
    t_bound = t0+seconds_to_integrate #Maximum time to integrate to
    iters = 0 #the number of iterations that have been performed already (leave as 0)
    total_iter = math.ceil(int(t_bound-t0)/dt) #calculated to show the user how long is left   
    print('total iterations:', total_iter)
    
    
    '''
    calculate initial values
    '''
    h2o,rh = h2o_rh(t0,temp,rel_humidity,numba_exp)   
    
    species,ppool,rate_numba,reactions_numba=import_all(filename) #import from MCM download
    
    pi = 4.0*numba_arctan(1.0) #for photolysis and some rates
    
    # dictionary for evaluating the reaction rates
    calc_dict={'M':M,
           'numba_exp':numba_exp,
           'temp':temp,
           'numba_log10':numba_log10,
           'numba_sqrt':numba_sqrt,
           'H2O':h2o,
           'PI':pi,
           'HMIX':HMIX,
           'numba_abs':numba_abs}
    
    calc_dict.update(const_dict) # add constants from settings to calc_dict
    
    '''
    Particles
    '''
    particle_species=[]
    if particles == True:
        #if the full MCM is not being used then the calcuations for tsp and anything involving tsp
        #will fail so particles can only be used with the full MCM at the moment 04/2020
        particle_species, particle_reactions, particle_vap_dict, part_calc_dict = particle_import()
        species = species + particle_species #add particle species to species list
        reactions_numba = reactions_check(reactions_numba,particle_reactions,species)
        rate_numba = rate_numba + [['kacid' , '1.5e-32*numba_exp(14770/temp)']]
        calc_dict.update(particle_vap_dict)
    
    '''
    Custom reactions and rates. Those not in the MCM download, the code does not
    check this so please make sure you are not adding any reactions that already 
    exist as it will just duplicate them.
    '''
    sums = []
    if custom == True:
        custom_filename="custom_input.txt"
        custom_rates, custom_reactions, custom_species, custom_RO2, sums = \
            custom_import(custom_filename,species)
        # Check that rates/constants/RO2 have not been added as species
        custom_species = [item for item in custom_species
                          if item not in (['RO2'] + list(calc_dict.keys()) +
                                          [name[0] for name in rate_numba])]
        species = species + custom_species
        rate_numba = rate_numba + custom_rates
        reactions_numba = reactions_numba + custom_reactions
        ppool = ppool + custom_RO2
        copyfile("custom_input.txt", "%s/%s/custom_input.txt" % (path,output_folder))
     
    '''
    INCHEM reactions and rates that are not included in MCM download.
    '''    
    if INCHEM_additional == True:
        from modules.inchem_chemistry import INCHEM_RO2, INCHEM_reactions, \
            INCHEM_rates, INCHEM_sums
        INCHEM_species = INCHEM_species_calc(INCHEM_reactions,species)
        species = species + INCHEM_species
        ppool = ppool + INCHEM_RO2
        reactions_numba = reactions_numba + INCHEM_reactions
        rate_numba = rate_numba + INCHEM_rates
        sums.extend(INCHEM_sums)
        '''
        Write INCHEM inputs to output folder for future reference
        '''
        with open("%s/%s/INCHEM_inputs.txt" % (path,output_folder), 'w') as f:
            f.write('Species:\n')
            for i in INCHEM_species:
                f.write("%s\n" % i)
            f.write('RO2 species:\n')
            for i in INCHEM_RO2:
                f.write("%s\n" % i)
            f.write('Summations:\n')
            for i in INCHEM_sums:
                f.write("%s\n" % i)
            f.write('Rates:\n')
            for i in INCHEM_rates:
                f.write("%s\n" % i)
            f.write('Reactions:\n')
            for i in INCHEM_reactions:
                f.write("%s\n" % i)
    
    '''
    Additional clean up, checking for summations from custom inputs
    '''
    summations = False
    if custom == True or INCHEM_additional == True:
        if len(sums) >= 1:
            summations = True
           
    reaction_number=[]
    for i in range(len(reactions_numba)): #for assigning within the master array
        reaction_number.append('r%s' % i)
        
    #if it's in the calc dict it shouldn't be calculated in any of the integrations
    for i in calc_dict.keys():
        if i in species: 
            species.remove(i)
        for j in rate_numba:
            if j[0] == i:
                rate_numba.remove(j)
                print("%s from custom_input.txt ignored. Defined elsewhere." % i)
    
    
    '''
    Photolysis
    
    The simulation works on solar time of day and does not require any input 
    of longitude. However, if a comparison is being done with measured data then
    the time_correct variable can be used to shift the photolysis a number of
    seconds in either direction, this will also shift the outdoor photolysis
    used in some of the outdoor concentration calculations. Not all of the
    outdoor concentrations rely on this method of calculation so adjustments
    must also be made to any other outdoor rates and thus I do not recommend 
    using this method. I would instead adjust the full output after the run.
    '''
    #calculations for determining solar position given time of year    
    day, month, year = map(int, date.split('-'))
    date = datetime.date(year, month, day)
    year_day = (date - datetime.date(year, 1, 1)).days + 1
    days_in_year = (datetime.date(year,12,31) - datetime.date(year, 1, 1)).days + 1
    radian = 180.0/pi
    dec = -23.45 * np.cos(((360/days_in_year)/radian)*(year_day+10))
    
    lat = lat/radian #conversion to radians
    dec = dec/radian #conversion to radians
    
    time_correct = 0 #adjustment for correction of solar time to measured time
    
    #calculations for spherical law of cosines for solar zenith angle
    sinld = numba_sin(lat)*numba_sin(dec)
    cosld = numba_cos(lat)*numba_cos(dec)
    
    photo_dict=Zixu_photolysis_compiled() #compiled equations
    
    #full dictionary of attenuation factors for different lights/glass
    light_dict=Zixu_photolysis(numba_abs,numba_exp) 
    
    
    indoor_photo_dict={**light_dict[light_type],**light_dict[glass]}
    indoor_photo_dict_off={**light_dict["off"],**light_dict[glass]}
    
    n = t0-((int(t0/86400))*86400) # keeps time within 24 hour cycle 
    
    # COSX and SECX involve local hour angles dependant on position and time
    # of year (latitude of location and declination of sun)
    lha = (1+((n-time_correct)/4.32E+4))*pi    # local hour angle, radians. Midnight start
    cosx = ((numba_cos(lha)*cosld)+sinld) # solar zenith angle
     
    # (http://mcm.leeds.ac.uk/MCM/parameters/photolysis_param.htt)
    # Keep cosx 0 or positive, we don't have negative photolysis
    if cosx <= 1E-30:
        cosx = 0.0  
        secx = 1.0E+30  
    else:  
        secx = 1.0 / (((cosx + numba_abs(cosx))/2)+1.0E-30)
    
    light_on_times = [[j * 3600 for j in i]for i in light_on_times] #conversion to seconds
    
    J_dict = {}
    for i in light_on_times:
        if i[0] <= t0 <= i[1]:
            indoor_photo_dict["cosx"] = cosx
            indoor_photo_dict["secx"] = secx
            photolysis_J(indoor_photo_dict,photo_dict,J_dict)
            break
        else:
            indoor_photo_dict_off["cosx"] = cosx
            indoor_photo_dict_off["secx"] = secx
            photolysis_J(indoor_photo_dict_off,photo_dict,J_dict)
    
    '''
    Outdoor species concentration calculations
    '''        
    out_calc_dict = {"numba_abs":numba_abs,
                     "numba_exp":numba_exp,
                     "numba_cos":numba_cos,
                     "numba_sin":numba_sin,
                     "n":n,
                     "cosx":cosx,
                     "secx":secx}
    
    #Outdoor dictionaries
    outdoor_dict=outdoor_rates(AER,particles,species)
    if diurnal == True:
        outdoor_dict_diurnal = outdoor_rates_diurnal(city)
        outdoor_rates_calc(outdoor_dict,outdoor_dict_diurnal,out_calc_dict)
        #diurnal rates will overide static rates if species shown in both
    
    '''
    Surface deposition
    '''
    surface_dict=surface_deposition(HMIX)
    for specie in species:
        if particles == True and specie in particle_species:
            surface_dict['%s_SURF' % specie]=0.004*HMIX
        elif '%s_SURF' % specie not in surface_dict.keys():
            surface_dict['%s_SURF' % specie]=0
    
    '''
    timed concentrations
    '''
    timed_dict={}
    if timed_emissions == True:
        for specie in species:
            timed_dict["%s_timed" % specie] = 0
        for key in timed_inputs:
            if key in species:
                for i in timed_inputs[key]:
                    if i[0] <= t0 <= i[1]:
                        timed_dict["%s_timed" % key] = i[2]
                        break
                    else:
                        timed_dict["%s_timed" % key] = 0
            else:
                print('%s not found in species list (timed input)' % key)
    
    '''
    importing initial concentrations
    '''
    density_dict,calc_dict = initial_conditions(initial_conditions_gas,M,species,\
                                                rate_numba,calc_dict,particles,\
                                                    initials_from_run,t0,path)
    density_dict['RO2']=ppool_density_calc(density_dict,ppool)
    
    #calculating t0 summations
    summations_dict={}
    if summations == True:
        summations_dict = summations_compile(sums)
        sums_dict={}
        sums_dict=summations_eval(summations_dict,density_dict,calc_dict)
        density_dict.update(sums_dict)
    
    if particles == True:
        particle_dict = particle_calcs(part_calc_dict,density_dict)
        density_dict.update(particle_dict)
    
    '''    
    saving initial conditions to an output directory for reference
    '''
    f = open("%s/%s/initial_concentrations.txt" % (path,output_folder),"w")
    for i in species:
        f.write('%s=%s ;\n' % (i,density_dict[i]))
    f.close()
    
    '''
    Calculating the reaction rates and compiling the master array of ODEs
    '''
    num_species=len(species) #some calculations ask for the number of species
    #better to calculate it once rather than every time
    
    reaction_compiled_dict=reaction_rate_compile(reactions_numba,reaction_number)
    reaction_rate_dict=reaction_eval(reaction_number,J_dict,calc_dict,density_dict,\
                                     dt,reaction_compiled_dict,outdoor_dict,\
                                         surface_dict,timed_dict)
    
    #creating the master array
    master_array_dict=master_calc(reactions_numba,species,reaction_number,particles,\
                                  particle_species,timed_emissions)
    
    #saving the master array to the output folder
    with open('%s/%s/master_array.pickle' % (path,output_folder),'wb') as handle:
        pickle.dump(master_array_dict,handle)
    
    #compiling the master array
    master_compiled=master_compiler(master_array_dict,species)
    
    #Create the jacobian and save it to the output folder
    write_jacobian_build(master_array_dict,species,output_folder,path)
    
    #import the jacobian function to create the jacobian dictionary which is a
    #dictionary of compiled calculations
    spec = importlib.util.spec_from_file_location("jac.jacobian_calc",\
                                                  "%s/%s/Jacobian.py" % (path,output_folder))
    jac = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(jac)
    #jac = importlib.import_module(path+output_folder+".Jacobian")
    dy_dy_dict=jac.jacobian_calc(species) 
    
    # reactivity and production calculations, details in the reactivity.py script
    reactivity_compiled, production_compiled = reactivity_summation(master_array_dict)
    reactivity_dict = {}
    reactivity_calc(reactivity_dict,reactivity_compiled,reaction_rate_dict,calc_dict,\
                    density_dict)
    production_dict = {}
    production_calc(production_dict,production_compiled,reaction_rate_dict,calc_dict,\
                    density_dict)
     
    '''
    #Integration
    '''
    #Create an array of concentrations to be updated during the integration. This
    #can then be used to repopulate the dictionaries with updated values
    y0=[[] for i in range(num_species)]     
    for i in range(num_species):    
        y0[i]=density_dict[species[i]]
    
    #Create arrays for storing the output. 
    n_new=np.array([[y0[i]] for i in range(num_species)])
    dt_out=np.array([])
    dt_out=np.append(dt_out,int(t0)) 
    iter_time_tot=[timing.time()-start_time]
    calculated_output_tot = {}
    calculated_output_tot['RO2'] = [density_dict['RO2']]
    if particles == True:
        calculated_output_tot['tsp'] = [density_dict['tsp']]
        calculated_output_tot['acidsum'] = [density_dict['acidsum']]
        calculated_output_tot['tspx'] = [density_dict['tspx']]
        calculated_output_tot['mwomv'] = [density_dict['mwomv']]
    for i in reactivity_dict:
        calculated_output_tot[i] = [reactivity_dict[i]]
    for i in production_dict:
        calculated_output_tot[i] = [production_dict[i]]
    for i in J_dict:
        calculated_output_tot[i] = [J_dict[i]]
    for i in outdoor_dict:
        calculated_output_tot[i] = [outdoor_dict[i]]
    if summations == True:
        for i in sums_dict:
            calculated_output_tot[i] = [density_dict[i]]
    
    ret=1 #if ret=2 success. ODE return code
    
    print('Integration starting')
    
    with threadpool_limits(limits=4, user_api='blas'): #limits threads used by integration
        while iters < total_iter and ret > 0:  
                n_new_in = n_new[:,-1]
                dt_out_in = dt_out[-1]
                dt_out_temp,n_new_temp,iters,ret,iter_time,calculated_output=\
                    integrate_function(iters,t_bound,n_new_in,dt_out_in,ret,save_rate,\
                                       num_species,total_iter,dt)
                dt_out=np.append(dt_out,dt_out_temp)
                n_new=np.append(n_new,n_new_temp,axis=1)
                iter_time_tot.extend(iter_time)
                for x in calculated_output:
                    calculated_output_tot[x].extend(calculated_output[x])
                
    output_data = pd.DataFrame(np.transpose(n_new),columns=species,index=dt_out)
    output_data = output_data.join(pd.DataFrame(calculated_output_tot,index=dt_out))
    
    integration_times = pd.DataFrame(np.transpose(iter_time_tot),index=dt_out)
    
    end_time=timing.time()
    delta = datetime.timedelta(seconds=(end_time-start_time))
    print('Return code:', ret) #ODE return code
    print('Total run time =', str(delta - datetime.timedelta(microseconds=delta.microseconds)))
    
    print('Saving output')
    
    # saves all of the output data to the output folder
    with open('%s/%s/out_data.pickle' % (path,output_folder),'wb') as handle:
        pickle.dump(output_data,handle)  
        
    #saves times for integrating each time step to a csv, useful for
    #analysing slow points in the system
    integration_times.to_csv("%s/%s/integration_times.csv" % (path,output_folder))
    print('Output saved')
    if output_graph == True:
        # creates and saves a simple graph of the set species from settings
        import matplotlib.pyplot as plt
        from itertools import cycle
        plt.figure(dpi=600,figsize=(8,4))
        colour=iter(plt.cm.gist_rainbow(np.linspace(0,1,len(output_species))))
        linestyle=cycle(['solid','dotted','dashed','dashdot'])
        for x in output_species:
            c=next(colour)
            l=next(linestyle)
            plt.plot(output_data.index/3600,output_data[x],label=x,c=c,linestyle=l)
        plt.yscale("log")
        plt.legend(loc='upper center', bbox_to_anchor=(0.45, -0.15),
          fancybox=True, shadow=True, ncol=6)
        plt.xlabel("Time of day (hours)")
        plt.ylabel("Concentration (molecules/cm\N{SUPERSCRIPT THREE})")
        plt.savefig('%s/%s/graph.png' % (path,output_folder), bbox_inches='tight')

        # additionally saves a csv of these set species for easy analysis
        output_data.to_csv("%s/%s/output.csv" % (path,output_folder), columns = output_species)

        plt.show()

    return None