scistack-db 0.1.9.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. scistack_db-0.1.9.dev0/.gitignore +16 -0
  2. scistack_db-0.1.9.dev0/PKG-INFO +128 -0
  3. scistack_db-0.1.9.dev0/README.md +98 -0
  4. scistack_db-0.1.9.dev0/pyproject.toml +71 -0
  5. scistack_db-0.1.9.dev0/src/scidb/__init__.py +144 -0
  6. scistack_db-0.1.9.dev0/src/scidb/across_variants.py +97 -0
  7. scistack_db-0.1.9.dev0/src/scidb/artifact_stamp.py +301 -0
  8. scistack_db-0.1.9.dev0/src/scidb/colname.py +57 -0
  9. scistack_db-0.1.9.dev0/src/scidb/column_selection.py +166 -0
  10. scistack_db-0.1.9.dev0/src/scidb/constant.py +295 -0
  11. scistack_db-0.1.9.dev0/src/scidb/csv_export.py +272 -0
  12. scistack_db-0.1.9.dev0/src/scidb/database.py +4345 -0
  13. scistack_db-0.1.9.dev0/src/scidb/discover.py +449 -0
  14. scistack_db-0.1.9.dev0/src/scidb/each_of.py +26 -0
  15. scistack_db-0.1.9.dev0/src/scidb/exceptions.py +81 -0
  16. scistack_db-0.1.9.dev0/src/scidb/exclusions.py +405 -0
  17. scistack_db-0.1.9.dev0/src/scidb/filters.py +1225 -0
  18. scistack_db-0.1.9.dev0/src/scidb/fixed.py +55 -0
  19. scistack_db-0.1.9.dev0/src/scidb/foreach.py +5009 -0
  20. scistack_db-0.1.9.dev0/src/scidb/foreach_config.py +197 -0
  21. scistack_db-0.1.9.dev0/src/scidb/hashing.py +9 -0
  22. scistack_db-0.1.9.dev0/src/scidb/inspect/__init__.py +63 -0
  23. scistack_db-0.1.9.dev0/src/scidb/inspect/api.py +1088 -0
  24. scistack_db-0.1.9.dev0/src/scidb/inspect/cli.py +819 -0
  25. scistack_db-0.1.9.dev0/src/scidb/inspect/graph.py +422 -0
  26. scistack_db-0.1.9.dev0/src/scidb/inspect/mutate.py +157 -0
  27. scistack_db-0.1.9.dev0/src/scidb/inspect/pick.py +81 -0
  28. scistack_db-0.1.9.dev0/src/scidb/inspect/render.py +732 -0
  29. scistack_db-0.1.9.dev0/src/scidb/inspect/report.py +559 -0
  30. scistack_db-0.1.9.dev0/src/scidb/lineage_save.py +30 -0
  31. scistack_db-0.1.9.dev0/src/scidb/log.py +14 -0
  32. scistack_db-0.1.9.dev0/src/scidb/merge.py +94 -0
  33. scistack_db-0.1.9.dev0/src/scidb/paths.py +9 -0
  34. scistack_db-0.1.9.dev0/src/scidb/pipeline.py +1301 -0
  35. scistack_db-0.1.9.dev0/src/scidb/provenance.py +412 -0
  36. scistack_db-0.1.9.dev0/src/scidb/provenance_query.py +1102 -0
  37. scistack_db-0.1.9.dev0/src/scidb/provenance_save.py +737 -0
  38. scistack_db-0.1.9.dev0/src/scidb/query.py +132 -0
  39. scistack_db-0.1.9.dev0/src/scidb/state.py +659 -0
  40. scistack_db-0.1.9.dev0/src/scidb/variable.py +815 -0
  41. scistack_db-0.1.9.dev0/src/scidb/variables/__init__.py +1 -0
  42. scistack_db-0.1.9.dev0/src/scidb/variant.py +153 -0
  43. scistack_db-0.1.9.dev0/src/scidb.egg-info/PKG-INFO +98 -0
  44. scistack_db-0.1.9.dev0/src/scidb.egg-info/SOURCES.txt +22 -0
  45. scistack_db-0.1.9.dev0/src/scidb.egg-info/dependency_links.txt +1 -0
  46. scistack_db-0.1.9.dev0/src/scidb.egg-info/requires.txt +10 -0
  47. scistack_db-0.1.9.dev0/src/scidb.egg-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ .venv
2
+ .DS_Store
3
+ */*/__pycache__
4
+ others_projects/sciforge
5
+ scistack-gui/extension/node_modules/
6
+ scistack-gui/frontend/node_modules/__pycache__/
7
+ *.pyc
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+
12
+ # Generated database artifacts (DuckDB data/lineage + write-ahead logs)
13
+ *.duckdb
14
+ *.duckdb.wal
15
+ *.wal
16
+ scistack-gui/frontend/node_modules/
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: scistack-db
3
+ Version: 0.1.9.dev0
4
+ Summary: A lightweight database framework for scientific computing with versioning and provenance tracking
5
+ License: MIT
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Science/Research
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: duckdb>=0.9.0
18
+ Requires-Dist: numpy>=1.20.0
19
+ Requires-Dist: pandas>=1.3.0
20
+ Requires-Dist: scicanonicalhash>=0.1.0
21
+ Requires-Dist: sciduckdb>=0.1.0
22
+ Requires-Dist: scifor>=0.1.0
23
+ Requires-Dist: scipathgen>=0.1.0
24
+ Requires-Dist: scistacklog>=0.1.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: matplotlib; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # SciDB
32
+
33
+ Database operations layer for SciStack. Provides abstractions for defining typed variables, configuring the database, and saving/loading data by metadata.
34
+
35
+ ## Named Filters (value-based, reusable)
36
+
37
+ `Filter` objects are first-class values — assign them to a variable and reuse
38
+ them in any `where=` clause, or compose them with `&` / `|` / `~`:
39
+
40
+ ```python
41
+ # Define once (this IS a ColumnFilter — no special wrapper needed)
42
+ clean_gr = GAITRiteLoadedCycle["StepLengths_GR"] != 0
43
+
44
+ # Use anywhere
45
+ for_each(mean_change_from_reference,
46
+ inputs={"baseline": Fixed(GAITRiteLoadedCycle["StepLengths_GR"], session="BL"),
47
+ "value": GAITRiteLoadedCycle["StepLengths_GR"]},
48
+ outputs=[DeltaStepLength],
49
+ where=clean_gr & (UAStartFoot() == "A"))
50
+
51
+ # Compose named filters
52
+ clean_and_unilateral = clean_gr & (UAStartFoot() == "U")
53
+ for_each(..., where=clean_and_unilateral)
54
+ ```
55
+
56
+ MATLAB equivalent:
57
+
58
+ ```matlab
59
+ clean_gr = GAITRiteLoadedCycle("StepLengths_GR") ~= 0;
60
+ scidb.for_each(@meanChangeFromReference, ..., where=clean_gr & (UAStartFoot() == "A"));
61
+ ```
62
+
63
+ ## Permanent Schema-Level Exclusions
64
+
65
+ For data that should be excluded from **every** analysis (e.g., a failed
66
+ recording session), use the exclusion registry instead of per-call filters.
67
+
68
+ ```python
69
+ # Mark a specific trial as excluded (persisted in the database)
70
+ scidb.exclude_schema(subject=1, trial=2,
71
+ reason="equipment malfunction during recording")
72
+
73
+ # Exclude an entire subject (trial omitted = wildcard)
74
+ scidb.exclude_schema(subject=3, reason="participant withdrew")
75
+
76
+ # Inspect currently-excluded combinations
77
+ exclusions_df = scidb.list_exclusions()
78
+
79
+ # Re-include (logged; the original exclusion row is preserved)
80
+ scidb.include_schema(subject=1, trial=2,
81
+ reason="re-reviewed video, recording was valid")
82
+ ```
83
+
84
+ MATLAB equivalent:
85
+
86
+ ```matlab
87
+ scidb.exclude_schema("equipment malfunction", 'subject', 1, 'trial', 2)
88
+ scidb.include_schema("re-reviewed, recording was valid", 'subject', 1, 'trial', 2)
89
+ tbl = scidb.list_exclusions()
90
+ ```
91
+
92
+ Exclusions are applied automatically by `for_each` before the iteration loop.
93
+ The full exclusion table is hashed and stored in `version_keys`
94
+ (`__schema_overrides_hash`) so that adding or removing an exclusion
95
+ invalidates cached results.
96
+
97
+ ```python
98
+ from scidb import configure_database, BaseVariable
99
+ import numpy as np
100
+
101
+ db = configure_database("experiment.duckdb", ["subject", "session"])
102
+
103
+ class RawSignal(BaseVariable):
104
+ schema_version = 1
105
+
106
+ RawSignal.save(np.array([1, 2, 3]), subject=1, session="A")
107
+ (raw,) = RawSignal.load(subject=1, session="A") # generator; unpack for single result
108
+ all_versions = list(RawSignal.load(subject=1, session="A", version="all"))
109
+ ```
110
+ ## Logging
111
+
112
+ Every scidb operation logs through the shared [`scistacklog`](../scistacklog/README.md) facade. `configure_database()` points the file sink at `scidb.log` next to the database file and writes a run-context header (package versions, Python version, pid), so each log file is self-describing.
113
+
114
+ Two sinks with independent levels, both defaulting to INFO:
115
+
116
+ - **console** (stderr): the pipeline narrative — for_each banner, periodic progress, run summary with failure reasons, `[timing]` summaries.
117
+ - **file** (`scidb.log`): the same narrative, with date + millisecond timestamps and the originating layer (`[scidb]`, `[scifor]`, `[matlab]`, …) on every line; one record per line.
118
+
119
+ For debugging, raise a sink to DEBUG to capture the full execution trace (named internal steps with durations, per-iteration `[run]`/`[skip]` lines, per-phase `[timing]` tables, column/dtype dumps):
120
+
121
+ ```python
122
+ from scidb import Log
123
+
124
+ Log.set_level("DEBUG", sink="file") # full detail in scidb.log only
125
+ Log.set_level("DEBUG") # both sinks
126
+ ```
127
+
128
+ or set the `SCIDB_LOG_LEVEL` environment variable, or pass `-v` to the `scidb` CLI.
@@ -0,0 +1,98 @@
1
+ # SciDB
2
+
3
+ Database operations layer for SciStack. Provides abstractions for defining typed variables, configuring the database, and saving/loading data by metadata.
4
+
5
+ ## Named Filters (value-based, reusable)
6
+
7
+ `Filter` objects are first-class values — assign them to a variable and reuse
8
+ them in any `where=` clause, or compose them with `&` / `|` / `~`:
9
+
10
+ ```python
11
+ # Define once (this IS a ColumnFilter — no special wrapper needed)
12
+ clean_gr = GAITRiteLoadedCycle["StepLengths_GR"] != 0
13
+
14
+ # Use anywhere
15
+ for_each(mean_change_from_reference,
16
+ inputs={"baseline": Fixed(GAITRiteLoadedCycle["StepLengths_GR"], session="BL"),
17
+ "value": GAITRiteLoadedCycle["StepLengths_GR"]},
18
+ outputs=[DeltaStepLength],
19
+ where=clean_gr & (UAStartFoot() == "A"))
20
+
21
+ # Compose named filters
22
+ clean_and_unilateral = clean_gr & (UAStartFoot() == "U")
23
+ for_each(..., where=clean_and_unilateral)
24
+ ```
25
+
26
+ MATLAB equivalent:
27
+
28
+ ```matlab
29
+ clean_gr = GAITRiteLoadedCycle("StepLengths_GR") ~= 0;
30
+ scidb.for_each(@meanChangeFromReference, ..., where=clean_gr & (UAStartFoot() == "A"));
31
+ ```
32
+
33
+ ## Permanent Schema-Level Exclusions
34
+
35
+ For data that should be excluded from **every** analysis (e.g., a failed
36
+ recording session), use the exclusion registry instead of per-call filters.
37
+
38
+ ```python
39
+ # Mark a specific trial as excluded (persisted in the database)
40
+ scidb.exclude_schema(subject=1, trial=2,
41
+ reason="equipment malfunction during recording")
42
+
43
+ # Exclude an entire subject (trial omitted = wildcard)
44
+ scidb.exclude_schema(subject=3, reason="participant withdrew")
45
+
46
+ # Inspect currently-excluded combinations
47
+ exclusions_df = scidb.list_exclusions()
48
+
49
+ # Re-include (logged; the original exclusion row is preserved)
50
+ scidb.include_schema(subject=1, trial=2,
51
+ reason="re-reviewed video, recording was valid")
52
+ ```
53
+
54
+ MATLAB equivalent:
55
+
56
+ ```matlab
57
+ scidb.exclude_schema("equipment malfunction", 'subject', 1, 'trial', 2)
58
+ scidb.include_schema("re-reviewed, recording was valid", 'subject', 1, 'trial', 2)
59
+ tbl = scidb.list_exclusions()
60
+ ```
61
+
62
+ Exclusions are applied automatically by `for_each` before the iteration loop.
63
+ The full exclusion table is hashed and stored in `version_keys`
64
+ (`__schema_overrides_hash`) so that adding or removing an exclusion
65
+ invalidates cached results.
66
+
67
+ ```python
68
+ from scidb import configure_database, BaseVariable
69
+ import numpy as np
70
+
71
+ db = configure_database("experiment.duckdb", ["subject", "session"])
72
+
73
+ class RawSignal(BaseVariable):
74
+ schema_version = 1
75
+
76
+ RawSignal.save(np.array([1, 2, 3]), subject=1, session="A")
77
+ (raw,) = RawSignal.load(subject=1, session="A") # generator; unpack for single result
78
+ all_versions = list(RawSignal.load(subject=1, session="A", version="all"))
79
+ ```
80
+ ## Logging
81
+
82
+ Every scidb operation logs through the shared [`scistacklog`](../scistacklog/README.md) facade. `configure_database()` points the file sink at `scidb.log` next to the database file and writes a run-context header (package versions, Python version, pid), so each log file is self-describing.
83
+
84
+ Two sinks with independent levels, both defaulting to INFO:
85
+
86
+ - **console** (stderr): the pipeline narrative — for_each banner, periodic progress, run summary with failure reasons, `[timing]` summaries.
87
+ - **file** (`scidb.log`): the same narrative, with date + millisecond timestamps and the originating layer (`[scidb]`, `[scifor]`, `[matlab]`, …) on every line; one record per line.
88
+
89
+ For debugging, raise a sink to DEBUG to capture the full execution trace (named internal steps with durations, per-iteration `[run]`/`[skip]` lines, per-phase `[timing]` tables, column/dtype dumps):
90
+
91
+ ```python
92
+ from scidb import Log
93
+
94
+ Log.set_level("DEBUG", sink="file") # full detail in scidb.log only
95
+ Log.set_level("DEBUG") # both sinks
96
+ ```
97
+
98
+ or set the `SCIDB_LOG_LEVEL` environment variable, or pass `-v` to the `scidb` CLI.
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.version]
6
+ source = "vcs"
7
+ raw-options = { search_parent_directories = true, local_scheme = "no-local-version" }
8
+
9
+ [project]
10
+ name = "scistack-db"
11
+ dynamic = ["version"]
12
+ description = "A lightweight database framework for scientific computing with versioning and provenance tracking"
13
+ readme = "README.md"
14
+ license = {text = "MIT"}
15
+ requires-python = ">=3.9"
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Database",
26
+ "Topic :: Scientific/Engineering",
27
+ ]
28
+ dependencies = [
29
+ "numpy>=1.20.0",
30
+ "pandas>=1.3.0",
31
+ "duckdb>=0.9.0",
32
+ "scipathgen>=0.1.0",
33
+ "scicanonicalhash>=0.1.0",
34
+ "sciduckdb>=0.1.0",
35
+ "scifor>=0.1.0",
36
+ "scistacklog>=0.1.0",
37
+ ]
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=7.0.0",
42
+ "pytest-cov>=4.0.0",
43
+ "matplotlib",
44
+ ]
45
+
46
+ [project.scripts]
47
+ scidb = "scidb.inspect.cli:main"
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["/src"]
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/scidb"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ python_files = ["test_*.py"]
58
+ python_functions = ["test_*"]
59
+ addopts = "-v --tb=short"
60
+
61
+ [tool.coverage.run]
62
+ source = ["src/scidb"]
63
+ branch = true
64
+
65
+ [tool.coverage.report]
66
+ exclude_lines = [
67
+ "pragma: no cover",
68
+ "def __repr__",
69
+ "raise NotImplementedError",
70
+ "if TYPE_CHECKING:",
71
+ ]
@@ -0,0 +1,144 @@
1
+ """
2
+ SciStack: Scientific Data Versioning Framework
3
+
4
+ A lightweight database framework for scientific computing that provides:
5
+ - Type-safe serialization of numpy arrays, DataFrames, and custom types
6
+ - Automatic content-based versioning
7
+ - Flexible metadata-based addressing
8
+ - DuckDB storage for data and lineage (via SciDuck)
9
+
10
+ Example:
11
+ from scidb import configure_database, BaseVariable
12
+ import numpy as np
13
+
14
+ # Setup (single DuckDB file for data + lineage, auto-registers types)
15
+ db = configure_database("experiment.duckdb", ["subject", "session"])
16
+
17
+ class RawSignal(BaseVariable):
18
+ schema_version = 1
19
+
20
+ # Save/load
21
+ RawSignal.save(np.array([1, 2, 3]), subject=1, session="A")
22
+ raw = RawSignal.load(subject=1, session="A")
23
+ """
24
+
25
+ # From scifor (Layer 1)
26
+ from scifor import Col, PathInput, PathOutput, get_schema, set_schema
27
+
28
+ from .across_variants import AcrossVariants
29
+ from .artifact_stamp import read_artifact_stamp, stamp_artifact
30
+ from .colname import ColName
31
+ from .column_selection import ColumnSelection
32
+ from .constant import Constant, constant
33
+ from .database import configure_database, get_database, get_user_id
34
+ from .discover import (
35
+ DiscoveryResult,
36
+ ModuleError,
37
+ ModuleExports,
38
+ PackageResult,
39
+ discover_module,
40
+ scan_package,
41
+ scan_project,
42
+ )
43
+ from .each_of import EachOf
44
+ from .exceptions import (
45
+ AmbiguousParamError,
46
+ AmbiguousVersionError,
47
+ DatabaseLockedError,
48
+ DatabaseNotConfiguredError,
49
+ NotFoundError,
50
+ NotRegisteredError,
51
+ PipelineCycleError,
52
+ ReservedMetadataKeyError,
53
+ SciStackError,
54
+ )
55
+ from .exclusions import exclude_schema, include_schema, list_exclusions
56
+ from .filters import raw_sql, schema_key
57
+ from .fixed import Fixed
58
+
59
+ # Batch execution (Layer 2 — DB-backed, no lineage)
60
+ from .foreach import for_each
61
+ from .foreach_config import ForEachConfig
62
+ from .lineage_save import save
63
+ from .log import Log
64
+ from .merge import Merge
65
+ from .pipeline import Pipeline, PipelineBinding, Step, active_pipeline, scistack
66
+ from .state import (
67
+ check_combo_state,
68
+ check_multiple_nodes_state,
69
+ check_node_state,
70
+ check_pathinput_node_state,
71
+ )
72
+ from .variable import BaseVariable
73
+ from .variant import Variant, branch_param
74
+
75
+ __version__ = "0.1.0"
76
+
77
+ __all__ = [
78
+ # Core classes
79
+ "BaseVariable",
80
+ "Constant",
81
+ "constant",
82
+ # Discovery
83
+ "scan_project",
84
+ "scan_package",
85
+ "discover_module",
86
+ "DiscoveryResult",
87
+ "PackageResult",
88
+ "ModuleExports",
89
+ "ModuleError",
90
+ # Configuration
91
+ "configure_database",
92
+ "get_database",
93
+ "get_user_id",
94
+ # Save
95
+ "save",
96
+ # Pipeline node staleness
97
+ "check_combo_state",
98
+ "check_node_state",
99
+ "check_pathinput_node_state",
100
+ "check_multiple_nodes_state",
101
+ # Logging
102
+ "Log",
103
+ # Batch execution
104
+ "for_each",
105
+ "scistack",
106
+ "Pipeline",
107
+ "PipelineBinding",
108
+ "Step",
109
+ "active_pipeline",
110
+ "Fixed",
111
+ "Variant",
112
+ "AcrossVariants",
113
+ "stamp_artifact",
114
+ "read_artifact_stamp",
115
+ "branch_param",
116
+ "Merge",
117
+ "ColumnSelection",
118
+ "ColName",
119
+ "EachOf",
120
+ "ForEachConfig",
121
+ "PathInput",
122
+ "PathOutput",
123
+ # Standalone / DataFrame support
124
+ "Col",
125
+ "set_schema",
126
+ "get_schema",
127
+ # Filter utilities
128
+ "raw_sql",
129
+ "schema_key",
130
+ # Schema exclusions
131
+ "exclude_schema",
132
+ "include_schema",
133
+ "list_exclusions",
134
+ # Exceptions
135
+ "SciStackError",
136
+ "NotRegisteredError",
137
+ "NotFoundError",
138
+ "DatabaseNotConfiguredError",
139
+ "ReservedMetadataKeyError",
140
+ "AmbiguousVersionError",
141
+ "AmbiguousParamError",
142
+ "DatabaseLockedError",
143
+ "PipelineCycleError",
144
+ ]
@@ -0,0 +1,97 @@
1
+ """AcrossVariants pooling wrapper: opt IN to cross-variant aggregation."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class AcrossVariants:
7
+ """
8
+ Wrapper to deliberately pool ALL branch_param variants of an input into one
9
+ aggregated table, with each row's upstream branch_params attached as columns.
10
+
11
+ Aggregation-mode ``for_each`` (iterating a strict subset of the schema keys)
12
+ **auto-splits** by upstream branch_param signature by default: one call per
13
+ variant group, as if the user had written
14
+ ``EachOf(Variant(X, ...), Variant(X, ...))``. That default is right for
15
+ ordinary aggregation and for endpoint (``plot_``/``stat_``) functions — a
16
+ pooled t-test double-counts, and a figure mixing two filter settings is
17
+ wrong.
18
+
19
+ ``AcrossVariants`` is the explicit opt-out for **multiverse /
20
+ specification-curve analysis**: the one legitimate cross-variant use case,
21
+ where the analysis deliberately spans pipeline decisions to quantify
22
+ robustness. The pooled rows keep their variant identity — each namespaced
23
+ branch_param key (e.g. ``bandpass.low_hz``) becomes an ordinary DataFrame
24
+ column so the function can group by specification::
25
+
26
+ def robustness(df):
27
+ return df.groupby("bandpass.low_hz")["value"].mean()
28
+
29
+ for_each(robustness, {"df": AcrossVariants(Filtered)}, [Spec],
30
+ subject=["S01", "S02"])
31
+
32
+ Composition:
33
+
34
+ - May wrap a variable type, a ``Fixed``, or a ``Variant``
35
+ (``AcrossVariants(Variant(X, low_hz=20))`` pins some params and pools
36
+ whatever variants remain).
37
+ - ``AcrossVariants(ColumnSelection)`` is an ERROR: the column selection
38
+ would drop the attached branch_param columns, silently defeating the
39
+ pooling contract.
40
+ - ``AcrossVariants(Merge(...))`` is an ERROR (mirrors ``Variant`` /
41
+ ``Fixed``): pool per constituent instead.
42
+ - ``AcrossVariants(EachOf(...))`` is an ERROR: ``EachOf`` must stay the
43
+ outermost wrapper.
44
+
45
+ In full-iteration mode (all schema keys iterated) each combo already sees
46
+ exactly one variant, so pooling is meaningless: the input behaves as if
47
+ unwrapped and a warning is logged.
48
+ """
49
+
50
+ def __init__(self, var_type: Any):
51
+ from .column_selection import ColumnSelection
52
+ from .each_of import EachOf
53
+ from .merge import Merge
54
+
55
+ if isinstance(var_type, Merge):
56
+ raise TypeError(
57
+ "AcrossVariants cannot wrap a Merge. branch_params are namespaced "
58
+ "per producing function, so pooling must happen per constituent: "
59
+ "Merge(AcrossVariants(A), B)."
60
+ )
61
+ if isinstance(var_type, EachOf):
62
+ raise TypeError(
63
+ "AcrossVariants cannot wrap an EachOf. EachOf must stay the "
64
+ "outermost wrapper."
65
+ )
66
+ if isinstance(var_type, ColumnSelection):
67
+ raise TypeError(
68
+ "AcrossVariants cannot wrap a ColumnSelection: the column "
69
+ "selection would drop the attached branch_param columns. "
70
+ "Select columns inside your function instead."
71
+ )
72
+ if isinstance(var_type, AcrossVariants):
73
+ var_type = var_type.var_type # idempotent
74
+
75
+ self.var_type = var_type
76
+
77
+ def to_key(self) -> str:
78
+ """Return a canonical string for use as a version key.
79
+
80
+ A pooled run and an auto-split run of the same function must not
81
+ collide: this key lands in the ``__inputs`` config key (same mechanism
82
+ as ``Variant.to_key()``).
83
+ """
84
+ if hasattr(self.var_type, "to_key"):
85
+ inner_key = self.var_type.to_key()
86
+ elif isinstance(self.var_type, type):
87
+ inner_key = self.var_type.__name__
88
+ else:
89
+ inner_key = repr(self.var_type)
90
+ return f"AcrossVariants({inner_key})"
91
+
92
+ @property
93
+ def __name__(self) -> str:
94
+ """Display name for format_inputs and error messages."""
95
+ from .foreach import _input_type_name
96
+
97
+ return f"AcrossVariants({_input_type_name(self.var_type)})"