Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (storage/writer) add validate_required_infos before creating and pushing heavy data to prevent later error
- (storage) add splits args to init_from_disk, add train_test_split for all backends.
- (utils/cgns_helper) add update_features_for_CGNS_compatibility which enable filtering features in conv.to_plaid while maintaining a correct CGNS tree (geometrical support is kept when only a field is requested, for instance).
- (containers/utils) strengthen `_check_names` to support `None` values and enforce CGNS node name length constraints (<=32 characters).
- (containers/features) validate all names in input trees in `SampleFeatures.add_tree` before insertion.

### Fixes

- (tests/containers) update sample field-name fixtures and add dedicated coverage for invalid CGNS name lengths.

### Removed

## [0.1.12] - 2026-01-22
Expand Down
15 changes: 15 additions & 0 deletions src/plaid/containers/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ def add_tree(self, tree: CGNSTree, time: Optional[float] = None) -> CGNSTree:
if tree == []:
raise ValueError("CGNS Tree should not be an empty list")

def _iter_node_names(node: CGNSNode) -> list[str]:
names = []
if isinstance(node, list) and len(node) > 0:
if isinstance(node[0], str):
names.append(node[0])
if len(node) > 2 and isinstance(node[2], list):
for child in node[2]:
names.extend(_iter_node_names(child))
return names

all_names = _iter_node_names(tree)
if all_names and all_names[0] == "CGNSTree":
all_names = all_names[1:]
_check_names(all_names)

time = self.resolve_time(time)

if not self.data:
Expand Down
12 changes: 9 additions & 3 deletions src/plaid/containers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# %% Imports

from pathlib import Path
from typing import Any, Union
from typing import Any, Optional, Union

import CGNS.PAT.cgnsutils as CGU
import numpy as np
Expand All @@ -34,22 +34,28 @@
path_to_location.update(retrocompatibility)


def _check_names(names: Union[str, list[str]]):
def _check_names(names: Union[str, list[Optional[str]], None]):
"""Check that names do not contain invalid character ``/``.

Args:
names (Union[str, list[str]]): The names to check.
names (Union[str, list[Optional[str]], None]): The names to check.

Raises:
ValueError: If any name contains the invalid character ``/``.
"""
if names is None:
names = [None]
if isinstance(names, str):
names = [names]
for name in names:
if (name is not None) and ("/" in name):
raise ValueError(
f"feature_names containing `/` are not allowed, but {name=}, you should first replace any occurence of `/` with something else, for example: `name.replace('/','__')`"
)
if (name is not None) and (len(name) > 32):
raise ValueError(
f"CGNS names must be shorter than or equal to 32 characters, but got {name=} with length {len(name)}"
)


def _read_index(pyTree: list, dim: list[int]):
Expand Down
82 changes: 50 additions & 32 deletions tests/containers/test_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# %% Imports

import copy
from pathlib import Path

import CGNS.PAT.cgnskeywords as CGK
Expand Down Expand Up @@ -117,12 +118,29 @@ def full_sample(sample_with_tree_and_scalar: Sample, tree3d):
def test_check_names():
_check_names("test name")
_check_names(["test name", "test_name_2"])
_check_names(None)
_check_names([None, "short_name"])
_check_names("a" * 31)
with pytest.raises(ValueError):
_check_names("test/name")
with pytest.raises(ValueError):
_check_names(["test/name"])
with pytest.raises(ValueError):
_check_names([r"test\/name"])
with pytest.raises(ValueError):
_check_names("a" * 33)
with pytest.raises(ValueError):
_check_names(["ok", "b" * 40])


def test_add_tree_invalid_name_length(sample: Sample, tree):
invalid_tree = copy.deepcopy(tree)
base_path = CGU.getPathsByTypeSet(invalid_tree, ["CGNSBase_t"])[0]
base_node = CGU.getNodeByPath(invalid_tree, base_path)
base_node[0] = "B" * 33

with pytest.raises(ValueError):
sample.features.add_tree(invalid_tree)


def test_read_index(tree, physical_dim):
Expand Down Expand Up @@ -797,150 +815,150 @@ def test_get_field_names_several_bases(self):
time=1.0,
)
sample.add_field(
name="test_vertex_Zone_1_Base_1_2_t_m0.1",
name="vertex_Zone_1_Base_1_2_t_m0.1",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_1",
base_name="Base_1_2",
time=-0.1,
)
sample.add_field(
name="test_cell_Zone_1_Base_1_2_t_m0.1",
name="cell_Zone_1_Base_1_2_t_m0.1",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_1",
base_name="Base_1_2",
time=-0.1,
)
sample.add_field(
name="test_vertex_Zone_2_Base_1_2_t_m0.1",
name="vertex_Zone_2_Base_1_2_t_m0.1",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_2",
base_name="Base_1_2",
time=-0.1,
)
sample.add_field(
name="test_cell_Zone_2_Base_1_2_t_m0.1",
name="cell_Zone_2_Base_1_2_t_m0.1",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_2",
base_name="Base_1_2",
time=-0.1,
)
sample.add_field(
name="test_vertex_Zone_1_Base_2_2_t_m0.1",
name="vertex_Zone_1_Base_2_2_t_m0.1",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_1",
base_name="Base_2_2",
time=-0.1,
)
sample.add_field(
name="test_cell_Zone_1_Base_2_2_t_m0.1",
name="cell_Zone_1_Base_2_2_t_m0.1",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_1",
base_name="Base_2_2",
time=-0.1,
)
sample.add_field(
name="test_vertex_Zone_2_Base_2_2_t_m0.1",
name="vertex_Zone_2_Base_2_2_t_m0.1",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_2",
base_name="Base_2_2",
time=-0.1,
)
sample.add_field(
name="test_cell_Zone_2_Base_2_2_t_m0.1",
name="cell_Zone_2_Base_2_2_t_m0.1",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_2",
base_name="Base_2_2",
time=-0.1,
)
sample.add_field(
name="test_vertex_Zone_1_Base_1_3_t_1.0",
name="vertex_Zone_1_Base_1_3_t_1.0",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_1",
base_name="Base_1_3",
time=1.0,
)
sample.add_field(
name="test_cell_Zone_1_Base_1_3_t_1.0",
name="cell_Zone_1_Base_1_3_t_1.0",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_1",
base_name="Base_1_3",
time=1.0,
)
sample.add_field(
name="test_vertex_Zone_2_Base_1_3_t_1.0",
name="vertex_Zone_2_Base_1_3_t_1.0",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_2",
base_name="Base_1_3",
time=1.0,
)
sample.add_field(
name="test_cell_Zone_2_Base_1_3_t_1.0",
name="cell_Zone_2_Base_1_3_t_1.0",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_2",
base_name="Base_1_3",
time=1.0,
)
sample.add_field(
name="test_vertex_Zone_1_Base_3_3_t_1.0",
name="vertex_Zone_1_Base_3_3_t_1.0",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_1",
base_name="Base_3_3",
time=1.0,
)
sample.add_field(
name="test_cell_Zone_1_Base_3_3_t_1.0",
name="cell_Zone_1_Base_3_3_t_1.0",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_1",
base_name="Base_3_3",
time=1.0,
)
sample.add_field(
name="test_vertex_Zone_2_Base_3_3_t_1.0",
name="vertex_Zone_2_Base_3_3_t_1.0",
field=np.random.randn(10),
location="Vertex",
zone_name="Zone_2",
base_name="Base_3_3",
time=1.0,
)
sample.add_field(
name="test_cell_Zone_2_Base_3_3_t_1.0",
name="cell_Zone_2_Base_3_3_t_1.0",
field=np.random.randn(10),
location="CellCenter",
zone_name="Zone_2",
base_name="Base_3_3",
time=1.0,
)
expected_field_names = [
"test_vertex_Zone_1_Base_1_2_t_m0.1",
"test_cell_Zone_1_Base_1_2_t_m0.1",
"test_vertex_Zone_2_Base_1_2_t_m0.1",
"test_cell_Zone_2_Base_1_2_t_m0.1",
"test_vertex_Zone_1_Base_2_2_t_m0.1",
"test_cell_Zone_1_Base_2_2_t_m0.1",
"test_vertex_Zone_2_Base_2_2_t_m0.1",
"test_cell_Zone_2_Base_2_2_t_m0.1",
"test_vertex_Zone_1_Base_1_3_t_1.0",
"test_cell_Zone_1_Base_1_3_t_1.0",
"test_vertex_Zone_2_Base_1_3_t_1.0",
"test_cell_Zone_2_Base_1_3_t_1.0",
"test_vertex_Zone_1_Base_3_3_t_1.0",
"test_cell_Zone_1_Base_3_3_t_1.0",
"test_vertex_Zone_2_Base_3_3_t_1.0",
"test_cell_Zone_2_Base_3_3_t_1.0",
"vertex_Zone_1_Base_1_2_t_m0.1",
"cell_Zone_1_Base_1_2_t_m0.1",
"vertex_Zone_2_Base_1_2_t_m0.1",
"cell_Zone_2_Base_1_2_t_m0.1",
"vertex_Zone_1_Base_2_2_t_m0.1",
"cell_Zone_1_Base_2_2_t_m0.1",
"vertex_Zone_2_Base_2_2_t_m0.1",
"cell_Zone_2_Base_2_2_t_m0.1",
"vertex_Zone_1_Base_1_3_t_1.0",
"cell_Zone_1_Base_1_3_t_1.0",
"vertex_Zone_2_Base_1_3_t_1.0",
"cell_Zone_2_Base_1_3_t_1.0",
"vertex_Zone_1_Base_3_3_t_1.0",
"cell_Zone_1_Base_3_3_t_1.0",
"vertex_Zone_2_Base_3_3_t_1.0",
"cell_Zone_2_Base_3_3_t_1.0",
]
assert sample.get_field_names() == sorted(set(expected_field_names))

Expand Down
Loading