# NOTE: For some of the vapour pressure values, you need to perform a boiling point estimation first # It is therefore wise to do this initially # 2a) Boiling points [(K)] boiling_point_dict = collections.defaultdict(lambda: collections.defaultdict()) for smiles in smiles_array: boiling_point_dict[smiles][ 'joback_and_reid'] = boiling_points.joback_and_reid( Pybel_object_dict[smiles]) boiling_point_dict[smiles][ 'stein_and_brown'] = boiling_points.stein_and_brown( Pybel_object_dict[smiles]) boiling_point_dict[smiles]['nannoolal'] = boiling_points.nannoolal( Pybel_object_dict[smiles]) # 2b) Vapour pressures [log10 (atm) at a specific temperature] # For those vapour pressure methods that require a boiling point, we have 3D dictionaries vapour_pressure_dict_BP = collections.defaultdict( lambda: collections.defaultdict(lambda: collections.defaultdict())) vapour_pressure_dict = collections.defaultdict( lambda: collections.defaultdict()) temperature = 298.15 for smiles in smiles_array: vapour_pressure_dict_BP[smiles]['VP_Nannoolal'][ 'BP_Nannoolal'] = vapour_pressures.nannoolal( Pybel_object_dict[smiles], temperature,
def Pure_component1(num_species, species_dict, species_dict2array, Pybel_object_dict, SMILES_dict, temp, vp_method, bp_method, critical_method, density_method, ignore_vp, vp_cutoff): """ This function calculates properties that dictate gas-to-particle partitioning inputs: • num_species - number of compounds used in the calculations • species_dict - dict holding names of all compounds • species_dict2array - dict that mapes compound name to array index • Pybel_object_dict - dict holding all Pybel objects of each compound [used in UManSysProp] • SMILES_dict - dict containing all SMILES strings of each compound • temp - temperature [K] • vp_method - choice of vapour pressure method [see calling function and/or UManSysProp] • bp_method - choice of boiling point method [see calling function and/or UManSysProp] • critical_method - choice of criticl property method [see calling function and/or UManSysProp] • density_method - choice of density method [see calling function and/or UManSysProp] • ignore_vp - flag to ignore compounds with vapour pressure above a defined value given by 'vp_cutoff' • vp_cutoff - log10 value of vapour pressure above which the odel will ignore partitioning outputs: • return_dict['y_density_array']=y_density_array - density of each condensing compound • return_dict['y_mw']=y_mw - molecular weight of each condensing compound • return_dict['sat_vp']=sat_vp - saturation vapour pressure of each condensing compound • return_dict['Delta_H']=Delta_H - enthalpy of vapourisation of each condensing compound • return_dict['Latent_heat_gas']=Latent_heat_gas - latent hear of condensation of each condensing compound • return_dict['ignore_index']=ignore_index - array that holds information on which compounds to ignore for partitioning • return_dict['ignore_index_fortran']=ignore_index_fortran - dense array that holds information on which compounds to ignore for partitioning in Fortran functions • return_dict['include_index']=include_index - array that holds information on which compounds to include for partitioning """ y_density_array = [1000.0] * num_species y_mw = [200.0] * num_species o_c = [0.0] * num_species h_c = [0.0] * num_species sat_vp = [100.0] * num_species sat_vp_org = dict( ) #Recorded seperately and will not include the extension for water. This is used for any checks with equilibrium partitioning predictions y_gas = [0.0] * num_species species_step = 0 ignore_index = [ ] #Append to this to identify any compounds that do not have automated calculation of properties. OR are species that will be ignored in partitioning include_index = [] include_dict = dict() ignore_index_fortran = numpy.zeros((num_species), ) print("Calculating component properties using UManSysProp") # Which boiling point method has been chosen boiling_point = { 'joback_and_reid': boiling_points.joback_and_reid, 'stein_and_brown': boiling_points.stein_and_brown, 'nannoolal': boiling_points.nannoolal, }[bp_method] vapour_pressure = { 'nannoolal': vapour_pressures.nannoolal, 'myrdal_and_yalkowsky': vapour_pressures.myrdal_and_yalkowsky, # Evaporation doesn't use boiling point 'evaporation': lambda c, t, b: vapour_pressures.evaporation(c, t), }[vp_method] # Which density method has been chosen critical_property = { 'nannoolal': critical_properties.nannoolal, 'joback_and_reid': critical_properties.joback_and_reid, }[critical_method] liquid_density = { 'girolami': lambda c, t, p: liquid_densities.girolami(c), 'schroeder': liquid_densities.schroeder, 'le_bas': liquid_densities.le_bas, 'tyn_and_calus': liquid_densities.tyn_and_calus, }[density_method] for compound in species_dict.values(): if compound in SMILES_dict.keys(): # Calculate a boiling point with Nanoolal for density methods #pdb.set_trace() b1 = boiling_points.nannoolal( Pybel_object_dict[SMILES_dict[compound]]) y_density_array[species_dict2array[compound]] = liquid_density( Pybel_object_dict[SMILES_dict[compound]], temp, critical_property(Pybel_object_dict[SMILES_dict[compound]], b1)) * 1.0E3 #y_density_array[species_dict2array[compound]]=(liquid_densities.girolami(Pybel_object_dict[SMILES_dict[compound]])*1.0E3) #Convert from g/cc to kg/m3 #y_density_array.append(1400.0) y_mw[species_dict2array[compound]] = ( Pybel_object_dict[SMILES_dict[compound]].molwt) #In the following you will need to select which vapour pressure method you like. groups_dict = groups.composition( Pybel_object_dict[SMILES_dict[compound]]) o_c[species_dict2array[ compound]] = groups_dict['O'] / groups_dict['C'] h_c[species_dict2array[ compound]] = groups_dict['H'] / groups_dict['C'] # Calculate boiling point b = boiling_point(Pybel_object_dict[SMILES_dict[compound]]) sat_vp[species_dict2array[compound]] = vapour_pressure( Pybel_object_dict[SMILES_dict[compound]], temp, b) if ignore_vp is True: if sat_vp[species_dict2array[compound]] > vp_cutoff: ignore_index.append(species_dict2array[compound]) ignore_index_fortran[species_dict2array[compound]] = 1.0 else: include_index.append(species_dict2array[compound]) else: include_index.append(species_dict2array[compound]) #sat_vp[species_dict2array[compound]]=(vapour_pressures.nannoolal(Pybel_object_dict[SMILES_dict[compound]], temp, boiling_points.nannoolal(Pybel_object_dict[SMILES_dict[compound]]))) #sat_vp_org[Pybel_object_dict[SMILES_dict[compound]]]=vapour_pressures.nannoolal(Pybel_object_dict[SMILES_dict[compound]], temp, boiling_points.nannoolal(Pybel_object_dict[SMILES_dict[compound]])) #y_gas.append(concentration_array[species_step]) else: ignore_index.append(species_dict2array[compound]) ignore_index_fortran[species_dict2array[compound]] = 1.0 species_step += 1 Delta_H = [ 0.0 ] * num_species #Dont prescribe an enthalpy of vaporisation. For non-VBS model simulations with varying temperature, we recalculate using GCM Latent_heat_gas = [ 0.0 ] * num_species # - Still need to account for any latent heat release [Future work] return_dict = dict() return_dict['y_density_array'] = y_density_array return_dict['y_mw'] = y_mw return_dict['o_c'] = o_c return_dict['h_c'] = h_c return_dict['sat_vp'] = sat_vp return_dict['Delta_H'] = Delta_H return_dict['Latent_heat_gas'] = Latent_heat_gas return_dict['ignore_index'] = ignore_index return_dict['ignore_index_fortran'] = ignore_index_fortran return_dict['include_index'] = include_index return return_dict
from umansysprop import boiling_points from umansysprop import vapour_pressures from umansysprop import liquid_densities NA = si.Avogadro # Avogadro's number (molecules/mol) # vapour pressures of components, excluding water and core at end Psat = np.zeros((1, num_comp-2)) TEMP = 298.15 # temperature (K) for i in range(num_comp-2): # component loop # vapour pressure (log10(atm)) (# eq. 6 of Nannoolal et al. (2008), with dB of # that equation given by eq. 7 of same reference) Psat[0, i] = ((vapour_pressures.nannoolal(Pybel_objects[i], TEMP, boiling_points.nannoolal(Pybel_objects[i])))) # convert from list to array y_mw = np.array((y_mw)) # convert vapour pressures in log10(atm) to saturation concentrations in ug/m3 # using eq. 1 of O'Meara et al. 2014 Psat = 1.e6*y_mw[0:-2].reshape(1, -1)*(10**(Psat))/(8.2057e-5*TEMP) # convert particle-phase concentrations from molecules/cc to ug/m3 to # be comparable with total secondary particulate matter concentration # isolate just particle-phase concentrations (molecules/cc) # note just one size bin in this simulation and want to exclude water # and core SPMi = y[:, num_comp:(num_comp*(num_sb)-2)] # convert to mol/cc
def prop_calc(rel_SMILES, Pybel_objects, TEMP, H2Oi, num_comp, Psat_water, vol_Comp, volP, testf, corei, pconc, umansysprop_update, core_dens, spec_namelist, ode_gen_flag, nuci, nuc_comp, num_asb, dens_comp, dens, seed_name, y_mw): # inputs: ------------------------------------------------------------ # rel_SMILES - array of SMILE strings for components # (omitting water and core, if present) # Pybel_objects - list of Pybel objects representing the components in rel_SMILES # (omitting water and core, if present) # TEMP - temperature (K) in chamber at time function called # vol_Comp - names of components (corresponding to those in chemical scheme file) # that have vapour pressures manually set in volP # testf - flag for whether in normal mode (0) or testing mode (1) # corei - index of seed particle component # pconc - initial number concentration of particles (# particles/cm3 (air)) # umansysprop_update - marker for cloning UManSysProp so that latest version used # core_dens - density of core material (g/cm3 (liquid/solid density)) # spec_namelist - list of component names in chemical equation file # ode_gen_flag - whether or not called from middle or ode_gen # nuci - index of nucleating component # nuc_comp - name of nucleating component # num_asb - number of actual size bins (excluding wall) # dens_comp - chemical scheme names of components with manually assigned # densities # dens - manually assigned densities (g/cm3) # seed_name - chemical scheme name(s) of component(s) comprising seed particles # y_mw - molar mass of components (g/mol) # ------------------------------------------------------------ if (testf == 1): return (0, 0, 0) # return dummies cwd = os.getcwd() # address of current working directory # if update required, note this update flag is set when model variables are checked if (umansysprop_update == 1): # download latest version of umansysprop # check if there is an existing umansysprop folder if os.path.isdir(cwd + '/umansysprop'): def handleRemoveReadonly(func, path, exc): excvalue = exc[1] if not os.access(path, os.W_OK): # Is the error an access error ? os.chmod(path, stat.S_IWUSR) func(path) else: raise # remove existing folder, onerror will change permission of directory if # needed shutil.rmtree(cwd + '/umansysprop', ignore_errors=False, onerror=handleRemoveReadonly) git_url = 'https://github.com/loftytopping/UManSysProp_public.git' Repo.clone_from(git_url, (cwd + '/umansysprop')) # point to umansysprop folder sys.path.insert(1, (cwd + '/umansysprop')) # address for updated version from umansysprop import boiling_points from umansysprop import vapour_pressures from umansysprop import liquid_densities NA = si.Avogadro # Avogadro's number (molecules/mol) y_dens = np.zeros((num_comp, 1)) # components' liquid density (kg/m3) # vapour pressures of components, ensures any seed component called # core has zero vapour pressure Psat = np.zeros((1, num_comp)) # oxygen:carbon ratio of components OC = np.zeros((1, num_comp)) if (ode_gen_flag == 0): # estimate densities if called from middle for i in range(num_comp): # loop through components # density estimation --------------------------------------------------------- if (i == H2Oi): # liquid-phase density of water y_dens[i] = 1. * 1.e3 # (kg/m3 (particle)) continue # core component properties if (i == corei[0]): # density of core y_dens[i] = core_dens * 1.e3 # core density (kg/m3 (particle)) continue if rel_SMILES[ i] == '[HH]': # omit H2 as unliked by liquid density code # liquid density code does not like H2, so manually input kg/m3 y_dens[i] = 1.e3 else: # density (convert from g/cm3 to kg/m3) y_dens[i] = liquid_densities.girolami(Pybel_objects[i]) * 1.e3 # ---------------------------------------------------------------------------- # account for any manually assigned component densities (kg/m3) if (len(dens_comp) > 0 and ode_gen_flag == 0): for i in range(len(dens_comp)): # index of component in list of components dens_indx = spec_namelist.index(dens_comp[i]) y_dens[dens_indx] = dens[i] # for records (e.g. plotting volatility basis set), # estimate and list the pure component saturation vapour # pressures (Pa) at standard temperature (298.15 K), though note that # any manually assigned vapour pressures overwrite these later in # this module Psat_Pa_rec = np.zeros((num_comp)) # estimate vapour pressures (log10(atm)) and O:C ratio # note when the O:C ratio and vapour pressure at 298.15 K are # combined, one can produce the two-dimensional volatility # basis set, as shown in Fig. 1 of https://doi.org/10.5194/acp-20-1183-2020 for i in range(num_comp): if (i == corei[0]): # if this component is 'core' # core component not included in Pybel_objects # assign an assumed O:C ratio of 0. OC[0, i] = 0. # continuing # here means its vapour pressure is 0 Pa, which is fine; if a # different vapour pressure is specified it is accounted for below continue # water vapour pressure already given by Psat_water (log10(atm)) # and water not included in Pybel_objects if (i == H2Oi): Psat[0, i] = Psat_water if (TEMP == 298.15): Psat_Pa_rec[i] = Psat[0, i] else: [_, Psat_Pa_rec[i], _] = water_calc(298.15, 0.5, si.N_A) OC[0, i] = 0. continue if (spec_namelist[i] == 'O3'): # vapour pressure of ozone from https://doi.org/10.1063/1.1700683 Psat[0, i] = np.log10( (8.25313 - (814.941587 / TEMP) - 0.001966943 * TEMP) * 1.31579e-3) if (TEMP == 298.15): Psat_Pa_rec[i] = Psat[0, i] else: Psat_Pa_rec[i] = np.log10( (8.25313 - (814.941587 / 298.15) - 0.001966943 * 298.15) * 1.31579e-3) OC[0, i] = 0. continue # use EVAPORATION method for vapour pressure (log10(atm)) of HOMs if 'api_' in spec_namelist[i] or 'API_' in spec_namelist[i]: Psat[0, i] = ((vapour_pressures.myrdal_and_yalkowsky( Pybel_objects[i], TEMP, boiling_points.nannoolal(Pybel_objects[i])))) if (TEMP == 298.15): Psat_Pa_rec[i] = Psat_Pa[0, i] else: Psat_Pa_rec[i] = ((vapour_pressures.myrdal_and_yalkowsky( Pybel_objects[i], 298.15, boiling_points.nannoolal(Pybel_objects[i])))) else: # vapour pressure (log10(atm)) (eq. 6 of Nannoolal et al. (2008), with dB of # that equation given by eq. 7 of same reference) Psat[0, i] = ((vapour_pressures.nannoolal( Pybel_objects[i], TEMP, boiling_points.nannoolal(Pybel_objects[i])))) if (TEMP == 298.15): Psat_Pa_rec[i] = Psat_Pa[0, i] else: Psat_Pa_rec[i] = ((vapour_pressures.nannoolal( Pybel_objects[i], 298.15, boiling_points.nannoolal(Pybel_objects[i])))) # O:C ratio determined from SMILES string if (rel_SMILES[i].count('C') > 0): OC[0, i] = (rel_SMILES[i].count('O')) / (rel_SMILES[i].count('C')) else: OC[0, i] = 0. ish = (Psat == 0.) Psat = (10.**Psat) * 101325. # convert to Pa from atm Psat_Pa_rec = (10.**Psat_Pa_rec) * 101325 # convert to Pa from atm # retain low volatility where wanted following unit conversion Psat[ish] = 0. # list to remember which components have vapour pressures specified vi_rec = [] # manually assigned vapour pressures (Pa) if (len(vol_Comp) > 0 and ode_gen_flag == 0): for i in range(len(vol_Comp)): # index of component in list of components vol_indx = spec_namelist.index(vol_Comp[i]) Psat[0, vol_indx] = volP[i] Psat_Pa_rec[vol_indx] = volP[i] vi_rec.append(vol_indx) # ensure if nucleating component is core that it is involatile if (nuc_comp == 'core'): Psat[0, nuci] = 0. Psat_Pa = np.zeros( (1, num_comp)) # for storing vapour pressures in Pa (Pa) Psat_Pa[0, :] = Psat[0, :] # convert saturation vapour pressures from Pa to # molecules/cm3 (air) using ideal # gas law, R has units cc.Pa/K.mol Psat = Psat * (NA / ((si.R * 1.e6) * TEMP)) # now, in preparation for ode solver, repeat over number of size bins if (num_asb > 0): Psat = np.repeat(Psat, num_asb, axis=0) return (Psat, y_dens, Psat_Pa, Psat_Pa_rec, OC)
def volat_calc(spec_list, Pybel_objects, TEMP, H2Oi, num_speci, Psat_water, vol_Comp, volP, testf, corei, seed_name, pconc, umansysprop_update, core_dens, spec_namelist, ode_gen_flag, nuci, nuc_comp): # inputs: ------------------------------------------------------------ # spec_list - array of SMILE strings for components # (omitting water and core, if present) # Pybel_objects - list of Pybel objects representing the species in spec_list # (omitting water and core, if present) # TEMP - temperature (K) in chamber at time function called # vol_Comp - names of components (corresponding to those in chemical scheme file) # that have vapour pressures manually set in volP # testf - flag for whether in normal mode (0) or testing mode (1) # corei - index of seed particle component # seed_name - name(s) of components(s) comprising seed particles # pconc - initial number concentration of particles (#/cc (air)) # umansysprop_update - marker for cloning UManSysProp so that latest version used # core_dens - density of core material (g/cc (liquid/solid density)) # spec_namelist - list of components' names in chemical equation file # ode_gen_flag - whether or not called from front or ode_gen # nuci - index of nucleating component # nuc_comp - name of nucleating component # ------------------------------------------------------------ if testf==1: return(0,0,0) # return dummies cwd = os.getcwd() # address of current working directory if umansysprop_update == 1: print('Cloning latest version of UManSysProp in volat_calc module') # download latest version of umansysprop # check if there is an existing umansysprop folder if os.path.isdir(cwd + '/umansysprop'): def handleRemoveReadonly(func, path, exc): excvalue = exc[1] if not os.access(path, os.W_OK): # Is the error an access error ? os.chmod(path, stat.S_IWUSR) func(path) else: raise # remove existing folder, onerror will change permission of directory if # needed shutil.rmtree(cwd + '/umansysprop', ignore_errors=False, onerror=handleRemoveReadonly) git_url = 'https://github.com/loftytopping/UManSysProp_public.git' Repo.clone_from(git_url, (cwd + '/umansysprop')) # point to umansysprop folder sys.path.insert(1, (cwd + '/umansysprop')) # address for updated version from umansysprop import boiling_points from umansysprop import vapour_pressures from umansysprop import liquid_densities NA = si.Avogadro # Avogadro's number (molecules/mol) y_dens = np.zeros((num_speci, 1)) # components' liquid density (kg/m3) Psat = np.zeros((num_speci, 1)) # species' vapour pressure if ode_gen_flag == 0: # estimate densities for i in range (num_speci): # density estimation --------------------------------------------------------- if i == H2Oi: y_dens[i] = 1.0*1.0E3 # (kg/m3 (particle)) continue # core properties if (i == corei[0]): y_dens[i] = core_dens*1.e3 # core density (kg/m3 (particle)) continue # nucleating component density, if component is core (kg/m3 (particle)) if i == nuci and nuc_comp[0] == 'core': y_dens[i] = 1.0*1.0E3 continue if spec_list[i] == '[HH]': # omit H2 as unliked by liquid density code # liquid density code does not like H2, so manually input kg/m3 y_dens[i] = 1.0e3 else: # density (convert from g/cc to kg/m3) y_dens[i] = liquid_densities.girolami(Pybel_objects[i])*1.0E3 # ---------------------------------------------------------------------------- # estimate vapour pressures (log10(atm)) for i in range (num_speci): if (i == corei[0]): # if this core component continue # core component not included in Pybel_objects if i == nuci and nuc_comp[0] == 'core': continue # core component not included in Pybel_objects # water vapour pressure already given by Psat_water (log10(atm)) if i == H2Oi: Psat[i] = Psat_water continue # water not included in Pybel_objects # vapour pressure (log10 atm) (# eq. 6 of Nannoolal et al. (2008), with dB of # that equation given by eq. 7 of same reference) Psat[i] = ((vapour_pressures.nannoolal(Pybel_objects[i], TEMP, boiling_points.nannoolal(Pybel_objects[i])))) ish = Psat==0.0 Psat = (np.power(10.0, Psat)*101325.0) # convert to Pa from atm # retain low volatility where wanted Psat[ish] = 0.0 # manually assigned vapour pressures (Pa) if len(vol_Comp)>0 and ode_gen_flag==0: for i in range (len(vol_Comp)): # index of component in list of components vol_indx = spec_namelist.index(vol_Comp[i]) Psat[vol_indx, 0] = volP[i] # ensure if nucleating component is core that it is involatile if nuc_comp == 'core': Psat[nuci, 0] = 0.0 Psat_Pa = np.zeros((len(Psat), 1)) # for storing vapour pressures in Pa (Pa) Psat_Pa[:, 0] = Psat[:, 0] # convert saturation vapour pressures from Pa to molecules/cc (air) using ideal # gas law, R has units cc.Pa/K.mol Psat = Psat*(NA/((si.R*1.e6)*TEMP)) return Psat, y_dens, Psat_Pa
def volat_calc(spec_list, Pybel_objects, TEMP, H2Oi, num_speci, Psat_water, voli, volP, testf, corei): # ------------------------------------------------------------ # inputs: # spec_list - array of SMILE strings for components # (omitting water and core, if present) # Pybel_objects - list of Pybel objects representing the species in spec_list # (omitting water and core, if present) # testf - flag for whether in normal mode (0) or testing mode (1) # corei - index of seed particle component # ------------------------------------------------------------ if testf == 1: return (0, 0, 0) # return dummies # if voli is in relative index (-n), change to absolute for i in range(len(voli)): if voli[i] < 0: voli[i] = num_speci + voli[i] NA = si.Avogadro # Avogadro's number (molecules/mol) y_dens = np.zeros((num_speci, 1)) # components' liquid density (kg/m3) Psat = np.zeros((num_speci, 1)) # species' vapour pressure for i in range(num_speci): # omit estimation for water as it's value is already given in Psat_water # (log10(atm)) if i == H2Oi: Psat[i] = Psat_water y_dens[i] = 1.0 * 1.0E3 # (kg/m3 (particle)) continue if i == corei: # core properties (only used if seed particles present) y_dens[i] = 1.77 * 1.0E3 # core density (kg/m3 (particle)) continue if spec_list[i] == '[HH]': # omit H2 as unliked by liquid density code # liquid density code does not like H2, so manually input kg/m3 y_dens[i] = 1.0e3 else: # density (convert from g/cc to kg/m3) y_dens[i] = liquid_densities.girolami(Pybel_objects[i]) * 1.0E3 # vapour pressure (log10 atm) (# eqn. 6 of Nannoolal et al. (2008), with dB of # that equation given by eq. 7) Psat[i] = ((vapour_pressures.nannoolal( Pybel_objects[i], TEMP, boiling_points.nannoolal(Pybel_objects[i])))) Psat = (np.power(10.0, Psat) * 101325.0) # convert to Pa # manually assigned vapour pressures (Pa) (including seed component (if applicable)) if len(voli) > 0: Psat[voli, 0] = volP Psat_Pa = np.zeros( (len(Psat), 1)) # for storing vapour pressures in Pa (Pa) Psat_Pa[:, 0] = Psat[:, 0] # convert saturation vapour pressures from Pa to molecules/cc (air) using ideal # gas law, R has units cc.Pa/K.mol Psat = Psat * (NA / (8.3144598e6 * TEMP)) return Psat, y_dens, Psat_Pa