track-certify 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- track_certify-0.1.0/LICENSE +21 -0
- track_certify-0.1.0/MANIFEST.in +1 -0
- track_certify-0.1.0/PKG-INFO +174 -0
- track_certify-0.1.0/README.md +145 -0
- track_certify-0.1.0/examples/01_certify_streaming.py +22 -0
- track_certify-0.1.0/examples/02_design_budget.py +12 -0
- track_certify-0.1.0/examples/03_robust_unknown_sigma.py +19 -0
- track_certify-0.1.0/pyproject.toml +47 -0
- track_certify-0.1.0/setup.cfg +4 -0
- track_certify-0.1.0/src/track_certify/__init__.py +49 -0
- track_certify-0.1.0/src/track_certify/_vendor/__init__.py +58 -0
- track_certify-0.1.0/src/track_certify/_vendor/assignment_oracle.py +100 -0
- track_certify-0.1.0/src/track_certify/_vendor/best_alternative.py +245 -0
- track_certify-0.1.0/src/track_certify/_vendor/finite_ray_model.py +198 -0
- track_certify-0.1.0/src/track_certify/_vendor/track_and_certify_general.py +368 -0
- track_certify-0.1.0/src/track_certify/api.py +354 -0
- track_certify-0.1.0/src/track_certify/cli.py +141 -0
- track_certify-0.1.0/src/track_certify/design.py +176 -0
- track_certify-0.1.0/src/track_certify/instances.py +137 -0
- track_certify-0.1.0/src/track_certify/py.typed +0 -0
- track_certify-0.1.0/src/track_certify/robust.py +105 -0
- track_certify-0.1.0/src/track_certify.egg-info/PKG-INFO +174 -0
- track_certify-0.1.0/src/track_certify.egg-info/SOURCES.txt +30 -0
- track_certify-0.1.0/src/track_certify.egg-info/dependency_links.txt +1 -0
- track_certify-0.1.0/src/track_certify.egg-info/entry_points.txt +2 -0
- track_certify-0.1.0/src/track_certify.egg-info/requires.txt +5 -0
- track_certify-0.1.0/src/track_certify.egg-info/top_level.txt +1 -0
- track_certify-0.1.0/tests/test_cli.py +31 -0
- track_certify-0.1.0/tests/test_design.py +70 -0
- track_certify-0.1.0/tests/test_equivalence.py +141 -0
- track_certify-0.1.0/tests/test_instances_robust.py +55 -0
- track_certify-0.1.0/tests/test_packaging.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Track-and-Certify authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include examples *.py
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: track-certify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Anytime joint certification of causal graphs and unknown intervention targets: certify-or-refuse sequential experiments with instance-optimal adaptive sampling
|
|
5
|
+
Author: The Track-and-Certify authors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/track-certify/
|
|
8
|
+
Keywords: causal-discovery,causal-inference,experimental-design,sequential-testing,e-process,anytime-valid,best-arm-identification,intervention-targets,fixed-confidence
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.22
|
|
25
|
+
Requires-Dist: scipy>=1.8
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# track-certify
|
|
31
|
+
|
|
32
|
+
**Anytime joint certification of causal graphs and unknown intervention
|
|
33
|
+
targets** — a certify-or-refuse layer for sequential causal experiments,
|
|
34
|
+
with instance-optimal adaptive sampling.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install track-certify
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Pure Python; depends only on `numpy` and `scipy`.
|
|
41
|
+
|
|
42
|
+
## The problem it solves
|
|
43
|
+
|
|
44
|
+
You run interventional experiments (gene perturbations, drug assays, A/B
|
|
45
|
+
interventions) where **the perturbed variables themselves are uncertain**: a
|
|
46
|
+
reagent may act off-target, weakly, or not at all. A causal conclusion is then
|
|
47
|
+
a *joint* claim — the graph **and** each environment's intervention target —
|
|
48
|
+
and every additional batch of samples costs money. Two questions matter more
|
|
49
|
+
than any point estimate:
|
|
50
|
+
|
|
51
|
+
1. **Where should the next observation be taken?**
|
|
52
|
+
2. **When is it safe to stop and certify the joint answer at error level δ?**
|
|
53
|
+
|
|
54
|
+
`track-certify` answers both with guarantees: an anytime-valid e-process
|
|
55
|
+
stopping rule (no peeking penalty, no multiplicity correction), a no-regret
|
|
56
|
+
allocation learner that provably attains the instance's information-theoretic
|
|
57
|
+
sample requirement `T*`, and an exact assignment oracle that replaces
|
|
58
|
+
factorial target enumeration with polynomial rectangular assignment. When it
|
|
59
|
+
cannot certify — budget cap reached, or model preconditions infeasible — it
|
|
60
|
+
**refuses rather than guesses**.
|
|
61
|
+
|
|
62
|
+
## 60 seconds: certify a joint answer
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import numpy as np
|
|
66
|
+
import track_certify as tc
|
|
67
|
+
|
|
68
|
+
# Declare the class: 2 candidate graphs over a known covariance,
|
|
69
|
+
# 2 environments with unknown, distinct targets of unknown amplitude.
|
|
70
|
+
sigma = np.array([[1.0, 0.5],
|
|
71
|
+
[0.5, 1.0]])
|
|
72
|
+
model = tc.build_model(sigma, {"X->Y": (0, 1), "Y->X": (1, 0)})
|
|
73
|
+
|
|
74
|
+
cert = tc.Certifier(model, n_environments=2, delta=0.01, cap=200_000)
|
|
75
|
+
|
|
76
|
+
while True:
|
|
77
|
+
e = cert.next_environment() # which environment to sample now
|
|
78
|
+
y = run_my_experiment(e) # one fresh d-vector from environment e
|
|
79
|
+
out = cert.observe(y)
|
|
80
|
+
if out.stopped:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
out.certificate # ("X->Y", (0, 1)) — graph + per-environment targets,
|
|
84
|
+
# wrong with probability <= 0.01 under optional stopping;
|
|
85
|
+
# None if out.refused (cap reached: refusal, not a guess)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The certifier never receives the true hypothesis, the amplitudes, `T*`, or
|
|
89
|
+
the optimal allocation.
|
|
90
|
+
|
|
91
|
+
## Before you sample: how hard is your instance?
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
rep = tc.characteristic_time(model, ("X->Y", (0, 1)), amplitudes=(1.5, 1.2))
|
|
95
|
+
rep.tstar # first-order samples per unit log(1/delta)
|
|
96
|
+
rep.w_star # optimal budget split across environments
|
|
97
|
+
rep.active_kinds # which wrong answers dominate: 'graph' / 'target' / 'coupled'
|
|
98
|
+
rep.budget(0.01) # first-order sample estimate at delta = 0.01
|
|
99
|
+
|
|
100
|
+
tax = tc.coupling_tax(model, ("X->Y", (0, 1)), (1.5, 1.2))
|
|
101
|
+
tax.tax # how much harder JOINT certification is than the harder
|
|
102
|
+
# conditional problem — near the identifiability boundary
|
|
103
|
+
# this diverges, and "estimate targets, then certify the
|
|
104
|
+
# graph" becomes unboundedly suboptimal
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Unknown covariance: certify or refuse, with explicit constants
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
obs = collect_observational_samples() # (n0, d), no interventions
|
|
111
|
+
try:
|
|
112
|
+
setup = tc.robust_certifier(
|
|
113
|
+
obs, {"X->Y": (0, 1), "Y->X": (1, 0)},
|
|
114
|
+
n_environments=2, delta=0.01, c_max=3.0)
|
|
115
|
+
except tc.Refusal as r:
|
|
116
|
+
print("infeasible for this n0/delta/d/c_max:", r) # pre-start refusal
|
|
117
|
+
else:
|
|
118
|
+
cert = setup.certifier # threshold already carries the certified
|
|
119
|
+
# envelope inflation — nothing calibrated
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Benchmark instances with theorem-known difficulty
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
inst = tc.solvable_instance(r=0.9) # closed-form T*, coupled alternative
|
|
126
|
+
# provably active (paper Prop. 3)
|
|
127
|
+
inst = tc.scalable_instance(rs=[0.8, 0.9], cs=[1.0, 1.2])
|
|
128
|
+
# additive T*, 2^m graphs (Cor. 1)
|
|
129
|
+
res = tc.simulate(inst.model, inst.truth, inst.amplitudes, delta=1e-3,
|
|
130
|
+
rng=np.random.default_rng(0))
|
|
131
|
+
res.tau, res.correct # compare against inst.tstar_closed_form
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run `track-certify demo` for a self-contained certification demo, or
|
|
135
|
+
`track-certify envelope --n0 500000 --delta 0.01 --d 3 --c-max 1.0` to check
|
|
136
|
+
robust-mode feasibility from the command line.
|
|
137
|
+
|
|
138
|
+
## Guarantees, precisely
|
|
139
|
+
|
|
140
|
+
* **Validity.** With a correctly declared class and known covariance, the
|
|
141
|
+
returned joint answer is wrong with probability at most `delta`, under
|
|
142
|
+
arbitrary adaptive sampling and optional stopping (Gaussian mixture
|
|
143
|
+
e-process + Ville's inequality; no union bound over hypotheses).
|
|
144
|
+
* **Optimality.** Expected stopping time satisfies
|
|
145
|
+
`E[tau]/log(1/delta) -> T*` as `delta -> 0` — the change-of-measure lower
|
|
146
|
+
bound for *any* delta-correct procedure, attained.
|
|
147
|
+
* **Estimated covariance.** With the split-sample envelope, validity is exact
|
|
148
|
+
whenever the envelope's computable feasibility conditions hold; otherwise
|
|
149
|
+
the workflow refuses before starting. Efficiency degrades by explicit,
|
|
150
|
+
measured constant factors.
|
|
151
|
+
* **Refusal semantics.** A reached cap or an infeasible envelope produces a
|
|
152
|
+
refusal, never a certificate. There is no procedure here that converts
|
|
153
|
+
arbitrary data into a certificate.
|
|
154
|
+
|
|
155
|
+
**Model scope** (checked where checkable, refused when violated): finite
|
|
156
|
+
declared graph class sharing one positive-definite covariance; mean-shift
|
|
157
|
+
interventions with one unknown, distinct target per environment; Gaussian
|
|
158
|
+
noise; no hidden confounding. Duplicate or ray-identical graph declarations
|
|
159
|
+
are refused by default (they make hypotheses mutually uncertifiable).
|
|
160
|
+
|
|
161
|
+
## Relation to the paper
|
|
162
|
+
|
|
163
|
+
This package accompanies *Track-and-Certify: Anytime Joint Certification of
|
|
164
|
+
Causal Graphs and Unknown Intervention Targets* (under review). The
|
|
165
|
+
statistical core in `track_certify/_vendor/` is vendored **byte-identically**
|
|
166
|
+
from the paper's audited evidence archive — the same code that produced every
|
|
167
|
+
number in the paper — and `tests/test_equivalence.py` proves trajectory
|
|
168
|
+
equivalence (identical actions, decisions, stopping times, and counts) between
|
|
169
|
+
the streaming `Certifier` and the audited driver over shared noise tapes, on
|
|
170
|
+
five instances under two allocation policies.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# track-certify
|
|
2
|
+
|
|
3
|
+
**Anytime joint certification of causal graphs and unknown intervention
|
|
4
|
+
targets** — a certify-or-refuse layer for sequential causal experiments,
|
|
5
|
+
with instance-optimal adaptive sampling.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install track-certify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Pure Python; depends only on `numpy` and `scipy`.
|
|
12
|
+
|
|
13
|
+
## The problem it solves
|
|
14
|
+
|
|
15
|
+
You run interventional experiments (gene perturbations, drug assays, A/B
|
|
16
|
+
interventions) where **the perturbed variables themselves are uncertain**: a
|
|
17
|
+
reagent may act off-target, weakly, or not at all. A causal conclusion is then
|
|
18
|
+
a *joint* claim — the graph **and** each environment's intervention target —
|
|
19
|
+
and every additional batch of samples costs money. Two questions matter more
|
|
20
|
+
than any point estimate:
|
|
21
|
+
|
|
22
|
+
1. **Where should the next observation be taken?**
|
|
23
|
+
2. **When is it safe to stop and certify the joint answer at error level δ?**
|
|
24
|
+
|
|
25
|
+
`track-certify` answers both with guarantees: an anytime-valid e-process
|
|
26
|
+
stopping rule (no peeking penalty, no multiplicity correction), a no-regret
|
|
27
|
+
allocation learner that provably attains the instance's information-theoretic
|
|
28
|
+
sample requirement `T*`, and an exact assignment oracle that replaces
|
|
29
|
+
factorial target enumeration with polynomial rectangular assignment. When it
|
|
30
|
+
cannot certify — budget cap reached, or model preconditions infeasible — it
|
|
31
|
+
**refuses rather than guesses**.
|
|
32
|
+
|
|
33
|
+
## 60 seconds: certify a joint answer
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import numpy as np
|
|
37
|
+
import track_certify as tc
|
|
38
|
+
|
|
39
|
+
# Declare the class: 2 candidate graphs over a known covariance,
|
|
40
|
+
# 2 environments with unknown, distinct targets of unknown amplitude.
|
|
41
|
+
sigma = np.array([[1.0, 0.5],
|
|
42
|
+
[0.5, 1.0]])
|
|
43
|
+
model = tc.build_model(sigma, {"X->Y": (0, 1), "Y->X": (1, 0)})
|
|
44
|
+
|
|
45
|
+
cert = tc.Certifier(model, n_environments=2, delta=0.01, cap=200_000)
|
|
46
|
+
|
|
47
|
+
while True:
|
|
48
|
+
e = cert.next_environment() # which environment to sample now
|
|
49
|
+
y = run_my_experiment(e) # one fresh d-vector from environment e
|
|
50
|
+
out = cert.observe(y)
|
|
51
|
+
if out.stopped:
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
out.certificate # ("X->Y", (0, 1)) — graph + per-environment targets,
|
|
55
|
+
# wrong with probability <= 0.01 under optional stopping;
|
|
56
|
+
# None if out.refused (cap reached: refusal, not a guess)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The certifier never receives the true hypothesis, the amplitudes, `T*`, or
|
|
60
|
+
the optimal allocation.
|
|
61
|
+
|
|
62
|
+
## Before you sample: how hard is your instance?
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
rep = tc.characteristic_time(model, ("X->Y", (0, 1)), amplitudes=(1.5, 1.2))
|
|
66
|
+
rep.tstar # first-order samples per unit log(1/delta)
|
|
67
|
+
rep.w_star # optimal budget split across environments
|
|
68
|
+
rep.active_kinds # which wrong answers dominate: 'graph' / 'target' / 'coupled'
|
|
69
|
+
rep.budget(0.01) # first-order sample estimate at delta = 0.01
|
|
70
|
+
|
|
71
|
+
tax = tc.coupling_tax(model, ("X->Y", (0, 1)), (1.5, 1.2))
|
|
72
|
+
tax.tax # how much harder JOINT certification is than the harder
|
|
73
|
+
# conditional problem — near the identifiability boundary
|
|
74
|
+
# this diverges, and "estimate targets, then certify the
|
|
75
|
+
# graph" becomes unboundedly suboptimal
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Unknown covariance: certify or refuse, with explicit constants
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
obs = collect_observational_samples() # (n0, d), no interventions
|
|
82
|
+
try:
|
|
83
|
+
setup = tc.robust_certifier(
|
|
84
|
+
obs, {"X->Y": (0, 1), "Y->X": (1, 0)},
|
|
85
|
+
n_environments=2, delta=0.01, c_max=3.0)
|
|
86
|
+
except tc.Refusal as r:
|
|
87
|
+
print("infeasible for this n0/delta/d/c_max:", r) # pre-start refusal
|
|
88
|
+
else:
|
|
89
|
+
cert = setup.certifier # threshold already carries the certified
|
|
90
|
+
# envelope inflation — nothing calibrated
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Benchmark instances with theorem-known difficulty
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
inst = tc.solvable_instance(r=0.9) # closed-form T*, coupled alternative
|
|
97
|
+
# provably active (paper Prop. 3)
|
|
98
|
+
inst = tc.scalable_instance(rs=[0.8, 0.9], cs=[1.0, 1.2])
|
|
99
|
+
# additive T*, 2^m graphs (Cor. 1)
|
|
100
|
+
res = tc.simulate(inst.model, inst.truth, inst.amplitudes, delta=1e-3,
|
|
101
|
+
rng=np.random.default_rng(0))
|
|
102
|
+
res.tau, res.correct # compare against inst.tstar_closed_form
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run `track-certify demo` for a self-contained certification demo, or
|
|
106
|
+
`track-certify envelope --n0 500000 --delta 0.01 --d 3 --c-max 1.0` to check
|
|
107
|
+
robust-mode feasibility from the command line.
|
|
108
|
+
|
|
109
|
+
## Guarantees, precisely
|
|
110
|
+
|
|
111
|
+
* **Validity.** With a correctly declared class and known covariance, the
|
|
112
|
+
returned joint answer is wrong with probability at most `delta`, under
|
|
113
|
+
arbitrary adaptive sampling and optional stopping (Gaussian mixture
|
|
114
|
+
e-process + Ville's inequality; no union bound over hypotheses).
|
|
115
|
+
* **Optimality.** Expected stopping time satisfies
|
|
116
|
+
`E[tau]/log(1/delta) -> T*` as `delta -> 0` — the change-of-measure lower
|
|
117
|
+
bound for *any* delta-correct procedure, attained.
|
|
118
|
+
* **Estimated covariance.** With the split-sample envelope, validity is exact
|
|
119
|
+
whenever the envelope's computable feasibility conditions hold; otherwise
|
|
120
|
+
the workflow refuses before starting. Efficiency degrades by explicit,
|
|
121
|
+
measured constant factors.
|
|
122
|
+
* **Refusal semantics.** A reached cap or an infeasible envelope produces a
|
|
123
|
+
refusal, never a certificate. There is no procedure here that converts
|
|
124
|
+
arbitrary data into a certificate.
|
|
125
|
+
|
|
126
|
+
**Model scope** (checked where checkable, refused when violated): finite
|
|
127
|
+
declared graph class sharing one positive-definite covariance; mean-shift
|
|
128
|
+
interventions with one unknown, distinct target per environment; Gaussian
|
|
129
|
+
noise; no hidden confounding. Duplicate or ray-identical graph declarations
|
|
130
|
+
are refused by default (they make hypotheses mutually uncertifiable).
|
|
131
|
+
|
|
132
|
+
## Relation to the paper
|
|
133
|
+
|
|
134
|
+
This package accompanies *Track-and-Certify: Anytime Joint Certification of
|
|
135
|
+
Causal Graphs and Unknown Intervention Targets* (under review). The
|
|
136
|
+
statistical core in `track_certify/_vendor/` is vendored **byte-identically**
|
|
137
|
+
from the paper's audited evidence archive — the same code that produced every
|
|
138
|
+
number in the paper — and `tests/test_equivalence.py` proves trajectory
|
|
139
|
+
equivalence (identical actions, decisions, stopping times, and counts) between
|
|
140
|
+
the streaming `Certifier` and the audited driver over shared noise tapes, on
|
|
141
|
+
five instances under two allocation policies.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Streaming certification on a two-graph class (known covariance)."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import track_certify as tc
|
|
4
|
+
from track_and_certify_general import true_means # vendored helper
|
|
5
|
+
|
|
6
|
+
sigma = np.array([[1.0, 0.5], [0.5, 1.0]])
|
|
7
|
+
model = tc.build_model(sigma, {"X->Y": (0, 1), "Y->X": (1, 0)})
|
|
8
|
+
|
|
9
|
+
# Simulated lab: truth is X->Y with env targets (node0, node1), amplitudes (1.5, 1.2)
|
|
10
|
+
truth, amps = ("X->Y", (0, 1)), (1.5, 1.2)
|
|
11
|
+
rng = np.random.default_rng(7)
|
|
12
|
+
chol = np.linalg.cholesky(sigma)
|
|
13
|
+
means = true_means(model, truth, amps)
|
|
14
|
+
|
|
15
|
+
cert = tc.Certifier(model, n_environments=2, delta=0.01, cap=100_000)
|
|
16
|
+
while True:
|
|
17
|
+
e = cert.next_environment()
|
|
18
|
+
y = rng.normal(size=2) @ chol.T + means[e] # your experiment here
|
|
19
|
+
out = cert.observe(y)
|
|
20
|
+
if out.stopped:
|
|
21
|
+
break
|
|
22
|
+
print("certificate:", out.certificate, "| tau:", out.t, "| counts:", out.counts)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Pre-experiment design: T*, optimal allocation, coupling tax."""
|
|
2
|
+
import track_certify as tc
|
|
3
|
+
|
|
4
|
+
inst = tc.solvable_instance(r=0.9, c1=1.0, c2=1.0)
|
|
5
|
+
rep = tc.characteristic_time(inst.model, inst.truth, inst.amplitudes)
|
|
6
|
+
tax = tc.coupling_tax(inst.model, inst.truth, inst.amplitudes)
|
|
7
|
+
print("T* (LP) :", round(rep.tstar, 4), "| closed form:",
|
|
8
|
+
round(inst.tstar_closed_form, 4))
|
|
9
|
+
print("optimal split :", tuple(round(w, 3) for w in rep.w_star))
|
|
10
|
+
print("active kinds :", rep.active_kinds)
|
|
11
|
+
print("coupling tax :", round(tax.tax, 3))
|
|
12
|
+
print("budget @ 1e-3 :", round(rep.budget(1e-3), 1), "samples (first order)")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Estimated-covariance workflow: certify or refuse with explicit constants."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import track_certify as tc
|
|
4
|
+
|
|
5
|
+
rng = np.random.default_rng(0)
|
|
6
|
+
sigma_true = np.array([[1.0, 0.5], [0.5, 1.0]])
|
|
7
|
+
n0 = 600_000
|
|
8
|
+
obs = rng.multivariate_normal(np.zeros(2), sigma_true, size=n0)
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
setup = tc.robust_certifier(obs, {"X->Y": (0, 1), "Y->X": (1, 0)},
|
|
12
|
+
n_environments=2, delta=0.01, c_max=3.0)
|
|
13
|
+
except tc.Refusal as r:
|
|
14
|
+
print("pre-start refusal:", r)
|
|
15
|
+
else:
|
|
16
|
+
env = setup.envelope
|
|
17
|
+
print(f"feasible: eps={env.eps:.4f}, eta_bar={env.eta_bar:.4f}, "
|
|
18
|
+
f"beta_scale={env.beta_scale:.3f}, drift={env.beta_drift:.2e}/step")
|
|
19
|
+
print("certifier ready:", type(setup.certifier).__name__)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "track-certify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Anytime joint certification of causal graphs and unknown intervention targets: certify-or-refuse sequential experiments with instance-optimal adaptive sampling"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "The Track-and-Certify authors"}]
|
|
13
|
+
keywords = [
|
|
14
|
+
"causal-discovery", "causal-inference", "experimental-design",
|
|
15
|
+
"sequential-testing", "e-process", "anytime-valid", "best-arm-identification",
|
|
16
|
+
"intervention-targets", "fixed-confidence",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = ["numpy>=1.22", "scipy>=1.8"]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest>=7"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://pypi.org/project/track-certify/"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
track-certify = "track_certify.cli:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
track_certify = ["py.typed"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""track-certify: anytime joint certification of causal graphs and unknown
|
|
2
|
+
intervention targets.
|
|
3
|
+
|
|
4
|
+
Given a declared finite class of candidate causal graphs over a shared
|
|
5
|
+
covariance, and K interventional environments whose targets and amplitudes
|
|
6
|
+
are unknown, this package answers three questions with guarantees:
|
|
7
|
+
|
|
8
|
+
* **Where to sample next, and when to stop.** :class:`Certifier` streams:
|
|
9
|
+
it emits the next environment to sample, consumes one fresh observation at
|
|
10
|
+
a time, and stops with a jointly certified (graph, target-vector) answer
|
|
11
|
+
whose error probability is at most ``delta`` under optional stopping -- or
|
|
12
|
+
with a refusal, never a guess.
|
|
13
|
+
* **How hard the problem is, before sampling.** :func:`characteristic_time`
|
|
14
|
+
returns the instance's first-order sample requirement ``T*`` and the
|
|
15
|
+
optimal budget split across environments; :func:`coupling_tax` quantifies
|
|
16
|
+
how much harder joint certification is than its staged relaxations.
|
|
17
|
+
* **What survives an estimated covariance.** :func:`certified_envelope` and
|
|
18
|
+
:func:`robust_certifier` implement the split-sample workflow with fully
|
|
19
|
+
explicit constants, refusing before starting when the envelope is
|
|
20
|
+
infeasible.
|
|
21
|
+
|
|
22
|
+
Ready-made benchmark instances with theorem-known difficulty are in
|
|
23
|
+
:func:`solvable_instance` and :func:`scalable_instance`; the audited
|
|
24
|
+
simulation driver behind the paper's experiments is exposed as
|
|
25
|
+
:func:`simulate`.
|
|
26
|
+
|
|
27
|
+
The statistical core is vendored byte-identically from the audited research
|
|
28
|
+
archive; ``tests/test_equivalence.py`` proves trajectory equivalence between
|
|
29
|
+
the streaming interface and that driver.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .api import (Certifier, Envelope, Refusal, StepOutcome, build_model,
|
|
33
|
+
certified_envelope)
|
|
34
|
+
from .design import (CouplingReport, DesignReport, characteristic_time,
|
|
35
|
+
coupling_tax)
|
|
36
|
+
from .instances import Instance, scalable_instance, solvable_instance
|
|
37
|
+
from .robust import RobustSetup, estimate_covariance, robust_certifier
|
|
38
|
+
from . import _vendor # noqa: F401
|
|
39
|
+
from track_and_certify_general import run_general as simulate # noqa: E402
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"Certifier", "Envelope", "Refusal", "StepOutcome", "build_model",
|
|
43
|
+
"certified_envelope",
|
|
44
|
+
"DesignReport", "CouplingReport", "characteristic_time", "coupling_tax",
|
|
45
|
+
"Instance", "solvable_instance", "scalable_instance",
|
|
46
|
+
"RobustSetup", "estimate_covariance", "robust_certifier",
|
|
47
|
+
"simulate",
|
|
48
|
+
]
|
|
49
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Vendored audited modules, byte-identical to the paper's evidence archive.
|
|
2
|
+
|
|
3
|
+
The four modules import one another by their original top-level names
|
|
4
|
+
(``assignment_oracle``, ``best_alternative``, ``finite_ray_model``,
|
|
5
|
+
``track_and_certify_general``). To keep them byte-identical, this package
|
|
6
|
+
loads them from files in dependency order and registers them in
|
|
7
|
+
``sys.modules`` under those names -- **without** touching ``sys.path``, so
|
|
8
|
+
nothing else on the import path is shadowed.
|
|
9
|
+
|
|
10
|
+
If one of these names is already imported from somewhere else (a user module
|
|
11
|
+
with the same name), loading raises ``ImportError`` immediately with an
|
|
12
|
+
explanation instead of silently mixing implementations. Each module is also
|
|
13
|
+
registered under ``track_certify._vendor.<name>`` for unambiguous access.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import importlib.util
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
_ORDER = (
|
|
22
|
+
"assignment_oracle",
|
|
23
|
+
"best_alternative",
|
|
24
|
+
"finite_ray_model",
|
|
25
|
+
"track_and_certify_general",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load(name):
|
|
30
|
+
path = os.path.join(_HERE, name + ".py")
|
|
31
|
+
existing = sys.modules.get(name)
|
|
32
|
+
if existing is not None:
|
|
33
|
+
existing_file = getattr(existing, "__file__", None)
|
|
34
|
+
if existing_file is None or \
|
|
35
|
+
os.path.dirname(os.path.abspath(existing_file)) != _HERE:
|
|
36
|
+
raise ImportError(
|
|
37
|
+
f"track-certify vendors an audited module named {name!r}, "
|
|
38
|
+
f"but a different module with that name is already imported "
|
|
39
|
+
f"from {existing_file!r}. Import track_certify before the "
|
|
40
|
+
f"conflicting module, or rename it.")
|
|
41
|
+
return existing
|
|
42
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
sys.modules[name] = module
|
|
45
|
+
try:
|
|
46
|
+
spec.loader.exec_module(module)
|
|
47
|
+
except Exception:
|
|
48
|
+
del sys.modules[name]
|
|
49
|
+
raise
|
|
50
|
+
return module
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
for _name in _ORDER:
|
|
54
|
+
_mod = _load(_name)
|
|
55
|
+
sys.modules[__name__ + "." + _name] = _mod
|
|
56
|
+
globals()[_name] = _mod
|
|
57
|
+
|
|
58
|
+
del _name, _mod
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Exact target-assignment oracle for joint graph--target certification.
|
|
2
|
+
|
|
3
|
+
For a fixed graph, each row is an intervention environment and each column is
|
|
4
|
+
a candidate target. The certification loss is additive across rows and the
|
|
5
|
+
main model requires distinct targets, so the best target vector is a
|
|
6
|
+
rectangular linear assignment. No hypothesis is pruned.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Dict, Hashable, Iterable, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy.optimize import linear_sum_assignment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AssignmentSolution:
|
|
18
|
+
cost: float
|
|
19
|
+
targets: Tuple[int, ...]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _checked_cost(cost: np.ndarray) -> np.ndarray:
|
|
23
|
+
arr = np.asarray(cost, dtype=float)
|
|
24
|
+
if arr.ndim != 2:
|
|
25
|
+
raise ValueError("cost must be a K-by-d matrix")
|
|
26
|
+
k, d = arr.shape
|
|
27
|
+
if k == 0 or k > d:
|
|
28
|
+
raise ValueError("distinct-target assignment requires 1 <= K <= d")
|
|
29
|
+
if np.isnan(arr).any():
|
|
30
|
+
raise ValueError("cost contains NaN")
|
|
31
|
+
return arr
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def best_assignment(
|
|
35
|
+
cost: np.ndarray,
|
|
36
|
+
forbidden_pairs: Iterable[Tuple[int, int]] = (),
|
|
37
|
+
) -> Optional[AssignmentSolution]:
|
|
38
|
+
"""Return the minimum-cost injective target vector, or None if infeasible."""
|
|
39
|
+
|
|
40
|
+
arr = _checked_cost(cost).copy()
|
|
41
|
+
for e, target in forbidden_pairs:
|
|
42
|
+
if not (0 <= e < arr.shape[0] and 0 <= target < arr.shape[1]):
|
|
43
|
+
raise ValueError("forbidden pair is outside the cost matrix")
|
|
44
|
+
arr[e, target] = np.inf
|
|
45
|
+
try:
|
|
46
|
+
rows, cols = linear_sum_assignment(arr)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return None
|
|
49
|
+
if len(rows) != arr.shape[0] or not np.isfinite(arr[rows, cols]).all():
|
|
50
|
+
return None
|
|
51
|
+
targets = np.empty(arr.shape[0], dtype=int)
|
|
52
|
+
targets[rows] = cols
|
|
53
|
+
return AssignmentSolution(float(arr[rows, cols].sum()), tuple(targets.tolist()))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def best_distinct_assignment(
|
|
57
|
+
cost: np.ndarray,
|
|
58
|
+
incumbent_targets: Sequence[int],
|
|
59
|
+
) -> Optional[AssignmentSolution]:
|
|
60
|
+
"""Return the best injective assignment different from the incumbent.
|
|
61
|
+
|
|
62
|
+
Every different assignment omits at least one incumbent row--target pair.
|
|
63
|
+
Solving K assignments, each forbidding one such pair, is therefore exact.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
arr = _checked_cost(cost)
|
|
67
|
+
incumbent = tuple(int(t) for t in incumbent_targets)
|
|
68
|
+
if len(incumbent) != arr.shape[0] or len(set(incumbent)) != len(incumbent):
|
|
69
|
+
raise ValueError("incumbent_targets must be an injective K-vector")
|
|
70
|
+
if any(t < 0 or t >= arr.shape[1] for t in incumbent):
|
|
71
|
+
raise ValueError("incumbent target is outside the cost matrix")
|
|
72
|
+
|
|
73
|
+
best: Optional[AssignmentSolution] = None
|
|
74
|
+
for e, target in enumerate(incumbent):
|
|
75
|
+
candidate = best_assignment(arr, forbidden_pairs=((e, target),))
|
|
76
|
+
if candidate is not None and (best is None or candidate.cost < best.cost):
|
|
77
|
+
best = candidate
|
|
78
|
+
return best
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def best_wrong_hypothesis(
|
|
82
|
+
graph_costs: Dict[Hashable, np.ndarray],
|
|
83
|
+
incumbent_graph: Hashable,
|
|
84
|
+
incumbent_targets: Sequence[int],
|
|
85
|
+
) -> Tuple[Hashable, AssignmentSolution]:
|
|
86
|
+
"""Return the exact lowest-loss graph--target answer excluding incumbent."""
|
|
87
|
+
|
|
88
|
+
if incumbent_graph not in graph_costs:
|
|
89
|
+
raise ValueError("incumbent graph is absent from graph_costs")
|
|
90
|
+
answer = None
|
|
91
|
+
for graph, cost in graph_costs.items():
|
|
92
|
+
if graph == incumbent_graph:
|
|
93
|
+
solution = best_distinct_assignment(cost, incumbent_targets)
|
|
94
|
+
else:
|
|
95
|
+
solution = best_assignment(cost)
|
|
96
|
+
if solution is not None and (answer is None or solution.cost < answer[1].cost):
|
|
97
|
+
answer = (graph, solution)
|
|
98
|
+
if answer is None:
|
|
99
|
+
raise ValueError("the declared class contains no alternative hypothesis")
|
|
100
|
+
return answer
|