""" Arithmetic expressions accepted by Biogeme: elementary expressions
:author: Michel Bierlaire
:date: Tue Mar 7 18:38:21 2023
"""
import logging
import biogeme.exceptions as excep
from .base_expressions import Expression
from .elementary_types import TypeOfElementaryExpression
from .numeric_tools import validate, MAX_VALUE
logger = logging.getLogger(__name__)
[docs]
class Elementary(Expression):
"""Elementary expression.
It is typically defined by a name appearing in an expression. It
can be a variable (from the database), or a parameter (fixed or to
be estimated using maximum likelihood), a random variable for
numrerical integration, or Monte-Carlo integration.
"""
[docs]
def __init__(self, name):
"""Constructor
:param name: name of the elementary experession.
:type name: string
"""
Expression.__init__(self)
self.name = name #: name of the elementary expressiom
self.elementaryIndex = None
"""The id should be unique for all elementary expressions
appearing in a given set of formulas.
"""
def __str__(self):
"""string method
:return: name of the expression
:rtype: str
"""
return f'{self.name}'
[docs]
def getStatusIdManager(self):
"""Check the elementary expressions that are associated with
an ID manager.
:return: two lists of elementary expressions, those with and
without an ID manager.
:rtype: tuple(list(str), list(str))
"""
if self.id_manager is None:
return [], [self.name]
return [self.name], []
[docs]
def getElementaryExpression(self, name):
"""
:return: an elementary expression from its name if it appears in the
expression. None otherwise.
:rtype: biogeme.Expression
"""
if self.name == name:
return self
return None
[docs]
def rename_elementary(self, names, prefix=None, suffix=None):
"""Rename elementary expressions by adding a prefix and/or a suffix
:param names: names of expressions to rename
:type names: list(str)
:param prefix: if not None, the expression is renamed, with a
prefix defined by this argument.
:type prefix: str
:param suffix: if not None, the expression is renamed, with a
suffix defined by this argument.
:type suffix: str
"""
if self.name in names:
if prefix is not None:
self.name = f'{prefix}{self.name}'
if suffix is not None:
self.name = f'{self.name}{suffix}'
[docs]
def number_of_multiple_expressions(self):
"""Count the number of "parallel" expressions
:return: the number of expressions
:rtype: int
"""
return 1
[docs]
class bioDraws(Elementary):
"""
Draws for Monte-Carlo integration
"""
[docs]
def __init__(self, name, drawType):
"""Constructor
:param name: name of the random variable with a series of draws.
:type name: string
:param drawType: type of draws.
:type drawType: string
"""
Elementary.__init__(self, name)
self.drawType = drawType
self.drawId = None
def __str__(self):
return f'bioDraws("{self.name}", "{self.drawType}")'
[docs]
def check_draws(self):
"""List of draws defined outside of 'MonteCarlo'
:return: List of names of variables
:rtype: list(str)
"""
return {self.name}
[docs]
def setIdManager(self, id_manager=None):
"""The ID manager contains the IDs of the elementary expressions.
It is externally created, as it may nee to coordinate the
numbering of several expressions. It is stored only in the
expressions of type Elementary.
:param id_manager: ID manager to be propagated to the
elementary expressions. If None, all the IDs are set to None.
:type id_manager: class IdManager
"""
self.id_manager = id_manager
if id_manager is None:
self.elementaryIndex = None
self.drawId = None
return
self.elementaryIndex = self.id_manager.elementary_expressions.indices[self.name]
self.drawId = self.id_manager.draws.indices[self.name]
[docs]
def getSignature(self):
"""The signature of a string characterizing an expression.
This is designed to be communicated to C++, so that the
expression can be reconstructed in this environment.
The list contains the following elements:
1. the name of the expression between < >
2. the id of the expression between { }, preceeded by a comma
3. the name of the draws
4. the unique ID (preceeded by a comma),
5. the draw ID (preceeded by a comma).
Consider the following expression:
.. math:: 2 \\beta_1 V_1 -
\\frac{\\exp(-\\beta_2 V_2) }{ \\beta_3 (\\beta_2 \\geq \\beta_1)}.
It is defined as::
2 * beta1 * Variable1 - expressions.exp(-beta2*Variable2) /
(beta3 * (beta2 >= beta1))
And its signature is::
[b'<Numeric>{4780527008},2',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<Times>{4780526952}(2),4780527008,4780277152',
b'<Variable>{4511837152}"Variable1",5,2',
b'<Times>{4780527064}(2),4780526952,4511837152',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<UnaryMinus>{4780527120}(1),4780277656',
b'<Variable>{4511837712}"Variable2",6,3',
b'<Times>{4780527176}(2),4780527120,4511837712',
b'<exp>{4780527232}(1),4780527176',
b'<Beta>{4780277264}"beta3"[1],2,0',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<GreaterOrEqual>{4780527288}(2),4780277656,4780277152',
b'<Times>{4780527344}(2),4780277264,4780527288',
b'<Divide>{4780527400}(2),4780527232,4780527344',
b'<Minus>{4780527456}(2),4780527064,4780527400']
:return: list of the signatures of an expression and its children.
:rtype: list(string)
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
elementary expression
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
draw
"""
if self.elementaryIndex is None:
error_msg = (
f'No id has been defined for elementary ' f'expression {self.name}.'
)
raise excep.BiogemeError(error_msg)
if self.drawId is None:
error_msg = f'No id has been defined for draw {self.name}.'
raise excep.BiogemeError(error_msg)
signature = f'<{self.getClassName()}>'
signature += f'{{{self.get_id()}}}'
signature += f'"{self.name}",{self.elementaryIndex},{self.drawId}'
return [signature.encode()]
[docs]
def dict_of_elementary_expression(self, the_type):
"""Extract a dict with all elementary expressions of a dpecific type
:param the_type: the type of expression
:type the_type: TypeOfElementaryExpression
"""
if the_type == TypeOfElementaryExpression.DRAWS:
return {self.name: self.drawType}
return {}
[docs]
class Variable(Elementary):
"""Explanatory variable
This represents the explanatory variables of the choice
model. Typically, they come from the data set.
"""
[docs]
def __init__(self, name):
"""Constructor
:param name: name of the variable.
:type name: string
"""
Elementary.__init__(self, name)
# Index of the variable
self.variableId = None
[docs]
def check_panel_trajectory(self):
"""List of variables defined outside of 'PanelLikelihoodTrajectory'
:return: List of names of variables
:rtype: list(str)
"""
return {self.name}
[docs]
def setIdManager(self, id_manager=None):
"""The ID manager contains the IDs of the elementary expressions.
It is externally created, as it may need to coordinate the
numbering of several expressions. It is stored only in the
expressions of type Elementary.
:param id_manager: ID manager to be propagated to the
elementary expressions. If None, all the IDs are set to None.
:type id_manager: class IdManager
"""
self.id_manager = id_manager
if id_manager is None:
self.elementaryIndex = None
self.variableId = None
return
self.elementaryIndex = self.id_manager.elementary_expressions.indices[self.name]
self.variableId = self.id_manager.variables.indices[self.name]
[docs]
def dict_of_elementary_expression(self, the_type):
"""Extract a dict with all elementary expressions of a specific type
:param the_type: the type of expression
:type the_type: TypeOfElementaryExpression
"""
if the_type == TypeOfElementaryExpression.VARIABLE:
return {self.name: self}
return {}
[docs]
def audit(self, database=None):
"""Performs various checks on the expressions.
:param database: database object
:type database: biogeme.database.Database
:return: tuple list_of_errors, list_of_warnings
:rtype: list(string), list(string)
:raise BiogemeError: if no database is provided.
:raise BiogemeError: if the name of the variable does not appear
in the database.
"""
list_of_errors = []
list_of_warnings = []
if database is None:
raise excep.BiogemeError(
'The database must be provided to audit the variable.'
)
if self.name not in database.data.columns:
the_error = f'Variable {self.name} not found in the database.'
list_of_errors.append(the_error)
return list_of_errors, list_of_warnings
[docs]
def getSignature(self):
"""The signature of a string characterizing an expression.
This is designed to be communicated to C++, so that the
expression can be reconstructed in this environment.
The list contains the following elements:
1. the name of the expression between < >
2. the id of the expression between { }
3. the name of the variable,
4. the unique ID, preceeded by a comma.
5. the variabvle ID, preceeded by a comma.
Consider the following expression:
.. math:: 2 \\beta_1 V_1 -
\\frac{\\exp(-\\beta_2 V_2) }{ \\beta_3 (\\beta_2 \\geq \\beta_1)}.
It is defined as::
2 * beta1 * Variable1 - expressions.exp(-beta2*Variable2) /
(beta3 * (beta2 >= beta1))
And its signature is::
[b'<Numeric>{4780527008},2',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<Times>{4780526952}(2),4780527008,4780277152',
b'<Variable>{4511837152}"Variable1",5,2',
b'<Times>{4780527064}(2),4780526952,4511837152',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<UnaryMinus>{4780527120}(1),4780277656',
b'<Variable>{4511837712}"Variable2",6,3',
b'<Times>{4780527176}(2),4780527120,4511837712',
b'<exp>{4780527232}(1),4780527176',
b'<Beta>{4780277264}"beta3"[1],2,0',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<GreaterOrEqual>{4780527288}(2),4780277656,4780277152',
b'<Times>{4780527344}(2),4780277264,4780527288',
b'<Divide>{4780527400}(2),4780527232,4780527344',
b'<Minus>{4780527456}(2),4780527064,4780527400']
:return: list of the signatures of an expression and its children.
:rtype: list(string)
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
elementary expression
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
variable
"""
if self.elementaryIndex is None:
error_msg = (
f'No id has been defined for elementary expression ' f'{self.name}.'
)
raise excep.BiogemeError(error_msg)
if self.variableId is None:
error_msg = f'No id has been defined for variable {self.name}.'
raise excep.BiogemeError(error_msg)
signature = f'<{self.getClassName()}>'
signature += f'{{{self.get_id()}}}'
signature += f'"{self.name}",{self.elementaryIndex},{self.variableId}'
return [signature.encode()]
[docs]
class DefineVariable(Variable):
"""Expression that defines a new variable and add a column in the database.
This expression allows the use to define a new variable that
will be added to the database. It avoids that it is
recalculated each time it is needed.
"""
[docs]
def __init__(self, name, expression, database):
"""Constructor
:param name: name of the variable.
:type name: string
:param expression: formula that defines the variable
:type expression: biogeme.expressions.Expression
:param database: object identifying the database.
:type database: biogeme.database.Database
:raise BiogemeError: if the expression is invalid, that is
neither a numeric value or a
biogeme.expressions.Expression object.
"""
raise excep.BiogemeError(
'This expression is obsolete. Use the same function in the '
'database object. Replace "new_var = DefineVariable(\'NEW_VAR\','
' expression, database)" by "new_var = database.DefineVariable'
'(\'NEW_VAR\', expression)"'
)
[docs]
class RandomVariable(Elementary):
"""
Random variable for numerical integration
"""
[docs]
def __init__(self, name):
"""Constructor
:param name: name of the random variable involved in the integration.
:type name: string.
"""
Elementary.__init__(self, name)
# Index of the random variable
self.rvId = None
[docs]
def check_rv(self):
"""List of random variables defined outside of 'Integrate'
:return: List of names of variables
:rtype: list(str)
"""
return {self.name}
[docs]
def setIdManager(self, id_manager=None):
"""The ID manager contains the IDs of the elementary expressions.
It is externally created, as it may nee to coordinate the
numbering of several expressions. It is stored only in the
expressions of type Elementary.
:param id_manager: ID manager to be propagated to the
elementary expressions. If None, all the IDs are set to None.
:type id_manager: class IdManager
"""
self.id_manager = id_manager
if id_manager is None:
self.elementaryIndex = None
self.rvId = None
return
self.elementaryIndex = self.id_manager.elementary_expressions.indices[self.name]
self.rvId = self.id_manager.random_variables.indices[self.name]
[docs]
def dict_of_elementary_expression(self, the_type):
"""Extract a dict with all elementary expressions of a specific type
:param the_type: the type of expression
:type the_type: TypeOfElementaryExpression
"""
if the_type == TypeOfElementaryExpression.RANDOM_VARIABLE:
return {self.name: self}
return {}
[docs]
def getSignature(self):
"""The signature of a string characterizing an expression.
This is designed to be communicated to C++, so that the
expression can be reconstructed in this environment.
The list contains the following elements:
1. the name of the expression between < >
2. the id of the expression between { }
3. the name of the random variable,
4. the unique ID, preceeded by a comma,
5. the ID of the random variable.
Consider the following expression:
.. math:: 2 \\beta_1 V_1 -
\\frac{\\exp(-\\beta_2 V_2) }{ \\beta_3 (\\beta_2 \\geq \\beta_1)}.
It is defined as::
2 * beta1 * Variable1 - expressions.exp(-beta2*Variable2) /
(beta3 * (beta2 >= beta1))
And its signature is::
[b'<Numeric>{4780527008},2',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<Times>{4780526952}(2),4780527008,4780277152',
b'<Variable>{4511837152}"Variable1",5,2',
b'<Times>{4780527064}(2),4780526952,4511837152',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<UnaryMinus>{4780527120}(1),4780277656',
b'<Variable>{4511837712}"Variable2",6,3',
b'<Times>{4780527176}(2),4780527120,4511837712',
b'<exp>{4780527232}(1),4780527176',
b'<Beta>{4780277264}"beta3"[1],2,0',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<GreaterOrEqual>{4780527288}(2),4780277656,4780277152',
b'<Times>{4780527344}(2),4780277264,4780527288',
b'<Divide>{4780527400}(2),4780527232,4780527344',
b'<Minus>{4780527456}(2),4780527064,4780527400']
:return: list of the signatures of an expression and its children.
:rtype: list(string)
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
elementary expression
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
random variable
"""
if self.elementaryIndex is None:
error_msg = (
f'No id has been defined for elementary ' f'expression {self.name}.'
)
raise excep.BiogemeError(error_msg)
if self.rvId is None:
error_msg = f'No id has been defined for random variable {self.name}.'
raise excep.BiogemeError(error_msg)
signature = f'<{self.getClassName()}>'
signature += f'{{{self.get_id()}}}'
signature += f'"{self.name}",{self.elementaryIndex},{self.rvId}'
return [signature.encode()]
[docs]
class Beta(Elementary):
"""
Unknown parameters to be estimated from data.
"""
[docs]
def __init__(self, name, value, lowerbound, upperbound, status):
"""Constructor
:param name: name of the parameter.
:type name: string
:param value: default value.
:type value: float
:param lowerbound: if different from None, imposes a lower
bound on the value of the parameter during the optimization.
:type lowerbound: float
:param upperbound: if different from None, imposes an upper
bound on the value of the parameter during the optimization.
:type upperbound: float
:param status: if different from 0, the parameter is fixed to
its default value, and not modified by the optimization algorithm.
:type status: int
:raise BiogemeError: if the first parameter is not a str.
:raise BiogemeError: if the second parameter is not a int or a float.
"""
if not isinstance(value, (int, float)):
error_msg = (
f'The second parameter for {name} must be '
f'a float and not a {type(value)}: {value}'
)
raise excep.BiogemeError(error_msg)
if not isinstance(name, str):
error_msg = (
f'The first parameter must be a string and '
f'not a {type(name)}: {name}'
)
raise excep.BiogemeError(error_msg)
Elementary.__init__(self, name)
the_value = validate(value, modify=False)
self.initValue = the_value
self.estimated_value = None
if lowerbound is not None:
the_lowerbound = validate(lowerbound, modify=False)
self.lb = the_lowerbound
else:
self.lb = -MAX_VALUE
if upperbound is not None:
the_upperbound = validate(upperbound, modify=False)
self.ub = the_upperbound
else:
self.ub = MAX_VALUE
self.status = status
self.betaId = None
[docs]
def setIdManager(self, id_manager=None):
"""The ID manager contains the IDs of the elementary expressions.
It is externally created, as it may need to coordinate the
numbering of several expressions. It is stored only in the
expressions of type Elementary.
:param id_manager: ID manager to be propagated to the
elementary expressions. If None, all the IDs are set to None.
:type id_manager: class IdManager
"""
self.id_manager = id_manager
if id_manager is None:
self.elementaryIndex = None
self.betaId = None
return
self.elementaryIndex = self.id_manager.elementary_expressions.indices[self.name]
if self.status != 0:
self.betaId = self.id_manager.fixed_betas.indices[self.name]
else:
self.betaId = self.id_manager.free_betas.indices[self.name]
def __str__(self):
return (
f"Beta('{self.name}', {self.initValue}, {self.lb}, "
f"{self.ub}, {self.status})"
)
[docs]
def fix_betas(self, beta_values, prefix=None, suffix=None):
"""Fix all the values of the beta parameters appearing in the
dictionary
:param beta_values: dictionary containing the betas to be
fixed (as key) and their value.
:type beta_values: dict(str: float)
:param prefix: if not None, the parameter is renamed, with a
prefix defined by this argument.
:type prefix: str
:param suffix: if not None, the parameter is renamed, with a
suffix defined by this argument.
:type suffix: str
"""
if self.name in beta_values:
self.initValue = beta_values[self.name]
self.status = 1
if prefix is not None:
self.name = f'{prefix}{self.name}'
if suffix is not None:
self.name = f'{self.name}{suffix}'
[docs]
def dict_of_elementary_expression(self, the_type):
"""Extract a dict with all elementary expressions of a specific type
:param the_type: the type of expression
:type the_type: TypeOfElementaryExpression
:return: returns a dict with the variables appearing in the
expression the keys being their names.
:rtype: dict(string:biogeme.expressions.Expression)
"""
if the_type == TypeOfElementaryExpression.BETA:
return {self.name: self}
if the_type == TypeOfElementaryExpression.FREE_BETA and self.status == 0:
return {self.name: self}
if the_type == TypeOfElementaryExpression.FIXED_BETA and self.status != 0:
return {self.name: self}
return {}
[docs]
def getValue(self):
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
:raise BiogemeError: if the Beta is not fixed.
"""
if self.status == 0:
if self.estimated_value is None:
error_msg = f'Parameter {self.name} must be estimated from data.'
raise excep.BiogemeError(error_msg)
return self.estimated_value
return self.initValue
[docs]
def change_init_values(self, betas):
"""Modifies the initial values of the Beta parameters.
The fact that the parameters are fixed or free is irrelevant here.
:param betas: dictionary where the keys are the names of the
parameters, and the values are the new value for
the parameters.
:type betas: dict(string:float)
"""
if self.name in betas:
self.initValue = betas[self.name]
def set_estimated_values(self, betas: dict[str, float]):
"""Set the estimated values of beta
:param betas: dictionary where the keys are the names of the
parameters, and the values are the new value for
the parameters.
"""
if self.name in betas:
self.estimated_value = betas[self.name]
[docs]
def getSignature(self):
"""The signature of a string characterizing an expression.
This is designed to be communicated to C++, so that the
expression can be reconstructed in this environment.
The list contains the following elements:
1. the name of the expression between < >
2. the id of the expression between { }
3. the name of the parameter,
4. the status between [ ]
5. the unique ID, preceeded by a comma
6. the beta ID, preceeded by a comma
Consider the following expression:
.. math:: 2 \\beta_1 V_1 -
\\frac{\\exp(-\\beta_2 V_2) }{ \\beta_3 (\\beta_2 \\geq \\beta_1)}.
It is defined as::
2 * beta1 * Variable1 - expressions.exp(-beta2*Variable2) /
(beta3 * (beta2 >= beta1))
And its signature is::
[b'<Numeric>{4780527008},2',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<Times>{4780526952}(2),4780527008,4780277152',
b'<Variable>{4511837152}"Variable1",5,2',
b'<Times>{4780527064}(2),4780526952,4511837152',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<UnaryMinus>{4780527120}(1),4780277656',
b'<Variable>{4511837712}"Variable2",6,3',
b'<Times>{4780527176}(2),4780527120,4511837712',
b'<exp>{4780527232}(1),4780527176',
b'<Beta>{4780277264}"beta3"[1],2,0',
b'<Beta>{4780277656}"beta2"[0],1,1',
b'<Beta>{4780277152}"beta1"[0],0,0',
b'<GreaterOrEqual>{4780527288}(2),4780277656,4780277152',
b'<Times>{4780527344}(2),4780277264,4780527288',
b'<Divide>{4780527400}(2),4780527232,4780527344',
b'<Minus>{4780527456}(2),4780527064,4780527400']
:return: list of the signatures of an expression and its children.
:rtype: list(string)
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
elementary expression
:raise biogeme.exceptions.BiogemeError: if no id has been defined for
parameter
"""
if self.elementaryIndex is None:
error_msg = (
f'No id has been defined for elementary ' f'expression {self.name}.'
)
raise excep.BiogemeError(error_msg)
if self.betaId is None:
raise excep.BiogemeError(
f'No id has been defined for parameter {self.name}.'
)
signature = f'<{self.getClassName()}>'
signature += f'{{{self.get_id()}}}'
signature += (
f'"{self.name}"[{self.status}],' f'{self.elementaryIndex},{self.betaId}'
)
return [signature.encode()]