diff --git a/CHANGELOG.md b/CHANGELOG.md index 1139ded9..1a0dc852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- (sample/features) cgns links and paths, as well as args `mesh_base_name` and `mesh_zone_name` - (sample) time_series support, now handle directly globals at time steps. - ## [0.1.9] - 2025-09-24 ### Added diff --git a/examples/containers/sample_example.py b/examples/containers/sample_example.py index 1da7a844..be629975 100644 --- a/examples/containers/sample_example.py +++ b/examples/containers/sample_example.py @@ -56,7 +56,7 @@ def show_sample(sample: Sample): # %% [markdown] # ## Section 1: Initializing an Empty Sample and Adding Data # -# This section demonstrates how to initialize an empty Sample and add scalars, and meshes / CGNS trees. +# This section demonstrates how to initialize an empty Sample and add scalars, time series data, and meshes / CGNS trees. # %% [markdown] # ### Create and display CGNS tree from an unstructured mesh @@ -146,17 +146,7 @@ def show_sample(sample: Sample): # Set meshes in the Sample new_sample_mult_mesh.features.set_meshes(meshes_dict) -print(f"{new_sample_mult_mesh.features.get_all_mesh_times() = }") - -# %% [markdown] -# ### Link tree from another sample - -# %% -path_linked_sample = Path.cwd() / "dataset/samples/sample_000000000/meshes/mesh_000000000.cgns" -new_sample_mult_mesh.link_tree( - path_linked_sample, linked_sample=sample, linked_time=0.0, time=1.5 -) -print(f"{new_sample_mult_mesh.features.get_all_mesh_times() = }") +print(f"{new_sample_mult_mesh.get_all_mesh_times() = }") # %% [markdown] # ## Section 2: Accessing and Modifying Sample Data diff --git a/src/plaid/bridges/huggingface_bridge.py b/src/plaid/bridges/huggingface_bridge.py index d3932355..730c7c6e 100644 --- a/src/plaid/bridges/huggingface_bridge.py +++ b/src/plaid/bridges/huggingface_bridge.py @@ -132,30 +132,21 @@ def to_plaid_sample(hf_sample: dict[str, bytes]) -> Sample: return Sample.model_validate(pickled_hf_sample) except ValidationError: - # If it fails, try to build the sample from its components - try: - scalars = pickled_hf_sample["scalars"] - meshes = pickled_hf_sample["meshes"] - - features = SampleFeatures( - data=meshes, - mesh_base_name=pickled_hf_sample.get("mesh_base_name"), - mesh_zone_name=pickled_hf_sample.get("mesh_zone_name"), - links=pickled_hf_sample.get("links"), - paths=pickled_hf_sample.get("paths"), - ) + features = SampleFeatures( + data=pickled_hf_sample.get("meshes"), + ) - sample = Sample( - path=pickled_hf_sample.get("path"), - features=features, - ) + sample = Sample( + path=pickled_hf_sample.get("path"), + features=features, + ) + scalars = pickled_hf_sample.get("scalars") + if scalars: for sn, val in scalars.items(): sample.add_scalar(sn, val) - return Sample.model_validate(sample) - except KeyError as e: - raise KeyError(f"Missing key {e!s} in HF data.") from e + return Sample.model_validate(sample) def generate_huggingface_description( diff --git a/src/plaid/containers/features.py b/src/plaid/containers/features.py index 15a31d43..86b4adab 100644 --- a/src/plaid/containers/features.py +++ b/src/plaid/containers/features.py @@ -1,11 +1,8 @@ """Module for implementing collections of features within a Sample.""" -import copy import logging -from pathlib import Path from typing import Optional -import CGNS.MAP as CGM import CGNS.PAT.cgnskeywords as CGK import CGNS.PAT.cgnslib as CGL import CGNS.PAT.cgnsutils as CGU @@ -20,7 +17,7 @@ _check_names, _read_index, ) -from plaid.types import Array, CGNSLink, CGNSNode, CGNSPath, CGNSTree, Field +from plaid.types import Array, CGNSNode, CGNSTree, Field from plaid.utils import cgns_helper as CGH logger = logging.getLogger(__name__) @@ -31,31 +28,18 @@ class SampleFeatures: Args: data (dict[float, CGNSTree], optional): A dictionary mapping time steps to CGNSTrees. Defaults to None. - mesh_base_name (str, optional): The base name for the mesh. Defaults to 'Base'. - mesh_zone_name (str, optional): The zone name for the mesh. Defaults to 'Zone'. - links (dict[float, list[CGNSLink]], optional): A dictionary mapping time steps to lists of links. Defaults to None. - paths (dict[float, list[CGNSPath]], optional): A dictionary mapping time steps to lists of paths. Defaults to None. """ def __init__( self, data: Optional[dict[float, CGNSTree]] = None, - mesh_base_name: str = "Base", - mesh_zone_name: str = "Zone", - links: Optional[dict[float, list[CGNSLink]]] = None, - paths: Optional[dict[float, list[CGNSPath]]] = None, ): self.data: dict[float, CGNSTree] = data if data is not None else {} - self._links = links if links is not None else {} - self._paths = paths if paths is not None else {} self._default_active_base: Optional[str] = None self._default_active_zone: Optional[str] = None self._default_active_time: Optional[float] = None - self._mesh_base_name: str = mesh_base_name - self._mesh_zone_name: str = mesh_zone_name - # -------------------------------------------------------------------------# def set_default_base(self, base_name: str, time: Optional[float] = None) -> None: @@ -160,6 +144,8 @@ def set_default_zone_base( self._default_active_zone = zone_name + # -------------------------------------------------------------------------# + def set_default_time(self, time: float) -> None: """Set the default time for the system. @@ -327,36 +313,16 @@ def init_tree(self, time: Optional[float] = None) -> CGNSTree: if not self.data: self.data = {time: CGL.newCGNSTree()} - self._links = {time: None} - self._paths = {time: None} elif time not in self.data: self.data[time] = CGL.newCGNSTree() - self._links[time] = None - self._paths[time] = None return self.data[time] - def get_links(self, time: Optional[float] = None) -> list[CGNSLink]: - """Retrieve the CGNS links for a specified time step, if available. - - Args: - time (float, optional): The time step for which to retrieve the CGNS links. If a specific time is not provided, the method will display the links for the default time step. - - Returns: - list: The CGNS links for the specified time step if available; otherwise, returns None. - """ - time = self.get_time_assignment(time) - return self._links[time] if (self._links) else None - - def get_mesh( - self, time: Optional[float] = None, apply_links: bool = False, in_memory=False - ) -> Optional[CGNSTree]: + def get_mesh(self, time: Optional[float] = None) -> Optional[CGNSTree]: """Retrieve the CGNS tree structure for a specified time step, if available. Args: time (float, optional): The time step for which to retrieve the CGNS tree structure. If a specific time is not provided, the method will display the tree structure for the default time step. - apply_links (bool, optional): Activates the following of the CGNS links to reconstruct the complete CGNS tree - in this case, a deepcopy of the tree is made to prevent from modifying the existing tree. - in_memory (bool, optional): Active if apply_links == True, ONLY WORKING if linked mesh is in the current sample. This option follows the link in memory from current sample. Returns: CGNSTree: The CGNS tree structure for the specified time step if available; otherwise, returns None. @@ -365,28 +331,7 @@ def get_mesh( return None time = self.get_time_assignment(time) - tree = self.data[time] - - links = self.get_links(time) - if not apply_links or links is None: - return tree - - tree = copy.deepcopy(tree) - for link in links: - if not in_memory: - subtree, _, _ = CGM.load(str(Path(link[0]) / link[1]), subtree=link[2]) - else: - linked_timestep = int(link[1].split(".cgns")[0].split("_")[1]) - linked_timestamp = list(self.data.keys())[linked_timestep] - subtree = self.get_mesh(linked_timestamp) - node_path = "/".join(link[2].split("/")[:-1]) - node_to_append = CGU.getNodeByPath(tree, node_path) - assert node_to_append is not None, ( - f"nodepath {node_path} not present in tree, cannot apply link" - ) - node_to_append[2].append(CGU.getNodeByPath(subtree, link[2])) - - return tree + return self.data[time] def set_meshes(self, meshes: dict[float, CGNSTree]) -> None: """Set all meshes with their corresponding time step. @@ -399,11 +344,6 @@ def set_meshes(self, meshes: dict[float, CGNSTree]) -> None: """ if not self.data: self.data = meshes - self._links = {} - self._paths = {} - for time in self.data.keys(): - self._links[time] = None - self._paths[time] = None else: raise KeyError( "meshes is already set, you cannot overwrite it, delete it first or extend it with `Sample.add_tree`" @@ -432,12 +372,8 @@ def add_tree(self, tree: CGNSTree, time: Optional[float] = None) -> CGNSTree: if not self.data: self.data = {time: tree} - self._links = {time: None} - self._paths = {time: None} elif time not in self.data: self.data[time] = tree - self._links[time] = None - self._paths[time] = None else: # TODO: gérer le cas où il y a des bases de mêmes noms... + merge # récursif des nœuds @@ -481,8 +417,6 @@ def del_tree(self, time: float) -> CGNSTree: if time not in self.data: raise KeyError(f"There is no CGNS tree for time {time}.") - self._links.pop(time, None) - self._paths.pop(time, None) return self.data.pop(time) # -------------------------------------------------------------------------# @@ -557,13 +491,7 @@ def init_base( time = self.get_time_assignment(time) if base_name is None: - base_name = ( - self._mesh_base_name - + "_" - + str(topological_dim) - + "_" - + str(physical_dim) - ) + base_name = "Base_" + str(topological_dim) + "_" + str(physical_dim) self.init_tree(time) if not (self.has_base(base_name, time)): @@ -662,7 +590,6 @@ def has_globals(self, time: Optional[float] = None) -> bool: Returns: bool: `True` if the CGNS tree has a Base called `Globals`, else return `False`. """ - # print(">>>>>>>>>>>", self.get_base_names(time=time)) return "Global" in self.get_base_names(time=time) def get_base( @@ -725,8 +652,7 @@ def init_zone( zone_name = self.get_zone_assignment(zone_name, base_name, time) if zone_name is None: - zone_name = self._mesh_zone_name - + zone_name = "Zone" zone_node = CGL.newZone(base_node, zone_name, zone_shape, zone_type) return zone_node diff --git a/src/plaid/containers/sample.py b/src/plaid/containers/sample.py index f9c9e1bf..f4ff8b71 100644 --- a/src/plaid/containers/sample.py +++ b/src/plaid/containers/sample.py @@ -26,8 +26,6 @@ from typing import Any, Optional, Union import CGNS.MAP as CGM -import CGNS.PAT.cgnslib as CGL -import CGNS.PAT.cgnsutils as CGU import numpy as np from pydantic import BaseModel, ConfigDict, PrivateAttr from pydantic import Field as PydanticField @@ -40,7 +38,6 @@ from plaid.containers.features import SampleFeatures from plaid.containers.utils import get_feature_type_and_details_from from plaid.types import ( - CGNSTree, Feature, FeatureIdentifier, Scalar, @@ -94,12 +91,7 @@ class Sample(BaseModel): - You can provide a path to a folder containing the sample data, and it will be loaded during initialization. - You can provide `SampleFeatures` and `SampleFeatures` instances to initialize the sample with existing data. - The default `SampleFeatures` instance is initialized with: - - `data=None`, `links=None`, and `paths=None` (i.e., no mesh data). - - `mesh_base_name="Base"` and `mesh_zone_name="Zone"`. - - The default `SampleFeatures` instance is initialized with: - - `scalars=None` (i.e., no scalar data). + The default `SampleFeatures` instance is initialized with `data=None`(i.e., no mesh data). """ # Pydantic configuration @@ -114,13 +106,7 @@ class Sample(BaseModel): ) features: Optional[SampleFeatures] = PydanticField( - default_factory=lambda _: SampleFeatures( - data=None, - mesh_base_name="Base", - mesh_zone_name="Zone", - links=None, - paths=None, - ), + default_factory=lambda _: SampleFeatures(data=None), description="An instance of SampleFeatures containing mesh data. Defaults to an empty `SampleFeatures` object.", ) @@ -192,127 +178,6 @@ def get_scalar_names(self) -> list[str]: # -------------------------------------------------------------------------# - def link_tree( - self, - path_linked_sample: Union[str, Path], - linked_sample: "Sample", - linked_time: float, - time: float, - ) -> CGNSTree: - """Link the geometrical features of the CGNS tree of the current sample at a given time, to the ones of another sample. - - Args: - path_linked_sample (Union[str,Path]): The absolute path of the folder containing the linked CGNS - linked_sample (Sample): The linked sample - linked_time (float): The time step of the linked CGNS in the linked sample - time (float): The time step the current sample to which the CGNS tree is linked. - - Returns: - CGNSTree: The deleted CGNS tree. - """ - # see https://pycgns.github.io/MAP/sids-to-python.html#links - # difficulty is to link only the geometrical objects, which can be complex - - # https://pycgns.github.io/MAP/examples.html#save-with-links - # When you load a file all the linked-to files are resolved to produce a full CGNS/Python tree with actual node data. - - path_linked_sample = Path(path_linked_sample) - - if linked_time not in linked_sample.features.data: # pragma: no cover - raise KeyError( - f"There is no CGNS tree for time {linked_time} in linked_sample." - ) - if time in self.features.data: # pragma: no cover - raise KeyError(f"A CGNS tree is already linked in self for time {time}.") - - tree = CGL.newCGNSTree() - - base_names = linked_sample.features.get_base_names(time=linked_time) - - for bn in base_names: - base_node = linked_sample.features.get_base(bn, time=linked_time) - base = [bn, base_node[1], [], "CGNSBase_t"] - tree[2].append(base) - - family = [ - "Bulk", - np.array([b"B", b"u", b"l", b"k"], dtype="|S1"), - [], - "FamilyName_t", - ] # maybe get this from linked_sample as well ? - base[2].append(family) - - zone_names = linked_sample.features.get_zone_names(bn, time=linked_time) - for zn in zone_names: - zone_node = linked_sample.features.get_zone( - zone_name=zn, base_name=bn, time=linked_time - ) - grid = [ - zn, - zone_node[1], - [ - [ - "ZoneType", - np.array( - [ - b"U", - b"n", - b"s", - b"t", - b"r", - b"u", - b"c", - b"t", - b"u", - b"r", - b"e", - b"d", - ], - dtype="|S1", - ), - [], - "ZoneType_t", - ] - ], - "Zone_t", - ] - base[2].append(grid) - zone_family = [ - "FamilyName", - np.array([b"B", b"u", b"l", b"k"], dtype="|S1"), - [], - "FamilyName_t", - ] - grid[2].append(zone_family) - - def find_feature_roots(sample: Sample, time: float, Type_t: str): - Types_t = CGU.getAllNodesByTypeSet(sample.features.get_mesh(time), Type_t) - # in case the type is not present in the tree - if Types_t == []: # pragma: no cover - return [] - types = [Types_t[0]] - for t in Types_t[1:]: - for tt in types: - if tt not in t: # pragma: no cover - types.append(t) - return types - - feature_paths = [] - for feature in ["ZoneBC_t", "Elements_t", "GridCoordinates_t"]: - feature_paths += find_feature_roots(linked_sample, linked_time, feature) - - self.features.add_tree(tree, time=time) - - dname = path_linked_sample.parent - bname = path_linked_sample.name - self.features._links[time] = [ - [str(dname), bname, fp, fp] for fp in feature_paths - ] - - return tree - - # -------------------------------------------------------------------------# - def del_all_fields( self, ) -> Self: @@ -783,7 +648,6 @@ def save( mesh_dir.mkdir() for i, time in enumerate(self.features.data.keys()): outfname = mesh_dir / f"mesh_{i:09d}.cgns" - if memory_safe: tmpfile = mesh_dir / f"mesh_{i:09d}.pkl" with open(tmpfile, "wb") as f: @@ -794,11 +658,7 @@ def save( logging.debug(f"save -> {outfname}") else: - status = CGM.save( - str(outfname), - self.features.data[time], - links=self.features._links.get(time), - ) + status = CGM.save(str(outfname), self.features.data[time]) logger.debug(f"save -> {status=}") @classmethod @@ -861,26 +721,11 @@ def load(self, path: Union[str, Path]) -> None: if meshes_dir.is_dir(): meshes_names = list(meshes_dir.glob("*")) nb_meshes = len(meshes_names) - # self.features = {} - self.features._links = {} - self.features._paths = {} for i in range(nb_meshes): - tree, links, paths = CGM.load(str(meshes_dir / f"mesh_{i:09d}.cgns")) + tree, _, _ = CGM.load(str(meshes_dir / f"mesh_{i:09d}.cgns")) time = CGH.get_time_values(tree) - ( - self.features.data[time], - self.features._links[time], - self.features._paths[time], - ) = ( - tree, - links, - paths, - ) - for i in range(len(self.features._links[time])): # pragma: no cover - self.features._links[time][i][0] = str( - meshes_dir / self.features._links[time][i][0] - ) + (self.features.data[time],) = (tree,) old_scalars_file = path / "scalars.csv" if old_scalars_file.is_file(): diff --git a/src/plaid/types/__init__.py b/src/plaid/types/__init__.py index 780eae8e..b42de25d 100644 --- a/src/plaid/types/__init__.py +++ b/src/plaid/types/__init__.py @@ -8,9 +8,7 @@ # from plaid.types.cgns_types import ( - CGNSLink, CGNSNode, - CGNSPath, CGNSTree, ) from plaid.types.common import Array, ArrayDType, IndexType @@ -30,8 +28,6 @@ "IndexType", "CGNSNode", "CGNSTree", - "CGNSLink", - "CGNSPath", "Scalar", "Field", "TimeSequence", diff --git a/src/plaid/types/cgns_types.py b/src/plaid/types/cgns_types.py index e48e31a4..bd8a898d 100644 --- a/src/plaid/types/cgns_types.py +++ b/src/plaid/types/cgns_types.py @@ -34,7 +34,3 @@ class CGNSNode(BaseModel): # A CGNSTree is simply the root CGNSNode CGNSTree: TypeAlias = CGNSNode - -# CGNS links and paths -CGNSLink: TypeAlias = list[str] # [dir, filename, source_path, target_path] -CGNSPath: TypeAlias = tuple[str, ...] # a path in the CGNS tree diff --git a/tests/bridges/test_huggingface_bridge.py b/tests/bridges/test_huggingface_bridge.py index c712f2e3..e89ca974 100644 --- a/tests/bridges/test_huggingface_bridge.py +++ b/tests/bridges/test_huggingface_bridge.py @@ -88,28 +88,11 @@ def test_to_plaid_sample_fallback_build_succeeds(self, dataset): "path": getattr(sample, "path", None), "scalars": {sn: sample.get_scalar(sn) for sn in sample.get_scalar_names()}, "meshes": sample.features.data, - "mesh_base_name": sample.features._mesh_base_name, - "mesh_zone_name": sample.features._mesh_zone_name, - "links": sample.features._links, - "paths": sample.features._paths, } old_hf_sample = {"sample": pickle.dumps(old_hf_sample)} plaid_sample = to_plaid_sample(old_hf_sample) assert isinstance(plaid_sample, Sample) - def test_to_plaid_sample_missing_key_raises_keyerror(self, dataset): - sample = dataset[0] - bad_sample = { - "path": getattr(sample, "path", None), - "mesh_base_name": sample.features._mesh_base_name, - "mesh_zone_name": sample.features._mesh_zone_name, - "links": sample.features._links, - "paths": sample.features._paths, - } - bad_hf_sample = {"sample": pickle.dumps(bad_sample)} - with pytest.raises(KeyError): - to_plaid_sample(bad_hf_sample) - def test_plaid_dataset_to_huggingface(self, dataset, problem_definition): hfds = huggingface_bridge.plaid_dataset_to_huggingface( dataset, problem_definition, split="train" diff --git a/tests/containers/test_sample.py b/tests/containers/test_sample.py index 46a1a6f0..34edeb6f 100644 --- a/tests/containers/test_sample.py +++ b/tests/containers/test_sample.py @@ -68,17 +68,6 @@ def nodes3d(): ) -@pytest.fixture() -def sample_with_linked_tree(tree, tmp_path): - sample_with_linked_tree = Sample() - sample_with_linked_tree.features.add_tree(tree) - path_linked_sample = tmp_path / "test_dir" / "meshes/mesh_000000000.cgns" - sample_with_linked_tree.link_tree( - path_linked_sample, sample_with_linked_tree, linked_time=0.0, time=1.0 - ) - return sample_with_linked_tree - - @pytest.fixture() def tree3d(nodes3d, triangles, vertex_field, cell_center_field): Mesh = MCT.CreateMeshOfTriangles(nodes3d, triangles) @@ -307,16 +296,6 @@ def test_get_mesh_empty(self, sample: Sample): def test_get_mesh(self, sample_with_tree_and_scalar): sample_with_tree_and_scalar.get_mesh() - def test_get_mesh_without_links(self, sample_with_linked_tree): - sample_with_linked_tree.get_mesh(time=1.0, apply_links=False) - - def test_get_mesh_with_links_in_memory(self, sample_with_linked_tree): - sample_with_linked_tree.get_mesh(time=1.0, apply_links=True, in_memory=True) - - def test_get_mesh_with_links(self, sample_with_linked_tree, tmp_path): - sample_with_linked_tree.save(tmp_path / "test_dir") - sample_with_linked_tree.get_mesh(time=1.0, apply_links=True) - def test_set_meshes_empty(self, sample, tree): sample.features.set_meshes({0.0: tree}) @@ -339,27 +318,9 @@ def test_del_tree(self, sample, tree): assert isinstance(sample.features.del_tree(0.2), list) assert list(sample.features.data.keys()) == [0.0] - assert list(sample.features._links.keys()) == [0.0] - assert list(sample.features._paths.keys()) == [0.0] assert isinstance(sample.features.del_tree(0.0), list) assert list(sample.features.data.keys()) == [] - assert list(sample.features._links.keys()) == [] - assert list(sample.features._paths.keys()) == [] - - def test_link_tree(self, sample_with_linked_tree): - link_checks = [ - "/Base_2_2/Zone/Elements_Selections", - "/Base_2_2/Zone/Points_Selections", - "/Base_2_2/Zone/Points_Selections/tag", - "/Base_2_2/Zone/Elements_TRI_3", - "/Base_2_2/Zone/GridCoordinates", - "/Base_2_2/Zone/ZoneBC", - ] - for link in sample_with_linked_tree.features._links[1]: - assert link[1] == "mesh_000000000.cgns" - assert link[2] == link[3] - assert link[2] in link_checks def test_on_error_del_tree(self, sample, tree): with pytest.raises(KeyError):