Skip to content

docs: computation-model corrections + full make() reproducibility contract (incl. tripartite)#202

Merged
dimitri-yatsenko merged 12 commits into
mainfrom
fix/make-already-in-transaction
Jul 15, 2026
Merged

docs: computation-model corrections + full make() reproducibility contract (incl. tripartite)#202
dimitri-yatsenko merged 12 commits into
mainfrom
fix/make-already-in-transaction

Conversation

@dimitri-yatsenko

@dimitri-yatsenko dimitri-yatsenko commented Jul 15, 2026

Copy link
Copy Markdown
Member

Corrections and clarifications to the Computation Model explanation and the AutoPopulate spec, prompted by review of explanation/computation-model. Two files: explanation/computation-model.md and reference/specs/autopopulate.md.

Correctness fixes (Best Practices)

  • Transaction guidance was wrong. The page recommended wrapping multi-row inserts in with dj.conn().transaction: inside make(). But populate() already opens a transaction per key, so a make()'s inserts are already atomic — and opening a nested transaction raises an error (DataJoint does not support nested transactions). Replaced with an explanation of the built-in atomicity plus a # WRONG example.
  • Single-key testing antipattern. The page suggested testing by calling make() directly (T().make(key)), which bypasses populate() — runs outside the per-key transaction, skips job reservation and error capture, writes as an uncontrolled side effect. Now recommends T.populate(key, max_calls=1); notes a future no-insert debug mode as the only case where a direct make() call would be safe.

The make() reproducibility contract, surfaced and completed

  • Five rules, not three. The "make() Contract" section listed only the three mechanical steps (receives/computes/inserts); it now enumerates the full five-rule reproducibility contract inline and links the normative spec (autopopulate.md §4.3).
  • Tripartite phase contract (documented in both the explanation and, normatively, autopopulate.md §4.5, with a cross-reference from §4.3):
    • Simple rule up front — make_fetch only fetches, make_compute only computes, make_insert only inserts.
    • Precise boundaries: make_fetch must not insert and must be bitwise reproducible (it is re-run and hash-verified inside the transaction); make_compute must neither fetch nor insert but need not be deterministic — stochastic algorithms are allowed, as it runs once and is never re-verified; make_insert always runs inside the transaction, so it may also fetch/compute.
    • Consequence: make_fetch/make_compute are safely testable directly (no DB writes); only make_insert/populate() writes.
    • Framed as contract rules the author follows and a pipeline is validated against at review/deploy time — not runtime-enforced.

Terminology

  • Reproducibility here means tracked derivation — every row is traceable to its declared inputsnot bitwise determinism. Stochastic computation is allowed. Reworded the "same output" claims accordingly, and used lineage/reproducibility rather than "provenance" in these framework docs.

References

  • Added "See also" links (AutoPopulate / Cascade / Diagram specs, run-computations & distributed-computing how-tos, related concept pages).

Files: src/explanation/computation-model.md (+124/−13), src/reference/specs/autopopulate.md (+14/−1).

make() runs inside a transaction that populate() opens per key, so its inserts
are already atomic. The old 'Best Practices' item recommended wrapping inserts
in an explicit `with dj.conn().transaction:` block — which is not just
redundant but raises an error, since DataJoint does not support nested
transactions. Replace it with an explanation of make()'s built-in atomicity and
a WRONG example showing the nested-transaction error.

Also add a 'See also' section linking the AutoPopulate/Cascade/Diagram specs,
the run-computations and distributed-computing how-tos, and related concept
pages.
The 'make() Contract' section listed only the three mechanical steps
(receives/computes/inserts); the full five-rule reproducibility contract was
referenced only in the following prose, so readers saw 'three points'. Enumerate
the five rules (populate-only; one entity per call; read only the upstream cone;
write only to self and Parts; no other result-affecting input) inline and link
the AutoPopulate §4.3 spec where the full contract lives. Also de-duplicate the
now-redundant link in 'Why the contract matters'.
The 'Best Practices' section recommended testing by calling make() directly
(Segmentation().make(key)). That bypasses populate(): it runs outside the
per-key transaction (no rollback on partial/failed make()), skips job
reservation and error capture, and writes to the DB as an uncontrolled side
effect. Recommend populate(key, max_calls=1) instead, which exercises the real
machinery. Add a note that a direct make() call could become a safe dry-run
only if/when a no-insert test/debug mode is added (future, not current).
…afe testing

Document the phase responsibilities of the three-part make as an extension of the
reproducibility contract: make_fetch reads only (no compute, no insert),
make_compute is pure (no reads, no writes), make_insert writes only. Because the
read and compute phases never touch the database as a side effect, they can be
called and tested directly and safely — unlike make()/make_insert, which write.
Add that guidance to the single-key testing best practice.
Narrow the phase rules to what actually holds: make_fetch may compute (only must
not insert); make_insert always runs inside the transaction, so it may fetch and
compute in addition to inserting. The only hard restrictions are (a) make_fetch
must not insert and (b) make_compute must neither fetch nor insert, since
make_compute runs outside the transaction. Fetch/compute remain safely testable
because make_fetch does not write and make_compute is pure.
…job-per-phase rule

State the easy convention users should follow up front — make_fetch only
fetches, make_compute only computes, make_insert only inserts — then present the
narrower technical boundaries as the reason it is safe.
…time-enforced

The framework does not enforce the phase rules at runtime; they are part of the
make() reproducibility contract that the author follows and against which a
pipeline should be validated (at review or deploy time). Reworded both the
intro and the closing so neither implies runtime enforcement.
Add the per-phase rules to §4.5 (make_fetch must not insert; make_compute must
neither fetch nor insert; make_insert always runs in the transaction and may
also fetch/compute) as the normative source, and describe the tripartite
extension from the canonical §4.3 contract anchor with a cross-link. Framed as
contract rules validated at review/deploy time, not runtime-enforced —
consistent with the explanation in computation-model.
…ochastic

Add the reproducibility asymmetry to the tripartite phase contract in both the
spec (autopopulate §4.5) and the explanation (computation-model): make_fetch's
output is re-fetched and hash-verified inside the transaction, so it must be
bitwise reproducible; make_compute runs once and is never re-verified, so it may
use stochastic functions and need not be bitwise reproducible.
…e' wording

Per the framework's terminology (DataJoint 2.0 paper): reserve 'provenance' for
the industry/platform category and use 'traceable to declared inputs' /
'derivation' for the core model. Reframe the make() contract's reproducibility
as complete lineage/derivation from declared inputs (not bitwise determinism),
and remove the 'provenance' terms I had introduced. Stochastic make_compute
remains allowed; make_fetch must be bitwise reproducible.
@dimitri-yatsenko dimitri-yatsenko changed the title docs(computation-model): fix transaction guidance; add spec/related references docs: computation-model corrections + full make() reproducibility contract (incl. tripartite) Jul 15, 2026
@dimitri-yatsenko

Copy link
Copy Markdown
Member Author

Ready for review — resynced against main (clean, no conflicts; the autopopulate.md overlap with #195 auto-merged). Net change is two files: explanation/computation-model.md and reference/specs/autopopulate.md.

Scope (see the updated PR description for detail): the transaction + single-key-testing best-practice corrections, the full five-rule make() contract surfaced with the tripartite phase contract (now normative in autopopulate.md §4.5, make_fetch bitwise-reproducible / make_compute may be stochastic / make_insert always in-transaction), the reproducibility-≠-determinism framing, and the "See also" references.

MilagrosMarin
MilagrosMarin previously approved these changes Jul 15, 2026

@MilagrosMarin MilagrosMarin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dimitri-yatsenko — solid correctness pass. Verified against source:

  • Transaction guidance. Connection.start_transaction() at connection.py:523 raises DataJointError("Nested connections are not supported.") when one's already open, and _populate_one opens one per key at autopopulate.py:655. The old "wrap in with dj.conn().transaction:" advice was outright broken; the # WRONG replacement is right.
  • Testing pattern. populate(*restrictions, max_calls=1) accepts a dict positionally, and T().make(key) genuinely bypasses start_transaction, self._upstream construction, and the finally-block cleanup.
  • Five-rule contract mirrors autopopulate.md §4.3.
  • Tripartite phase contract matches the impl precisely: make_fetch runs outside (line 707) then inside (line 713) with deepdiff.DeepHash verification (lines 709+714); make_compute runs once outside (line 708), never re-verified; make_insert runs after start_transaction() (line 711) via gen.send() (line 717). "Bitwise reproducible for fetch, stochastic allowed for compute" is genuinely the shape of the guarantee, and stating it out loud is the clearest single win in this PR.

One pre-existing snag worth flagging (not this PR's regression): the tripartite worked example in computation-model.md shows def make_compute(self, key, fetched) with body (raw_signal,) = fetched, but the impl at autopopulate.py:305 calls self.make_compute(key, *fetched_data). So make_compute receives raw_signal, not the tuple — the destructuring breaks. Same shape for make_insert. Worth a small follow-up to either drop the tuple wrapping in the example returns or align the impl's unpacking.

Approving.

Per review (#202): the worked example and phase-contract bullets contradicted the
implementation. make() invokes make_compute(key, *fetched_data) and
make_insert(key, *computed_result) (autopopulate.py:283-311) — the returned
tuples are unpacked into positional args, and make_insert receives only the
computed result (not fetched). Corrected the computation-model worked example,
the How-It-Works pseudocode, the safe-testing snippet, and the phase-contract
bullets in both computation-model.md and autopopulate.md §4.5; also wrapped the
§4.5 example returns in tuples and fixed its make_fetch docstring.
@dimitri-yatsenko

Copy link
Copy Markdown
Member Author

Thanks @MilagrosMarin — great catch. Fixed the tripartite example to match the implementation' calling convention (make_compute(key, *fetched_data), make_insert(key, *computed_result) per autopopulate.py:283-311):

  • Worked example: make_compute(self, key, raw_signal) and make_insert(self, key, avg) now receive the unpacked tuple (no more (raw_signal,) = fetched, and make_insert no longer takes a fetched param it never receives).
  • Fixed the "How It Works" pseudocode and the safe-testing snippet to show *fetched / *computed.
  • Corrected the same signatures in the phase-contract bullets (both computation-model.md and autopopulate.md §4.5).
  • While there: autopopulate.md §4.5's example returned bare scalars where the impl *-unpacks — wrapped those returns in tuples and fixed the make_fetch docstring ("runs outside the transaction; re-run and hash-verified").

Re-requesting review.

@MilagrosMarin MilagrosMarin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks

@dimitri-yatsenko dimitri-yatsenko merged commit 607b381 into main Jul 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants