Source code for biogeme.expressions.multiple_expressions

"""Defines the interface for a catalog of expressions that may be
considered in a specification

Michel Bierlaire
13.04.2025 17:02
"""

import abc
import logging
from typing import Iterator, NamedTuple, final

from biogeme import exceptions
from .base_expressions import Expression

logger = logging.getLogger(__name__)

SEPARATOR = ';'
SELECTION_SEPARATOR = ':'


[docs] class NamedExpression(NamedTuple): name: str expression: Expression
[docs] class CatalogItem(NamedTuple): catalog_name: str item_index: int item_name: str
[docs] def delegate_to_selected(method_name: str): """ Create a method that delegates its call to `self.selected()[1].<method_name>(...)`. This is useful for classes that wrap or dispatch to another `Expression` object, avoiding code duplication for common method overrides. :param method_name: The name of the method to delegate. It must be a valid method name on the `Expression` returned by `self.selected()[1]`. :return: A method that can be assigned to a class and will delegate the call. """ def method(self, *args, **kwargs): _, expr = self.selected() return getattr(expr, method_name)(*args, **kwargs) method.__name__ = method_name return method
[docs] class MultipleExpression(Expression, metaclass=abc.ABCMeta): """Interface for catalog of expressions that are interchangeable. Only one of them defines the specification. They are designed to be modified algorithmically. """ def __init__(self, the_name: str): self.name = the_name # The name of the expression catalog if SEPARATOR in name or SELECTION_SEPARATOR in name: error_msg = ( f'Invalid name: {the_name}. Cannot contain characters ' f'{SELECTION_SEPARATOR} or {SELECTION_SEPARATOR}' ) raise exceptions.BiogemeError(error_msg) super().__init__()
[docs] def deep_flat_copy(self) -> Expression: """Provides a copy of the expression. It is deep in the sense that it generates copies of the children. It is flat in the sense that any `MultipleExpression` such as this one is transformed into the currently selected expression. The flat part is irrelevant for this expression. """ the_expression = self.selected_expression() return the_expression.deep_flat_copy()
[docs] @abc.abstractmethod def selected(self) -> NamedExpression: """Return the selected expression and its name :return: the name and the selected expression :rtype: tuple(str, biogeme.expressions.Expression) """
[docs] @abc.abstractmethod def get_iterator(self) -> Iterator[NamedExpression]: """Returns an iterator on NamedExpression"""
[docs] @final def catalog_size(self) -> int: """Provide the size of the catalog :return: number of expressions in the catalog :rtype: int """ the_iterator = self.get_iterator() return len(list(the_iterator))
[docs] @final def selected_name(self) -> str: """Obtain the name of the selection :return: the name of the selected expression :rtype: str """ the_name, _ = self.selected() return the_name
[docs] @final def selected_expression(self) -> NamedExpression: """Obtain the selected expression :return: the selected expression :rtype: biogeme.expressions.Expression """ _, the_expression = self.selected() return the_expression
def __str__(self) -> str: named_expression: NamedExpression = self.selected() return f'[{self.name}: {named_expression.name}]{named_expression.expression}'
# We now specify all methods that must be delegated to the selected expression delegated_methods = [ 'set_maximum_number_of_observations_per_individual', 'change_init_values', 'rename_elementary', 'get_elementary_expression', 'get_value', 'requires_draws', 'recursive_construct_jax_function', 'logit_choice_avail', 'add_suffix_to_all_variables', 'set_specific_id', ] for name in delegated_methods: setattr(MultipleExpression, name, final(delegate_to_selected(name)))