|
|
|
@ -1,5 +1,13 @@
|
|
|
|
|
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:
|
|
|
|
@ -7,13 +15,15 @@ class REEPS:
|
|
|
|
|
Takes in experimental data
|
|
|
|
|
Returns parameters for GEM
|
|
|
|
|
:param exp_csv_filename: (str) csv file name with experimental data
|
|
|
|
|
:param param_xml_file: (str) xml file with parameters for equilibrium calc
|
|
|
|
|
: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
|
|
|
|
|
:param diluant_name: (str) name of diluant
|
|
|
|
|
: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)
|
|
|
|
@ -22,32 +32,42 @@ class REEPS:
|
|
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
|
exp_csv_filename,
|
|
|
|
|
param_xml_file,
|
|
|
|
|
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(param_xml_file, phase_names)
|
|
|
|
|
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.set_in_moles()
|
|
|
|
|
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
|
|
|
|
@ -60,8 +80,13 @@ class REEPS:
|
|
|
|
|
def get_phases(self) -> list:
|
|
|
|
|
return self._phases
|
|
|
|
|
|
|
|
|
|
def set_phases(self, param_xml_file, phase_names):
|
|
|
|
|
self._phases = ct.import_phases(param_xml_file, phase_names)
|
|
|
|
|
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:
|
|
|
|
@ -99,6 +124,20 @@ class REEPS:
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
@ -120,7 +159,8 @@ class REEPS:
|
|
|
|
|
self._diluant_rho = diluant_rho
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def set_in_moles(self):
|
|
|
|
|
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
|
|
|
|
@ -145,6 +185,8 @@ class REEPS:
|
|
|
|
|
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)
|
|
|
|
@ -168,7 +210,6 @@ class REEPS:
|
|
|
|
|
self._diluant_rho = diluant_rho
|
|
|
|
|
|
|
|
|
|
in_moles_data = []
|
|
|
|
|
feed_vol = 1. # g/L
|
|
|
|
|
aq_phase_solvent_moles = feed_vol * solvent_rho / solvent_mw
|
|
|
|
|
for row in exp_df.values:
|
|
|
|
|
h_plus_moles = feed_vol * row[0]
|
|
|
|
@ -176,7 +217,7 @@ class REEPS:
|
|
|
|
|
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_rho / extractant_mw
|
|
|
|
|
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
|
|
|
|
@ -196,3 +237,99 @@ class REEPS:
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|