Source code for biogeme.expressions.unary_expressions

""" Arithmetic expressions accepted by Biogeme: unary operators

:author: Michel Bierlaire
:date: Sat Sep  9 15:51:53 2023
"""
import logging
import numpy as np
import biogeme.exceptions as excep
from .base_expressions import Expression
from .numeric_tools import is_numeric

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. """
[docs] def __init__(self, child): """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 or a biogeme.expressions.Expression object. """ Expression.__init__(self) if is_numeric(child): from .numeric_expressions import Numeric self.child = Numeric(child) #: child else: if not isinstance(child, Expression): raise excep.BiogemeError(f'This is not a valid expression: {child}') self.child = child self.children.append(self.child)
[docs] class UnaryMinus(UnaryOperator): """ Unary minus expression """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'(-{self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ return -self.child.getValue()
[docs] class MonteCarlo(UnaryOperator): """ Monte Carlo integration """
[docs] def __init__(self, child): """Constructor :param child: arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'MonteCarlo({self.child})'
[docs] def check_draws(self): """List of draws defined outside of 'MonteCarlo' :return: List of names of variables :rtype: list(str) """ return set()
[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) """ list_of_errors, list_of_warnings = self.child.audit(database) if database is None: if self.child.embedExpression('PanelLikelihoodTrajectory'): theWarning = ( 'The formula contains a PanelLikelihoodTrajectory ' 'expression, and no database is given' ) list_of_warnings.append(theWarning) else: if database.isPanel() and not self.child.embedExpression( '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.embedExpression('bioDraws'): the_error = ( f'The argument of MonteCarlo must contain a' f' bioDraws: {self}' ) list_of_errors.append(the_error) if self.child.embedExpression('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 """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'bioNormalCdf({self.child})'
[docs] class PanelLikelihoodTrajectory(UnaryOperator): """ Likelihood of a sequences of observations for the same individual """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'PanelLikelihoodTrajectory({self.child})'
[docs] def check_panel_trajectory(self): """List of variables defined outside of 'PanelLikelihoodTrajectory' :return: List of names of variables :rtype: list(str) """ return set()
[docs] def countPanelTrajectoryExpressions(self): """Count the number of times the PanelLikelihoodTrajectory is used in the formula. """ return 1 + self.child.countPanelTrajectoryExpressions()
[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) """ list_of_errors, list_of_warnings = self.child.audit(database) if not database.isPanel(): 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 """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'exp({self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ return np.exp(self.child.getValue())
[docs] class sin(UnaryOperator): """ sine expression """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'sin({self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ return np.sin(self.child.getValue())
[docs] class cos(UnaryOperator): """ cosine expression """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'cos({self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ return np.cos(self.child.getValue())
[docs] class log(UnaryOperator): """ logarithm expression """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'log({self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ return np.log(self.child.getValue())
[docs] class logzero(UnaryOperator): """ logarithm expression. Returns zero if the argument is zero. """
[docs] def __init__(self, child): """Constructor :param child: first arithmetic expression :type child: biogeme.expressions.Expression """ UnaryOperator.__init__(self, child)
def __str__(self): return f'logzero({self.child})'
[docs] def getValue(self): """Evaluates the value of the expression :return: value of the expression :rtype: float """ v = self.child.getValue() return 0 if v == 0 else np.log(v)
[docs] class Derive(UnaryOperator): """ Derivative with respect to an elementary expression """
[docs] def __init__(self, child, name): """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 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 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, 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) """ elementaryIndex = self.id_manager.elementary_expressions.indices[ self.elementaryName ] list_of_signatures = [] list_of_signatures += self.child.getSignature() mysignature = f'<{self.getClassName()}>' mysignature += f'{{{self.get_id()}}}' mysignature += f',{self.child.get_id()}' mysignature += f',{elementaryIndex}' list_of_signatures += [mysignature.encode()] return list_of_signatures
def __str__(self): return 'Derive({self.child}, "{self.elementName}")'
[docs] class Integrate(UnaryOperator): """ Numerical integration """
[docs] def __init__(self, child, name): """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): """List of random variables defined outside of 'Integrate' :return: List of names of variables :rtype: list(str) """ return set()
[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) """ list_of_errors, list_of_warnings = self.child.audit(database) if not self.child.embedExpression('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 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 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) """ randomVariableIndex = self.id_manager.random_variables.indices[ self.randomVariableName ] list_of_signatures = [] list_of_signatures += self.child.getSignature() mysignature = f'<{self.getClassName()}>' mysignature += f'{{{self.get_id()}}}' mysignature += f',{self.child.get_id()}' mysignature += f',{randomVariableIndex}' list_of_signatures += [mysignature.encode()] return list_of_signatures
def __str__(self): return f'Integrate({self.child}, "{self.randomVariableName}")'
[docs] class BelongsTo(UnaryOperator): """ Check if a value belongs to a set """
[docs] def __init__(self, child, the_set): """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=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) """ 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 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 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) """ list_of_signatures = [] list_of_signatures += self.child.getSignature() signature = f'<{self.getClassName()}>' 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): return f'BelongsTo({self.child}, "{self.the_set}")'