from abc import ABC, abstractmethod
from typing import Dict, Optional
import numpy as np
import pandas as pd
from scipy import interpolate
# from MagmaPandas.Fe_redox.Fe3Fe2_errors import (
# error_params_1bar,
# error_params_high_pressure,
# )
from MagmaPandas.tools.model_errors import _error_func
# Fe3+/Fe2+ limits of the moving standard deviation in a 30 point window of the validation dataset (provided at ./data/Fe3Fe2_validation_data.csv).
validation_limits_1bar = (0.0351966873706004, 5.948890681577911)
validation_limits_high_pressure = (0.052631579, 2.160641174)
def _Fe3Fe2_error_func(a, b, c, d, Fe3Fe2):
return _error_func(data=Fe3Fe2, a=a, b=b, c=c, d=d)
def _Fe3Fe2_spline(Fe3Fe2, parameters):
return interpolate.splev(Fe3Fe2, parameters)
[docs]
class Fe3Fe2_model(ABC):
[docs]
@abstractmethod
def calculate_Fe3Fe2(
cls, melt_mol_fractions, T_K, fO2, *args, **kwargs
) -> float | np.ndarray:
"""
Calculate melt |Fe3Fe2| ratios
Parameters
----------
melt_mol_fractions : :py:class:`Pandas DataFrame <pandas:pandas.DataFrame>`
Melt composition in oxide mol fractions
T_K : float, array-like
temperature in Kelvin
fO2 : float, array-like
Oxygen fugacity
Returns
-------
float, array-like
melt |Fe3Fe2| ratio
"""
pass
@classmethod
def _calculate_Fe3Fe2_(
cls, melt_mol_fractions, T_K, fO2, offset_parameters=0.0, *args, **kwargs
):
"""
Calculate melt Fe3Fe2 ratios and offset results by ``offset_paramaters`` * standard deviation of the model.
"""
Fe3Fe2 = cls.calculate_Fe3Fe2(
melt_mol_fractions=melt_mol_fractions,
T_K=T_K,
fO2=fO2,
**kwargs,
)
if offset_parameters == 0.0:
return Fe3Fe2
# if self._Fe3Fe2_offset_parameters != 0.0:
offset = cls.get_offset(
melt_composition=melt_mol_fractions,
Fe3Fe2=Fe3Fe2,
offset_parameters=offset_parameters,
pressure=kwargs["P_bar"],
)
Fe3Fe2 = Fe3Fe2 + offset
# Make sure no negative values are returned
try:
Fe3Fe2[Fe3Fe2 <= 0.0] = 1e-6
except TypeError:
Fe3Fe2 = 1e-6 if Fe3Fe2 <= 0 else Fe3Fe2
# Make sure arrays of length 1 are returned as floats instead
try:
if len(Fe3Fe2) == 1:
return np.array(Fe3Fe2)[0]
return Fe3Fe2
except TypeError:
return Fe3Fe2
[docs]
@classmethod
def get_error(
cls,
Fe3Fe2,
error_params_1bar: Dict,
error_params_high_pressure: Dict,
pressure: Optional[pd.Series] = None,
*args,
**kwargs,
) -> float | np.ndarray:
"""
Returns one standard deviation error on |Fe3Fe2| ratios, calculated from a compiled validation dataset.
Parameters
----------
Fe3Fe2 : array-like
melt |Fe3Fe2| ratios
pressure : array-like, optional
pressures of each element in ``Fe3Fe2``. If this term is not included, errors will be calculated strictly at 1 bar.
Returns
-------
float, array-like
|Fe3Fe2| error
"""
name = cls.__name__
# TODO change error_params_1bar to a method argument to reduce dependencies
error_1bar = _Fe3Fe2_error_func(*error_params_1bar[name], Fe3Fe2=Fe3Fe2)
if pressure is None:
return error_1bar
# TODO change error_params_high_pressure to a method argument to reduce dependencies
error_high_pressure = _Fe3Fe2_spline(
Fe3Fe2=Fe3Fe2, parameters=error_params_high_pressure[name]
)
errors = error_1bar.copy()
# catch index errors for ints, floats and 0-dimensional arrays.
try:
error_high_pressure[0]
except (TypeError, IndexError):
# convert everyting to a 1-dimensional arrays.
pressure = np.array([pressure]).flatten()
errors = np.array([errors]).flatten()
error_high_pressure = np.array([error_high_pressure]).flatten()
hp = pressure > 1
errors[hp] = error_high_pressure[hp]
return errors
@classmethod
def get_offset(
cls, Fe3Fe2, offset_parameters, pressure=None, *args, **kwargs
) -> float | np.ndarray:
return cls.get_error(Fe3Fe2=Fe3Fe2, pressure=pressure) * offset_parameters
@staticmethod
def get_offset_parameters(n: int = 1) -> float | np.ndarray:
return np.random.normal(loc=0, scale=1, size=n)