-
Notifications
You must be signed in to change notification settings - Fork 50
/
specification.py
190 lines (159 loc) · 6.74 KB
/
specification.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
######
# 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 context
:author: Michel Bierlaire
:date: Mon Apr 10 12:33:18 2023
Implements a model specification in a multiple expression context (using Catalogs)
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Callable
from biogeme_optimization.pareto import SetElement
import biogeme.biogeme as bio
import biogeme.tools.unique_ids
from biogeme.configuration import Configuration
from biogeme.exceptions import BiogemeError
from biogeme.parameters import get_default_value
from biogeme.validity import Validity
if TYPE_CHECKING:
from biogeme.results import bioResults
logger = logging.getLogger(__name__)
class Specification:
"""Implements a specification"""
database = None #: :class:`biogeme.database.Database` object
all_results = {} #: dict(str: `biogeme.results.bioResults`)
expression = None #: :class:`biogeme.expressions.Expression` object
"""
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 names
def __init__(
self,
configuration: Configuration,
maximum_number_of_parameters: int | None = None,
):
"""Creates a specification from a configuration
:param configuration: configuration of the multiple expression
:type configuration: biogeme.configuration.Configuration
"""
if not isinstance(configuration, Configuration):
error_msg = 'Ctor needs an object of type Configuration'
raise BiogemeError(error_msg)
self.configuration = configuration
self.model_names = None
self.validity = None
self.maximum_number_parameters = (
get_default_value(
name='maximum_number_parameters', section='AssistedSpecification'
)
if maximum_number_of_parameters is None
else maximum_number_of_parameters
)
self._estimate()
assert (
self.validity is not None
), 'Validity must be set by the _estimate function'
@classmethod
def from_string_id(cls, configuration_id: str):
"""Constructor using a configuration"""
return cls(Configuration.from_string(configuration_id))
def configure_expression(self) -> None:
"""Configure the expression to the current configuration"""
self.expression.configure_catalogs(self.configuration)
@classmethod
def default_specification(cls) -> Specification:
"""Alternative constructor for generate the default specification"""
cls.expression.reset_expression_selection()
the_config = cls.expression.current_configuration()
return cls(the_config)
@property
def config_id(self) -> str:
"""Defined config_id as a property"""
return self.configuration.get_string_id()
@config_id.setter
def config_id(self, value: str) -> None:
self.configuration = Configuration.from_string(value)
def get_results(self) -> bioResults:
"""Obtain the estimation results of the specification"""
the_results = self.all_results.get(self.config_id)
if the_results is None:
error_msg = f'No result is available for specification {self.config_id}'
raise BiogemeError(error_msg)
return the_results
def __repr__(self) -> str:
return str(self.config_id)
def _estimate(self) -> None:
"""Estimate the parameter of the current specification, if not already done"""
if self.expression is None:
error_msg = 'No expression has been provided for the model.'
raise BiogemeError(error_msg)
if self.database is None:
error_msg = 'No database has been provided for the estimation.'
raise BiogemeError(error_msg)
if self.model_names is None:
self.model_names = biogeme.tools.unique_ids.ModelNames(
prefix=self.generic_name
)
if self.config_id in self.all_results:
results = self.all_results.get(self.config_id)
else:
logger.debug(f'****** Estimate {self.config_id}')
the_biogeme = bio.BIOGEME.from_configuration(
config_id=self.config_id,
expression=self.expression,
database=self.database,
)
number_of_parameters = the_biogeme.number_unknown_parameters()
if 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}'
),
)
return
the_biogeme.modelName = self.model_names(self.config_id)
logger.info(f'*** Estimate {the_biogeme.modelName}')
the_biogeme.generate_html = False
the_biogeme.generate_pickle = False
results = the_biogeme.quick_estimate()
self.all_results[self.config_id] = results
if not results.algorithm_has_converged():
self.validity = Validity(
status=False, reason=f'Optimization algorithm has not converged'
)
return
if self.user_defined_validity_check is not None:
self.validity = self.user_defined_validity_check(results)
else:
self.validity = Validity(status=True, reason='')
def describe(self) -> str:
"""Short description of the solution. Used for reporting.
:return: short description of the solution.
:rtype: str
"""
the_results = self.get_results()
return f'{the_results.short_summary()}'
def get_element(
self, multi_objectives: Callable[[bioResults], 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_id
the_results = self.get_results()
the_objectives = multi_objectives(the_results)
element = SetElement(the_id, the_objectives)
logger.debug(f'{element=}')
return element