Source code for data.OXData

"""
OXData Module
=============

This module provides the base data object class for the OptiX optimization framework.
It implements scenario-based data management capabilities that allow optimization
problems to handle multiple data variants (e.g., optimistic, pessimistic, realistic
scenarios) using the same model structure.

The module is designed to enable sensitivity analysis and what-if scenario modeling
by maintaining multiple attribute value sets while preserving the object's core
structure and relationships.

Key Features:
    - **Scenario Management**: Support for multiple named scenarios with different attribute values
    - **Dynamic Attribute Access**: Automatic scenario-aware attribute resolution
    - **Type Safety**: Built on dataclasses with proper type annotations
    - **Base Integration**: Extends OXObject for UUID-based identity and framework integration

Architecture:
    The OXData class uses Python's ``__getattribute__`` method to implement transparent
    scenario switching. When an attribute is accessed, the system first checks the active
    scenario for that attribute, falling back to the object's base attributes if not found.

Example:
    Basic usage of OXData with multiple scenarios:

    .. code-block:: python

        from data.OXData import OXData
        
        # Create a data object with base values
        demand_data = OXData()
        demand_data.quantity = 100
        demand_data.cost = 50.0
        
        # Create scenarios for sensitivity analysis
        demand_data.create_scenario("High_Demand", quantity=150, cost=55.0)
        demand_data.create_scenario("Low_Demand", quantity=75, cost=45.0)
        
        # Switch between scenarios
        print(demand_data.quantity)  # 100 (Default scenario)
        
        demand_data.active_scenario = "High_Demand"
        print(demand_data.quantity)  # 150
        
        demand_data.active_scenario = "Low_Demand"
        print(demand_data.quantity)  # 75

Module Dependencies:
    - dataclasses: For structured data object definitions
    - typing: For type annotations and generics
    - base: For OXObject base class and exception handling

Notes:
    - Scenario names are case-sensitive and should follow consistent naming conventions
    - The "Default" scenario is automatically created when the first custom scenario is added
    - Certain fields (id, class_name, active_scenario, scenarios) are excluded from scenario management
"""

from dataclasses import dataclass, field, fields
from typing import Any

from base import OXObject, OXception

#: List of field names that are excluded from scenario management to prevent infinite loops
#: and maintain object integrity. These fields are always accessed from the base object.
NON_SCENARIO_FIELDS = ["active_scenario", "scenarios", "id", "class_name"]


[docs] @dataclass class OXData(OXObject): """A base class for data objects with scenario support. This class provides a mechanism for storing different attribute values for different scenarios. When an attribute is accessed, the system first checks if it exists in the active scenario, and if not, falls back to the object's own attribute. Attributes: active_scenario (str): The name of the currently active scenario. Defaults to "Default". scenarios (dict[str, dict[str, Any]]): A dictionary mapping scenario names to dictionaries of attribute values. Examples: >>> data = OXData() >>> data.value = 10 >>> data.create_scenario("Optimistic", value=20) >>> data.create_scenario("Pessimistic", value=5) >>> print(data.value) # Default scenario 10 >>> data.active_scenario = "Optimistic" >>> print(data.value) # Optimistic scenario 20 >>> data.active_scenario = "Pessimistic" >>> print(data.value) # Pessimistic scenario 5 """ active_scenario: str = "Default" scenarios: dict[str, dict[str, Any]] = field(default_factory=dict)
[docs] def __getattribute__(self, item): """Custom attribute access that checks the active scenario first. When an attribute is accessed, this method first checks if it exists in the active scenario, and if not, falls back to the object's own attribute. Args: item (str): The name of the attribute to access. Returns: Any: The value of the attribute in the active scenario, or the object's own attribute if not found in the active scenario. """ if item in NON_SCENARIO_FIELDS: # Prevent Infinite Loop! return super().__getattribute__(item) active_scenario_values = self.scenarios.get(self.active_scenario, {}) if len(active_scenario_values) > 0: if item in active_scenario_values: return active_scenario_values[item] return super().__getattribute__(item)
[docs] def create_scenario(self, scenario_name: str, **kwargs): """Create a new scenario with the specified attribute values. If the "Default" scenario doesn't exist yet, it is created first, capturing the object's current attribute values. Args: scenario_name (str): The name of the new scenario. **kwargs: Attribute-value pairs for the new scenario. Raises: OXception: If an attribute in kwargs doesn't exist in the object. Examples: >>> data = OXData() >>> data.value = 10 >>> data.create_scenario("Optimistic", value=20) >>> data.active_scenario = "Optimistic" >>> print(data.value) 20 """ if 'Default' not in self.scenarios: self.scenarios['Default'] = {} obj_fields = fields(self) for field in obj_fields: if field.name not in NON_SCENARIO_FIELDS: self.scenarios['Default'][field.name] = getattr(self, field.name) self.scenarios[scenario_name] = {} for key, value in kwargs.items(): if key not in NON_SCENARIO_FIELDS: if hasattr(self, key): self.scenarios[scenario_name][key] = value else: raise OXception(f"Object {self} has no attribute {key}")