'''
The equilibrate module implements a Python interface to the Equilibrate and
EquilState classes. It also implements a Python class called MELTSmodel that
wraps the objective-C classes: EquilibrateUsingMELTSv102,
EquilibrateUsingMELTSv110, EquilibrateUsingMELTSv120,
EquilibrateUsingpMELTSv561, EquilibrateUsingMELTSwithDEW,
and EquilibrateUsingStixrude.
The Equilibrate class provides methods to calculate an equilibrium phase
assemblage given a list of Phase class instances.
You can calculate equilibrium in closed thermodynamic systems under a
variety of constraints:
- Temperature and pressure (Gibbs free energy minimization)
- Entropy and pressure (enthalpy minimization)
- Temperature and volume (Helmholtz free energy minimization)
- Entropy and volume (internal energy minimization)
You can also calculate equilibrium under constraints of fixed temperature and
pressure in open thermodynamic systems by specifying one or more fixed elemental
chemical potentials.
For details of the underlying theory and algorithms implemented in the Equilibrate
class, see the notebooks in the PublicNotebooks/Equilibrate folder on
the `ENKI server <https://enki.ofm-research.org/hub/login>`_.
'''
import numpy as np
import scipy as sp
import sympy as sym
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import statsmodels.api as sm
import xml.etree.ElementTree as ET
import locale
import sys
from thermoengine import core
from thermoengine import chem
from thermoengine import phases
# Objective-C imports
import ctypes
from ctypes import cdll
from ctypes import util
from collections import OrderedDict
from rubicon.objc import ObjCClass, ObjCProtocol, NSObject, objc_method
cdll.LoadLibrary(util.find_library('phaseobjc'))
# Excel imports
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
__all__ = ['Equilibrate', 'EquilState', 'MELTSmodel']
# array length for elements
NE = 106
[docs]class EquilState:
"""Class for holding the phase state for an equilibrium model determined by Equilibrate
Parameters
----------
elements_l : []
A list of strings identifying the chemical elements present in the
system
phases_l : []
A list of class instances for phases that are in the system. These
instances must derive from the *PurePhase* or *SolutionPhase* classes,
which are defined in the *phases* module.
Attributes
----------
c_matrix
c_omni
c_omni_qr
c_qr_decomp
element_l
phase_d
pressure
temperature
"""
def __init__(self, elements_l, phases_l):
elements_present = [False for i in range (0,NE)]
for elm in elements_l:
index = core.chem.PERIODIC_ORDER.tolist().index(elm)
elements_present[index] = True
self._element_l = [i for i,e in enumerate(elements_present) if e]
self._phase_d = OrderedDict()
for phase in phases_l:
entry = dict()
entry['obj'] = phase
nc = int(phase.props['endmember_num']) \
if 'endmember_num' in phase.props else 1
entry['name'] = phase.props['phase_name']
entry['abbrev'] = phase.props['abbrev']
entry['nc'] = nc
entry['end_name'] = phase.props['endmember_name']
entry['end_formula'] = phase.props['formula']
entry['end_molwts'] = phase.props['molwt']
entry['moles'] = np.zeros(nc)
entry['mole_frac'] = np.zeros(nc)
entry['mole_nz'] = np.ones(nc)
entry['grams'] = 0.0
entry['affinity'] = 0.0
# create a list of length nc where each entry is a numpy array
# that contains stoichiometric coefficients converting each
# endmember to system elements (in the order _element_l)
conv_to_elm = []
conv_to_elm_sum = np.zeros(len(self._element_l))
conv_to_oxd = []
for i in range(0,nc):
mol = np.zeros(nc)
mol[i] = 1.0
if nc == 1:
mol_elm = phase.props['element_comp'][0]
else:
mol_elm = entry['obj'].covert_endmember_comp(
mol,output='moles_elements')
conv_entry = np.zeros(len(self._element_l))
for i in range(0,NE):
if mol_elm[i] != 0.0:
conv_entry[self._element_l.index(i)] = mol_elm[i]
conv_to_elm_sum = np.add(conv_to_elm_sum, conv_entry)
conv_to_elm.append(conv_entry)
conv_to_oxd.append(core.chem.calc_mol_oxide_comp(mol_elm))
entry['conv_to_elm'] = conv_to_elm
entry['conv_to_oxd'] = conv_to_oxd
entry['omnicomponent'] = True if np.count_nonzero(
conv_to_elm_sum) == len(conv_to_elm_sum) else False
name = phase.props['phase_name']
self._phase_d[name] = entry
first = True
for entry in self._phase_d.values():
nc = entry['nc']
for row in entry['conv_to_elm']:
if first:
c_matrix = np.reshape(row,(1,row.size))
first = False
else:
c_matrix = np.vstack((c_matrix,np.reshape(
row,(1,row.size))))
self._c_matrix = c_matrix
self._c_qr_decomp = np.linalg.qr(self.c_matrix)
self._c_omni = None
self._c_omni_qr = None
self._temperature = 1000.0
self._pressure = 1000.0
############################
# Properties of the system #
############################
@property
def phase_d(self):
"""
A dictionary of dictionaries that holds properties of system phases
Returns
-------
Dictionary of phases in the system (dict)
"""
return self._phase_d
@property
def element_l(self):
"""
List of atomic numbers of elements in the system
Returns
-------
A Python list
"""
return self._element_l
@property
def c_matrix(self):
"""
Numpy matrix that maps elements to moles of endmembers
Moles of endmembers are indexed by row, and moles of elements
are indexed by column.
Returns
-------
Numpy 2-D array
"""
return self._c_matrix
@property
def c_qr_decomp(self):
"""
Tuple of Numpy matrices of the Q,R decomposition of the c_matrix
Returns
-------
tuple
"""
return self._c_qr_decomp
@property
def c_omni(self):
"""
Numpy conversion matrix for the omnicomponent phase (if one exists)
Moles of endmembers are indexed by row, and moles of elements
are indexed by column.
Returns
-------
np 2-D array
"""
return self._c_omni
@property
def c_omni_qr(self):
"""
Tuple of Numpy matrices of the Q,R decomposition of c_omni
Returns
-------
tuple
"""
return self._c_omni_qr
@property
def temperature(self):
"""
Temperature of the assemblage, in Kelvins
Returns
-------
float
"""
return self._temperature
@temperature.setter
def temperature(self, value):
assert isinstance(value, (int, float))
assert value > 0
self._temperature = value
@property
def pressure(self):
"""
Pressure of the assemblage, in bars
Returns
-------
float
"""
return self._pressure
@pressure.setter
def pressure(self, value):
assert isinstance(value, (int, float))
assert value > 0
self._pressure = value
################################
# Methods for phases in system #
################################
[docs] def omni_phase(self):
"""
Returns first omnicomponent phase in the phase dictionary
"""
for entry in self.phase_d.values():
if entry['omnicomponent']:
return entry['name']
return None
[docs] def c_matrix_for_phase(self, phase_name):
"""
Returns a slice of c_matrix relevant to the specified phase_name
Parameters
----------
phase_name : str
Name of a system phase
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
row_min = 0
for key,entry in self.phase_d.items():
if key == phase_name:
row_max = row_min + entry['nc']
break
else:
row_min += entry['nc']
return self.c_matrix[row_min:row_max,:]
[docs] def set_phase_comp(self, phase_name, cmp, input_as_elements=False):
"""
Sets the endmember moles and total moles a phase in the system
Parameters
----------
phase_name : str
Name of a system phase
cmp : ndarray
1-D Numpy array with compositional data
input_as_elements : bool, def False
If True, convert input array from moles of elements to moles of
endmember components.
Returns
-------
valid : bool
True if input composition is valid for phase
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return False
assert type(cmp) is np.ndarray, \
'bulk_comp must be set to an numpy array'
if not input_as_elements:
assert cmp.size == self.phase_d[phase_name]['nc'], \
'cmp array must have length ' \
+ str(self.phase_d[phase_name]['nc'])
else:
assert cmp.size == len(self.element_l), \
'cmp array must have length ' + str(len(self.element_l))
cT = self.c_matrix_for_phase(phase_name).T
cTinv = np.linalg.inv(cT) if cT.shape[0] == cT.shape[1] else np.linalg.pinv(cT)
cmp = np.matmul(cTinv, cmp)
self.phase_d[phase_name]['moles'] = cmp
np.put(self.phase_d[phase_name]['mole_nz'],
np.flatnonzero(np.abs(cmp) < 10.0*np.finfo(float).eps), 0.0)
return self.phase_d[phase_name]['obj'].test_endmember_comp(cmp)
[docs] def tot_moles_phase(self, phase_name):
"""
Returns total moles of phase
Parameters
----------
phase_name : str
Name of a system phase
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return 0.0
return np.sum(self.phase_d[phase_name]['moles'])
[docs] def tot_grams_phase(self, phase_name):
"""
Returns total grams of phase
Parameters
----------
phase_name : str
Name of a system phase
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return 0.0
grams = np.matmul(self.phase_d[phase_name]['moles'],
self.phase_d[phase_name]['end_molwts'])
return np.sum(grams)
[docs] def moles_elements(self, phase_name):
"""
Returns array of moles of elements in phase
Parameters
----------
phase_name : str
Name of a system phase
Returns
-------
result : np_array
Numpy array of mole numbers of the reduced set of elements in the
phase
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
elm = np.zeros(len(self.element_l))
for moles,coeff in zip(self.phase_d[phase_name]['moles'],
self.phase_d[phase_name]['conv_to_elm']):
if moles != 0:
elm = np.add(elm,moles*coeff)
return elm
[docs] def tot_moles_elements(self):
"""
Returns array of moles of elements in system
Returns
-------
result : np_array
Numpy array of mole numbers of the reduced set of elements in the
system
"""
elm = np.zeros(len(self.element_l))
for phase in self.phase_d.keys():
if self.tot_moles_phase(phase) > 0:
elm += self.moles_elements(phase)
return elm
[docs] def oxide_comp(self, phase_name, output='wt%', prune_list=True):
"""
Returns a dictionary of concentrations of oxides in phase
Parameters
----------
phase_name : str
Name of a system phase
output : str, default "wt%"
Units of output: 'wt%' (default), 'moles', 'grams'
prune_list : bool, default True
Remove zeroed valued entries
Returns
-------
result : collections.OrderedDict
Dictionary of oxide concentration values, keyed by oxide
"""
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
oxd = np.zeros(core.chem.oxide_props['oxide_num'])
for moles,coeff in zip(self.phase_d[phase_name]['moles'],
self.phase_d[phase_name]['conv_to_oxd']):
if moles != 0:
oxd = np.add(oxd,moles*coeff)
result = OrderedDict()
for key,value in zip(core.chem.oxide_props['oxides'],oxd):
result[key] = value
if output == 'wt%' or output == 'grams':
mod_result = OrderedDict()
sum_oxd = 0.0
for index,(key,value) in enumerate(result.items()):
mod_result[key] = value*chem.oxide_props['molwt'][index]
sum_oxd += mod_result[key]
result = mod_result
if output == 'wt%' and sum_oxd > 0:
mod_result = OrderedDict()
for (key,value) in result.items():
mod_result[key] = 100.0*value/sum_oxd
result = mod_result
if prune_list:
mod_result = OrderedDict()
for (key,value) in result.items():
if value != 0:
mod_result[key] = value
result = mod_result
return result
[docs] def properties(self, phase_name=None, props=None, units=False):
"""
Returns properties of phases in the system
Parameters
----------
phase_name : str, default None
Name of phase in system. If None, returns a dictionary of system
phases, with names as keys and 'stable' or 'unstable' as values.
props : str, default None
Name of property to be retrieved. If None, a list of valid properties
is returned.
units : bool, default False
Property units are provided as the second entry of a returned tuple.
"""
prop_d = OrderedDict([ ('Mass','g'), ('GibbsFreeEnergy','J'),
('Enthalpy','J'), ('Entropy','J/K'), ('HeatCapacity','J/K'),
('DcpDt','J/K^2'), ('Volume','J/bar'), ('DvDt','J/bar-K'),
('DvDp','J/bar^2'), ('D2vDt2','J/bar-K^2'), ('D2vDtDp','J/bar^2-K'),
('D2vDp2','J/bar^3'), ('Density','g/cm^3'), ('Alpha','1/K'),
('Beta','1/bar'), ('K','GPa'), ("K'",'none'), ('Gamma','none') ])
if phase_name is None:
phase_d = {}
for key in self.phase_d.keys():
phase_d[key] = 'stable' if self.tot_moles_phase(
key) > 0 else 'unstable'
phase_d['System'] = 'stable'
return phase_d
if phase_name not in self.phase_d and phase_name != 'System':
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
if props is None:
if units:
return set(zip(list(prop_d.keys()), list(prop_d.values())))
else:
return list(prop_d.keys())
if props not in prop_d:
print('Error: ' + props + ' is not a valid property.')
return None
t = self.temperature
p = self.pressure
if phase_name != 'System':
entry = self.phase_d[phase_name]
moles = entry['moles']
if np.sum(moles) == 0:
result = 0.0
if units:
result = (0.0, None)
return result
nc = entry['nc']
sys = False
else:
sys = True
result = None
if units:
result = (None, None)
if props == 'Mass':
if sys:
result = 0.0
for phs in self.phase_d.keys():
grams = np.matmul(self.phase_d[phs]['moles'],
self.phase_d[phs]['end_molwts'])
result += np.sum(grams)
else:
grams = np.matmul(self.phase_d[phase_name]['moles'],
self.phase_d[phase_name]['end_molwts'])
result = np.sum(grams)
if units:
result = (result, prop_d['Mass'])
elif props == 'GibbsFreeEnergy':
if sys:
result = self.G(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].gibbs_energy(t,p)
else:
result = entry['obj'].gibbs_energy(t,p,mol=moles)
if units:
result = (result, prop_d['GibbsFreeEnergy'])
elif props == 'Enthalpy':
if sys:
result = self.G(t, p) - t*self.dGdT(t, p)
else:
if nc == 1:
result = moles[0]*(entry['obj'].gibbs_energy(t,p)
+t*entry['obj'].entropy(t,p))
else:
result = (entry['obj'].gibbs_energy(t,p,mol=moles)
+t*entry['obj'].entropy(t,p,mol=moles))
if units:
result = (result, prop_d['Enthalpy'])
elif props == 'Entropy':
if sys:
result = -self.dGdT(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].entropy(t,p)
else:
result = entry['obj'].entropy(t,p,mol=moles)
if units:
result = (result, prop_d['Entropy'])
elif props == 'HeatCapacity':
if sys:
result = -t*self.d2GdT2(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].heat_capacity(t,p)
else:
result = entry['obj'].heat_capacity(t,p,mol=moles)
if units:
result = (result, prop_d['HeatCapacity'])
elif props == 'DcpDt':
if sys:
result = -t*self.d3GdT3(t, p) - self.d2GdT2(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].heat_capacity(
t,p,deriv={'dT':1})
else:
result = entry['obj'].heat_capacity(
t,p,mol=moles,deriv={'dT':1})
if units:
result = (result, prop_d['DcpDt'])
elif props == 'Volume':
if sys:
result = self.dGdP(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(t,p)
else:
result = entry['obj'].volume(t,p,mol=moles)
if units:
result = (result, prop_d['Volume'])
elif props == 'DvDt':
if sys:
result = self.d2GdTdP(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(t,p,deriv={'dT':1})
else:
result = entry['obj'].volume(t,p,mol=moles,deriv={'dT':1})
if units:
result = (result, prop_d['DvDt'])
elif props == 'DvDp':
if sys:
result = self.d2GdP2(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(t,p,deriv={'dP':1})
else:
result = entry['obj'].volume(t,p,mol=moles,deriv={'dP':1})
if units:
result = (result, prop_d['DvDp'])
elif props == 'D2vDt2':
if sys:
result = self.d3GdT2dP(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(t,p,deriv={'dT':2})
else:
result = entry['obj'].volume(t,p,mol=moles,deriv={'dT':2})
if units:
result = (result, prop_d['D2vDt2'])
elif props == 'D2vDtDp':
if sys:
result = self.d3GdTdP2(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(
t,p,deriv={'dT':1,'dP':1})
else:
result = entry['obj'].volume(t,p,mol=moles,
deriv={'dT':1,'dP':1})
if units:
result = (result, prop_d['D2vDtDp'])
elif props == 'D2vDp2':
if sys:
result = self.d3GdP3(t, p)
else:
if nc == 1:
result = moles[0]*entry['obj'].volume(t,p,deriv={'dP':2})
else:
result = entry['obj'].volume(t,p,mol=moles,deriv={'dP':2})
if units:
result = (result, prop_d['D2vDp2'])
elif props == 'Density':
v = self.properties(phase_name=phase_name, props='Volume',
units=False)
m = self.properties(phase_name=phase_name, props='Mass',
units=False)
result = m/v/10.0
if units:
result = (result, prop_d['Density'])
elif props == 'Alpha':
v = self.properties(phase_name=phase_name, props='Volume',
units=False)
dvdt = self.properties(phase_name=phase_name, props='DvDt',
units=False)
result = 10.0*dvdt/v
if units:
result = (result, prop_d['Alpha'])
elif props == 'Beta':
v = self.properties(phase_name=phase_name, props='Volume',
units=False)
dvdp = self.properties(phase_name=phase_name, props='DvDp',
units=False)
result = -10.0*dvdp/v
if units:
result = (result, prop_d['Beta'])
elif props == 'K':
beta = self.properties(phase_name=phase_name, props='Beta',
units=False)
result = 1.0/beta/10000.0
if units:
result = (result, prop_d['K'])
elif props == "K'":
v = self.properties(phase_name=phase_name, props='Volume',
units=False)
dvdp = self.properties(phase_name=phase_name, props='DvDp',
units=False)
d2vdp2 = self.properties(phase_name=phase_name, props='D2vDp2',
units=False)
result = v*d2vdp2/dvdp/dvdp - 1
if units:
result = (result, prop_d["K'"])
elif props == 'Gamma':
v = self.properties(phase_name=phase_name, props='Volume',
units=False)
dvdp = self.properties(phase_name=phase_name, props='DvDp',
units=False)
dvdt = self.properties(phase_name=phase_name, props='DvDt',
units=False)
cp = self.properties(phase_name=phase_name, props='HeatCapacity',
units=False)
result = (-v*dvdt/dvdp)/(cp+t*dvdt*dvdt/dvdp)
if units:
result = (result, prop_d['Gamma'])
return result
[docs] def compositions(self, phase_name=None, ctype='components', units='moles'):
"""
Returns compositions of phases in the system
Parameters
----------
phase_name : str, default None
Name of phase in system
ctype : str, default 'components'
Compositional descriptor. Permitted: 'components', 'oxides',
'elements'
units : str, default 'moles'
Units of composition. Permitted: 'moles', 'mole_frac', 'wt%'
Returns
-------
result : numpy array
One dimensional vector
Notes
-----
If units is set to 'oxides' or 'elements', results are returned in an
array of standard length and order.
"""
if phase_name is None:
print ('Error: Method requires a phase_name.')
return None
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
if ctype not in ['components', 'oxides', 'elements']:
print ('Error: ' + ctype + ' is not permitted. See doc string.')
return None
if units not in ['moles', 'mole_frac', 'wt%']:
print ('Error: ' + units + ' is not permitted. See doc string.')
return None
entry = self.phase_d[phase_name]
if ctype == 'components':
if units == 'moles' or units == 'mole_frac':
result = entry['moles']
if units == 'mole_frac':
result = result/np.sum(result)
elif units == 'wt%':
result = entry['moles']*entry['end_molwts']
result = 100.0*result/np.sum(result)
elif ctype == 'oxides':
if units == 'moles' or units == 'mole_frac':
result = self.oxide_comp(phase_name, output='moles',
prune_list=False)
result = np.array(list(result.values()))
if units == 'mole_frac':
result = result/np.sum(result)
elif units == 'wt%':
result = self.oxide_comp(phase_name, output='wt%',
prune_list=False)
result = np.array(list(result.values()))
elif ctype == 'elements':
moles = entry['moles']
if entry['nc'] == 1:
result = moles[0]*entry['obj'].props['element_comp'][0]
else:
result = np.zeros(NE)
for i in range(0,entry['nc']):
mol = np.zeros(entry['nc'])
mol[i] = 1.0
mol_elm = entry['obj'].covert_endmember_comp(
mol,output='moles_elements')
for j in range(1,NE):
result[j] += moles[i]*mol_elm[j]
if units == 'mole_frac':
result = result/np.sum(result)
elif units == 'wt%':
for i in range(1,NE):
result[i] *=chem.PERIODIC_WEIGHTS[i]
result = 100.0*result/np.sum(result)
return result
[docs] def affinities(self, phase_name=None):
"""
Returns affinity of phases in the system
Parameters
----------
phase_name : str, default None
Name of phase in system. If None, returns a dictionary of system
phases, with names as keys and 'stable' or 'unstable' as values.
"""
if phase_name is None:
print ('Error: Please specify a phase name.')
return None
if phase_name not in self.phase_d:
print('Error: ' + phase_name + ' is not in phase dictionary.')
return None
tot_mol = self.tot_moles_phase(phase_name)
if tot_mol > 0:
return 0.0
else:
return self.phase_d[phase_name]['affinity']
[docs] def print_state(self, level='summary', wt_as_oxides=True):
"""
Prints results about the system state
Parameters
----------
level : str
Level of detail to be printed
wt_as_oxides : bool, default True
Print wt% values on an oxide basis; otherwise print wt% of
endmember components.
"""
print (' ')
print ('T = {0:10.2f} °C, P = {1:10.1f} MPa'.format(
self.temperature-273.15, self.pressure/10.0))
for key,entry in self.phase_d.items():
tot_mol = self.tot_moles_phase(key)
if tot_mol > 0:
tot_grm = self.tot_grams_phase(key)
print ('{0:<15s}'.format(entry['name']), end=' ')
print ('moles: {0:10.6f}'.format(tot_mol), end=' ')
print ('grams: {0:7.3f}'.format(tot_grm))
if wt_as_oxides:
oxd = self.oxide_comp(key)
oxd_s = list(oxd.keys())
oxd_v = list(oxd.values())
n_oxd = len(oxd)
nc = entry['nc']
if nc > 1:
for i in range(0,nc):
gram = entry['moles'][i]*entry['end_molwts'][i]
print ('{0:>15s}'.format(
entry['end_name'][i][:15]), end=' ')
print ('form: {0:<10s}'.format(
entry['end_formula'][i][:10]), end=' ')
print ('X: {0:7.4f}'.format(
entry['moles'][i]/tot_mol), end=' ')
if wt_as_oxides:
if i < n_oxd:
print ('wt% {0:>7s} {1:7.2f}'.format(
oxd_s[i], oxd_v[i]))
else:
print ('')
else:
print ('wt% {0:7.2f}'.format(100.0*gram/tot_grm))
if wt_as_oxides and nc < n_oxd:
for i in range(nc,n_oxd):
print ('{0:49s} wt% {1:>7s} {2:7.2f}'.format(
' ', oxd_s[i], oxd_v[i]))
else:
print ('{0:<15s}'.format(entry['name']), end=' ')
print ('affn: {0:10.2f}'.format(entry['affinity']))
nc = entry['nc']
if nc > 1:
for i in range(0,nc):
print ('{0:>15s}'.format(entry['end_name'][i][:15]), end=' ')
print ('form: {0:<10s}'.format(
entry['end_formula'][i][:10]), end=' ')
print ('X: {0:7.4f}'.format(entry['mole_frac'][i]))
###################################################
# System potentials and compositional derivatives #
###################################################
[docs] def moles_v(self, reaction_v):
"""
Computes of a reaction product
moles (scalar) = (mole vector of all endmembers of all active phases
in the system) x (input mole vector of reaction coefficients)
Parameters
----------
reaction_v : ndarray
Array of reaction coefficients
Returns
-------
moles : float
Moles of reaction product
"""
moles = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
for x in entry['moles']:
moles.append(x)
moles = np.array(moles)
moles = np.matmul(reaction_v, moles)
return moles
[docs] def G(self, t=1000.0, p=1000.0):
"""
Returns Gibbs free energy of the system; a scalar
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(t,p)
else:
result += entry['obj'].gibbs_energy(t,p,mol=moles)
return result
[docs] def dGdT(self, t=1000.0, p=1000.0):
"""
Returns the temperature derivative of the Gibbs free energy of the
system; a scalar, and the negative of the system entropy
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += -moles[0]*entry['obj'].entropy(t,p)
else:
result += -entry['obj'].entropy(t,p,mol=moles)
return result
[docs] def dGdP(self, t=1000.0, p=1000.0):
"""
Returns the pressure derivative of the Gibbs free energy of the
system; a scalar, and the system volume
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dP':1})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dP':1})
return result
[docs] def dGdn(self, t=1000.0, p=1000.0, element_basis=True, full_output=False,
use_omni_phase=False):
"""
Returns the first molar derivative of G of the system; a 1-D numpy array
Parameters
----------
element_basis : bool, def True
If True, returns a projected array of element chemical potentials
full_output : bool, def False
If True, returns a tuple with full output from numpy lstlq method
use_omni_phase : bool, def False
If True, returns a projected array of element chemical potentials
using the omnicomponent phase as a basis. The system must have
an omnicomponent phase, and element_basis must be set to True for
this keyword to have effect.
"""
if use_omni_phase:
omni_phase_name = self.omni_phase()
if omni_phase_name is None:
assert(True,
'ERROR in dGdn method: use omni_phase keyword specified' +
' when the system has no omnicomponent phase')
element_basis = True
if use_omni_phase and element_basis:
# computes chemical potentials for all component endmembers
entry = self.phase_d[omni_phase_name]
result = []
nc = entry['nc']
if nc == 1:
row = np.array([entry['obj'].gibbs_energy(t, p)])
else:
moles = entry['moles']
row = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dmol':1})[0]
np.put(row, np.flatnonzero(entry['mole_nz'] == 0), 0.0)
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
else:
# computes chemical potentials for all component endmembers in a
# system phase if the total moles of that phase is > 0
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([entry['obj'].gibbs_energy(t, p)])
else:
moles = entry['moles']
row = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dmol':1})[0]
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
if not element_basis:
return result
else:
if use_omni_phase:
if self.c_omni is None:
self._c_omni = self.c_matrix_for_phase(omni_phase_name)
self._c_omni_qr = np.linalg.qr(self.c_omni)
Q, R = self.c_omni_qr
else:
first = True
for key,entry in self._phase_d.items():
if self.tot_moles_phase(key) > 0.0:
nc = entry['nc']
for row in entry['conv_to_elm']:
if first:
c_matrix = np.reshape(row,(1,row.size))
first = False
else:
c_matrix = np.vstack((c_matrix,np.reshape(
row,(1,row.size))))
Q, R = np.linalg.qr(c_matrix)
Qb = np.matmul(Q.T, result)
result, residuals, rank, S = np.linalg.lstsq(R, Qb, rcond=None)
return result if not full_output else (result, residuals, rank, S)
[docs] def d2GdT2(self, t=1000.0, p=1000.0):
"""
Returns the second temperature derivative of the Gibbs free energy of
the system; a scalar, and the negative of the temperature times the
system heat capacity
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += -moles[0]*entry['obj'].heat_capacity(t,p)/t
else:
result += -entry['obj'].heat_capacity(t,p,mol=moles)/t
return result
[docs] def d2GdTdP(self, t=1000.0, p=1000.0):
"""
Returns the cross temperature-pressure derivative of the Gibbs free
energy of the system; a scalar, and the volume times the system's
coefficient of isothermal expansion
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dT':1, 'dP':1})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dT':1, 'dP':1})
return result
[docs] def d2GdP2(self, t=1000.0, p=1000.0):
"""
Returns the second pressure derivative of the Gibbs free energy of
the system; a scalar, and the negative of the volume times the
system's coefficient of isothermal compressibility
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dP':2})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dP':2})
return result
[docs] def d2GdTdn(self, t=1000.0, p=1000.0):
"""
Returns the second derivative of G of the system with respect to
temperature and mole numbers; a 1-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([-entry['obj'].entropy(t, p)])
else:
moles = entry['moles']
row = -entry['obj'].entropy(t, p, mol=moles,
deriv={'dmol':1})[0]
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
return result
[docs] def d2GdPdn(self, t=1000.0, p=1000.0):
"""
Returns the second derivative of G of the system with respect to
pressure and mole numbers; a 1-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([entry['obj'].gibbs_energy(
t, p, deriv={'dP':1})])
else:
moles = entry['moles']
row = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dP':1, 'dmol':1})[0]
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
return result
def _sym_to_dense(self, n, sym_m):
den_m = np.empty((n,n))
ind = 0
for i in range(0,n):
for j in range(i,n):
entry = sym_m[ind]
den_m[i,j] = entry
den_m[j,i] = entry
ind += 1
return den_m
[docs] def d2Gdn2(self, t=1000.0, p=1000.0, element_basis=True):
"""
Returns the second molar derivative of G of system; a 2-D numpy array
Parameters
----------
element_basis : bool, def True
If True, returns a projected Hessian on an element basis
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
if entry['nc'] == 1:
mat = np.array([[0.0]])
else:
moles = entry['moles']
mat = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dmol':2})[0]
if len(mat.shape) == 1:
mat = self._sym_to_dense(entry['nc'], mat)
result.append(mat)
result = sp.linalg.block_diag(*result)
if not element_basis:
return result
else:
first = True
for key,entry in self._phase_d.items():
if self.tot_moles_phase(key) > 0.0:
nc = entry['nc']
for row in entry['conv_to_elm']:
if first:
c_matrix = np.reshape(row,(1,row.size))
first = False
else:
c_matrix = np.vstack((c_matrix,np.reshape(
row,(1,row.size))))
Q, R = np.linalg.qr(c_matrix)
#Qb = np.matmul(Q.T, np.matmul(result, Q))
Qb = np.matmul(Q.T, result)
result, residuals, rank, S = np.linalg.lstsq(R, Qb, rcond=None)
return result
[docs] def d3GdT2dn(self, t=1000.0, p=1000.0):
"""
Returns the third derivative of G of the system twice with respect to
temperature and once with respect to mole numbers; a 1-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([-entry['obj'].heat_capacity(t, p)/t])
else:
moles = entry['moles']
row = -entry['obj'].heat_capacity(t, p, mol=moles,
deriv={'dmol':1})[0]/t
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
return result
[docs] def d3GdP2dn(self, t=1000.0, p=1000.0):
"""
Returns the third derivative of G of the system with respect to
pressure twice and mole numbers once; a 1-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([entry['obj'].gibbs_energy(
t, p, deriv={'dP':2})])
else:
moles = entry['moles']
row = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dP':2, 'dmol':1})[0]
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
return result
[docs] def d3GdTdPdn(self, t=1000.0, p=1000.0):
"""
Returns the third derivative of G of the system with respect to
temperature, pressure and mole numbers; a 1-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
if nc == 1:
row = np.array([entry['obj'].gibbs_energy(
t, p, deriv={'dT':1, 'dP':1})])
else:
moles = entry['moles']
row = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dT':1, 'dP':1, 'dmol':1})[0]
result.append(np.reshape(row,(nc,1)))
result = np.vstack(tuple(result))
return result
[docs] def d3GdTdn2(self, t=1000.0, p=1000.0):
"""
Returns the first temperature and second molar derivative of G of
system; a 2-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
if entry['nc'] == 1:
mat = np.array([[0.0]])
else:
moles = entry['moles']
mat = -entry['obj'].entropy(t, p, mol=moles,
deriv={'dmol':2})[0]
if len(mat.shape) == 1:
mat = self._sym_to_dense(entry['nc'], mat)
result.append(mat)
result = sp.linalg.block_diag(*result)
return result
[docs] def d3GdPdn2(self, t=1000.0, p=1000.0):
"""
Returns the first pressure and second molar derivative of G of system;
a 2-D numpy array
"""
result = []
for entry in self.phase_d.values():
if self.tot_moles_phase(entry['name']) > 0.0:
if entry['nc'] == 1:
mat = np.array([[0.0]])
else:
moles = entry['moles']
mat = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dP':1, 'dmol':2})[0]
if len(mat.shape) == 1:
mat = self._sym_to_dense(entry['nc'], mat)
result.append(mat)
result = sp.linalg.block_diag(*result)
return result
[docs] def d3GdT3(self, t=1000.0, p=1000.0):
"""
Returns the third temperature derivative of the Gibbs free energy of
the system; a scalar
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
cp = entry['obj'].heat_capacity(t,p)
dcpdt = entry['obj'].heat_capacity(t,p,deriv={'dT':1})
result += moles[0]*(cp/t/t - dcpdt/t)
else:
cp = entry['obj'].heat_capacity(t,p,mol=moles)
dcpdt = entry['obj'].heat_capacity(t,p,mol=moles,
deriv={'dT':1})
result += cp/t/t - dcpdt/t
return result
[docs] def d3GdT2dP(self, t=1000.0, p=1000.0):
"""
Returns the third pressure derivative of the Gibbs free energy of
the system; a scalar
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dT':2, 'dP':1})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dT':2, 'dP':1})
return result
[docs] def d3GdTdP2(self, t=1000.0, p=1000.0):
"""
Returns the third pressure derivative of the Gibbs free energy of
the system; a scalar
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dT':1, 'dP':2})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dT':1, 'dP':2})
return result
[docs] def d3GdP3(self, t=1000.0, p=1000.0):
"""
Returns the third pressure derivative of the Gibbs free energy of
the system; a scalar
"""
result = 0.0
for entry in self.phase_d.values():
moles = entry['moles']
if np.sum(moles) > 0.0:
if entry['nc'] == 1:
result += moles[0]*entry['obj'].gibbs_energy(
t,p,deriv={'dP':3})
else:
result += entry['obj'].gibbs_energy(
t,p,mol=moles,deriv={'dP':3})
return result
def _sym_ten_to_dense(self, n, sym_t):
den_t = np.empty((n,n,n))
ind = 0
for i in range(0,n):
for j in range(i,n):
for k in range(j,n):
entry = sym_m[ind]
den_t[i,j,k] = entry
den_t[j,i,k] = entry
den_t[i,k,j] = entry
den_t[j,k,i] = entry
den_t[k,i,j] = entry
den_t[k,j,i] = entry
ind += 1
return den_t
[docs] def d3Gdn3(self, t=1000.0, p=1000.0, phase=None, cmp=None):
"""
Returns the third molar derivative of G system for the cmp component
of the phase named phase as a 2-D numpy array
Parameters
----------
phase : str, def None
cmp : int, def None
for the phase named phase d3Gdn3[cmp][*][*] is returned
"""
if phase is None or cmp is None:
return None
if self.tot_moles_phase(phase) <= 0.0:
return None
entry = self.phase_d[phase]
if entry['nc'] == 1:
mat = np.array([[0.0]])
else:
moles = entry['moles']
mat = entry['obj'].gibbs_energy(t, p, mol=moles,
deriv={'dmol':3})[0]
if len(mat.shape) == 1:
mat = self._sym_ten_to_dense(entry['nc'], mat)
nc = mat.shape[0]
result = np.zeros((nc, nc))
for i in range(0,nc):
for j in range(i,nc):
for k in range(j,nc):
if i == cmp:
result[j][k] = mat[i][j][k]
result[k][j] = mat[i][j][k]
elif j == cmp:
result[i][k] = mat[i][j][k]
result[k][i] = mat[i][j][k]
elif k == cmp:
result[i][j] = mat[i][j][k]
result[j][i] = mat[i][j][k]
return result
[docs]class Equilibrate:
"""Class for minimizing a generic thermodynamic potential in order to
calculate an equilibrium phase assemblage.
The default potential is the Gibbs free energy.
Parameters
----------
element_l : [], default None
See documentation for element_list attribute.
phase_l : [], default None
See documentation for phase_list attribute.
lagrange_l : [], default None
See documentation for lagrange_list attribute.
Attributes
----------
A_omni_inv
bulk_comp
CTf
element_list
entropy
eps_linear
eps_minimal_energy
eps_quad_optimal
eps_quad_suboptimal
eps_rank
equil_cycle_max
equil_linear_min
lagrange_list
lagrange_moles
lagrange_no_mol_deriv
lagrange_use_omni
max_linear_iters
max_quad_iters
moles_in
moles_out
phase_list
phase_separ_threshold
reactions
rotate_orthog_proj
use_numpy_lstsq
volume
VT_null
Notes
-----
Alternate potentials are specified by stipulating appropriate Lagrange
transformations of the Gibbs potential.
"""
def __init__(self, element_l=None, phase_l=None, lagrange_l=None):
self._element_list = element_l
self._phase_list = phase_l
self._lagrange_list = lagrange_l
self._bulk_comp = None
self._moles_in = 1.0e-5
self._moles_out = 1.0e-8
self._max_quad_iters = 100
self._max_linear_iters = 100
self._phase_separ_threshold = -0.1
self._entropy = False
self._volume = False
self._CTf = None
self._VT_null = None
self._reactions = None
self._lagrange_moles = None
self._lagrange_use_omni = True
self._rotate_orthog_proj = False
self._A_omni_inv = None
self._lagrange_no_mol_deriv = False
self._eps_linear = 10.0*np.finfo(float).eps
self._eps_quad_optimal = pow(np.finfo(float).eps, 2./3.)
self._eps_quad_suboptimal = np.sqrt(np.finfo(float).eps)
self._eps_rank = 10.0*np.finfo(float).eps
self._equil_cycle_max = 3
self._equil_linear_min = 5
self._eps_minimal_energy = np.sqrt(np.finfo(float).eps)
self._use_numpy_lstsq = True
phases.Phase.MINVAL = np.finfo(float).eps
@property
def element_list(self):
"""
A list of strings that identify the element basis for the system
For example,['H','C','O','Na','Mg','Al','Si','P','K','Ca','Ti','Cr',
'Mn','Fe','Co','Ni']
readonly
Returns
-------
A Python list
"""
return self._element_list
@property
def phase_list(self):
"""
A list of class instances for phases that are permitted to form
These instances must derive from the *PurePhase* or *SolutionPhase* classes,
which are defined in the *phases* module.
readonly
Returns
-------
A Python list
"""
return self._phase_list
@property
def lagrange_list(self):
"""
A list of tuples characterizing the Lagrange transformation of the
Gibbs free energy to form the operative thermodynamic potential
Each tuple has two terms:
1) Either of the following:
- A string with content 'T' or 'P'
- A dictionary keyed on element symbols with values corresponding to
stoichiometric coefficients of element chemical potentials. These
linear combinations of potentials are fixed by the constraint.
2) A function to compute values for the constraints specified in (1)
The function has a signature func(T, P, state), where *T* is temperature
in K, *P* is pressure in bars, and *state* is an instance of the EquilState
class.
readonly
Returns
-------
A Python list of tuples
"""
return self._lagrange_list
@property
def bulk_comp(self):
"""
Bulk composition of the system as moles of elements
readwrite
Returns
-------
numpy array
"""
return self._bulk_comp
@bulk_comp.setter
def bulk_comp(self, value):
assert type(value) is np.ndarray, \
'bulk_comp must be set to an numpy array'
assert value.size == len(self.element_list), \
'bulk_comp array must have same length as element_list'
self._bulk_comp = value
@property
def moles_in(self):
"""
Moles of phase added to the system on detection of saturation
readwrite
Returns
-------
Number of moles added (number)
"""
return self._moles_in
@moles_in.setter
def moles_in(self, value):
assert isinstance(value, (int, float)), \
'ERROR: moles_in must a a number.'
self._moles_in = value
@property
def moles_out(self):
"""
Minimum total moles of phase allowed in system
If there is less than this quantity, the phase is discarded.
readwrite
Returns
-------
Minimum moles allowed (number)
"""
return self._moles_out
@moles_out.setter
def moles_out(self, value):
assert isinstance(value, (int, float)), \
'ERROR: moles_in must a a number.'
self._moles_out = value
@property
def max_quad_iters(self):
"""
Maximum number of Newton quadratic minimization steps allowed in
computation
readwrite
Returns
-------
Number of steps allowed (int)
"""
return self._max_quad_iters
@max_quad_iters.setter
def max_quad_iters(self, value):
assert isinstance(value, int), \
'ERROR: max_quad_iters must be an integer.'
self._max_quad_iters = value
@property
def max_linear_iters(self):
"""
Maximum number of linear search steps associated with each quadratic
iteration
readwrite
Returns
-------
Number of linear search steps (int)
"""
return self._max_linear_iters
@max_linear_iters.setter
def max_linear_iters(self, value):
assert isinstance(value, int), \
'ERROR: max_linear_iters must be an integer.'
self._max_linear_iters = value
@property
def phase_separ_threshold(self):
"""
Minimum energy in Joules for phase separation
Gibbs free energy threshold that must be attained in order to add
another instance of a phase to the equilibrium assemblage. In the phase
unmixing (immiscibility) algorithm, candidate compositions are tested
and if one is found with a Gibbs free energy below the projected
tangent plane to the input composition by this amount, then the new
phase instance is added to the system. The value is in Joules and
should always be negative. Values larger than the default will likely
encourage the detection of false unmixing events; values more
negative will likely prevent the detection of unmixing and result in
metastable assemblages. Use caution when altering the default value.
Default is -0.1
readwrite
Returns
-------
Threshold value (number)
"""
return self._phase_separ_threshold
@phase_separ_threshold.setter
def phase_separ_threshold(self, value):
self._phase_separ_threshold = value
@property
def entropy(self):
"""
Indicates if entropy is an independent variable of the minimal potential
and if temperature is a dependent variable
True if yes; False if reverse.
readonly
Returns
-------
Flag (bool)
"""
return self._entropy
@property
def volume(self):
"""
Indicates if volume is an independent variable of the minimal potential
and if pressure is a dependent variable
True if yes; False if reverse.
readonly
Returns
-------
Flag (bool)
"""
return self._volume
@property
def CTf(self):
"""
Stoichiometric vectors of element concentrations that embody the
Lagrange constraints
Default is None
readonly
Returns
-------
np_array or None
"""
return self._CTf
@property
def VT_null(self):
"""
Orthogonal projection operator obtained from constraints on chemical
potentials derived from the lagrange_list entries
Default is None
readonly
Returns
-------
np array or None
"""
return self._VT_null
@property
def reactions(self):
"""
Balanced reactions pertaining to the Lagrange constraints
Default is None
readonly
Returns
-------
np.array or None
"""
return self._reactions
@property
def lagrange_moles(self):
"""
Moles of chemical potential entities stipulated in dictionaries,
which are contained in the lagrange_list constraints
Default is None
readonly
Returns
-------
np.array or None
"""
return self._lagrange_moles
@property
def lagrange_use_omni(self):
"""
If set to True, the algorithm uses the omnicomponent phase exclusively
to balance non-linear constraint reactions.
Default is True
readwrite
Returns
-------
Flag (bool)
"""
return self._lagrange_use_omni
@lagrange_use_omni.setter
def lagrange_use_omni(self, value):
self._lagrange_use_omni = value
@property
def rotate_orthog_proj(self):
"""
Prevents creation of a search direction for potential
minimization that has unwanted coupling of oxygen-bearing components
The basis vectors of the orthogonal projection matrix, VT_null,
generated from the lagrange_list attribute, are rotated to zero to
minimize the contribution of oxygen to the null space. This flag is
enabled to avoid creating a search direction for potential
minimization that has unwanted coupling of oxygen-bearing components
(for more information, see method _compute_null_space(...)). This
option is seldom needed and can be applied only if no omnicomponent
phase is present in the system.
Default is False
readwrite
Returns
-------
Flag (bool)
"""
return self._rotate_orthog_proj
@rotate_orthog_proj.setter
def rotate_orthog_proj(self, value):
self._rotate_orthog_proj = value
@property
def A_omni_inv(self):
"""
A matrix that transforms an array/matrix of mole numbers of elements to
an array/matrix of mole numbers of components of the omnicomponent
phase
This matrix is the inverse of a matrix that maps elemental
abundances to component mole numbers for the omnicomponent phase.
This property is available only if chemical potential constraints
are specified in lagrange_list and if there is an omnicomponent phase
in the system.
Default is None
readonly
Returns
-------
2-d nd_array
"""
return self._A_omni_inv
@property
def lagrange_no_mol_deriv(self):
"""
Produces a simplified gradient and hessian of the generalized
Khorzhinskii potential
Assume that the generalized Khorzhinkii potential is constructed so
that the imposed chemical potential is not a function of mole numbers
of any phase in the system. Alternatively, the imposed potential is
stoichiometrically assembled using reaction coefficients involving
equilibrium phases in the assemblage. This option produces a simplified
gradient and hessian of the generalized Khorzhinskii potential, but
does not affect construction of the equality constraint matrix nor the
Lagrange multiplier terms in the hessian of the Lagrangian function.
It is seldom necessary to set this flag to True.
Default is False
readwrite
Returns
-------
Flag (bool)
"""
return self._lagrange_no_mol_deriv
@lagrange_no_mol_deriv.setter
def lagrange_no_mol_deriv(self, value):
self._lagrange_no_mol_deriv = value
@property
def eps_linear(self):
"""
Convergence criteria for the norm of the linear projection phase of the
equilibrium calculation
readwrite
Returns
-------
Convergence tolerance (number)
"""
return self._eps_linear
@eps_linear.setter
def eps_linear(self, value):
assert isinstance(value, (int, float)), \
'ERROR: eps_linear must be a number.'
assert value > np.finfo(float).eps, \
'The value of eps_linear should be greater than machine ' \
'precision, ' + str(np.finfo(float).eps)
self._eps_linear = value
@property
def eps_quad_optimal(self):
"""
Convergence criteria for the norm of the quadratic projection phase of
the equilibrium calculation
readwrite
Returns
-------
Convergence tolerance (number)
"""
return self._eps_quad_optimal
@eps_quad_optimal.setter
def eps_quad_optimal(self, value):
assert isinstance(value, (int, float)), \
'ERROR: eps_quad_optimal must be a number.'
assert value > np.finfo(float).eps, \
'The value of eps_quad_optimal should be greater than machine ' \
'precision, ' + str(np.finfo(float).eps)
self._eps_quad_optimal = value
@property
def eps_quad_suboptimal(self):
"""
Relaxed convergence criteria for the norm of the quadratic projection
phase of the equilibrium calculation
readwrite
Returns
-------
Convergence tolerance (number)
"""
return self._eps_quad_suboptimal
@eps_quad_suboptimal.setter
def eps_quad_suboptimal(self, value):
assert isinstance(value, (int, float)), \
'ERROR: eps_quad_suboptimal must be a number.'
assert value > np.finfo(float).eps, \
'The value of eps_quad_suboptimal should be greater than machine ' \
'precision, ' + str(np.finfo(float).eps)
self._eps_quad_suboptimal = value
@property
def eps_rank(self):
"""
Tolerance for establishing the rank of the projected Hessian, which is
needed to compute a valid quadratic search direction
readwrite
Returns
-------
Tolerance (number)
"""
return self._eps_rank
@eps_rank.setter
def eps_rank(self, value):
assert isinstance(value, (int, float)), \
'ERROR: eps_rank must be a number.'
assert value > np.finfo(float).eps, \
'The value of eps_rank should be greater than machine ' \
'precision, ' + str(np.finfo(float).eps)
self._eps_rank = value
@property
def equil_cycle_max(self):
"""
Number of addition/removal cycles allowed for a phase before it is
suppressed from the equilibrium assemblage
readwrite
Returns
-------
Iteration limit (int)
"""
return self._equil_cycle_max
@equil_cycle_max.setter
def equil_cycle_max(self, value):
assert isinstance(value, int), \
'ERROR: equil_cycle_max must be an integer.'
assert value > 0, \
'The value of equil_cycle_max should be greater than zero'
self._equil_cycle_max = value
@property
def equil_linear_min(self):
"""
Number of successful linear minimization attempts that do not result in
a subsequent dimunition of the quadratic norm after which convergence is
accepted as a "minimal energy" condition.
readwrite
Returns
-------
Iteration limit (int)
"""
return self._equil_linear_min
@equil_linear_min.setter
def equil_linear_min(self, value):
assert isinstance(value, int), \
'ERROR: equil_linear_min must be an integer.'
assert value > 0, \
'The value of equil_linear_min should be greater than zero'
self._equil_linear_min = value
@property
def eps_minimal_energy(self):
"""
Tolerance for establishing criteria for a minimum in the system
potential.
Successive linear minimization attempts (equil_linear_min) that do not
result in a subsequent dimunition of the quadratic norm within this
tolerance result in convergence as a "minimal energy" condition.
readwrite
Returns
-------
Tolerance (number)
"""
return self._eps_minimal_energy
@eps_minimal_energy.setter
def eps_minimal_energy(self, value):
assert isinstance(value, (int, float)), \
'ERROR: eps_rank must be a number.'
assert value > np.finfo(float).eps, \
'The value of eps_rank should be greater than machine ' \
'precision, ' + str(np.finfo(float).eps)
self._eps_minimal_energy = value
@property
def use_numpy_lstsq(self):
"""
Flag to toggle method used to solve for quadratic search direction
The quadratic search direction, dn, is computed by solving a system of
equations given by H dn = -g, where H is the projected Hessian and g the
projected gradient of the system potential (e.g., the system Gibbs free
energy). If *use_numpy_lstsq* is True (default), the solution method is
numpy's linalg.lstsq method. If False, a *QR* decomposition method from
the Python package statsmodels is utilized. The numpy method uses SVD
decomposition and is slower, but more tolerant of rank deficient
solutions (e.g., near phase rule violations).
Returns
-------
Flag (bool)
"""
return self._use_numpy_lstsq
@use_numpy_lstsq.setter
def use_numpy_lstsq(self, value):
assert isinstance(value, bool), 'Value must be True or False.'
self._use_numpy_lstsq = value
################################
# Class private helper methods #
################################
def _print_matrix_repr(self, M):
if M.shape[0] == 0 or M.shape[1] == 0:
return
for i in range(0,M.shape[0]):
print (' ', end=' ')
for j in range(0,M.shape[1]):
print ('-', end=' ') if M[i][j] == 0 else print ('x', end=' ')
print (' ')
def _calc_sat_state_affinities(self, state, t, p, mu_elm, omni_phase=None,
debug=0):
"""
Determine saturation state affinities for all system phases with zero
moles.
"""
A_min = sys.float_info.max
A_min_name = None
for entry in state.phase_d.values():
name = entry['name']
if (name != omni_phase) and (state.tot_moles_phase(name) == 0.0):
if debug > 0:
print ('******************************** ')
print ('Calculating saturation state for ' + name)
c_mat = state.c_matrix_for_phase(name)
nc = entry['nc']
mu_end = np.zeros(nc)
for i in range(0,nc):
c_row = c_mat[i,:]
c_mask = np.not_equal(c_row,0)
valid = np.count_nonzero(c_row) == np.count_nonzero(
np.extract(c_mask, self.bulk_comp))
mu_end[i] = np.matmul(c_row, mu_elm) if valid else 0.0
if debug > 1:
print ('Target chemical potentials: ', mu_end)
# Now call a phase method that converts mu_elm to A and X
(A,X) = entry['obj'].affinity_and_comp(t, p, mu_end,
debug=(True if debug > 2 else False), method='special')
if debug > 0:
print ('Affinity, mole fraction', A, X)
print (' ')
entry['affinity'] = A
entry['mole_frac'] = X
if A < A_min:
A_min = A
A_min_name = name
return (A_min, A_min_name)
def _determine_phase_stability(self, state, t, p, phase_name, debug=0):
"""
Calculate if the specified phase is stable with respect to unmixing.
Returns:
--------
result: tuple
bool, ndarray - unmixing detected; None or composition of second phase
"""
if phase_name == state.omni_phase():
return False, None
phase = state.phase_d[phase_name]
moles = phase['moles']
if moles.size == 1:
return False, None
tot_moles = np.sum(moles)
if tot_moles <= 0:
return False, None
if debug > 0:
print ('')
print ('Unmixing calculation for', phase_name, moles/tot_moles)
mu0 = []
nc = phase['nc']
for i in range(0,nc):
x = np.full(nc,0.000001)
x[i] = 1.0-np.sum(x)
mu0.append(phase['obj'].gibbs_energy(t, p, mol=x))
gHat = phase['obj'].gibbs_energy(t, p, mol=moles)
dgdn = phase['obj'].gibbs_energy(t, p, mol=moles, deriv={'dmol':1})[0]
for i in range(0,nc):
gHat -= mu0[i]*moles[i]
dgdn[i] -= mu0[i]
gHat /= tot_moles
def deltaG(x, *args):
state, t, p, phase, moles, gHat, dgdn, mu0 = args
f = phase['obj'].gibbs_energy(t, p, mol=x)
df = phase['obj'].gibbs_energy(t, p, mol=x, deriv={'dmol':1})[0]
for i in range(0,phase['nc']):
f -= mu0[i]*x[i]
df[i] -= mu0[i]
f /= np.sum(x)
f -= gHat + np.matmul(x/np.sum(x)-moles/np.sum(moles), dgdn)
df -= dgdn
return f, df
bound_set = []
for i in range(0,nc):
bound_set.append((0.01,0.99))
lowest_fun = 0.0
lowest_comp = None
debug_flag = True if debug > 1 else False
for i in range(0,nc):
x = np.full(nc,0.05)
x[i] = 1.0-np.sum(x)
result = sp.optimize.minimize(deltaG, x,
args=(state, t, p, phase, moles, gHat, dgdn, mu0),
method='L-BFGS-B', jac=True,
bounds=bound_set,
options={'maxiter':250, 'disp':debug_flag})
if debug > 0:
print (result)
print ('')
if result.fun < lowest_fun:
lowest_fun = result.fun
lowest_comp = result.x
if lowest_fun < self.phase_separ_threshold:
return True, lowest_comp
else:
return False, None
def _add_phase_to_system(self, state, name):
"""
Add a small amount of phase 'name' to the system and maintain mass
balance by substracting amount added from the omnicomponent phase.
"""
omni_phase_name = state.omni_phase()
omni_phase = state.phase_d[omni_phase_name]
new_phase = state.phase_d[name]
for i in range(0,new_phase['nc']):
new_phase['moles'][i] = new_phase['mole_frac'][i]*self.moles_in
np.put(new_phase['mole_nz'],
np.flatnonzero(np.abs(new_phase['moles']) < 10.0*np.finfo(float).eps),
0.0)
c_new_phase = state.c_matrix_for_phase(name)
# determine elements associated with new_phase
elm = np.matmul(c_new_phase.T, new_phase['moles'])
# cast these elements into moles of the omnicomponent phase
cT = state.c_matrix_for_phase(omni_phase_name).T
cTinv = np.linalg.inv(cT) if cT.shape[0] == cT.shape[1] else np.linalg.pinv(cT)
comp = np.matmul(cTinv, elm)
# now subtract this quanity from the omnicomponent phase
omni_phase['moles'] = np.subtract(omni_phase['moles'], comp)
return omni_phase['obj'].test_endmember_comp(omni_phase['moles'])
def _remove_phase_from_system(self, state, name):
"""
Remove a phase 'name' from the system and maintain mass balance by
adding amount removed to the omnicomponent phase.
"""
omni_phase_name = state.omni_phase()
omni_phase = state.phase_d[omni_phase_name]
old_phase = state.phase_d[name]
c_old_phase = state.c_matrix_for_phase(name)
# determine elements associated with old_phase
elm = np.matmul(c_old_phase.T, old_phase['moles'])
# cast these elements into moles of the omnicomponent phase
cT = state.c_matrix_for_phase(omni_phase_name).T
cTinv = np.linalg.inv(cT) if cT.shape[0] == cT.shape[1] else np.linalg.pinv(cT)
comp = np.matmul(cTinv, elm)
# now add this quanity to the omnicomponent phase
omni_phase['moles'] = np.add(omni_phase['moles'], comp)
# finally, zero mole numbers of the phase removed from the system
for i in range(0,old_phase['nc']):
old_phase['moles'][i] = 0.0
return omni_phase['obj'].test_endmember_comp(omni_phase['moles'])
def _apply_non_linear_constraints(self, tp_l, state, debug):
"""
Enforce the non-linear constraints (entropy, volume, chemical
potential(s)) on the system.
"""
def linear_search(step, state, t, p, n_ref, row, f):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
corr = step*self.reactions[row][index]
entry['moles'][i] = n_ref[index] + corr
index += 1
dgdn = state.dGdn(t, p, element_basis=False)
result = np.matmul(self.reactions[row], dgdn)[0]
result -= f(t, p, state)
return np.sqrt(result*result)
def test_bounds_for_search(step, state, t, p, row):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
nc = entry['nc']
moles = np.empty(nc)
for i in range(0,nc):
corr = step*self.reactions[row][index]
moles[i] = entry['moles'][i] + corr
index += 1
if nc == 1:
if moles[0] < 0.0:
return False
else:
if not entry['obj'].test_endmember_comp(moles):
return False
return True
def root_target(x, state, t, p, c, f):
if c == 'T':
result = state.dGdT(x, p) - f(t, p, state)
Dresult = state.d2GdT2(x, p)
return result, Dresult
elif c == 'P':
result = state.dGdP(t, x) - f(t, p, state)
Dresult = state.d2GdP2(t, x)
return result, Dresult
else:
return None, None
t = tp_l[0]
p = tp_l[1]
row = 0
func_for_row = []
for c,f in self.lagrange_list:
if type(c) is dict:
if self.reactions.shape[0] > 1:
func_for_row.append(f)
continue
step_max = 0.5
while not test_bounds_for_search(step_max, state, t, p, row):
step_max *= 0.9
if step_max < sys.float_info.epsilon:
break
if debug > 1:
print ('Maximum linear step search length; ', step_max)
step_min = -0.5
while not test_bounds_for_search(step_min, state, t, p, row):
step_min *= 0.9
if step_min > -sys.float_info.epsilon:
break
if debug > 1:
print ('Minimum linear step search length; ', step_min)
n_ref = []
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
n_ref.append(entry['moles'][i])
n_ref = np.array(n_ref)
result = sp.optimize.minimize_scalar(linear_search,
bracket=(0,1), bounds=(step_min,step_max),
args=(state, t, p, n_ref, row, f), method='Bounded',
options={'maxiter':250, 'disp':debug, 'xatol':1e-8})
if debug > 1:
print ('Optimal linear search length:', result)
if np.absolute(result.fun) > 1.0:
print ('Imposed chemical potential constraints', end=' ')
print ('cannot be reconciled within 1 Joule.')
print ('Consider setting the "lagrange_use_omni"', end=' ')
print ('property to True.')
self._lagrange_moles[row] += result.x
row += 1
elif type(c) is str:
if self.entropy and self.volume:
if c == 'T':
tFun = f
elif c == 'P':
pFun = f
continue
result = sp.optimize.root_scalar(root_target,
args=(state, t, p, c, f),
method='newton',
x0=t if c == 'T' else p,
fprime=True,
maxiter=50)
if debug > 0:
print (result)
if c == 'T':
tp_l[0] = result.root
elif c == 'P':
tp_l[1] = result.root
if self.entropy and self.volume:
# tFun and pFun from above
# TBD
def roots_fun(x, state, t, p, tFun, pFun):
resT = state.dGdT(x[0], x[1]) - tFun(t, p, state)
DresTT = state.d2GdT2(x[0], x[1])
resP = state.dGdP(x[0], x[1]) - pFun(t, p, state)
DresPP = state.d2GdP2(x[0], x[1])
DresTP = state.d2GdTdP(x[0], x[1])
return [resT,resP], np.array([[DresTT,DresTP],[DresTP,DresPP]])
result = sp.optimize.root(roots_fun,
[t,p],
args=(state, t, p, tFun, pFun),
method='hybr',
jac=True)
if debug > 0:
print (result)
tp_l[0] = result.x[0]
tp_l[1] = result.x[1]
if len(func_for_row) > 0:
if debug > 0:
print ('Solving with multiple chemical potential constraints.')
def multi_search(step, state, t, p, n_ref, func_for_row):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
corr = 0.0
for j in range(0,len(func_for_row)):
corr += step[j]*self.reactions[j][index]
entry['moles'][i] = n_ref[index] + corr
index += 1
dgdn = state.dGdn(t, p, element_basis=False)
result = []
for row,f in enumerate(func_for_row):
result.append(np.matmul(self.reactions[row], dgdn)[0]
- f(t, p, state))
return result
def test_bounds_for_multi_search(step, state, t, p, func_for_row):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
nc = entry['nc']
moles = np.empty(nc)
for i in range(0,nc):
corr = 0.0
for j in range(0,len(func_for_row)):
corr += step[j]*self.reactions[j][index]
moles[i] = entry['moles'][i] + corr
index += 1
if nc == 1:
if moles[0] < 0.0:
return False
else:
if not entry['obj'].test_endmember_comp(moles):
return False
return True
n_ref = []
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
n_ref.append(entry['moles'][i])
n_ref = np.array(n_ref)
if debug > 0:
comp = state.oxide_comp('Liquid')
for key,value in comp.items():
print ("{0:<6s} {1:6.2f}".format(key, value), end=' ')
print ('')
result = sp.optimize.root(multi_search,
np.zeros(len(func_for_row)),
args=(state, t, p, n_ref, func_for_row),
method='hybr',
jac=False)
if debug > 0:
comp = state.oxide_comp('Liquid')
for key,value in comp.items():
print ("{0:<6s} {1:6.2f}".format(key, value), end=' ')
print ('')
print (result)
state.print_state()
for i in range(0,len(func_for_row)):
self._lagrange_moles[i] += result.x[i]
def _compute_a_and_qr(self, t, p, state, P_nz, debug):
"""
From the system state, calculate the mass balance constraint matrix and
its QR decomposition.
"""
filtr = lambda x : x if abs(x) > float(1000*sys.float_info.epsilon) else 0
vfiltr = np.vectorize(filtr, otypes=[float])
first = True
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0.0:
if first:
c_mat = state.c_matrix_for_phase(name)
first = False
else:
c_mat = np.vstack((c_mat, state.c_matrix_for_phase(name)))
A = c_mat.T
if self.lagrange_list is not None:
if debug > 1:
print ('A before projection and augmentation', A.shape)
if debug > 2:
print (A)
else:
self._print_matrix_repr(A)
A = np.matmul(self.A_omni_inv, A)
if debug > 1:
print ('A after projection and before augmentation', A.shape)
if debug > 2:
print (A)
else:
self._print_matrix_repr(A)
if self.CTf.shape[0] > 0:
ne = len(self.element_list)
omni_phase_name = state.omni_phase()
if self.lagrange_use_omni and (omni_phase_name is not None):
first = True
for phase in state.phase_d.values():
if state.tot_moles_phase(phase['name']) > 0:
if phase['name'] == omni_phase_name:
entry = np.zeros((ne,phase['nc']))
else:
entry = np.ones((ne,phase['nc']))
A_mask = entry if first else np.hstack((A_mask, entry))
first = False
A_react = np.ma.array(A, mask=A_mask, fill_value=0)
A_react = A_react.filled()
else:
A_react = np.copy(A)
if debug > 1:
print ('A reaction matrix', A_react.shape)
if debug > 2:
print (A_react)
else:
self._print_matrix_repr(A_react)
# Create a mass balance constraint on constrained entity
react, res, rank, s = np.linalg.lstsq(A_react,
np.matmul(self.A_omni_inv,self.CTf.T), rcond=None)
self._reactions = vfiltr(react.T)
if not self.entropy and not self.volume:
if debug > 0:
print (".....")
print ('Enforcing chemical potential constraints.')
self._apply_non_linear_constraints([t,p], state, debug)
if debug > 0:
print (".....")
if debug > 1:
print ('Balanced Lagrange reactions', self._reactions.shape)
print (self._reactions)
if debug > 2:
print ('... residuals', res.shape)
print (res)
print ('... rank', rank)
print ('... singular values', s.shape)
print (s)
if debug > 1:
dgdn = state.dGdn(t, p, element_basis=False)
deltaG = np.matmul(self.reactions, dgdn)
row = 0
for c,f in self.lagrange_list:
if type(c) is dict:
deltaG[row] -= f(t, p, state)
row += 1
print ('Lagrange chemical potential constraints',
deltaG.shape)
print (deltaG)
# Project the A matrix into the NULL space
Aproj = np.matmul(self.VT_null, A)
if debug > 1:
print ('Aproj', Aproj.shape)
if debug > 2:
print (Aproj)
else:
self._print_matrix_repr(Aproj)
# Add rows for the constraint
###### d2gdn2 = state.d2Gdn2(t, p, element_basis=True)
###### con_rows = np.matmul(self.CTf, d2gdn2)
d2gdn2 = state.d2Gdn2(t, p, element_basis=False)
con_rows = np.matmul(self._reactions, d2gdn2)
if debug > 2:
print ('Constraint rows', con_rows.shape)
print (con_rows)
Aproj = np.vstack((Aproj, con_rows))
if debug > 1:
print ('A proj augmented with constraints', Aproj.shape)
if debug > 2:
print (Aproj)
else:
self._print_matrix_repr(Aproj)
A = Aproj
if self.entropy and self.volume:
d2GdTdn = state.d2GdTdn(t, p)
d2GdT2 = state.d2GdT2(t, p)
d2GdPdn = state.d2GdPdn(t, p)
d2GdP2 = state.d2GdP2(t, p)
d2GdTdP = state.d2GdTdP(t, p)
A = np.vstack((A, d2GdTdn.T, d2GdPdn.T))
rows,cols = A.shape
col = np.zeros((rows,2))
col[-2][0] = d2GdT2
col[-1][1] = d2GdP2
col[-2][1] = d2GdTdP
col[-1][0] = d2GdTdP
A =np.hstack((A,col))
elif self.entropy:
d2GdTdn = state.d2GdTdn(t, p)
d2GdT2 = state.d2GdT2(t, p)
A = np.vstack((A, d2GdTdn.T))
rows,cols = A.shape
col = np.zeros((rows,1))
col[-1][0] = d2GdT2
A =np.hstack((A,col))
elif self.volume:
d2GdPdn = state.d2GdPdn(t, p)
d2GdP2 = state.d2GdP2(t, p)
A = np.vstack((A, d2GdPdn.T))
rows,cols = A.shape
col = np.zeros((rows,1))
col[-1][0] = d2GdP2
A = np.hstack((A,col))
if debug > 1:
print ('A augmented with constraints', A.shape)
if debug > 2:
print (A)
else:
self._print_matrix_repr(A)
# delete rows that have zero concentrations of elements in the system
if self.lagrange_list is not None:
bc = np.matmul(self.A_omni_inv, self.bulk_comp)
if self.CTf.shape[0] > 0:
bc = np.matmul(self.VT_null, bc)
A = np.delete(A, np.argwhere(bc == 0), axis=0)
else:
A = np.delete(A, np.argwhere(self.bulk_comp == 0), axis=0)
# delete columns that have zero concentrations of solution components
A = np.matmul(A, P_nz)
row,col = A.shape
rref = np.array(sym.Matrix(A).rref()[0]).astype(np.float64)
row_rank = np.count_nonzero(np.linalg.norm(rref, axis=1))
if row == row_rank:
df = col - row
else:
if debug > 1:
print ('A matrix has reduced row space by:', row-row_rank)
df = col - row_rank
R, Q = sp.linalg.rq(A, mode='full')
if debug > 1:
print ('RQ = A, R matrix', R.shape)
if debug > 2:
print (vfiltr(R))
else:
self._print_matrix_repr(vfiltr(R))
if debug > 1:
print ('RQ = A, Q matrix', Q.shape)
if debug > 2:
print (vfiltr(Q))
else:
self._print_matrix_repr(vfiltr(Q))
R11 = vfiltr(R[:,df:])
Q1 = vfiltr(Q[df:,:])
Q2 = vfiltr(Q[0:df,:])
return (A, df, Q1, Q2, R11)
def _compute_null_space(self, state, debug):
"""
Construct a projection operator to account for open system constraints
"""
ne = len(self.element_list)
CTf = np.empty((0,ne))
for c,f in self.lagrange_list:
if type(c) is str:
if c == 'T':
self._entropy = True
elif c == 'P':
self._volume = True
else:
assert(True,'Legendre transform must specify "T", "P", '
+ 'or a dictionary of chemical potential constraints.')
elif type(c) is dict:
row = np.zeros(ne)
for key,value in c.items():
index = self.element_list.index(key)
row[index] = value
CTf = np.vstack((CTf, row))
else:
assert(True,'Legendre transform must specify "T", "P", '
+ 'or a dictionary of chemical potential constraints.')
self._CTf = CTf
self._lagrange_moles = np.zeros(len(self.lagrange_list))
if state.omni_phase() is not None:
self._A_omni_inv = np.linalg.inv(
state.c_matrix_for_phase(state.omni_phase()).T)
else:
self._A_omni_inv = np.eye(ne)
if debug > 1:
print ('CTf', CTf.shape)
print (CTf)
print ('A_omni_inv', self.A_omni_inv.shape)
if debug > 2:
print (self.A_omni_inv)
else:
self._print_matrix_repr(self.A_omni_inv)
CTf_pad = np.pad(CTf,((0,CTf.shape[1]-CTf.shape[0]),(0,0)),mode='constant')
U,S,VT = np.linalg.svd(np.matmul(CTf_pad,self.A_omni_inv.T))
if debug > 1:
print ('U', U.shape)
if debug > 2:
print (U)
else:
self._print_matrix_repr(U)
print ('S', S.shape)
if debug > 2:
print (S)
print ('VT', VT.shape)
if debug > 2:
print (VT)
else:
self._print_matrix_repr(VT)
rank = np.count_nonzero(S)
self._VT_null = VT[rank:,:]
if debug > 1:
print ('VT null space', self._VT_null.shape)
if debug > 2:
print (self._VT_null)
else:
self._print_matrix_repr(self._VT_null)
if self.rotate_orthog_proj and state.omni_phase() is None:
try:
row_ind = self.element_list.index("O")
except ValueError:
if debug > 1:
print ("ERROR! Oxygen is not present in the system and ")
print (" a null-space rotation on O is specified.")
return
if debug > 1:
print ("----------")
print ("Adjusting the null space basis for oxygen.")
ns = self._VT_null.T
filtr = lambda x : x if abs(x) > float(
1000*sys.float_info.epsilon) else 0
vfiltr = np.vectorize(filtr, otypes=[float])
# zero the null space bassis coefficient at col_ind and row_ind
def zeros(deg_a, v_a, u, ns, col_ind_a, row_ind, ret_matrix):
ns_p = ns
for (deg,v,col_ind) in zip(deg_a,v_a,col_ind_a):
theta = (deg/180.0)*np.pi
P = np.eye(v.shape[0]) + np.sin(theta)*(np.outer(u,v)
-np.outer(v,u)) + (np.cos(theta)-1)*(np.outer(u,u)
-np.outer(v,v))
ns_p = np.matmul(P,ns_p)
if ret_matrix:
return ns_p
else:
result = []
for col_ind in col_ind_a:
result.append(ns_p[row_ind][col_ind])
return np.array(result)
u = np.zeros(ns.shape[0])
u[row_ind] = 1
v_a = []
col_ind_a = []
guess = []
for col in range(0,ns.shape[1]):
if ns[row_ind][col] != 0.0:
v_a.append(ns[:,col])
col_ind_a.append(col)
guess.append(0)
result = sp.optimize.root(zeros,
args=(v_a, u, ns, col_ind_a, row_ind, False),
x0=np.array(guess))
if debug > 1:
print ("... rotation algoritm output:")
print (result)
ns_p = vfiltr(zeros(result.x, v_a, u, ns, col_ind_a, row_ind, True))
self._VT_null = ns_p.T
if debug > 1:
print ('rotated null space basis', self._VT_null.shape)
if debug > 2:
print (self._VT_null)
else:
self._print_matrix_repr(self._VT_null)
print ("----------")
def _augment_gradient_hessian(self, t, p, g, H, state, debug):
"""
Ammend the gradient and Hessian of the Gibbs free energy to account for
the Khorzhinskii corections.
Returns
-------
result: tuple
corrected (g, H)
"""
dgdn = np.copy(g)
d2gdn2 = np.copy(H)
rank_H = H.shape[0]
idx = 0
for c,f in self.lagrange_list:
if type(c) is dict:
# determine molar equivalent of open system reactant
moles = state.moles_v(self.reactions[idx])
# determine the first order correction to the gradient
gAdd_1 = np.matmul(self.reactions[idx], dgdn)[0]
gAdd_1 *= self.reactions[idx]
gAdd_1 = np.reshape(gAdd_1, (gAdd_1.shape[0],1))
if debug > 2:
print ('moles', moles)
print ('1st gradient addition', gAdd_1.shape)
print (-gAdd_1)
# determine the second order correction to the gradient
if not self.lagrange_no_mol_deriv:
gAdd_2 = np.matmul(self.reactions[idx], d2gdn2)
gAdd_2 = np.reshape(gAdd_2, (gAdd_2.shape[0],1))
if debug > 2:
print ('2nd gradient addition', gAdd_2.shape)
print (-moles*gAdd_2)
else:
gAdd_2 = np.zeros(g.shape)
# add contributions to gradient
g = g - gAdd_1 - moles*gAdd_2
# add contributions to Hessian
# ... first, index the structure of the hessian
phase_info = []
offset = 0
for entry in state.phase_d.values():
if state.tot_moles_phase(entry['name']) > 0.0:
for idx2 in range(0,entry['nc']):
phase_info.append((entry['name'],offset,idx2))
offset += entry['nc']
if debug > 1:
print ('phase_info', len(phase_info), 'reactions',
self.reactions.shape)
print (phase_info)
# ... second, build the Hessian additions
if not self.lagrange_no_mol_deriv:
hAdd_1 = np.outer(self.reactions[idx],gAdd_2)
hAdd_2 = np.outer(gAdd_2, self.reactions[idx])
hAdd_3 = np.zeros(H.shape)
for idx2,coeff in enumerate(self.reactions[idx]):
if coeff != 0:
name,offset,col = phase_info[idx2]
HAdd = coeff*state.d3Gdn3(t, p, name, cmp=col)
idx3 = rank_H - HAdd.shape[0] - offset
hAdd_3 += np.pad(HAdd,((offset,idx3),(offset,idx3)),
'constant',constant_values=0)
if debug > 2:
print ('1st hessian addition', hAdd_1.shape)
print (-hAdd_1)
print ('2nd hessian addition', hAdd_2.shape)
print (-hAdd_2)
print ('3rd hessian addition', hAdd_3.shape)
print (-moles*hAdd_3)
H += -hAdd_1 - hAdd_2 - moles*hAdd_3
idx += 1
if self.entropy and self.volume:
g = g - t*state.d2GdTdn(t, p)
g = g - p*state.d2GdPdn(t, p)
d2gdtdp = state.d2GdTdP(t, p)
g = np.vstack((g, -t*state.d2GdT2(t, p) - p*d2gdtdp))
g = np.vstack((g, -p*state.d2GdP2(t, p) - t*d2gdtdp))
H = H - t*state.d3GdTdn2(t, p) - p*state.d3GdPdn2(t, p)
d3gdt2dn = state.d3GdT2dn(t, p)
d3gdp2dn = state.d3GdP2dn(t, p)
d3gdtdpdn = state.d3GdTdPdn(t, p)
colT = -t*d3gdt2dn - p*d3gdtdpdn
colP = -p*d3gdp2dn - t*d3gdtdpdn
H = np.hstack((H, colT, colP))
d3gdt2dp = state.d3GdT2dP(t,p)
d3gdtdp2 = state.d3GdTdP2(t,p)
termTT = -state.d2GdT2(t,p) - t*state.d3GdT3(t,p) - p*d3gdt2dp
termPP = -state.d2GdP2(t,p) - p*state.d3GdP3(t,p) - t*d3gdtdp2
termTP = -state.d2GdTdP(t,p) - t*d3gdt2dp - p*d3gdtdp2
rowT = np.hstack((colT.T, np.array([termTT],ndmin=2),
np.array([termTP],ndmin=2)))
rowP = np.hstack((colP.T, np.array([termTP],ndmin=2),
np.array([termPP],ndmin=2)))
H = np.vstack((H, rowT, rowP))
elif self.entropy:
g = g - t*state.d2GdTdn(t, p)
g = np.vstack((g, -t*state.d2GdT2(t, p)))
H = H - t*state.d3GdTdn2(t, p)
col = -t*state.d3GdT2dn(t, p)
H = np.hstack((H, col))
term = -state.d2GdT2(t,p) - t*state.d3GdT3(t,p)
row = np.hstack((col.T, np.array([term],ndmin=2)))
H = np.vstack((H,row))
elif self.volume:
g = g - p*state.d2GdPdn(t, p)
g = np.vstack((g, -p*state.d2GdP2(t, p)))
H = H - p*state.d3GdPdn2(t, p)
col = -p*state.d3GdP2dn(t, p)
H = np.hstack((H, col))
term = -state.d2GdP2(t,p) - p*state.d3GdP3(t,p)
row = np.hstack((col.T, np.array([term],ndmin=2)))
H = np.vstack((H,row))
if debug > 1:
print ('Augmented g', g.shape)
if debug > 2:
print (g)
print ('Augmented H', H.shape)
if debug > 2:
print (H)
else:
self._print_matrix_repr(H)
return (g,H)
def _compute_lagrange_multipliers(self, g, A, debug):
"""
Estimate Lagrange multipliers from the gradient and equality constraint
derivative matrix.
"""
soln, res, rank, s = np.linalg.lstsq(A.T, g, rcond=None)
if debug > 1:
print ('Lagrange multiplier solution', rank)
print ('... solution', soln.shape)
print (soln)
if debug > 2:
print ('... residuals', res.shape)
print (res)
print ('... singular values', s.shape)
print (s)
return soln
def _augment_hessian_return_wronskian(self, t, p, H, l_mult, state, debug):
"""
Augment the Hessian matrix with Lagrange multiplier terms for the
non-linear constraints.
"""
if self.reactions is not None:
rows = self.reactions.shape[0]
row = 0
for i in range(0,rows):
lagrange = l_mult[l_mult.size-rows+row]
react = self.reactions[i]
col = 0
result = []
if debug > 1:
print ('Lagrange multiplier:', lagrange)
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0.0:
nc = entry['nc']
if entry['nc'] == 1:
coeff = react[col]
mat = coeff*np.array([[0.0]])
col += 1
else:
mat = np.zeros((nc,nc))
for j in range(0,nc):
if react[col] != 0.0:
coeff = react[col]
mat_r = state.d3Gdn3(t, p, phase=name, cmp=j)
mat = np.add(mat, coeff*mat_r)
col += 1
result.append(mat)
result = -lagrange*sp.linalg.block_diag(*result)
if debug > 1:
print ('Hessian addition:', result.shape)
if debug > 2:
print (result)
H = np.add(H, result)
row += 1
if self.entropy and self.volume:
lam_T = l_mult[-2]
lam_P = l_mult[-1]
result = -lam_T*state.d3GdTdn2(t, p) - lam_P*state.d3GdPdn2(t, p)
d3gdtdpdn = state.d3GdTdPdn(t,p)
col_T = -lam_T*state.d3GdT2dn(t,p) - lam_P*d3gdtdpdn
col_P = -lam_T*d3gdtdpdn - lam_P*state.d3GdP2dn(t,p)
result = np.hstack((result, col_T, col_P))
d3gdt2dp = state.d3GdT2dP(t,p)
d3gdtdp2 = state.d3GdTdP2(t,p)
term_TT = -lam_T*state.d3GdT3(t,p) - lam_P*d3gdt2dp
term_TP = -lam_T*d3gdt2dp - lam_P*d3gdtdp2
term_PP = -lam_T*d3gdtdp2 - lam_P*state.d3GdP3(t,p)
row_T = np.hstack((col_T.T, np.array([term_TT],ndmin=2),
np.array([term_TP],ndmin=2)))
row_P = np.hstack((col_P.T, np.array([term_TP],ndmin=2),
np.array([term_PP],ndmin=2)))
result = np.vstack((result, row_T, row_P))
if debug > 1:
print ('Hessian addition (S,V):', result.shape)
if debug > 2:
print (result)
H = np.add(H, result)
elif self.entropy:
lam_T = l_mult[-1]
result = -lam_T*state.d3GdTdn2(t, p)
col_T = -lam_T*state.d3GdT2dn(t,p)
result = np.hstack((result, col_T))
term_TT = -lam_T*state.d3GdT3(t,p)
row_T = np.hstack((col_T.T, np.array([term_TT],ndmin=2)))
result = np.vstack((result, row_T))
if debug > 1:
print ('Hessian addition (S):', result.shape)
if debug > 2:
print (result)
H = np.add(H, result)
elif self.volume:
lam_P = l_mult[-1]
result = -lam_P*state.d3GdPdn2(t, p)
col_P = -lam_P*state.d3GdP2dn(t,p)
result = np.hstack((result, col_P))
term_PP = -lam_P*state.d3GdP3(t,p)
row_P = np.hstack((col_P.T, np.array([term_PP],ndmin=2)))
result = np.vstack((result, row_P))
if debug > 1:
print ('Hessian addition (V):', result.shape)
if debug > 2:
print (result)
H = np.add(H, result)
return H
########################
# Class public methods #
########################
[docs] def mu0O2(self, t, p):
"""
Calculates the chemical potential of oxygen gas in the standard state
of unit fugacity at one bar and any tenperature
Parameters
----------
t : float
Temperature in Kelvins
p : float
Pressure in bars
Returns
-------
result : float
standard state chemical potential in Joules/mole of O2 gas
Notes
-----
Algorithm from Berman (1988).
"""
tr = 298.15
hs = 23.10248*(t-tr) + 2.0*804.8876*(np.sqrt(t)-np.sqrt(tr)) \
- 1762835.0*(1.0/t-1.0/tr) - 18172.91960*np.log(t/tr) \
+ 0.5*0.002676*(t*t-tr*tr)
ss = 205.15 + 23.10248*np.log(t/tr) \
- 2.0*804.8876*(1.0/np.sqrt(t)-1.0/np.sqrt(tr)) \
- 0.5*1762835.0*(1.0/(t*t)-1.0/(tr*tr)) \
+ 18172.91960*(1.0/t-1.0/tr) + 0.002676*(t-tr)
return hs - t*ss
[docs] def log10NNO(self, t, p):
"""
Calculates the base 10 logarithm of oxygen fugacity along the nickel-
nickel oxide buffer
Parameters
----------
t : float
Temperature in Kelvins
p : float
Pressure in bars
Returns
-------
result : float
log (10) f O2
Notes
-----
Algorithm from O'Neill and Pownceby (1993, Contributions to Mineralogy
and Petrology, 114, 296-314) using the pressure correction suggested by
Frost (1991, Mineralogical Society of America, Reviews in Mineralogy,
v. 25, 1-9)
"""
return -25018.7/t + 12.981 + 0.046*(p-1.0)/t - 0.5117*np.log(t)
[docs] def muNNO(self, t, p, delta=0.0):
"""
Calculates the excess chemical potential of oxygen along the nickel-
nickel oxide (+ delta offset) buffer
Parameters
----------
t : float
Temperature in Kelvins
p : float
Pressure in bars
delta : float, default 0.0
Offset in base 10 log units relative to the nickel-nickel oxide
oxygen buffer
Returns
-------
result : float
Excess chemical potential of oxygen in Joules/mole of O2
Notes
-----
See definition of function log10NNO(t, p)
"""
return 8.3144598*t*np.log(10.0)*(self.log10NNO(t, p) + delta)
[docs] def kc_ferric_ferrous(self, t, p, m, mtype='components', compute='logfO2',
deltaNNO=0.0, debug=0):
"""
Calculates oxygen fugacity or ferric-ferrous ratio for silicate melts
using the Kress and Carmichael (1991) calibration
Parameters
----------
t : float
Temperature in Kelvins
p : float
Pressure in bars
m : numpy ndarray
An array of moles of endmember liquid components correcponding to
the model of Ghiorso and Sack (1995) (m has length 15) or the
model of Ghiorso and Gualda (2015) (m has length 16)
mtype : str, default 'components'
Type of values in m.
compute : str, default 'logfO2'
Type of output requested, see Returns.
deltaNNO : float, default 0.0
If ferric-ferrous computation if requested (see compute), the ratio
will be computed at log 10 f O2 = nickel-nickel oxide + deltaNNO
debug : int, default 0
Level of detail printed by the method:
- 0, no information
- 1, minimal progress information
- 2, normal debugint output level
- 3, verbose debuging output level
Returns
-------
result : float
log 10 f O2, if compute is set to 'logfO2' (default)
excess chemical potential of O2, if compute is set to 'chem_pot'
result : numpy ndarray
array of oxide values, with the computed ferric-ferrous ratio
appropriate to the imposed log f O2. The oxides are in standard
order: SiO2, TiO2, Al2O3, Fe2O3, Cr2O3, FeO, MnO, MgO, NiO, CoO,
CaO, Na2O, K2O, P2O5, H2O, [CO2]
"""
assert isinstance(m, np.ndarray), 'm must be an numpy array.'
assert m.size == 15 or m.size == 16, \
'm must be a 1-d numpy array of length 15 or 16'
assert mtype == 'components', 'the value of mtype must be "components"'
assert compute == 'logfO2' or compute == 'chem_pot' or \
compute == 'oxides', \
'the value of compute must be "logfO2" or "chem_pot" or "oxides"'
t0 = 1673.15
a = 0.196
b = 1.1492e4
c = -6.675
e = -3.364
f = -7.01e-7 * 1.0e5
g = -1.54e-10 * 1.0e5
h = 3.85e-17 * 1.0e5 * 1.0e5
d = np.array([0, 0, -2.243, 0, 0, -1.828, 0, 0, 0, 0, 3.201, 5.854,
6.215, 0, 0, 0])
# 'SiO2', 'TiO2', 'Al2O3', 'Fe2O3', 'MgCr2O4', 'Fe2SiO4', 'MnSi0.5O2',
# 'Mg2SiO4', 'NiSi0.5O2', 'CoSi0.5O2', 'CaSiO3', 'Na2SiO3', 'KAlSiO4',
# 'Ca3(PO4)2', 'H2O', 'CO2'
ox = np.array([m[0]+m[5]+m[6]/2.+m[7]+m[8]/2.+m[9]/2.+m[10]+m[11]+m[12], # SiO2
m[1], # TiO2
m[2]+m[12]/2., # Al2O3
m[3], # Fe2O3
m[4], # Cr2O3
m[5]*2., # FeO
m[6], # MnO
m[4]+m[7]*2.0, # MgO
m[8], # NiO
m[9], # CoO
m[10]+m[13]*3., # CaO
m[11], # Na2O
m[12]/2., # K2O
m[13], # P2O5
m[14] # H2O
])
if debug > 0:
print ('kc Fe3+/Fe2+ input grams Fe2O3, FeO',
ox[3]*core.chem.oxide_props['molwt'][3],
ox[5]*core.chem.oxide_props['molwt'][5])
if m.size == 15:
ox = np.append(ox, 0.0)
elif m.size == 16:
ox = np.append(ox, m[15]) # CO2
tot = np.sum(ox) + m[3]
if (m[3] == 0.0 and m[5] == 0.0) or (tot == 0.0):
if debug > 0:
print ('kc Fe3+/Fe2+ no iron in input composition.')
if compute == 'logfO2' or compute == 'chem_pot':
return 0.0
else:
return ox
if compute == 'logfO2' or compute == 'chem_pot':
temp = b/t + c + e*(1.0-t0/t - np.log(t/t0)) + f*p/t \
+ g*(t-t0)*p/t + h*p*p/t
temp += (np.dot(ox, d) + 2.0*d[5]*ox[3] - d[3]*ox[3])/tot
logfo2 = (np.log(ox[3]/ox[5]) - temp)/(a*np.log(10.0))
if debug > 0:
print ('kc Fe3+/Fe2+ log fO2', logfo2)
return logfo2 if compute == 'logfO2' \
else 8.3144598*t*np.log(10.0)*logfo2
elif compute == 'oxides':
ox[5] += 2.0*ox[3]
ox[3] = 0.0
logfO2 = self.log10NNO(t, p) + deltaNNO
temp = a*np.log(10.0)*logfO2 + b/t + c + e*(1.0-t0/t \
- np.log(t/t0)) + f*p/t + g*(t-t0)*p/t + h*p*p/t
temp += np.dot(ox, d)/tot
temp = np.exp(temp)
ox[3] = temp*ox[5]/(1.0 + 2.0*temp)
ox[5] -= 2.0*ox[3]
if debug > 0:
print ('kc Fe3+/Fe2+ comp grams Fe2O3, FeO',
ox[3]*core.chem.oxide_props['molwt'][3],
ox[5]*core.chem.oxide_props['molwt'][5])
return ox
else:
return None
[docs] def kc_print_state(self, state=None, silent=False):
"""
Prints information about the current system state in terms of the Kress
and Carmichael (1991) ferrous-ferric equilibrium model
Parameters
----------
state : EquilState, default None
An instance of the EquilState class generated by this method. This
parameter is only specified for a sequence of equilibrium
calculations where the state of the system is initialized from a
previous computation.
silent : bool, default False
Flag to silence printing
Returns
-------
result : tuple or None
Returns None if state is None or the system does not contain an
omnicomponent phase name 'Liquid', else returns a tuple with the
computer log 10 fO2 of the liquid phase relative to the NNO oxygen
buffer, the value of log 10 fO2 on the buffer, and the total number
of moles of oxygen in the system
"""
if state is None:
print ('Current system "state" must be provided.')
return None
if 'Liquid' not in state.phase_d:
print ('Current system must contain the phase "Liquid".')
return None
t = state.temperature
p = state.pressure
moles = state.phase_d['Liquid']['moles']
logfO2 = self.kc_ferric_ferrous(t, p, moles)
NNOfO2 = self.log10NNO(t, p)
moles_O = state.tot_moles_elements()[state.element_l.index(8)]
if not silent:
print ('System log 10 fO2', logfO2-NNOfO2, 'realtive to NNO.')
print ('System log 10 NNO', NNOfO2)
print ('Moles of O in system', moles_O)
return (logfO2-NNOfO2, NNOfO2, moles_O)
[docs] def execute(self, t=1000.0, p=1000.0, bulk_comp=None, state=None,
con_deltaNNO=None, debug=0, stats=False):
"""
Calculates an equilibrium assemblage, returning the results as an
instance of the EquilState class
Parameters
----------
t : float, default 1000.0
Temperature in Kelvins
p : float, default 1000.0
Pressure in bars
bulk_comp : numpy array, default None
Bulk composition of the system. Array of element concentrations,
of the length and order of self.element_list
state : EquilState, default None
An instance of the EquilState class generated by this method. This
parameter is only specified for a sequence of equilibrium
calculations where the state of the system is initialized from a
previous computation.
con_deltaNNO : float or None
A non-None value for this parameter is ignored unless the system
contains an omnicomponnet liquid phase of either 15 or 16 endmember
components modeled after Ghiorso and Sack (1995) or Ghiorso and
Gualda (2015), i.e. a MELTS silicate liquid thermodynamic model. If
a value is set, the liquid is constrained to follow the nickel-
nickel oxide oxygen buffer plus the offset value, con_deltaNNO,
which is specified in log 10 fO2 units. For example, a value of 1.0
will force the system to equilibrate on the NNO+1 oxygen buffer by
adjusting the ratio of ferric and ferrous iron according to the
Kress and Carmichael (1991) calibration. Setting a value permits
the system to be open to oxygen transfer; the bulk moles of
oxygen in the system is no longer constrained by input bulk
composition.
debug : int, default 0
Level of detail printed by the method:
- 0, no information
- 1, minimal progress information
- 2, normal debugint output level
- 3, verbose debuging output level
stats : bool, default False
Toggle printing of statistics associated with calculation progress
Returns
-------
state: EquilState
An instance of the EquilState class that contains the equilibrium
state of the system
Notes
-----
Both the bulk_comp and state parameters cannot be set simultaneously.
"""
if state is not None and bulk_comp is not None:
assert False, \
'Both "bulk_comp" and "state" parameters cannot be specified.'
if bulk_comp is not None:
self.bulk_comp = bulk_comp
if self.bulk_comp is None and state is None:
print ('Bulk composition of the system must be set.')
return None
# Initialize the EquilState class instance if one is not specified
require_quad_pass_before_phase_addition = False
if state is None:
state = EquilState(self.element_list, self.phase_list)
# Currently, one phase must be omnicomponent
omni_phase = state.omni_phase()
if omni_phase is None:
print ("There is no omnicomponent phase in the system.")
return None
# Assume the system inititally has just the omnicomponent phase
state.set_phase_comp(omni_phase, self.bulk_comp,
input_as_elements=True)
force_once_quad_pass = False
state.temperature = t
state.pressure = p
if self.lagrange_list is not None:
self._compute_null_space(state, debug)
force_once_quad_pass = True
if not self.entropy and not self.volume:
#imposed chemical potential constraints
require_quad_pass_before_phase_addition = True
else:
omni_phase = state.omni_phase()
force_once_quad_pass = True
state.temperature = t
state.pressure = p
if debug > 0:
print (omni_phase + ' is the omnicomponent phase.')
ne = len(self.element_list)
if self.entropy or self.volume:
tp_l = [t, p]
self._apply_non_linear_constraints(tp_l, state, debug)
(t,p) = tuple(tp_l)
state.temperature = t
state.pressure = p
if debug > 0 and self.entropy:
print ('Final corrected temperature', t)
if debug > 0 and self.volume:
print ('Final corrected pressure', p)
if con_deltaNNO is not None:
assert omni_phase is not None, \
'Specifying a value for con_deltaNNO requires an ' \
+ 'omnicomponent phase in the assemblage.'
assert omni_phase == 'Liquid', \
'The omnicomponent phase must be named "Liquid" ' \
+ 'and must inherit from the silicate liquid model of ' \
+ 'Ghiorso and Sack (1995) or Ghiorso and Gualda (2015).'
omni_nc = state.phase_d['Liquid']['nc']
assert omni_nc == 15 or omni_nc == 16, \
'Liquid phase must have 15 or 16 endmember components.'
###########################
# Calculation starts here #
###########################
while True:
# Special case: adjust ferric-ferrous of silicate liquid
if con_deltaNNO is not None:
omni_moles = state.phase_d['Liquid']['moles']
omni_oxide = self.kc_ferric_ferrous(t, p, omni_moles,
compute='oxides', deltaNNO=con_deltaNNO, debug=debug)
omni_moles, oxide_res = \
state.phase_d['Liquid']['obj'].calc_endmember_comp(
mol_oxide_comp=omni_oxide, method='intrinsic',
output_residual=True)
state.set_phase_comp(omni_phase, omni_moles)
# Obtain chemical potentials of elements in the system
mu_elm = state.dGdn(t, p, element_basis=True, use_omni_phase=True)
# If a phase has zero moles, calculate its chemical affinity
A_min,A_min_name = self._calc_sat_state_affinities(state, t, p,
mu_elm, omni_phase=omni_phase, debug=debug)
# if a phase is supersaturated, add it to the system
if A_min >= 0.0 and not force_once_quad_pass:
if debug > 0:
print ('Stable phase assemblage computed. Exiting.')
break
force_once_quad_pass = False
if A_min < 0.0 and not require_quad_pass_before_phase_addition:
if debug > 0:
print ('Adding phase ', A_min_name,
' to system. with affinity = ', A_min)
if stats:
print ('Add:', A_min_name)
okay = self._add_phase_to_system(state, A_min_name)
if okay and debug > 1:
print ('... phase successfully added.')
elif not okay:
print ('... phase addition unsuccessful. Exiting.')
break
require_quad_pass_before_phase_addition = False
quad_loop = True
quad_iter = 0
quad_energies = []
while quad_loop:
# Deal with equality/inequality constraints
if (quad_iter == 0) or self.lagrange_list is not None:
"""
Create projection operator to strip columns from to
account for missing composition variables
"""
result = []
for entry in state.phase_d.values():
if state.tot_moles_phase(entry['name']) > 0.0:
nc = entry['nc']
result.append(np.reshape(entry['mole_nz'],(nc,1)))
P_nz = np.vstack(tuple(result))
if self.entropy:
P_nz = np.vstack((P_nz, np.ones(1)))
if self.volume:
P_nz = np.vstack((P_nz, np.ones(1)))
P_nz = np.diag(P_nz[:,0])
"""
now remove zero columns of the projection matrix
taken from: https://stackoverflow.com/questions/51769962/
find-and-delete-all-zero-columns-from-numpy-array-using-fancy-indexing/51770365
"""
P_nz = np.delete(P_nz, np.argwhere(np.all(P_nz[..., :] == 0,
axis=0)), axis=1)
if debug > 1:
print ('Zero element projection matrix', P_nz.shape)
if debug > 2:
print (P_nz)
else:
self._print_matrix_repr(P_nz)
A, df, Q1, Q2, R11 = self._compute_a_and_qr(t, p, state,
P_nz, debug)
if debug > 0:
print ('Exit constraint matrix and RQ decomposition.')
if debug > 1:
print ('... df', df, 'A', A.shape)
if debug > 2:
print (A)
else:
self._print_matrix_repr(A)
if debug > 1:
print ('... Q1', Q1.shape)
if debug > 2:
print (Q1)
else:
self._print_matrix_repr(Q1)
if debug > 1:
print ('... Q2', Q2.shape)
if debug > 2:
print (Q2)
else:
self._print_matrix_repr(Q2)
if debug > 1:
print ('... R11', R11.shape)
if debug > 2:
print (R11)
else:
self._print_matrix_repr(R11)
# Compute gradient and Hessian
g = state.dGdn(t, p, element_basis=False)
H = state.d2Gdn2(t, p, element_basis=False)
if debug > 1:
print ('Gradient', g.shape)
if debug > 2:
print (g)
if debug > 1:
print ('Hessian', H.shape)
if debug > 2:
print (H)
else:
self._print_matrix_repr(H)
if self.lagrange_list is not None:
g,H = self._augment_gradient_hessian(t, p, g, H, state,
debug)
l_mult = self._compute_lagrange_multipliers(
g if P_nz.shape[0] == P_nz.shape[1] else np.matmul(
P_nz.T, g), A, debug)
H = self._augment_hessian_return_wronskian(t, p, H,
l_mult, state, debug)
if debug > 1:
print ('Final Hessian', H.shape)
if debug > 2:
print (H)
else:
self._print_matrix_repr(H)
# projection gradient and Hessian into the null space
if P_nz.shape[0] > P_nz.shape[1]:
g = np.matmul(P_nz.T, g)
H = np.matmul(P_nz.T, np.matmul(H, P_nz))
g_p = np.matmul(Q2, g)
H_p = np.matmul(np.matmul(Q2, H), Q2.T)
if debug > 1:
print ('Projected gradient:', g_p.shape)
if debug > 2:
print (g_p)
if debug > 1:
print ('Projected Hessian:', H_p.shape)
if debug > 2:
print (H_p)
else:
self._print_matrix_repr(H_p)
# Exit if the problem is trivial
if H_p.size == 0 and g_p.size == 0:
quad_normal_exit = True
break
"""
Solve the null space problem. Could use self.eps_rank here for
the condition number, but the default value computed setting
rcond=None in linalg.lstsq is usually optimal
"""
if self.use_numpy_lstsq:
result, residuals, rank, S = np.linalg.lstsq(H_p, -g_p,
rcond=None)
if debug > 0:
print ('')
print ('Quadratic solution, SVD algorithm, rank', rank)
if debug > 1:
print (result)
if debug > 2:
print ('... residuals:')
print (residuals)
print ('... S:')
print (S)
else:
result = sm.OLS(-g_p, H_p, hasconst=False).fit(method='qr')
if debug > 0:
print ('')
print ('Quadratic solution, QR algorithm.')
if debug > 1:
print (result.params.shape)
print (result.params)
if debug > 2:
print (result.summary())
result = np.reshape(result.params,(result.params.shape[0],1))
n2 = np.matmul(Q2.T, result)
if P_nz.shape[0] > P_nz.shape[1]:
n2 = np.matmul(P_nz, n2)
if debug > 0:
print ('Re-inflated delta n solution vector:', n2.shape)
for ind in range(0,n2.shape[0]):
if n2[ind][0] == 0.0:
print ('0.0',end=' ')
else:
print ("{0:10.3e}".format(n2[ind][0]),end=' ')
print ('')
norm = np.linalg.norm(n2)
if debug > 0:
print ('Norm: ', norm, 'Op, sub-Op:', self.eps_quad_optimal,
self.eps_quad_suboptimal)
if stats:
print ('Quad ({0:03d}) norm: {1:20.13e}'.format(quad_iter,
norm), end = '')
if norm < self.eps_quad_optimal:
quad_normal_exit = True
break
elif quad_iter == self.max_quad_iters:
if norm < self.eps_quad_suboptimal:
print (' Quadratic norm is acceptable, but not optimal.')
quad_normal_exit = True
break
# Perform a linear search along the quadratic search direction
n_ref = []
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
n_ref.append(entry['moles'][i])
if self.lagrange_list is not None:
row = 0
for c,f in self.lagrange_list:
if type(c) is dict:
n_ref.append(self.lagrange_moles[row])
elif type(c) is str:
n_ref.append(t if c == 'T' else p)
row += 1
n_ref = np.array(n_ref)
if debug > 2:
print ('Reference phase/component moles', n_ref.shape,
np.linalg.norm(n_ref))
print (n_ref)
# Potential function to be minimized
def linear_search(step, state, t, p, n_ref, n2):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
for i in range(0,entry['nc']):
entry['moles'][i] = n_ref[index] + step*n2[index]
index += 1
if entry['nc'] > 1:
assert entry['obj'].test_endmember_comp(
entry['moles']), 'Bounds failure for ' + name
if self.entropy:
t = n_ref[index] + step*n2[index]
index += 1
if self.volume:
p = n_ref[index] + step*n2[index]
index += 1
if self.entropy or self.volume:
tp_l = [t, p]
self._apply_non_linear_constraints(tp_l, state, debug)
(t,p) = tuple(tp_l)
result = state.G(t, p)
if self.lagrange_list is not None:
dgdn = state.dGdn(t, p, element_basis=False)
row = 0
for c,f in self.lagrange_list:
if type(c) is dict:
moles = state.moles_v(self.reactions[row])
contrib = moles*(
np.matmul(self.reactions[row], dgdn)[0])
if debug > 1:
print ('... linear search. ', end=' ')
print ('ref = ', n_ref[index], end=' ')
print ('index = ', index, 'i = ',row, end=' ')
print ('moles = ', moles, end=' ')
print ('contrib ', contrib)
result -= contrib
self.lagrange_moles[row] = moles
index += 1
row += 1
if self.entropy:
result -= t*state.dGdT(t,p)
if self.volume:
result -= p*state.dGdP(t,p)
return result
# Determine domain bounds on the potential linear search
def test_bounds_for_search(step, state, t, p, n_ref, n2):
index = 0
for name,entry in state.phase_d.items():
if state.tot_moles_phase(name) > 0:
nc = entry['nc']
moles = np.empty(nc)
for i in range(0,nc):
moles[i] = n_ref[index] + step*n2[index]
index += 1
if nc == 1:
if moles[0] < 0.0:
return False
else:
if not entry['obj'].test_endmember_comp(moles):
return False
if self.entropy:
t_est = n_ref[index] + step*n2[index]
if (t_est < 773.15) or (t_est > 2773.15):
return False
index += 1
if self.volume:
p_est = n_ref[index] + step*n2[index]
if (p_est < 1.0) or (p_est > 100000.0):
return False
index += 1
return True
step_max = 2.0
while not test_bounds_for_search(step_max, state, t, p, n_ref,
n2):
step_max *= 0.9
if step_max < sys.float_info.epsilon:
break
if debug > 1:
print ('Maximum linear step search length; ', step_max)
step_min = -2.0
while not test_bounds_for_search(step_min, state, t, p, n_ref,
n2):
step_min *= 0.9
if step_min > -sys.float_info.epsilon:
break
if debug > 1:
print ('Minimum linear step search length; ', step_min)
result = sp.optimize.minimize_scalar(linear_search,
bounds=(step_min,step_max),
args=(state, t, p, n_ref, n2), method='Bounded',
options={'maxiter':self.max_linear_iters,
'disp':debug, 'xatol':self.eps_linear})
step_val = result.x[0] if isinstance(result.x, np.ndarray
) else result.x
func_val = result.fun[0] if isinstance(result.fun, np.ndarray
) else result.fun
quad_energies.append(func_val)
if stats:
print (' Lin ({0:03d}) step: {1:20.13e} func: {2:20.13e}'.format(
result.nfev, step_val, func_val))
# insure that state is evaluated at the last estimate for step
linear_search(step_val, state, t, p, n_ref, n2)
if self.entropy and self.volume:
t_est = n_ref[-2] + step_val*n2[-2]
p_est = n_ref[-1] + step_val*n2[-1]
t = t_est[0] if isinstance(t_est, np.ndarray) else t_est
p = p_est[0] if isinstance(p_est, np.ndarray) else p_est
elif self.entropy:
t_est = n_ref[-1] + step_val*n2[-1]
t = t_est[0] if isinstance(t_est, np.ndarray) else t_est
elif self.volume:
p_est = n_ref[-1] + step_val*n2[-1]
p = p_est[0] if isinstance(p_est, np.ndarray) else p_est
if debug > 0:
print ('Optimal linear search length:', step_val)
print ('Optimal function value', func_val)
state.print_state()
if debug > 1:
print (result)
if self.entropy:
print ('Approximate corrected temperature', t)
if self.volume:
print ('Approximate corrected pressure', p)
if self.reactions is not None:
blk_cmp = state.tot_moles_elements()
diff = blk_cmp - self.bulk_comp
self._lagrange_moles += np.matmul(self.CTf, diff)
if debug > 1:
print ('Change in bulk composition:', diff.shape)
print (diff)
print ('lagrange_moles', self.lagrange_moles.shape)
print (self.lagrange_moles)
if self.entropy or self.volume:
tp_l = [t, p]
self._apply_non_linear_constraints(tp_l, state, debug)
(t,p) = tuple(tp_l)
state.temperature = t
state.pressure = p
if debug > 0 and self.entropy:
print ('Final corrected temperature', t)
if debug > 0 and self.volume:
print ('Final corrected pressure', p)
if con_deltaNNO is not None:
omni_moles = state.phase_d['Liquid']['moles']
omni_oxide = self.kc_ferric_ferrous(t, p, omni_moles,
compute='oxides', deltaNNO=con_deltaNNO, debug=debug)
omni_moles, oxide_res = \
state.phase_d['Liquid']['obj'].calc_endmember_comp(
mol_oxide_comp=omni_oxide, method='intrinsic',
output_residual=True)
state.set_phase_comp(omni_phase, omni_moles)
quad_iter += 1
if quad_iter > self.max_quad_iters:
quad_normal_exit = False
break
if len(quad_energies) > self.equil_linear_min:
last_three = quad_energies[-3:]
average = (last_three[0]+last_three[1]+last_three[2])/3.0
sumsqr = (last_three[0]-average)**2 + (last_three[1]-
average)**2 + (last_three[2]-average)**2
if debug > 0:
print ('Quad pot:', quad_energies[-1], 'Ave:',
average, 'SS:', sumsqr)
if debug > 2:
print (last_three)
if np.sqrt(sumsqr) < np.fabs(average)*self.eps_minimal_energy:
quad_normal_exit = True
print ('Minimal energy termination of quadratic loop.')
break
for name,entry in state.phase_d.items():
mol_p = state.tot_moles_phase(name)
if mol_p > 0.0 and mol_p < self.moles_out:
self._remove_phase_from_system(state, name)
if stats:
print ('Remove:', name)
if debug > 0:
print ('----------')
print ('--> Phase', name, 'removed from the system.')
print ('----------')
if name[-2:] == '_1':
del state.phase_d[name]
quad_iter = 0
# termination of quad_loop
if quad_normal_exit:
if stats:
print ('')
for phase_name in state.phase_d.keys():
result, result_comp = self._determine_phase_stability(state,
t, p, phase_name, debug=debug)
if result:
break
if result:
if stats:
print ('Unmixing:', phase_name)
if debug > 0:
print ('Phase separation found for', phase_name)
print (' composition:', result_comp)
new_phase = phase_name + '_1'
ident = 1
while new_phase in state.phase_d:
ident += 1
new_phase = phase_name + '_' + str(ident)
state.phase_d[new_phase] = state.phase_d[phase_name].copy()
entry = state.phase_d[new_phase]
entry['moles'] = np.zeros(entry['nc'])
entry['mole_frac'] = np.zeros(entry['nc'])
for i in range(0,entry['nc']):
entry['mole_frac'][i] = result_comp[i]
state._c_matrix = np.vstack(
(state._c_matrix, state.c_matrix_for_phase(phase_name)))
if debug > 2:
print ('Old phase dictionary ...')
print (state.phase_d[phase_name])
print ('New phase dictionary ...')
print (state.phase_d[new_phase])
okay = self._add_phase_to_system(state, new_phase)
if okay and debug > 1:
print ('... phase successfully added.')
elif not okay:
print ('... phase addition unsuccessful. Exiting.')
force_once_quad_pass = True
else:
print ('No check made for phase separation.')
# termination of phase saturation loop
return state
[docs]class MELTSmodel:
"""Class for creating an instance of the Equilibrate PhaseObjC class that is tailored to
calculate equilibrium phase assemblages using one of the MELTS calibrations.
Valid initializers are version='1.0.2', '1.1.0', '1.2.0', '5.6.1', 'DEW', 'OnlyDEW'
"""
def __init__(self, version='1.0.2'):
if version == '1.0.2':
MELTSclass = ObjCClass('EquilibrateUsingMELTSv102')
elif version == '1.1.0':
MELTSclass = ObjCClass('EquilibrateUsingMELTSv110')
elif version == '1.2.0':
MELTSclass = ObjCClass('EquilibrateUsingMELTSv120')
elif version == '5.6.1':
MELTSclass = ObjCClass('EquilibrateUsingpMELTSv561')
elif version == 'DEW':
MELTSclass = ObjCClass('EquilibrateUsingMELTSwithDEW')
elif version == 'OnlyDEW':
MELTSclass = ObjCClass('EquilibrateUsingMELTSandOnlyDEW')
else:
assert False, 'Unknown version of MELTS stipulated'
self.melts = MELTSclass.alloc().init()
self.melts.setDebugS_(0)
self.melts.setDebugV_(0)
oxide_names_NSArray = self.melts.oxideNames()
self.noxides = len(oxide_names_NSArray) # was .count converted to len() in going from 0.2.7 -> 0.2.10
self.oxide_names_a = [oxide_names_NSArray.objectAtIndex_(i) for i in range(self.noxides)]
phase_names_NSArray = self.melts.phaseNames()
self.nphases = len(phase_names_NSArray) # was .count converted to len() in going from 0.2.7 -> 0.2.10
self.phase_names_a = [phase_names_NSArray.objectAtIndex_(i) for i in range(self.nphases)]
locale.setlocale(locale.LC_NUMERIC, '')
[docs] def print_default_settings(self):
"""Prints list of options and tolerance settings for algorithm.
"""
option_settings = {
'PPOPTIONS_DISTRIBUTION': ('Make failure in element redistribution non-fatal', self.melts.PPOPTIONS_DISTRIBUTION),
'PPOPTIONS_CHEM_POTENTIAL': ('Make chemical potential recast errors non-fatal', self.melts.PPOPTIONS_CHEM_POTENTIAL),
'PPOPTIONS_RANK': ('Make rank deficient quadratic solutions fatal', self.melts.PPOPTIONS_RANK),
'PPOPTIONS_ZERO_LINEAR': ('Make "zero" steplength linear searches non-fatal', self.melts.PPOPTIONS_ZERO_LINEAR),
'PPOPTIONS_QUAD_ITERS': ('Terminate if quadratic iterations ever exceed maximum', self.melts.PPOPTIONS_QUAD_ITERS),
'PPOPTIONS_FORCE_COMPONENTS': ('Never zero a component concentration in a system phase', self.melts.PPOPTIONS_FORCE_COMPONENTS),
'PPOPTIONS_DETECT_MINIMAL': ('Prevent minimal energy test for convergence', self.melts.PPOPTIONS_DETECT_MINIMAL)
}
for key in option_settings:
value = option_settings[key]
print ("{0:<60.60s} {1:<30.30s} {2:1d}".format(value[0], key, value[1]))
tolerance_settings = {
'PPPARAMETERS_CHEM_POTENTIAL': \
('Residual tolerance for acceptable recasting of phase chemical potentials to those of the elements', \
float(self.melts.PPPARAMETERS_CHEM_POTENTIAL)),
'PPPARAMETERS_RANK': \
('Tolerance (relative to Hessian norm) for computation of pseudorank during quadratic search', \
self.melts.PPPARAMETERS_RANK),
'PPPARAMETERS_QUAD_OPTIMAL': \
('Residual norm relative tolerance for optimal quadratic convergence', \
self.melts.PPPARAMETERS_QUAD_OPTIMAL),
'PPPARAMETERS_QUAD_SUBOPTIMAL': \
('Residual norm relative tolerance for sub-optimal quadratic convergence', \
self.melts.PPPARAMETERS_QUAD_SUBOPTIMAL),
'PPPARAMETERS_QUAD_MAX_ITERS': \
('Maximal number of quadratic iterations', \
self.melts.PPPARAMETERS_QUAD_MAX_ITERS),
'PPPARAMETERS_LINEAR_MAX_ITERS': \
('Maximal number of linear search iterations', \
self.melts.PPPARAMETERS_LINEAR_MAX_ITERS),
'PPPARAMETERS_LINEAR_RELTEST': \
('Relative convergence criteria for estimation of the minimum during a linear search step', \
self.melts.PPPARAMETERS_LINEAR_RELTEST),
'PPPARAMETERS_LINEAR_MIN_STEPLENGTH': \
('Minimum allowed steplength in a linear search step', \
self.melts.PPPARAMETERS_LINEAR_MIN_STEPLENGTH),
'PPPARAMETERS_COMPONENT_MINIMUM': \
('Minimum molar concentration of a component in a phase (absolute value)', \
self.melts.PPPARAMETERS_COMPONENT_MINIMUM),
'PPPARAMETERS_MASSOUT': \
('Phase mass that triggers removal of that phase from the system', \
self.melts.PPPARAMETERS_MASSOUT),
'PPPARAMETERS_MOLESIN': \
('Moles of phase added to system on detection of phase saturation', \
self.melts.PPPARAMETERS_MOLESIN),
'PPPARAMETERS_LINEAR_MINIMAL': \
('Number of quadratic iterations over which to average potential for minimal energy convergence test', \
self.melts.PPPARAMETERS_LINEAR_MINIMAL),
'PPPARAMETERS_CYCLES_MAX': \
('Maximal number of phase inclusion/phase removal cycles permitted', \
self.melts.PPPARAMETERS_CYCLES_MAX)
}
for key in tolerance_settings:
value = tolerance_settings[key]
print ("{0:<70.70s} {1:<30.30s} {2:13.6e}".format(value[0], key, value[1]))
[docs] def set_debug_state(self, debugS=False, debugV=False):
"""Sets debug output level for Equilibrate class and subclasses
Parameters
==========
debugS : boolean, optional
Sets on or off low level debug output. Default is off (False).
debugV : boolean, optional
Sets on or off high level debug output. Default is off (False).
"""
if debugS == False:
self.melts.setDebugS_(False)
else:
self.melts.setDebugS_(True)
if debugV == False:
self.melts.setDebugV_(False)
else:
self.melts.setDebugV_(True)
[docs] def get_oxide_names(self):
"""Retrieves a list of system oxides
Composition of the system can be expressed in terms of these oxides.
Returns
-------
array : strings
An array of strings naming system components in terms of oxides
"""
return self.oxide_names_a
[docs] def get_phase_names(self):
"""Retrieves a list of system phases
Names of phases known to the system
Returns
-------
array : strings
An array of strings naming system phases
"""
return self.phase_names_a
[docs] def get_phase_inclusion_status(self):
"""Retrieves a dictionary of the inclusion status of each phase
Returns
-------
dict : dictionary
A dictionary of boolean values indicating the inclusion status of each phase
(key) known to the system
"""
dict = {}
state_NSdict = self.melts.getPermissablePhasesState()
for phase in self.phase_names_a:
value = state_NSdict.valueForKey_(phase) #.boolValue removed in going from 0.2.7 -> 0.2.10
if value == 1:
value = True
else:
value = False
dict[phase] = value
return dict
[docs] def set_phase_inclusion_status(self, status_d):
"""Sets the inclusion status of specified phases
Parameters
----------
status_d : dictionary
A dictionary of phase name keys and boolean values. True sets inclusion, and
False prevents inclusion of a phase in the equilibrium assemblage. Note that
the chemical affinity of the phase will still be calculated even if the
inclusion level is set to False.
"""
state_NSdict = self.melts.getPermissablePhasesState()
nsarray_cls = ObjCClass('NSMutableArray')
nsarray = nsarray_cls.arrayWithCapacity_(self.nphases)
for phase in self.phase_names_a:
value = state_NSdict.valueForKey_(phase) #.boolValue removed in going from 0.2.7 -> 0.2.10
if phase in status_d:
if status_d[phase] == True:
value = 1
else:
value = 0
nsarray.addObject_(ObjCClass('NSNumber').numberWithBool_(value))
self.melts.setPermissablePhasesState_(nsarray)
[docs] def set_bulk_composition(self, oxide_d={}):
"""Sets the bulk composition of the system
This function first tests if the composition is feasible before setting the bulk
composition of the system. You should check to make sure that the composition is
feasible before proceeding.
Parameters
----------
oxide_d : a python dictionary
A dictionary of oxide names and values, e.g. {'SiO2':77.8, 'Al2O3':12.0, ..., 'H2O':3.74}
Returns
-------
boolean : True or False
True if the composition is feasible, in which case the composition of the
system is defined.
False if the composition is infeasible, in which case the composition of the
system is undefined.
Notes
-----
Feasibility call has yet to be implemented: (Objective-C method call:)
-(BOOL)compositionIsFeasible:(NSArray \*)compositionInWtPercentOxides forSolution:(id <SolutionPhaseProtocol>)omniComponentPhase;
"""
wt = (ctypes.c_double*self.noxides)()
ctypes.cast(wt, ctypes.POINTER(ctypes.c_double))
for i in range(0, self.noxides):
wt[i] = 0.0
for key,value in oxide_d.items():
index = self.oxide_names_a.index(key)
wt[index] = value
self.melts.setComposition_(wt)
[docs] def set_temperature(self, t_c=800.0):
"""Sets the temperature of the system and reinitializes a calculation sequence
Parameters
----------
t_c : float optional
Float value to set the system temperature in degrees centigrade. Default is 800 °C.
"""
self.t = t_c + 273.15
self.melts.setTemperature_(self.t)
[docs] def set_entropy(self, s):
"""Sets the entropy of the system and reinitializes a calculation sequence
Parameters
----------
s : float
Float value to set the system entropy in J/K.
"""
self.entropy = s
self.melts.setEntropy_(self.entropy)
[docs] def set_pressure(self, p_mpa=200.0):
"""Sets the pressure of the system and reinitializes a calculation sequence
Parameters
----------
p_mpa : float optional
Float value to set the system pressure in mega-Pascals. Default is 200 MPa.
"""
self.p = p_mpa*10.0
self.melts.setPressure_(self.p)
[docs] def set_volume(self, v):
"""Sets the volume of the system and reinitializes a calculation sequence
Parameters
----------
v : float
Float value to set the system volume in J/bar
"""
self.volume = v
self.melts.setVolume_(self.volume)
[docs] def get_object_for_phase(self, phase_name):
"""Retrieve an object instance for the named phase.
Parameters
----------
phase_name: string
Name of phase
Returns
-------
object: instance of a phase class
null if the phase is not in the stable assemblage
"""
phase_list = self.melts.dictionaryOfPhasesInSystem # dictionary of EquilibrateStatePhase instances
if phase_name in phase_list:
return phase_list[phase_name].phaseClassInstance
else:
return None
[docs] def get_properties_of_DEWFluid(self, property_name='species', T=1000, P=1000):
"""Retrieves a dictionary of properties of the specified type
DEWFluid must exist in the equilibrium assemblage; otherwise an empty dictionary
is returned.
Parameters
----------
property_name: string, optional
'species' (default) returns a dictionary of species mole fractions in the
equilibrated solution.
'mu' returns a dictionary of species chemical potentials in the equilibrated
solution.
'activity' returns a dictionary of species activities in the equilibrated
solution.
T : float optional
Temperature in degrees centigrade (default is 1000°C)
P : float optional
Pressure in bars (default is 1000 bars)
Returns
-------
dictionary: a python dictionary
Keys are species names (strings).
Values are species concentrations in mole fraction, or chemical potential,
or thermodynamic activity, depending on the value of ``property_name``.
"""
phase_list = self.melts.dictionaryOfPhasesInSystem # dictionary of EquilibrateStatePhase instances
if 'DEWFluid' in phase_list:
DEW_object = phase_list['DEWFluid'].phaseClassInstance # instantiated class
elements = phase_list['DEWFluid'].bulkCompositionInElements
moles = DEW_object.convertElementsToMoles_(elements.pointerToDouble()).pointerToDouble()
if property_name == 'species':
return DEW_object.getSpeciesMoleFractionsForBulkComposition_aT_andP_(moles, T+273.15, P)
elif property_name == 'mu':
ns = DEW_object.numberOfSolutionSpecies()
mu = DEW_object.chemicalPotentialsOfSpeciesFromMolesOfComponents_andT_andP_(moles, T+273.15, P).pointerToDouble()
result = dict()
for i in range(0,ns):
name = DEW_object.nameOfSolutionSpeciesAtIndex_(i)
result[name] = mu[i]
return result
elif property_name == 'activity':
ns = DEW_object.numberOfSolutionSpecies()
activity = DEW_object.activitiesOfSpeciesFromMolesOfComponents_andT_andP_(moles, T+273.15, P).pointerToDouble()
result = dict()
for i in range(0,ns):
name = DEW_object.nameOfSolutionSpeciesAtIndex_(i)
result[name] = activity[i]
return result
else:
return dict()
else:
return dict()
[docs] def equilibrate_tp(self, T_a, P_a, initialize=False):
"""Determines the equilibrium phase assemblage at a temperature-pressure point
or along a series of T-P points.
The bulk composition of the system must first be set by calling the function:
``set_bulk_composition``
Parameters
----------
t_a : float or numpy array of floats
Temperature in degrees centigrade. Either a scaler values or a numpy array
of float values must be provided.
p_a : float or numpy array of floats
Pressure in mega-Pascals. Either a scaler values or a numpy array of float
values must be provided.
NOTE: If both ``t_a`` and ``p_a`` are arrays, then they must both be the
same length.
initialize : bool, optional
True if this is a T-, P-point that starts a sequence of calculations.
False if this is a continuation T-,P-pair or series of pairs.
Returns
-------
output_a : an array of tuples
tuple = (status, T, P, xmlout):
* **status** is a string indicating the status of the calculation:
success/failiure, Reason for success/failure.
* **T** is a float value corresponding to the temperature in degrees centigrade.
* **P** is a float value corresponding to the pressure in mega-Pascals.
* **xmlout** is an xml document tree of the type xml.etree.ElementTree. The
xml tree contains information on the masses and abundances of all phases in
the system. ``xmlout`` is utilized as input for a number of functions in this
package that retrieve properties of the equilibrium assemblage.
Notes
-----
The ``xmlout`` document tree will be expanded to include thermodynamic properties
of the phases and chemical affinities of phases not present in the equilibrium
assemblage.
"""
T_a, P_a = core.fill_array(T_a, P_a)
output_a = []
for ind,(T,P) in enumerate(zip(T_a,P_a)):
if initialize:
self.melts.setTemperature_(T+273.15)
self.melts.setPressure_(P*10.0)
nsarray_cls = ObjCClass('NSMutableArray')
nsarraykeys = nsarray_cls.arrayWithCapacity_(5)
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('ordinate'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('abscissa'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('imposeBuffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('buffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('bufferOffset'))
nsarrayvalues = nsarray_cls.arrayWithCapacity_(5)
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Temperature (°C)'))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Pressure (MPa)'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool_(0))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('QFM'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool(0)) # Double initialization does not work (?)
nsdict_cls = ObjCClass('NSDictionary')
nsdict = nsdict_cls.dictionaryWithObjects_forKeys_(nsarrayvalues, nsarraykeys)
self.melts.setCalculationOptions_(nsdict)
else:
self.melts.incrementTemperature_(T+273.15)
self.melts.incrementPressure_(P*10.0)
output_NSDictionary = self.melts.execute()
xmlout = ET.fromstring(self.melts.equilibrateResultsAsXML())
output_a.append((output_NSDictionary.objectForKey_('status'), T, P, xmlout))
return output_a
[docs] def equilibrate_sp(self, S_a, P_a, initialize=False):
"""Determines the equilibrium phase assemblage at an entropy-pressure point or
along a series of S-P points.
The bulk composition of the system must first be set by calling the function:
``set_bulk_composition``
Parameters
----------
S_a : float or numpy array of floats
Entropy in Joules per Kelvins. Either a scaler values or a numpy array of
float values must be provided.
P_a : float or numpy array of floats
Pressure in mega-Pascals. Either a scaler values or a numpy array of float
values must be provided.
NOTE: If both ``t_a`` and ``p_a`` are arrays, then they must both be the
same length.
initialize : bool, optional
True if this is a S-, P-point that starts a sequence of calculations.
False if this is a continuation S-,P-pair or series of pairs.
Returns
-------
output_a : an array of tuples
tuple = (status, T, P, xmlout):
* **status** is a string indicating the status of the calculation:
success/failiure, Reason for success/failure.
* **T** is a float value corresponding to the temperature in degrees
centigrade.
* **P** is a float value corresponding to the pressure in mega-Pascals.
* **xmlout** is an xml document tree of the type xml.etree.ElementTree. The
xml tree contains information on the masses and abundances of all phases in
the system. ``xmlout`` is utilized as input for a number of functions in
this package that retrieve properties of the equilibrium assemblage.
Notes
-----
The ``xmlout`` document tree will be expanded to include thermodynamic properties
of the phases and chemical affinities of phases not present in the equilibrium
assemblage.
"""
S_a, P_a =core.fill_array(S_a, P_a)
output_a = []
for ind,(S,P) in enumerate(zip(S_a,P_a)):
if initialize:
self.melts.setEntropy_(S)
self.melts.setPressure_(P*10.0)
nsarray_cls = ObjCClass('NSMutableArray')
nsarraykeys = nsarray_cls.arrayWithCapacity_(5)
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('ordinate'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('abscissa'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('imposeBuffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('buffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('bufferOffset'))
nsarrayvalues = nsarray_cls.arrayWithCapacity_(5)
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Entropy (J/K-kg)'))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Pressure (MPa)'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool_(0))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('QFM'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool(0)) # Double initialization does not work (?)
nsdict_cls = ObjCClass('NSDictionary')
nsdict = nsdict_cls.dictionaryWithObjects_forKeys_(nsarrayvalues, nsarraykeys)
self.melts.setCalculationOptions_(nsdict)
else:
self.melts.incrementEntropy_(S)
self.melts.incrementPressure_(P*10.0)
output_NSDictionary = self.melts.execute()
xmlout = ET.fromstring(self.melts.equilibrateResultsAsXML())
T = locale.atof(xmlout.find(".//Temperature").text)
output_a.append((output_NSDictionary.objectForKey_('status'), T, P, xmlout))
return output_a
[docs] def equilibrate_tv(self, T_a, V_a, initialize=False):
"""Determines the equilibrium phase assemblage at a temperature-volume point
or along a series of T-V points.
The bulk composition of the system must first be set by calling the function:
``set_bulk_composition``
Parameters
----------
T_a : float or numpy array of floats
Temperature in degrees centigrade. Either a scaler values or a numpy array
of float values must be provided.
V_a : float or numpy array of floats
Volume in Joules per bar. Either a scaler values or a numpy array of float
values must be provided.
NOTE: If both ``t_a`` and ``p_a`` are arrays, then they must both be the
same length.
initialize : bool, optional
True if this is a T-, V-point that starts a sequence of calculations.
False if this is a continuation T-,V-pair or series of pairs.
Returns
-------
output_a : an array of tuples
tuple = (status, T, P, xmlout):
* **status** is a string indicating the status of the calculation:
success/failiure, Reason for success/failure.
* **T** is a float value corresponding to the temperature in degrees centigrade.
* **P** is a float value correcponding to the pressure in mega-Pascals.
* **xmlout** is an xml document tree of the type xml.etree.ElementTree.
The xml tree contains information on the masses and abundances of all phases
in the system. ``xmlout`` is utilized as input for a number of functions in
this package that retrieve properties of the equilibrium assemblage.
Notes
-----
The ``xmlout`` document tree will be expanded to include thermodynamic properties
of the phases and chemical affinities of phases not present in the equilibrium
assemblage.
"""
T_a, V_a = core.fill_array(T_a, V_a)
output_a = []
for ind,(T,V) in enumerate(zip(T_a,V_a)):
if initialize:
self.melts.setTemperature_(T+273.15)
self.melts.setVolume_(V)
nsarray_cls = ObjCClass('NSMutableArray')
nsarraykeys = nsarray_cls.arrayWithCapacity_(5)
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('ordinate'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('abscissa'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('imposeBuffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('buffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('bufferOffset'))
nsarrayvalues = nsarray_cls.arrayWithCapacity_(5)
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Temperature (°C)'))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Volume (cc/kg)'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool_(0))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('QFM'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool(0)) # Double initialization does not work (?)
nsdict_cls = ObjCClass('NSDictionary')
nsdict = nsdict_cls.dictionaryWithObjects_forKeys_(nsarrayvalues, nsarraykeys)
self.melts.setCalculationOptions_(nsdict)
else:
self.melts.incrementTemperature_(T+273.15)
self.melts.incrementVolume_(V)
output_NSDictionary = self.melts.execute()
xmlout = ET.fromstring(self.melts.equilibrateResultsAsXML())
output_a.append((output_NSDictionary.objectForKey_('status'), T, P, xmlout))
return output_a
[docs] def equilibrate_sv(self, S_a, V_a, initialize=False):
"""Determines the equilibrium phase assemblage at an entropy-volume point
or along a series of S-V points.
The bulk composition of the system must first be set by calling the function:
``set_bulk_composition``
Parameters
----------
S_a : float or numpy array of floats
Entropy in Joules per Kelvins. Either a scaler values or a numpy array of
float values must be provided.
V_a : float or numpy array of floats
Volume in Joules per bar. Either a scaler values or a numpy array of float
values must be provided.
NOTE: If both ``t_a`` and ``p_a`` are arrays, then they must both be the
same length.
initialize : bool, optional
True if this is a S-, V-point that starts a sequence of calculations.
False if this is a continuation S-,V-pair or series of pairs.
Returns
-------
output_a : an array of tuples
tuple = (status, T, P, xmlout):
* **status** is a string indicating the status of the calculation:
success/failiure, Reason for success/failure.
* **T** is a float value corresponding to the temperature in degrees centigrade.
* **P** is a float value corresponding to the pressure in mega-Pascals.
* **xmlout** is an xml document tree of the type xml.etree.ElementTree.
The xml tree contains information on the masses and abundances of all phases
in the system. ``xmlout`` is utilized as input for a number of functions in
this package that retrieve properties of the equilibrium assemblage.
Notes
-----
The ``xmlout`` document tree will be expanded to include thermodynamic properties
of the phases and chemical affinities of phases not present in the equilibrium
assemblage.
"""
S_a, V_a = core.fill_array(S_a, V_a)
output_a = []
for ind,(S,V) in enumerate(zip(S_a,V_a)):
if initialize:
self.melts.setEntropy_(S)
self.melts.setVolume_(V)
nsarray_cls = ObjCClass('NSMutableArray')
nsarraykeys = nsarray_cls.arrayWithCapacity_(5)
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('ordinate'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('abscissa'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('imposeBuffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('buffer'))
nsarraykeys.addObject_(ObjCClass('NSString').stringWithString_('bufferOffset'))
nsarrayvalues = nsarray_cls.arrayWithCapacity_(5)
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Entropy (J/K-kg)'))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('Volume (cc/kg)'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool_(0))
nsarrayvalues.addObject_(ObjCClass('NSString').stringWithString_('QFM'))
nsarrayvalues.addObject_(ObjCClass('NSNumber').numberWithBool(0)) # Double initialization does not work (?)
nsdict_cls = ObjCClass('NSDictionary')
nsdict = nsdict_cls.dictionaryWithObjects_forKeys_(nsarrayvalues, nsarraykeys)
self.melts.setCalculationOptions_(nsdict)
else:
self.melts.incrementEntropy_(S)
self.melts.incrementVolume_(V)
output_NSDictionary = self.melts.execute()
xmlout = ET.fromstring(self.melts.equilibrateResultsAsXML())
output_a.append((output_NSDictionary.objectForKey_('status'), T, P, xmlout))
return output_a
[docs] def get_list_of_phases_in_assemblage(self, root):
"""Returns a list of phases in the specified equilibrium assemblage.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
Returns
-------
list : list
A Python list of all phases in the equilibrium assemblage
"""
list = []
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
list.append(phase)
return list
[docs] def get_dictionary_of_default_fractionation_coefficients(self, fracLiq=False, fracSolid=True, fracFluid=True, fracCoeff=1.0):
"""Returns a dictionary of default coefficients for phase fractionation.
These coefficients may be modified by the user. They are used when setting
the extent to which a phase will fractionate from the equilibrium assemblage.
Parameters
----------
fracLiq : bool, optional
Flag to indicate if liquid phases should be fractionated from the system.
Default is False.
fracSolids : bool, optional
Flag to indicate if solid phases should be fractionated from the system.
Default is True.
fracFluids : bool, optional
Flag to indicate if fluid phases should be fractionated from the system.
Default is True.
fracCoeff : float, optional
Fractionation coefficient, which gives the fractional extend to which the
mass of the phase is extracted during phase fractionation.
Default is 1.0 (100%).
Returns
-------
dict : dictionary
A Python dictionary keyed on phases with values giving the extent (in fractional
units) that a phase will fractionation mass.
Notes
-----
This dictionary is provided as input to the function ``fractionate_phases()``.
The default configuration fractionates all solid/fluid phases and retains liquid.
"""
dict ={}
for phase in self.phase_names_a:
if phase == 'Liquid':
if fracLiq:
dict[phase] = fracCoeff
else:
dict[phase] = 0.0
elif phase == 'Water' or phase == 'Fluid':
if fracFluid:
dict[phase] = fracCoeff
else:
dict[phase] = 0.00
else:
if fracSolid:
dict[phase] = fracCoeff
else:
dict[phase] = 0.0
return dict
[docs] def get_mass_of_phase(self, root, phase_name='System'):
"""Returns the mass of a phase in the specified equilibrium assemblage.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
phase_name : string, optional
The name of the phase whose abundance is requested, or the string 'System',
which returns the combined mass of all phases in the system. Default value
is 'System'.
Returns
-------
value : float
The mass of the phase in the equilibrium assemblage specified by ``root``, in grams.
If the specified phase is not in the equilibrium assemblage, a value of zero is retuned.
"""
if phase_name == 'System':
value = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
value += float(root.find(".//System/Phase[@Type='" + phase + "']/Mass").text)
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
value = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Mass").text)
else:
value = 0.0
return value
[docs] def get_composition_of_phase(self, root, phase_name='System', mode='oxide_wt'):
"""Returns the composition of a phase in the specified equilibrium assemblage
as a dictionary, with composition tabluated in the specified mode.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
phase_name : string, optional
The name of the phase whose abundance is requested, or the string 'System',
which returns the combined mass of all phases in the system. Default value
is 'System'.
mode : string, optional
Controls the contents of the returned dictionary.
* 'formula' returns a dictionary with the string 'formula' as key and value
set to a string representation of the phase formula. For pure component
phases, this is the standard phase formula. For solutions, this is the
actual formula constructed by weighting the endmember components by their
mole fractions.
* 'oxide_wt' returns a dictionary of oxide string keys with values in wt%.
This is a valid ``mode`` for all ``phase_name`` entries.
* 'component' returns a dictionary of endmember component keys with values in
mole fraction. The length of this dictionary will vary dependening on the
number of components that describe the solution. Pure phases return an empty
dictionary, as does ``phase_name`` set to 'System'.
The default value of ``mode`` is 'oxide_wt'.
Returns
-------
dict : dictionary
A dictionary describing the composition of ``phase_name`` according to the
``mode`` specified. The dictionary will be empty if ``phase_name`` is not
present in the equilibrium assemblage. It will also be empty for certain
cases described above under ``mode``.
"""
dict = {}
if phase_name == 'System':
if mode == 'oxide_wt':
oxides = list(root.findall(".//Composition/Oxide"))
for oxide in oxides:
key = oxide.attrib['Type']
value = float(oxide.text)
dict[key] = value
else:
phase = root.find(".//System/Phase[@Type='" + phase_name + "']")
if phase != None:
if mode == 'formula':
dict['formula'] = phase.find("Formula").text
elif mode == 'oxide_wt':
oxides = list(phase.findall("Oxide"))
for oxide in oxides:
key = oxide.attrib['Type']
value = float(oxide.text)
dict[key] = value
elif mode == 'component':
components = list(phase.findall("Component"))
if not components:
dict['formula'] = phase.find("Formula").text
else:
for component in components:
key = component.attrib['Name']
value = float(component.text)
dict[key] = value
return dict
[docs] def fractionate_phases(self, root, frac_coeff):
"""Fractionates phases from the system.
Partitions and maintains an internal dictionary of fractionates and automatically
modifies system bulk composition to reflect fractionation.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
frac_coeff : dictionary
A dictionary keyed on phase names with values that indicate the fraction of
each phase that should fractionate.
See get_dictionary_of_default_fractionation_coefficients().
Returns
-------
dict : dictionary
A dictionary keyed on phase names with values corresponding to a dictionary
of phase properties. Keys are property names.
"""
dict = {}
bc = {}
bcFactor = locale.atof(root.find(".//Mass").text)/100.0
oxides = list(root.findall(".//Composition/Oxide"))
for oxide in oxides:
key = oxide.attrib['Type']
value = locale.atof(oxide.text)
bc[key] = value
phases = list(root.findall(".//System/Phase"))
for phase in phases:
phase_name = phase.attrib['Type']
dict[phase_name] = {}
fraction = frac_coeff[phase_name]
if fraction > 0.0:
mass = locale.atof(phase.find("Mass").text)
oxides = list(phase.findall("Oxide"))
for oxide in oxides:
key = oxide.attrib['Type']
value = locale.atof(oxide.text)
dict[phase_name][key] = value*fraction*mass/100.0
bc[key] -= value*fraction*mass/100.0
if bc[key] < 0.0:
bc[key] = 0.0
print ("Zeroed: " + key)
self.set_bulk_composition(bc)
return dict
[docs] def get_thermo_properties_of_phase_components(self, root, phase_name, mode='mu'):
"""Returns a dictionary of the specified component thermodynamic properties of
the designated phase.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``.
phase_name : string
The name of the phase whose abundance is requested.
mode : string, optional
Controls the contents of the returned dictionary.
* 'mu' returns a dictionary of endmember component keys with values of
chemical potential. The length of this dictionary will vary depending on
the number of components that describe the solution. Pure phases return a
dictionary of unit length, with the phase name as key and their specific
Gibbs free energy as value (J/g).
* 'excess' returns a dictionary of endmember component keys with values of
excess chemical potential. The length of this dictionary will vary
depending on the number of components that describe the solution.
Pure phases return a dictionary of unit length, with the phase name as key
and zero as value.
* 'activity' returns a dictionary of endmember component keys with values of
component activity. The length of this dictionary will vary depending on
the number of components that describe the solution. Pure phases return a
dictionary of unit length, with the phase name as key and unity as value.
Returns
-------
dict : dictionary
A dictionary describing the thermodynamic properties of components in
``phase_name`` according to the ``mode`` specified. The dictionary will be
empty if ``phase_name`` is not present in the equilibrium assemblage.
"""
dict = {}
phase = root.find(".//System/Phase[@Type='" + phase_name + "']")
if phase != None:
if mode == 'mu':
mus = list(phase.findall("ChemicalPotential"))
if not mus:
value = locale.atof(phase.find("GibbsFreeEnergy").text)
mass = float(phase.find("Mass").text)
dict[phase_name] = value/mass
else:
for mu in mus:
key = mu.attrib['Name']
value = locale.atof(mu.text)
dict[key] = value
elif mode == 'excess':
mus = list(phase.findall("ExcessChemicalPotential"))
if not mus:
dict[phase_name] = 0.0
else:
for mu in mus:
key = mu.attrib['Name']
value = locale.atof(mu.text)
dict[key] = value
elif mode == 'activity':
mus = list(phase.findall("ExcessChemicalPotential"))
if not mus:
dict[phase_name] = 1.0
else:
t = locale.atof(root.find(".//Temperature").text) + 273.15
for mu in mus:
key = mu.attrib['Name']
value = locale.atof(mu.text)
dict[key] = np.exp(value/8.3143/t)
return dict
[docs] def get_list_of_properties(self):
"""Returns a list of properties reported for each phase in an equilibrium assemblage.
Returns
-------
list : list
A Python list of all properties of phases in an equilibrium assemblage
"""
list = ['Mass','GibbsFreeEnergy','Enthalpy','Entropy','HeatCapacity','DcpDt', 'Volume','DvDt','DvDp','D2vDt2','D2vDtDp','D2vDp2', \
'Density','Alpha','Beta','K',"K'",'Gamma']
return list
[docs] def get_units_of_property(self, prop='Mass'):
"""Returns the units of a specified property.
Returns
-------
string : string
The units of the specified property. Returns 'none' if property is invalid.
"""
dict = {'Mass':'g','GibbsFreeEnergy':'J','Enthalpy':'J','Entropy':'J/K','HeatCapacity':'J/K','DcpDt':'J/K^2', \
'Volume':'J/bar','DvDt':'J/bar-K','DvDp':'J/bar^2','D2vDt2':'J/bar-K^2','D2vDtDp':'J/bar^2-K','D2vDp2':'J/bar^3', \
'Density':'g/cm^3','Alpha':'1/K','Beta':'1/bar','K':'GPa',"K'":'none','Gamma':'none'}
return dict.get(prop)
[docs] def get_property_of_phase(self, root, phase_name='System', property_name='Mass'):
"""Returns the specified property of a phase in the specified equilibrium assemblage.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
phase_name : string, optional
The name of the phase whose property is requested, or the string 'System',
which returns the combined property of all phases in the system. Default
value is 'System'.
property_name : string, optional
The name of the property to be returned. Default value is 'Mass'.
Returns
-------
value : float
The property of the phase in the equilibrium assemblage specified by ``root``,
in standard units.
If the specified phase is not in the equilibrium assemblage, a value of zero
is returned.
If the property is not in the standard list, a value of zero is returned.
"""
standard = ['Mass','GibbsFreeEnergy','Enthalpy','Entropy','HeatCapacity','DcpDt', 'Volume','DvDt','DvDp','D2vDt2','D2vDtDp','D2vDp2']
derived = ['Density','Alpha','Beta','K',"K'",'Gamma']
if property_name in standard:
if phase_name == 'System':
value = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/" + property_name) != None:
value += locale.atof(root.find(".//System/Phase[@Type='" + phase + "']/" + property_name).text)
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/" + property_name) != None:
value = locale.atof(root.find(".//System/Phase[@Type='" + phase_name + "']/" + property_name).text)
else:
value = 0.0
elif property_name in derived:
if property_name == 'Density':
if phase_name == 'System':
volume = 0.0
mass = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
volume += float(root.find(".//System/Phase[@Type='" + phase + "']/Volume").text)
mass += float(root.find(".//System/Phase[@Type='" + phase + "']/Mass").text)
value = mass/volume/10.0 # g/cc
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
volume = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Volume").text)
mass = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Mass").text)
value = mass/volume/10.0 # g/cc
else:
value = 0.0
elif property_name == 'Alpha':
if phase_name == 'System':
volume = 0.0
dvdt = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
volume += float(root.find(".//System/Phase[@Type='" + phase + "']/Volume").text)
dvdt += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDt").text)
value = dvdt/volume
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
volume = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Volume").text)
dvdt = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDt").text)
value = dvdt/volume
else:
value = 0.0
elif property_name == 'Beta':
if phase_name == 'System':
volume = 0.0
dvdp = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
volume += float(root.find(".//System/Phase[@Type='" + phase + "']/Volume").text)
dvdp += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDp").text)
value = -dvdp/volume
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
volume = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Volume").text)
dvdp = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDp").text)
value = -dvdp/volume
else:
value = 0.0
elif property_name == 'K':
if phase_name == 'System':
volume = 0.0
dvdp = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
volume += float(root.find(".//System/Phase[@Type='" + phase + "']/Volume").text)
dvdp += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDp").text)
value = -volume/dvdp/10000.0
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
volume = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Volume").text)
dvdp = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDp").text)
value = -volume/dvdp/10000.0
else:
value = 0.0
elif property_name == "K'":
if phase_name == 'System':
volume = 0.0
dvdp = 0.0
d2vdp2 = 0.0
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
volume += float(root.find(".//System/Phase[@Type='" + phase + "']/Volume").text)
dvdp += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDp").text)
d2vdp2 += float(root.find(".//System/Phase[@Type='" + phase + "']/D2vDp2").text)
value = (volume*d2vdp2/dvdp/dvdp-1.0)
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
volume = float(root.find(".//System/Phase[@Type='" + phase_name + "']/Volume").text)
dvdp = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDp").text)
d2vdp2 = float(root.find(".//System/Phase[@Type='" + phase_name + "']/D2vDp2").text)
value = (volume*d2vdp2/dvdp/dvdp-1.0)
else:
value = 0.0
elif property_name == 'Gamma':
dvdp = 0.0
dvdt = 0.0
cp = 0.0
t = locale.atof(root.find(".//Temperature").text)+273.15
if phase_name == 'System':
for phase in self.phase_names_a:
if root.find(".//System/Phase[@Type='" + phase + "']/Mass") != None:
dvdp += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDp").text)
dvdt += float(root.find(".//System/Phase[@Type='" + phase + "']/DvDt").text)
cp += float(root.find(".//System/Phase[@Type='" + phase + "']/HeatCapacity").text)
value = -dvdt/(cp*dvdp + t*dvdt*dvdt)
else:
if root.find(".//System/Phase[@Type='" + phase_name + "']/Mass") != None:
dvdp = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDp").text)
dvdt = float(root.find(".//System/Phase[@Type='" + phase_name + "']/DvDt").text)
cp = float(root.find(".//System/Phase[@Type='" + phase_name + "']/HeatCapacity").text)
value = -dvdt/(cp*dvdp + t*dvdt*dvdt)
else:
value = 0.0
else:
value = 0.0
return value
[docs] def get_dictionary_of_affinities(self, root, sort=True):
"""Returns an ordered dictionary of tuples of chemical affinity and phase formulae for
undersaturated phases in the system.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_xx``
sort : boolean
A flag when set to sort the dictionary in order of ascending affinities
Returns
-------
dict : OrderedDict
A Python ordered dictionary. Dictionary keys are strings naming the phases
not in the equilibrium assemblage but known to the system. These phases are
by definition undersaturated. Dictionary values are tuples consisting of a
float value and a string: (affinity, formula). For a solution phase, the
formula is the composition closest to equilibrium with the reported phase
assemblage. Dictionary ordering corresponds to array order in
get_phase_names(), unless ``sorted`` is set to True; then entries are
ordered by ascending chemical affinity.
"""
dict = OrderedDict()
for phase in self.phase_names_a:
if root.find(".//Potential/Phase[@Type='" + phase + "']") != None:
affinity = locale.atof(root.find(".//Potential/Phase[@Type='" + phase + "']/Affinity").text)
formulae = root.find(".//Potential/Phase[@Type='" + phase + "']/Formula").text
if affinity == 0.0:
affinity = 999999.0
dict[phase] = (affinity, formulae)
if sort == True:
return OrderedDict(sorted(dict.items(), key=lambda t:t[1]))
else:
return dict
[docs] def output_summary(self, root, printT=True, printP=True, printMass=False, printSysWt=False, printSysM=False, printPhs=True, printPhsWt=False, printPhsM=False):
"""Prints information about the specified equilibrium phase assemblage.
Parameters
----------
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_tp``
printT : bool, optional, default=True
Print the system temperature in degrees centigrade
printP : bool, optional, default=True
Print the system pressure in mega-Pascals
printMass ; bool, optional, default=False
Print the mass of the system in grams
printSysWt : bool, optional, default=False
Print the composition of the system in wt% oxides
printSysM : bool, optional, default=False
Print the composition of the system in moles of elements
printPhs : bool, optional, default=True
Print the phases present in the system, their masses (in grams) and their
chemical formulas
printPhsWt : bool, optional, default=False
Print the composition of each phase in wt% oxides (most often used in
conjunction with printPhs=True)
printPhsM : bool, optional, default=False
Print the composition of each phase in moles of endmember components
(most often used in conjunction with printPhs=True)
"""
if printT:
print ("{0:<10s} {1:>8.2f}".format("T (°C)", locale.atof(root.find(".//Temperature").text)))
if printP:
print ("{0:<10s} {1:>8.2f}".format("P (MPa)", locale.atof(root.find(".//Pressure").text)*1000.0))
if printMass:
print ("{0:<10s} {1:>8.3f}".format("Mass (g)", float(root.find(".//Mass").text)))
if printSysM:
print ("Bulk composition in elemental abundances (moles):")
bcElements = list(root.findall(".//Composition/Element"))
for element in bcElements:
print (" {0:>2s} {1:>8.5f}".format(element.attrib['Type'], float(element.text)))
if printSysWt:
print ("Bulk composition in oxide abundances (wt %):")
bcOxides = list(root.findall(".//Composition/Oxide"))
for oxide in bcOxides:
print (" {0:>5s} {1:>8.4f}".format(oxide.attrib['Type'], float(oxide.text)))
phases = list(root.findall(".//System/Phase"))
for phase in phases:
if printPhs:
formula = phase.find("Formula").text.split(" ")
if len(formula) == 1:
print ("{0:<15.15s} {1:>8.4f} (g) {2:<60s}".format(phase.attrib['Type'], float(phase.find("Mass").text), formula[0]))
else:
# this formula consists of oxide, value pairs (liquid phase)
n_oxides = int(len(formula)/2)
n_oxides_1 = int(n_oxides/2)
n_oxides_2 = int(n_oxides - n_oxides_1)
if n_oxides <= 8:
n_oxides_1 = n_oxides
n_oxides_2 = 0
line_1 = ""
for i in range(n_oxides_1):
line_1 = line_1 + ' ' + formula[2*i] + ' ' + formula[2*i+1]
print ("{0:<15.15s} {1:>8.4f} (g) {2:<s}".format(phase.attrib['Type'], float(phase.find("Mass").text), line_1))
line_2 = ""
for i in range(n_oxides_2):
line_2 = line_2 + ' ' + formula[n_oxides_1*2+2*i] + ' ' + formula[n_oxides_1*2+2*i+1]
print ("{0:<33.33s}{1:<s}".format(" ", line_2))
if printPhsWt:
oxides = list(phase.findall("Oxide"))
for oxide in oxides:
value = float(oxide.text)
if (value != 0.0):
print (" {0:<5s} {1:>8.4f}".format(oxide.attrib['Type'], float(oxide.text)))
if printPhsM:
components = list(phase.findall("Component"))
for component in components:
value = float(component.text)
if (value != 0.0):
print (" {0:<15s} {1:>9.6f}".format(component.attrib['Name'], float(component.text)))
# =================================================================
# Block of Excel workbook functions for output
# =================================================================
[docs] def start_excel_workbook_with_sheet_name(self, sheetName="Summary"):
"""Create an Excel workbook with one named sheet.
Parameters
----------
sheetName : string
Sheet name in the new empty Excel workbook
Returns
-------
wb : type Workbook
A pointer to an Excel workbook
Notes
-----
Part of the Excel workbook functions subpackage
"""
wb = Workbook()
ws = wb.active
ws.title = sheetName
self.row = 0
return wb
[docs] def add_sheet_to_workbook_named(self, wb, sheetName):
"""Creates a new sheet in an existing Excel workbook.
Parameters
----------
wb : type Workbook
A pointer to an existing Excel workbook
sheetName : string
New sheet name in the specified Excel workbook
Returns
-------
ws : type Worksheet
A pointer to an Excel worksheet in the specified workbook, ``wb``
Notes
-----
Part of the Excel workbook functions subpackage
"""
ws = wb.create_sheet(title=sheetName)
return ws
[docs] def write_to_cell_in_sheet(self, ws, col, row, value, format='general'):
"""Writes information into the specified row, col on the specified worksheet.
Parameters
----------
ws : type Worksheet
A pointer to a previously created Excel worksheet
(see add_sheet_to_workbook_named)
col : int
Column number to write entry to. Numbering starts at column one.
row : int
Row number to write entry to. Numbering starts at row one.
value : float
Value to place at entry
format : string, optional
Format to use for value
* 'general' is the default; no formatting applied
* 'number' formats as '0.00'
* 'scientific' formats as '0.00E+00'
Notes
-----
Part of the Excel workbook functions subpackage
"""
if format == 'number':
ws.cell(column=col, row=row, value=float(value)).number_format = '0.00'
elif format == 'scientific':
ws.cell(column=col, row=row, value=float(value)).number_format = '0.00E+00'
else:
ws.cell(column=col, row=row, value=value)
[docs] def write_excel_workbook(self, wb, fileName="junk.xlsx"):
"""Writes the specified Excel workbook to disk.
Parameters
----------
wb : type Workbook
A pointer to an existing Excel workbook
fileName : string, optional
Name of the file that will contain the specified Excel notebook.
Default file name is 'junk.xlsx'.
Notes
-----
Part of the Excel workbook functions subpackage
"""
wb.save(filename = fileName)
[docs] def update_excel_workbook(self, wb, root):
"""Writes the specified equilibrium system state to the specified Excel workbook.
Parameters
----------
wb : type Workbook
A pointer to an existing Excel workbook
root : type xml.etree.ElementTree
An xml document tree returned by the function ``equilibrate_tp``
Notes
-----
The workbook is structured with a Summary worksheet and one worksheet for each
equilibrium phase. The evolution of the system is recorded in successive rows.
Row integrity is maintained across all sheets. This function may be called
repeatedly using different ``root`` objects. Part of the Excel workbook functions
subpackage.
"""
t = locale.atof(root.find(".//Temperature").text)
p = locale.atof(root.find(".//Pressure").text)*1000.0
bcElements = list(root.findall(".//Composition/Element"))
bcOxides = list(root.findall(".//Composition/Oxide"))
wsSummary = wb["Summary"] # .get_sheet_by_name("Summary") upgraded
if (self.row == 0):
col = 1
self.row = 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, "T °C")
col += 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, "P MPa")
col += 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, "Mass g")
col += 1
for element in bcElements:
self.write_to_cell_in_sheet(wsSummary, col, self.row, element.attrib['Type'])
col += 1
for oxide in bcOxides:
self.write_to_cell_in_sheet(wsSummary, col, self.row, oxide.attrib['Type'])
col += 1
self.row += 1
col = 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, t, format='number')
col += 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, p, format='number')
col += 1
self.write_to_cell_in_sheet(wsSummary, col, self.row, root.find(".//Mass").text, format='number')
col += 1
for element in bcElements:
self.write_to_cell_in_sheet(wsSummary, col, self.row, element.text, format='scientific')
col += 1
for oxide in bcOxides:
self.write_to_cell_in_sheet(wsSummary, col, self.row, oxide.text, format='number')
col += 1
phases = list(root.findall(".//System/Phase"))
for phase in phases:
phaseType = phase.attrib['Type']
oxides = list(phase.findall("Oxide"))
components = list(phase.findall("Component"))
try:
wsPhase = wb[phaseType] # .get_sheet_by_name(phaseType) upgraded
except KeyError:
wsPhase = wb.create_sheet(phaseType)
col = 1
self.write_to_cell_in_sheet(wsPhase, col, 1, "T °C")
col += 1
self.write_to_cell_in_sheet(wsPhase, col, 1, "P MPa")
col += 1
self.write_to_cell_in_sheet(wsPhase, col, 1, "Mass g")
col += 1
self.write_to_cell_in_sheet(wsPhase, col, 1, "Formula")
col += 1
for oxide in oxides:
self.write_to_cell_in_sheet(wsPhase, col, 1, oxide.attrib['Type'])
col += 1
for component in components:
self.write_to_cell_in_sheet(wsPhase, col, 1, component.attrib['Name'])
col += 1
col = 1
self.write_to_cell_in_sheet(wsPhase, col, self.row, t, format='number')
col += 1
self.write_to_cell_in_sheet(wsPhase, col, self.row, p, format='number')
col += 1
self.write_to_cell_in_sheet(wsPhase, col, self.row, phase.find("Mass").text, format='number')
col += 1
self.write_to_cell_in_sheet(wsPhase, col, self.row, phase.find("Formula").text)
col += 1
for oxide in oxides:
self.write_to_cell_in_sheet(wsPhase, col, self.row, oxide.text, format='number')
col += 1
for component in components:
self.write_to_cell_in_sheet(wsPhase, col, self.row, component.text, format='scientific')
col += 1