From 2f6e06e624a14d55998927e2de4082cac2275c9e Mon Sep 17 00:00:00 2001
From: Norbert Grunwald <norbert.grunwald@ufz.de>
Date: Mon, 31 Mar 2025 09:49:50 +0200
Subject: [PATCH] components now created in pairs for a certain phase

---
 ogstools/__init__.py                          |   2 +
 ogstools/materiallib/__init__.py              |   2 +
 ogstools/materiallib/core/__init__.py         |   2 +
 ogstools/materiallib/core/component.py        | 135 +++++++++++++++++-
 ogstools/materiallib/core/components.py       |  39 +++++
 ogstools/materiallib/core/material_list.py    |  83 +++++++++--
 ogstools/materiallib/schema/process_schema.py |  14 +-
 7 files changed, 258 insertions(+), 19 deletions(-)
 create mode 100644 ogstools/materiallib/core/components.py

diff --git a/ogstools/__init__.py b/ogstools/__init__.py
index 10aba63f..e6594142 100644
--- a/ogstools/__init__.py
+++ b/ogstools/__init__.py
@@ -13,6 +13,7 @@ from ._find_ogs import cli, status
 # from .materiallib import MaterialList, MaterialDB, MaterialLib, validate_medium
 from .materiallib import (
     Component,
+    Components,
     MaterialDB,
     # MaterialLib,
     MaterialList,
@@ -46,6 +47,7 @@ __all__ = [
     "Medium",
     "Phase",
     "Component",
+    "Components",
     "Property",
     "validate_medium",
 ]
diff --git a/ogstools/materiallib/__init__.py b/ogstools/materiallib/__init__.py
index 1ef54aa1..98c03be9 100644
--- a/ogstools/materiallib/__init__.py
+++ b/ogstools/materiallib/__init__.py
@@ -7,6 +7,7 @@
 
 from .core import (
     Component,
+    Components,
     MaterialDB,
     # MaterialLib,
     MaterialList,
@@ -23,6 +24,7 @@ __all__ = [
     "Medium",
     "Phase",
     "Component",
+    "Components",
     "Property",
     "validate_medium",
 ]
diff --git a/ogstools/materiallib/core/__init__.py b/ogstools/materiallib/core/__init__.py
index d83b3045..2ad04c50 100644
--- a/ogstools/materiallib/core/__init__.py
+++ b/ogstools/materiallib/core/__init__.py
@@ -8,6 +8,7 @@
 
 
 from .component import Component
+from .components import Components
 from .material import Material
 from .material_db import MaterialDB
 from .material_list import MaterialList
@@ -24,6 +25,7 @@ __all__ = [
     "Medium",
     "Phase",
     "Component",
+    "Components",
     "Property",
     "MaterialLib",
 ]
diff --git a/ogstools/materiallib/core/component.py b/ogstools/materiallib/core/component.py
index c732be81..251a5bbb 100644
--- a/ogstools/materiallib/core/component.py
+++ b/ogstools/materiallib/core/component.py
@@ -1,10 +1,133 @@
-from typing import Any
+from ogstools.materiallib.schema.process_schema import PROCESS_SCHEMAS
+
+from .material import Material
+from .property import Property
 
 
 class Component:
-    def __init__(self, name: str, properties: list | None = None):
-        self.name = name
-        self.properties = properties or []
+    def __init__(
+        self,
+        material: Material,
+        phase_type: str,
+        role: str,  # This materials role in the phase, e.g. 'solute' or 'solvent, etc.
+        process: str,
+    ):
+        self.material = material
+        self.phase_type = phase_type
+        self.role = role
+        self.name = material.name
+
+        self.schema = PROCESS_SCHEMAS.get(process)
+        if not self.schema:
+            msg = f"No process schema found for '{process}'."
+            raise ValueError(msg)
+
+        self.properties: list[Property] = self._get_filtered_properties()
+
+    def _get_filtered_properties(self) -> list[Property]:
+        """
+        This method filters the material properties based on the process schema
+        and the role (gas or liquid).
+        """
+        required_properties = set()
+
+        # Process schema check and filter required properties
+        if self.schema:
+            print(
+                f"Processing schema for phase type: {self.phase_type}"
+            )  # Debug: Phase being processed
+
+            # Iterate over the process schema to find the relevant properties
+            for key, value in self.schema.items():
+                print(
+                    f"Checking key: {key}, value: {value}"
+                )  # Debug: Output the schema's current key and value
+
+                # Ensure 'value' is a dictionary and the phase matches the current phase type
+                if isinstance(value, dict) and key == self.phase_type:
+                    # Debug output for components in the phase
+                    if "Components" in value:
+                        print(
+                            f"Components found for {key}: {value['Components']}"
+                        )  # Debug: Show components
+
+                        # Now check the role within the components
+                        if self.role in value["Components"]:
+                            # Debug: Role found, print properties
+                            print(
+                                f"Properties for role '{self.role}': {value['Components'][self.role]}"
+                            )
+
+                            # Add the properties for the current role (gas or liquid)
+                            required_properties.update(
+                                value["Components"][self.role]
+                            )
+                        else:
+                            print(
+                                f"Role '{self.role}' not found in components for {key}"
+                            )  # Debug: Role not found
+                    else:
+                        print(
+                            f"No components found in schema for phase {key}"
+                        )  # Debug: No components
+                else:
+                    print(
+                        f"Skipping phase {key} because it does not match {self.phase_type} or is not a dict"
+                    )  # Debug: Phase mismatch
+
+        # Debug: Print the required properties
+        print(f"Required properties: {required_properties}")
+
+        # Now filter the material properties based on the schema
+        filtered_properties = [
+            prop
+            for prop in self.material.get_properties()
+            if prop.name in required_properties
+        ]
+
+        # Debug: Print filtered properties
+        print(f"Filtered properties: {filtered_properties}")
+
+        return filtered_properties
+
+    # def _get_filtered_properties(self) -> list[Property]:
+    #     """
+    #     This method filters the material properties based on the process schema
+    #     and the role (gas or liquid).
+    #     """
+    #     required_properties = set()
+
+    #     # Process schema check and filter required properties
+    #     if self.schema:
+    #         # Here we filter the properties that are specified in the process schema
+    #         # for this phase and role
+    #         for key, value in self.schema.items():
+    #             # Make sure `value` is a dictionary and the role is in the components
+    #             if (
+    #                 isinstance(value, dict)
+    #                 and key == self.phase_type
+    #                 and "Components" in value
+    #                 and self.role in value["Components"]
+    #             ):
+    #                 # Add all properties for the role (gas or liquid)
+    #                 required_properties.update(value["Components"][self.role])
+    #                 print(value["Components"][self.role])
+    #                 print("##############################")
+
+    #     # Now we filter the material properties based on the schema
+    #     return [
+    #         prop
+    #         for prop in self.material.get_properties()
+    #         if prop.name in required_properties
+    #     ]
+
+    def __repr__(self) -> str:
+        # Sammle die Namen der Eigenschaften
+        property_names = [prop.name for prop in self.properties]
 
-    def add_property(self, prop: Any) -> None:
-        self.properties.append(prop)
+        # Gebe die Namen der Eigenschaften aus
+        return (
+            f"<Component '{self.name}' (Role: {self.role}, Phase: {self.phase_type}) with "
+            f"{len(self.properties)} properties:\n"
+            f"  ├─ {'\n  ├─ '.join(property_names)}>"
+        )
diff --git a/ogstools/materiallib/core/components.py b/ogstools/materiallib/core/components.py
new file mode 100644
index 00000000..ceb15fea
--- /dev/null
+++ b/ogstools/materiallib/core/components.py
@@ -0,0 +1,39 @@
+from .component import Component
+from .material import Material
+from .property import Property
+
+
+class Components:
+    def __init__(
+        self,
+        phase_type: str,
+        gas_component: Material,
+        liquid_component: Material,
+        process: str,
+    ):
+        self.phase_type = phase_type
+        self.gas_properties: list[Property] = gas_component.get_properties()
+        self.liquid_properties: list[Property] = (
+            liquid_component.get_properties()
+        )
+        self.process = process
+
+        self.gas_component = gas_component
+        self.liquid_component = liquid_component
+
+        self.gas_component_obj = self._create_component(self.gas_component, "A")
+        self.liquid_component_obj = self._create_component(
+            self.liquid_component, "W"
+        )
+
+    def _create_component(self, material: Material, role: str) -> Component:
+        return Component(material, self.phase_type, role, self.process)
+
+    def __repr__(self) -> str:
+        return (
+            f"<Components for phase '{self.phase_type}' with "
+            f"Gas Component: {self.gas_component_obj.name}, "
+            f"Liquid Component: {self.liquid_component_obj.name}.\n>"
+            f"{self.gas_component_obj.name}: {self.gas_component_obj},\n>"
+            f"{self.liquid_component_obj.name}: {self.liquid_component_obj},\n>"
+        )
diff --git a/ogstools/materiallib/core/material_list.py b/ogstools/materiallib/core/material_list.py
index 6c125590..e178b3df 100644
--- a/ogstools/materiallib/core/material_list.py
+++ b/ogstools/materiallib/core/material_list.py
@@ -1,4 +1,4 @@
-from typing import cast
+from typing import Any, cast
 
 from ogstools.materiallib.schema.process_schema import PROCESS_SCHEMAS
 
@@ -40,7 +40,7 @@ class MaterialList:
     [2] matID="1", name="bentonit", properties: ["Density", "Permeability", ...]
     [3] matID="2", name="concrete", properties: ["Density", "Permeability", ...]
     [4] matID="3", name="concrete", properties: ["Density", "Permeability", ...]
-    [5] matID="4", name="water", properties: ["Density", "Viscosity", ...]
+    [5] matID="X", name="water", properties: ["Density", "Viscosity", ...]
     """
 
     def __init__(
@@ -106,10 +106,65 @@ class MaterialList:
             material = Material(name=mat_name, properties=filtered_props)
             self.fluid_materials[phase_type] = material
 
+    # def get_required_property_names(self) -> set[str]:
+    #     """
+    #     Returns a set of all property names required by the current process schema.
+
+    #     Prints a structured overview of the required properties, grouped by hierarchy level):
+    #     - Medium-level properties
+    #     - Phase-level properties
+    #     - Component-level properties (if fluid)
+
+    #     """
+    #     if self.schema is None:
+    #         raise ValueError("Process schema not set. Cannot determine required properties.")
+
+    #     required = set()
+
+    #     # Print an overview of the required property structure
+    #     print("=== Required Property Structure ===")
+    #     print("===-----------------------------===")
+
+    #     # Medium-level properties
+    #     medium_props = self.schema.get("properties", [])
+    #     if medium_props:
+    #         print("Medium-level properties:")
+    #         for prop in medium_props:
+    #             print(f"  - {prop}")
+    #         print()
+    #         required.update(medium_props)
+
+    #     # Phases and their properties
+    #     for phase in self.schema.get("phases", []):
+    #         phase_type = phase.get("type")
+
+    #         # Phase-level
+    #         phase_props = phase.get("properties", [])
+    #         if phase_props:
+    #             print(f"Phase-level properties for '{phase_type}':")
+    #             for prop in phase_props:
+    #                 print(f"  - {prop}")
+    #             print()
+    #             required.update(phase_props)
+
+    #         # Component-level (if fluid)
+    #         if phase_type in ["AqueousLiquid", "Gas"]:
+    #             components = phase.get("components", {})
+    #             for comp_name, comp_props in components.items():
+    #                 if comp_props:
+    #                     print(f"Component-level properties for '{comp_name}' in phase '{phase_type}':")
+    #                     for prop in comp_props:
+    #                         print(f"  - {prop}")
+    #                     print()
+    #                     required.update(comp_props)
+    #     print("===-----------------------------===")
+
+    #     return required
+
     def get_required_property_names(self) -> set[str]:
         """
         Returns a set of all property names required by the current process schema.
-        This includes medium, solid, phase and component properties.
+        This includes medium-level, phase-level, and component-level properties.
         """
         if self.schema is None:
             msg = (
@@ -117,16 +172,20 @@ class MaterialList:
             )
             raise ValueError(msg)
 
-        required = set()
+        required = set[str]()
+        PHASES_WITH_COMPONENTS = {"AqueousLiquid", "Gas", "NonAqueousLiquid"}
+
+        # Medium-level
+        medium_properties = cast(list[str], self.schema.get("properties", []))
+        required.update(medium_properties)
 
-        for key, value in self.schema.items():
-            if key == "_fluids":
-                fluid_defs = cast(dict[str, dict[str, list[str]]], value)
-                for fluid_def in fluid_defs.values():
-                    required.update(fluid_def.get("phase_properties", []))
-                    required.update(fluid_def.get("component_properties", []))
-            else:
-                required.add(key)
+        # Phase-level and component-level
+        phases = cast(list[dict[str, Any]], self.schema.get("phases", []))
+        for phase in phases:
+            required.update(phase.get("properties", []))
+            if phase.get("type") in PHASES_WITH_COMPONENTS:
+                for component_props in phase.get("components", {}).values():
+                    required.update(component_props)
 
         return required
 
diff --git a/ogstools/materiallib/schema/process_schema.py b/ogstools/materiallib/schema/process_schema.py
index f71597f0..09e872e6 100644
--- a/ogstools/materiallib/schema/process_schema.py
+++ b/ogstools/materiallib/schema/process_schema.py
@@ -1,4 +1,16 @@
 PROCESS_SCHEMAS = {
+    "SMALL_DEFORMATION": {
+        "phases": [{"type": "Solid", "properties": ["density"]}],
+        "properties": [],
+    },
+    "HEAT_CONDUCTION": {
+        "phases": [],
+        "properties": [
+            "thermal_conductivity",
+            "density",
+            "specific_heat_capacity",
+        ],
+    },
     "TH2M_PT": {
         "phases": [
             {
@@ -54,7 +66,7 @@ PROCESS_SCHEMAS = {
             "thermal_conductivity",
             "bishops_effective_stress",
         ],
-    }
+    },
 }
 
 
-- 
GitLab