Source code for ogstools.materiallib.core.phase

# Copyright (c) 2012-2025, OpenGeoSys Community (http://www.opengeosys.org)
#            Distributed under a Modified BSD License.
#            See accompanying file LICENSE.txt or
#            http://www.opengeosys.org/project/license
#

import logging
from typing import Any

from ogstools.materiallib.schema.process_schema import PROCESS_SCHEMAS

from .component import Component
from .components import Components
from .material import Material
from .property import MaterialProperty

logger = logging.getLogger(__name__)


[docs] class Phase:
[docs] def __init__( self, phase_type: str, gas_material: Material | None = None, liquid_material: Material | None = None, solid_material: Material | None = None, process: str = "", ): self.type = phase_type self.process = process self.gas_material = gas_material self.liquid_material = liquid_material self.solid_material = solid_material schema = PROCESS_SCHEMAS.get(process) if not schema: msg = f"No process schema found for '{process}'." raise ValueError(msg) self.schema: dict[str, Any] = schema # Check for material consistency match phase_type: case "AqueousLiquid": if liquid_material is None: raise ValueError( ("AqueousLiquid requires liquid_material.",)[0] ) if solid_material is not None: raise ValueError( ("AqueousLiquid must not have solid_material.",)[0] ) case "Gas": if gas_material is None: raise ValueError(("Gas requires gas_material.",)[0]) if solid_material is not None: raise ValueError(("Gas must not have solid_material.",)[0]) case "Solid": if solid_material is None: raise ValueError(("Solid requires solid_material.",)[0]) if gas_material is not None or liquid_material is not None: raise ValueError( ( "Solid must not have gas_material or liquid_material.", )[0] ) self.properties: list[MaterialProperty] = [] self.components: list[Component] = [] if not self.schema: msg = f"No process schema found for '{process}'." raise ValueError(msg) self._load_phase_properties() if ( gas_material is not None and liquid_material is not None and any( "components" in p for p in self.schema.get("phases", []) if p.get("type") == self.type ) ): self._load_components(gas_material, liquid_material)
def _load_phase_properties(self) -> None: logger.debug("Loading properties for phase type: %s", self.type) assert self.schema is not None phase_def = next( ( p for p in self.schema.get("phases", []) if p.get("type") == self.type ), None, ) if phase_def is None: msg = f"No phase definition found for type '{self.type}'" raise ValueError(msg) logger.debug("Found phase definition for %s", self.type) required = set(phase_def.get("properties", [])) logger.debug("Required properties: %s", required) source = { "AqueousLiquid": self.liquid_material, "Gas": self.gas_material, "Solid": self.solid_material, }.get(self.type) if source is None: msg = f"Don't know how to load properties for phase type '{self.type}'" raise ValueError(msg) logger.debug("Source material: %s", source.name) self.properties = [ prop for prop in source.properties if prop.name in required and ( prop.extra.get("scope") == "phase" or "scope" not in prop.extra ) ] loaded = {prop.name for prop in self.properties} missing = required - loaded if missing: msg = f"Missing required properties for phase type '{self.type}', material '{source.name}': {missing}" raise ValueError(msg) logger.debug( "Loaded %s properties for phase type '%s'", len(self.properties), self.type, ) logger.debug(self.properties) def _load_components( self, gas_material: Material, liquid_material: Material, Diffusion_coefficient: float | None = None, ) -> None: self.components_obj = Components( phase_type=self.type, gas_component=gas_material, liquid_component=liquid_material, process=self.process, Diffusion_coefficient=Diffusion_coefficient, ) self.components = [ self.components_obj.gas_component_obj, self.components_obj.liquid_component_obj, ]
[docs] def add_property(self, prop: MaterialProperty) -> None: self.properties.append(prop)
[docs] def add_component(self, component: Component) -> None: self.components.append(component)
[docs] def validate(self) -> bool: self._validate_phase_exists() self._validate_required_properties() self._validate_extra_properties() self._validate_components() return True
def _validate_phase_exists(self) -> None: if not any(p["type"] == self.type for p in self.schema["phases"]): msg = f"Phase '{self.type}' is not defined for this process." raise ValueError(msg) def _validate_required_properties(self) -> None: required = self._required_properties() found = self._found_property_names() missing = [p for p in required if p not in found] if missing: msg = ( f"Phase '{self.type}' is missing required properties: {missing}" ) raise ValueError(msg) def _validate_extra_properties(self) -> None: required = self._required_properties() found = self._found_property_names() extra = [p for p in found if p not in required] if extra: msg = f"Phase '{self.type}' has unknown/unsupported properties: {extra}" raise ValueError(msg) def _validate_components(self) -> None: for component in self.components: if not component.validate(): msg = f"Component '{component.name}' in phase '{self.type}' is invalid." raise ValueError(msg) def _required_properties(self) -> list[str]: for p in self.schema["phases"]: if p["type"] == self.type: return p.get("properties", []) return [] def _found_property_names(self) -> list[str]: return [p.name for p in self.properties] # ----------------------- # Representation # ----------------------- def __repr__(self) -> str: lines = [f"<Phase '{self.type}'>"] if self.properties: lines.append(f" ├─ {len(self.properties)} properties:") for prop in self.properties: lines.append(" │ " + repr(prop)) else: lines.append(" ├─ no properties") if self.components: lines.append(f" ├─ {len(self.components)} component groups:") for comp in self.components: for line in repr(comp).splitlines(): lines.append(" │ " + line) else: lines.append(" └─ no components") return "\n".join(lines)