"""The Core package implements general Python functions
required by the thermoengine package. Typically, these are focused on interfacing
with Objective-C vectors, arrays, matrices, etc.
"""
import collections
import numpy as np
from scipy import optimize
from scipy.optimize import minimize
# Objective-C imports
import ctypes
from ctypes import cdll
from ctypes import util
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from rubicon.objc import ObjCClass, NSObject, objc_method
if util.find_library('/usr/local/lib/libphaseobjc.dylib') is not None:
cdll.LoadLibrary(util.find_library('/usr/local/lib/libphaseobjc.dylib'))
elif util.find_library('/usr/local/lib/libphaseobjc.so') is not None:
cdll.LoadLibrary(util.find_library('/usr/local/lib/libphaseobjc.so'))
from collections import OrderedDict
# __all__ flag does not work in module file
__all__ = ['fill_array',
'double_vector_to_array',
'array_to_double_vector',
'double_matrix_to_array',
'make_scale_matrix',
'get_src_object',
'chem',
'UnorderedList']
##################
# Array Handling #
##################
[docs]def fill_array(var1, var2):
"""Equilizes the dimension (shape) of two arrays or an array/scalar pair.
This function is not normally called outside the equilibratepy module.
On input ``var1`` and ``var2`` are either scalar/array pairs or arrays of the same shape.
On output, the function returns a tuple with both arrays of the same shape. A scalar is extended with
contant entries if that action is required to match the dimension of a scalar/array pair. Two scalars
are converted to two single dimension numpy arrays of length one.
Uses the ``numpy.full_like`` function.
Parameters
----------
var1 : scalar or array
If ``var1`` is an array, ``var2`` must be either a scalar or an array of the same shape.
var2 : scalar or array
If ``var2`` is an array, ``var1`` must be either a scalar or an array of the same shape.
Returns
-------
result : tuple, (numpy array, numpy array)
``var1`` and ``var2`` converted to numpy arrays
Examples
--------
>>> t = [500.0, 600.0]
>>> p = 1000.0
>>> t_a, p_a = fill_array(t, p)
>>> print (t_a)
[500.0, 600.0]
>>> print (p_a)
[1000.0, 1000.0]
"""
var1_a = np.asarray( var1 )
var2_a = np.asarray( var2 )
if var1_a.shape==():
var1_a = np.asarray( [var1] )
if var2_a.shape==():
var2_a = np.asarray( [var2] )
# Begin try/except block to handle all cases for filling an array
while True:
try:
assert var1_a.shape == var2_a.shape
break
except: pass
try:
var1_a = np.full_like( var2_a, var1_a )
break
except: pass
try:
var2_a = np.full_like( var1_a, var2_a )
break
except: pass
# If none of the cases properly handle it, throw error
assert False, 'var1 and var2 must both be equal shape or size=1'
return var1_a, var2_a
################
# Phase ObjC #
################
[docs]def double_vector_to_array(vec):
"""Converts a DoubleVector Objective-C instance into a numpy 1-D array.
This function is not normally called outside the equilibratepy module.
Parameters
----------
vec : an instance of the Objective-C class DoubleVector
Contents of ``vec`` are a sequence of double precision entries.
Returns
-------
array : numpy array
Contents of ``vec`` as a 1-D numpy array
"""
size = vec.size
array = np.empty(size)
m = vec.pointerToDouble()
ctypes.cast(m, ctypes.POINTER(ctypes.c_double))
for i in range(size):
array[i] = m[i]
return array
[docs]def array_to_double_vector(array):
"""Converts a 1-D numpy array into an instance of a DoubleVector Objective-C class.
This function is not normally called outside the equilibratepy module.
Parameters
----------
array : an instance of a 1-D numpy array
Contents of ``array`` must be a sequence of double precision entries.
Returns
-------
vec : an instance of the Objective-C class DoubleVector
Contents of ``array`` as a pointer to an instance of DoubleVector
"""
doublevec_cls = ObjCClass('DoubleVector')
# vec = (ctypes.c_double*array.size)()
# ctypes.cast(vec, ctypes.POINTER(ctypes.c_double))
vec = doublevec_cls.alloc().initWithSize_( array.size )
vec_pointer = vec.pointerToDouble()
for ind, val in enumerate(array):
vec_pointer[ind] = val
return vec
def array_to_ctype_array(np_array):
"""Converts a 1-D numpy array into a c-type array.
Parameters
----------
np_array : an instance of a 1-D numpy array
Contents of ``array`` must be a sequence of double precision entries.
Returns
-------
ctype_array : a c-type array
"""
nc = len(np_array)
m = (ctypes.c_double*nc)()
ctypes.cast(m, ctypes.POINTER(ctypes.c_double))
for i in range(np_array.size):
m[i] = np_array[i]
return m
def ctype_array_to_array(ctype_array):
"""Converts a c-type array into a numpy array
Parameters
----------
ctype_array : a c-type array
Contents of ``array`` must be a sequence of double precision entries
Returns
-------
np_array : an instance of a 1-D numpy array
"""
N = ctype_array.size
np_array = np.zeros(N)
for i in range(N):
np_array[i] = ctype_array.valueAtIndex_(i)
return np_array
[docs]def double_matrix_to_array(mat):
"""Converts a DoubleMatrix Objective-C instance into a numpy 2-D array.
This function is not normally called outside the equilibratepy module.
Parameters
----------
mat : an instance of the Objective-C class DoubleMatrix
Contents of ``mat`` are a sequence of double precision entries organized as a matrix.
Returns
-------
array : numpy array
Contents of ``mat`` as a 2-D numpy array
"""
Nrow, Ncol = mat.rowSize, mat.colSize
array = np.empty((Nrow,Ncol))
m = mat.pointerToPointerToDouble()
ctypes.cast(m,ctypes.POINTER(ctypes.POINTER(ctypes.c_double)))
for i in range(Nrow):
for j in range(Ncol):
array[i,j] = m[i][j]
return array
def double_tensor_to_array(ten):
"""Converts a DoubleTensor Objective-C instance into a numpy 3-D array.
This function is not normally called outside the phases.py module.
Parameters
----------
ten : an instance of the Objective-C class DoubleTensor
Contents of ``ten`` are a sequence of double precision entries organized as a 3x3 tensor
Returns
-------
array : numpy array
Contents of ``ten`` as a 3-D numpy array
"""
N1st, N2nd, N3rd = ten.firstSize, ten.secondSize, ten.thirdSize
array = np.empty((N1st,N2nd,N3rd))
m = ten.pointerToPointerToPointerToDouble()
ctypes.cast(m,ctypes.POINTER(ctypes.POINTER(ctypes.POINTER(ctypes.c_double))))
for i in range(N1st):
for j in range(N2nd):
for k in range(N3rd):
array[i,j,k] = m[i][j][k]
return array
[docs]def get_src_object(classnm, return_class=False):
"""Initialize object from underlying source code.
Parameters
----------
classnm: str
Name of src class
return_class: bool, default False
If True, return both src object and class as a tuple
Returns
-------
src_obj: initialized src object
src_cls: (if return_class is True) src class
"""
src_cls = ObjCClass(classnm)
src_obj = src_cls.alloc().init()
if return_class:
return src_obj, src_cls
else:
return src_obj
########
# Math #
########
def make_scale_matrix(array):
scl_mat_a = np.dot(np.expand_dims(array,-1),
np.expand_dims(array,0))
return scl_mat_a
#############
# Chemistry #
#############
class _Chem:
OXIDE_ORDER = np.array([
'SiO2','TiO2','Al2O3','Fe2O3','Cr2O3','FeO','MnO','MgO','NiO',
'CoO','CaO','Na2O','K2O','P2O5','H2O','CO2'])
PERIODIC_ORDER = np.array([
None, 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na',
'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V',
'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se',
'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh',
'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La',
'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm',
'Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl',
'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U',
'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr',
'Rf', 'Db', 'Sg' ])
PERIODIC_NAMES = np.array([
None, 'hydrogen', 'helium', 'lithium', 'beryllium', 'boron',
'carbon', 'nitrogen', 'oxygen', 'fluorine', 'neon', 'sodium',
'magnesium', 'aluminum', 'silicon', 'phosphorous', 'sulfur',
'chlorine', 'argon', 'potassium', 'calcium', 'scandium', 'titanium',
'vanadium', 'chromium', 'manganese', 'iron', 'cobalt', 'nickel',
'copper', 'zinc', 'gallium', 'germanium', 'arsenic', 'selenium',
'bromine', 'krypton', 'rubidium', 'strontium', 'yttrium', 'zirconium',
'niobium', 'molybdenum', 'technetium', 'ruthenium', 'rhodium',
'palladium', 'silver', 'cadmium', 'indium', 'tin', 'antimony',
'tellurium', 'iodine', 'xenon', 'cesium', 'barium', 'lantahnum',
'cerium', 'praseodymium', 'neodymium', 'promethium', 'samarium',
'europium', 'gadolinium', 'terbium', 'dysprosium', 'holmium', 'erbium',
'thulium', 'ytterbium', 'lutetium', 'hafnium', 'tantalum', 'tungsten',
'rhenium', 'osmium', 'iridium', 'platinum', 'gold', 'mercury',
'thallium', 'lead', 'bismuth', 'polonium', 'astatine', 'radon',
'francium', 'radium', 'actinium', 'thorium', 'protactinium', 'uranium',
'neptunium', 'plutonium', 'americium', 'curium', 'berkelium',
'californium', 'einsteinium', 'fermium', 'mendelevium', 'nobelium',
'lawrencium', 'ruferfordium', 'dubnium', 'seaborgium' ])
PERIODIC_WEIGHTS = np.array([
0.0, 1.0079, 4.00260, 6.94, 9.01218, 10.81, 12.011, 14.0067, 15.9994,
18.998403, 20.179, 22.98977, 24.305, 26.98154, 28.0855, 30.97376, 32.06,
35.453, 39.948, 39.102, 40.08, 44.9559, 47.90, 50.9415, 51.996, 54.9380,
55.847, 58.9332, 58.71, 63.546, 65.38, 69.735, 72.59, 74.9216, 78.96,
79.904, 83.80, 85.4678, 87.62, 88.9059, 91.22, 92.9064, 95.94, 98.9062,
101.07, 102.9055, 106.4, 107.868, 112.41, 114.82, 118.69, 121.75,
127.60, 126.9045, 131.30, 132.9054, 137.33, 138.9055, 140.12, 140.9077,
144.24, 145., 150.4, 151.96, 157.25, 158.9254, 162.50, 164.9304, 167.26,
168.9342, 173.04, 174.967, 178.49, 180.9479, 183.85, 186.207, 190.2,
192.22, 195.09, 196.9665, 200.59, 204.37, 207.2, 208.9804, 209., 210.,
222., 223., 226.0254, 227., 232.0381, 231.0359, 238.029, 237.0482, 244.,
243., 247., 247., 251., 254., 257., 258., 259., 260., 260., 260., 263.])
# These entropy values are from Robie, Hemingway and Fisher (1979) USGS
# Bull 1452 as stipulated by Berman (1988). They are NOT the most recent
# values (e.g.NIST)
DBL_MAX = 999999.0
PERIODIC_ENTROPES = ([
0.0, 130.68/2.0, 126.15, 29.12, 9.54, 5.90, 5.74, 191.61/2.0,
205.15/2.0, 202.79/2.0, 146.32, 51.30, 32.68, 28.35, 18.81, 22.85,
31.80, 223.08/2.0, 154.84, 64.68, 41.63, 34.64, 30.63, 28.91, 23.64,
32.01, 27.28, 30.04, 29.87, 33.15, 41.63, 40.83, 31.09, 35.69, 42.27,
245.46/2.0, 164.08, 76.78, 55.40, 44.43, 38.99, 36.40, 28.66, DBL_MAX,
28.53, 31.54, 37.82, 42.55, 51.80, 57.84, 51.20, 45.52, 49.50,
116.15/2.0, 169.68, 85.23, 62.42, 56.90, 69.46, 73.93, 71.09,
DBL_MAX, 69.50, 80.79, 68.45, 73.30, 74.89, 75.02, 73.18, 74.01,
59.83, 50.96, 43.56, 41.51, 32.64, 36.53, 32.64, 35.48, 41.63, 47.49,
75.90, 64.18, 65.06, 56.74, DBL_MAX, DBL_MAX, 176.23, DBL_MAX, DBL_MAX,
DBL_MAX, 53.39, DBL_MAX, 50.29, DBL_MAX, 51.46, DBL_MAX, DBL_MAX,
DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX,
DBL_MAX, DBL_MAX ])
"""
Mole oxide to element conversion matrix
- rows = oxides in standard OXIDE_ORDER
- columns = elements in same order as they appear in OXIDE_ORDER matrix;
thus, conversion matrix only valid for elements present in OXIDE_ORDER
matrix; all Fe is converted to total Fe in column 4
- column order = Si, Ti, Al, Fe, Cr, Mn, Mg, Ni, Co, Ca, Na, K, P, H, C, O
"""
MOL_OXIDE_TO_ELEM = np.array([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 5],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2]])
LEPR_phase_symbols = {
'Liquid':'Liq',
'Clinopyroxene':'Cpx',
'Garnet':'Grt',
'Olivine':'Ol',
'Orthopyroxene':'Opx',
'Biotite':'Bt',
'Fluid':None,
'Corundum':'Crn',
'Rutile':'Rt',
'Plagioclase':'Fsp',
'Amphibole':'Cam',
'Zoisite':'Zo',
'Cordierite':'Crd',
'Muscovite':'Ms',
'Quartz':'Qz',
'Kyanite':'Ky',
'Potassium feldspar':'Fsp',
'Sillimanite':'Sil',
'Spinel':'SplS',
'Staurolite':None ,
'Melilite':'Mll',
'Carbonate melt':None,
'Nepheline':'NphS',
'Ilmenite':'Ilm',
'Eskolaite':None,
'Anorthite':'An',
'cc-dol':None}
def __init__(self):
self._init_oxide_props()
pass
def _init_oxide_props(self):
"""Dictionary of oxide properties
Returns
-------
oxide_data: dict with keys
`oxides` : list of oxide strings
`cations` : list of cation strings
`molwt` : array of molecular weights
`cat_num` : array of cation numbers
`oxy_num` : array of oxygen numbers
`oxycat_ratio` : array of oxygen/cation ratios
"""
def make_oxide_dat(name, cation, molwt, charge, catnum, oxynum):
oxycat_ratio = oxynum/catnum
oxide_dat = {'name':name, 'cation':cation, 'molwt':molwt,
'charge':charge, 'catnum':catnum, 'oxynum':oxynum,
'oxycat_ratio': oxycat_ratio}
return oxide_dat
oxide_data = []
oxide_data.append(make_oxide_dat('SiO2', 'Si', 60.0848, +4, 1, 2))
oxide_data.append(make_oxide_dat('TiO2', 'Ti', 79.8988, +4, 1, 2))
oxide_data.append(make_oxide_dat('Al2O3', 'Al', 101.96128, +3, 2, 3))
oxide_data.append(make_oxide_dat('Fe2O3', 'Fe', 159.6922, +3, 2, 3))
oxide_data.append(make_oxide_dat('Cr2O3', 'Cr', 151.9902, +3, 2, 3))
oxide_data.append(make_oxide_dat('FeO', 'Fe', 71.8464, +2, 1, 1))
oxide_data.append(make_oxide_dat('MnO', 'Mn', 70.9374, +2, 1, 1))
oxide_data.append(make_oxide_dat('MgO', 'Mg', 40.3044, +2, 1, 1))
oxide_data.append(make_oxide_dat('NiO', 'Ni', 74.7094, +2, 1, 1))
oxide_data.append(make_oxide_dat('CoO', 'Co', 74.9326, +2, 1, 1))
oxide_data.append(make_oxide_dat('CaO', 'Ca', 56.0794, +2, 1, 1))
oxide_data.append(make_oxide_dat('Na2O', 'Na', 61.97894, +1, 2, 1))
oxide_data.append(make_oxide_dat('K2O', 'K', 94.1954, +1, 2, 1))
oxide_data.append(make_oxide_dat('P2O5', 'P', 141.94452, +5, 2, 5))
oxide_data.append(make_oxide_dat('H2O', 'H', 18.0152, +1, 2, 1))
oxide_data.append(make_oxide_dat('CO2', 'C', 44.0098, +4, 1, 2))
oxide_props = OrderedDict()
oxide_props['oxide_num'] = len(oxide_data)
oxide_props['oxides'] = np.array([idat['name'] for idat in oxide_data])
oxide_props['cations'] = np.array([idat['cation'] for idat in oxide_data])
oxide_props['molwt'] = np.array([idat['molwt'] for idat in oxide_data])
oxide_props['charge'] = np.array([idat['charge'] for idat in oxide_data])
oxide_props['cat_num'] = np.array([idat['catnum'] for idat in oxide_data])
oxide_props['oxy_num'] = np.array([idat['oxynum'] for idat in oxide_data])
oxide_props['oxycat_ratio'] = np.array([idat['oxycat_ratio']
for idat in oxide_data])
# for idat in oxide_data:
# oxide = idat['name']
# oxide_props[oxide] = idat
self._oxide_props = oxide_props
pass
@property
def oxide_props(self):
return self._oxide_props
def select_oxides(self, oxide_names, oxide_values):
# oxide_molwt = chem.oxide_props['molwt']
oxides = chem.oxide_props['oxides']
assert np.all([oxname in oxides for oxname in oxide_names]),(
'oxide_names must all be valid oxide names')
value = np.squeeze(np.array([oxide_values[oxides==iname]
for iname in oxide_names]))
return value
def calc_mol_oxide_comp(self, element_comp):
major_cations = self.oxide_props['cations']
# NOTE: not necessarily actually monovalent, but where we consider only one valence state
monovalent_oxide_ind = np.where(self.oxide_props['cations']!='Fe')[0]
FeO_oxide_ind = np.where(self.oxide_props['oxides']=='FeO')[0]
Fe2O3_oxide_ind = np.where(self.oxide_props['oxides']=='Fe2O3')[0]
def get_atomic_indices(monovalent_cations):
monovalent_elem_ind = np.array([np.where(elems==icat)[0][0]
for icat in monovalent_cations])
oxy_elem_ind = np.where(elems=='O')[0][0]
Fe_elem_ind = np.where(elems=='Fe')[0][0]
return monovalent_elem_ind, oxy_elem_ind, Fe_elem_ind
def calc_Fe_oxides(Fe_remain, oxy_remain):
if Fe_remain == 0:
mol_FeO = 0
mol_Fe2O3 = 0
else:
#ratio = oxy_remain/Fe_remain
#frac_Fe2O3 = (ratio-1)/(1.5-1)
#frac_FeO = 1-frac_Fe2O3
#mol_Fe_oxide = Fe_remain/(2*frac_Fe2O3 + 1*frac_FeO)
#mol_FeO = mol_Fe_oxide*frac_FeO
#mol_Fe2O3 = mol_Fe_oxide*frac_Fe2O3
mol_Fe2O3 = oxy_remain - Fe_remain
mol_FeO = 3.0*Fe_remain - 2.0*oxy_remain
return mol_FeO, mol_Fe2O3
monovalent_oxides = self.oxide_props['oxides'][monovalent_oxide_ind]
monovalent_cations = self.oxide_props['cations'][monovalent_oxide_ind]
monovalent_cat_num = self.oxide_props['cat_num'][monovalent_oxide_ind]
monovalent_oxy_num = self.oxide_props['oxy_num'][monovalent_oxide_ind]
elems = self.PERIODIC_ORDER
monovalent_elem_ind, oxy_elem_ind, Fe_elem_ind = get_atomic_indices(monovalent_cations)
# extract array of all major elements from the composition
monovalent_element_comp = element_comp[monovalent_elem_ind]
monovalent_mol_oxide_comp = monovalent_element_comp/monovalent_cat_num
monovalent_mol_oxy_tot = np.sum(monovalent_oxy_num*monovalent_mol_oxide_comp)
oxy_remain = element_comp[oxy_elem_ind] - monovalent_mol_oxy_tot
Fe_remain = element_comp[Fe_elem_ind]
mol_FeO, mol_Fe2O3 = calc_Fe_oxides(Fe_remain, oxy_remain)
mol_oxide_comp = np.zeros(self.oxide_props['oxide_num'])
mol_oxide_comp[monovalent_oxide_ind] = monovalent_mol_oxide_comp
mol_oxide_comp[FeO_oxide_ind] = mol_FeO
mol_oxide_comp[Fe2O3_oxide_ind] = mol_Fe2O3
return mol_oxide_comp
def format_mol_oxide_comp(self, mol_oxides, convert_grams_to_moles=False):
"""
convert mol_oxide dictionary to mol_oxide array
Parameters:
==========
mol_oxides: Dictionary
Dictionary of molar oxide compositions with oxide
names as keys
convert_grams_to_moles: False, default
boolean flag indicating weight input in grams
Returns:
========
mol_oxide_comp: np array
Molar oxide array in standard oxide order
"""
OXIDE_ORDER = self.OXIDE_ORDER
mol_oxide_comp = np.zeros(len(OXIDE_ORDER))
assert np.all([oxide in OXIDE_ORDER
for oxide in mol_oxides])
for oxide in mol_oxides:
mol_oxide = mol_oxides[oxide]
ind, = np.where(OXIDE_ORDER==oxide)
if convert_grams_to_moles:
mol_oxide_comp[ind] = mol_oxide/self.oxide_props['molwt'][ind]
else:
mol_oxide_comp[ind] = mol_oxide
return mol_oxide_comp
def mol_oxide_to_elem(self, mol_oxides, oxide_names=None):
"""
Convert mole oxide composition to mole element composition.
Parameters
----------
mol_oxides : array
mole oxide composition defined in standard OXIDE_ORDER
Returns
-------
mol_elem : array
mole element composition with elements in same order as oxide array;
only functions for elements present in OXIDE_ORDER; Fe given as total Fe
elem order is Si, Ti, Al, Fe, Cr, Mn, Mg, Ni, Co, Ca, Na, K, P, H, C, O
"""
# MOL_OXIDE_TO_ELEM = self.MOL_OXIDE_TO_ELEM
MOL_OXIDE_TO_ELEM, oxide_names = self._validate_oxides_list(
self.MOL_OXIDE_TO_ELEM, oxide_names)
# oxide_molecular_wts = self.oxide_props['molwt']
# oxide_molecular_wts = self.get_molwt(oxide_names)
# MOL_OXIDE_TO_ELEM = self.select_oxides(oxide_names, self.MOL_OXIDE_TO_ELEM)
mol_elem = np.dot(mol_oxides, MOL_OXIDE_TO_ELEM)
return mol_elem
def get_Berman_formula(self, element_comp):
"""Get chemical formula in Berman form (e.g., H(2.0)O(1.0)).
Parameters
----------
element_comp : double array
Element composition defined in standard PERIODIC_ORDER
Returns
-------
formula : str
"""
formula = ''
for amt, sym in zip(element_comp, self.PERIODIC_ORDER):
if amt > 0.0:
formula += sym + '(' + str(amt) + ')'
return formula
def elem_to_oxide(self):
# %el x (oxide molecular weight/el weight) = wt% oxide
raise NotImplemented
def oxide_to_elem(self, oxide_names, oxide_wts):
#oxide_names = np.array([oxide_names])
#oxide_wts = np.array([oxide_wts])
#oxide_molecular_wts = self.oxide_props['molwt']
#wt% oxide to el% is -- wt% oxide x (el weight/oxide molecular weight)=el%
raise NotImplemented
def get_comp_subset(self):
raise NotImplemented
def _validate_oxides_list(self, oxide_values, oxide_names):
oxide_values = np.array(oxide_values)
ndim = oxide_values.ndim
if ndim==2:
noxides = oxide_values.shape[1]
else:
noxides = len(oxide_values)
if oxide_names is None:
oxide_names = self.OXIDE_ORDER
oxide_names = np.array(oxide_names)
assert len(oxide_names)==noxides, (
'Num. of oxide names must match oxide wts.'
)
return oxide_values, oxide_names
def _normalize_oxide_comp(self, oxide_values):
if oxide_values.ndim == 2:
totals = np.sum(oxide_values, axis=1)[:,np.newaxis]
else:
totals = np.sum(oxide_values)
oxide_values = oxide_values/totals
return oxide_values
def wt_to_mol_oxide(self, oxide_wts, oxide_names=None):
oxide_wts, oxide_names = self._validate_oxides_list(
oxide_wts, oxide_names)
# oxide_molecular_wts = self.oxide_props['molwt']
# oxide_molecular_wts = self.get_molwt(oxide_names)
oxide_molecular_wts = self.select_oxides(oxide_names, self.oxide_props['molwt'])
mol_oxides = oxide_wts/oxide_molecular_wts
mol_oxides = self._normalize_oxide_comp(mol_oxides)
return mol_oxides
def mol_to_wt_oxide(self, mol_oxides, oxide_names=None):
mol_oxides, oxide_names = self._validate_oxides_list(
mol_oxides, oxide_names)
oxide_molecular_wts = self.oxide_props['molwt']
wt_oxides = mol_oxides*oxide_molecular_wts
wt_oxides = self._normalize_oxide_comp(wt_oxides)
return wt_oxides
def get_phase_symbols(self, rxn_data):
return rxn_data['phase_symbols']['phase_symbol'].tolist()
def format_meas_mineral_comp(self, mineral_comp, o_site_total):
mineral_mol_elem_comp = self.mol_oxide_to_elem(mineral_comp)
meas_elem_comp = mineral_mol_elem_comp*o_site_total/mineral_mol_elem_comp[-1]
return meas_elem_comp
def _validate_site_occ_input(self, single_value_index, site_id_index, endmember_site_occ,
site_totals):
for ival in single_value_index:
index = site_id_index[ival]
for iendmem in endmember_site_occ:
assert iendmem[-1] ==site_totals[-1], (
'The last column of the site occupancy matrix and last element '
'of the site totals array must be equal. Recall, the last column '
'of the site_occupancy matrix must be oxygen and must have '
'values equal to the last element of the site totals array.')
assert iendmem[index]==site_totals[ival], (
'Invariant sites in the site occ matrix are not equal to'
'the site totals.')
def _success_test(self, residual, threshold):
if np.max(np.abs(residual))< threshold:
success = 'minimization successful'
else:
success = 'minimization failed; residual greater than threshold value'
return success
def lstsqr_endmember_comp(self, mol_oxide_comp, mol_oxide_comp_endmembers, decimals):
#mol_oxide_comp_endmembers = phases.props['mol_oxide_comp']
#mol_oxide_comp_endmembers = modelDB.phases[abbrev].props['mol_oxide_comp']
output = np.linalg.lstsq(
mol_oxide_comp_endmembers.T, mol_oxide_comp, rcond=None)
endmember_comp = output[0]
endmember_comp = np.round(endmember_comp, decimals=decimals)
mol_oxide_comp_model = np.dot(mol_oxide_comp_endmembers.T, endmember_comp)
mol_oxide_comp_residual = (mol_oxide_comp - mol_oxide_comp_model)
return endmember_comp, mol_oxide_comp_model, mol_oxide_comp_residual
def site_spec_lstsq_endmember_comp(self, meas_elem_comp, endmember_elem_stoic,
endmember_site_occ):
"""
Infer endmember composition using least squares method.
Parameters:
==========
meas_elem_comp: array
measured mole element composition for specific phase
elements must be in the order (Si, Ti, Al, Fe, Cr, Mn,
Mg, Ni, Co, Ca, Na, K, P, H, C, O))
endmember_elem_stoic: array
stoichiometry of endmembers in terms of elements
rows = individual endmembers
columns = elements (following the order Si, Ti, Al, Fe, Cr, Mn,
Mg, Ni, Co, Ca, Na, K, P, H, C, O)
endmember_site_occ: array
site occupancies for each endmember
rows = individual endmembers
columns = site occupancies in the order of elements on X site,
elements on Y site, T site, followed by oxygen.
Returns:
========
endmember_comp_lsq: np array
endmember proportions from least squares fit
site_occ_lsq: array
site occupancy proportions from least squares fit
resid_lsq: array
least squares residual
"""
endmember_comp_lsq, resid_lsq, rank, sing_vals = np.lingalg.lstsq(endmember_elem_stoic.T, meas_elem_comp)
site_occ_lsq = np.dot(endmember_site_occ.T, endmember_comp_lsq)
return endmember_comp_lsq, site_occ_lsq, resid_lsq
def nnls_endmember_comp(self, meas_elem_comp, endmember_elem_stoic, endmember_site_occ):
endmember_comp_nnls, resid_nnls = optimize.nnls(endmember_elem_stoic.T, meas_elem_comp)
site_occ_nnls = np.dot(endmember_site_occ.T, endmember_comp_nnls)
return endmember_comp_nnls, site_occ_nnls, resid_nnls
def _null_vectors(self, endmember_site_occ_dev):
u, s, vh = np.linalg.svd(endmember_site_occ_dev)
null_vectors=[]
threshold = 1e-10
for ivh in vh:
if np.all(np.abs((endmember_site_occ_dev.dot(ivh))) <= threshold):
null_vectors.append(ivh)
else:
pass
return null_vectors
def _site_occ_constraints(self, endmember_comp_lsq, site_occ_stoic, meas_elem_comp,
endmember_site_occ, null_vectors, site_occ_lsq,
avg_endmember_site_occ, site_totals, site_id, uniq_site_id,
single_value_index):
fn = lambda endmember_comp_lsq, site_occ_stoic, meas_elem_comp: (
np.linalg.norm(site_occ_stoic.dot(endmember_comp_lsq) - meas_elem_comp))
bounds = [[0., None]]*len(endmember_site_occ.T)
cons = []
for inull_vec in null_vectors:
con = {}
con['type'] = 'eq'
con['fun'] = lambda site_occ_lsq, avg_endmember_site_occ=avg_endmember_site_occ, inull_vec=inull_vec: (
inull_vec.dot(site_occ_lsq-avg_endmember_site_occ))
cons.append(con)
for ivalue in single_value_index:
uniq_site_id.remove(ivalue)
for isite in uniq_site_id:
imask = site_id ==isite
isite_dev = lambda site_occ_lsq, imask=imask, site_totals=site_totals,isite=isite: (
np.sum(site_occ_lsq[imask])-site_totals[isite])
cons.append({'type': 'eq', 'fun': isite_dev})
return fn, bounds, cons
def _constrained_minimization(self, fn, site_occ_lsq, site_occ_stoic, meas_elem_comp, bounds, cons,
endmember_site_occ, threshold):
sol = minimize(fn, site_occ_lsq, args=(site_occ_stoic, meas_elem_comp), method='SLSQP',
bounds=bounds, constraints=cons)
site_occ_constr = sol.x
endmember_comp_constr = np.linalg.pinv(endmember_site_occ.T).dot(site_occ_constr)
resid_constr = site_occ_stoic.dot(site_occ_constr) - meas_elem_comp
rms_resid_constr = np.sqrt(np.mean(resid_constr**2))
success = self._success_test(resid_constr, threshold)
return site_occ_constr, endmember_comp_constr, resid_constr, rms_resid_constr, success
def infer_endmember_comp(self, meas_elem_comp, endmember_elem_stoic,
endmember_site_occ, site_totals, site_id,
threshold=1e-1, lstsq_fit=False, output=True):
"""
Infer endmember composition using minimization method.
Notes
-----
* This method is not currently being used in calibration code; there
is an analogous function in phases.py called "calc_endmember_comp"
* This function implements a complex mechanism of calculating endmember
compositions in which it involves detailed site occupancy constraints
not inherent in either the intrinsic or least squares minimizations to
get endmember compositions
* This function has a more extensive output and will spit out site
occupanices under least squares and constrained minimizations as well
as residuals and success statements
Parameters:
==========
meas_elem_comp: array
measured mole element composition for specific phase
endmember_elem_stoic: array
stoichiometry of endmembers in terms of elements
rows = individual endmembers
columns = elements (following the order Si, Ti, Al, Fe, Cr, Mn,
Mg, Ni, Co, Ca, Na, K, P, H, C, O)
endmember_site_occ: array
site occupancies for each endmember
rows = individual endmembers
columns = site occupancies in the order of elements on X site,
elements on Y site, T site, followed by oxygen.
site_totals: array
total atoms allowed on each site
output: True, default
boolean flag indicating whether full output dictionary is returned;
if False, function will return endmember proportions only
Returns:
========
output: dict
dictionary contents are as follows:
endmember_comp_lsq: array
endmember proportions from least squares fit
site_occ_lsq: array
site occupancie proportions from least squares fit
site_occ_constr: array
final site occupancy proportions from minimization function
endmember_comp_constr: array
final endmember proportions after minimization
resid_lsq: array
least squares residual
resid_constr: array
residual using site proportions from minimization
(minimizes Ax-b=0)
rms_resid_constr: int
root mean square of resid_constr array
OR
endmember_comp_constr: array
final endmember proportions after minimization
"""
site_id_info = np.unique(site_id, return_counts=True, return_index=True)
uniq_site_id = site_id_info[0]
uniq_site_id = uniq_site_id.tolist()
site_id_index = site_id_info[1]
site_id_count = site_id_info[2]
single_value_index, = np.where(site_id_count==1)
self._validate_site_occ_input(single_value_index, site_id_index, endmember_site_occ,
site_totals)
if lstsq_fit == True:
endmember_comp_lsq, site_occ_lsq, resid_lsq = (
self.lstsq_endmember_comp(meas_elem_comp, endmember_elem_stoic,
endmember_site_occ))
endmember_site_occ_inv = np.linalg.pinv(endmember_site_occ.T)
site_occ_stoic = np.dot(endmember_elem_stoic.T, endmember_site_occ_inv)
avg_endmember_site_occ = np.mean(endmember_site_occ, axis=0)
endmember_site_occ_dev = endmember_site_occ-avg_endmember_site_occ
null_vectors = self._null_vectors(endmember_site_occ_dev)
fn, bounds, cons = self._site_occ_constraints(endmember_comp_lsq, site_occ_stoic, meas_elem_comp,
endmember_site_occ, null_vectors, site_occ_lsq,
avg_endmember_site_occ, site_totals, site_id, uniq_site_id,
single_value_index)
site_occ_constr, endmember_comp_constr, resid_constr, rms_resid_constr, success = (
self._constrained_minimization(fn, site_occ_lsq, site_occ_stoic,meas_elem_comp, bounds, cons,
endmember_site_occ, threshold))
if output == True:
output = {}
output['success'] = success
output['endmember_comp_lsq'] = endmember_comp_lsq
output['site_occ_lsq'] = site_occ_lsq
output['resid_lsq'] = resid_lsq
output['site_occ_constr'] = site_occ_constr
output['endmember_comp_constr'] = endmember_comp_constr
output['resid_constr'] = resid_constr
output['rms_resid_constr'] = rms_resid_constr
return output
else:
return success, endmember_comp_constr
else:
endmember_comp_nnls, site_occ_nnls, resid_nnls = (
self.nnls_endmember_comp(meas_elem_comp, endmember_elem_stoic,
endmember_site_occ))
success = self._success_test(resid_nnls, threshold)
if output == True:
output = {}
output['success'] = success
output['endmember_comp_nnls'] = endmember_comp_nnls
output['site_occ_nnls'] = site_occ_nnls
output['resid_nnls'] = resid_nnls
return output
else:
return success, endmember_comp_nnls
def calc_reaction_svd(self, phase_symbols, TOLsvd=1e-4, modelDB=None):
"""
Obtain svd of valid set of reactions
Parameters
----------
phase_symbols : list of strings
list of phases (according to Berman abbreviations) that are
allowed to participate in any given reaction
TOL :
Returns
-------
rxn_svd: array
set of linearly independent reactions with variance minimized and
orthogonality maximized
"""
if modelDB is None:
from thermoengine import model
modelDB = model.Database()
oxide_num = self.oxide_props['oxide_num']
all_mol_oxide_comp = np.zeros((0,oxide_num))
all_phase_name = []
all_phase_symbol = []
all_endmember_name = []
all_endmember_id = np.zeros((0))
all_phase_ind = np.zeros((0))
all_atom_num = []
for (ind_phs, iabbrev) in enumerate(phase_symbols):
#iphs_props = modelDB.phase_attributes[iabbrev]['props']
iphs_props = modelDB.phases[iabbrev].props
iall_mol_oxide_comp = iphs_props['mol_oxide_comp']
iphase_name = iphs_props['phase_name']
iendmember_name = iphs_props['endmember_name']
iendmember_num = len(iendmember_name)
iendmember_id = np.arange(iendmember_num)
iatom_num = iphs_props['atom_num']
#iphase_name_tile = np.tile(np.array([iphs_props['phase_name']]), iendmember_num)
all_phase_ind = np.hstack((all_phase_ind, np.tile(ind_phs,(iendmember_num))))
all_mol_oxide_comp= np.vstack((all_mol_oxide_comp, iall_mol_oxide_comp))
all_phase_name.extend([iphase_name for i in range(iendmember_num)])
all_phase_symbol.extend([iabbrev for i in range(iendmember_num)])
all_endmember_name.extend(iendmember_name)
all_endmember_id = np.hstack((all_endmember_id, iendmember_id))
all_atom_num.extend(iatom_num)
endmember_num = len(all_endmember_name)
all_atom_num = np.array(all_atom_num)
# oxide_atom_num = self.oxide_props['cat_num']+self.oxide_props['oxy_num']
# oxide_comp_per_atom = all_mol_oxide_comp/np.tile(oxide_atom_num[np.newaxis,:],(endmember_num,1))
# oxide_comp_per_atom /= np.tile(np.sum(oxide_comp_per_atom, axis=1)[:,np.newaxis],(1,oxide_atom_num.size))
# np.sum(oxide_comp_per_atom, axis=1)
# Nendmember, Noxide = oxide_comp_per_atom.shape
# u, s, vh =np.linalg.svd(oxide_comp_per_atom.T)
# convert all_mol_oxide comp to moles of atoms for each oxide; divide by oxide num and norm
# to 1;
Nendmember, Noxide = all_mol_oxide_comp.shape
u, s, vh =np.linalg.svd(all_mol_oxide_comp.T)
TOL = TOLsvd
N_nonrxn = np.sum(np.abs(s)>= TOL)
N_rxn = Nendmember-Noxide + np.sum(np.abs(s)< TOL)
non_rxn = vh[0:N_nonrxn]
rxn_svd0 = vh[N_nonrxn:]
scl = np.array([np.linalg.norm(irxn) for irxn in rxn_svd0])
rxn_svd0 = rxn_svd0/np.tile(scl[:,np.newaxis],(1,endmember_num))
# Describe code below; add to documentation
reac_atom_num = 0.5*np.sum(np.abs(rxn_svd0)*all_atom_num, axis=1)
rxn_svd = rxn_svd0*all_atom_num[np.newaxis,:]/(
reac_atom_num[:,np.newaxis])
rxn_svd_props = OrderedDict()
rxn_svd_props['rxn_svd'] = rxn_svd
rxn_svd_props['oxide_num'] = oxide_num
rxn_svd_props['all_mol_oxide_comp'] = all_mol_oxide_comp
rxn_svd_props['all_phase_name'] = all_phase_name
rxn_svd_props['all_phase_symbol'] = all_phase_symbol
rxn_svd_props['all_endmember_name'] = all_endmember_name
rxn_svd_props['all_endmember_id'] = all_endmember_id
rxn_svd_props['all_phase_ind'] = all_phase_ind
rxn_svd_props['all_atom_num'] = all_atom_num
#self._rxn_svd_props = rxn_svd_props #TK: What is the purpose of this?
return rxn_svd_props
#rxn_svd, rxn_svd_props = get_reaction_svd(phase_symbols, TOLsvd=1e-4)
def get_wtcoefs_ortho(self, wtcoefs, wtcoefs_ortho, apply_norm=True):
wts_ortho = wtcoefs.copy()
for iwtcoefs_ortho in wtcoefs_ortho:
wts_ortho -= np.dot(iwtcoefs_ortho, wts_ortho)*iwtcoefs_ortho
if apply_norm:
wts_ortho /= np.linalg.norm(wts_ortho)
return wts_ortho
def random_rxn(self, wtcoefs_ortho=None, Nbasis=36):
wts = 2*np.random.rand(Nbasis)-1
wts /= np.linalg.norm(wts)
if wtcoefs_ortho is not None:
wts = self.get_wtcoefs_ortho(wts, wtcoefs_ortho)
return wts
def _ortho_penalty(self, wts, wtcoefs_ortho=None, scale=1):
if wtcoefs_ortho is None:
cost = 0
else:
wts_ortho = self.get_wtcoefs_ortho(wts, wtcoefs_ortho, apply_norm=False)
frac = np.minimum(np.linalg.norm(wts_ortho)/np.linalg.norm(wts), 1)
cost = scale*(1-frac**2)
return cost
def rxn_complexity(self, rxn_svd, TOL=1e-10):
irxn_abs = np.abs(rxn_svd)
xlogx = irxn_abs*np.log(irxn_abs)
xlogx[irxn_abs<TOL] = 0
complexity = -np.sum(xlogx)
return complexity
def lasso_rxn_complexity(self, rxn_svd, scl=1.0):
irxn_abs = np.abs(rxn_svd)
complexity = -np.sum(-irxn_abs/scl-np.log(2*scl))
return complexity
def linear_combo(self, wts, rxn, return_scl=False):
wts = np.array(wts)
endmember_num = rxn.shape[1]
scl = np.tile(wts[:,np.newaxis],(1,endmember_num))
rxn_wt = np.sum(scl*rxn, axis=0)
scl_tot = np.linalg.norm(rxn_wt)
rxn_wt /= scl_tot
if return_scl:
return rxn_wt, wts/scl_tot
else:
return rxn_wt
def rxn_costfun(self, wts, rxn, wtcoefs_ortho=None, ortho_scale=1, lasso_scale=1, debug=False, TOL=1e-10):
rxn_wt = self.linear_combo(wts, rxn)
# complexity = self.rxn_complexity(rxn_wt, TOL=TOL)
complexity = self.lasso_rxn_complexity(
rxn_wt, scl=lasso_scale)
cost = complexity + self._ortho_penalty(wts, wtcoefs_ortho=wtcoefs_ortho, scale=ortho_scale)
if debug:
if wtcoefs_ortho is not None:
wts_ortho = self.get_wtcoefs_ortho(wts, wtcoefs_ortho, apply_norm=False)
print('norm(wts) = {wts_norm}'.format(wts_norm=np.linalg.norm(wts)))
print('norm(ortho) = {ortho_norm}'.format(ortho_norm=np.linalg.norm(wts_ortho)))
ortho_cost = self._ortho_penalty(wts, wtcoefs_ortho=wtcoefs_ortho, scale=ortho_scale)
print('ortho_cost = ', ortho_cost)
return cost
def _draw_basic_rxns(self, rxn_svd, wtcoefs_ortho=None, Ndraw=10, ortho_scale=1, lasso_scale=1.0, Nbasis=36, TOL=1e-10):
from scipy import optimize
Nbasis = rxn_svd.shape[0]
Nendmem = rxn_svd.shape[1]
wtcoefs = np.zeros((Ndraw, Nbasis))
cost = np.zeros(Ndraw)
rxn_coefs = np.zeros((Ndraw, Nendmem))
for ind in range(Ndraw):
iwts0 = self.random_rxn(wtcoefs_ortho=wtcoefs_ortho, Nbasis=Nbasis)
def costfun(wts, rxn=rxn_svd, wtcoefs_ortho=wtcoefs_ortho,lasso_scale=lasso_scale, ortho_scale=ortho_scale):
return self.rxn_costfun(wts, rxn, wtcoefs_ortho=wtcoefs_ortho, lasso_scale=lasso_scale, ortho_scale=ortho_scale, TOL=TOL)
for ind_fit in range(1):
ifit = optimize.minimize(costfun, iwts0, tol=1e-10)
iwts0 = ifit['x']
iwt_fit = ifit['x']
self.rxn_costfun(iwt_fit, rxn_svd, wtcoefs_ortho=wtcoefs_ortho, lasso_scale=lasso_scale, ortho_scale=ortho_scale, debug=True, TOL=TOL)
irxn_coefs = self.linear_combo(iwt_fit, rxn_svd)
wtcoefs[ind] = iwt_fit
rxn_coefs[ind] = irxn_coefs
cost[ind] = ifit['fun']
return wtcoefs, rxn_coefs, cost
def next_basic_rxn(self, rxn_svd, wtcoefs_ortho=None, Ndraw=10, ortho_scale=1, lasso_scale=1, Nbasis=36, TOL=1e-10):
wtcoefs, rxn_coefs, cost = self._draw_basic_rxns(rxn_svd, wtcoefs_ortho=wtcoefs_ortho, Ndraw=Ndraw, ortho_scale=ortho_scale,
lasso_scale=lasso_scale, Nbasis=Nbasis, TOL=TOL)
ind = np.argmin(cost)
return wtcoefs[ind], rxn_coefs[ind], cost[ind]
def get_rxns(self, rxn_svd, Ndraw=10, ortho_scale=10, lasso_scale=1.0, Nbasis=36, TOL=1e-10):
"""
filter rxn_svd based on cost analysis and get basic reactions
Parameters
----------
rxn_svd : double array
set of linearly independent reactions
Returns
-------
"""
Nbasis = rxn_svd.shape[0]
Nendmem = rxn_svd.shape[1]
wtcoefs_ortho = np.zeros((Nbasis, Nbasis))
wtcoefs = np.zeros((Nbasis, Nbasis))
rxn_coefs = np.zeros((Nbasis, Nendmem))
costs = np.zeros(Nbasis)
for ind in range(Nbasis):
iwtcoefs, irxn_coefs, icost = self.next_basic_rxn(rxn_svd, wtcoefs_ortho=wtcoefs_ortho, Ndraw=Ndraw, ortho_scale=ortho_scale, lasso_scale=lasso_scale, Nbasis=Nbasis, TOL=TOL)
iwtcoefs_ortho = self.get_wtcoefs_ortho(iwtcoefs, wtcoefs_ortho)
wtcoefs[ind] = iwtcoefs
wtcoefs_ortho[ind] = iwtcoefs_ortho
rxn_coefs[ind] = irxn_coefs
costs[ind] = icost
print('icost({ind}) = {icost}'.format(ind=ind,icost=icost))
print('=====')
return wtcoefs, costs, rxn_coefs, wtcoefs_ortho
chem = _Chem()
[docs]class UnorderedList(collections.UserList):
def __eq__(self, other):
return sorted(self.data) == sorted(other)