Source code for cashocs.io.config

# Copyright (C) 2020-2024 Sebastian Blauth
#
# This file is part of cashocs.
#
# cashocs is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cashocs is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cashocs.  If not, see <https://www.gnu.org/licenses/>.

"""Management of configuration files."""

from __future__ import annotations

from configparser import ConfigParser
import json
import pathlib
from typing import Any, Dict, List, Optional

from cashocs import _exceptions
from cashocs import _loggers


[docs] def load_config(path: str) -> ConfigParser: """Loads a config object from a config file. Loads the config from a .ini file via the configparser package. Args: path: The path to the .ini file storing the configuration. Returns: The output config file, which includes the path to the .ini file. """ return Config(path)
def _check_for_config_list(string: str) -> bool: """Checks, if string is a valid python list consisting of numbers. Args: string: The input string. Returns: ``True`` if the string is valid, ``False`` otherwise """ result = False for char in string: if not (char.isdigit() or char.isspace() or char in ["[", "]", ".", ",", "-"]): return result if string[0] != "[": return result if string[-1] != "]": return result result = True return result
[docs] class Config(ConfigParser): """Class for handling the config in cashocs.""" def __init__(self, config_file: Optional[str] = None) -> None: """Initializes self. Args: config_file: Path to the config file. """ super().__init__() self.config_errors: List[str] = [] self.config_scheme: Dict[str, Dict[str, Dict[str, Any]]] = { "Mesh": { "mesh_file": { "type": "str", "attributes": ["file"], "file_extension": "xdmf", }, "gmsh_file": { "type": "str", "attributes": ["file"], "file_extension": "msh", }, "geo_file": { "type": "str", "attributes": ["file"], "file_extension": "geo", }, "remesh": { "type": "bool", "requires": [("Mesh", "gmsh_file")], }, "show_gmsh_output": { "type": "bool", }, }, "StateSystem": { "is_linear": { "type": "bool", }, "newton_rtol": { "type": "float", "attributes": ["less_than_one", "positive"], }, "newton_atol": { "type": "float", "attributes": ["non_negative"], }, "newton_iter": { "type": "int", "attributes": ["non_negative"], }, "newton_damped": { "type": "bool", }, "newton_inexact": { "type": "bool", }, "newton_verbose": { "type": "bool", }, "picard_iteration": { "type": "bool", }, "picard_rtol": { "type": "float", "attributes": ["positive", "less_than_one"], }, "picard_atol": { "type": "float", "attributes": ["non_negative"], }, "picard_iter": { "type": "int", "attributes": ["non_negative"], }, "picard_verbose": { "type": "bool", }, }, "OptimizationRoutine": { "algorithm": { "type": "str", "possible_options": [ "gd", "gradient_descent", "bfgs", "lbfgs", "nonlinear_cg", "ncg", "nonlinear_conjugate_gradient", "conjugate_gradient", "newton", "sphere_combination", "convex_combination", "none", ], }, "rtol": { "type": "float", "attributes": ["less_than_one", "non_negative"], }, "atol": { "type": "float", "attributes": ["non_negative"], }, "max_iter": { "type": "int", "attributes": ["non_negative"], }, "gradient_method": { "type": "str", "possible_options": ["direct", "iterative"], }, "gradient_tol": { "type": "float", "attributes": ["less_than_one", "positive"], }, "soft_exit": { "type": "bool", }, }, "LineSearch": { "method": { "type": "str", "possible_options": ["armijo", "polynomial"], }, "initial_stepsize": { "type": "float", "attributes": ["positive"], }, "epsilon_armijo": { "type": "float", "attributes": ["positive", "less_than_one"], }, "beta_armijo": { "type": "float", "attributes": ["positive", "larger_than_one"], }, "safeguard_stepsize": {"type": "bool"}, "polynomial_model": { "type": "str", "possible_options": ["cubic", "quadratic"], }, "factor_high": { "type": "float", "attributes": ["less_than_one", "positive"], "larger_than": ("LineSearch", "factor_low"), }, "factor_low": { "type": "float", "attributes": ["less_than_one", "positive"], }, "fail_if_not_converged": { "type": "bool", }, }, "AlgoLBFGS": { "bfgs_memory_size": { "type": "int", "attributes": ["non_negative"], }, "use_bfgs_scaling": { "type": "bool", }, "bfgs_periodic_restart": { "type": "int", "attributes": ["non_negative"], }, "damped": { "type": "bool", }, }, "AlgoCG": { "cg_method": { "type": "str", "possible_options": ["fr", "pr", "hs", "dy", "hz"], }, "cg_periodic_restart": { "type": "bool", }, "cg_periodic_its": { "type": "int", "attributes": ["non_negative"], }, "cg_relative_restart": { "type": "bool", }, "cg_restart_tol": { "type": "float", "attributes": ["positive"], }, }, "AlgoTNM": { "inner_newton": { "type": "str", "possible_options": ["cg", "cr"], }, "inner_newton_rtol": { "type": "float", "attributes": ["positive", "less_than_one"], }, "inner_newton_atol": { "type": "float", "attributes": ["non_negative"], }, "max_it_inner_newton": { "type": "int", "attributes": ["non_negative"], }, }, "ShapeGradient": { "shape_bdry_def": { "type": "list", }, "shape_bdry_fix": { "type": "list", }, "shape_bdry_fix_x": { "type": "list", }, "shape_bdry_fix_y": { "type": "list", }, "shape_bdry_fix_z": { "type": "list", }, "fixed_dimensions": { "type": "list", }, "use_pull_back": { "type": "bool", }, "lambda_lame": { "type": "float", }, "damping_factor": { "type": "float", "attributes": ["non_negative"], }, "mu_def": { "type": "float", "attributes": ["positive"], }, "mu_fix": { "type": "float", "attributes": ["positive"], }, "use_sqrt_mu": { "type": "bool", }, "inhomogeneous": { "type": "bool", }, "update_inhomogeneous": { "type": "bool", }, "inhomogeneous_exponent": { "type": "float", "attributes": ["non_negative"], }, "use_distance_mu": { "type": "bool", }, "dist_min": { "type": "float", "attributes": ["non_negative"], }, "dist_max": { "type": "float", "larger_equal_than": ("ShapeGradient", "dist_min"), "attributes": ["non_negative"], }, "mu_min": { "type": "float", "attributes": ["positive"], }, "mu_max": { "type": "float", "attributes": ["positive"], }, "boundaries_dist": { "type": "list", }, "smooth_mu": { "type": "bool", }, "use_p_laplacian": { "type": "bool", }, "p_laplacian_power": { "type": "int", "attributes": ["larger_than_one"], }, "p_laplacian_stabilization": { "type": "float", "attributes": ["non_negative", "less_than_one"], }, "degree_estimation": { "type": "bool", }, "global_deformation": { "type": "bool", }, "test_for_intersections": { "type": "bool", }, }, "Regularization": { "factor_volume": { "type": "float", "attributes": ["non_negative"], }, "target_volume": { "type": "float", "attributes": ["non_negative"], }, "use_initial_volume": { "type": "bool", }, "factor_surface": { "type": "float", "attributes": ["non_negative"], }, "target_surface": { "type": "float", "attributes": ["non_negative"], }, "use_initial_surface": { "type": "bool", }, "factor_curvature": { "type": "float", "attributes": ["non_negative"], }, "factor_barycenter": { "type": "float", "attributes": ["non_negative"], }, "target_barycenter": { "type": "list", }, "use_initial_barycenter": { "type": "bool", }, "x_start": { "type": "float", }, "x_end": { "type": "float", "larger_than": ("Regularization", "x_start"), }, "y_start": { "type": "float", }, "y_end": { "type": "float", "larger_than": ("Regularization", "y_start"), }, "z_start": { "type": "float", }, "z_end": { "type": "float", "larger_than": ("Regularization", "z_start"), }, "use_relative_scaling": { "type": "bool", }, }, "MeshQuality": { "volume_change": { "type": "float", "attributes": ["positive", "larger_than_one"], }, "angle_change": { "type": "float", "attributes": ["positive"], }, "tol_lower": { "type": "float", "attributes": ["less_than_one", "non_negative"], }, "tol_upper": { "type": "float", "attributes": ["less_than_one", "positive"], "larger_than": ("MeshQuality", "tol_lower"), }, "measure": { "type": "str", "possible_options": [ "skewness", "radius_ratios", "maximum_angle", "condition_number", ], }, "type": { "type": "str", "possible_options": ["min", "avg", "minimum", "average"], }, "remesh_iter": { "type": "int", "attributes": ["non_negative"], }, }, "MeshQualityConstraints": { "min_angle": { "type": "float", "attributes": ["non_negative"], }, "tol": { "type": "float", "attributes": ["positive"], }, "mode": { "type": "str", "possible_options": ["approximate"], }, "feasible_angle_reduction_factor": { "type": "float", "attributes": ["less_than_one", "non_negative"], }, }, "TopologyOptimization": { "angle_tol": { "type": "float", "attributes": ["positive"], }, "interpolation_scheme": { "type": "str", "possible_options": ["angle", "volume"], }, "normalize_topological_derivative": { "type": "bool", }, "re_normalize_levelset": { "type": "bool", }, "topological_derivative_is_identical": { "type": "bool", }, }, "Output": { "verbose": { "type": "bool", }, "save_results": { "type": "bool", }, "save_txt": { "type": "bool", }, "save_state": { "type": "bool", }, "save_adjoint": { "type": "bool", }, "save_gradient": { "type": "bool", }, "save_mesh": { "type": "bool", "requires": [("Mesh", "gmsh_file")], }, "result_dir": { "type": "str", }, "precision": { "type": "int", "attributes": ["positive"], }, "time_suffix": { "type": "bool", }, }, "Debug": { "remeshing": { "type": "bool", }, "restart": { "type": "bool", }, }, "DEFAULT": {}, } self.default_config_str = """ [Mesh] remesh = False show_gmsh_output = False [StateSystem] is_linear = False newton_rtol = 1e-11 newton_atol = 1e-13 newton_iter = 50 newton_damped = False newton_inexact = False newton_verbose = False picard_iteration = False picard_rtol = 1e-10 picard_atol = 1e-12 picard_iter = 50 picard_verbose = False [OptimizationRoutine] algorithm = none rtol = 1e-3 atol = 0.0 max_iter = 100 soft_exit = False gradient_tol = 1e-9 gradient_method = direct [LineSearch] method = armijo epsilon_armijo = 1e-4 beta_armijo = 2.0 initial_stepsize = 1.0 safeguard_stepsize = True polynomial_model = cubic factor_high = 0.5 factor_low = 0.1 fail_if_not_converged = False [ShapeGradient] lambda_lame = 0.0 damping_factor = 0.0 mu_def = 1.0 mu_fix = 1.0 use_sqrt_mu = False use_p_laplacian = False p_laplacian_power = 2 p_laplacian_stabilization = 0.0 use_pull_back = True use_distance_mu = False mu_min = 1.0 mu_max = 1.0 dist_min = 1.0 dist_max = 1.0 boundaries_dist = [] smooth_mu = False inhomogeneous = False update_inhomogeneous = False inhomogeneous_exponent = 1.0 fixed_dimensions = [] shape_bdry_def = [] shape_bdry_fix = [] shape_bdry_fix_x = [] shape_bdry_fix_y = [] shape_bdry_fix_z = [] degree_estimation = True global_deformation = False test_for_intersections = True [Regularization] factor_volume = 0.0 target_volume = 0.0 use_initial_volume = False factor_surface = 0.0 target_surface = 0.0 use_initial_surface = False factor_curvature = 0.0 factor_barycenter = 0.0 target_barycenter = [0.0, 0.0, 0.0] use_initial_barycenter = False use_relative_scaling = False x_start = 0.0 x_end = 1.0 y_start = 0.0 y_end = 1.0 z_start = 0.0 z_end = 1.0 [AlgoTNM] inner_newton = cr max_it_inner_newton = 50 inner_newton_rtol = 1e-15 inner_newton_atol = 0.0 [AlgoLBFGS] bfgs_memory_size = 5 use_bfgs_scaling = True bfgs_periodic_restart = 0 damped = False [AlgoCG] cg_method = DY cg_periodic_restart = False cg_periodic_its = 10 cg_relative_restart = False cg_restart_tol = 0.25 [MeshQuality] tol_lower = 0.0 tol_upper = 1e-15 measure = skewness type = min volume_change = inf angle_change = inf remesh_iter = 0 [MeshQualityConstraints] min_angle = 0.0 feasible_angle_reduction_factor = 0.0 tol = 1e-2 mode = approximate [TopologyOptimization] angle_tol = 1.0 interpolation_scheme = volume normalize_topological_derivative = False re_normalize_levelset = False topological_derivative_is_identical = False [Output] save_results = True verbose = True save_txt = True save_state = False save_adjoint = False save_gradient = False save_mesh = False result_dir = ./results precision = 3 time_suffix = False [Debug] remeshing = False restart = False """ self.read_string(self.default_config_str) if config_file is not None: file = pathlib.Path(config_file) if file.is_file(): self.read(config_file) else: _loggers.warning( f"Could not find the specified config file {config_file}. " "Using cashocs default config instead." )
[docs] def getlist(self, section: str, option: str, **kwargs: Any) -> List: """Extracts a list from a config file. Args: section: The section where the list is placed. option: The option which contains the list. **kwargs: A list of keyword arguments that get passed to :py:meth:``self.get`` Returns: The list which is specified in section ``section`` and key ``option``. """ if ( self.config_scheme[section][option]["type"] == "list" ) and _check_for_config_list(self.get(section, option)): py_list: List = json.loads(self.get(section, option, **kwargs)) return py_list else: raise _exceptions.InputError( "Config.getlist", "option", f"option {option} in section {section} cannot be used as list.", )
[docs] def validate_config(self) -> None: """Validates the configuration file.""" self._check_sections() self._check_keys() if len(self.config_errors) > 0: raise _exceptions.ConfigError(self.config_errors)
def _check_sections(self) -> None: """Checks whether all sections are valid.""" for section_name, section in self.items(): if section_name not in self.config_scheme: self.config_errors.append( f"The following section is not valid: {section}\n" ) def _check_keys(self) -> None: """Checks the keys of the sections.""" for section_name, section in self.items(): for key in section.keys(): if section_name in self.config_scheme: if key not in self.config_scheme[section_name].keys(): self.config_errors.append( f"Key {key} is not valid for section {section_name}.\n" ) else: self._check_key_type(section_name, key) self._check_possible_options(section_name, key) self._check_attributes(section_name, key) self._check_key_requirements(section_name, key) self._check_larger_than_relation(section_name, key) self._check_larger_equal_than_relation(section_name, key) def _check_key_type(self, section: str, key: str) -> None: """Checks if the type of the key is correct. Args: section: The corresponding section key: The corresponding key """ key_type = self.config_scheme[section][key]["type"] try: if key_type.casefold() == "str": self.get(section, key) elif key_type.casefold() == "bool": self.getboolean(section, key) elif key_type.casefold() == "int": self.getint(section, key) elif key_type.casefold() == "float": self.getfloat(section, key) elif key_type.casefold() == "list": if not _check_for_config_list(self.get(section, key)): raise ValueError except ValueError: self.config_errors.append( f"Key {key} in section {section} has the wrong type. " f"Required type is {key_type}.\n" ) def _check_key_requirements(self, section: str, key: str) -> None: """Checks, whether the requirements for the key are satisfied. Args: section: The corresponding section key: The corresponding key """ if ( self.config_scheme[section][key]["type"].casefold() == "bool" and self[section][key].casefold() == "true" ): if "requires" in self.config_scheme[section][key].keys(): requirements = self.config_scheme[section][key]["requires"] for req in requirements: if not self.has_option(req[0], req[1]): self.config_errors.append( f"Key {key} in section {section} requires " f"key {req[1]} in section {req[0]} to be present.\n" ) def _check_possible_options(self, section: str, key: str) -> None: """Checks, whether the given option is possible. Args: section: The corresponding section key: The corresponding key """ if "possible_options" in self.config_scheme[section][key].keys(): if ( self[section][key].casefold() not in self.config_scheme[section][key]["possible_options"] ): self.config_errors.append( f"Key {key} in section {section} has a wrong value. " f"Possible options are " f"{self.config_scheme[section][key]['possible_options']}.\n" ) def _check_larger_than_relation(self, section: str, key: str) -> None: """Checks, whether a given option is larger than one. Args: section: The corresponding section key: The corresponding key """ if "larger_than" in self.config_scheme[section][key].keys(): higher_value = self.getfloat(section, key) partner = self.config_scheme[section][key]["larger_than"] lower_value = self.getfloat(partner[0], partner[1]) if lower_value >= higher_value: self.config_errors.append( f"The value of key {key} in section {section} is smaller than " f"the value of key {partner[1]} in section {partner[0]}, " f"but it should be larger.\n" ) def _check_larger_equal_than_relation(self, section: str, key: str) -> None: """Checks, whether a given option is larger or equal to another. Args: section: The corresponding section key: The corresponding key """ if "larger_equal_than" in self.config_scheme[section][key].keys(): higher_value = self.getfloat(section, key) partner = self.config_scheme[section][key]["larger_equal_than"] lower_value = self.getfloat(partner[0], partner[1]) if lower_value > higher_value: self.config_errors.append( f"The value of key {key} in section {section} is smaller than " f"the value of key {partner[1]} in section {partner[0]}, " f"but it should be larger.\n" ) def _check_attributes(self, section: str, key: str) -> None: """Checks the attributes of a key. Args: section: The corresponding section key: The corresponding key """ if "attributes" in self.config_scheme[section][key].keys(): key_attributes = self.config_scheme[section][key]["attributes"] self._check_file_attribute(section, key, key_attributes) self._check_non_negative_attribute(section, key, key_attributes) self._check_positive_attribute(section, key, key_attributes) self._check_less_than_one_attribute(section, key, key_attributes) self._check_larger_than_one_attribute(section, key, key_attributes) def _check_file_attribute( self, section: str, key: str, key_attributes: List[str] ) -> None: """Checks, whether a file specified in key exists. Args: section: The corresponding section key: The corresponding key key_attributes: The list of attributes for key. """ if "file" in key_attributes: file = pathlib.Path(self.get(section, key)) if not file.is_file(): self.config_errors.append( f"Key {key} in section {section} should point to a file, " f"but the file does not exist.\n" ) self._check_file_extension( section, key, self.config_scheme[section][key]["file_extension"] ) def _check_file_extension(self, section: str, key: str, extension: str) -> None: """Checks, whether key has the correct file extension. Args: section: The corresponding section. key: The corresponding key. extension: The file extension. """ path_to_file = self.get(section, key) if not path_to_file.split(".")[-1] == extension: self.config_errors.append( f"Key {key} in section {section} has the wrong file extension, " f"it should end in .{extension}.\n" ) def _check_non_negative_attribute( self, section: str, key: str, key_attributes: List[str] ) -> None: """Checks, whether key is nonnegative. Args: section: The corresponding section key: The corresponding key key_attributes: The list of attributes for key. """ if "non_negative" in key_attributes: if self.getfloat(section, key) < 0: self.config_errors.append( f"Key {key} in section {section} is negative, but it must not be.\n" ) def _check_positive_attribute( self, section: str, key: str, key_attributes: List[str] ) -> None: """Checks, whether key is positive. Args: section: The corresponding section key: The corresponding key key_attributes: The list of attributes for key. """ if "positive" in key_attributes: if self.getfloat(section, key) <= 0: self.config_errors.append( f"Key {key} in section {section} is non-positive, " f"but it most be positive.\n" ) def _check_less_than_one_attribute( self, section: str, key: str, key_attributes: List[str] ) -> None: """Checks, whether key is less than one. Args: section: The corresponding section key: The corresponding key key_attributes: The list of attributes for key. """ if "less_than_one" in key_attributes: if self.getfloat(section, key) >= 1: self.config_errors.append( f"Key {key} in section {section} is larger than one, " f"but it must be smaller.\n" ) def _check_larger_than_one_attribute( self, section: str, key: str, key_attributes: List[str] ) -> None: """Checks, whether key is larger than one. Args: section: The corresponding section key: The corresponding key key_attributes: The list of attributes for key. """ if "larger_than_one" in key_attributes: if self.getfloat(section, key) <= 1: self.config_errors.append( f"Key {key} in section {section} is smaller than one, " f"but it must be larger.\n" )