scistackplotdb 0.1.26__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.
- scistackplotdb-0.1.26/.gitignore +28 -0
- scistackplotdb-0.1.26/PKG-INFO +142 -0
- scistackplotdb-0.1.26/README.md +116 -0
- scistackplotdb-0.1.26/pyproject.toml +60 -0
- scistackplotdb-0.1.26/src/scistackplotdb/__init__.py +80 -0
- scistackplotdb-0.1.26/src/scistackplotdb/endpoint.py +257 -0
- scistackplotdb-0.1.26/src/scistackplotdb/hierarchy.py +138 -0
- scistackplotdb-0.1.26/src/scistackplotdb/load.py +463 -0
- scistackplotdb-0.1.26/src/scistackplotdb/source.py +784 -0
- scistackplotdb-0.1.26/src/scistackplotdb/variants.py +225 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
*.egg-info/
|
|
12
|
+
# Python package build output (rebuilt fresh by CI on every publish; a
|
|
13
|
+
# committed dist/ gets deleted by `rm -rf dist` mid-build and dirties the
|
|
14
|
+
# tree for hatch-vcs). Excludes scistack-gui/extension/dist/, which is an
|
|
15
|
+
# intentionally committed compiled bundle, not a Python build artifact.
|
|
16
|
+
/dist/
|
|
17
|
+
/*/dist/
|
|
18
|
+
# Generated database artifacts (DuckDB data/lineage + write-ahead logs)
|
|
19
|
+
*.duckdb
|
|
20
|
+
*.duckdb.wal
|
|
21
|
+
*.wal
|
|
22
|
+
scistack-gui/frontend/node_modules/
|
|
23
|
+
# mkdocs build output (regenerated by `mkdocs build`)
|
|
24
|
+
/site/
|
|
25
|
+
# Runtime output of code_export_service (pipeline-to-code export) — timestamped
|
|
26
|
+
# per-run files, not source.
|
|
27
|
+
/exports/
|
|
28
|
+
*.log
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scistackplotdb
|
|
3
|
+
Version: 0.1.26
|
|
4
|
+
Summary: scidb-backed plotting: load variables into long tables and plot them with scistackplot
|
|
5
|
+
Author: SciStack Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: plotting,provenance,scidb,scistack,visualization
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: scistack-db>=0.1.0
|
|
19
|
+
Requires-Dist: scistackplot>=0.1.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: matplotlib>=3.6; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: seaborn>=0.12; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# scistackplotdb
|
|
28
|
+
|
|
29
|
+
## Plot what's in the database
|
|
30
|
+
|
|
31
|
+
`scistackplotdb` loads SciDB variables into the long format
|
|
32
|
+
[`scistackplot`](../scistackplot/README.md) consumes, and generates pipeline
|
|
33
|
+
endpoints from a finished plot spec.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install scistackplotdb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from scidb import configure_database
|
|
41
|
+
from scistackplot import PlotSpec, Role, PlotKind, render
|
|
42
|
+
from scistackplotdb import ScidbSource
|
|
43
|
+
|
|
44
|
+
db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
|
|
45
|
+
source = ScidbSource(db)
|
|
46
|
+
|
|
47
|
+
table = source.get_table(["StepLength"])
|
|
48
|
+
spec = PlotSpec(
|
|
49
|
+
measures=["StepLength"],
|
|
50
|
+
roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
|
|
51
|
+
kind=PlotKind.BOX,
|
|
52
|
+
)
|
|
53
|
+
figure = render(table, spec)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What this layer actually solves
|
|
57
|
+
|
|
58
|
+
The long format is nearly free — schema keys are already columns once a
|
|
59
|
+
variable is joined to `_schema`, the same shape `stat_` functions receive. The
|
|
60
|
+
real work is the four things a flat CSV never had.
|
|
61
|
+
|
|
62
|
+
**Shape classification.** Scalar, 1-D, or 2-D, decided from observed values
|
|
63
|
+
rather than declared SQL type names, and cached. It determines which plot kinds
|
|
64
|
+
are offered at all.
|
|
65
|
+
|
|
66
|
+
**Joins across schema depth.** Plotting trial-level `Speed` against
|
|
67
|
+
subject-level `Mass` broadcasts the shallower variable down the hierarchy:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
source.joinable_with("StepLength") # -> ["Mass"] (Signal is 1-D: no x axis)
|
|
71
|
+
table = source.get_table(["StepLength", "Mass"]) # one Mass value per trial row
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Because the dataset schema is an ordered, contiguous hierarchy, one variable's
|
|
75
|
+
levels are always a prefix of the other's or the two cannot be joined — and
|
|
76
|
+
`join_frames` refuses the latter with a message saying why.
|
|
77
|
+
|
|
78
|
+
**Variants are factors — this one is a correctness trap.** A variable produced
|
|
79
|
+
at two filter cutoffs has *two records per schema combination*. Treating those
|
|
80
|
+
branch params as ordinary columns silently plots two pipelines' results as if
|
|
81
|
+
they were replicates of one:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
spec = PlotSpec(measures=["Scaled"], roles={"session": Role.X})
|
|
85
|
+
validate(spec, table)
|
|
86
|
+
# RoleError: Variant factor(s) ['scale.factor'] would be pooled: their levels
|
|
87
|
+
# are different pipeline variants, not replicates... Assign them
|
|
88
|
+
# 'color'/'facet'/'iterate', select the variants you want with
|
|
89
|
+
# PlotSpec.variant_sets, or — to pool them deliberately — set them to
|
|
90
|
+
# 'aggregate' or 'free' yourself.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**A transport budget.** 1-D data across hundreds of trials is megabytes.
|
|
94
|
+
`resolve(..., max_points=N)` downsamples for the interactive panel; export
|
|
95
|
+
never does.
|
|
96
|
+
|
|
97
|
+
## From spec to pipeline endpoint
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from scistackplotdb import generate_endpoint
|
|
101
|
+
|
|
102
|
+
code = generate_endpoint(spec, table, input_variable="StepLength")
|
|
103
|
+
print(code.source)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
def plot_steplength(df, filename):
|
|
108
|
+
...
|
|
109
|
+
return g.figure
|
|
110
|
+
|
|
111
|
+
for_each(
|
|
112
|
+
plot_steplength,
|
|
113
|
+
inputs={
|
|
114
|
+
"df": StepLength,
|
|
115
|
+
"filename": PathOutput("plots/steplength_{subject}.png"),
|
|
116
|
+
},
|
|
117
|
+
outputs=[StepLengthFigure],
|
|
118
|
+
as_table=['df'],
|
|
119
|
+
finalized=True,
|
|
120
|
+
subject=[],
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The one translation that has to be exactly right is `Role.ITERATE` → a
|
|
125
|
+
`for_each` iteration keyword. Interactively, ITERATE fans out through a pandas
|
|
126
|
+
`groupby`; in the pipeline it fans out through `for_each` + `PathOutput`. If
|
|
127
|
+
those disagree, the exported pipeline is not what you previewed —
|
|
128
|
+
`tests/test_fanout_parity.py` runs both paths against the same database and
|
|
129
|
+
compares the figure sets.
|
|
130
|
+
|
|
131
|
+
Everything about *recording* the figure — `finalized`, artifact stamping,
|
|
132
|
+
`skip_computed`, `scidb report` — is SciDB's existing endpoint machinery and is
|
|
133
|
+
untouched.
|
|
134
|
+
|
|
135
|
+
## Ordering
|
|
136
|
+
|
|
137
|
+
Factor levels are ordered by SciDB's declared `schema_key_types`, not by
|
|
138
|
+
pandas' default: a key declared `numeric` sorts numerically, and everything
|
|
139
|
+
else goes through a natural sort so zero-padded IDs land as
|
|
140
|
+
`01, 02, … 10` instead of `01, 10, 02`.
|
|
141
|
+
|
|
142
|
+
See [`docs/claude/plotting-library-design.md`](../docs/claude/plotting-library-design.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# scistackplotdb
|
|
2
|
+
|
|
3
|
+
## Plot what's in the database
|
|
4
|
+
|
|
5
|
+
`scistackplotdb` loads SciDB variables into the long format
|
|
6
|
+
[`scistackplot`](../scistackplot/README.md) consumes, and generates pipeline
|
|
7
|
+
endpoints from a finished plot spec.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install scistackplotdb
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from scidb import configure_database
|
|
15
|
+
from scistackplot import PlotSpec, Role, PlotKind, render
|
|
16
|
+
from scistackplotdb import ScidbSource
|
|
17
|
+
|
|
18
|
+
db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
|
|
19
|
+
source = ScidbSource(db)
|
|
20
|
+
|
|
21
|
+
table = source.get_table(["StepLength"])
|
|
22
|
+
spec = PlotSpec(
|
|
23
|
+
measures=["StepLength"],
|
|
24
|
+
roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
|
|
25
|
+
kind=PlotKind.BOX,
|
|
26
|
+
)
|
|
27
|
+
figure = render(table, spec)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What this layer actually solves
|
|
31
|
+
|
|
32
|
+
The long format is nearly free — schema keys are already columns once a
|
|
33
|
+
variable is joined to `_schema`, the same shape `stat_` functions receive. The
|
|
34
|
+
real work is the four things a flat CSV never had.
|
|
35
|
+
|
|
36
|
+
**Shape classification.** Scalar, 1-D, or 2-D, decided from observed values
|
|
37
|
+
rather than declared SQL type names, and cached. It determines which plot kinds
|
|
38
|
+
are offered at all.
|
|
39
|
+
|
|
40
|
+
**Joins across schema depth.** Plotting trial-level `Speed` against
|
|
41
|
+
subject-level `Mass` broadcasts the shallower variable down the hierarchy:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
source.joinable_with("StepLength") # -> ["Mass"] (Signal is 1-D: no x axis)
|
|
45
|
+
table = source.get_table(["StepLength", "Mass"]) # one Mass value per trial row
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Because the dataset schema is an ordered, contiguous hierarchy, one variable's
|
|
49
|
+
levels are always a prefix of the other's or the two cannot be joined — and
|
|
50
|
+
`join_frames` refuses the latter with a message saying why.
|
|
51
|
+
|
|
52
|
+
**Variants are factors — this one is a correctness trap.** A variable produced
|
|
53
|
+
at two filter cutoffs has *two records per schema combination*. Treating those
|
|
54
|
+
branch params as ordinary columns silently plots two pipelines' results as if
|
|
55
|
+
they were replicates of one:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
spec = PlotSpec(measures=["Scaled"], roles={"session": Role.X})
|
|
59
|
+
validate(spec, table)
|
|
60
|
+
# RoleError: Variant factor(s) ['scale.factor'] would be pooled: their levels
|
|
61
|
+
# are different pipeline variants, not replicates... Assign them
|
|
62
|
+
# 'color'/'facet'/'iterate', select the variants you want with
|
|
63
|
+
# PlotSpec.variant_sets, or — to pool them deliberately — set them to
|
|
64
|
+
# 'aggregate' or 'free' yourself.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**A transport budget.** 1-D data across hundreds of trials is megabytes.
|
|
68
|
+
`resolve(..., max_points=N)` downsamples for the interactive panel; export
|
|
69
|
+
never does.
|
|
70
|
+
|
|
71
|
+
## From spec to pipeline endpoint
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from scistackplotdb import generate_endpoint
|
|
75
|
+
|
|
76
|
+
code = generate_endpoint(spec, table, input_variable="StepLength")
|
|
77
|
+
print(code.source)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def plot_steplength(df, filename):
|
|
82
|
+
...
|
|
83
|
+
return g.figure
|
|
84
|
+
|
|
85
|
+
for_each(
|
|
86
|
+
plot_steplength,
|
|
87
|
+
inputs={
|
|
88
|
+
"df": StepLength,
|
|
89
|
+
"filename": PathOutput("plots/steplength_{subject}.png"),
|
|
90
|
+
},
|
|
91
|
+
outputs=[StepLengthFigure],
|
|
92
|
+
as_table=['df'],
|
|
93
|
+
finalized=True,
|
|
94
|
+
subject=[],
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The one translation that has to be exactly right is `Role.ITERATE` → a
|
|
99
|
+
`for_each` iteration keyword. Interactively, ITERATE fans out through a pandas
|
|
100
|
+
`groupby`; in the pipeline it fans out through `for_each` + `PathOutput`. If
|
|
101
|
+
those disagree, the exported pipeline is not what you previewed —
|
|
102
|
+
`tests/test_fanout_parity.py` runs both paths against the same database and
|
|
103
|
+
compares the figure sets.
|
|
104
|
+
|
|
105
|
+
Everything about *recording* the figure — `finalized`, artifact stamping,
|
|
106
|
+
`skip_computed`, `scidb report` — is SciDB's existing endpoint machinery and is
|
|
107
|
+
untouched.
|
|
108
|
+
|
|
109
|
+
## Ordering
|
|
110
|
+
|
|
111
|
+
Factor levels are ordered by SciDB's declared `schema_key_types`, not by
|
|
112
|
+
pandas' default: a key declared `numeric` sorts numerically, and everything
|
|
113
|
+
else goes through a natural sort so zero-padded IDs land as
|
|
114
|
+
`01, 02, … 10` instead of `01, 10, 02`.
|
|
115
|
+
|
|
116
|
+
See [`docs/claude/plotting-library-design.md`](../docs/claude/plotting-library-design.md).
|
|
@@ -0,0 +1,60 @@
|
|
|
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 = "scistackplotdb"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
description = "scidb-backed plotting: load variables into long tables and plot them with scistackplot"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "SciStack Contributors" }
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"plotting",
|
|
21
|
+
"visualization",
|
|
22
|
+
"scidb",
|
|
23
|
+
"scistack",
|
|
24
|
+
"provenance",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Science/Research",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"scistackplot>=0.1.0",
|
|
39
|
+
"scistack-db>=0.1.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0",
|
|
45
|
+
"pytest-cov>=4.0",
|
|
46
|
+
"matplotlib>=3.6",
|
|
47
|
+
"seaborn>=0.12",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = [
|
|
52
|
+
"/src",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/scistackplotdb"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scistackplotdb — plot scidb variables.
|
|
3
|
+
|
|
4
|
+
Loads variables into the long format ``scistackplot`` consumes, adds the four
|
|
5
|
+
things a flat table never needed (shape classification, schema-depth joins,
|
|
6
|
+
variants as factors, and a transport budget), and generates pipeline endpoints
|
|
7
|
+
from a finished spec.
|
|
8
|
+
|
|
9
|
+
::
|
|
10
|
+
|
|
11
|
+
from scidb import configure_database
|
|
12
|
+
from scistackplot import PlotSpec, Role, PlotKind, render
|
|
13
|
+
from scistackplotdb import ScidbSource
|
|
14
|
+
|
|
15
|
+
db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
|
|
16
|
+
source = ScidbSource(db)
|
|
17
|
+
|
|
18
|
+
table = source.get_table(["StepLength"])
|
|
19
|
+
spec = PlotSpec(
|
|
20
|
+
measures=["StepLength"],
|
|
21
|
+
roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
|
|
22
|
+
kind=PlotKind.BOX,
|
|
23
|
+
)
|
|
24
|
+
figure = render(table, spec)
|
|
25
|
+
|
|
26
|
+
Everything about recording a figure — ``finalized``, artifact stamping,
|
|
27
|
+
``scidb report`` — belongs to scidb's existing ``plot_`` endpoint machinery and
|
|
28
|
+
is unchanged. See ``docs/claude/plotting-library-design.md``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from .endpoint import (
|
|
34
|
+
EndpointCode,
|
|
35
|
+
default_output_variable,
|
|
36
|
+
default_path_template,
|
|
37
|
+
generate_endpoint,
|
|
38
|
+
)
|
|
39
|
+
from .hierarchy import join_frames, join_kind, joinable, joined_levels
|
|
40
|
+
from .load import (
|
|
41
|
+
LATEST_COLUMN,
|
|
42
|
+
MISSING_VERSION_LEVEL,
|
|
43
|
+
VERSION_FACTOR_PREFIX,
|
|
44
|
+
VariableFrame,
|
|
45
|
+
attach_variants,
|
|
46
|
+
data_columns_for,
|
|
47
|
+
load_variable,
|
|
48
|
+
registered_variables,
|
|
49
|
+
sample_value,
|
|
50
|
+
schema_keys,
|
|
51
|
+
)
|
|
52
|
+
from .source import ScidbSource
|
|
53
|
+
from .variants import selection_for, variant_graph, variant_set
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"ScidbSource",
|
|
57
|
+
"variant_set",
|
|
58
|
+
"variant_graph",
|
|
59
|
+
"selection_for",
|
|
60
|
+
"VariableFrame",
|
|
61
|
+
"VERSION_FACTOR_PREFIX",
|
|
62
|
+
"MISSING_VERSION_LEVEL",
|
|
63
|
+
"LATEST_COLUMN",
|
|
64
|
+
"load_variable",
|
|
65
|
+
"attach_variants",
|
|
66
|
+
"registered_variables",
|
|
67
|
+
"schema_keys",
|
|
68
|
+
"data_columns_for",
|
|
69
|
+
"sample_value",
|
|
70
|
+
"join_kind",
|
|
71
|
+
"joinable",
|
|
72
|
+
"join_frames",
|
|
73
|
+
"joined_levels",
|
|
74
|
+
"generate_endpoint",
|
|
75
|
+
"EndpointCode",
|
|
76
|
+
"default_output_variable",
|
|
77
|
+
"default_path_template",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Turning a ``PlotSpec`` into a pipeline endpoint.
|
|
3
|
+
|
|
4
|
+
The GUI's "Add to pipeline" produces two things: a ``plot_`` function (generated
|
|
5
|
+
by ``scistackplot.codegen`` — literal seaborn, no runtime dependency on this
|
|
6
|
+
package) and the ``for_each`` call that runs it. This module owns the second
|
|
7
|
+
half, and with it the one translation that has to be exactly right:
|
|
8
|
+
|
|
9
|
+
Role.ITERATE -> a for_each iteration keyword
|
|
10
|
+
|
|
11
|
+
Interactively, ITERATE fans out through a pandas ``groupby`` inside
|
|
12
|
+
``resolve()``. In the pipeline it fans out through ``for_each`` + ``PathOutput``.
|
|
13
|
+
If those two ever disagree, the exported pipeline is not what the user
|
|
14
|
+
previewed — the worst failure mode this layer has, and what
|
|
15
|
+
``tests/test_fanout_parity.py`` exists to prevent.
|
|
16
|
+
|
|
17
|
+
Everything else the endpoint needs already exists in scidb: ``finalized``,
|
|
18
|
+
artifact stamping, ``skip_computed`` and ``scidb report`` are untouched.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
|
|
26
|
+
from scistacklog import Log
|
|
27
|
+
from scistackplot import LongTable, PlotSpec, default_function_name
|
|
28
|
+
from scistackplot import generate_plot_function
|
|
29
|
+
from scistackplot.codegen import group_param, variant_params
|
|
30
|
+
from scistackplot.roles import fanout_keys
|
|
31
|
+
from scistackplot.variants import defined_sets
|
|
32
|
+
|
|
33
|
+
from .load import LATEST_COLUMN
|
|
34
|
+
|
|
35
|
+
LAYER = "scistackplotdb"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EndpointCode:
|
|
40
|
+
"""Generated source for one plotting endpoint."""
|
|
41
|
+
|
|
42
|
+
function_name: str
|
|
43
|
+
function_source: str
|
|
44
|
+
foreach_source: str
|
|
45
|
+
iterate_keys: list[str] = field(default_factory=list)
|
|
46
|
+
path_template: str = ""
|
|
47
|
+
output_variable: str = ""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def source(self) -> str:
|
|
51
|
+
"""Function and call together, ready to append to a pipeline module."""
|
|
52
|
+
return f"{self.function_source}\n\n{self.foreach_source}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def generate_endpoint(
|
|
56
|
+
spec: PlotSpec,
|
|
57
|
+
table: LongTable,
|
|
58
|
+
*,
|
|
59
|
+
input_variable: str,
|
|
60
|
+
function_name: str | None = None,
|
|
61
|
+
output_variable: str | None = None,
|
|
62
|
+
path_template: str | None = None,
|
|
63
|
+
finalized: bool = True,
|
|
64
|
+
x_variable: str | None = None,
|
|
65
|
+
) -> EndpointCode:
|
|
66
|
+
"""
|
|
67
|
+
Generate the ``plot_`` function and its ``for_each`` call.
|
|
68
|
+
|
|
69
|
+
``input_variable`` is the scidb variable type supplying the data;
|
|
70
|
+
``x_variable`` is the optional second measure for an x–y scatter.
|
|
71
|
+
"""
|
|
72
|
+
name = function_name or default_function_name(spec)
|
|
73
|
+
# NOT spec.iterate_factors: the fan-out includes schema keys promoted
|
|
74
|
+
# because a nested key iterates, and runs in schema order. The preview and
|
|
75
|
+
# the generated for_each have to agree on both — that is what
|
|
76
|
+
# tests/test_fanout_parity.py checks.
|
|
77
|
+
iterate_keys = fanout_keys(spec, table)
|
|
78
|
+
output = output_variable or default_output_variable(input_variable)
|
|
79
|
+
template = path_template or default_path_template(name, iterate_keys)
|
|
80
|
+
|
|
81
|
+
function_source = generate_plot_function(spec, table, function_name=name)
|
|
82
|
+
foreach_source = _foreach_call(
|
|
83
|
+
spec=spec,
|
|
84
|
+
function_name=name,
|
|
85
|
+
input_variable=input_variable,
|
|
86
|
+
x_variable=x_variable,
|
|
87
|
+
output_variable=output,
|
|
88
|
+
path_template=template,
|
|
89
|
+
iterate_keys=iterate_keys,
|
|
90
|
+
finalized=finalized,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
Log.info(
|
|
94
|
+
"generated endpoint %s: input=%s output=%s iterate=%s finalized=%s",
|
|
95
|
+
name,
|
|
96
|
+
input_variable,
|
|
97
|
+
output,
|
|
98
|
+
iterate_keys or "none",
|
|
99
|
+
finalized,
|
|
100
|
+
layer=LAYER,
|
|
101
|
+
)
|
|
102
|
+
return EndpointCode(
|
|
103
|
+
function_name=name,
|
|
104
|
+
function_source=function_source,
|
|
105
|
+
foreach_source=foreach_source,
|
|
106
|
+
iterate_keys=iterate_keys,
|
|
107
|
+
path_template=template,
|
|
108
|
+
output_variable=output,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def default_output_variable(input_variable: str) -> str:
|
|
113
|
+
"""``StepLength`` -> ``StepLengthFigure``."""
|
|
114
|
+
return f"{input_variable}Figure"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def default_path_template(function_name: str, iterate_keys: list[str]) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Build a PathOutput template that cannot collide.
|
|
120
|
+
|
|
121
|
+
Every ITERATE key goes into the filename. Omitting one would make two
|
|
122
|
+
figures write the same file; for schema keys scidb treats that as
|
|
123
|
+
pre-existing overwrite behavior (no error), and for variants its collision
|
|
124
|
+
guard raises before anything renders. Including them all avoids both.
|
|
125
|
+
"""
|
|
126
|
+
slug = re.sub(r"^plot_", "", function_name)
|
|
127
|
+
parts = "".join(f"_{{{key}}}" for key in iterate_keys)
|
|
128
|
+
return f"plots/{slug}{parts}.png"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def variant_expression(input_variable: str, variant_set) -> str:
|
|
132
|
+
"""A ``Variant(...)`` call selecting one named variant's records.
|
|
133
|
+
|
|
134
|
+
The inverse of :func:`~scistackplotdb.variants.selection_for`, and the thing
|
|
135
|
+
that makes a variant figure reproducible by hand: what the popup's
|
|
136
|
+
checkboxes and version dropdowns produced comes back out as the same
|
|
137
|
+
wrapper a scientist would have typed.
|
|
138
|
+
|
|
139
|
+
Selections spanning two producing functions **nest** rather than resorting
|
|
140
|
+
to dotted-string kwargs::
|
|
141
|
+
|
|
142
|
+
Variant(Variant(EMG, fn="loadEMG", code_version="v1"), fn="bandpass", low_hz=20)
|
|
143
|
+
|
|
144
|
+
Nesting is documented, merges the two filters (``scidb.variant.Variant``
|
|
145
|
+
handles it explicitly), and keeps every keyword readable — where
|
|
146
|
+
``**{"__code__.loadEMG": "v1"}`` would leak a reserved namespace into code a
|
|
147
|
+
user is meant to edit.
|
|
148
|
+
"""
|
|
149
|
+
from scistackplot import CODE_FACTOR_PREFIX, LATEST
|
|
150
|
+
|
|
151
|
+
by_function: dict[str, dict[str, object]] = {}
|
|
152
|
+
for column, value in (variant_set.selection or {}).items():
|
|
153
|
+
if column.startswith(CODE_FACTOR_PREFIX):
|
|
154
|
+
fn_name = column[len(CODE_FACTOR_PREFIX) :]
|
|
155
|
+
by_function.setdefault(fn_name, {})["code_version"] = value
|
|
156
|
+
elif column == LATEST_COLUMN:
|
|
157
|
+
# The source's "these are the current records" recommendation. scidb
|
|
158
|
+
# spells the same thing `code_version="latest"`, resolved per schema
|
|
159
|
+
# location by the same rule — not "the highest ordinal".
|
|
160
|
+
by_function.setdefault(None, {})["code_version"] = LATEST
|
|
161
|
+
else:
|
|
162
|
+
fn_name, _, param = column.rpartition(".")
|
|
163
|
+
by_function.setdefault(fn_name or None, {})[param] = value
|
|
164
|
+
|
|
165
|
+
expression = input_variable
|
|
166
|
+
for fn_name in sorted(by_function, key=lambda n: (n is None, n or "")):
|
|
167
|
+
arguments = []
|
|
168
|
+
if fn_name:
|
|
169
|
+
arguments.append(f"fn={fn_name!r}")
|
|
170
|
+
arguments.extend(f"{key}={value!r}" for key, value in by_function[fn_name].items())
|
|
171
|
+
expression = f"Variant({expression}, {', '.join(arguments)})"
|
|
172
|
+
return expression
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _single_variant_expression(input_variable: str, spec) -> str:
|
|
176
|
+
"""The lone variant's pin, or the bare variable when nothing is selected.
|
|
177
|
+
|
|
178
|
+
The row may name its own variable, in which case that is what the endpoint
|
|
179
|
+
loads — ``input_variable`` is only the default.
|
|
180
|
+
"""
|
|
181
|
+
sets = defined_sets(spec.variant_sets)
|
|
182
|
+
if len(sets) != 1:
|
|
183
|
+
return input_variable
|
|
184
|
+
return variant_expression(sets[0].variable or input_variable, sets[0])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _foreach_call(
|
|
188
|
+
*,
|
|
189
|
+
spec: PlotSpec,
|
|
190
|
+
function_name: str,
|
|
191
|
+
input_variable: str,
|
|
192
|
+
x_variable: str | None,
|
|
193
|
+
output_variable: str,
|
|
194
|
+
path_template: str,
|
|
195
|
+
iterate_keys: list[str],
|
|
196
|
+
finalized: bool,
|
|
197
|
+
) -> str:
|
|
198
|
+
variant_inputs = variant_params(spec)
|
|
199
|
+
if variant_inputs:
|
|
200
|
+
# One input per named variant, each loaded through its own pin. See
|
|
201
|
+
# `codegen.variant_params` for why this cannot be a single `df`.
|
|
202
|
+
# Zipped against `defined_sets`, not `spec.variant_sets`: unfilled rows
|
|
203
|
+
# produce no input, so indexing the raw list would pair a parameter with
|
|
204
|
+
# the wrong variant's selection.
|
|
205
|
+
inputs = [
|
|
206
|
+
f' "{generated.param}": '
|
|
207
|
+
f"{variant_expression(generated.variable, variant)},"
|
|
208
|
+
for generated, variant in zip(
|
|
209
|
+
variant_inputs, defined_sets(spec.variant_sets), strict=True
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
table_inputs = [generated.param for generated in variant_inputs]
|
|
213
|
+
else:
|
|
214
|
+
pinned = _single_variant_expression(input_variable, spec)
|
|
215
|
+
inputs = [f' "df": {pinned},']
|
|
216
|
+
table_inputs = ["df"]
|
|
217
|
+
if x_variable:
|
|
218
|
+
inputs.append(f' "df_x": {x_variable},')
|
|
219
|
+
table_inputs.append("df_x")
|
|
220
|
+
for group in spec.factor_variables:
|
|
221
|
+
# A grouping variable arrives as its own input and is merged onto the
|
|
222
|
+
# data inside the function: `as_table` hands a function schema keys and
|
|
223
|
+
# data columns only, so a subject-level Condition cannot ride along on
|
|
224
|
+
# the measure's frame.
|
|
225
|
+
inputs.append(f' "{group_param(group)}": {group},')
|
|
226
|
+
table_inputs.append(group_param(group))
|
|
227
|
+
inputs.append(f' "filename": PathOutput("{path_template}"),')
|
|
228
|
+
|
|
229
|
+
lines = [
|
|
230
|
+
"for_each(",
|
|
231
|
+
f" {function_name},",
|
|
232
|
+
" inputs={",
|
|
233
|
+
*inputs,
|
|
234
|
+
" },",
|
|
235
|
+
f" outputs=[{output_variable}],",
|
|
236
|
+
# A plot_ function receives the long-format table (schema keys as
|
|
237
|
+
# columns) — as_table defaults ON only for stat_, so say it explicitly.
|
|
238
|
+
f" as_table={table_inputs!r},",
|
|
239
|
+
f" finalized={finalized},",
|
|
240
|
+
]
|
|
241
|
+
for key in iterate_keys:
|
|
242
|
+
# [] means "every value present" — the same all-values resolution
|
|
243
|
+
# scifor applies to an empty iteration list.
|
|
244
|
+
lines.append(f" {key}=[],")
|
|
245
|
+
lines.append(")")
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def required_declarations(code: EndpointCode) -> list[str]:
|
|
250
|
+
"""
|
|
251
|
+
Variable types the generated call needs that may not exist yet.
|
|
252
|
+
|
|
253
|
+
The GUI declares these through the normal entity-declaration path before
|
|
254
|
+
writing the code, so a generated endpoint never references an undeclared
|
|
255
|
+
type.
|
|
256
|
+
"""
|
|
257
|
+
return [code.output_variable]
|