Source code for biogeme.catalog.catalog
"""Defines a catalog of expressions that may be considered in a specification
Michel Bierlaire
Wed Apr 16 18:35:02 2025
"""
from __future__ import annotations
import logging
from typing import Iterator
from biogeme.exceptions import BiogemeError
from biogeme.expressions import (
Expression,
MultipleExpression,
NamedExpression,
validate_and_convert,
)
from .controller import Controller
logger = logging.getLogger(__name__)
[docs]
class Catalog(MultipleExpression):
"""Catalog of expressions that are interchangeable. Only one of
them defines the specification. They are designed to be
modified algorithmically by a controller.
"""
def __init__(
self,
catalog_name: str,
named_expressions: list[NamedExpression],
controlled_by: Controller | None = None,
):
"""Ctor
:param catalog_name: name of the catalog of expressions
:param named_expressions: list of NamedExpression,
each containing a name and an expression.
:param controlled_by: Object controlling the selection of the specifications.
:raise BiogemeError: if list_of_named_expressions is empty
:raise BiogemeError: if incompatible Controller
"""
super().__init__(catalog_name)
if not named_expressions:
raise BiogemeError(
f'{catalog_name}: cannot create a catalog from an empty list.'
)
if controlled_by and not isinstance(controlled_by, Controller):
error_msg = (
f'The controller must be of type Controller and not '
f'{type(controlled_by)}'
)
raise BiogemeError(error_msg)
self.named_expressions = [
NamedExpression(
name=named.name,
expression=validate_and_convert(named.expression),
)
for named in named_expressions
]
# Declare the expressions as children of the catalog
for named_expression in self.named_expressions:
self.children.append(named_expression.expression)
names = [named_expr.name for named_expr in self.named_expressions]
if controlled_by is None:
controller_name = catalog_name
self.controlled_by = Controller(
controller_name=controller_name, specification_names=names
)
else:
self.controlled_by = controlled_by
controller_names = list(controlled_by.specification_names)
if names != controller_names:
error_msg = (
f'Incompatible IDs between catalog [{names}] and controller '
f'[{controller_names}]'
)
raise BiogemeError(error_msg)
[docs]
def get_all_controllers(self) -> set[Controller]:
"""Provides all controllers controlling the specifications of
a multiple expression
:return: a set of controllers
:rtype: set(biogeme.controller.Controller)
"""
all_controllers = {self.controlled_by}
for e in self.children:
all_controllers |= e.get_all_controllers()
return all_controllers
[docs]
@classmethod
def from_dict(
cls,
catalog_name: str,
dict_of_expressions: dict[str, Expression],
controlled_by: Controller | None = None,
) -> Catalog:
"""Ctor using a dict instead of a list.
Python used not to guarantee the order of elements of a dict,
although, in practice, it is always preserved. If the order is
critical, it is better to use the main constructor. If not,
this constructor provides a more readable code.
:param catalog_name: name of the catalog
:param dict_of_expressions: dict associating the name of an
expression and the expression itself.
:param controlled_by: Object controlling the selection of the specifications.
"""
named_expressions = [
NamedExpression(name=name, expression=expression)
for name, expression in dict_of_expressions.items()
]
return cls(
catalog_name=catalog_name,
named_expressions=named_expressions,
controlled_by=controlled_by,
)
def __iter__(self) -> Iterator[NamedExpression]:
"""Obtain an iterator on the named expressions"""
return self.get_iterator()
[docs]
def get_iterator(self) -> Iterator[NamedExpression]:
"""Obtain an iterator on the named expressions"""
return iter(self.named_expressions)
[docs]
def selected(self) -> NamedExpression:
"""Return the selected expression and its name
:return: the name and the selected expression
:rtype: NamedExpression
"""
return self.named_expressions[self.controlled_by.current_index]