Standards#
Every method in Andrey is held to the requirements below. Each section states a requirement, how we check it, and where the exceptions and the gaps in the check are recorded. The tests that run on every pull request cover every method; the quality gate, the reference comparisons, and the benchmark campaigns cover the 49 supported ones (Testing).
The setting its paper claims#
A method is judged in the setting its paper claims: the model, the data regime, and the scale. Its docstring states that setting in a When to use. paragraph, and where it stops working in a Limitations. paragraph.
Check. tests/unit/test_spec_registry.py fails when a docstring lacks either paragraph. The
recovery tests (tests/recovery/), the quality gate (qa/quality_gate.py), and the benchmark
campaigns draw their data from the claimed setting; review, not a test, checks that they do.
Exceptions. A benchmark row measured outside the claim carries "claim": "out" in its
published summary: its release card reports it, and no gate judges it.
Agreement with reference implementations#
Where the authors’ code or a standard package implements a method, Andrey is never less accurate than it on the same input in a setting that repeats on fresh data, and every difference has a cited cause.
Check. The campaigns record each setting’s paired difference in errors in
benchmarks/published/<release>/summary.json. From 0.8.0, qa/reference_parity.py counts a
setting as worse when the bootstrap 95% interval of that difference, over at least 10 paired
datasets, lies above 0; a setting rerun on fresh datasets is judged by the rerun. The release
quality gate prints each worse setting, and the release checklist reviews every one.
Exceptions. benchmarks/published/parity-allowlist.json gives each worse setting’s cause, with
a citation: Andrey follows the paper and the reference does not, the reference has a defect, the
two break an exact tie differently, or a default trades this setting for others. A worse setting
does not fail the gate on its own, a setting with fewer than 10 paired datasets is reported and not
judged, and the summaries before 0.8.0 compare counts or means without an interval.
Better than the empty graph#
In its claimed setting, a method makes fewer errors than the empty graph, which has no edges.
Check. The quality gate passes a method on a dataset only when its median SHD is below the
empty graph’s, and every supported method must pass before a release merges. Where a reference
makes significantly fewer errors than the empty graph, qa/reference_parity.py requires the same of
Andrey. A fit that does not finish in time never counts as an empty graph.
Exceptions. A method’s Limitations. paragraph and its release card give the settings where it does no better than the empty graph.
How far each method runs#
Each method’s reach is measured: the largest number of variables, at a given number of rows and density, at which it finishes within fixed time and memory caps and beats the empty graph.
Check. The 0.10.0 release card measured it, with a 1-hour
and a 28 GB cap on 4 cores. src/andrey/api/_boundary_table.py holds the result, a fit past its
method’s boundary warns with a BoundaryWarning, and
tests/unit/test_boundary.py fails when the table and the newest published boundary differ.
Exceptions. The map covers the methods Andrey had at 0.10.0. A method added since has no map and no warning; its Limitations. paragraph gives the sizes its campaign measured.
Column order#
The order of the columns says nothing about the causal structure, so reordering them returns the same graph with its nodes relabeled.
Check. tests/unit/test_column_labels.py refits every method whose registry entry says it
relabels on other orders of three columns and compares the edges by name. Ties between equal scores
go to a rank computed from the data, not to a column’s position
(How Andrey breaks ties).
qa/identity.py --shuffle K refits each supported method on K column orders of its quality-gate
datasets and lists every output that changes.
Exceptions. Each method’s column_order in the registry records whether reordering relabels its
result, and the column-order table prints it with the reason.
qa/identity.py runs by hand and does not refit ICP, IGSP, FCI-JCI, or J-PCMCI+; the last release
card that reports a run is 0.9.0’s.
Units and transforms#
Rescaled columns. Rescaling a column, a change of its units, returns the same graph unless the
method’s published algorithm reads the units. tests/unit/test_column_transforms.py fits every
method that takes a table with each column multiplied by its own power of two, from 2**-10 to
2**10, and again with environments and on mixed data where a method takes them, and requires the
same graph, mark for mark. The registry marks calm, notears, dagma, ica_lingam, and dynotears as reads_units: each must
change its graph there instead, so the mark cannot go stale, and its page says how it reads the
units. tests/unit/test_offset_invariance.py shifts every column, or scales and shifts them all
alike, and requires the same graph from every method. Each check runs on one small dataset per kind
of input, so it covers the code those data reach.
Benchmark data. A benchmark must not reward a method for reading the simulation’s units: in
linear data drawn with the usual weights, a variable’s variance, and the share of it the other
variables explain, its , grow along the causal order. The benchmarks’ linear and nonlinear
regimes z-score every column, which sets var-sortability to 0.5, and the _unitless regimes, like
andrey.data.sample_scm(scale="unitless"), bring -sortability close to 0.5 as well
(Scale). Every benchmark record carries its dataset’s varsortability and
r2_sortability, and tests/unit/test_data_sortability.py checks both measures. Z-scoring leaves
-sortability as in the raw data, and the unitless regimes and the controls that sort the
columns by variance or by appear only in the campaigns whose cards name them, such as the
0.17.0 comparisons of NOTEARS, DAGMA, SCORE, and NoGAM.
Monotone transforms and category codes. Under a rank-based test or score, a strictly increasing
transform of a column, such as a log, returns the same graph; under a categorical test or score, so
does recoding the categories. tests/unit/test_core_ci.py checks that the p-values of
indep_test="normal_scores" do not change under such transforms, tests/unit/test_core_score.py
that the scores of score_func="normal_scores" do not, and tests/unit/test_core_discrete.py that
"chisq", "gsq", BDeu, and the categorical BIC do not depend on the codes.
tests/unit/test_column_transforms.py refits methods with each column through its own increasing
map and requires the same graph: pc, fci, cdnod, fci_jci, iamb, inter_iamb, mmpc, igsp, ut_igsp, pcmci, pcmciplus, jpcmciplus, and lpcmci, which read the data only through their
test, with indep_test="normal_scores"; ges, xges, hc, tabu, exact_search, boss, and grasp, which read it only through
their score, with score_func="normal_scores"; and gfci, mmhc, rsmax2, boss_fci, and grasp_fci, which take both,
with both, each method that takes the score on ten datasets. With their default score, the methods
that take both read the values, so a monotone transform can change their graph even under the
rank-based test. The file also refits every method that takes "chisq" or "gsq" with each
column’s categories recoded in order. A method that scores categorical data without a test, such as
ges with score_func="bdeu", is checked through its score alone. Both "normal_scores" options
are experimental, and the score takes no env.
Faster paths, the same output#
A faster path returns the same output as the path it replaces: the same graph, weights, causal order, and metadata, bit for bit.
Check. On every pull request, tests/unit/test_parallel_ges.py, test_parallel_skeleton.py,
and test_parallel_dsep.py compare the worker pools of GES, the PC skeleton, and FCI’s
possible-d-separation search with the serial search, and the PyTorch paths must match NumPy
(Testing). For a speed change, qa/identity.py hashes every supported method’s full
output, floats as bytes, on the base commit and on the change; --compare fails on any changed
output or any rise in the number of queries a search makes.
Exceptions. For one speed change, qa/identity.py runs by hand. Each release runs it against
the release before, and Release checks gives the results. The GPU
parity suite runs on our own machines, not in CI, and agrees with NumPy within a tolerance.
Workers and machines#
A fit returns the same result with any number of worker processes and on any machine.
Check. The pool tests above require identical results on one and on several workers, and the
recovery tests the same result on every run. qa/identity.py --workers N refits PC, FCI, FCI-JCI,
GFCI, CD-NOD, RSMAX2, GES, and PNL on N workers. Scores are compared after rounding, ties go to
ranks computed from the data, and most methods run their linear algebra on one BLAS thread, so the
host’s thread count cannot change their result.
Exceptions. Across machines names the methods whose result can change on another machine, and why. No check compares outputs across machines; the one published measurement is ICA-LiNGAM’s, on six BLAS kernels, in the same guide.
Release checks#
Before a release merges, qa/identity.py fits every supported method on the release and on the
release before it, on complete data with default arguments, and compares the outputs bit for bit.
The quality gate and the reference comparisons run on the same code.
Release |
Outputs against the release before |
Quality gate |
Reference comparisons |
|---|---|---|---|
0.17.1 |
49 methods, 1,963 outputs, none changed |
145 checks, 0 failed |
2,971 rows, 0 failed |
0.17.0 |
45 methods, 1,819 outputs, none changed; 4 methods new |
145 checks, 0 failed |
2,971 rows, 0 failed |