# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 NeoFOAM authors
"""
Configuration Context
Central context for inter-model configuration exchange during initialization.
"""
from typing import Any, Optional
[docs]
def is_configurable_field(field_info: Any) -> bool:
"""
Check if a field is marked as Configurable.
"""
# Check if field has metadata with "configurable"
if hasattr(field_info, "metadata") and field_info.metadata:
return "configurable" in field_info.metadata
return False
[docs]
class ConfigContext:
"""
Context for inter-model configuration exchange during RESOLVE_DEPENDENCIES stage.
Models are registered by name and can be retrieved by other models
during the RESOLVE_DEPENDENCIES stage to establish dependencies and
exchange configuration.
Supports both intra-region (same region) and inter-region (cross-region)
model access via dot notation:
- "model_name" → model in current region
- "region.model_name" → model in specified region
"""
def __init__(self, current_region: str = "default"):
"""
Initialize the configuration context.
Args:
current_region: Name of the current region (default: "default")
"""
self.current_region = current_region
self._regions: dict[str, dict[str, Any]] = {current_region: {}}
def _parse_path(self, path: str) -> tuple[str, str]:
"""Parse a model path into (region, name)."""
if "." in path:
maybe_region, rest = path.split(".", 1)
if maybe_region in self._regions:
return maybe_region, rest
return self.current_region, path
[docs]
def register(self, name: str, model: Any, region: Optional[str] = None) -> None:
"""
Register a model by name in a region.
Args:
name: Unique identifier for the model within the region
model: The model instance to register
region: Region name (defaults to current_region)
"""
region = region or self.current_region
if region not in self._regions:
self._regions[region] = {}
self._regions[region][name] = model
[docs]
def get(self, path: str) -> Any:
"""
Get a registered model by path.
Supports both intra-region and inter-region access:
- "model_name" → model in current region
- "region.model_name" → model in specified region
Args:
path: Model path (name or region.name)
Returns:
The model instance, or None if not found
Example:
# Same region access
velocity = config.get("velocity")
transport = config.get("transport")
# Cross-region access
fluid_temp = config.get("fluid.temperature")
solid_temp = config.get("solid.temperature")
"""
region, name = self._parse_path(path)
return self._regions.get(region, {}).get(name)
[docs]
def all(self, region: Optional[str] = None) -> dict[str, Any]:
"""
Get all registered models in a region.
Args:
region: Region name (defaults to current_region)
Returns:
Dictionary mapping model names to model instances
"""
region = region or self.current_region
return self._regions.get(region, {}).copy()
[docs]
def contains(self, path: str) -> bool:
"""
Check if a model is registered.
Supports both intra-region and inter-region paths.
Args:
path: Model path (name or region.name)
Returns:
True if the model is registered, False otherwise
"""
region, name = self._parse_path(path)
return region in self._regions and name in self._regions[region]
[docs]
def get_by_type(self, model_type: type, region: Optional[str] = None) -> list[Any]:
"""
Get all registered models of a specific type in a region.
Useful for working with multiple instances of the same model type
(e.g., multiple heat sources, multiple porous zones).
Args:
model_type: The type/class to filter by
region: Region name (defaults to current_region)
Returns:
List of all model instances of the specified type
Example:
heat_sources = config.get_by_type(HeatSource)
for source in heat_sources:
source.enabled = False
"""
region = region or self.current_region
models = self._regions.get(region, {})
return [model for model in models.values() if isinstance(model, model_type)]
[docs]
def get_by_prefix(
self, prefix: str, region: Optional[str] = None
) -> dict[str, Any]:
"""
Get all models with names starting with a prefix in a region.
Useful for finding related model instances that follow a naming
convention (e.g., "heat_source_1", "heat_source_2").
Args:
prefix: The prefix to match against model names
region: Region name (defaults to current_region)
Returns:
Dictionary of models with matching names
Example:
sources = config.get_by_prefix("heat_source_")
for name, source in sources.items():
print(f"{name}: {source.power}W")
"""
region = region or self.current_region
models = self._regions.get(region, {})
return {
name: model for name, model in models.items() if name.startswith(prefix)
}
[docs]
def get_configurable_fields(self, path: str) -> dict[str, Any]:
"""
Get all configurable fields and their current values from a model.
Scans a model's field definitions for fields marked with
Configurable[T] type annotation and returns their current values.
Args:
path: Model path (name or region.name)
Returns:
Dictionary mapping configurable field names to their current values,
or empty dict if model not found or has no configurable fields
Example:
configurable = config.get_configurable_fields("pressure_algorithm")
# Returns: {"use_buoyancy": False, "algorithm": "SIMPLE"}
# Can check what's configurable before modifying
if "use_buoyancy" in configurable:
pressure.use_buoyancy = True
"""
model = self.get(path)
if not model:
return {}
# Check if model has Pydantic fields
if not hasattr(model.__class__, "model_fields"):
return {}
result = {}
for field_name, field_info in model.__class__.model_fields.items():
# Check if field is marked as configurable using type annotation
if is_configurable_field(field_info):
result[field_name] = getattr(model, field_name)
return result
def __getattr__(self, name: str) -> Any:
"""
Enable attribute-style access to registered models.
This allows models to be accessed as attributes:
config.algorithm # equivalent to config.get("algorithm")
Args:
name: The model name
Returns:
The model instance, or raises AttributeError if not found
"""
# Avoid infinite recursion for internal / dunder attributes
if name.startswith("_"):
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
model = self.get(name)
if model is None:
raise AttributeError(
f"No model registered with name '{name}' in region '{self.current_region}'"
)
return model
@property
def regions(self) -> list[str]:
"""
Get list of all registered region names.
Returns:
List of region names
"""
return list(self._regions.keys())