"""Calculate derived constants and shortcut definitions."""
# Loading yaml config file
import pkg_resources
import yaml
from collections.abc import Mapping
from typing import Optional, List, Any
# TODO:
# - ensure that this works in a from-scratch installation
def __getAllKeys_gen(dl: Any) -> list:
"""Help recursive key search with generator.
Args:
dl (Any): Some dictionary or dict value.
Returns:
list: The list of keys in dl.
Yields:
Iterator[list]: A generator of keys.
"""
if isinstance(dl, dict):
for val in dl.values():
yield from __getAllKeys_gen(val)
yield list(dl.keys())
def _getAllKeys(dl: dict) -> List[str]:
"""Return all keys in a potentially nested dict.
Args:
dl (dict): A dictionary of dictionaries of arbitrary depth.
Returns:
List[str]: All keys.
"""
keys_list = list(__getAllKeys_gen(dl))
return [key for subl in keys_list for key in subl]
def _nestedDictUpdate(d: dict, u: dict) -> dict:
"""Update a nested dictionary with another.
Both dictionaries come from the config yaml.
We only want to update the entries in the custom
config (u) that are different from the default (d).
Args:
d (dict): The reference nested dictionary.
u (dict): The nested dictionary containing the updates.
Returns:
dict: The updated nested dictionary.
"""
# courtesy of stackoverflow (Alex Martelli)
for k, v in u.items():
if isinstance(v, Mapping):
d[k] = _nestedDictUpdate(d.get(k, {}), v)
else:
d[k] = v
return d
def _loadConfig(fpath: Optional[str] = None) -> dict:
"""Load the default config file and the custom one.
Args:
fpath (Optional[str], optional): The filepath of the custom
config file. Defaults to None.
Returns:
dict: The loaded config as dict.
"""
# the default config is listed as part of the package data
# therefore we can access it with pkg_resources
defaultConfig_fpath = pkg_resources.resource_filename(
"ethz_snow", "config/snowConfig_default.yaml"
)
with open(defaultConfig_fpath) as f:
config = yaml.load(f, Loader=yaml.FullLoader)
if fpath is not None:
with open(fpath) as f:
customConfig = yaml.load(f, Loader=yaml.FullLoader)
# ensure that custom updates are
# subset of the valid set of keys
# (there is an edge case where the user
# uses a valid key nested in the wrong place
# we currently do not control for that)
validKeys = _getAllKeys(config)
newKeys = _getAllKeys(customConfig)
diffSet = set(newKeys) - set(validKeys)
if len(diffSet) > 0:
print(
(
f"WARNING: Custom config key{'s'*(len(diffSet) > 1)} {diffSet} "
+ f"{'is' if (len(diffSet) == 1) else 'are'} not valid "
+ "and will be ignored."
)
)
config = _nestedDictUpdate(config, customConfig)
return config
[docs]def calculateDerived(fpath: Optional[str] = None) -> dict:
"""Compute the constants needed for Snowflake. Derive where needed.
Args:
fpath (Optional[str], optional): The filepath of the custom
config file. Defaults to None.
Raises:
NotImplementedError: Vial geometry is not cubic.
Returns:
dict: A dictionary of constants.
"""
# the below code uses somewhat clunky casting
# it's needed because pyyaml parses certain numbers
# in scienfitic notation as strings (YAML 1.1 vs 1.2 I suppose)
# I'm too lazy to write something more sophisticated.
config = _loadConfig(fpath)
const = dict()
# copy directly from yaml
T_eq = float(config["solution"]["T_eq"])
kb = float(config["kinetics"]["kb"])
b = float(config["kinetics"]["b"])
# derived properties of vial
if not config["vial"]["geometry"]["shape"].startswith("cub"):
raise NotImplementedError(
(
f'Cannot handle shape "{config["vial"]["geometry"]["shape"]}". '
+ "Only cubic shape is supported at this moment."
)
)
A = float(config["vial"]["geometry"]["length"]) * float(
config["vial"]["geometry"]["width"]
)
V = A * float(config["vial"]["geometry"]["height"])
# derived properties of solution
cp_s = float(config["solution"]["cp_s"])
solid_fraction = float(config["solution"]["solid_fraction"])
cp_w = float(config["water"]["cp_w"])
cp_i = float(config["water"]["cp_i"])
Dcp = cp_i - cp_w
cp_solution = (
solid_fraction * cp_s + (1 - solid_fraction) * cp_w
) # heat capacity of solution
# Shortcut definitions
mass = float(config["solution"]["rho_l"]) * V
hl = mass * cp_solution
depression = (
float(config["solution"]["k_f"])
/ float(config["solution"]["M_s"])
* (solid_fraction / (1 - solid_fraction))
)
alpha = (
-mass * float(config["water"]["Dh"]) * (1 - solid_fraction)
) # used for sigma time step
beta_solution = depression * mass * cp_solution
# bundle things into a dict now
# don't do it earlier for readability
constVars = [
"T_eq",
"kb",
"b",
"A",
"V",
"cp_s",
"solid_fraction",
"cp_w",
"cp_i",
"cp_solution",
"mass",
"hl",
"depression",
"alpha",
"beta_solution",
]
for myvar in constVars:
const[myvar] = locals()[myvar]
return const