"""Classes for represernting the variaton of material properties with state."""
import copy
import numpy as np
import scipy.interpolate
import asteval
def _create_interp_arrays_from_yaml_table_2d(yaml_dict, state_vars, state_vars_interp_scales):
"""
Create 2d interpolation arrays for `griddata` from a YAML dict.
Arguments:
yaml_dict (dict): variation with state subdictionary extracted from a YAML file.
state_vars (list of string): State variable names.
state_vars_interp_scales (list of string): Interpolation scale for each state \
variable. Should be 'log' or 'linear'.
Returns:
ndarray: interpolation points.
ndarray: interpolation values.
"""
if len(state_vars) != 2:
raise ValueError()
# TODO there has got to be a less awful way to represent a 2+ dimensional
# interpolation table in YAML and load it.
interp_points = np.empty((0, 2))
interp_values = np.array([])
tbl_dict = yaml_dict[state_vars[0]]
interp_points_0 = np.array(list(tbl_dict.keys()))
interp_points_0.sort()
interp_points_1 = np.array(
[tbl_dict[p0][state_vars[1]] for p0 in interp_points_0])
# From the yaml dictionary 2d table, build an array compatible with
# scipy.interpolate.griddata
for i in range(len(interp_points_0)):
# The value of the 0th state variable for this slice of the interpolation table.
point_0 = interp_points_0[i]
interp_points = np.append(
interp_points,
np.stack(
(
np.full_like(interp_points_1[i], point_0, dtype=np.double),
interp_points_1[i]
),
axis=1),
axis=0)
interp_values = np.append(interp_values, tbl_dict[point_0]['values'])
for j in range(2):
if state_vars_interp_scales[j] == 'log':
interp_points[:, j] = np.log(interp_points[:, j])
return interp_points, interp_values
def _create_interp_arrays_from_yaml_table(yaml_dict, state_vars, state_vars_interp_scales):
"""
Create 1d or 2d interpolation arrays for `griddata` from a YAML dict.
Arguments:
yaml_dict (dict): property dictionary extracted from a YAML file.
state_vars (list of string): State variable names.
state_vars_interp_scales (list of string): Interpolation scale for each state
variable. Should be 'log' or 'linear'.
Returns:
ndarray: interpolation points.
ndarray: interpolation values.
"""
if len(state_vars) == 1:
interp_points = np.array(
yaml_dict[state_vars[0]])
interp_values = np.array(
yaml_dict['values'])
if state_vars_interp_scales[0] == 'log':
interp_points = np.log(interp_points)
elif len(state_vars) == 2:
interp_points, interp_values = _create_interp_arrays_from_yaml_table_2d(
yaml_dict, state_vars, state_vars_interp_scales)
else:
raise NotImplementedError('More than two state variables not supported.')
return interp_points, interp_values
[docs]class VariationWithState:
"""
A model of a material property's variation with state.
Arguments:
representation (string): Description of how the data is represented, e.g. "table".
state_vars (list of string): Names of the state variables.
state_vars_units (dict of string): Units of measure for each state variable.
value_type (string): Is the stored value a multiplier on the default value,
or does it override the default value?
reference (string): Bibtex tag for the source of the data.
"""
def __init__(self, representation, state_vars, state_vars_units, value_type, reference):
self.representation = representation
self.state_vars = state_vars
self.state_vars_units = state_vars_units
for sv in self.state_vars:
if not sv in self.state_vars_units:
raise ValueError('No units given for {:s}'.format(sv))
allowed_value_types = ['multiplier', 'override']
if value_type not in allowed_value_types:
raise ValueError(
'value_type "{:s}" is not allowed.\n'.format(value_type)
+ 'Allowed value_types are {}'.format(allowed_value_types))
self.value_type = value_type
self.reference = reference
[docs] def query_value(self, state):
"""Query the variation with state model at a particular state."""
pass
[docs] def get_state_domain(self):
"""Get the domain over which the variation with state model is valid."""
pass
[docs] def is_state_in_domain(self, state):
"""
Check that the state is within the valid domain.
Returns:
bool: True if `state` is in the valid domain for the model.
"""
# Check that all the state variables have been provided in `state`.
for var_name in self.state_vars:
if var_name not in state.keys():
raise ValueError('{:s} not provided for query'.format(var_name))
state_domain = self.get_state_domain() # pylint: disable=assignment-from-no-return
for state_name in state:
value = state[state_name]
smin = state_domain[state_name][0]
smax = state_domain[state_name][1]
if np.any(value < smin) or np.any(value > smax):
return False
return True
def __str__(self):
state_var_str = ', '.join(self.state_vars)
domain_str_list = []
domain = self.get_state_domain() # pylint: disable=assignment-from-no-return
for name in self.state_vars:
domain_str_list.append(
'{:.4g} to {:.4g} {:s}'.format(
domain[name][0], domain[name][1], self.state_vars_units[name]))
domain_str = ', '.join(domain_str_list)
return 'Variation with {:s} over {:s}, represented as a {:s}. [Data from {:s}]'.format(
state_var_str, domain_str, self.representation, self.reference)
[docs]class VariationWithStateTable(VariationWithState):
"""A material property's variation with state, represented as a table."""
def __init__(self, state_vars, state_vars_units, value_type, reference,
interp_points, interp_values, state_vars_interp_scales):
VariationWithState.__init__(self, 'table', state_vars, state_vars_units, value_type, reference)
self._interp_points = interp_points
self._interp_values = interp_values
self._state_vars_interp_scales = state_vars_interp_scales
[docs] def query_value(self, state, method='linear', fill_value=np.nan, rescale=True):
"""
Query the value of the property at a particular state.
Arguments:
state (dict): The state at which to query the values. It must have
a key for each variable name in `self.state_vars`. `state[s1]`
specifies the query point for state variable `s1`. The query point
for each state may be an array or a scalar. e.g.\n
\t`state={'s1': 0, 's2': 1}`\n
\t`state={'s1': 0, 's2': [1, 2, 3]}`\n
\tand\n
\t`state={'s1': [5, 6, 7], 's2': [1, 2, 3]}`\n
are all valid.
`method`, `fill_value`, and `rescale`: are passed through to `scipy.interpolate.griddata`.
Returns:
scalar or array: value(s) of the property at the provided state(s).
"""
# Check that all the state variables have been provided in `state`.
for var_name in self.state_vars:
if var_name not in state.keys():
raise ValueError('{:s} not provided for query'.format(var_name))
# Copy the state argument - we might need to mutate it.
state = copy.deepcopy(state)
# Form an array of points at which to query the interpolation.
is_state_scalar = True
if len(self.state_vars) == 1:
query_points = np.array(state[self.state_vars[0]])
if np.ndim(query_points) > 0:
is_state_scalar = False
if self._state_vars_interp_scales[0] == 'log':
query_points = np.log(query_points)
elif len(self.state_vars) == 2:
# Handle cases with 1 array and 1 scalar state
# by using `np.full_like` to convert the scalar state to an array
if np.ndim(state[self.state_vars[0]]) > 0 \
and np.ndim(state[self.state_vars[1]]) == 0:
state[self.state_vars[1]] = np.full_like(
state[self.state_vars[0]], state[self.state_vars[1]])
if np.ndim(state[self.state_vars[0]]) == 0 \
and np.ndim(state[self.state_vars[1]]) > 0:
state[self.state_vars[0]] = np.full_like(
state[self.state_vars[1]], state[self.state_vars[0]])
# Handle case where both state queries are arrays
# This is intentionally *not* an `elif` - the above cases should
# flow into this.
if np.ndim(state[self.state_vars[0]]) > 0 \
and np.ndim(state[self.state_vars[1]]) > 0:
if len(state[self.state_vars[0]]) != len(state[self.state_vars[1]]):
raise ValueError('Query arrays must be of equal length for each state.')
is_state_scalar = False
query_points = np.stack(
(state[self.state_vars[0]], state[self.state_vars[1]]), axis=1)
# Handle case where both state queries are scalars
else:
query_points = np.array([[state[self.state_vars[0]], state[self.state_vars[1]]]])
# If one of the state variables is interpolated on a log scale,
# take its log in the query.
for j in range(len(self.state_vars)):
if self._state_vars_interp_scales[j] == 'log':
query_points[:, j] = np.log(query_points[:, j])
values = scipy.interpolate.griddata(self._interp_points, self._interp_values,
query_points,
method=method, fill_value=fill_value, rescale=rescale)
if is_state_scalar and np.ndim(values) > 0:
return values[0]
return values
[docs] def get_state_domain(self):
"""
Get the domain over which the property's variation with state model is valid.
Returns:
dict: each key is the name of a state variable.
Values are tuples `(smin, smax)` where `smin` is the minimum bound
of the valid domain in that state variable and `smax` is the maximum bound.
The units of `smin` and `smax` are given by `state_vars_units[key]`.
"""
if len(self.state_vars) == 1:
# Assumes interpolation points are in ascending order.
smin = self._interp_points[0]
smax = self._interp_points[-1]
if self._state_vars_interp_scales[0] == 'log':
smin = np.exp(smin)
smax = np.exp(smax)
result = {self.state_vars[0]: (smin, smax)}
elif len(self.state_vars) == 2:
result = {}
for j in range(2):
# Assumes interpolation points are in ascending order.
smin = self._interp_points[0, j]
smax = self._interp_points[-1, j]
if self._state_vars_interp_scales[j] == 'log':
smin = np.exp(smin)
smax = np.exp(smax)
result[self.state_vars[j]] = (smin, smax)
return result
[docs]class VariationWithStateEquation(VariationWithState):
"""
A material property's variation with state, represented as an equation.
Arguments:
state_vars (list of string): Names of the state variables.
state_vars_units (dict of string): Units of measure for each state variable.
value_type (string): Is the stored value a multiplier on the default value, or does it override the default?
reference (string): Bibtex tag for the source of the data.
expression (string): A mathematical expression for the value of the property
as a function of the state variables. This should be a string containing
python math operators, the state variable names, and \'math\' functions, e.g.\n
\t\'value = 1.23 * temperature + 4.5 * temperature**2\' or \n
\t\'value = 5.6 * exp(7.8 / temperature)\'\n
When evaluated, this expression gives the value of the property.
state_domain (dict): each key is the name of a state variable.
Values are tuples \'(smin, smax)\' where \'smin\' is the minimum bound
of the valid domain in that state variable and \'smax\' is the maximum bound.
"""
def __init__(self, state_vars, state_vars_units, value_type, reference,
expression, state_domain):
VariationWithState.__init__(self, 'equation', state_vars, state_vars_units, value_type, reference)
# Create an asteval Procedure which evaluates the `expression`
if 'value' not in expression:
raise ValueError('`expression` must set value equal to a function of the state varaibles.')
aeval = asteval.Interpreter()
args_str = ', '.join(state_vars)
func_str = 'def f({:s}):\n {:s}\n return value'.format(args_str, expression)
aeval(func_str)
self.procedure = aeval('f')
# Check that `state_domain` provides a valid domain for each state.
for name in state_vars:
if name not in state_domain:
raise ValueError('The provided `state_domain` dict does not'
+ ' have a domain for state {:s}'.format(name))
self.state_domain = state_domain
[docs] def query_value(self, state):
"""
Query the value of the property at a particular state.
Arguments:
state (dict): The state at which to query the values. It must have
a key for each variable name in `self.state_vars`. `state[s1]`
specifies the query point for state variable `s1`. The query point
for each state may be an array or a scalar. e.g.\n
\t`state={'s1': 0, 's2': 1}`\n
\t`state={'s1': 0, 's2': np.array([1, 2, 3])}`\n
\tand\n
\t`state={'s1': np.array([5, 6, 7]), 's2': np.array([1, 2, 3])}`\n
are all valid.
Returns:
scalar or array: value(s) of the property at the provided state(s).
"""
# Check that all the state variables have been provided in `state`.
for var_name in self.state_vars:
if var_name not in state.keys():
raise ValueError('{:s} not provided for query'.format(var_name))
if not self.is_state_in_domain(state):
return np.nan
result = self.procedure(**state)
return result
[docs] def get_state_domain(self):
"""
Get the domain over which the property's variation with state model is valid.
Returns:
dict: each key is the name of a state variable.
Values are tuples `(smin, smax)` where `smin` is the minimum bound
of the valid domain in that state variable and `smax` is the maximum bound.
The units of `smin` and `smax` are given by `state_vars_units[key]`.
"""
return self.state_domain
[docs]def build_from_yaml(yaml_dict):
"""Construct a variation with state object from a YAML-derived dictionary."""
state_vars = yaml_dict['state_vars']
state_vars_units = yaml_dict['state_vars_units']
value_type = yaml_dict['value_type']
reference = yaml_dict['reference']
if yaml_dict['representation'] == 'table':
state_vars_interp_scales = yaml_dict.get(
'state_vars_interp_scales', ['linear'] * len(state_vars))
interp_points, interp_values = _create_interp_arrays_from_yaml_table(
yaml_dict, state_vars, state_vars_interp_scales)
return VariationWithStateTable(
state_vars, state_vars_units, value_type, reference,
interp_points, interp_values, state_vars_interp_scales)
elif yaml_dict['representation'] == 'equation':
expression = yaml_dict['expression']
state_domain = yaml_dict['state_domain']
return VariationWithStateEquation(
state_vars, state_vars_units,
value_type, reference,
expression, state_domain)
else:
raise NotImplementedError('Representations other than table or equation are'
+ ' not yet supported.')