Skip to content

Commit 18c8814

Browse files
data-pipelines: DAG shown at both levels — collapsed modules and expanded tables (#265)
* data-pipelines: show the DAG at both levels (collapsed modules, expanded tables) The regenerated figure shows tables, so "nodes represent Python modules" had become inaccurate. The section now pairs a collapsed module-level view (nodes are schemas/modules; edges bundle foreign key references and the import dependency) with the expanded table-level view (nodes are tables; each edge is an individual foreign key constraint). * data-pipelines: substantial pipeline so bundling is visible scan→imaging now carries two foreign keys (Scan→ScanQuality, ScanInfo→MotionCorrection) that collapse into one heavier bundled edge; reference supplies both scan and imaging. Prose points at the bundling in both views. * data-pipelines: consolidate to four modules for a more substantial collapse Subject moves into lab; Session references User directly; Scan and ScanInfo move into session. Twelve tables collapse to four nodes, with two boundaries (lab→session, session→imaging) each bundling two foreign keys into one heavier edge. * data-pipelines: one consistent example across the whole page The canonical four-module pipeline (lab, reference, session, imaging) now serves every section: the two-level DAG figures, the schema- organization file tree (lab.py/reference.py/session.py/imaging.py), a drill-in of the imaging module (master-part: Segmentation.Roi, Fluorescence.Trace), the OAS storage-tier snippet (ScanInfo), and the workflow steps name the example's tables. The unrelated scan-schema example (AcquisitionSoftware/Field/Channel) is dropped from this page. * remove scan-schema.svg — no longer referenced (data-pipelines now uses the imaging drill-in) * index: Migrate to 2.x, not 2.0 * Diagram notation: update stored notebook outputs and prose to the shipped brand notation - Stored dj.Diagram SVG outputs in 16 notebooks recolored from the interim #1534 hues to the shipped #1544 tier palette (deterministic hex mapping, light and dark values). - Stored make_mermaid text output updated to the palette the library now emits. - Legends and notation prose corrected (calcium-imaging, both ephys tutorials, relational-workflow-model, diagram spec): orange computed tier, rounded boxes, entity clusters for master-part, underline = new dimension; the spec's tier table now carries the exact palette. - rwm-pipeline.svg recolored; rwm-legend.svg already current. * data-pipelines: correct lab→session edge direction to User → Session The table-level SVG has `User -> Session` (and `Lab -> User`, placing `User` in the `lab` schema). The reversed form contradicted the collapsed figure above, which has no `session → lab` edge, and would have put a cycle in the DAG claim the same paragraph makes. * scripts: add the generator for the pipeline explanation diagrams The three figures on the data-pipelines and relational-workflow-model pages were committed SVGs with no recoverable source: the 14-table pipeline behind them existed nowhere in the repo, so 'regenerate against a new release' meant reconstructing the schemas by reading the picture. scripts/pipeline_example/ defines that pipeline as four modules, one schema each -- the correspondence the page describes -- and scripts/gen_pipeline_diagrams.py renders the three figures from it. --check reports any committed figure that differs and exits non-zero. Verified to reproduce the committed figures' nodes, tiers, shapes, edge weights and styles, tooltips, clusters and labels. Three caveats are documented in the module docstring: the generator needs an empty database, tooltip padding entities vary by pydot version, and the collapsed lab -> session edge is traversal-order dependent in the renderer. * scripts: isolate the diagram example by database, not by server On PostgreSQL a dj.Schema is a schema within a database, so DJ_DATABASE_NAME is enough to keep the example's unprefixed schema names from colliding -- separate databases can hold same-named schemas. The previous note asked for a throwaway server, which is stronger than necessary. * data-pipelines: don't read per-foreign-key notation on a collapsed edge The page claimed 'a heavier edge carries a larger bundle'. The figure directly beneath contradicts it: lab -> session and session -> imaging both bundle exactly two foreign keys and render 0.75 and 2. A collapsed edge inherits the attributes of one arbitrary bundle member, so weight there is neither bundle size nor a property of the bundle. Softening to 'weight encodes cardinality' would still mislead for the same reason, so the collapsed view now attributes no meaning to weight or style: it is one edge per schema pair, recording that a bundle exists. The weight and style semantics stay with the table-level figure, where they are well defined, now pointing at the Diagram spec.
1 parent 4d57cbd commit 18c8814

31 files changed

Lines changed: 2256 additions & 1685 deletions

scripts/gen_pipeline_diagrams.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Generate the pipeline diagrams used in the explanation pages.
2+
3+
Three committed figures come from one four-module example pipeline, defined in
4+
``scripts/pipeline_example/``:
5+
6+
- ``pipeline-modules.svg`` — the whole pipeline at the table level, dashed
7+
clusters grouping each module (``src/explanation/data-pipelines.md``).
8+
- ``pipeline-modules-collapsed.svg`` — the same pipeline at the module level,
9+
one node per schema, via ``Diagram.collapse()`` (same page).
10+
- ``imaging-schema.svg`` — the ``imaging`` module on its own
11+
(``src/explanation/relational-workflow-model.md``, ``src/index.md``).
12+
13+
Keeping the pipeline in the repo makes the figures reproducible. The prose
14+
describes specific edges, tiers, and table counts; those claims are only
15+
checkable if the pipeline that produced them can be rebuilt.
16+
17+
Usage
18+
-----
19+
Needs a database and a graphviz ``dot`` on PATH::
20+
21+
docker compose up -d postgres
22+
DJ_HOST=localhost DJ_PORT=5432 DJ_USER=postgres DJ_PASS=tutorial \
23+
DJ_BACKEND=postgresql DJ_USE_TLS=false \
24+
DJ_DATABASE_NAME=docs_diagrams \
25+
python scripts/gen_pipeline_diagrams.py
26+
27+
``DJ_DATABASE_NAME`` matters: on PostgreSQL a ``dj.Schema`` is a schema *within*
28+
a database, so giving this example its own database lets it keep unprefixed
29+
schema names without colliding with anything else on the server. Create it once
30+
with ``createdb docs_diagrams``.
31+
32+
``--check`` renders without writing and exits non-zero if any committed figure
33+
differs — suitable for CI. The example schemas are dropped afterwards unless
34+
``--keep-schemas`` is given.
35+
36+
This reproduces the committed figures' nodes, tiers, edges, tooltips, clusters
37+
and labels exactly, with the caveats below.
38+
39+
Reproducibility caveats
40+
-----------------------
41+
- **Give it its own database.** The schema names are unprefixed (``reference``,
42+
``lab``, ``session``, ``imaging``) because ``dj.Diagram`` takes each cluster
43+
label from the Python module name and the two must agree. Unprefixed names are
44+
safe as long as ``DJ_DATABASE_NAME`` points at a database reserved for this
45+
example — separate databases can hold same-named schemas. Without it the
46+
connection lands in the default ``postgres`` database, where a pre-existing
47+
schema of the same name is picked up silently and rendered instead.
48+
- **Padding entities depend on pydot.** Tooltip padding is emitted as `` ``
49+
by the pydot that produced the committed figures and as literal spaces by
50+
4.0.1, which shows up as a whole-file diff with no visual change. Compare
51+
rendered content, not bytes, when the pydot version moves. Nothing pins pydot.
52+
- **One collapsed edge is traversal-order dependent.** A collapsed edge inherits
53+
the attributes of whichever foreign key in its bundle is visited first
54+
(``diagram.py``, ``_collapse_graph``: ``if not new_graph.has_edge(...)``), with
55+
no aggregation over the bundle. Where a bundle mixes a primary and a secondary
56+
foreign key — ``lab -> session`` here, which bundles ``Subject -> Session``
57+
(primary) and ``User -> Session`` (secondary) — the edge renders solid or
58+
dashed depending on order alone. The committed figure has it solid; this script
59+
produces dashed. Both are outputs of the same renderer.
60+
61+
A non-empty diff after a DataJoint upgrade is the signal to review the notation
62+
and the surrounding prose together — see issue #246.
63+
"""
64+
65+
import argparse
66+
import os
67+
import sys
68+
import tempfile
69+
from pathlib import Path
70+
71+
import datajoint as dj
72+
73+
sys.path.insert(0, str(Path(__file__).resolve().parent))
74+
75+
from pipeline_example import imaging, lab, reference, session # noqa: E402
76+
77+
IMAGES = Path(__file__).resolve().parent.parent / "src" / "images"
78+
79+
MODULES = (reference, lab, session, imaging)
80+
81+
# dj.Diagram labels each node by resolving its table against this context. Passing
82+
# the classes under their bare names keeps node labels unqualified ("Session", not
83+
# "session.Session") while the cluster labels still come from the module names.
84+
CONTEXT = {
85+
name: obj
86+
for module in MODULES
87+
for name, obj in vars(module).items()
88+
if isinstance(obj, type) and issubclass(obj, dj.Table)
89+
}
90+
91+
92+
def diagram(schema) -> dj.Diagram:
93+
return dj.Diagram(schema, context=CONTEXT)
94+
95+
96+
def whole_pipeline() -> dj.Diagram:
97+
"""The four modules unioned into one diagram."""
98+
result = diagram(reference.schema)
99+
for module in MODULES[1:]:
100+
result += diagram(module.schema)
101+
return result
102+
103+
104+
FIGURES = {
105+
# Whole pipeline, table level: every module expanded.
106+
"pipeline-modules.svg": whole_pipeline,
107+
# Same pipeline, module level: one node per schema.
108+
"pipeline-modules-collapsed.svg": lambda: whole_pipeline().collapse(),
109+
# The imaging module on its own.
110+
"imaging-schema.svg": lambda: diagram(imaging.schema),
111+
}
112+
113+
114+
def main() -> int:
115+
parser = argparse.ArgumentParser(
116+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
117+
)
118+
parser.add_argument(
119+
"--check",
120+
action="store_true",
121+
help="report figures that differ from the committed SVGs without writing them; "
122+
"exits 1 if any differ",
123+
)
124+
parser.add_argument(
125+
"--keep-schemas",
126+
action="store_true",
127+
help="leave the example schemas in the database (default: drop them)",
128+
)
129+
args = parser.parse_args()
130+
131+
# Left-to-right layout, matching scripts/execute-notebooks.sh.
132+
with tempfile.TemporaryDirectory() as tmp:
133+
with dj.config.override(display__diagram_direction="LR"):
134+
rendered = {}
135+
for name, build in FIGURES.items():
136+
staged = Path(tmp) / name
137+
build().save(str(staged))
138+
rendered[name] = staged.read_text()
139+
140+
if not args.keep_schemas:
141+
for module in reversed(MODULES):
142+
module.schema.drop(prompt=False)
143+
144+
differs = []
145+
for name, svg in rendered.items():
146+
target = IMAGES / name
147+
old = target.read_text() if target.exists() else None
148+
if old == svg:
149+
print(f" unchanged {name}")
150+
elif args.check:
151+
differs.append(name)
152+
print(f" DIFFERS {name}")
153+
else:
154+
differs.append(name)
155+
target.write_text(svg)
156+
print(f" written {name}")
157+
158+
if args.check and differs:
159+
print(
160+
f"\n{len(differs)} figure(s) differ from the committed SVGs. Re-run "
161+
"without --check to update them, then review the notation and the "
162+
"prose in src/explanation/ together (see #246). If only tooltip "
163+
"padding moved, check the pydot version first — see the module "
164+
"docstring.",
165+
file=sys.stderr,
166+
)
167+
return 1
168+
return 0
169+
170+
171+
if __name__ == "__main__":
172+
os.environ.setdefault("DJ_USE_TLS", "false")
173+
raise SystemExit(main())
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""The example pipeline behind the diagrams in the explanation pages.
2+
3+
Four modules, one database schema each — the correspondence
4+
``src/explanation/data-pipelines.md`` describes. ``dj.Diagram`` takes the group
5+
label for each cluster from the Python module name, so these module names are
6+
what put ``reference`` / ``lab`` / ``session`` / ``imaging`` on the figures.
7+
8+
Rendered by ``scripts/gen_pipeline_diagrams.py``; not imported by the site build.
9+
"""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Computed results, including two master-part pairs.
2+
3+
``ScanQuality`` depends on ``session.Scan`` and ``MotionCorrection`` on
4+
``session.ScanInfo`` — the two foreign keys bundled into the ``session → imaging``
5+
edge at the module level.
6+
"""
7+
8+
import datajoint as dj
9+
10+
from .reference import SegmentationMethod
11+
from .session import Scan, ScanInfo
12+
13+
schema = dj.Schema("imaging")
14+
15+
16+
@schema
17+
class ScanQuality(dj.Computed):
18+
definition = """
19+
-> Scan
20+
---
21+
quality_score : float64
22+
"""
23+
24+
25+
@schema
26+
class MotionCorrection(dj.Computed):
27+
definition = """
28+
-> ScanInfo
29+
---
30+
x_shifts : bytes
31+
y_shifts : bytes
32+
"""
33+
34+
35+
@schema
36+
class Segmentation(dj.Computed):
37+
definition = """
38+
-> MotionCorrection
39+
-> SegmentationMethod
40+
---
41+
num_rois : int32
42+
"""
43+
44+
class Roi(dj.Part):
45+
definition = """
46+
-> master
47+
roi_idx : int32
48+
---
49+
mask : bytes
50+
"""
51+
52+
53+
@schema
54+
class Fluorescence(dj.Computed):
55+
definition = """
56+
-> Segmentation
57+
---
58+
timestamps : bytes
59+
"""
60+
61+
class Trace(dj.Part):
62+
definition = """
63+
-> master
64+
-> Segmentation.Roi
65+
---
66+
trace : bytes
67+
"""

scripts/pipeline_example/lab.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Who runs the experiments, and what they are run on."""
2+
3+
import datajoint as dj
4+
5+
schema = dj.Schema("lab")
6+
7+
8+
@schema
9+
class Lab(dj.Manual):
10+
definition = """
11+
lab_name : varchar(32)
12+
---
13+
institution : varchar(64)
14+
"""
15+
16+
17+
@schema
18+
class User(dj.Manual):
19+
definition = """
20+
-> Lab
21+
user_name : varchar(32)
22+
---
23+
email : varchar(64)
24+
"""
25+
26+
27+
@schema
28+
class Subject(dj.Manual):
29+
definition = """
30+
subject_id : int32
31+
---
32+
species : varchar(64)
33+
date_of_birth : date
34+
"""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Lookup tables: the shared vocabulary the rest of the pipeline refers to."""
2+
3+
import datajoint as dj
4+
5+
schema = dj.Schema("reference")
6+
7+
8+
@schema
9+
class ScannerModel(dj.Lookup):
10+
definition = """
11+
scanner_model : varchar(32)
12+
---
13+
manufacturer : varchar(64)
14+
"""
15+
16+
17+
@schema
18+
class SegmentationMethod(dj.Lookup):
19+
definition = """
20+
seg_method : varchar(32)
21+
---
22+
method_notes : varchar(255)
23+
"""
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""The experimental record: sessions, scans, and what the scanner reported.
2+
3+
``Session`` depends on ``lab.Subject`` in its primary key and on ``lab.User`` as
4+
a secondary reference — the two foreign keys that the module-level figure bundles
5+
into the single ``lab → session`` edge.
6+
"""
7+
8+
import datajoint as dj
9+
10+
from .lab import Subject, User
11+
from .reference import ScannerModel
12+
13+
schema = dj.Schema("session")
14+
15+
16+
@schema
17+
class Session(dj.Manual):
18+
definition = """
19+
-> Subject
20+
session_date : date
21+
---
22+
-> User
23+
session_notes : varchar(255)
24+
"""
25+
26+
27+
@schema
28+
class Scan(dj.Manual):
29+
definition = """
30+
-> Session
31+
scan_idx : int32
32+
---
33+
-> ScannerModel
34+
depth : float64
35+
"""
36+
37+
38+
@schema
39+
class ScanInfo(dj.Imported):
40+
definition = """
41+
-> Scan
42+
---
43+
nframes : int32
44+
fps : float64
45+
"""

0 commit comments

Comments
 (0)