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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any