Skip to Content
ReferenceAPIProvenance and auditing

Provenance and auditing

What a run recorded about itself: the versions every tool actually ran at, every parameter as it was hashed, and the references by accession and checksum — and, the other way round, what a number you published rests on.

Methods

class Methods( steps: 'list[StepMethods]' = <factory>, gaps: 'list[MethodsGap]' = <factory>, references: 'list[PinnedReference]' = <factory>, citations: 'list[str]' = <factory>, uncited: 'list[str]' = <factory>, claims: 'list[ClaimCheck]' = <factory>, engines: 'list[str]' = <factory>, platforms: 'list[str]' = <factory>, summary: 'str' = '', text: 'str' = '', bundle: 'Bundle | None' = None, verification: 'Verification | None' = None, ) -> None

How a set of results was produced.

Attributes

AttributeDescription
stepsEach distinct way a step was run, in the order the pipeline runs them.
gapsSteps nothing is known about.
referencesEvery reference any step was given, pinned, with no duplicates.
citationsThe reference list, as a methods section prints it.
uncitedTools with nothing to cite — named rather than omitted, so a short reference list cannot pass for a complete one.
claimsEach claim the pipeline declared, and what became of it. Empty for a pipeline that declared none, which is why nothing about an existing write-up changes.
enginesEvery version of Ligase that produced any of the results. One entry for a run on one machine; more for a distributed run where a worker was on an older wheel.
platformsEvery platform any of the results were produced on. One entry for a run on one machine; more for a mixed cluster, or an ARM workstation beside x86 nodes.
summaryOne line — 12 steps, all accounted for.
textThe paragraph and the reference list, as the command line prints them.
bundleThe bundle written, where one was asked for.
verificationWhat checking a bundle found, where one was asked for.

Methods.accounted_for

accounted_for: bool # property

Whether every step’s production is recorded and every claim bound.

False means the write-up has a hole in it and says so. A number the analysis does not support is such a hole: a property with this name that returned True for one would be making the engine’s own promise falsely.

Methods.tools

tools: list[tuple[str, str]] # property

Every tool any step used, with the version it ran at.

Methods.unbacked

unbacked: list[ClaimCheck] # property

The claims this write-up cannot stand behind.

Contradicted ones included: a number the run positively disagrees with is not a number the write-up stands behind.

StepMethods

class StepMethods( step: 'str', nodes: 'int', scopes: 'list[str]', scope_kind: 'str | None', tools: 'list[tuple[str, str]]', parameters: 'list[tuple[str, str]]', command: 'str | None', ) -> None

One way a step was run.

One written step becomes one node per sample, and all of them are one entry here. Where two nodes of a step genuinely were run differently they are two entries, because then the difference is the thing worth reporting.

Attributes

AttributeDescription
stepThe step’s name, as written.
nodesHow many nodes this covers.
scopesWhat tells them apart, e.g. the sample identifiers.
scope_kindWhat kind of thing that is, e.g. sample.
toolsEach tool and the whole version it printed.
parametersEach plain parameter and its value, as it was hashed.
commandThe command as written in the pipeline, placeholders and all, or None for a step whose body is Python.

MethodsGap

class MethodsGap(step: 'str', scopes: 'list[str]', nodes: 'int') -> None

A step nothing is known about.

Its output is in the cache from a run that recorded no provenance, and nothing can reconstruct how it was produced: the command that would have said so is not in the hash, by design. ligase run --force runs it again and records it.

Attributes

AttributeDescription
stepThe step’s name.
scopesWhat tells its nodes apart.
nodesHow many of them are unaccounted for.

Claim

class Claim( text: 'str', step: 'str', scope: 'str | None' = None, parameter: 'str | None' = None, tool: 'str | None' = None, value: 'str | float | None' = None, at: 'Cell | None' = None, tolerance: 'float | None' = None, ) -> None

A number or a phrase a paper states, and where it came from.

pipe = lg.Pipeline( outputs, claims=[ lg.Claim( “variants were called above 1% allele frequency”, step=“call”, parameter=“min_freq”, value=0.01, ), ], )

text and value are separate on purpose. A paper writes 1%; the pipeline holds 0.01. Comparing the sentence against the parameter would be a guess about arithmetic in English, so the sentence is what a reader greps the write-up for and value is what the engine compares — written the way the pipeline holds it.

Everything except text is checked against the run: the step is one the write-up accounts for, the sample is one it ran on, the parameter or tool is one the record holds, and the value agrees. A claim that survives all of that is bound to the step key and the output hash behind it; one that does not is reported rather than reprinted, because a declaration nothing can check is a comment.

A claim never enters a cache key, so editing one reruns nothing.

Raises

RaisesWhen
ValueErrorIf it names both a parameter and a tool, gives a value with nothing to compare it against, names either with no value, gives at without a value and a tolerance, or gives at beside a parameter or a tool — each of which is a declaration with nothing behind it, or two declarations in one.

Attributes

AttributeDescription
textThe sentence as the paper states it. Never checked, and cannot be.
stepThe step that produced it, by name.
scopeThe sample it is about, where it is about one. None means the step across every sample it ran on, which is right for “every sample was interpreted” and wrong for a count from one of them. There is no inference.
parameterThe parameter it restates, where it restates one.
toolThe tool whose version it states, where it states one.
valueWhat the paper says that parameter or version is, rendered the way the run recorded it — or, beside at, the number the paper publishes.
atWhere in the step’s output the number lives, from lg.cell. Given, the engine reads that cell and compares: the claim comes back verified or contradicted rather than merely bound. Left out, the claim rests on the computation and the number is not checked, which is what it has always done.
toleranceHow far the number in the output may be from the one the paper states. Required with at, and never defaulted: a claim to three decimals and a claim to machine precision are different claims, and a default would invent a precision nobody stated.

Claim.located

located: tuple[float, str, int, float] | None # property

The number, the cell and the tolerance, as the engine takes them.

None for a claim that names no cell, which is most claims. __post_init__ has already refused every incomplete combination, so this reads the three parts without checking them again.

Claim.rendered

rendered: str | None # property

The value as the engine compares it: text, however it was given.

value=0.01 is what a person writes and 0.01 is how the parameter was hashed, so one str() is the whole conversion. A number in a paper is text.

None for a located claim, whose value is a number the engine compares numerically rather than a string the record holds.

ClaimCheck

class ClaimCheck( claim: 'Claim', verdict: 'str', reason: 'str', recorded: 'str | None', bound: 'list[BoundTo]' = <factory>, ) -> None

One claim, and what the run had to say about it.

Attributes

AttributeDescription
claimThe claim as it was declared.
verdictbound, verified, unbacked or contradicted. verified is bound and more: the number was read out of the output and agrees. contradicted for a located claim means the number in the sentence is not the number in the bytes.
reasonThe sentence the write-up prints for it, so a script reporting one claim says what ligase methods says.
recordedWhat the run recorded, where that disagrees with the claim — and, for a verified claim, the number that was read. None otherwise.
boundThe computations it rests on. Empty for an unbacked claim; one entry for a claim naming a sample, and one per sample for a claim that names none.

ClaimCheck.is_bound

is_bound: bool # property

Whether the write-up can stand behind the claim.

True for a verified claim as well as a bound one: verified is bound and more, so a property that excluded it would make accounted_for false for the claims the engine is most sure of.

ClaimCheck.is_verified

is_verified: bool # property

Whether the number was read out of the output and agreed.

Cell

class Cell(column: 'str', row: 'int' = 0) -> None

A cell of a step’s output: a column by name, a row by position.

lg.Claim( “malaria F1 = 0.333 against seven curated targets”, step=“validate”, value=0.333, at=lg.cell(column=“f1”, row=0), tolerance=5e-4, )

Row 0 is the first row after the header, because that is the first row of data and a claim is about data. A ranking’s top row is therefore row 0, which is the row its own rank 1 is.

The engine reads the place it is given and nowhere else. It will not go looking for a number in output it was not pointed at, and it parses one format — a TSV with a header — rather than guessing at others. A claim that cannot say where its number is stays step-only, which is a legitimate state and not an error.

Attributes

AttributeDescription
columnThe column’s name, as the output’s header spells it.
rowThe row’s position among the data rows, counting from zero.

cell

cell(*, column: 'str', row: 'int' = 0) -> 'Cell'

Name the cell of a step’s output a claim’s number lives in.

Parameters

ParameterDescription
columnThe column’s name, as the output’s header spells it.
rowThe row’s position among the data rows, from zero. Defaults to the first row of data, which is the common case for a one-row summary table.

Returns

A location a Claim can carry in at=.

BoundTo

class BoundTo( scope: 'str | None', key: 'str', canonical: 'str', recorded_at: 'int', platform: 'str', ) -> None

One computation a claim rests on.

The step key, not the occasion. A key is the identity of a computation — the same code, parameters, inputs and tool versions — so a step served from the cache backs its claim completely, and what this adds is when that computation happened, which may be long before the run that wrote the claim up.

Attributes

AttributeDescription
scopeThe sample it was for, where anything tells the nodes apart.
keyThe step key, in full.
canonicalThe canonical hash of what it produced.
recorded_atWhen the computation happened, as seconds since the Unix epoch — not when this run fetched it.
platformThe platform it happened on.

Bundle

class Bundle( root: 'str', files: 'list[str]', steps: 'int', gaps: 'int', ) -> None

A reproducibility bundle that was written.

Attributes

AttributeDescription
rootWhere it is.
filesEverything in it, relative to the root.
stepsHow many steps it accounts for.
gapsHow many it could not.

Verification

class Verification( results: 'list[ResultCheck]' = <factory>, inputs: 'list[InputCheck]' = <factory>, reproduced: 'bool' = False, verdict: 'str' = 'missing', summary: 'str' = '', text: 'str' = '', ) -> None

What checking a bundle against this machine found.

Attributes

AttributeDescription
resultsEvery result the bundle recorded.
inputsEvery input it recorded.
reproducedWhether every recorded result came out the same.
verdictThe worst thing found.
summaryOne line — 12 results: 4 identical, 8 reproduced.
textThe whole report as the command line prints it.

Verification.differing

differing: list[ResultCheck] # property

The results that did not come out the same.

Verification.moved

moved: list[InputCheck] # property

The inputs that are not the ones the bundle was built from.

Worth reading before any differs verdict: a changed input explains a changed output, and most of a bundle’s inputs are named rather than carried — a local file by content hash, a pinned remote by locator and digest — so they are the ones that can have moved. A recorded query cannot appear here, because its response body travels in the bundle and is replayed from it.

PinnedReference

class PinnedReference( param: 'str', path: 'str', build: 'str | None', accession: 'str | None', contigs: 'int', length: 'int', checksum: 'str | None', fingerprint: 'str', ) -> None

A reference sequence, pinned as firmly as its file allows.

Nothing here is declared or guessed. The accession is the sequence name where the name is one — K03455.1 carries its own version — and the checksum is the MD5 the SAM specification defines for @SQ M5, over the uppercased sequence with whitespace removed.

Attributes

AttributeDescription
paramThe parameter it filled.
pathWhere it was when the step ran.
buildThe label it was declared under, e.g. HXB2. Display only.
accessionThe accession with its version, where the sequence name is one, and None where it is not — chr1 is a name, not an accession, and a methods section must not cite it as one.
contigsHow many sequences it holds.
lengthTheir total length in bases.
checksumThe MD5 of the sequence, for a single-sequence reference.
fingerprintA short digest of the whole identity, always present.

AuditReport

class AuditReport( steps: 'list[StepAudit]' = <factory>, failures: 'list[StepFailure]' = <factory>, verdict: 'str' = 'deterministic', summary: 'str' = '', text: 'str' = '', ) -> None

What an audit found.

Attributes

AttributeDescription
stepsEvery node audited, in graph order.
failuresEvery node that could not be audited because it failed.
verdictThe worst verdict any node got — the worst rather than the commonest, since one irreproducible sample in a cohort is an irreproducible step.
summaryOne line — 4 steps: 1 deterministic, 3 reproducible.
textThe whole report as the command line prints it, worded by the engine so that every surface reads the same.

AuditReport.reproducible

reproducible: bool # property

Whether every node audited agreed with itself.

AuditReport.unreproducible

unreproducible: list[StepAudit] # property

The nodes whose two runs disagreed about the result.

StepAudit

class StepAudit( step: 'str', sample: 'str | None', scope: 'str | None', verdict: 'str', first: 'str', second: 'str', first_canonical: 'str', second_canonical: 'str', first_duration_ms: 'int', second_duration_ms: 'int', node: 'int', seed: 'tuple[str, str] | None' = None, ) -> None

What two runs of one step showed.

Attributes

AttributeDescription
stepThe step’s name.
sampleThe sample it was for, or None for a step that spans more than one.
scopeHow a report names this node where its step’s name is shared — sample SRR8525904 — or None where the name is already unambiguous.
verdictdeterministic, reproducible, or not reproducible.
firstThe first run’s output hash.
secondThe second run’s output hash.
first_canonicalWhat the first run’s output means.
second_canonicalWhat the second run’s output means.
first_duration_msHow long the first run took.
second_duration_msHow long the second run took.
nodeWhich node of the graph it was.
seedThe parameter this step declared as its seed and the value it was held at, as ("seed", "42"). None for a step that declared none. A step with a seed is never deterministic, however its bytes fall: byte equality under a fixed seed says the seed held, not that the step has no randomness in it. The reproducible it gets instead is the best verdict available to it.

StepAudit.reproducible

reproducible: bool # property

Whether the two runs agreed on the result.

True for a step whose bytes moved and whose meaning did not: that is what reproducible means here.

Report

class Report( errors: 'list[CheckError]', summary: 'str', text: 'str', ) -> None

What a check found.

Attributes

AttributeDescription
errorsOne error per failing step, in the order the steps run. A problem that hits four samples is four errors, each naming its own sample.
summaryOne line summarising the lot — 4 errors in 4 steps. — and the steps it counts are places rather than names, since one written step is many steps under fan-out.
textThe whole report as the command line prints it: each distinct problem stated once, with every sample it affects named, and the summary at the end. Worded by the engine so that every surface reports a check the same way.

RunReport

class RunReport( steps: 'list[StepRun]' = <factory>, failures: 'list[StepFailure]' = <factory>, summary: 'str' = '', text: 'str' = '', hashed: 'int' = 0, prefiltered: 'int' = 0, canonicalised: 'int' = 0, worker: 'str | None' = None, divergences: 'list[Divergence]' = <factory>, ) -> None

Everything a run did.

Attributes

AttributeDescription
stepsEvery step that produced an output, in graph order.
failuresEvery step that did not.
summaryOne line — 12 steps: 8 ran, 4 from cache. — counting places rather than names, since one written step is many steps under fan-out.
textThe whole report as the command line prints it, worded by the engine so that every surface reads the same.
hashedInput files that had to be read and hashed.
prefilteredInput files whose hash was already known from their size and mtime, and so were not read at all.
canonicalisedFiles that had to be parsed to find out what they mean. Like hashed, this is zero on a run that found everything it needed already known.
workerWhat this worker called itself, or None for an ordinary run.
divergencesSteps another worker computed to a different answer. Empty for almost every run, and for every run on one machine.

RunReport.cached

cached: list[StepRun] # property

The steps that were served from the cache.

RunReport.outputs

outputs(self, step: 'str') -> 'list[str]'

Where one step’s outputs are, in graph order.

Parameters

ParameterDescription
stepThe step’s name. Under fan-out there is one output per sample.

Returns

One path per node of that name.

RunReport.ran

ran: list[StepRun] # property

The steps that actually ran, in graph order.

RunReport.skipped

skipped: list[StepRun] # property

The steps an upstream rerun turned out not to change.

StepRun

class StepRun( step: 'str', sample: 'str | None', outcome: 'str', output: 'str', key: 'str', hash: 'str', canonical: 'str', duration_ms: 'int', node: 'int', notes: 'tuple[str, ...]' = (), ) -> None

One step that produced an output.

Attributes

AttributeDescription
stepThe step’s name.
sampleThe sample it was for, or None for a step that spans more than one — naming such a step after one of its samples would send you to the wrong data.
outcomeran, cached, or skipped. skipped is early cutoff: the step’s key was already known although something above it reran, because that rerun produced the same output as before.
outputWhere the output is, in the content-addressed store.
keyThe cache key, as hex.
hashThe hash of the output’s bytes, as hex, which is also its name in the store.
canonicalThe hash of what the output means, as hex: its records sorted and its volatile headers stripped. This is what the steps below were keyed on, so two runs that agree here skip everything downstream however far their bytes diverged.
duration_msHow long it took, when it ran; zero otherwise.
nodeWhich node of the graph it was.
notesWhat was noticed about this output and its hash cannot say — one sentence each, and usually none. A note is not a failure: the step ran and its result stands. Two things put them here. Canonicalising the output finds the first kind, of which there is one in v0.1: a sample identifier that is a file name rather than a sample, which makes this output’s canonical hash depend on where that file was. The step’s own body says the second kind — an assumption its work rested on, which the file it wrote need not show. A resistance reading made without coverage is taking every position the caller did not report to be reference, and says so here (D32).

StepRun.ran

ran: bool # property

Whether this step actually ran, rather than being served.

Last updated on