Source code for causaloop.core.variable.base

"""
Base variable abstraction for the CausaLoop system.

This module provides the foundational BaseVariable class and related
abstractions that form the core of the variable system. BaseVariable
represents the minimal variable abstraction without causal features,
focusing on domain validation and basic value management.
"""

from __future__ import annotations
from typing import Optional, Any
from .types import (
    VariableType,
    Domain,
    Interval,
    Categorical,
    Boolean
)


[docs] class BaseVariable(): """ Minimal variable abstraction without causal features. BaseVariable provides the foundation for all variables in the CausaLoop system. It encapsulates a named value within a defined domain, with automatic validation and type inference. This class does not include causal tracking, multi-source resolution, or temporal logic - those features are added in subclasses. Parameters ---------- name : str Unique identifier for the variable within its context. Should be descriptive and follow naming conventions of the domain. domain : Domain The domain defining valid values for this variable. Determines validation rules and variable type. value : Any, optional Initial value for the variable. Must be valid according to the domain's validation rules. If None, variable starts uninitialized. units : str, optional Physical or logical units for the variable's value. Used for dimensional analysis and unit conversion. Examples: "mg/dL", "seconds", "USD", "count". Attributes ---------- name : str Variable identifier. domain : Domain Domain defining valid values. units : Optional[str] Measurement units, if applicable. _value : Optional[Any] Internal storage for the current value. Raises ------ ValueError If the provided initial value fails domain validation. Examples -------- >>> from causaloop import BaseVariable, Interval >>> # Valid construction >>> temp = BaseVariable("temperature", Interval(-273.15, 1000), 25.0, "°C") >>> temp.value 25.0 >>> # Invalid construction - value outside domain >>> try: ... var = BaseVariable("var", Interval(0, 1), 1.1, "g/mol") ... except ValueError as e: ... print(e) Value 1.1 invalid for domain Interval(0, 1) >>> # Construction with None value >>> uninitialized = BaseVariable("pressure", Interval(0, 100)) >>> uninitialized.value is None True See Also -------- Variable : Extended variable with multi-source resolution. CausalVariable : Variable with causal tracking and temporal logic. Domain : Base class for value domains. """ def __init__( self, name: str, domain: Domain, value: Optional[Any] = None, units: Optional[str] = None, ): """ Initialize a BaseVariable with name, domain, and optional value/units. Parameters ---------- name : str Unique identifier for the variable. domain : Domain Domain object defining valid values. value : Any, optional Initial value. Must pass domain validation if not None. units : str, optional Measurement units for the variable's value. Raises ------ ValueError If value is not None and fails domain validation. Examples -------- >>> from causaloop import BaseVariable, Interval, Categorical >>> var = BaseVariable("glucose", Interval(50, 100), 65) >>> var.value 65 >>> var = BaseVariable("valid", Categorical(["yes", "no"])) >>> var.value is None True Notes ----- The constructor validates the initial value against the domain immediately. This ensures variables are never in an invalid state. To create a variable without an initial value, pass value=None. """ self.name = name self.domain = domain # Validate initial value if provided if value is not None and not domain.validate(value): raise ValueError( f"Value {value!r} invalid for domain {domain}" ) self._value = value self.units = units @property def value(self) -> Optional[Any]: """ Get the current value of the variable. Returns ------- Optional[Any] The current value, or None if the variable is uninitialized. Examples -------- >>> from causaloop import BaseVariable, Interval, Categorical >>> var = BaseVariable("count", Interval(0, 100), 42.) >>> var.value 42.0 >>> var = BaseVariable("status", Categorical(["on", "off"])) >>> var.value is not None False """ return self._value @value.setter def value(self, val: Any) -> None: """ Set the variable's value with domain validation. Parameters ---------- val : Any New value to assign to the variable. Must be valid according to the variable's domain. Raises ------ ValueError If the value fails domain validation. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("score", Interval(0, 100), 50) >>> var.value = 75 # Valid >>> var.value 75 >>> try: ... var.value = 150 # Outside domain ... except ValueError as e: ... print(e) Value 150 invalid for domain Interval(0, 100) Notes ----- The setter performs full validation using the domain's validate method. For Interval domains, this includes type conversion attempts (e.g., string "75" to float 75.0). Setting value to None is allowed and represents an uninitialized state. Use clear() method for explicit uninitialization. """ if val is not None and not self.domain.validate(val): raise ValueError( f"Value {val!r} invalid for domain {self.domain}" ) self._value = val @property def type(self) -> VariableType: """ Infer the variable's data type from its domain. Returns ------- VariableType The variable type inferred from the domain class. Notes ----- Type inference is based on the domain class hierarchy: - Interval → CONTINUOUS - Categorical → CATEGORICAL - Boolean → BOOLEAN - All others → DISCRETE This inference is used for serialization, visualization, and automatic solver selection. Examples -------- >>> from causaloop import BaseVariable, Interval, Boolean, Categorical >>> var = BaseVariable("temp", Interval(0, 100), 25) >>> var.type <VariableType.CONTINUOUS: 'continuous'> >>> var = BaseVariable("flag", Boolean(), True) >>> var.type <VariableType.BOOLEAN: 'boolean'> >>> var = BaseVariable("category", Categorical(["A", "B"]), "A") >>> var.type <VariableType.CATEGORICAL: 'categorical'> """ if isinstance(self.domain, Interval): return VariableType.CONTINUOUS if isinstance(self.domain, Categorical): return VariableType.CATEGORICAL if isinstance(self.domain, Boolean): return VariableType.BOOLEAN return VariableType.DISCRETE
[docs] def clear(self) -> None: """ Clear the variable's value, setting it to None. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("counter", Interval(0, 100), 42) >>> var.value 42 >>> var.clear() >>> var.value is None True Notes ----- Clearing a variable sets its value to None, representing an uninitialized state. This is different from setting a value that happens to be None within the domain (e.g., for optional categorical variables). Use this method when you need to reset a variable without destroying the object. """ self._value = None
[docs] def is_valid(self, value: Any) -> bool: """ Check if a value would be valid for the variable's domain. Parameters ---------- value : Any Value to check against the variable's domain. Returns ------- bool True if the value is valid for this variable's domain, False otherwise. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("age", Interval(0, 120)) >>> var.is_valid(25) True >>> var.is_valid(-5) False >>> var.is_valid("twenty") # Non-numeric string False Notes ----- This method performs the same validation as the value setter but without actually setting the value. Useful for pre-checking values before assignment. See Also -------- value : Property setter that uses the same validation. """ return value is None or self.domain.validate(value)
def __repr__(self) -> str: """ Return a string representation of the variable. Returns ------- str String representation showing class name, variable name, and current value. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("temperature", Interval(0, 100), 25.0) >>> repr(var) "BaseVariable('temperature', value=25.0)" >>> var.clear() >>> repr(var) "BaseVariable('temperature', value=None)" Notes ----- The representation is designed for debugging and logging. It shows the essential information: class, name, and value. For more detailed information including domain and units, use __str__() method (if implemented in subclasses). """ return f"{self.__class__.__name__}({self.name!r}, value={self.value})" def __str__(self) -> str: """ Return a human-readable string representation. Returns ------- str Formatted string with name, value, units, and type. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("glucose", Interval(50, 300), 95.0, "mg/dL") >>> str(var) 'glucose = 95.0 mg/dL (continuous)' """ value_str = str(self.value) if self.value is not None else "uninitialized" unit_str = f" {self.units}" if self.units else "" type_str = f" ({self.type.value})" return f"{self.name} = {value_str}{unit_str}{type_str}"
[docs] def to_dict(self) -> dict: """ Convert variable to dictionary representation. Returns ------- dict Dictionary containing variable metadata and current state. Examples -------- >>> from causaloop import BaseVariable, Interval >>> var = BaseVariable("pressure", Interval(0, 100), 85.5, "kPa") >>> var.to_dict() { 'name': 'pressure', 'type': 'continuous', 'value': 85.5, 'units': 'kPa', 'domain': 'Interval(0, 100)' } Notes ----- This representation is suitable for serialization (JSON, YAML) and persistence. The domain is represented as its string representation for compactness. """ return { "name": self.name, "type": self.type.value, "value": self.value, "units": self.units, "domain": str(self.domain) }