Source code for biogeme.parameters

"""Centralized management of the parameters.

Parameters are organized by section. Parameters within the same
section must have different names

:author: Michel Bierlaire
:date: Fri Dec  2 12:17:38 2022

"""

import logging
from datetime import datetime
from typing import NamedTuple
import tomlkit as tk
import biogeme.exceptions as excep
from biogeme.version import getVersion
from biogeme.default_parameters import all_parameters_tuple, ParameterTuple

logger = logging.getLogger(__name__)

TRUE_STR = ('True', 'true', 'Yes', 'yes')
FALSE_STR = ('False', 'false', 'No', 'no')
DEFAULT_FILE_NAME = 'biogeme.toml'
ROW_LENGTH = 80


[docs] class NameSectionTuple(NamedTuple): name: str section: str
[docs] def format_comment(the_param): """Format the content of the file, in particular the description of the parameter :param the_param: parmeter to format :type the_param: biogeme.default_parameters.ParameterTuple :return: formatted text :rtype: str """ the_str = f'{the_param.name} = {the_param.value}' multiplier = len(the_str) + 1 words = the_param.description.split(' ') current_row = '#' rows = [] for word in words: the_length = len(current_row) if not rows: the_length += multiplier if the_length + len(word) > ROW_LENGTH: # Start a new line rows.append(current_row) blanks = ' ' * multiplier if the_param.type in (bool, str): blanks += ' ' current_row = f'{blanks}#' current_row += ' ' + word if current_row != '': rows.append(current_row) formatted = '\n'.join(rows) return formatted
[docs] def parse_boolean(value): """Transforms one of the string representing a boolean into an actual boolean :param value: value to be transformed :type value: str :return: equivalent boolean :rtype: bool """ if value in TRUE_STR: return True if value in FALSE_STR: return False error_msg = f'{value} is not a valid boolean. Use "True" of "False"' raise excep.BiogemeError(error_msg)
[docs] class Parameters: """Parameters management"""
[docs] def __init__(self): # Store values in a dict self.document = None # TOML document self.file_name = None self.all_parameters_dict = {} # Add the default parameters for param in all_parameters_tuple: self.add_parameter(param)
[docs] def add_parameter(self, parameter_tuple): """Add one aprameter :param parameter_tuple: tuple containing the data associated with the parameter :type parameter_tuple: biogeme.default_parameters.ParameterTuple """ key = NameSectionTuple( name=parameter_tuple.name, section=parameter_tuple.section ) ok, messages = self.check_parameter_value(parameter_tuple) if not ok: raise excep.BiogemeError(messages) self.all_parameters_dict[key] = parameter_tuple
[docs] def read_file(self, file_name): """Read TOML file""" self.file_name = DEFAULT_FILE_NAME if file_name is None else file_name try: with open(self.file_name, 'r', encoding='utf-8') as f: content = f.read() self.document = tk.parse(content) self.import_document() logger.info(f'File {self.file_name} has been parsed.') except FileNotFoundError: self.dump_file(self.file_name)
[docs] def parameters_in_section(self, section_name): """Returns the names of the parameters in a section :param section_name: name of the section :type section_name: str """ results = [ p.name for p in self.all_parameters_dict.values() if p.section == section_name ] return results
[docs] def sections_from_name(self, name): """Obtain the list of sections containing the given entry :param name: name of the parameter :type name: str """ return {p.section for p in self.all_parameters_dict if p.name == name}
[docs] def dump_file(self, file_name): """Dump the values of the parameters in the TOML file""" self.document = self.generate_document() with open(file_name, 'w', encoding='utf-8') as f: print(tk.dumps(self.document), file=f) logger.warning(f'File {file_name} has been created')
[docs] def import_document(self): """Record the values of the parameters in the TOML document""" for section_name, entries in self.document.items(): for entry_name, entry_value in entries.items(): key = NameSectionTuple(name=entry_name, section=section_name) default = self.all_parameters_dict.get(key) if default is None: warning_msg = ( f'Entry {entry_name} in Section {section_name} is ' f'ignored by Biogeme.' ) logger.warning(warning_msg) continue if entry_value is None: value = default.value elif default.type is bool: try: value = parse_boolean(entry_value) except excep.BiogemeError as e: error_msg = ( f'Error with entry {entry_name} in Section ' f'{section_name}: {e}' ) raise error_msg from e else: value = entry_value the_parameter = ParameterTuple( name=entry_name, value=value, type=default.type, section=section_name, description=default.description, check=default.check, ) self.add_parameter(the_parameter)
[docs] def generate_document(self): """Generate the TOML document""" current_datetime = datetime.now() formatted_datetime = current_datetime.strftime("%B %d, %Y. %H:%M:%S") doc = tk.document() doc.add(tk.comment(f'Default parameter file for Biogeme {getVersion()}')) doc.add(tk.comment(f'Automatically created on {formatted_datetime}')) sections = {p.section for p in self.all_parameters_dict} tables = {section: tk.table() for section in sections} for parameter in self.all_parameters_dict.values(): if isinstance(parameter.value, bool): value = 'True' if parameter.value else 'False' else: value = parameter.value tables[parameter.section].add(parameter.name, value) tables[parameter.section][parameter.name].comment(format_comment(parameter)) for s, t in tables.items(): doc[s] = t return doc
[docs] def get_param_tuple(self, name, section=None): """Obtain a tuple describing the parameter :param name: name of the parameter :type name: str :param section: section where the parameter is relevant :type section: str :return: tuple containing the name, section and value of the parameter, or None if not found :rtype: biogeme.default_parameters.ParameterTuple """ if section is None: sections = self.sections_from_name(name) if len(sections) > 1: error_msg = ( f'Ambiguity: parameter {name} belongs to multiple ' f'sections: {sections}.' ) raise excep.BiogemeError(error_msg) if not sections: error_msg = f'Parameter {name} does not belong to any section.' raise excep.BiogemeError(error_msg) section = sections.pop() name_section = NameSectionTuple(name=name, section=section) the_tuple = self.all_parameters_dict.get(name_section) if the_tuple is None: if section is None: error_msg = f'Unknown parameter {name}' else: known = self.parameters_in_section(section) if known: known_msg = f'Known parameter(s): {", ".join(known)}.' else: known_msg = 'The section does not exist.' error_msg = ( f'Unknown parameter {name} in section {section}. {known_msg}' ) raise excep.BiogemeError(error_msg) return the_tuple
[docs] @staticmethod def check_parameter_value(the_tuple): """Check if the value of the parameter is valid :param the_tuple: tuple to check :type the_tuple: biogeme.default_parameters.ParameterTuple :return: a boolean that is True if the value is valid. If not, diagnostics are provided. :rtype: tuple(bool, list(str)) """ ok = True messages = [] if the_tuple.check is None: return ok, messages for check in the_tuple.check: is_ok, diag = check(the_tuple.value) if not is_ok: ok = False messages.append( f'Error for parameter {the_tuple.name} in ' f'section {the_tuple.section}. {diag}' ) return ok, messages
[docs] def set_value(self, name, value, section=None): """Set a value of a parameter. If the parameter appears in only one section, the name of the section can be ommitted. :param name: name of the parameter :type name: str :param value: value of the parameter :type value: str :param section: section containing the parameter :type section: str """ the_tuple = self.get_param_tuple(name, section) the_parameter = ParameterTuple( name=the_tuple.name, value=value, type=the_tuple.type, section=the_tuple.section, description=the_tuple.description, check=the_tuple.check, ) self.add_parameter(the_parameter)
[docs] def get_value(self, name, section=None): """Get the value of a parameter. If the parameter appears in only one section, the name of the section can be ommitted. :param name: name of the parameter :type name: str :param section: section containing the parameter :type section: str """ the_tuple = self.get_param_tuple(name, section) # logger.info(f'{name} = {the_tuple.value}') return the_tuple.value
biogeme_parameters = Parameters()