""" Arithmetic expressions accepted by Biogeme: unary operators
:author: Michel Bierlaire
:date: Sat Sep 9 15:51:53 2023
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import numpy as np
from . import validate_and_convert
from .base_expressions import Expression
from ..deprecated import deprecated
if TYPE_CHECKING:
from biogeme.database import Database
from . import ExpressionOrNumeric
logger = logging.getLogger(__name__)
[docs]
class UnaryOperator(Expression):
"""
Base class for arithmetic expressions that are unary operators.
Such an expression is the result of the modification of another
expressions, typically changing its sign.
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
:raise BiogemeError: if one of the expressions is invalid, that is
neither a numeric value nor a
biogeme.expressions.Expression object.
"""
Expression.__init__(self)
self.child = validate_and_convert(child)
self.children.append(self.child)
[docs]
class UnaryMinus(UnaryOperator):
"""
Unary minus expression
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'(-{self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
return -self.child.get_value()
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class MonteCarlo(UnaryOperator):
"""
Monte Carlo integration
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'MonteCarlo({self.child})'
[docs]
def check_draws(self) -> set[str]:
"""List of draws defined outside of 'MonteCarlo'
:return: List of names of variables
:rtype: list(str)
"""
return set()
[docs]
def audit(self, database: Database = None) -> tuple[list[str], list[str]]:
"""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)
"""
list_of_errors, list_of_warnings = self.child.audit(database)
if database is None:
if self.child.embed_expression('PanelLikelihoodTrajectory'):
the_warning = (
'The formula contains a PanelLikelihoodTrajectory '
'expression, and no database is given'
)
list_of_warnings.append(the_warning)
else:
if database.is_panel() and not self.child.embed_expression(
'PanelLikelihoodTrajectory'
):
the_error = (
f'As the database is panel, the argument '
f'of MonteCarlo must contain a'
f' PanelLikelihoodTrajectory: {self}'
)
list_of_errors.append(the_error)
if not self.child.embed_expression('bioDraws'):
the_error = (
f'The argument of MonteCarlo must contain a' f' bioDraws: {self}'
)
list_of_errors.append(the_error)
if self.child.embed_expression('MonteCarlo'):
the_error = (
f'It is not possible to include a MonteCarlo '
f'statement in another one: {self}'
)
list_of_errors.append(the_error)
return list_of_errors, list_of_warnings
[docs]
class bioNormalCdf(UnaryOperator):
"""
Cumulative Distribution Function of a normal random variable
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'bioNormalCdf({self.child})'
[docs]
class PanelLikelihoodTrajectory(UnaryOperator):
"""
Likelihood of a sequences of observations for the same individual
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'PanelLikelihoodTrajectory({self.child})'
[docs]
def check_panel_trajectory(self) -> set[str]:
"""List of variables defined outside of 'PanelLikelihoodTrajectory'
:return: List of names of variables
:rtype: list(str)
"""
return set()
[docs]
def count_panel_trajectory_expressions(self) -> int:
"""Count the number of times the PanelLikelihoodTrajectory
is used in the formula.
"""
return 1 + self.child.count_panel_trajectory_expressions()
[docs]
def audit(self, database: Database = None) -> tuple[list[str], list[str]]:
"""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)
"""
list_of_errors, list_of_warnings = self.child.audit(database)
if not database.is_panel():
the_error = (
f'Expression PanelLikelihoodTrajectory can '
f'only be used with panel data. Use the statement '
f'database.panel("IndividualId") to declare the '
f'panel structure of the data: {self}'
)
list_of_errors.append(the_error)
return list_of_errors, list_of_warnings
[docs]
class exp(UnaryOperator):
"""
exponential expression
"""
def __init__(self, child: ExpressionOrNumeric) -> None:
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'exp({self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
return np.exp(self.child.get_value())
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class sin(UnaryOperator):
"""
sine expression
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'sin({self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
return np.sin(self.child.get_value())
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class cos(UnaryOperator):
"""
cosine expression
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'cos({self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
return np.cos(self.child.get_value())
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class log(UnaryOperator):
"""
logarithm expression
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'log({self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
return np.log(self.child.get_value())
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class logzero(UnaryOperator):
"""
logarithm expression. Returns zero if the argument is zero.
"""
def __init__(self, child: ExpressionOrNumeric):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
def __str__(self) -> str:
return f'logzero({self.child})'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
v = self.child.get_value()
return 0 if v == 0 else np.log(v)
[docs]
@deprecated(get_value)
def getValue(self) -> float:
pass
[docs]
class PowerConstant(UnaryOperator):
"""
Raise the argument to a constant power.
"""
def __init__(self, child: ExpressionOrNumeric, exponent: float):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
self.exponent: float = exponent
self.integer_exponent: int | None = (
int(exponent) if exponent.is_integer() else None
)
def __str__(self) -> str:
return f'{self.child}**{self.exponent}'
[docs]
def get_value(self) -> float:
"""Evaluates the value of the expression
:return: value of the expression
:rtype: float
"""
v = self.child.get_value()
if v == 0:
return 0.0
if v > 0:
return v**self.exponent
if self.integer_exponent is not None:
return v**self.integer_exponent
if v < 0:
error_msg = f'Cannot calculate {v}**{self.exponent}'
raise (error_msg)
return 0 if v == 0 else np.log(v)
[docs]
def get_signature(self) -> list[bytes]:
"""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 signatures of the child expression,
2. the name of the expression between < >
3. the id of the expression between { }, preceded by a comma
4. the id of the children, preceded by a comma
5. the index of the randon variable, preceded 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)
"""
list_of_signatures = []
list_of_signatures += self.child.get_signature()
signature = f'<{self.get_class_name()}>'
signature += f'{{{self.get_id()}}}'
signature += f',{self.child.get_id()}'
signature += f',{self.exponent}'
list_of_signatures += [signature.encode()]
return list_of_signatures
[docs]
class Derive(UnaryOperator):
"""
Derivative with respect to an elementary expression
"""
def __init__(self, child: ExpressionOrNumeric, name: str):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
"""
UnaryOperator.__init__(self, child)
# Name of the elementary expression by which the derivative is taken
self.elementaryName = name
[docs]
def get_signature(self) -> list[bytes]:
"""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 signatures of the child expression,
2. the name of the expression between < >
3. the id of the expression between { }
4. the id of the child, preceded 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)
"""
elementary_index = self.id_manager.elementary_expressions.indices[
self.elementaryName
]
list_of_signatures = []
list_of_signatures += self.child.get_signature()
my_signature = f'<{self.get_class_name()}>'
my_signature += f'{{{self.get_id()}}}'
my_signature += f',{self.child.get_id()}'
my_signature += f',{elementary_index}'
list_of_signatures += [my_signature.encode()]
return list_of_signatures
def __str__(self) -> str:
return 'Derive({self.child}, "{self.elementName}")'
[docs]
class Integrate(UnaryOperator):
"""
Numerical integration
"""
def __init__(self, child: ExpressionOrNumeric, name: str):
"""Constructor
:param child: first arithmetic expression
:type child: biogeme.expressions.Expression
:param name: name of the random variable for the integration.
:type name: string
"""
UnaryOperator.__init__(self, child)
self.randomVariableName = name
[docs]
def check_rv(self) -> set[str]:
"""List of random variables defined outside of 'Integrate'
:return: List of names of variables
:rtype: list(str)
"""
return set()
[docs]
def audit(self, database: Database = None) -> tuple[list[str], list[str]]:
"""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)
"""
list_of_errors, list_of_warnings = self.child.audit(database)
if not self.child.embed_expression('RandomVariable'):
the_error = (
f'The argument of Integrate must contain a ' f'RandomVariable: {self}'
)
list_of_errors.append(the_error)
return list_of_errors, list_of_warnings
[docs]
def get_signature(self) -> list[bytes]:
"""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 signatures of the child expression,
2. the name of the expression between < >
3. the id of the expression between { }, preceeded by a comma
4. the id of the children, preceeded by a comma
5. the index of the randon variable, 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)
"""
random_variable_index = self.id_manager.random_variables.indices[
self.randomVariableName
]
list_of_signatures = []
list_of_signatures += self.child.get_signature()
my_signature = f'<{self.get_class_name()}>'
my_signature += f'{{{self.get_id()}}}'
my_signature += f',{self.child.get_id()}'
my_signature += f',{random_variable_index}'
list_of_signatures += [my_signature.encode()]
return list_of_signatures
def __str__(self) -> str:
return f'Integrate({self.child}, "{self.randomVariableName}")'
[docs]
class BelongsTo(UnaryOperator):
"""
Check if a value belongs to a set
"""
def __init__(self, child: ExpressionOrNumeric, the_set: set[float]):
"""Constructor
:param child: arithmetic expression
:type child: biogeme.expressions.Expression
:param the_set: set of values
:type the_set: set(float)
"""
UnaryOperator.__init__(self, child)
self.the_set = the_set
[docs]
def audit(self, database: Database = None) -> tuple[list[str], list[str]]:
"""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)
"""
list_of_errors, list_of_warnings = self.child.audit(database)
if not all(float(x).is_integer() for x in self.the_set):
the_warning = (
f'The set of numbers used in the expression "BelongsTo" contains '
f'numbers that are not integer. If it is the intended use, ignore '
f'this warning: {self.the_set}.'
)
list_of_warnings.append(the_warning)
return list_of_errors, list_of_warnings
[docs]
def get_signature(self) -> list[bytes]:
"""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 signatures of the child expression,
2. the name of the expression between < >
3. the id of the expression between { }, preceded by a comma
4. the id of the children, preceded by a comma
5. the index of the randon variable, preceded 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)
"""
list_of_signatures = []
list_of_signatures += self.child.get_signature()
signature = f'<{self.get_class_name()}>'
signature += f'{{{self.get_id()}}}'
signature += f'({len(self.the_set)})'
signature += f',{self.child.get_id()}'
for elem in self.the_set:
signature += f',{elem}'
list_of_signatures += [signature.encode()]
return list_of_signatures
def __str__(self) -> str:
return f'BelongsTo({self.child}, "{self.the_set}")'