"""Object containing the output of numerical expression calculation"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Sequence, TypeVar
import numpy as np
GenericType = TypeVar('GenericType')
[docs]
def convert_to_dict(
the_sequence: Sequence[GenericType], the_map: dict[str, int]
) -> dict[str, GenericType]:
"""Convert an indexed sequence into a dict mapping a name to the elements.
:param the_sequence: sequence of element to convert
:param the_map: dict mapping the names with their index in the sequence.
:return: the sequence converted into dict
"""
# Check for any index out of range errors before creating the dictionary
if any(index >= len(the_sequence) or index < 0 for index in the_map.values()):
raise IndexError('One or more indices are out of the acceptable range.')
# Use dictionary comprehension to create the result
result = {name: the_sequence[index] for name, index in the_map.items()}
return result
[docs]
@dataclass
class FunctionOutput:
"""Output of a function calculation"""
function: float
gradient: np.ndarray | None = None
hessian: np.ndarray | None = None
[docs]
@dataclass
class BiogemeFunctionOutput(FunctionOutput):
"""Output of a function calculation"""
bhhh: np.ndarray | None = None
[docs]
class SmartOutputProxy:
"""Backward compatibility allowing to unpack the results into a tuple"""
def __init__(self, data):
self.data = data
self._iterated = False
def __getattr__(self, name):
# Allow direct attribute access to the underlying data object
return getattr(self.data, name)
[docs]
class BiogemeFunctionOutputSmartOutputProxy(SmartOutputProxy):
def __iter__(self):
# Only allow iteration to happen once, mimicking tuple unpacking behavior
warning_msg = (
f'Deprecated usage. The function returns an object of type '
f'BiogemeFunctionOutput, not a tuple anymore.'
)
if self._iterated:
raise TypeError('Multiple unpacking not allowed.')
self._iterated = True
# Define the order of attributes for unpacking
yield self.data.function
yield self.data.gradient
yield self.data.hessian
yield self.data.bhhh
[docs]
@dataclass
class BiogemeDisaggregateFunctionOutput:
"""Output of a function calculation"""
functions: np.ndarray
gradients: np.ndarray | None = None
hessians: np.ndarray | None = None
bhhhs: np.ndarray | None = None
def __len__(self):
return len(self.functions)
[docs]
def unique_entry(self) -> BiogemeFunctionOutput | None:
"""When there is only one entry, we generate the BiogemeFunctionOutput object"""
if len(self) == 1:
return BiogemeFunctionOutput(
function=float(self.functions[0]),
gradient=self.gradients[0] if self.gradients else None,
hessian=self.hessians[0] if self.hessians else None,
bhhh=self.bhhhs[0] if self.bhhhs else None,
)
return None
[docs]
class BiogemeDisaggregateFunctionOutputSmartOutputProxy(SmartOutputProxy):
def __len__(self):
return len(self.data)
def __iter__(self):
# Only allow iteration to happen once, mimicking tuple unpacking behavior
warning_msg = (
f'Deprecated usage. The function returns an object of type '
f'BiogemeDisaggregateFunctionOutput, not a tuple anymore.'
)
if self._iterated:
raise TypeError('Multiple unpacking not allowed.')
self._iterated = True
# Define the order of attributes for unpacking
yield self.data.functions
yield self.data.gradients
yield self.data.hessians
yield self.data.bhhhs
[docs]
class NamedFunctionOutput:
"""Output of a function calculation, with names of variables"""
def __init__(
self, function_output: FunctionOutput, mapping: dict[str, int]
) -> None:
"""
Constructor
:param function_output: function output stored as numpy parray.
:param mapping: dict mapping the names with their index in the sequence.
"""
self._mapping = mapping
self.function_output = function_output
self.function: float = function_output.function
self.gradient: dict[str, float] | None = (
None
if function_output.gradient is None
else convert_to_dict(function_output.gradient, mapping)
)
self.hessian: dict[str, dict[str, float]] | None = (
None
if function_output.hessian is None
else convert_to_dict(
[convert_to_dict(row, mapping) for row in function_output.hessian],
mapping,
)
)
@property
def mapping(self):
return self._mapping
def __repr__(self):
""""""
...
[docs]
class NamedBiogemeFunctionOutput(NamedFunctionOutput):
"""Output of a function calculation, with names of variables"""
def __init__(
self, function_output: BiogemeFunctionOutput, mapping: dict[str, int]
) -> None:
super().__init__(function_output=function_output, mapping=mapping)
self.bhhh: dict[str, dict[str, float]] | None = (
None
if function_output.bhhh is None
else (
convert_to_dict(
[convert_to_dict(row, mapping) for row in function_output.bhhh],
mapping,
)
)
)
[docs]
@dataclass
class NamedBiogemeDisaggregateFunctionOutput:
"""Output of a function calculation"""
def __init__(
self,
function_output: BiogemeDisaggregateFunctionOutput,
mapping: dict[str, int],
) -> None:
self.function_output = function_output
self.mapping = mapping
self.functions: list[float] = [value for value in function_output.functions]
self.gradients: list[dict[str, float]] | None = (
None
if function_output.gradients is None
else [
convert_to_dict(gradient, mapping)
for gradient in function_output.gradients
]
)
self.hessians: list[dict[str, dict[str, float]]] | None = (
None
if function_output.hessians is None
else (
[
convert_to_dict(
[convert_to_dict(row, mapping) for row in hessian],
mapping,
)
for hessian in function_output.hessians
]
)
)
self.bhhhs: list[dict[str, dict[str, float]]] | None = (
None
if function_output.bhhhs is None
else (
[
convert_to_dict(
[convert_to_dict(row, mapping) for row in bhhh],
mapping,
)
for bhhh in function_output.bhhhs
]
)
)