Skip to content

Commit b493457

Browse files
committed
plaidcheck
1 parent 800b179 commit b493457

2 files changed

Lines changed: 87 additions & 148 deletions

File tree

src/plaid/cli/plaidcheck.py

Lines changed: 85 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
import numpy as np
1111

1212
from plaid.storage import init_from_disk
13+
from plaid.storage.registry import available_backends
1314
from plaid.storage.common.reader import (
1415
load_infos_from_disk,
1516
load_metadata_from_disk,
1617
load_problem_definitions_from_disk,
1718
)
18-
19+
from plaid.constants import (
20+
CGNS_ELEMENT_NAMES,
21+
CGNS_FIELD_LOCATIONS,
22+
)
1923

2024
@dataclass
2125
class CheckMessage:
@@ -214,14 +218,26 @@ def compute_checksum(sample):
214218
def check_dataset(
215219
path: Path,
216220
splits: Optional[list[str]] = None,
217-
max_samples: Optional[int] = None,
218221
) -> CheckReport:
219222
"""Run integrity checks on a local PLAID dataset.
220223
224+
Algorithm overview:
225+
1. Validate the required on-disk PLAID layout.
226+
2. Load infos, metadata, and split-specific dataset/converter objects.
227+
3. Validate top-level declarations from ``infos.yaml`` (backend, sample counts).
228+
4. Resolve requested splits and report unknown ones.
229+
5. For each checked split:
230+
- verify split-level schema/value consistency,
231+
- validate sample IDs,
232+
- convert each sample and validate values,
233+
- compute checksums for duplicate-data detection,
234+
- build scalar signatures to detect duplicated DOE-like inputs.
235+
6. Validate optional problem definitions against available features/splits/indices.
236+
7. Emit an ``OK`` info message when no issue is detected.
237+
221238
Args:
222239
path: Dataset directory.
223240
splits: Optional selected split names.
224-
max_samples: Optional cap for per-sample checks.
225241
226242
Returns:
227243
A populated :class:`CheckReport`.
@@ -276,7 +292,7 @@ def check_dataset(
276292
target_splits = set(splits) if splits else dataset_splits
277293
unknown_splits = target_splits - dataset_splits
278294
for split in sorted(unknown_splits):
279-
report.add("error", "UNKNOWN_SPLIT", split, "Split not found in dataset")
295+
report.add("error", "UNKNOWN_SPLIT", split, f"Split not found in dataset, available are {' and '.join('\"'+x+'\"' for x in dataset_splits)}")
280296
target_splits = target_splits & dataset_splits
281297

282298
checksum_report = {}
@@ -302,6 +318,7 @@ def check_dataset(
302318
split,
303319
"No constant schema for split",
304320
)
321+
305322
if split not in flat_cst:
306323
report.add(
307324
"error",
@@ -310,88 +327,11 @@ def check_dataset(
310327
"No constant values for split",
311328
)
312329

313-
ids = getattr(dataset, "ids", None)
314-
if ids is not None and isinstance(expected_n, int):
315-
id_list = [int(i) for i in ids]
316-
duplicates = len(id_list) - len(set(id_list))
317-
if duplicates > 0:
318-
report.add(
319-
"warning",
320-
"DUPLICATED_SAMPLE_IDS",
321-
split,
322-
f"Found {duplicates} duplicated sample id(s)",
323-
)
324-
325-
expected_ids = set(range(expected_n))
326-
missing_ids = sorted(expected_ids - set(id_list))
327-
extra_ids = sorted(set(id_list) - expected_ids)
328-
if missing_ids:
329-
report.add(
330-
"warning",
331-
"MISSING_SAMPLE_IDS",
332-
split,
333-
f"Missing sample ids (first 10): {missing_ids[:10]}",
334-
)
335-
if extra_ids:
336-
report.add(
337-
"warning",
338-
"UNEXPECTED_SAMPLE_IDS",
339-
split,
340-
f"Unexpected sample ids (first 10): {extra_ids[:10]}",
341-
)
342-
343-
# Deep-check a bounded number of samples to validate content and detect
344-
# duplicated scalar DOE signatures without necessarily scanning all data.
345-
n_to_check = actual_n if max_samples is None else min(max_samples, actual_n)
346-
doe_signatures: dict[tuple[Any, ...], int] = {}
347-
for idx in range(n_to_check):
348-
# CGNS-backed datasets expose sample trees directly through
349-
# `to_plaid`, so validate global scalar values from the tree API.
350-
if converter.backend == "cgns":
351-
try:
352-
sample = converter.to_plaid(dataset, idx)
353-
except Exception as exc:
354-
report.add(
355-
"error",
356-
"SAMPLE_CONVERSION_ERROR",
357-
f"{split}[{idx}]",
358-
str(exc),
359-
)
360-
continue
361330

362-
# Track whole-sample checksums to detect duplicated data across
363-
# all checked splits after the per-split loop completes.
364-
sample_checksum = compute_checksum(sample)
365-
checksum_report[(idx, split)] = sample_checksum
366-
367-
scalar_signature: list[Any] = []
368-
for global_name in sample.get_global_names():
369-
value = sample.get_feature_by_path(global_name)
370-
if _is_branch_without_data(sample, global_name):
371-
continue
372-
issue = _check_numeric_content(value)
373-
if issue is not None:
374-
report.add(
375-
"warning",
376-
"INVALID_DATA_VALUE A",
377-
f"{split}[{idx}] global/{global_name}",
378-
issue,
379-
)
380-
arr = np.asarray(value)
381-
if arr.size == 1 and np.issubdtype(arr.dtype, np.number):
382-
scalar_signature.append(
383-
("global", global_name, float(arr.ravel()[0]))
384-
)
385-
386-
sig_key = tuple(sorted(scalar_signature))
387-
if sig_key:
388-
doe_signatures[sig_key] = doe_signatures.get(sig_key, 0) + 1
389-
continue
390-
391-
# Non-CGNS backends are normalized to dictionaries so validation can
392-
# iterate over time keys and feature maps in a backend-neutral way.
331+
# Deep-check to validate content and detect non valide data in fields (nan inf)
332+
for idx in range(actual_n):
393333
try:
394-
sample_dict = converter.to_dict(dataset, idx)
334+
sample = converter.to_plaid(dataset, idx)
395335
except Exception as exc:
396336
report.add(
397337
"error",
@@ -401,55 +341,60 @@ def check_dataset(
401341
)
402342
continue
403343

404-
# Track whole-sample checksums for duplicate data detection.
405-
sample_checksum = compute_checksum(sample_dict)
344+
# Track whole-sample checksums to detect duplicated data across
345+
# all checked splits after the per-split loop completes.
346+
sample_checksum = compute_checksum(sample)
406347
checksum_report[(idx, split)] = sample_checksum
407348

408-
# Validate each materialized feature value while ignoring branch
409-
# entries that exist only to group child feature paths.
410-
for time_key, feat_map in sample_dict.items():
411-
for feature_name, value in feat_map.items():
412-
if _is_branch_without_data_in_mapping(
413-
feature_name, value, feat_map
414-
):
415-
continue
416-
417-
issue = _check_numeric_content(value)
418-
419-
if issue is not None:
420-
report.add(
421-
"warning",
422-
"INVALID_DATA_VALUE B",
423-
f"{split}[{idx}]/{time_key} {feature_name}",
424-
issue,
425-
)
426-
427-
# Build a scalar signature for duplicate DOE input detection within
428-
# this split. Only scalar numeric values are included.
429-
scalar_signature: list[Any] = []
430-
for time_key, feat_map in sample_dict.items():
431-
for feature_name, value in feat_map.items():
432-
arr = np.asarray(value)
433-
if arr.size == 1 and np.issubdtype(arr.dtype, np.number):
434-
scalar_signature.append(
435-
(str(time_key), feature_name, float(arr.ravel()[0]))
436-
)
437-
438-
sig_key = tuple(sorted(scalar_signature))
439-
if sig_key:
440-
doe_signatures[sig_key] = doe_signatures.get(sig_key, 0) + 1
441-
442-
repeated = sum(1 for count in doe_signatures.values() if count > 1)
443-
if repeated > 0:
444-
report.add(
445-
"warning",
446-
"DUPLICATED_DOE_INPUTS",
447-
split,
448-
f"Detected {repeated} duplicated scalar signature(s) in checked samples",
449-
)
349+
for global_name in sample.get_global_names():
350+
global_path = "Global/" + global_name
351+
value = sample.get_feature_by_path(global_path)
352+
353+
if _is_branch_without_data(sample, global_path):
354+
continue
355+
356+
issue = _check_numeric_content(value)
357+
if issue is not None:
358+
report.add(
359+
"warning",
360+
"INVALID_DATA_VALUE A",
361+
f"{split}[{idx}] global/{global_name}",
362+
issue,
363+
)
364+
365+
for time in sample.get_all_time_values():
366+
local_bases = sample.get_base_names(time=time)
367+
for base in local_bases:
368+
zone_names = sample.features.get_zone_names(
369+
base=base, time=time
370+
)
371+
for zone in zone_names:
372+
for location in CGNS_FIELD_LOCATIONS:
373+
field_names = sample.get_field_names(
374+
location=location,
375+
zone=zone,
376+
base=base,
377+
time=time,
378+
)
379+
380+
for f_name in field_names:
381+
field_value = sample.get_field(f_name,
382+
location= location,
383+
zone=zone,
384+
base=base,
385+
time=time)
386+
issue = _check_numeric_content(field_value)
387+
if issue is not None:
388+
report.add(
389+
"warning",
390+
"INVALID_DATA_VALUE A",
391+
f"{split}[{idx}][{time}] {base}/{zone}/{location}/{f_name}",
392+
issue,
393+
)
450394

451395
# Compare checksums from every checked sample to flag identical sample data.
452-
if len(checksum_report) != len(np.unique(checksum_report.values())):
396+
checksum_values = list(checksum_report.values())
397+
if len(checksum_report) != len(np.unique(checksum_values)):
453398
k = list(checksum_report.keys())
454399
v = list(checksum_report.values())
455400
uni, cou = np.unique(v, return_counts=True)
@@ -464,7 +409,6 @@ def check_dataset(
464409
str(duplicated),
465410
"duplicated sample",
466411
)
467-
468412
# If problem definitions are present, verify that their feature references,
469413
# split names, and sample indices are compatible with the dataset.
470414
pb_def_dir = path / "problem_definitions"
@@ -478,7 +422,7 @@ def check_dataset(
478422
"problem_definitions",
479423
str(exc),
480424
)
481-
pb_defs = {}
425+
return report
482426

483427
all_features = set(variable_schema.keys())
484428
for split_cst in flat_cst.values():
@@ -493,13 +437,6 @@ def check_dataset(
493437
f"problem_definitions/{pb_name}",
494438
f"Unknown input feature: {feat}",
495439
)
496-
if "GlobalConvergenceHistory" not in feat and "Global" not in feat:
497-
report.add(
498-
"warning",
499-
"DOE_INPUT_NOT_SCALAR",
500-
f"problem_definitions/{pb_name}",
501-
f"Input feature may not be scalar/global for DOE: {feat}",
502-
)
503440

504441
for feat in pb_def.output_features:
505442
if feat not in all_features:
@@ -514,6 +451,15 @@ def check_dataset(
514451
split_dict = getattr(pb_def, split_dict_name)
515452
if split_dict is None:
516453
continue
454+
#split_dict must have only one elements
455+
if len(split_dict) > 1 :
456+
report.add(
457+
"error",
458+
"PB_DEF_SPLIT",
459+
f"problem_definitions/{pb_name}",
460+
f"{split_dict_name} has more than 1 split: {list(split_dict.keys())}",
461+
)
462+
continue
517463
split_name = next(iter(split_dict.keys()))
518464
split_ids = next(iter(split_dict.values()))
519465
if split_name not in dataset_splits:
@@ -564,12 +510,6 @@ def _build_parser() -> argparse.ArgumentParser:
564510
default=None,
565511
help="Split to check (can be provided multiple times)",
566512
)
567-
parser.add_argument(
568-
"--max-samples",
569-
type=int,
570-
default=None,
571-
help="Maximum number of samples per split for deep checks",
572-
)
573513
parser.add_argument(
574514
"--json",
575515
action="store_true",
@@ -597,8 +537,7 @@ def main(argv: Optional[list[str]] = None) -> int:
597537

598538
report = check_dataset(
599539
path=args.path,
600-
splits=args.split,
601-
max_samples=args.max_samples,
540+
splits=args.split
602541
)
603542

604543
if args.json:

tests/cli/test_plaidcheck.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_check_dataset_valid_reference(tmp_path: Path) -> None:
3636
"""Reference dataset should pass with no errors."""
3737
dataset_path = _copy_reference_dataset(tmp_path)
3838

39-
report = check_dataset(dataset_path, max_samples=2)
39+
report = check_dataset(dataset_path)
4040

4141
assert not report.has_errors()
4242

@@ -69,7 +69,7 @@ def test_main_json_output_and_exit_code(tmp_path: Path, capsys) -> None:
6969
"""CLI should output JSON and return expected status code."""
7070
dataset_path = _copy_reference_dataset(tmp_path)
7171

72-
code = main([str(dataset_path), "--json", "--max-samples", "1"])
72+
code = main([str(dataset_path), "--json", ])
7373
out = capsys.readouterr().out
7474
payload = json.loads(out)
7575

0 commit comments

Comments
 (0)