####### TO DO: combine the validity conditions with the user defined validity# Do something with the validity message (why it is invalid)"""Model specification in a multiple expression contextMichel BierlaireSat Jun 28 2025, 12:13:22Implements a model specification in a multiple expression context (using Catalogs)"""from__future__importannotationsimportloggingfromtypingimportCallablefrombiogeme_optimization.paretoimportSetElementfrombiogeme.catalogimportCentralController,Configurationfrombiogeme.databaseimportDatabasefrombiogeme.exceptionsimportBiogemeErrorfrombiogeme.parametersimportParameters,get_default_valuefrombiogeme.results_processingimportEstimationResultsfrombiogeme.toolsimportModelNamesfrombiogeme.validityimportValiditylogger=logging.getLogger(__name__)
[docs]classSpecification:"""Implements a specification"""database:Database|None=None#: :class:`biogeme.database.Database` objectall_results:dict[str:EstimationResults]={}central_controller:CentralController|None=None""" function that generates all the objectives: fct(bioResults) -> list[floatNone] """user_defined_validity_check=None""" function that checks the validity of the results """generic_name='default_name'#: short name for file namesbiogeme_parameters:Parameters|None=Nonemodel_names:ModelNames|None=Nonedef__init__(self,configuration:Configuration,biogeme_parameters:Parameters|None=None,):"""Creates a specification from a configuration :param configuration: configuration of the multiple expression """ifnotisinstance(configuration,Configuration):error_msg='Ctor needs an object of type Configuration'raiseBiogemeError(error_msg)self.configuration=configurationself.validity=Noneself.biogeme_parameters=biogeme_parametersself.maximum_number_parameters=(get_default_value(name='maximum_number_parameters',section='AssistedSpecification')ifbiogeme_parametersisNoneelsebiogeme_parameters.get_value(name='maximum_number_parameters',section='AssistedSpecification'))logger.debug(f'{self.maximum_number_parameters=}')self._estimate()assert(self.validityisnotNone),'Validity must be set by the _estimate function'
[docs]@classmethoddeffrom_string_id(cls,configuration_id:str):"""Constructor using a configuration"""returncls(Configuration.from_string(configuration_id))
[docs]defconfigure_expression(self)->None:"""Configure the expression to the current configuration"""self.central_controller.set_configuration(self.configuration)
[docs]@classmethoddefdefault_specification(cls)->Specification:"""Alternative constructor for generate the default specification"""ifcls.central_controllerisNone:raiseBiogemeError('No central controller has been defined')cls.central_controller.reset_selection()the_config=cls.central_controller.get_configuration()returncls(the_config)
@propertydefconfig_id(self)->str:"""Defined config_id as a property"""returnself.configuration.get_string_id()@config_id.setterdefconfig_id(self,value:str)->None:self.configuration=Configuration.from_string(value)
[docs]defget_results(self)->EstimationResults:"""Obtain the estimation results of the specification"""the_results=self.all_results.get(self.config_id)ifthe_resultsisNone:error_msg=f'No result is available for specification {self.config_id}'raiseBiogemeError(error_msg)returnthe_results
def__repr__(self)->str:returnstr(self.config_id)def_estimate(self)->None:"""Estimate the parameter of the current specification, if not already done"""ifself.central_controllerisNone:error_msg='No central controller has been provided for the model.'raiseBiogemeError(error_msg)ifself.databaseisNone:error_msg='No database has been provided for the estimation.'raiseBiogemeError(error_msg)if__class__.model_namesisNone:__class__.model_names=ModelNames(prefix=self.generic_name)ifself.config_idinself.all_results:results=self.all_results.get(self.config_id)else:logger.debug(f'****** Estimate {self.config_id}')frombiogeme.biogemeimportBIOGEMEthe_biogeme:BIOGEME=BIOGEME.from_configuration(config_id=self.config_id,multiple_expression=self.expression,database=self.database,generate_html=False,generate_yaml=False,save_iterations=False,)number_of_parameters=the_biogeme.number_unknown_parameters()logger.info(f'Model with {number_of_parameters} unknown parameters [max: {self.maximum_number_parameters}]')ifnumber_of_parameters>self.maximum_number_parameters:logger.info(f'Invalid as it exceeds the maximum number of parameters: {self.maximum_number_parameters}')self.validity=Validity(status=False,reason=(f'Too many parameters: {number_of_parameters} > 'f'{self.maximum_number_parameters}'),)self.all_results[self.config_id]=Nonereturnthe_biogeme.model_name=__class__.model_names(self.config_id)logger.info(f'*** Estimate {the_biogeme.model_name}')results=the_biogeme.quick_estimate()self.all_results[self.config_id]=resultsifnotresults.algorithm_has_converged:self.validity=Validity(status=False,reason=f'Optimization algorithm has not converged')returnifself.user_defined_validity_checkisnotNone:self.validity=self.user_defined_validity_check(results)else:self.validity=Validity(status=True,reason='')
[docs]defdescribe(self)->str:"""Short description of the solution. Used for reporting. :return: short description of the solution. :rtype: str """the_results=self.get_results()ifthe_resultsisNone:returnf'Invalid model: {self.validity.reason}'returnf'{the_results.short_summary()}'
[docs]defget_element(self,multi_objectives:Callable[[EstimationResults|None],list[float]])->SetElement:"""Obtains the element from the Pareto set corresponding to a specification :param multi_objectives: function calculating the objectives from the estimation results :type multi_objectives: fct(biogeme.results.bioResults) --> list[float] :return: element from the Pareto set :rtype: biogeme.pareto.SetElement """the_id=self.config_idthe_results=self.get_results()the_objectives=multi_objectives(the_results)element=SetElement(the_id,the_objectives)logger.debug(f'{element=}')returnelement