Skip to content

Commit 1dae67c

Browse files
authored
Merge pull request #128 from ocean-uhh/103-feat-new-layer-conversiontransformation-for-internal-pipeline
feat: New layer conversion/transformation for internal pipeline
2 parents 2c76fb8 + ceb8f68 commit 1dae67c

22 files changed

Lines changed: 948 additions & 17 deletions

seasenselib/cli/commands/data_commands.py

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,14 @@ def _build_stage_kwargs(args):
144144

145145
# Check for --pipeline-skip-stages argument
146146
elif getattr(args, 'pipeline_skip_stages', None):
147-
# Build a list of all steps except the skipped ones
148147
skip = [s.strip() for s in args.pipeline_skip_stages.split(',')]
149148
try:
150-
from ...pipeline.registry import StageRegistry
151149
from ...pipeline.config import PipelineConfig
152-
registry = StageRegistry.get_instance()
153-
all_steps = registry.list_stages()
154-
selected_steps = [s for s in all_steps if s not in skip]
155-
# Build pipeline config with selected steps
156-
config = PipelineConfig()
157-
for step_name in selected_steps:
158-
config.add_stage(step_name)
150+
config = PipelineConfig.from_resource("default")
151+
config.pipeline = [
152+
stage for stage in config.pipeline
153+
if stage.name not in skip
154+
]
159155
stage_kwargs['pipeline_config'] = config
160156
except Exception:
161157
# If registry fails, just pass the skip info and let read() handle it
@@ -272,6 +268,7 @@ def _write_processing_protocol(
272268
protocol["handlers_applied"] = metadata.get("handlers_applied")
273269
protocol["variable_mappings"] = metadata.get("variable_mappings")
274270
protocol["derived_parameters"] = metadata.get("derived_parameters")
271+
protocol["transformations"] = metadata.get("transformations")
275272
unit_conversions = _format_unit_conversions(metadata.get("unit_conversions"))
276273
if unit_conversions is not None:
277274
protocol["unit_conversions"] = unit_conversions
@@ -296,6 +293,112 @@ def _write_processing_protocol(
296293
json.dump(protocol, f, indent=2, sort_keys=True)
297294

298295

296+
def _format_example_value(value) -> str:
297+
"""Format one scalar preview value without verbose dtype wrappers."""
298+
try:
299+
import numpy as np
300+
except Exception:
301+
np = None
302+
303+
if np is not None:
304+
if isinstance(value, np.datetime64):
305+
return np.datetime_as_string(value)
306+
if isinstance(value, np.timedelta64):
307+
return str(value)
308+
if isinstance(value, np.generic):
309+
value = value.item()
310+
311+
if isinstance(value, bytes):
312+
try:
313+
return value.decode("utf-8")
314+
except UnicodeDecodeError:
315+
return value.hex()
316+
if isinstance(value, float):
317+
return f"{value:.6g}"
318+
return str(value)
319+
320+
321+
def _example_selector(array, max_values: int) -> tuple[dict, str, int]:
322+
"""Return a small indexer and sampled dimension for an array preview."""
323+
if not array.dims:
324+
return {}, "", 1
325+
326+
sample_dim = "time" if "time" in array.dims else array.dims[-1]
327+
sample_count = min(int(array.sizes[sample_dim]), max_values)
328+
indexer = {}
329+
for dim in array.dims:
330+
if dim == sample_dim:
331+
indexer[dim] = slice(0, sample_count)
332+
else:
333+
indexer[dim] = 0
334+
return indexer, sample_dim, sample_count
335+
336+
337+
def _format_example_selector(array, indexer: dict, sample_dim: str) -> str:
338+
"""Describe the small indexer used for an example preview."""
339+
if not array.dims:
340+
return ""
341+
342+
parts = []
343+
for dim in array.dims:
344+
selector = indexer[dim]
345+
if dim == sample_dim:
346+
parts.append(f"{dim}=0:{selector.stop}")
347+
continue
348+
349+
label = "0"
350+
if dim in array.coords and array.coords[dim].size:
351+
try:
352+
label = _format_example_value(array.coords[dim].isel({dim: 0}).values)
353+
except Exception:
354+
label = "0"
355+
parts.append(f"{dim}={label}")
356+
return ", ".join(parts)
357+
358+
359+
def _format_array_example(name: str, array, max_values: int = 5) -> str:
360+
"""Format a bounded example line for one xarray coordinate or variable."""
361+
indexer, sample_dim, _sample_count = _example_selector(array, max_values)
362+
subset = array.isel(indexer) if indexer else array
363+
364+
try:
365+
values = subset.values
366+
except Exception as exc:
367+
return f" {name}: <failed to read preview: {exc}>"
368+
369+
try:
370+
import numpy as np
371+
flat = np.asarray(values).reshape(-1)
372+
except Exception:
373+
flat = [values]
374+
375+
preview = ", ".join(
376+
_format_example_value(value)
377+
for value in list(flat)[:max_values]
378+
)
379+
selector_text = _format_example_selector(array, indexer, sample_dim)
380+
selector_text = f" [{selector_text}]" if selector_text else ""
381+
return (
382+
f" {name}{selector_text}: dims={array.dims}, "
383+
f"shape={tuple(array.shape)}, values=[{preview}]"
384+
)
385+
386+
387+
def _print_dataset_example(dataset, max_values: int = 5) -> None:
388+
"""Print bounded examples without materializing the whole Dataset."""
389+
print(f"Example values (up to {max_values} values per variable):")
390+
391+
if dataset.coords:
392+
print("\nCoordinates:")
393+
for name, array in dataset.coords.items():
394+
print(_format_array_example(name, array, max_values=max_values))
395+
396+
if dataset.data_vars:
397+
print("\nData variables:")
398+
for name, array in dataset.data_vars.items():
399+
print(_format_array_example(name, array, max_values=max_values))
400+
401+
299402
class ConvertCommand(BaseCommand):
300403
"""Handle file conversion with lazy loading."""
301404

@@ -428,8 +531,7 @@ def execute(self, args: argparse.Namespace) -> CommandResult:
428531
elif args.schema == 'info':
429532
data.info()
430533
elif args.schema == 'example':
431-
df = data.to_dataframe()
432-
print(df.head())
534+
_print_dataset_example(data)
433535

434536
# Write processing protocol if requested
435537
if want_protocol:

seasenselib/config/pipeline/default.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
]
2121
}
2222
},
23+
{
24+
"name": "transformation",
25+
"config": {
26+
"handlers": [
27+
"reader"
28+
]
29+
}
30+
},
2331
{
2432
"name": "derivation",
2533
"config": {

seasenselib/config/pipeline/full.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
]
2222
}
2323
},
24+
{
25+
"name": "transformation",
26+
"config": {
27+
"handlers": [
28+
"reader"
29+
]
30+
}
31+
},
2432
{
2533
"name": "derivation",
2634
"config": {

seasenselib/pipeline/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from .pipeline import Pipeline
99
from .config import PipelineConfig, StageConfig
1010
from .registry import StageRegistry
11+
from .interfaces import ITransformation, TransformationRecord
12+
from .transformation import TransformationStage
1113
from .factory import (
1214
default_pipeline,
1315
minimal_pipeline,
@@ -22,6 +24,9 @@
2224
"PipelineConfig",
2325
"StageConfig",
2426
"StageRegistry",
27+
"ITransformation",
28+
"TransformationRecord",
29+
"TransformationStage",
2530
"default_pipeline",
2631
"minimal_pipeline",
2732
"create_pipeline",

seasenselib/pipeline/factory.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ def default_pipeline() -> Pipeline:
1919
The default pipeline includes (in order):
2020
1. Mapping
2121
2. Unit Handling
22-
3. Derivation
23-
4. Metadata Extraction
24-
5. Metadata Enrichment
25-
6. Validation
26-
7. Finalization
22+
3. Transformation
23+
4. Derivation
24+
5. Metadata Extraction
25+
6. Metadata Enrichment
26+
7. Validation
27+
8. Finalization
2728
"""
2829
config = PipelineConfig.from_resource("default")
2930
return create_pipeline(config=config)

seasenselib/pipeline/finalization/handlers/global_attributes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,9 @@ def _build_history_entry(self, timestamp: str, context: StageContext) -> str:
149149
if 'derived_parameters' in context.metadata:
150150
derived = ', '.join(context.metadata['derived_parameters'])
151151
parts.append(f"Derived: {derived}")
152+
153+
if 'transformations' in context.metadata:
154+
count = len(context.metadata['transformations'])
155+
parts.append(f"Transformed: {count} step(s)")
152156

153157
return '; '.join(parts)

seasenselib/pipeline/finalization/handlers/processor_metadata.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dataclasses import dataclass
1010
from datetime import datetime, timezone
1111
from typing import Dict, Any
12+
import json
1213
import logging
1314
import platform
1415

@@ -72,6 +73,14 @@ def set_attr(key: str, value: Any) -> None:
7273
if self.include_os:
7374
set_attr("processor_os", f"{platform.system()} {platform.release()}")
7475

76+
transformations = meta.get("transformations")
77+
if transformations:
78+
set_attr(
79+
"processor_transformations",
80+
json.dumps(transformations, ensure_ascii=False, default=str),
81+
)
82+
set_attr("processor_transformations_count", len(transformations))
83+
7584
context.dataset = ds
7685
logger.debug("Added processor metadata attributes")
7786
return context

seasenselib/pipeline/handler_catalog.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
HANDLER_GROUP_DERIVATIONS,
2020
HANDLER_GROUP_METADATA_EXTRACTORS,
2121
HANDLER_GROUP_CONVENTIONS,
22+
HANDLER_GROUP_TRANSFORMATIONS,
2223
HANDLER_GROUP_VALIDATORS,
2324
)
2425
from .interfaces import (
2526
IMappingStrategy,
2627
IDerivation,
2728
IMetadataExtractor,
2829
IConvention,
30+
ITransformation,
2931
IValidator,
3032
)
3133

@@ -56,6 +58,9 @@
5658
"normalize": "seasenselib.pipeline.unit_handling.handlers.unit_normalizer.UnitNormalizer",
5759
"convert": "seasenselib.pipeline.unit_handling.handlers.unit_converter.UnitConverter",
5860
},
61+
"transformation": {
62+
"reader": "seasenselib.pipeline.transformation.handlers.reader_transformations.ReaderTransformations",
63+
},
5964
"metadata_enrichment": {
6065
"cf": "seasenselib.pipeline.metadata_enrichment.handlers.cf_convention.CFConvention",
6166
"acdd": "seasenselib.pipeline.metadata_enrichment.handlers.acdd_convention.ACDDConvention",
@@ -81,6 +86,7 @@
8186
"derivation": (HANDLER_GROUP_DERIVATIONS, IDerivation),
8287
"metadata_extraction": (HANDLER_GROUP_METADATA_EXTRACTORS, IMetadataExtractor),
8388
"metadata_enrichment": (HANDLER_GROUP_CONVENTIONS, IConvention),
89+
"transformation": (HANDLER_GROUP_TRANSFORMATIONS, ITransformation),
8490
"validation": (HANDLER_GROUP_VALIDATORS, IValidator),
8591
}
8692

seasenselib/pipeline/handler_registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
HANDLER_GROUP_DERIVATIONS = "seasenselib.pipeline.derivations"
1818
HANDLER_GROUP_METADATA_EXTRACTORS = "seasenselib.pipeline.metadata_extractors"
1919
HANDLER_GROUP_CONVENTIONS = "seasenselib.pipeline.conventions"
20+
HANDLER_GROUP_TRANSFORMATIONS = "seasenselib.pipeline.transformations"
2021
HANDLER_GROUP_VALIDATORS = "seasenselib.pipeline.validators"
2122

2223

@@ -85,5 +86,6 @@ def get(cls, group: str, base_class: Type[T]) -> Dict[str, Type[T]]:
8586
"HANDLER_GROUP_DERIVATIONS",
8687
"HANDLER_GROUP_METADATA_EXTRACTORS",
8788
"HANDLER_GROUP_CONVENTIONS",
89+
"HANDLER_GROUP_TRANSFORMATIONS",
8890
"HANDLER_GROUP_VALIDATORS",
8991
]

0 commit comments

Comments
 (0)