scistackplotdb 0.1.26__py3-none-any.whl
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/__init__.py +80 -0
- scistackplotdb/endpoint.py +257 -0
- scistackplotdb/hierarchy.py +138 -0
- scistackplotdb/load.py +463 -0
- scistackplotdb/source.py +784 -0
- scistackplotdb/variants.py +225 -0
- scistackplotdb-0.1.26.dist-info/METADATA +142 -0
- scistackplotdb-0.1.26.dist-info/RECORD +9 -0
- scistackplotdb-0.1.26.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Variant selection, in scidb's vocabulary.
|
|
3
|
+
|
|
4
|
+
Two jobs, both translations rather than policy:
|
|
5
|
+
|
|
6
|
+
* :func:`variant_set` turns a ``scidb.Variant`` — the selector a scientist
|
|
7
|
+
already writes in ``for_each`` — into the column-keyed
|
|
8
|
+
``scistackplot.VariantSet`` a spec can serialize. Writing the selector once
|
|
9
|
+
and having it mean the same thing in a run and in a figure is the whole point;
|
|
10
|
+
a second, plotting-only dialect for "the v1 records" would be one more thing
|
|
11
|
+
to keep in sync and one more thing to get wrong.
|
|
12
|
+
* :func:`variant_graph` answers what a variant picker needs to draw itself:
|
|
13
|
+
which axes exist for a variable, and every version each function in its chain
|
|
14
|
+
has ever run under.
|
|
15
|
+
|
|
16
|
+
The translation lives here because it needs *both* halves: scidb's namespacing
|
|
17
|
+
(``__code__``, ``bandpass.low_hz``) and the plotting layer's column names
|
|
18
|
+
(``Code:bandpass``). scistackplot must not import scidb — the CSV path depends
|
|
19
|
+
on that — and scidb has no business knowing what a figure calls a column.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from scistacklog import Log
|
|
27
|
+
from scistackplot import CODE_FACTOR_PREFIX, LongTable, VariantSet, natural_sort_key
|
|
28
|
+
|
|
29
|
+
from .load import LATEST_COLUMN
|
|
30
|
+
|
|
31
|
+
LAYER = "scistackplotdb"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def variant_set(
|
|
35
|
+
name: str | None,
|
|
36
|
+
variant: Any,
|
|
37
|
+
table: LongTable | None = None,
|
|
38
|
+
) -> VariantSet:
|
|
39
|
+
"""
|
|
40
|
+
Build a named :class:`~scistackplot.spec.VariantSet` from a ``scidb.Variant``.
|
|
41
|
+
|
|
42
|
+
::
|
|
43
|
+
|
|
44
|
+
table = source.get_table(["StepLength"])
|
|
45
|
+
spec = PlotSpec(
|
|
46
|
+
measures=["StepLength"],
|
|
47
|
+
variant_sets=[
|
|
48
|
+
variant_set("baseline", Variant(StepLength, code_version="v1"), table),
|
|
49
|
+
variant_set("new filter", Variant(StepLength, fn="bandpass", low_hz=20), table),
|
|
50
|
+
],
|
|
51
|
+
roles={"Variant": Role.COLOR, "session": Role.X},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
``table`` is what resolves the *abbreviations* scidb allows — a bare
|
|
55
|
+
``code_version=`` (which function?) and a bare branch-param name (which
|
|
56
|
+
producing function's ``low_hz``?). Both are resolved exactly as scidb
|
|
57
|
+
resolves them at load time: by looking at what actually exists, and raising
|
|
58
|
+
rather than guessing when more than one candidate matches. Omit ``table``
|
|
59
|
+
only when every key is already unambiguous (``fn=``-qualified, or a dotted
|
|
60
|
+
name), and this raises if one is not — a variant that quietly selected the
|
|
61
|
+
wrong function's version would be undetectable in the figure.
|
|
62
|
+
"""
|
|
63
|
+
branch_params = getattr(variant, "branch_params", None)
|
|
64
|
+
if branch_params is None:
|
|
65
|
+
raise TypeError(
|
|
66
|
+
f"variant_set expects a scidb.Variant (or anything carrying "
|
|
67
|
+
f"branch_params); got {type(variant).__name__}. For a selection you "
|
|
68
|
+
f"have already written as columns, build "
|
|
69
|
+
f"scistackplot.VariantSet(name, selection) directly."
|
|
70
|
+
)
|
|
71
|
+
return VariantSet(name=name, selection=selection_for(branch_params, table))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def selection_for(
|
|
75
|
+
branch_params: dict[str, Any], table: LongTable | None = None
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""``Variant.branch_params`` → a column-keyed selection.
|
|
78
|
+
|
|
79
|
+
``__code__``/``__code__.fn`` become ``Code:fn``; branch params keep their
|
|
80
|
+
namespaced name, which is already the column name.
|
|
81
|
+
"""
|
|
82
|
+
from scidb.variant import CODE_PIN_PREFIX
|
|
83
|
+
|
|
84
|
+
axes = _axes_of(table)
|
|
85
|
+
code_axes = [a for a in axes if a["kind"] == "code"]
|
|
86
|
+
|
|
87
|
+
selection: dict[str, Any] = {}
|
|
88
|
+
for key, value in branch_params.items():
|
|
89
|
+
if key == CODE_PIN_PREFIX:
|
|
90
|
+
selection[_bare_code_column(code_axes)] = value
|
|
91
|
+
elif key.startswith(f"{CODE_PIN_PREFIX}."):
|
|
92
|
+
selection[f"{CODE_FACTOR_PREFIX}{key.split('.', 1)[1]}"] = value
|
|
93
|
+
else:
|
|
94
|
+
selection[_param_column(key, axes)] = value
|
|
95
|
+
return selection
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _axes_of(table: LongTable | None) -> list[dict]:
|
|
99
|
+
if table is None:
|
|
100
|
+
return []
|
|
101
|
+
return [f.origin for f in table.variant_factors if f.origin]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _bare_code_column(code_axes: list[dict]) -> str:
|
|
105
|
+
"""The column a bare ``code_version=`` means, or a message naming the choice.
|
|
106
|
+
|
|
107
|
+
Same rule as ``scidb.database._filter_records_by_code_version``: exactly one
|
|
108
|
+
versioned function is unambiguous, several are not.
|
|
109
|
+
"""
|
|
110
|
+
if len(code_axes) == 1:
|
|
111
|
+
return code_axes[0]["column"]
|
|
112
|
+
if not code_axes:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"code_version= needs a table to resolve against (pass one), and "
|
|
115
|
+
"that table must hold at least one versioned function. If you know "
|
|
116
|
+
'the function, name it: Variant(X, fn="bandpass", code_version="v1").'
|
|
117
|
+
)
|
|
118
|
+
names = sorted(a["function"] for a in code_axes)
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"code_version= is ambiguous: {len(names)} functions in this table hold "
|
|
121
|
+
f"more than one version ({names}). Name one with "
|
|
122
|
+
f'fn="{names[0]}", or add a variant per function.'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _param_column(key: str, axes: list[dict]) -> str:
|
|
127
|
+
"""Resolve a branch-param key to its column, suffix-matching a bare name."""
|
|
128
|
+
columns = [a["column"] for a in axes if a["kind"] == "param"]
|
|
129
|
+
if not columns or key in columns:
|
|
130
|
+
# Already namespaced (or nothing to check it against — the caller passed
|
|
131
|
+
# no table, having promised the key is exact).
|
|
132
|
+
return key
|
|
133
|
+
hits = [c for c in columns if c.rsplit(".", 1)[-1] == key]
|
|
134
|
+
if len(hits) == 1:
|
|
135
|
+
return hits[0]
|
|
136
|
+
if not hits:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"No variant axis matches branch param {key!r}. This table's axes "
|
|
139
|
+
f"are {sorted(columns)}."
|
|
140
|
+
)
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Branch param {key!r} is ambiguous — {len(hits)} producing functions "
|
|
143
|
+
f"use it ({sorted(hits)}). Disambiguate with fn=, e.g. "
|
|
144
|
+
f'Variant(X, fn="{hits[0].rsplit(".", 1)[0]}", {key}=…).'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def variant_graph(db, variable_frame, functions: list[str] | None = None) -> dict:
|
|
149
|
+
"""
|
|
150
|
+
Everything a variant picker needs for one variable.
|
|
151
|
+
|
|
152
|
+
Returns ``{"axes": [...], "versions": {fn: [...]}, "latest_column": ...}``:
|
|
153
|
+
|
|
154
|
+
``axes``
|
|
155
|
+
One entry per variant column — ``kind``/``function``/``param`` from
|
|
156
|
+
:func:`~scistackplotdb.load.attach_variants`, plus the levels present.
|
|
157
|
+
``versions``
|
|
158
|
+
Every recorded version of every function in this variable's upstream
|
|
159
|
+
chain, **including functions that have only ever had one**. Single
|
|
160
|
+
version functions are not an *axis* (they distinguish no two records),
|
|
161
|
+
but they are still a legitimate thing to pick a version of, and a
|
|
162
|
+
dropdown that omitted them would be empty for the common case.
|
|
163
|
+
``functions`` adds further names to look up — the GUI passes every
|
|
164
|
+
function node on the canvas, so nodes outside this variable's chain can
|
|
165
|
+
still say what they have run.
|
|
166
|
+
"""
|
|
167
|
+
from scidb.provenance_query import code_versions_batch, function_versions
|
|
168
|
+
|
|
169
|
+
frame = variable_frame.frame
|
|
170
|
+
record_ids = frame["record_id"].tolist() if "record_id" in frame.columns else []
|
|
171
|
+
chain: set[str] = set()
|
|
172
|
+
if record_ids:
|
|
173
|
+
for by_fn in code_versions_batch(db._duck, record_ids).values():
|
|
174
|
+
chain.update(by_fn)
|
|
175
|
+
|
|
176
|
+
wanted = sorted(chain | set(functions or ()))
|
|
177
|
+
versions = function_versions(db._duck, wanted) if wanted else {}
|
|
178
|
+
|
|
179
|
+
axes = []
|
|
180
|
+
for axis in variable_frame.variant_axes:
|
|
181
|
+
column = axis["column"]
|
|
182
|
+
levels = (
|
|
183
|
+
sorted(
|
|
184
|
+
{str(v) for v in frame[column].dropna().unique()},
|
|
185
|
+
key=natural_sort_key,
|
|
186
|
+
)
|
|
187
|
+
if column in frame.columns
|
|
188
|
+
else []
|
|
189
|
+
)
|
|
190
|
+
axes.append({**axis, "levels": levels})
|
|
191
|
+
|
|
192
|
+
Log.info(
|
|
193
|
+
"variant_graph(%s): %d axis/axes, versions for %d function(s) "
|
|
194
|
+
"(%d in this variable's chain)",
|
|
195
|
+
variable_frame.name,
|
|
196
|
+
len(axes),
|
|
197
|
+
len(versions),
|
|
198
|
+
len(chain),
|
|
199
|
+
layer=LAYER,
|
|
200
|
+
)
|
|
201
|
+
# Each axis in the vocabulary the PICKER has to match on. `param` here is the
|
|
202
|
+
# producing function's ARGUMENT name (scidb's `fn.param` namespacing) — not
|
|
203
|
+
# the name of the Parameter entity feeding it, which is what the canvas node
|
|
204
|
+
# is labelled with. The two coincide only until someone renames a Parameter
|
|
205
|
+
# or wires the port from a glue node, and when they stop coinciding the axis
|
|
206
|
+
# silently drops out of the graph. Logged so the popup's binding can be
|
|
207
|
+
# checked against what it was actually given.
|
|
208
|
+
for axis in axes:
|
|
209
|
+
Log.info(
|
|
210
|
+
" axis %s: kind=%s function=%s param=%s, %d level(s) %s",
|
|
211
|
+
axis["column"],
|
|
212
|
+
axis["kind"],
|
|
213
|
+
axis["function"],
|
|
214
|
+
axis["param"],
|
|
215
|
+
len(axis["levels"]),
|
|
216
|
+
axis["levels"][:10],
|
|
217
|
+
layer=LAYER,
|
|
218
|
+
)
|
|
219
|
+
return {
|
|
220
|
+
"variable": variable_frame.name,
|
|
221
|
+
"axes": axes,
|
|
222
|
+
"versions": versions,
|
|
223
|
+
"chain_functions": sorted(chain),
|
|
224
|
+
"latest_column": variable_frame.latest_column or LATEST_COLUMN,
|
|
225
|
+
}
|
|
@@ -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,9 @@
|
|
|
1
|
+
scistackplotdb/__init__.py,sha256=EVqx9UIPTkru8YgQzmFx_RTq5EEY0Snt_5cLSVutGts,2125
|
|
2
|
+
scistackplotdb/endpoint.py,sha256=HOr4wrC4Sv76IO28FwKqUup1V_spRVVS99kwMHpjo_c,9713
|
|
3
|
+
scistackplotdb/hierarchy.py,sha256=AixJ7MZEYAiXGhx9C3Nh0Wv9osEjXkpmiZhvc0hWO4s,5281
|
|
4
|
+
scistackplotdb/load.py,sha256=qerlIOLZIVKgXP-psIaNsiov1AfPrcl2Z8J1ZVyw4uQ,19844
|
|
5
|
+
scistackplotdb/source.py,sha256=cY7mEW43SQAXLGvVC04Wwqu7T7fPzp-aROrbnVqgn_A,33855
|
|
6
|
+
scistackplotdb/variants.py,sha256=F4k4fRUpo_LzawKsMj3ucT3HA_qROt5IKnikz7Dmfow,8982
|
|
7
|
+
scistackplotdb-0.1.26.dist-info/METADATA,sha256=Ky5shScyDimYHlybNW8FEor5L5r6mG3iElX6_SqV9mI,4990
|
|
8
|
+
scistackplotdb-0.1.26.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
scistackplotdb-0.1.26.dist-info/RECORD,,
|