Source code for pyMETHES.config

#  Copyright (c) 2020-2021 ETH Zurich

"""
Module for the Config class of the simulation.
"""

# Import Packages
from typing import Tuple, Union
import warnings
from collections.abc import Callable
import json
import json5
import scipy.constants as csts

num = Union[int, float]


[docs]class Config: """ Configuration class for the Monte-Carlo simulation. The configuration can be loaded from, or saved to, json or json5 files. Alternatively, it can be provided as, or exported to, a (nested) dictionary. The gas number density is not a configuration parameter, but a cached property of the Config class, which is computed from the pressure and temperature. Attributes: paths_to_cross_section_files (list): paths to the cross section files in txt format gases (list): sum formulae of gases fractions (list): proportions of the gases in the gas mixture max_cross_section_energy (float): maximum cross section energy (eV) output_directory (str): path to the output directory base_name (str): prefix of the output filename save_simulation_pickle (bool): save the simulation as pickle file save_temporal_evolution (bool): save temporal evolution save_swarm_parameters (bool): save swarm parameters save_energy_distribution (bool): save energy distribution EN (float): E/N ratio in (Td) _pressure (float): gas pressure in Pa _temperature (float): gas temperature in K _gas_number_density (float): gas number density in m-3 num_e_initial (int): initial number of electrons initial_pos_electrons (list): initial position [x, y, z] of the electrons' center of mass initial_std_electrons (list): initial broadening of gaussian distributed electrons in x, y and z direction initial_energy_distribution (str): The initial energy distribution of the electrons. Can be either ``"zero"`` (all electrons have zero kinetic energy), ``"fixed"`` (all electrons have the same energy) or ``"maxwell-boltzmann"`` (at temperature :py:attr:`initial_temperature`). Maxwell-Boltzmann support is experimental, check :py:func:`pyMETHES.utils.maxwell_boltzmann_random` and its test case to see if the required precision is achieved. The default is ``"zero"``. If the initial distribution is `"fixed"`, :py:attr:`initial_energy` and :py:attr:`initial_direction` must be set. initial_energy (float): The initial energy of the electrons in eV. initial_direction (Union[Tuple[float, float, float], str]): The initial direction of the electrons. Either the string ``"random"`` to give each electron a random direction or a tuple with three elements x, y, z specifying a single direction for all electrons. initial_temperature (Union[Float, int]): The initial temperature in K. Used for the Maxwell-Boltzmann distribution. num_energy_bins (int): number of energy bins to group the electrons for the energy distribution energy_sharing_factor (float): energy sharing factor for ionization collisions isotropic_scattering (bool): scattering: isotropic (true), non-isotropic according to Vahedi et al. (false) conserve (bool): conservation of the number of electrons num_e_max (int): maximum allowed electron number (when it is reached, the number of electrons is then conserved until simulation ends) seed (int, str): optional. If set to an integer it is used to seed the Simulation. If set to the string `"random"` no seeding occurs. Default value is `"random"`. end_condition_type (str): Specifies the end condition. Can be ``"steady-state"``, ``"num_col_max"``, ``"w_tol+ND_tol"`` or ``"custom"``. The ``"custom"`` end condition requires :py:attr:`is_done` to be set as well. Defaults to ``"w_tol+ND_tol"`` w_tol (float): tolerance on the flux drift velocity. simulation ends when w_err/w < w_tol DN_tol (float): tolerance on the flux diffusion coefficient. simulation ends when DN_err/w < DN_tol num_col_max (int): maximum number of collisions during the simulation, simulation ends when it is reached is_done (Callable): This function gets called to determine whether to end the simulation or not. Gets passed the simulation object as argument. Return ``True`` to stop the simulation, ``False`` otherwise. timeout (int): End the simulation after ``timeout`` seconds. Zero means no timeout. Defaults to zero. """
[docs] def __init__(self, config: Union[str, dict]): """ Instantiate the config. Args: config (str, dict): path to a json or json5 config file, or dictionary. """ if isinstance(config, str): if config.endswith('.json5'): with open(config, "r") as json_file: config = json5.load(json_file) elif config.endswith('.json'): with open(config, "r") as json_file: config = json.load(json_file) else: raise ValueError(f"Configuration file '{config}' has invalid extension." " Extensions '.json' or '.json5' are expected.") # gases input_gases = config['input_gases'] self.paths_to_cross_section_files: list = \ input_gases['paths_to_cross_section_files'] self.gases: list = input_gases['gases'] self.fractions: list = input_gases['fractions'] self.max_cross_section_energy: float = \ float(input_gases['max_cross_section_energy']) # output output = config['output'] self.output_directory: str = output['output_directory'] self.base_name: str = output['base_name'] self.save_simulation_pickle: bool = output['save_simulation_pickle'] self.save_temporal_evolution: bool = output['save_temporal_evolution'] self.save_swarm_parameters: bool = output['save_swarm_parameters'] self.save_energy_distribution: bool = output['save_energy_distribution'] # physical conditions physical_conditions = config['physical_conditions'] self.EN: float = float(physical_conditions['EN']) self._pressure: float = float(physical_conditions['pressure']) self._temperature: float = float(physical_conditions['temperature']) self._gas_number_density: float = None # initial state initial_state = config['initial_state'] self.num_e_initial: int = int(initial_state['num_e_initial']) self.initial_pos_electrons: list = initial_state['initial_pos_electrons'] self.initial_std_electrons: list = initial_state['initial_std_electrons'] self.initial_energy_distribution: str = "zero" self.initial_energy: float = None self.initial_direction: Union[Tuple[float, float, float], str] = None self.initial_temperature: Union[int, float] = None if 'initial_energy_distribution' in initial_state: val = initial_state['initial_energy_distribution'] if val not in ("zero", "fixed", "maxwell-boltzmann"): raise ValueError( "initial_energy_distribution must be zero, fixed or " "maxwell-boltzmann") self.initial_energy_distribution = val if 'initial_energy' in initial_state and \ initial_state['initial_energy'] is not None: self.initial_energy = float(initial_state['initial_energy']) if self.initial_energy < 0: raise ValueError("initial_energy cannot be negative") if 'initial_direction' in initial_state and \ initial_state['initial_direction'] is not None: val = initial_state['initial_direction'] if isinstance(val, str) and val == "random": self.initial_direction = val elif isinstance(val, (list, tuple)): if len(val) != 3: raise ValueError("initial_direction must be \"random\" " "or list of three floats") self.initial_direction = [float(item) for item in val] if self.initial_direction == [0, 0, 0]: raise ValueError("initial_direction cannot be all zero") else: raise ValueError("Invalid value for initial_direction") if 'initial_temperature' in initial_state: self.initial_temperature = float(initial_state['initial_temperature']) if self.initial_temperature < 0: raise ValueError("initial_temperature must be positive") if self.initial_energy_distribution in ("zero", "maxwell-boltzmann"): if self.initial_energy is not None: warnings.warn("initial_energy setting useless with " f"{self.initial_energy_distribution} distribution") if self.initial_direction is not None: warnings.warn("initial_direction setting useless with " f"{self.initial_energy_distribution} distribution") else: assert self.initial_energy_distribution == "fixed" if self.initial_energy is None: raise ValueError("Must set initital_energy") if self.initial_direction is None: raise ValueError("Must set initial_direction") if self.initial_energy_distribution == "maxwell-boltzmann" and \ self.initial_temperature is None: raise ValueError("Must set initial_temperature for Maxwell-Boltzmann") # simulation settings simulation = config['simulation_settings'] self.num_energy_bins: int = simulation['num_energy_bins'] self.energy_sharing_factor: float = float(simulation['energy_sharing_factor']) self.isotropic_scattering: bool = simulation['isotropic_scattering'] self.conserve: bool = simulation['conserve'] self.num_e_max: int = int(simulation['num_e_max']) self.seed: Union[int, str] = "random" if 'seed' in simulation: seed = simulation['seed'] if isinstance(seed, int) or (isinstance(seed, str) and seed == "random"): self.seed = seed else: raise ValueError("seed must be an integer or the string \"random\"") # end conditions end_conditions = config['end_conditions'] self.end_condition_type: str = "w_tol+ND_tol" if 'end_condition_type' in end_conditions: val = str(end_conditions['end_condition_type']) if val not in ("steady-state", "num_col_max", "w_tol+ND_tol", "custom"): raise ValueError("end_condition_type must be \"steady-state\", " "\"num_col_max\", \"w_tol+ND_tol\" or \"custom\"") self.end_condition_type = val self.w_tol: float = float(end_conditions['w_tol']) self.DN_tol: float = float(end_conditions['DN_tol']) self.num_col_max: int = int(end_conditions['num_col_max']) self.is_done: Callable = None self.timeout: int = 0 if 'is_done' in end_conditions and end_conditions['is_done'] is not None: self.is_done = end_conditions['is_done'] if not isinstance(self.is_done, Callable): raise TypeError("is_done must be a function") if self.end_condition_type == "custom" and self.is_done is None: raise ValueError("custom requires is_done to be set to a callback") elif self.end_condition_type != "custom" and self.is_done is not None: warnings.warn("Setting is_done is useless without end_condition_type " "custom") if 'timeout' in end_conditions: self.timeout = int(end_conditions['timeout']) if self.timeout < 0: raise ValueError("timeout must be >= 0")
@property def gas_number_density(self) -> float: if self._gas_number_density is None: self._gas_number_density = \ self.pressure / (csts.Boltzmann * self.temperature) return self._gas_number_density @property def pressure(self) -> float: return self._pressure @pressure.setter def pressure(self, value: float): """ Pressure setter. If a new value is set, resets the cache for the gas number density. Args: value: pressure in Pascal """ self._pressure = value self._gas_number_density = None @property def temperature(self) -> float: return self._temperature @temperature.setter def temperature(self, value: float) -> None: """ Temperature setter. If a new value is set, resets the cache for the gas number density. Args: value: temperature in Kelvin """ self._temperature = value self._gas_number_density = None
[docs] def to_dict(self) -> dict: """ Returns the current configuration as a dictionary. Returns: dict of configuration """ return { 'input_gases': { 'gases': self.gases, 'paths_to_cross_section_files': self.paths_to_cross_section_files, 'fractions': self.fractions, 'max_cross_section_energy': self.max_cross_section_energy, }, 'output': { 'output_directory': self.output_directory, 'base_name': self.base_name, 'save_simulation_pickle': self.save_simulation_pickle, 'save_temporal_evolution': self.save_temporal_evolution, 'save_swarm_parameters': self.save_swarm_parameters, 'save_energy_distribution': self.save_energy_distribution, }, 'physical_conditions': { 'EN': self.EN, 'pressure': self.pressure, 'temperature': self.temperature, }, 'initial_state': { 'num_e_initial': self.num_e_initial, 'initial_pos_electrons': self.initial_pos_electrons, 'initial_std_electrons': self.initial_std_electrons, 'initial_energy_distribution': self.initial_energy_distribution, 'initial_energy': self.initial_energy, 'initial_direction': self.initial_direction, }, 'simulation_settings': { 'num_energy_bins': self.num_energy_bins, 'energy_sharing_factor': self.energy_sharing_factor, 'isotropic_scattering': self.isotropic_scattering, 'conserve': self.conserve, 'num_e_max': self.num_e_max, 'seed': self.seed, }, 'end_conditions': { 'end_condition_type': self.end_condition_type, 'w_tol': self.w_tol, 'DN_tol': self.DN_tol, 'num_col_max': self.num_col_max, 'is_done': self.is_done, } }
[docs] def save_json5(self, path: str = 'config.json5') -> None: """ Saves the current configuration to a json5 file. Args: path (str): path including the file name and extension, example: 'data/config.json5' """ d = self.to_dict() if d['end_conditions']['is_done'] is not None: del d['end_conditions']['is_done'] warnings.warn("Cannot save custom callback to json5!") with open(path, "w") as config_file: json5.dump(d, config_file, indent=2)
[docs] def save_json(self, path: str = 'config.json') -> None: """ Saves the current configuration to a json file. Args: path (str): path including the file name and extension, example: 'data/config.json' """ d = self.to_dict() if d['end_conditions']['is_done'] is not None: del d['end_conditions']['is_done'] warnings.warn("Cannot save custom callback to json!") with open(path, "w") as config_file: json.dump(d, config_file, indent=2)