def totalQ(Ta, Td, Pa, Pd, ya, yd, iso_Ta, iso_Td,cp, vf, ms, qa, wcvf_a, iso_df): """ Returns [0] Q_{thermal} computed from eq. 1 in 10.1039/c4ee02636e (kJ/mol) [1] Qs, sensible heat (kJ/mol) [2] Qd, enthalpy of desorption (kJ/mol, defined as positive) [3] WCv, mass CO2 per m3 of bed (kg/m3) [4] pur, final CO2 purity (-) """ CO2_KGMOL = 0.044 # kg/mol # extract or compute the adsorption conditions if not already stored if not qa: logging.debug("Ta = {:.3e}, Pa = {:.3e}, ya = {:.3e}, yd = {:.3e}".format(Ta,Pa,ya,yd)) wcvf_a['CO_2'] = wcvf(ya, Ta, Pa, vf, ms) wcvf_a['N_2'] = wcvf(1-ya, Ta, Pa, vf, ms) # gas uptake @ adsorption qa['CO_2'], qa['N_2'] = pyiast.iast( np.array([ya, 1-ya]) * Pa, [iso_Ta['CO_2'], iso_Ta['N_2']], verboseflag=False, warningoff=True, # no complain about extrapolation ) logging.debug("qa['CO_2']: {:.3e}, qa['N_2']: {:.3e}".format(qa['CO_2'], qa['N_2'])) # Compute desorption conditions wcvf_d = {} wcvf_d['CO_2'] = wcvf(yd, Td, Pd, vf, ms) wcvf_d['N_2'] = wcvf(1-yd, Td, Pd, vf, ms) qd, qds = {}, {} # gas uptake @ desorption qd['CO_2'], qd['N_2'] = pyiast.iast( np.array([yd, 1-yd]) * Pd, [iso_Td['CO_2'], iso_Td['N_2']], verboseflag=False, warningoff=True, # no complain about extrapolation adsorbed_mole_fraction_guess=[0.99999999, 0.00000001] ) logging.debug("qd['CO_2']: {:.3e}, qd['N_2']: {:.3e}".format(qd['CO_2'], qd['N_2'])) wc, wct = {}, {} for m in ['CO_2', 'N_2']: wc[m] = qa[m] - qd[m] # working capacity in the adsorbent (mol_component/kg_ADS) wct[m] = wc[m] + wcvf_a[m] - wcvf_d[m] # working capacity total = adsorbent + void WCv = wct['CO_2'] * ms * CO2_KGMOL # kgCO2/m3 logging.debug("wct['CO_2'] = {:.3e} => WCv = {:.3e}".format(wct['CO_2'], WCv)) if WCv < 0: logging.debug("NEGATIVE wct['CO_2']") return np.nan, np.nan, np.nan, WCv, np.nan else: # Compute volumetric working capacity, and final purity pur = wct['CO_2'] / (wct['CO_2'] + wct['N_2']) logging.debug("WCv = {:.3e}, pur = {:.3e}".format(WCv, pur)) # Compute Q_{thermal} (Qt) = sensible_heat (Qs) + desorption_enthalpy (Qd) Qs = cp * ms * (Td - Ta) / WCv # J/kgCO2 Qd = 0 for m in ['CO_2', 'N_2']: # Get the enthalpy from the input data, at the closest conditions to desorption (assuming constant HoA with loading) idx_closest_Pd = (iso_df[m]['pressure(Pa)']-Pd).abs().argsort()[0] h = - iso_df[m].loc[idx_closest_Pd]['HoA(kJ/mol)'] # positive number, we have to provide this enthalpy Qd += h * 1e3 * wc[m] * ms / WCv # J/kgCO2 Qt = Qs + Qd # J/kgCO2 logging.debug("Qs = {:.3e}, Qd = {:.3e}, Qt = {:.3e}".format(Qs, Qd, Qt)) return Qt, Qs, Qd, WCv, pur
def iast_loading(partial_pressures, i): """ Calculate loading of component i according to IAST partial_pressures: Array, partial pressures of each component i: component in the mixture """ component_loadings = pyiast.iast(partial_pressures, isotherms) return component_loadings[i]
def equations(p): x = np.array(p) ads = list( pyiast.iast(p, PureIsotherms, verboseflag=False, warningoff=True)) return ([ phi * x_i / RT + (1. - phi) * rho * ads_i - Ntot_i for (x_i, ads_i, Ntot_i) in zip(x, ads, Ntot) ])
fill_value=df_ch3ch3["Loading(mmol/g)"].max()) pyiast.plot_isotherm(ch3ch3_isotherm) # ## Perform IAST at same mixture conditions as binary GCMC simulations # In[9]: n_mixtures = df_mixture.shape[0] iast_component_loadings = np.zeros((2, n_mixtures)) # store component loadings here for i in range(n_mixtures): y_ethane = df_mixture['y_ethane'].iloc[i] partial_pressures = 65.0 * np.array([y_ethane, 1.0 - y_ethane]) iast_component_loadings[:, i] = pyiast.iast(partial_pressures, [ch3ch3_isotherm, ch4_isotherm], verboseflag=False) # ## Compare pyIAST predictions to binary GCMC # In the following plot, the points are the dual component GCMC simulation loadings at the respective ethane mole fraction in the gas phase. The lines are the result of the IAST calculation. The IAST calculations match the binary GCMC simulations very well. # In[10]: fig = plt.figure() plt.scatter(df_mixture['y_ethane'], df_mixture['EthaneLoading(mmol/g)'], color=color_key["ethane"], label='Ethane', s=50) plt.scatter(df_mixture['y_ethane'], df_mixture['MethaneLoading(mmol/g)'], color=color_key["methane"], label='Methane', s=50)
pressure_key="Pressure(bar)", fill_value=df_ch3ch3["Loading(mmol/g)"].max()) pyiast.plot_isotherm(ch3ch3_isotherm) # ## Perform IAST at same mixture conditions as binary GCMC simulations # In[9]: n_mixtures = df_mixture.shape[0] iast_component_loadings = np.zeros( (2, n_mixtures)) # store component loadings here for i in range(n_mixtures): y_ethane = df_mixture['y_ethane'].iloc[i] partial_pressures = 65.0 * np.array([y_ethane, 1.0 - y_ethane]) iast_component_loadings[:, i] = pyiast.iast( partial_pressures, [ch3ch3_isotherm, ch4_isotherm], verboseflag=False) # ## Compare pyIAST predictions to binary GCMC # In the following plot, the points are the dual component GCMC simulation loadings at the respective ethane mole fraction in the gas phase. The lines are the result of the IAST calculation. The IAST calculations match the binary GCMC simulations very well. # In[10]: fig = plt.figure() plt.scatter( df_mixture['y_ethane'], df_mixture['EthaneLoading(mmol/g)'], color=color_key["ethane"], label='Ethane', s=50)
def run(test=False): # Set to False if you **do not** want to recalculate pure gas adsorption isotherms is_simulate_loadings = False loadings_file = 'loadings.dat' # Option to draw the isotherms: it is either **'ToFile'** or **'ToScreen'** (case insensitive). # Any other value will be interpreted as no graphics graphing = 'none' # Gas names as they will be referred through simulations gas_names = ['ch4', 'co2'] # Corresponding mole fractions of gases that will allow us to calculate their partial pressures through the Dalton's law mol_frac = [0.5, 0.5] # Calibrated previously functional forms of chemical potentials of gases for GCMC simulations as a functions of pressure chem_pots = [ lambda x: 2.4153 * numpy.log(x) - 36.722, lambda x: 2.40 * numpy.log(x) - 40.701 ] # Root directory for some data (For PySIMM examples it is ) data_dir = osp.join('..', '09_cassandra_simulations', 'gcmc') # Setup of adsorbate model gases = [] for gn in gas_names: gases.append(system.read_lammps(osp.join(data_dir, gn + '.lmps'))) gases[-1].forcefield = 'trappe/amber' # Setup of adsorbent model frame = system.read_lammps('pim.lmps') frame.forcefield = 'trappe/amber' # Constant for loadings calculations molec2mmols_g = 1e+3 / frame.mass # Setup of the GCMC simulations css = cassandra.Cassandra(frame) sim_settings = css.read_input('run_props.inp') # This function in given context will calculate the loading from short GCMC simulations def calculate_isotherm_point(gas_name, press): run_fldr = osp.join(gas_name, str(press)) idx = gas_names.index(gas_name) # sim_settings.update({'Run_Name': 'gcmc'}) css.add_gcmc(species=gases[idx], is_new=True, chem_pot=chem_pots[idx](press), out_folder=run_fldr, props_file='gcmc.inp', **sim_settings) css.run() full_prp = css.run_queue[0].get_prp() return molec2mmols_g * numpy.average( full_prp[3][int(len(2 * full_prp[3]) / 3):]) # This function in given context will load the pre-calculated loading value from previously done GCMC simulations def load_isotherm_point(gas_name, press): with open(loadings_file, 'r') as pntr: stream = pntr.read() tmp = stream.split('\n' + gas_name)[1] idx = re.search('[a-zA-Z]|\Z', tmp) value = re.findall('\n{:}\s+\d+\.\d+'.format(press), tmp[:idx.start()])[0] return float(re.split('\s+', value)[-1]) # Calculation of adsorption isotherms for pure CH4 and CO2 gases for further usage in IAST simulations. # This is the **MOST TIME CONSUMING STEP** in this example, if you want to skip it switch the key is_simulated to False # The IAST will be done using PyIAST package, thus isotherms are wrapped into the corresponding object gas_press = [0.1, 1, 5, 10, 25, 50] lk = 'Loading(mmol/g)' pk = 'Pressure(bar)' isotherms = [] loadings = dict.fromkeys(gas_names) for gn in gas_names: loadings[gn] = [] for p in gas_press: if is_simulate_loadings: data = calculate_isotherm_point(gn, p) else: data = load_isotherm_point(gn, p) loadings[gn].append(data) isotherms.append( pyiast.ModelIsotherm(pandas.DataFrame(zip(gas_press, loadings[gn]), columns=[pk, lk]), loading_key=lk, pressure_key=pk, model='BET', optimization_method='Powell')) # The PyIAST run for calculating of mixed adsorption isotherm # Initial guesses of adsorbed mole fractions do span broad range of values, because PyIAST might not find # solution at certain values of mole fractions and through an exception guesses = [[a, 1 - a] for a in numpy.linspace(0.01, 0.99, 50)] for in_g in guesses: mix_loadings = [] try: for p in gas_press: mix_loadings.append( list( pyiast.iast(p * numpy.array(mol_frac), isotherms, verboseflag=False, adsorbed_mole_fraction_guess=in_g))) mix_loadings = numpy.array(mix_loadings) break except: print('Initial guess {:} had failed to converge'.format(in_g)) continue mix_loadings = numpy.sum(mix_loadings, axis=1) mix_isotherm = pyiast.ModelIsotherm(pandas.DataFrame(zip( gas_press, mix_loadings), columns=[pk, lk]), loading_key=lk, pressure_key=pk, model='BET', optimization_method='L-BFGS-B') # Output: Graphing of constructed isotherms def _plot_isotherms(ax, loc_gp, loc_isoth, loc_mix_load, loc_mix_isoth): rng = numpy.linspace(min(loc_gp), max(loc_gp), 100) ax.plot(loc_gp, loadings[gas_names[0]], 'og', lw=2.5, label='{:} loadings'.format(gas_names[0].upper())) ax.plot(rng, [loc_isoth[0].loading(t) for t in rng], '--g', lw=2, label='BET fit of {:} loadings'.format(gas_names[0].upper())) ax.plot(loc_gp, loadings[gas_names[1]], 'or', lw=2.5, label='{:} loadings'.format(gas_names[1].upper())) ax.plot(rng, [loc_isoth[1].loading(t) for t in rng], '--r', lw=2, label='BET fit of {:} loadings'.format(gas_names[1].upper())) ax.plot(loc_gp, loc_mix_load, 'ob', lw=2.5, label='1-to-1 mixture loadings') ax.plot(rng, [loc_mix_isoth.loading(t) for t in rng], '--b', lw=2, label='BET fit of 1-to-1 mixture loadings') ax.set_xlabel('Gas pressure [bar]', fontsize=20) ax.set_ylabel('Loading [mmol / g]', fontsize=20) ax.tick_params(axis='both', labelsize=16) ax.grid(True) ax.legend(fontsize=16) mplp.tight_layout() if graphing.lower() == 'tofile': fig, axs = mplp.subplots(1, 1, figsize=(10, 5)) _plot_isotherms(axs, gas_press, isotherms, mix_loadings, mix_isotherm) mplp.savefig('pim1_mix_adsorption.png', dpi=192) elif graphing.lower() == 'toscreen': mplp.figure() axs = mplp.gca() _plot_isotherms(axs, gas_press, isotherms, mix_loadings, mix_isotherm) mplp.show() with open('iast_loadings.dat', 'w') as pntr: pntr.write('{:}\t\t{:}\n'.format(pk, lk)) pntr.write('{:}-{:} 1-to-1\n'.format(gas_names[0], gas_names[1])) for p, ml in zip(gas_press, mix_loadings): pntr.write('{:}\t\t{:}\n'.format(p, ml))
pressure_key=pk, model='BET', optimization_method='Powell')) # The PyIAST run for calculating of mixed adsorption isotherm # Initial guesses of adsorbed mole fractions do span broad range of values, because PyIAST might not find # solution at certain values of mole fractions and through an exception guesses = [[a, 1 - a] for a in numpy.linspace(0.01, 0.99, 50)] for in_g in guesses: mix_loadings = [] try: for p in gas_press: mix_loadings.append( list( pyiast.iast(p * numpy.array(mol_frac), isotherms, verboseflag=False, adsorbed_mole_fraction_guess=in_g))) mix_loadings = numpy.array(mix_loadings) break except: print('Initial guess {:} had failed to converge'.format(in_g)) continue mix_loadings = numpy.sum(mix_loadings, axis=1) mix_isotherm = pyiast.ModelIsotherm(pandas.DataFrame(zip( gas_press, mix_loadings), columns=[pk, lk]), loading_key=lk, pressure_key=pk, model='BET', optimization_method='L-BFGS-B')