docs: computation-model corrections + full make() reproducibility contract (incl. tripartite)#202
Conversation
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.
|
Ready for review — resynced against Scope (see the updated PR description for detail): the transaction + single-key-testing best-practice corrections, the full five-rule |
MilagrosMarin
left a comment
There was a problem hiding this comment.
Thanks @dimitri-yatsenko — solid correctness pass. Verified against source:
- Transaction guidance.
Connection.start_transaction()atconnection.py:523raisesDataJointError("Nested connections are not supported.")when one's already open, and_populate_oneopens one per key atautopopulate.py:655. The old "wrap inwith dj.conn().transaction:" advice was outright broken; the# WRONGreplacement is right. - Testing pattern.
populate(*restrictions, max_calls=1)accepts a dict positionally, andT().make(key)genuinely bypassesstart_transaction,self._upstreamconstruction, and the finally-block cleanup. - Five-rule contract mirrors
autopopulate.md§4.3. - Tripartite phase contract matches the impl precisely:
make_fetchruns outside (line 707) then inside (line 713) withdeepdiff.DeepHashverification (lines 709+714);make_computeruns once outside (line 708), never re-verified;make_insertruns afterstart_transaction()(line 711) viagen.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.
|
Thanks @MilagrosMarin — great catch. Fixed the tripartite example to match the implementation' calling convention (
Re-requesting review. |
Corrections and clarifications to the Computation Model explanation and the AutoPopulate spec, prompted by review of explanation/computation-model. Two files:
explanation/computation-model.mdandreference/specs/autopopulate.md.Correctness fixes (Best Practices)
with dj.conn().transaction:insidemake(). Butpopulate()already opens a transaction per key, so amake()'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# WRONGexample.make()directly (T().make(key)), which bypassespopulate()— runs outside the per-key transaction, skips job reservation and error capture, writes as an uncontrolled side effect. Now recommendsT.populate(key, max_calls=1); notes a future no-insert debug mode as the only case where a directmake()call would be safe.The make() reproducibility contract, surfaced and completed
autopopulate.md§4.3).autopopulate.md§4.5, with a cross-reference from §4.3):make_fetchonly fetches,make_computeonly computes,make_insertonly inserts.make_fetchmust not insert and must be bitwise reproducible (it is re-run and hash-verified inside the transaction);make_computemust neither fetch nor insert but need not be deterministic — stochastic algorithms are allowed, as it runs once and is never re-verified;make_insertalways runs inside the transaction, so it may also fetch/compute.make_fetch/make_computeare safely testable directly (no DB writes); onlymake_insert/populate()writes.Terminology
References
Files:
src/explanation/computation-model.md(+124/−13),src/reference/specs/autopopulate.md(+14/−1).