Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- (sample) **Breaking**: remove the `path` constructor argument from `Sample`; use `Sample.load_from_dir(path)` or `sample.load(path)` for explicit disk loading.
- (infos) add an infos.py file with a pydantic Info class at the same level as ProblemDefinition.
- (cgns_backend) remove constant/variable mechanism in reading and writing paths for the cgns_backend.
- (docs) update logos and migrate to Zensical.
Expand Down
4 changes: 4 additions & 0 deletions docs/source/concepts/sample.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ sample.save_to_dir("sample_000000000", overwrite=True)
sample = Sample.load_from_dir("sample_000000000")
```

`Sample()` creates an empty in-memory sample. Loading from disk is explicit:
use `Sample.load_from_dir(path)` to create a loaded sample, or instantiate an
empty sample and call `sample.load(path)`.

The CGNS files can be open in Paraview for advanced visualization and post-treatment.

For large samples, `save_to_dir(..., memory_safe=True)` writes CGNS files through
Expand Down
1 change: 1 addition & 0 deletions docs/source/upgrade_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ the [reader API](api/storage/reader.md), and the [backend API](api/storage/backe
| `Dataset()` + `add_sample` / `add_samples` / `from_list_of_samples` | `save_to_disk(sample_constructor=..., ids=...)` |
| `Dataset.save_to_dir(path)` / `add_to_dir` | `save_to_disk(path, sample_constructor=..., ids=...)` |
| `Dataset.load(path)` | `init_from_disk(path)` → `(datasetdict, converterdict)` |
| `Sample(path=sample_dir)` | `Sample.load_from_dir(sample_dir)` or `sample = Sample(); sample.load(sample_dir)` |
| `dataset[i]` / `get_samples()` | `converter.to_plaid(dataset, i)` |
| `len(dataset)` / `get_number_of_samples()` | `len(dataset)` (per-split backend object) |
| `dataset.set_infos(...)` / `get_infos()` | pass [`Infos`](concepts/infos.md) to `save_to_disk(infos=...)`; read back with `Infos.from_path(path)` |
Expand Down
18 changes: 5 additions & 13 deletions examples/containers/sample_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,26 +558,18 @@

sample.save_to_dir(sample_save_fname)

# %% [markdown]
# #### Load a Sample from a directory via initialization

# %%
new_sample = Sample(path=sample_save_fname)

print(sample.summarize())

# %% [markdown]
# #### Load a Sample from a directory via the Sample class

# %%
new_sample_2 = Sample.load_from_dir(test_pth / "test")
new_sample_from_dir = Sample.load_from_dir(sample_save_fname)

print(sample.summarize())
print(new_sample_from_dir.summarize())

# %% [markdown]
# #### Load the Sample from a directory via a Sample instance

# %%
new_sample = Sample()
new_sample.load(sample_save_fname)
print(sample.summarize())
new_sample_via_instance = Sample()
new_sample_via_instance.load(sample_save_fname)
print(new_sample_via_instance.summarize())
6 changes: 3 additions & 3 deletions src/plaid/containers/managers/default_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def set_default_time(self, time: float) -> None:
.. code-block:: python

from plaid import Sample
sample = Sample("path_to_plaid_sample")
sample = Sample.load_from_dir("path_to_plaid_sample")
print(sample)
>>> Sample(2 scalars, 1 timestamp, 5 fields)
print(sample.show_tree(0.5))
Expand Down Expand Up @@ -122,7 +122,7 @@ def set_default_base(self, base: str, time: Optional[float] = None) -> None:
.. code-block:: python

from plaid import Sample
sample = Sample("path_to_plaid_sample")
sample = Sample.load_from_dir("path_to_plaid_sample")
print(sample)
>>> Sample(2 scalars, 1 timestamp, 5 fields)
print(sample.get_physical_dim("BaseA", 0.5))
Expand Down Expand Up @@ -173,7 +173,7 @@ def set_default_zone_base(
.. code-block:: python

from plaid import Sample
sample = Sample("path_to_plaid_sample")
sample = Sample.load_from_dir("path_to_plaid_sample")
print(sample)
>>> Sample(2 scalars, 1 timestamp, 5 fields)
print(sample.get_zone_type("ZoneX", "BaseA", 0.5))
Expand Down
30 changes: 6 additions & 24 deletions src/plaid/containers/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
import subprocess
from copy import deepcopy
from pathlib import Path
from typing import Any, Optional, Union
from typing import Optional, Union

import CGNS.MAP as CGM
import CGNS.PAT.cgnskeywords as CGK
import CGNS.PAT.cgnslib as CGL
import CGNS.PAT.cgnsutils as CGU
import numpy as np
from CGNS.PAT.cgnsutils import __CHILDREN__, __NAME__
from pydantic import BaseModel, ConfigDict, model_validator
from pydantic import BaseModel, model_validator
from pydantic import Field as PydanticField

from ..constants import (
Expand All @@ -48,35 +48,24 @@
CGNS_WORKER = Path(__file__).parent.parent / "utils" / "cgns_worker.py"


class Sample(BaseModel):
class Sample(BaseModel, arbitrary_types_allowed=True, extra="forbid"):
"""Represents a single sample. It contains data and information related to a single observation or measurement within a dataset.

By default, the sample is empty but:
- You can provide a path to a folder containing the sample data, and it will be loaded during initialization.
By default, the sample is empty. Use :meth:`Sample.load_from_dir` or
:meth:`Sample.load` to load sample data from disk.

Note:
Mesh/field/global operations are directly implemented on ``Sample`` via
inheritance from internal feature operations.
"""

# Pydantic configuration
# TODO(FB) check why arbitrary_types_allowed is needed, and if it can be removed
model_config = ConfigDict(
arbitrary_types_allowed=True, revalidate_instances="always", extra="forbid"
)

# Attributes
path: Optional[Union[str, Path]] = PydanticField(
None,
description="Path to the folder containing the sample data. If provided, the sample will be loaded from this path during initialization. Defaults to None.",
)

data: dict[float, CGNSTree] = PydanticField(
default_factory=dict,
description="A dictionary mapping time steps to CGNS trees.",
)

defaults: DefaultManager = PydanticField(
defaults: Optional[DefaultManager] = PydanticField(
default=None,
exclude=True,
repr=False,
Expand All @@ -90,13 +79,6 @@ def initialize_defaults(self) -> "Sample":
self.defaults = DefaultManager(self)
return self

def model_post_init(self, _context: Any) -> None:
"""Run post-initialization hooks (e.g. load sample from path)."""
# Load if path is provided
if self.path is not None:
path = Path(self.path)
self.load(path)

def copy(self) -> Self: # pyright: ignore[reportIncompatibleMethodOverride]
"""Create a deep copy of the current `Sample` instance.

Expand Down
26 changes: 16 additions & 10 deletions src/plaid/infos.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
from typing import Any, Union

import yaml
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic.dataclasses import dataclass
from pydantic import BaseModel, Field, ValidationError

logger = logging.getLogger(__name__)

_PD_CONFIG = ConfigDict(
extra="forbid", str_strip_whitespace=True, validate_assignment=True
)


@dataclass(config=_PD_CONFIG)
class DataProduction:
class DataProduction(
BaseModel,
revalidate_instances="always",
str_strip_whitespace=True,
validate_assignment=True,
extra="forbid",
):
"""Dataset production context metadata."""

owner: str | None = None
Expand All @@ -43,10 +43,16 @@ class DataProduction:
)


class Infos(BaseModel):
class Infos(
BaseModel,
revalidate_instances="always",
str_strip_whitespace=True,
validate_assignment=True,
extra="forbid",
):
"""Structured representation of a PLAID dataset ``infos`` payload."""

model_config = _PD_CONFIG
# model_config = _PD_CONFIG

owner: str
license: str
Expand Down
16 changes: 11 additions & 5 deletions src/plaid/problem_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import Any, Literal, Sequence, Union

import yaml
from pydantic import BaseModel, ConfigDict, field_validator
from pydantic import BaseModel, field_validator

# %% Globals

Expand All @@ -26,12 +26,18 @@ def _normalize_list(v):
return sorted(map(str, v))


class ProblemDefinition(BaseModel):
class ProblemDefinition(
BaseModel,
revalidate_instances="always",
str_strip_whitespace=True,
validate_assignment=True,
extra="forbid",
):
"""Defines the input and output features for a machine learning problem."""

model_config = ConfigDict(
revalidate_instances="always", validate_assignment=True, extra="forbid"
)
# model_config = ConfigDict(
# revalidate_instances="always", validate_assignment=True, extra="forbid"
# )

input_features: list[str]
output_features: list[str]
Expand Down
9 changes: 3 additions & 6 deletions src/plaid/storage/cgns/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __getitem__(self, idx: int) -> Sample:
Sample: A PLAID Sample object.
"""
assert idx in self.ids
return Sample(path=self.path / f"sample_{idx:09d}")
return Sample.load_from_dir(self.path / f"sample_{idx:09d}")

def __len__(self) -> int:
"""Get the number of samples in the dataset.
Expand Down Expand Up @@ -122,12 +122,9 @@ def sample_generator(
allow_patterns=[f"data/{split}/sample_{idx:09d}/"],
local_dir=temp_folder,
)
sample = Sample(
path=Path(temp_folder) / "data" / f"{split}" / f"sample_{idx:09d}"
sample = Sample.load_from_dir(
Path(temp_folder) / "data" / f"{split}" / f"sample_{idx:09d}"
)
# Sample data are eagerly loaded in memory during initialization;
# clear the transient on-disk path before leaving the temp dir.
sample.path = None
yield sample


Expand Down
2 changes: 1 addition & 1 deletion src/plaid/storage/common/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def to_plaid_sample(
for time, flat_tree in sample_dict.items():
sample_data[time] = unflatten_cgns_tree(flat_tree, cgns_types)

return Sample(path=None, data=sample_data)
return Sample(data=sample_data)


def plaid_to_sample_dict(
Expand Down
20 changes: 10 additions & 10 deletions tests/containers/test_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def current_directory() -> Path:

class Test_Sample:
# -------------------------------------------------------------------------#
def test___init__(self, current_directory):
def test_load_from_dir_multiple_samples(self, current_directory):
sample_path_1 = (
current_directory / "dataset_cgns" / "data" / "test" / "sample_000000000"
)
Expand All @@ -188,19 +188,19 @@ def test___init__(self, current_directory):
sample_path_3 = (
current_directory / "dataset_cgns" / "data" / "test" / "sample_000000002"
)
sample_already_filled_1 = Sample(path=sample_path_1)
sample_already_filled_2 = Sample(path=sample_path_2)
sample_already_filled_3 = Sample(path=sample_path_3)
sample_already_filled_1 = Sample.load_from_dir(sample_path_1)
sample_already_filled_2 = Sample.load_from_dir(sample_path_2)
sample_already_filled_3 = Sample.load_from_dir(sample_path_3)
assert sample_already_filled_1
assert sample_already_filled_2
assert sample_already_filled_3

def test__init__unknown_directory(self, current_directory):
def test_load_from_dir_unknown_directory(self, current_directory):
sample_path = current_directory / "dataset" / "samples" / "sample_000000298"
with pytest.raises(FileNotFoundError):
Sample(path=sample_path)
Sample.load_from_dir(sample_path)

def test__init__file_provided(self, current_directory):
def test_load_from_dir_file_provided(self, current_directory):
sample_path = (
current_directory
/ "dataset_cgns"
Expand All @@ -211,13 +211,13 @@ def test__init__file_provided(self, current_directory):
/ "mesh_000000000.cgns"
)
with pytest.raises(FileExistsError):
Sample(path=sample_path)
Sample.load_from_dir(sample_path)

def test__init__path(self, current_directory):
def test_load_from_dir_path(self, current_directory):
sample_path = (
current_directory / "dataset_cgns" / "data" / "test" / "sample_000000000"
)
Sample(path=sample_path)
Sample.load_from_dir(sample_path)

def test_copy(self, sample_with_tree_and_scalar):
sample_with_tree_and_scalar.copy()
Expand Down
Loading