You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LLEPE/reeps.py

336 lines
12 KiB

import time
import cantera as ct
import pandas as pd
import numpy as np
from scipy.optimize import minimize
# noinspection PyPep8Naming
import xml.etree.ElementTree as ET
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
class REEPS:
"""REEPS (Rare earth extraction parameter searcher)
Takes in experimental data
Returns parameters for GEM
:param exp_csv_filename: (str) csv file name with experimental data
:param phases_xml_filename: (str) xml file with parameters for equilibrium calc
:param x_guess: (float) guess for multiplier variable
:param h_guess: (float) initial guess standard enthalpy (J/kmol)
:param phase_names: (list) names of phases in xml file
:param aq_solvent_name: (str) name of aqueous solvent in xml file
:param extractant_name: (str) name of extractant in xml file
:param diluant_name: (str) name of diluant in xml file
:param complex_name: (str) name of complex in xml file
:param rare_earth_ion_name: (str) name of rare earth ion in xml file
:param aq_solvent_rho: (float) density of solvent (g/L)
:param extractant_rho: (float) density of extractant (g/L)
:param diluant_rho: (float) density of extractant (g/L)
If no density is given, molar volume/molecular weight is used from xml
"""
def __init__(self,
exp_csv_filename,
phases_xml_filename,
x_guess,
h_guess,
phase_names,
aq_solvent_name,
extractant_name,
diluant_name,
complex_name,
rare_earth_ion_name,
aq_solvent_rho=None,
extractant_rho=None,
diluant_rho=None,
):
self._exp_csv_filename = exp_csv_filename
self._phases_xml_filename = phases_xml_filename
self._x_guess = x_guess
self._h_guess = h_guess
self._phase_names = phase_names
self._aq_solvent_name = aq_solvent_name
self._extractant_name = extractant_name
self._diluant_name = diluant_name
self._complex_name = complex_name
self._rare_earth_ion_name = rare_earth_ion_name
self._aq_solvent_rho = aq_solvent_rho
self._extractant_rho = extractant_rho
self._diluant_rho = diluant_rho
self._phases = ct.import_phases(phases_xml_filename, phase_names)
self._exp_df = pd.read_csv(self._exp_csv_filename)
self._in_moles = None
self._aq_ind = None
self._org_ind = None
self.set_in_moles(feed_vol=1)
def get_exp_csv_filename(self) -> str:
return self._exp_csv_filename
def set_exp_csv_filename(self, exp_csv_filename):
self._exp_csv_filename = exp_csv_filename
self._exp_df = pd.read_csv(self._exp_csv_filename)
return None
def get_phases(self) -> list:
return self._phases
def set_phases(self, phases_xml_filename, phase_names):
"""Change xml and phase names
Also runs set_in_mole to set initial moles to 1 g/L"""
self._phases_xml_filename = phases_xml_filename
self._phase_names = phase_names
self._phases = ct.import_phases(phases_xml_filename, phase_names)
self.set_in_moles(feed_vol=1)
return None
def get_x_guess(self) -> float:
return self._x_guess
def set_x_guess(self, x_guess):
self._x_guess = x_guess
return None
def get_h_guess(self) -> float:
return self._h_guess
def set_h_guess(self, h_guess):
self._h_guess = h_guess
return None
def get_aq_solvent_name(self) -> str:
return self._aq_solvent_name
def set_aq_solvent_name(self, aq_solvent_name):
self._aq_solvent_name = aq_solvent_name
return None
def get_extractant_name(self) -> str:
return self._extractant_name
def set_extractant_name(self, extractant_name):
self._extractant_name = extractant_name
return None
def get_diluant_name(self) -> str:
return self._diluant_name
def set_diluant_name(self, diluant_name):
self._diluant_name = diluant_name
return None
def get_complex_name(self) -> str:
return self._complex_name
def set_complex_name(self, complex_name):
self._complex_name = complex_name
return None
def get_rare_earth_ion_name(self) -> str:
return self._rare_earth_ion_name
def set_rare_earth_ion_name(self, rare_earth_ion_name):
self._rare_earth_ion_name = rare_earth_ion_name
return None
def get_aq_solvent_rho(self) -> str:
return self._aq_solvent_rho
def set_aq_solvent_rho(self, aq_solvent_rho):
self._aq_solvent_rho = aq_solvent_rho
return None
def get_extractant_rho(self) -> str:
return self._extractant_rho
def set_extractant_rho(self, extractant_rho):
self._extractant_rho = extractant_rho
return None
def get_diluant_rho(self) -> str:
return self._diluant_rho
def set_diluant_rho(self, diluant_rho):
self._diluant_rho = diluant_rho
return None
def set_in_moles(self, feed_vol):
""":param feed_vol: (float) feed volume of mixture (g/L)"""
phases_copy = self._phases.copy()
exp_df = self._exp_df.copy()
solvent_name = self._aq_solvent_name
extractant_name = self._extractant_name
diluant_name = self._diluant_name
solvent_rho = self._aq_solvent_rho
extractant_rho = self._extractant_rho
diluant_rho = self._diluant_rho
mixed = ct.Mixture(phases_copy)
aq_ind = None
solvent_ind = None
for ind, phase in enumerate(phases_copy):
if solvent_name in phase.species_names:
aq_ind = ind
solvent_ind = phase.species_names.index(solvent_name)
if aq_ind is None:
raise Exception('Solvent "{0}" not found \
in xml file'.format(solvent_name))
if aq_ind == 0:
org_ind = 1
else:
org_ind = 0
self._aq_ind = aq_ind
self._org_ind = org_ind
extractant_ind = phases_copy[org_ind].species_names.index(
extractant_name)
diluant_ind = phases_copy[org_ind].species_names.index(diluant_name)
mix_aq = mixed.phase(aq_ind)
mix_org = mixed.phase(org_ind)
solvent_mw = mix_aq.molecular_weights[solvent_ind] # g/mol
extractant_mw = mix_org.molecular_weights[extractant_ind]
diluant_mw = mix_org.molecular_weights[diluant_ind]
if solvent_rho is None:
solvent_rho = mix_aq(aq_ind).partial_molar_volumes[
solvent_ind] / solvent_mw * 1e6 # g/L
self._aq_solvent_rho = solvent_rho
if extractant_rho is None:
extractant_rho = mix_org(org_ind).partial_molar_volumes[
extractant_ind] / extractant_mw * 1e6
self._extractant_rho = extractant_rho
if diluant_rho is None:
diluant_rho = mix_org(org_ind).partial_molar_volumes[
extractant_ind] / extractant_mw * 1e6
self._diluant_rho = diluant_rho
in_moles_data = []
aq_phase_solvent_moles = feed_vol * solvent_rho / solvent_mw
for row in exp_df.values:
h_plus_moles = feed_vol * row[0]
hydroxide_ions = 0
rare_earth_moles = feed_vol * row[6]
chlorine_moles = 3 * rare_earth_moles + h_plus_moles
extractant_moles = feed_vol * row[3]
extractant_vol = extractant_moles * extractant_mw / extractant_rho
diluant_vol = feed_vol - extractant_vol
diluant_moles = diluant_vol * diluant_rho / diluant_mw
complex_moles = 0
species_moles = [aq_phase_solvent_moles,
h_plus_moles,
hydroxide_ions,
chlorine_moles,
rare_earth_moles,
extractant_moles,
diluant_moles,
complex_moles,
]
in_moles_data.append(species_moles)
self._in_moles = pd.DataFrame(in_moles_data, columns=mixed.species_names)
return None
def get_in_moles(self) -> pd.DataFrame:
return self._in_moles
def objective(self, x):
h = x[0] * self._h_guess
phases_copy = self._phases.copy()
aq_ind = self._aq_ind
org_ind = self._org_ind
complex_name = self._complex_name
rare_earth_ion_name = self._rare_earth_ion_name
in_moles = self._in_moles
exp_df = self._exp_df
complex_index = phases_copy[org_ind].species_index(complex_name)
complex_species = phases_copy[org_ind].species(complex_index)
new_thermo_coeffs = complex_species.thermo.coeffs
new_thermo_coeffs[1] = h
complex_species.thermo = ct.ConstantCp(
complex_species.thermo.min_temp,
complex_species.thermo.max_temp,
complex_species.thermo.reference_pressure,
new_thermo_coeffs)
phases_copy[org_ind].modify_species(complex_index, complex_species)
mix = ct.Mixture(phases_copy)
pred = []
for row in in_moles.values:
mix.species_moles = row
mix.equilibrate('TP', log_level=0)
re_org = mix.species_moles[mix.species_index(
org_ind, complex_name)]
re_aq = mix.species_moles[mix.species_index(
aq_ind, rare_earth_ion_name)]
pred.append(np.log10(re_org / re_aq))
pred = np.array(pred)
meas = np.log10(exp_df['D(m)'].values)
obj = np.sum((pred - meas) ** 2)
return obj
def fit(self, kwargs) -> float:
"""Fits experimental to modeled data by estimating complex reference enthalpy
Returns estimated complex enthalpy in J/mol
:param kwargs: (dict) parameters and options for scipy.minimize
"""
x_guess = self._x_guess
h_guess = self._h_guess
res = minimize(self.objective, x_guess, **kwargs)
pred_complex_enthalpy = res.x[0] * h_guess / 1000 # J/mol
return pred_complex_enthalpy
def update_xml(self,
complex_enthalpy,
phases_xml_filename=None,
complex_name=None):
"""updates xml file with new complex_enthalpy"""
if phases_xml_filename is None:
phases_xml_filename = self._phases_xml_filename
if complex_name is None:
complex_name = self._complex_name
tree = ET.parse(phases_xml_filename)
root = tree.getroot()
# Update xml file
for species in root.iter('species'):
if species.attrib['name'] is complex_name:
for h0 in species.iter('h0'):
h0.text = str(complex_enthalpy)
h0.set('updated', 'Updated at ' + time.strftime('%X'))
tree.write(phases_xml_filename)
def parity_plot(self):
"""Parity plot between measured and predicted rare earth composition"""
phases_copy = self._phases.copy()
mix = ct.Mixture(phases_copy)
aq_ind = self._aq_ind
exp_df = self._exp_df
in_moles = self._in_moles
rare_earth_ion_name = self._rare_earth_ion_name
pred = []
for row in in_moles.values:
mix.species_moles = row
mix.equilibrate('TP', log_level=0)
re_aq = mix.species_moles[mix.species_index(
aq_ind, rare_earth_ion_name)]
pred.append(re_aq)
pred = np.array(pred)
meas = exp_df['REeq(m)'].values
min_data = np.min([pred, meas])
max_data = np.max([pred, meas])
min_max_data = np.array([min_data, max_data])
fig, ax = plt.subplots()
sns.scatterplot(meas, pred, color="r")
sns.lineplot(min_max_data, min_max_data, color="b")
ax.set(xlabel='Measured X equilibrium', ylabel='Predicted X equilibrium')
plt.show()
return None