causal-hybrid-control 0.2.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.
- causal_hybrid_control-0.2.0/.github/workflows/ci.yml +41 -0
- causal_hybrid_control-0.2.0/.github/workflows/release.yml +41 -0
- causal_hybrid_control-0.2.0/.gitignore +43 -0
- causal_hybrid_control-0.2.0/.python-version +1 -0
- causal_hybrid_control-0.2.0/CHANGELOG.md +211 -0
- causal_hybrid_control-0.2.0/LICENSE +21 -0
- causal_hybrid_control-0.2.0/PKG-INFO +175 -0
- causal_hybrid_control-0.2.0/README.md +141 -0
- causal_hybrid_control-0.2.0/notebooks/01_causal_vs_predictive_control.ipynb +433 -0
- causal_hybrid_control-0.2.0/notebooks/01_causal_vs_predictive_control.py +121 -0
- causal_hybrid_control-0.2.0/notebooks/02_learn_hidden_physics.ipynb +404 -0
- causal_hybrid_control-0.2.0/notebooks/02_learn_hidden_physics.py +165 -0
- causal_hybrid_control-0.2.0/notebooks/03_causal_inference_toolkit.ipynb +593 -0
- causal_hybrid_control-0.2.0/notebooks/03_causal_inference_toolkit.py +140 -0
- causal_hybrid_control-0.2.0/notebooks/04_epidemic_and_pessimism.ipynb +314 -0
- causal_hybrid_control-0.2.0/notebooks/04_epidemic_and_pessimism.py +106 -0
- causal_hybrid_control-0.2.0/notebooks/05_benchmark_scoreboard.ipynb +426 -0
- causal_hybrid_control-0.2.0/notebooks/05_benchmark_scoreboard.py +127 -0
- causal_hybrid_control-0.2.0/notebooks/05_confounding_robust_control.ipynb +325 -0
- causal_hybrid_control-0.2.0/notebooks/05_confounding_robust_control.py +128 -0
- causal_hybrid_control-0.2.0/notebooks/06_cruise_control_confounded.ipynb +352 -0
- causal_hybrid_control-0.2.0/notebooks/06_cruise_control_confounded.py +140 -0
- causal_hybrid_control-0.2.0/notebooks/07_real_data_lalonde.ipynb +435 -0
- causal_hybrid_control-0.2.0/notebooks/07_real_data_lalonde.py +151 -0
- causal_hybrid_control-0.2.0/proofs/adaptive_exploration.v +109 -0
- causal_hybrid_control-0.2.0/proofs/bandit_causal.v +37 -0
- causal_hybrid_control-0.2.0/proofs/barrier_feasibility.v +197 -0
- causal_hybrid_control-0.2.0/proofs/box_projection.v +38 -0
- causal_hybrid_control-0.2.0/proofs/c2_end_to_end.v +93 -0
- causal_hybrid_control-0.2.0/proofs/causal_mpc.v +52 -0
- causal_hybrid_control-0.2.0/proofs/causal_pathway.v +99 -0
- causal_hybrid_control-0.2.0/proofs/clustered_van_trees.v +93 -0
- causal_hybrid_control-0.2.0/proofs/composition_transfer.v +37 -0
- causal_hybrid_control-0.2.0/proofs/confounded_turnpike.v +40 -0
- causal_hybrid_control-0.2.0/proofs/confounding_lq_regret.v +58 -0
- causal_hybrid_control-0.2.0/proofs/confounding_robust_control.v +162 -0
- causal_hybrid_control-0.2.0/proofs/confounding_robust_cvar.v +71 -0
- causal_hybrid_control-0.2.0/proofs/congestion_contraction.v +91 -0
- causal_hybrid_control-0.2.0/proofs/constrained_ce_regret.v +60 -0
- causal_hybrid_control-0.2.0/proofs/contractive_euler.v +42 -0
- causal_hybrid_control-0.2.0/proofs/doubly_robust.v +34 -0
- causal_hybrid_control-0.2.0/proofs/dynamic_causal_mpc.v +54 -0
- causal_hybrid_control-0.2.0/proofs/ensemble_control.v +50 -0
- causal_hybrid_control-0.2.0/proofs/equilibrium_transfer.v +98 -0
- causal_hybrid_control-0.2.0/proofs/exposure_map_c2.v +62 -0
- causal_hybrid_control-0.2.0/proofs/highprob_regret.v +37 -0
- causal_hybrid_control-0.2.0/proofs/hinf_robust_regret.v +66 -0
- causal_hybrid_control-0.2.0/proofs/information_lower_bound.v +35 -0
- causal_hybrid_control-0.2.0/proofs/interference_convexity.v +34 -0
- causal_hybrid_control-0.2.0/proofs/interference_orthogonal.v +40 -0
- causal_hybrid_control-0.2.0/proofs/interference_regret.v +56 -0
- causal_hybrid_control-0.2.0/proofs/lipschitz_rollout.v +168 -0
- causal_hybrid_control-0.2.0/proofs/multichannel_control.v +55 -0
- causal_hybrid_control-0.2.0/proofs/nonlinear_regret.v +53 -0
- causal_hybrid_control-0.2.0/proofs/optimal_exploration.v +56 -0
- causal_hybrid_control-0.2.0/proofs/orthogonal_control.v +40 -0
- causal_hybrid_control-0.2.0/proofs/partial_id_control.v +31 -0
- causal_hybrid_control-0.2.0/proofs/pessimistic_optimality.v +90 -0
- causal_hybrid_control-0.2.0/proofs/port_hamiltonian_lyapunov.v +69 -0
- causal_hybrid_control-0.2.0/proofs/transportability_regret.v +42 -0
- causal_hybrid_control-0.2.0/proofs/van_trees.v +51 -0
- causal_hybrid_control-0.2.0/pyproject.toml +96 -0
- causal_hybrid_control-0.2.0/scripts/confounding_demo.py +29 -0
- causal_hybrid_control-0.2.0/scripts/epidemic_demo.py +55 -0
- causal_hybrid_control-0.2.0/scripts/flagship_demo.py +46 -0
- causal_hybrid_control-0.2.0/scripts/reachability_demo.py +110 -0
- causal_hybrid_control-0.2.0/scripts/run_benchmark.py +47 -0
- causal_hybrid_control-0.2.0/scripts/run_causal_bench.py +17 -0
- causal_hybrid_control-0.2.0/scripts/run_confounding_control_demo.py +40 -0
- causal_hybrid_control-0.2.0/scripts/run_dynamic_confounding_demo.py +38 -0
- causal_hybrid_control-0.2.0/scripts/run_marketplace_demo.py +17 -0
- causal_hybrid_control-0.2.0/scripts/spine_demo.py +43 -0
- causal_hybrid_control-0.2.0/src/chc/__init__.py +552 -0
- causal_hybrid_control-0.2.0/src/chc/adjoint.py +109 -0
- causal_hybrid_control-0.2.0/src/chc/barrier.py +401 -0
- causal_hybrid_control-0.2.0/src/chc/benchmark.py +620 -0
- causal_hybrid_control-0.2.0/src/chc/causal.py +288 -0
- causal_hybrid_control-0.2.0/src/chc/causal_bench.py +138 -0
- causal_hybrid_control-0.2.0/src/chc/control.py +58 -0
- causal_hybrid_control-0.2.0/src/chc/cost.py +36 -0
- causal_hybrid_control-0.2.0/src/chc/deep_galerkin.py +79 -0
- causal_hybrid_control-0.2.0/src/chc/did.py +154 -0
- causal_hybrid_control-0.2.0/src/chc/discovery.py +196 -0
- causal_hybrid_control-0.2.0/src/chc/dynamics.py +59 -0
- causal_hybrid_control-0.2.0/src/chc/epidemic.py +86 -0
- causal_hybrid_control-0.2.0/src/chc/estimators.py +282 -0
- causal_hybrid_control-0.2.0/src/chc/flagship.py +91 -0
- causal_hybrid_control-0.2.0/src/chc/galerkin.py +95 -0
- causal_hybrid_control-0.2.0/src/chc/games.py +421 -0
- causal_hybrid_control-0.2.0/src/chc/gmethods.py +102 -0
- causal_hybrid_control-0.2.0/src/chc/independence.py +62 -0
- causal_hybrid_control-0.2.0/src/chc/integrate.py +36 -0
- causal_hybrid_control-0.2.0/src/chc/irf.py +103 -0
- causal_hybrid_control-0.2.0/src/chc/koopman.py +92 -0
- causal_hybrid_control-0.2.0/src/chc/lalonde.py +116 -0
- causal_hybrid_control-0.2.0/src/chc/lqr.py +104 -0
- causal_hybrid_control-0.2.0/src/chc/marketplace.py +180 -0
- causal_hybrid_control-0.2.0/src/chc/matching.py +164 -0
- causal_hybrid_control-0.2.0/src/chc/meanfield.py +110 -0
- causal_hybrid_control-0.2.0/src/chc/metrics.py +79 -0
- causal_hybrid_control-0.2.0/src/chc/mintime.py +100 -0
- causal_hybrid_control-0.2.0/src/chc/mpc.py +64 -0
- causal_hybrid_control-0.2.0/src/chc/network_causal.py +202 -0
- causal_hybrid_control-0.2.0/src/chc/offpolicy.py +70 -0
- causal_hybrid_control-0.2.0/src/chc/pathway.py +274 -0
- causal_hybrid_control-0.2.0/src/chc/plan.py +258 -0
- causal_hybrid_control-0.2.0/src/chc/py.typed +0 -0
- causal_hybrid_control-0.2.0/src/chc/reachability.py +294 -0
- causal_hybrid_control-0.2.0/src/chc/regret.py +2573 -0
- causal_hybrid_control-0.2.0/src/chc/residual.py +444 -0
- causal_hybrid_control-0.2.0/src/chc/scm.py +113 -0
- causal_hybrid_control-0.2.0/src/chc/sensitivity.py +155 -0
- causal_hybrid_control-0.2.0/src/chc/spine.py +158 -0
- causal_hybrid_control-0.2.0/src/chc/splitting.py +49 -0
- causal_hybrid_control-0.2.0/src/chc/support.py +120 -0
- causal_hybrid_control-0.2.0/src/chc/surrogate.py +90 -0
- causal_hybrid_control-0.2.0/src/chc/toeplitz.py +165 -0
- causal_hybrid_control-0.2.0/src/chc/train.py +107 -0
- causal_hybrid_control-0.2.0/src/chc/transport.py +167 -0
- causal_hybrid_control-0.2.0/src/chc/uncertainty.py +879 -0
- causal_hybrid_control-0.2.0/tests/conftest.py +5 -0
- causal_hybrid_control-0.2.0/tests/test_adjoint.py +49 -0
- causal_hybrid_control-0.2.0/tests/test_barrier.py +220 -0
- causal_hybrid_control-0.2.0/tests/test_benchmark.py +103 -0
- causal_hybrid_control-0.2.0/tests/test_benchmark_multiseed.py +53 -0
- causal_hybrid_control-0.2.0/tests/test_cate.py +36 -0
- causal_hybrid_control-0.2.0/tests/test_causal.py +106 -0
- causal_hybrid_control-0.2.0/tests/test_causal_bench.py +29 -0
- causal_hybrid_control-0.2.0/tests/test_confounding_lq_regret.py +101 -0
- causal_hybrid_control-0.2.0/tests/test_confounding_penalty.py +97 -0
- causal_hybrid_control-0.2.0/tests/test_confounding_robust.py +99 -0
- causal_hybrid_control-0.2.0/tests/test_confounding_robust_control.py +143 -0
- causal_hybrid_control-0.2.0/tests/test_control.py +35 -0
- causal_hybrid_control-0.2.0/tests/test_deep_galerkin.py +18 -0
- causal_hybrid_control-0.2.0/tests/test_did.py +75 -0
- causal_hybrid_control-0.2.0/tests/test_diffrax_adjoint.py +57 -0
- causal_hybrid_control-0.2.0/tests/test_discovery.py +58 -0
- causal_hybrid_control-0.2.0/tests/test_discovery_wiring.py +68 -0
- causal_hybrid_control-0.2.0/tests/test_epidemic.py +26 -0
- causal_hybrid_control-0.2.0/tests/test_estimators.py +84 -0
- causal_hybrid_control-0.2.0/tests/test_flagship.py +12 -0
- causal_hybrid_control-0.2.0/tests/test_galerkin.py +58 -0
- causal_hybrid_control-0.2.0/tests/test_games.py +209 -0
- causal_hybrid_control-0.2.0/tests/test_gmethods.py +55 -0
- causal_hybrid_control-0.2.0/tests/test_graph_residual.py +70 -0
- causal_hybrid_control-0.2.0/tests/test_hybrid.py +31 -0
- causal_hybrid_control-0.2.0/tests/test_independence.py +42 -0
- causal_hybrid_control-0.2.0/tests/test_irf.py +108 -0
- causal_hybrid_control-0.2.0/tests/test_kan.py +60 -0
- causal_hybrid_control-0.2.0/tests/test_koopman.py +51 -0
- causal_hybrid_control-0.2.0/tests/test_lalonde.py +39 -0
- causal_hybrid_control-0.2.0/tests/test_lipschitz_rollout.py +123 -0
- causal_hybrid_control-0.2.0/tests/test_lqr.py +72 -0
- causal_hybrid_control-0.2.0/tests/test_marketplace.py +72 -0
- causal_hybrid_control-0.2.0/tests/test_matching.py +49 -0
- causal_hybrid_control-0.2.0/tests/test_meanfield.py +9 -0
- causal_hybrid_control-0.2.0/tests/test_metrics.py +51 -0
- causal_hybrid_control-0.2.0/tests/test_mintime.py +55 -0
- causal_hybrid_control-0.2.0/tests/test_mpc.py +40 -0
- causal_hybrid_control-0.2.0/tests/test_network_causal.py +39 -0
- causal_hybrid_control-0.2.0/tests/test_offpolicy.py +55 -0
- causal_hybrid_control-0.2.0/tests/test_pathway.py +104 -0
- causal_hybrid_control-0.2.0/tests/test_plan.py +187 -0
- causal_hybrid_control-0.2.0/tests/test_reachability.py +266 -0
- causal_hybrid_control-0.2.0/tests/test_regret.py +476 -0
- causal_hybrid_control-0.2.0/tests/test_residual_backbones.py +99 -0
- causal_hybrid_control-0.2.0/tests/test_scm.py +78 -0
- causal_hybrid_control-0.2.0/tests/test_sensitivity.py +35 -0
- causal_hybrid_control-0.2.0/tests/test_sensitivity_properties.py +130 -0
- causal_hybrid_control-0.2.0/tests/test_spine.py +60 -0
- causal_hybrid_control-0.2.0/tests/test_splitting.py +61 -0
- causal_hybrid_control-0.2.0/tests/test_support.py +50 -0
- causal_hybrid_control-0.2.0/tests/test_surrogate.py +48 -0
- causal_hybrid_control-0.2.0/tests/test_tigramite_adapter.py +37 -0
- causal_hybrid_control-0.2.0/tests/test_toeplitz.py +92 -0
- causal_hybrid_control-0.2.0/tests/test_train.py +83 -0
- causal_hybrid_control-0.2.0/tests/test_transport.py +62 -0
- causal_hybrid_control-0.2.0/tests/test_uncertainty.py +125 -0
- causal_hybrid_control-0.2.0/tests/test_wasserstein.py +128 -0
- causal_hybrid_control-0.2.0/uv.lock +2526 -0
- causal_hybrid_control-0.2.0/validation/adaptive_exploration.mac +63 -0
- causal_hybrid_control-0.2.0/validation/bandit_causal.mac +30 -0
- causal_hybrid_control-0.2.0/validation/barrier_feasibility.mac +151 -0
- causal_hybrid_control-0.2.0/validation/barrier_feasibility.py +182 -0
- causal_hybrid_control-0.2.0/validation/c2_dml_remainder.mac +34 -0
- causal_hybrid_control-0.2.0/validation/c2_end_to_end.mac +34 -0
- causal_hybrid_control-0.2.0/validation/causal_mpc.mac +37 -0
- causal_hybrid_control-0.2.0/validation/causal_pathway.mac +37 -0
- causal_hybrid_control-0.2.0/validation/clustered_rate_check.R +52 -0
- causal_hybrid_control-0.2.0/validation/clustered_van_trees.mac +36 -0
- causal_hybrid_control-0.2.0/validation/composition_transfer.mac +36 -0
- causal_hybrid_control-0.2.0/validation/confounded_turnpike.mac +42 -0
- causal_hybrid_control-0.2.0/validation/confounding_lq_regret.mac +49 -0
- causal_hybrid_control-0.2.0/validation/confounding_robust_control.mac +57 -0
- causal_hybrid_control-0.2.0/validation/confounding_robust_cvar.mac +42 -0
- causal_hybrid_control-0.2.0/validation/congestion_contraction.mac +81 -0
- causal_hybrid_control-0.2.0/validation/congestion_contraction.py +97 -0
- causal_hybrid_control-0.2.0/validation/constrained_ce_regret.mac +50 -0
- causal_hybrid_control-0.2.0/validation/contractive_euler.mac +38 -0
- causal_hybrid_control-0.2.0/validation/doubly_robust.mac +25 -0
- causal_hybrid_control-0.2.0/validation/dynamic_causal_mpc.mac +26 -0
- causal_hybrid_control-0.2.0/validation/ensemble_control.mac +41 -0
- causal_hybrid_control-0.2.0/validation/equilibrium_transfer.mac +76 -0
- causal_hybrid_control-0.2.0/validation/equilibrium_transfer.py +83 -0
- causal_hybrid_control-0.2.0/validation/exposure_map_c2.mac +30 -0
- causal_hybrid_control-0.2.0/validation/hat_stiffness.mac +9 -0
- causal_hybrid_control-0.2.0/validation/highprob_regret.mac +33 -0
- causal_hybrid_control-0.2.0/validation/hinf_robust_regret.mac +63 -0
- causal_hybrid_control-0.2.0/validation/information_lower_bound.mac +34 -0
- causal_hybrid_control-0.2.0/validation/interference_convexity.mac +33 -0
- causal_hybrid_control-0.2.0/validation/interference_orthogonal.mac +28 -0
- causal_hybrid_control-0.2.0/validation/interference_regret.mac +33 -0
- causal_hybrid_control-0.2.0/validation/kantorovich_ot.m +42 -0
- causal_hybrid_control-0.2.0/validation/lipschitz_rollout.mac +43 -0
- causal_hybrid_control-0.2.0/validation/lqr_care.gp +8 -0
- causal_hybrid_control-0.2.0/validation/lqr_care.input +6 -0
- causal_hybrid_control-0.2.0/validation/lqr_care.m +23 -0
- causal_hybrid_control-0.2.0/validation/lqr_care.mac +31 -0
- causal_hybrid_control-0.2.0/validation/marketplace_interference.m +45 -0
- causal_hybrid_control-0.2.0/validation/matrix_exp.mac +10 -0
- causal_hybrid_control-0.2.0/validation/multichannel_control.mac +44 -0
- causal_hybrid_control-0.2.0/validation/nonlinear_regret.mac +27 -0
- causal_hybrid_control-0.2.0/validation/optimal_exploration.mac +48 -0
- causal_hybrid_control-0.2.0/validation/orthogonal_control.mac +46 -0
- causal_hybrid_control-0.2.0/validation/partial_id_control.mac +27 -0
- causal_hybrid_control-0.2.0/validation/pessimistic_optimality.mac +33 -0
- causal_hybrid_control-0.2.0/validation/port_hamiltonian_lyapunov.mac +50 -0
- causal_hybrid_control-0.2.0/validation/transportability_regret.mac +35 -0
- causal_hybrid_control-0.2.0/validation/van_trees.mac +38 -0
- causal_hybrid_control-0.2.0/validation/wasserstein_dro.m +46 -0
- causal_hybrid_control-0.2.0/validation/wasserstein_dro.mac +22 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- name: Install uv
|
|
14
|
+
uses: astral-sh/setup-uv@v6
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- name: Sync dependencies
|
|
18
|
+
run: uv sync --group dev
|
|
19
|
+
- name: Lint
|
|
20
|
+
run: uv run ruff check .
|
|
21
|
+
- name: Format check
|
|
22
|
+
run: uv run ruff format --check .
|
|
23
|
+
- name: Type check
|
|
24
|
+
run: uv run ty check
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@v6
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
- name: Sync dependencies
|
|
39
|
+
run: uv sync --group dev
|
|
40
|
+
- name: Tests
|
|
41
|
+
run: uv run pytest
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Cut a release by pushing an annotated tag: `git tag -a v0.1.0 -m ... && git push origin v0.1.0`.
|
|
4
|
+
# Publishing uses PyPI Trusted Publishing (OIDC) -- no stored token. One-time PyPI-side setup:
|
|
5
|
+
# add this repo + the `release.yml` workflow + the `pypi` environment as a trusted publisher.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Build sdist + wheel
|
|
21
|
+
run: uv build
|
|
22
|
+
- name: Check metadata
|
|
23
|
+
run: uvx twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # OIDC token for PyPI Trusted Publishing
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- name: Publish to PyPI
|
|
41
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# uv / venv
|
|
10
|
+
.venv/
|
|
11
|
+
uv.lock.bak
|
|
12
|
+
|
|
13
|
+
# tooling caches
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ty_cache/
|
|
18
|
+
|
|
19
|
+
# Rocq / Coq build artifacts
|
|
20
|
+
*.vo
|
|
21
|
+
*.vok
|
|
22
|
+
*.vos
|
|
23
|
+
*.glob
|
|
24
|
+
.nra.cache
|
|
25
|
+
.*.aux
|
|
26
|
+
.lia.cache
|
|
27
|
+
|
|
28
|
+
# JAX / experiment artifacts
|
|
29
|
+
*.npz
|
|
30
|
+
*.pkl
|
|
31
|
+
outputs/
|
|
32
|
+
wandb/
|
|
33
|
+
|
|
34
|
+
# datasets (notebooks download to a local cache; never commit data)
|
|
35
|
+
data/
|
|
36
|
+
|
|
37
|
+
# editors / OS
|
|
38
|
+
.vscode/
|
|
39
|
+
.idea/
|
|
40
|
+
.zed/
|
|
41
|
+
.DS_Store
|
|
42
|
+
.ipynb_checkpoints/
|
|
43
|
+
.coq-native/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to adhere to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) once the API stabilises (pre-1.0 it may
|
|
6
|
+
still change).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] — 2026-07-29
|
|
11
|
+
|
|
12
|
+
Work landed on `main` since `v0.1.0`. The theme is **guarantees**: most of it is a machine-checked
|
|
13
|
+
result line at the causal↔control seam (Maxima derivation → Rocq proof → numeric certificate), with the
|
|
14
|
+
matching runtime primitives shipped alongside. The proof scripts themselves are in `proofs/` and the
|
|
15
|
+
symbolic derivations in `validation/`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Sensitivity-aware control under hidden confounding** (`chc.sensitivity`, a facade over
|
|
20
|
+
`chc.regret` + `chc.uncertainty`). Bounded-density-ratio (marginal MSM) worst-case effect as a CVaR
|
|
21
|
+
mixture → pessimism-radius inflation (`confounding_robust_inflation`, `msm_worst_case_mean`,
|
|
22
|
+
`confounding_robust_radius`); the confounding regret floor is *second order* in the effect bias
|
|
23
|
+
(`confounding_robust_lq_regret`, plus a matrix Frobenius lift); a **minimax controller** whose gain
|
|
24
|
+
the radius shifts under asymmetric over/under-shoot loss (`confounding_robust_control`, sign
|
|
25
|
+
dichotomy, piecewise improvement gap); the radius inside the replanning tube
|
|
26
|
+
(`confounding_robust_closed_loop_bound`). Grounded on a synthetic observational confounded
|
|
27
|
+
marketplace, then lifted into a genuine receding-horizon **closed loop** on a confounded plant
|
|
28
|
+
(`confounding_robust_tracking_loop`, `confounding_robust_tracking_benchmark`).
|
|
29
|
+
- **`ConfoundingRobustPenalty`** (`chc.uncertainty`) — a `PenaltyModel` carrying the sensitivity radius
|
|
30
|
+
into the general pessimistic-control stack (`radius·Σ‖u_t‖`, from the *transition*-error bound
|
|
31
|
+
`‖Δ_B·u_t‖ ≤ radius·‖u_t‖`; an identification-radius regulariser rather than a certified cost bound,
|
|
32
|
+
since the latter needs a cost-to-go Lipschitz multiplier `lam_unc` currently absorbs), and **`ConfoundingRobustTask`** (`chc.benchmark`), its leaderboard row: under a
|
|
33
|
+
*hidden* confounder no estimator can help, and the radius still cuts regret ~40% vs
|
|
34
|
+
certainty-equivalence with separated multi-seed CIs.
|
|
35
|
+
- **Certified planning** (`chc.uncertainty`, `chc.residual`) — certified-Lipschitz rollout-error tubes
|
|
36
|
+
via discrete Grönwall feeding the pessimism radius, with time-varying tubes, constraint tightening,
|
|
37
|
+
a certified-safe horizon and a closed-loop (replanning) variant; **`ContractiveResidual`** with a
|
|
38
|
+
certified negative log-norm, which replaces the `e^{LT}` growth with a bounded radius; a
|
|
39
|
+
**port-Hamiltonian** residual with a machine-checked damping-injection Lyapunov certificate;
|
|
40
|
+
**`WassersteinPenalty`**, a W1-DRO distribution-shift margin.
|
|
41
|
+
- **`NestedCVaRPenalty`** (`chc.uncertainty`) — a time-consistent aggregation of ensemble
|
|
42
|
+
disagreement in the same `PenaltyModel` slot. `EnsembleUncertainty` *sums* member variance along the
|
|
43
|
+
trajectory, so one very bad step averages away against many quiet ones; this replaces the sum with
|
|
44
|
+
`rho_t = c_t + CVaR_alpha[rho_{t+1}]`, and `static_penalty_trajectory` keeps the other adversary
|
|
45
|
+
(commit to one member for the whole horizon) so the two are comparable. `nested_risk_certificate`
|
|
46
|
+
checks the ordering that must hold — nested ≥ static ≥ risk-neutral, collapsing at `alpha = 1` — and
|
|
47
|
+
the gap is the price of time consistency, which is what a receding-horizon controller needs if it is
|
|
48
|
+
not to chase its own tail across re-solves. Scoped honestly: with the members re-evaluated
|
|
49
|
+
independently the recursion collapses to `Sum_t CVaR_alpha[c_t]`, a risk-averse *aggregation* rule
|
|
50
|
+
rather than a dynamic-programming solve of a nested-risk MDP.
|
|
51
|
+
- **`chc.pathway`** — one `causal_pathway(target)` API over the temporal causal graph, with
|
|
52
|
+
Rocq-certified walk-sum / geometric-truncation / weakest-link structural laws.
|
|
53
|
+
- **Marketplace layer** — `chc.matching` (Kantorovich OT dispatch with dual surge prices) and
|
|
54
|
+
`chc.marketplace` (offline causal control under equilibrium interference, where naive and MOPO-style
|
|
55
|
+
baselines go negative); influence-function standard errors and CIs on the cross-fit DML effect.
|
|
56
|
+
- **Certified strategic layer** (`chc.games`) — `fixed_point`, a differentiable equilibrium solver that
|
|
57
|
+
iterates to a *relative* residual and returns an `EquilibriumSolution` carrying `residual` and
|
|
58
|
+
`converged`, with the backward pass as the implicit-function VJP (`jax.custom_vjp`, adjoint solved as
|
|
59
|
+
its own fixed point) rather than an unrolled loop; `congestion_contraction_modulus` /
|
|
60
|
+
`congestion_damping` / `congestion_contraction_certificate` to certify or refuse a configuration
|
|
61
|
+
before it runs (`spec(J) ⊆ [0, ½]` sharp ⟹ the uniform Jacobian bound is below 1 iff
|
|
62
|
+
`0 < d < 4/(2+κ)` — sharp over the class of congestion maps, sufficient for any one game — with
|
|
63
|
+
`d* = 4/(4+κ)` certifying every `κ`); `equilibrium_transfer_certificate`, which measures the equilibrium's
|
|
64
|
+
*local* conditioning — exactly 1 in the ambient norm uniformly in `κ` (attained only along the mass
|
|
65
|
+
direction that mass conservation never excites) and strictly below 1 on the fixed-mass tangent space
|
|
66
|
+
where displacements actually live — while the naive contraction constant `1/μ` is loose by up to
|
|
67
|
+
100×. So a `C/μ²` regret bound must not take its constant from *this* solver's contraction margin.
|
|
68
|
+
- **`chc.barrier`** — the sensitivity radius spent on a *constraint* instead of on the objective:
|
|
69
|
+
the robust control-barrier margin guaranteed against every effect in the identified set
|
|
70
|
+
(`robust_barrier_margin`), its maximiser — exactly zero once the radius swallows the control
|
|
71
|
+
channel, which is optimal and not conservatism (`robust_safe_action`), the closed-form certified
|
|
72
|
+
action set and the least-restrictive filter that clips a nominal action into it
|
|
73
|
+
(`admissible_action_interval`, `robust_safety_filter`), the sharp radius at which certification
|
|
74
|
+
dies (`identification_radius_threshold`) and the sensitivity level it corresponds to
|
|
75
|
+
(`barrier_gamma_star`) — *the largest sensitivity-model level under which the barrier stays
|
|
76
|
+
certified* (a model parameter, not a measured amount of hidden confounding). Both
|
|
77
|
+
thresholds are **case-split** rather than one formula: a self-satisfying drift gives `inf`, a
|
|
78
|
+
deficit beyond what a perfectly identified channel delivers gives an empty set, and `Δ(Γ)`
|
|
79
|
+
saturating at the CVaR gap gives `Γ* = inf`. The measured consequence: safety margin degrades at
|
|
80
|
+
**first** order in the effect bias — until the radius swallows the channel, past which the loss
|
|
81
|
+
saturates at `U·|g|` — while performance regret degrades at second, because the envelope theorem
|
|
82
|
+
protects an interior optimum and not a binding constraint. `safety_filter_benchmark` pays that argument in a closed loop on an
|
|
83
|
+
unstable plant whose reference sits past the limit: the radius spent on the constraint never
|
|
84
|
+
leaves the safe set, the same-sized budget spent the regret way violates on 93% of steps.
|
|
85
|
+
Re-exported through `chc.sensitivity`.
|
|
86
|
+
- **`chc.plan.causal_plan`** — the one-call spine: a `CausalPlan` carrying the actions *together with*
|
|
87
|
+
the certified error tube and the `certified_actions` prefix, so a caller cannot take the plan and
|
|
88
|
+
leave the certificate behind. With no safety arguments it is exactly `projected_gradient_control`;
|
|
89
|
+
each safety argument switches on one existing layer, and an uncertainty penalty without a support
|
|
90
|
+
model is rejected rather than silently ignored.
|
|
91
|
+
- **`chc.plan.certify_safety`** — the other half of the spine: §40 evaluated along a finished plan, so
|
|
92
|
+
the safety result has a consumer instead of staying an orphan primitive. Returns the certified
|
|
93
|
+
*prefix* (does the action the planner chose still clear the barrier at this `Γ`?) next to the plan's
|
|
94
|
+
`Γ*` (could **any** admissible action have, and up to which level?) — two questions that come apart
|
|
95
|
+
in both directions and are diagnostic together. The plan-level `Γ*` is the minimum over steps and is
|
|
96
|
+
*attained*: `Δ(Γ)` is increasing, so each step certifies on a down-set and the plan certifies on
|
|
97
|
+
their intersection; a step that certifies at no `Γ` empties it and reports `nan` rather than being
|
|
98
|
+
skipped. A separate call on a finished plan rather than a planner argument — it audits, it does not
|
|
99
|
+
silently change the actions. Re-exported through `chc.sensitivity`.
|
|
100
|
+
- **`chc.spine`** (`scripts/spine_demo.py`) — the four layers on one decision, because until now every
|
|
101
|
+
layer had its own demo and none of them ran end to end. Two zones of a mobile driver pool, one
|
|
102
|
+
incentive lever whose `[+b, -b]` column *is* driver conservation, and a supply floor in the zone the
|
|
103
|
+
lever drains: an effect fitted from confounded logs (naively and with the backdoor adjustment), a
|
|
104
|
+
constrained plan with its Grönwall tube, a `certify_safety` audit of that plan, and finally the same
|
|
105
|
+
plan executed on the **true** plant so the offline numbers can be checked against what happened. The
|
|
106
|
+
confounded arm plans a cost of 13.6 and pays 38.5; `Γ*` separates the two arms *before either acts*
|
|
107
|
+
(7.46 vs 1.18) with no access to ground truth. Deliberately control-affine — `certify_safety` reads
|
|
108
|
+
the channel off the Jacobian at `u = 0`, which is exact for an affine plant and only a linearisation
|
|
109
|
+
otherwise, so the softmax-equilibrium market of `chc.marketplace` is the wrong plant to certify.
|
|
110
|
+
- **`chc.reachability`** — a Hamilton–Jacobi backward reachable tube whose adversary is the §32
|
|
111
|
+
identification radius: `V(x,T) = max_u min_{ΔB} min_s h(ξ(s))` by Lax–Friedrichs on a 2-D grid, with
|
|
112
|
+
the same robust-margin algebra as `chc.barrier` but `p = ∇V` solved for rather than assumed. It
|
|
113
|
+
exists to price the barrier certificate, and `barrier_reachability_gap` is that price:
|
|
114
|
+
`valid_cbf` makes the CBF theorem executable (condition on all of `{h ≥ 0}` ⟹ the tube **is**
|
|
115
|
+
`{h ≥ 0}`, checked to a cell), and `certified_but_unreachable` measures where the *pointwise* §40
|
|
116
|
+
check certifies a state no controller can hold — on a relative-degree-2 barrier the §40 verdict is
|
|
117
|
+
identical at every radius while the true tube shrinks. Verified against two analytic solutions (a
|
|
118
|
+
rigidly sliding level set to `1e-5`; the double-integrator braking parabola to 100% off-boundary
|
|
119
|
+
agreement), with a CFL guard that refuses rather than reporting an optimistic safe set.
|
|
120
|
+
- **Regret / guarantee line** — the orthogonal-to-control transfer theorem (order `p` → `2p`, scalar
|
|
121
|
+
and multivariate-LQ), multi-channel network control (debias *every* channel), the adaptive
|
|
122
|
+
information-exploration duality with its `√T` lower bound, the C2 end-to-end theorem with a clustered
|
|
123
|
+
van-Trees lower bound and an exposure-map generalisation, plus a batch of scoped propositions and
|
|
124
|
+
corollaries (doubly-robust control, H∞-as-pessimism, constrained piecewise-quadratic regret,
|
|
125
|
+
confounded turnpike, transportability, ensemble heterogeneity, partial-identification sign threshold).
|
|
126
|
+
|
|
127
|
+
### Changed
|
|
128
|
+
|
|
129
|
+
- Eleven rounds of external review folded in as **scope and honesty corrections**, not new claims: the
|
|
130
|
+
explicit-Euler contraction factor was wrong (`√(1+2μΔt+L²Δt²)`, sufficient step `Δt < 2c/L²`); the
|
|
131
|
+
confounding effect error needed the control magnitude to be dimensionally right; the `§35`
|
|
132
|
+
improvement gap is piecewise (the undershoot-dominant branch was unproved while the benchmark ran in
|
|
133
|
+
it); `§32` is the bounded-density-ratio *marginal* special case of Tan's MSM, not the full model, and
|
|
134
|
+
its monotonicity argument is feasible-set nesting; the confounded-marketplace benchmark is
|
|
135
|
+
*observational*, not a randomised switchback. Several results were relabelled to their honest status
|
|
136
|
+
(order-transfer *lemma*, local-not-global, scalar-not-universal, `≈`-not-`=`). Round eleven: the
|
|
137
|
+
equilibrium conditioning of `1` is an *ambient*-norm statement saturated by the mass-conservation
|
|
138
|
+
direction, and the binding number on the fixed-mass tangent space is strictly smaller; the safety
|
|
139
|
+
margin loss is `U·min(d,|g|)` and so first-order only while control authority lasts; the zero-action
|
|
140
|
+
rule in general reads "the identified interval contains zero", the symmetric ball being one case;
|
|
141
|
+
and the `§38` premium is quoted with both normalisations because they differ by an order of
|
|
142
|
+
magnitude.
|
|
143
|
+
- Documentation counts corrected after an audit found a silently drifting entry count in the research
|
|
144
|
+
log; the README test count was stale by two releases.
|
|
145
|
+
|
|
146
|
+
### Fixed
|
|
147
|
+
|
|
148
|
+
- `ConfoundingRobustPenalty` used `‖u‖`, whose gradient is NaN at `u = 0` — exactly where the solver
|
|
149
|
+
starts — so `0·NaN` poisoned every step and the control stayed pinned at zero. Now a smoothed
|
|
150
|
+
`√(‖u‖²+ε)`, which zeroes the gradient at the origin and preserves the linear bound.
|
|
151
|
+
- The congestion equilibrium ran a fixed trip count and returned whatever point it reached. Outside
|
|
152
|
+
the contraction region that is a 2-cycle, not an equilibrium: at `βc = 200` it returned a point with
|
|
153
|
+
residual 3.21 on a mass of 6.0 and most of the mass on the *least* attractive zone, and no caller
|
|
154
|
+
could tell. The solver now reports convergence; the root cause was the hard-coded damping `d = ½`,
|
|
155
|
+
whose ceiling is `βc < 6`, so `congestion_damping` supplies the modulus-optimal `d*` that contracts
|
|
156
|
+
for every `βc`. Both shipped users (`chc.marketplace`, causaldyn-bench Track H) sit at `βc = 5`,
|
|
157
|
+
inside the certified region, and their docstrings now say so executably.
|
|
158
|
+
- The convergence tolerance was absolute while the residual carries the units of the state, so a
|
|
159
|
+
float32 solve at the shipped mass could not reach `1e-8` and reported failure at a residual of
|
|
160
|
+
3.8e-07. The residual is now relative (`‖x−T(x)‖ / max(1, ‖x‖)`).
|
|
161
|
+
|
|
162
|
+
### Notes
|
|
163
|
+
|
|
164
|
+
The repository moved to the `causaldyn` organisation and is public as of this release; all project
|
|
165
|
+
URLs point there. This is the **first version published to PyPI** — `0.1.0` below was prepared and
|
|
166
|
+
tagged locally, then overtaken by 71 commits before it was ever pushed, so it never reached an index.
|
|
167
|
+
|
|
168
|
+
## [0.1.0] — 2026-07-19
|
|
169
|
+
|
|
170
|
+
Prepared but never published; superseded by `0.2.0`. `chc` is a small JAX library that fuses
|
|
171
|
+
**physics-structured hybrid dynamics**, **causal identification**, and **constrained optimal control**,
|
|
172
|
+
made safe on offline/confounded data by an explicit **pessimism/support** layer — evaluating decisions
|
|
173
|
+
as interventions, not correlations.
|
|
174
|
+
|
|
175
|
+
### Added
|
|
176
|
+
|
|
177
|
+
- **Hybrid dynamics & sensitivity** — additive `f_known + r_θ` (`chc.dynamics`); MLP / RBF-KAN / graph /
|
|
178
|
+
zero residual backends (`chc.residual`); RK4 rollout (`chc.integrate`); a hand-written discrete adjoint
|
|
179
|
+
verified against autodiff and finite differences, plus an adaptive `diffrax` continuous adjoint
|
|
180
|
+
(`chc.adjoint`); Lie–Trotter / Strang–Marchuk operator splitting (`chc.splitting`).
|
|
181
|
+
- **Causal frontier** — pluggable effect estimators (backdoor OLS, IV/2SLS, cross-fitted Double ML,
|
|
182
|
+
sensitivity + refutation, VanderWeele–Ding **E-values**) behind one Strategy interface
|
|
183
|
+
(`chc.causal`, `chc.estimators`), with lazy EconML/DoWhy adapters; the **R-learner** CATE meta-learner;
|
|
184
|
+
staggered-adoption **DiD** — Callaway–Sant'Anna group-time ATT and the de Chaisemartin–d'Haultfoeuille
|
|
185
|
+
DID_M (`chc.did`); **augmented synthetic control** (`chc.scm`); Robins **g-methods** for time-varying
|
|
186
|
+
treatment (`chc.gmethods`); autocorrelation-robust CI testing + lagged-parent **discovery**
|
|
187
|
+
(`chc.independence`, `chc.discovery`); network/interference-aware DML with a learned GNN nuisance
|
|
188
|
+
(`chc.network_causal`).
|
|
189
|
+
- **Offline safety & guarantees** — density-distance support penalty (`chc.support`) plus **calibrated**
|
|
190
|
+
deep-ensemble + split-conformal uncertainty (`chc.uncertainty`); IPS/SNIPS off-policy value + overlap
|
|
191
|
+
gate (`chc.offpolicy`); an **LQ certainty-equivalence regret bound** and a local nonlinear certificate
|
|
192
|
+
via linearisation (`chc.regret`, `chc.lqr.linearized_regret_certificate`).
|
|
193
|
+
- **Control** — projected-gradient optimal control and receding-horizon MPC (`chc.control`, `chc.mpc`);
|
|
194
|
+
LQR / Riccati (`chc.lqr`); Koopman-LQR (`chc.koopman`); mean-field control (`chc.meanfield`); optimal
|
|
195
|
+
transport (`chc.transport`); differentiable Stackelberg games (`chc.games`); PMP time-optimal bang-bang
|
|
196
|
+
(`chc.mintime`); classical step-response metrics (`chc.metrics`).
|
|
197
|
+
- **Structured operators & dynamic effects** — Toeplitz FFT matvec, Levinson–Durbin, Gohberg–Semencul
|
|
198
|
+
inverse + few-sample covariance (`chc.toeplitz`); impulse-response / local-projection dynamic effects
|
|
199
|
+
(`chc.irf`); Galerkin FEM and a mesh-free Deep Galerkin Poisson solver (`chc.galerkin`,
|
|
200
|
+
`chc.deep_galerkin`).
|
|
201
|
+
- **Benchmark & real-data validation** — oracle-regret tasks (pricing, inventory, support-shift,
|
|
202
|
+
model-uncertainty) with a leaderboard and multi-seed bootstrap CIs (`chc.benchmark`); a causal-methods
|
|
203
|
+
leaderboard vs naive baselines (`chc.causal_bench`); the **LaLonde-Dehejia-Wahba** external causal
|
|
204
|
+
benchmark (`chc.lalonde`, recovers the randomised ATE from CPS-confounded data). The control loop is
|
|
205
|
+
validated live on a real **BOPTEST** `bestest_hydronic_heat_pump` emulator (via the `causaldyn-bench`
|
|
206
|
+
sibling), where the identification + forecast-MPC beat the tuned built-in baseline on every KPI.
|
|
207
|
+
- **Tooling** — `src`-layout, `uv`-managed, `py.typed`; `ruff` + astral `ty` gates; CI test matrix on
|
|
208
|
+
Python 3.12 / 3.13 / 3.14.
|
|
209
|
+
|
|
210
|
+
[0.2.0]: https://github.com/causaldyn/causal-hybrid-control/releases/tag/v0.2.0
|
|
211
|
+
[0.1.0]: https://github.com/causaldyn/causal-hybrid-control/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilia Gradina
|
|
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,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: causal-hybrid-control
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Physics-structured dynamics with a learned causal residual, controlled by constrained optimal control / MPC.
|
|
5
|
+
Project-URL: Homepage, https://github.com/causaldyn/causal-hybrid-control
|
|
6
|
+
Project-URL: Repository, https://github.com/causaldyn/causal-hybrid-control
|
|
7
|
+
Project-URL: Issues, https://github.com/causaldyn/causal-hybrid-control/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/causaldyn/causal-hybrid-control/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Ilia Gradina <ilia.gradina@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: causal-inference,hybrid-models,jax,mpc,optimal-control,scientific-machine-learning,system-identification
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: <3.15,>=3.12
|
|
23
|
+
Requires-Dist: diffrax>=0.6
|
|
24
|
+
Requires-Dist: equinox>=0.11.4
|
|
25
|
+
Requires-Dist: jax>=0.4.30
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: optax>=0.2.3
|
|
28
|
+
Requires-Dist: scipy>=1.13
|
|
29
|
+
Provides-Extra: trees
|
|
30
|
+
Requires-Dist: catboost>=1.2; extra == 'trees'
|
|
31
|
+
Requires-Dist: lightgbm>=4; extra == 'trees'
|
|
32
|
+
Requires-Dist: scikit-learn>=1.4; extra == 'trees'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# causal-hybrid-control
|
|
36
|
+
|
|
37
|
+
Physics-structured dynamics with a **learned causal residual**, controlled by **constrained optimal
|
|
38
|
+
control / MPC**, and made safe on offline, confounded data by an explicit **pessimism / support** layer.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
ẋ = f_known(x, u, t; p) + r_θ(x, u, t) # known mechanism + learned residual
|
|
42
|
+
u* = argmin_u J_task(u) + λ_unc·U(x,u) + λ_supp·D((x,u), 𝒟) # pessimistic constrained control
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Most data science stops at prediction. The value is in *decisions* — and a decision changes the future,
|
|
46
|
+
so it must be evaluated as an **intervention**, not a correlation, and chosen by **optimal control**, not
|
|
47
|
+
by argmax over a predictive score. `chc` is a small JAX library for that step.
|
|
48
|
+
|
|
49
|
+
## The one result
|
|
50
|
+
|
|
51
|
+
On a confounded offline log, fitting the effect of the action *without* adjusting for the confounder
|
|
52
|
+
flips its sign (true `+1.0` → naive `-0.2`). Control the true system with each estimate:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
controller cost regret viol ood
|
|
56
|
+
causal-CHC 4.59 -0.00 0.00 0.00
|
|
57
|
+
oracle 4.59 0.00 0.00 0.00
|
|
58
|
+
predictive 13740.08 13735.49 0.97 1.00
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The **causal** controller matches the oracle; the **predictive** one is catastrophic on every metric —
|
|
62
|
+
it drives the state the wrong way (`x → -20` for target `+2`), violates constraints 97% of the time, and
|
|
63
|
+
acts entirely out of the logged support. That is one seed, with the constraint and support columns;
|
|
64
|
+
`uv run python scripts/run_benchmark.py` runs this task and four others over 12 seeds with bootstrap
|
|
65
|
+
CIs (predictive regret `13734.15 [13732.55, 13735.31]`), and
|
|
66
|
+
`uv run --group viz python scripts/flagship_demo.py` draws the figure.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv sync # JAX + Diffrax + Equinox + Optax + NumPy + SciPy (Python 3.12–3.14)
|
|
72
|
+
uv run pytest # 353 passed, 2 skipped (tigramite, lightgbm: bring-your-own-env)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import jax, jax.numpy as jnp
|
|
79
|
+
from chc import DampedOscillator, HybridDynamics, KANResidual, QuadraticCost, mpc_control
|
|
80
|
+
|
|
81
|
+
# hybrid dynamics: known oscillator + a learnable (KAN) residual, swappable for MLP/linear
|
|
82
|
+
model = HybridDynamics(
|
|
83
|
+
known=DampedOscillator(omega=1.0, zeta=0.1),
|
|
84
|
+
residual=KANResidual(state_dim=2, control_dim=1, out_dim=2, key=jax.random.key(0)),
|
|
85
|
+
)
|
|
86
|
+
cost = QuadraticCost(Q=jnp.diag(jnp.array([1.0, 0.1])), R=jnp.array([[0.05]]),
|
|
87
|
+
Qf=jnp.diag(jnp.array([5.0, 1.0])), x_target=jnp.zeros(2))
|
|
88
|
+
|
|
89
|
+
xs, us = mpc_control(model, jnp.array([1.0, 0.0]), cost, dt=0.1,
|
|
90
|
+
horizon=20, u_lo=-5.0, u_hi=5.0, n_steps=40) # closed-loop MPC
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Example notebooks
|
|
94
|
+
|
|
95
|
+
Worked, executed notebooks (figures + tables) under [`notebooks/`](notebooks/) — open in JupyterLab
|
|
96
|
+
(`uv sync --group notebooks && uv run --group notebooks jupyter lab`) or read on GitHub:
|
|
97
|
+
|
|
98
|
+
| notebook | what it shows |
|
|
99
|
+
|---|---|
|
|
100
|
+
| [`01_causal_vs_predictive_control`](notebooks/01_causal_vs_predictive_control.ipynb) | the headline: predictive control diverges under confounding, causal control matches the oracle |
|
|
101
|
+
| [`02_learn_hidden_physics`](notebooks/02_learn_hidden_physics.ipynb) | hybrid dynamics + system ID: recover an omitted cubic term; multi-step training cuts drift |
|
|
102
|
+
| [`03_causal_inference_toolkit`](notebooks/03_causal_inference_toolkit.ipynb) | adjustment · IV/2SLS · Double ML · sensitivity · refutation, side by side |
|
|
103
|
+
| [`04_epidemic_and_pessimism`](notebooks/04_epidemic_and_pessimism.ipynb) | flatten an epidemic curve under a capacity cap; pessimism vs a greedy controller |
|
|
104
|
+
| [`05_benchmark_scoreboard`](notebooks/05_benchmark_scoreboard.ipynb) | the scoreboard: regret vs oracle across every task — CHC lands next to the oracle, the baseline blows up |
|
|
105
|
+
| [`05_confounding_robust_control`](notebooks/05_confounding_robust_control.ipynb) | when **no adjustment set exists**: a sensitivity level `Γ` → identification radius → minimax action. Worst-case cost 1.23 → 0.35; 96% cheaper at realistic confounding, and the price is a 26%-of-the-CE-downside premium when there is none |
|
|
106
|
+
| [`06_cruise_control_confounded`](notebooks/06_cruise_control_confounded.ipynb) | relatable end-to-end: adaptive cruise control from confounded fleet logs (Simpson's paradox → IV → control) |
|
|
107
|
+
| [`07_real_data_lalonde`](notebooks/07_real_data_lalonde.ipynb) | **real data, experimental ground truth**: on LaLonde NSW the naive estimate flips sign (−$8.5k), Double ML recovers the randomised truth — +$1.6k against the experiment's +$1.8k, within $234 |
|
|
108
|
+
|
|
109
|
+
Sources are paired `.py` (jupytext) next to each `.ipynb`.
|
|
110
|
+
|
|
111
|
+
## What's inside
|
|
112
|
+
|
|
113
|
+
| area | module | what it does |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| dynamics | `dynamics`, `residual`, `integrate` | hybrid `f_known + r_θ`; MLP / **RBF-KAN** / graph / **port-Hamiltonian** (passive, Lyapunov-stable) / **Lipschitz-certified** residuals; RK4 |
|
|
116
|
+
| sensitivity | `adjoint` | discrete adjoint (verified == autodiff == finite differences) |
|
|
117
|
+
| classical OC | `lqr` | LQR / AKOR (Riccati) — the `r_θ→0` limit and correctness baseline |
|
|
118
|
+
| identification | `train`, `causal`, `estimators`, `gmethods` | system ID (one/multi-step); pluggable effect backend — adjustment, **IV/2SLS**, **DML**, sensitivity, refutation, + optional **EconML/DoWhy** adapters; Robins' **g-formula** (cross-fitted) for a treatment *sequence* under time-varying confounding |
|
|
119
|
+
| control | `cost`, `control`, `mpc`, `splitting`, `plan` | Bolza objective; projected-gradient OC; receding-horizon MPC; **Strang–Marchuk** splitting; `causal_plan` — the one-call spine returning a plan *with* its uncertainty tube and certified horizon attached |
|
|
120
|
+
| offline safety | `support`, `offpolicy`, `uncertainty` | pessimism penalty; IPS/SNIPS off-policy value + overlap gate; **calibrated** deep-ensemble + split-conformal uncertainty; a **time-consistent nested-CVaR** aggregation of that disagreement (the risk-neutral sum averages one very bad step away); **Wasserstein-1 DRO** distribution-shift margin; **certified rollout tubes** (Lipschitz / contractive-log-norm Grönwall bounds → time-varying uncertainty tube, safety-tightening, certified-safe horizon), **Rocq-proved** |
|
|
121
|
+
| guarantee | `regret` | LQ certainty-equivalence bound — quadratic in model error (Dean–Mania–Tu–Recht–Matni); **interference-aware regret certificate** (extra exposure-map-error term), **machine-checked in Rocq** |
|
|
122
|
+
| sensitivity-aware control | `sensitivity` (facade over `regret`, `uncertainty`, `barrier`) | **control under HIDDEN CONFOUNDING**: bounded-density-ratio (MSM) CVaR worst-case → pessimism-radius inflation; the confounding-regret floor is *second-order* in the effect bias; a **minimax controller** that shifts the gain under asymmetric (over/under-shoot) loss and beats certainty-equivalence — now a **closed-loop** controller on a confounded dynamic plant (bounds the worst-case downside, 82% cheaper over 30 steps), plus a `ConfoundingRobustPenalty` that carries the sensitivity radius into the general pessimistic-control stack — all **Rocq-certified**. `chc.sensitivity` is the one-import surface (estimate→radius→control) |
|
|
123
|
+
| safety under partial ID | `barrier`, `plan` | the same sensitivity radius spent on a **constraint**: robust control-barrier margin, a least-restrictive safety filter (closed-form certified action interval, no QP), and `Gamma*` — **the largest sensitivity-model level under which the barrier stays certified** (a model parameter, not measured confounding). Safety degrades at *first* order in the effect bias (until the radius swallows the channel and the loss saturates) where performance regret degrades at second (the envelope theorem protects objectives, not binding constraints), **Rocq-certified**; in closed loop a regret-sized budget violates the limit on 93% of steps where the constraint-sized one never does. `certify_safety` audits a finished plan against all of it — the certified prefix next to the plan's `Gamma*` (the weakest step's, exactly) |
|
|
124
|
+
| what the certificate is worth | `reachability` | the **Hamilton–Jacobi** answer the barrier only approximates: `V(x,T) = max_u min_{ΔB} min_s h(ξ(s))` on a Lax–Friedrichs grid, with the §32 identification radius as the adversary. Same robust-margin algebra as `barrier`, but `p = ∇V` is *solved for* rather than assumed. Turns the CBF theorem into an executable check (condition on all of `{h ≥ 0}` ⟹ the tube **is** `{h ≥ 0}`) and prices what pointwise certification misses — on a relative-degree-2 barrier the §40 verdict is identical at every radius while the true tube shrinks (6.4% of the grid certified-and-unreachable), so `certify_safety`'s per-step prefix is a filter, not a proof. `uv run python scripts/reachability_demo.py` |
|
|
125
|
+
| end to end | `spine` | all four layers on **one** decision — confounded logs → causal gain → constrained plan → `Gamma*` certificate → the same plan run on the *true* plant. Two zones of a mobile driver pool, one incentive lever whose `[+b, -b]` column is driver conservation, a supply floor in the zone it drains. The confounded arm plans 13.6 and pays 38.5; `Gamma*` tells the two arms apart (7.46 vs 1.18) **before either acts**, without ground truth. `uv run python scripts/spine_demo.py` |
|
|
126
|
+
| causal frontier | `did`, `scm`, `estimators`, `causal` | Callaway–Sant'Anna staggered **DiD**; **augmented synthetic control**; **R-learner** CATE; **E-values** beside Cinelli–Hazlett; **influence-function CIs** on cross-fit DML |
|
|
127
|
+
| dynamic effects | `irf`, `toeplitz` | impulse-response / local-projection dynamic effects; Toeplitz / Levinson–Durbin / Gohberg–Semencul operators |
|
|
128
|
+
| structure discovery | `discovery`, `independence`, `network_causal`, `pathway` | lagged-parent discovery; MCI partial-correlation test; network/spillover orthogonal DML; **ranked temporal causal pathway** — which lagged variables & multi-step chains drive a target, signed + actionable (Rocq-certified walk-sum / geometric-truncation / weakest-link laws) |
|
|
129
|
+
| advanced control | `koopman`, `meanfield`, `transport`, `matching`, `games`, `mintime` | Koopman-LQR; mean-field control; continuum + discrete **Kantorovich OT** (driver↔rider matching → **dual surge prices**); differentiable Stackelberg games over a **certified** congestion equilibrium (implicit-function gradients, contraction certificate, optimal damping — the solver reports its residual instead of silently returning a non-equilibrium); PMP time-optimal bang-bang |
|
|
130
|
+
| marketplace moat | `marketplace` | **offline causal control under equilibrium interference**: learn incentives from confounded switchback logs where SUTVA fails — de-confounded + equilibrium-aware + W-DRO-pessimistic control recovers the oracle where MOPO / naive-causal go *negative* |
|
|
131
|
+
| evaluation | `benchmark`, `causal_bench`, `flagship`, `lalonde`, `metrics`, `surrogate` | pricing / inventory / support-shift / **model-uncertainty** / **confounding-robust** oracle-regret tasks + leaderboard with multi-seed bootstrap CIs; a causal-methods table scoring every frontier estimator against the naive baseline it is meant to beat; real-data **LaLonde** validation; step-response quality metrics; a gradient-boosted tree surrogate as the tabular prediction competitor (optional `trees` extra) |
|
|
132
|
+
| scientific / PDE | `epidemic`, `galerkin`, `deep_galerkin` | SIR epidemic control (flatten the curve); 1D/2D Galerkin FEM (progonka); mesh-free **Deep Galerkin** neural Poisson solver |
|
|
133
|
+
|
|
134
|
+
## Validation
|
|
135
|
+
|
|
136
|
+
Correctness is cross-checked in independent tools, symbolic first (`validation/`): the ARE / matrix
|
|
137
|
+
exponential are verified **Maxima**-authoritative (exact + high-precision `bfloat`) against **PARI/GP**
|
|
138
|
+
(50-digit) and **Octave**, with SciPy used only as the fast float64 numeric. The control and guarantee
|
|
139
|
+
invariants are **formally proved in Rocq** — 37 files under `proofs/`, from the box-projection bounds
|
|
140
|
+
and idempotence (`box_projection.v`) to the interference-aware regret certificate.
|
|
141
|
+
|
|
142
|
+
## Honest positioning
|
|
143
|
+
|
|
144
|
+
`chc` composes ideas that exist — hybrid dynamics (SciML UDE), pessimistic offline control
|
|
145
|
+
(MOPO/MOReL/Delphic), sequential causal identification (g-methods / dynamic treatment regimes),
|
|
146
|
+
differentiable control (Neuromancer). The contribution is the *integration behind one API* plus a
|
|
147
|
+
benchmark with ground-truth interventional effects. KAN is **one interpretable residual backend**, not
|
|
148
|
+
the identity of the framework. The release-by-release record, scope corrections included, is in
|
|
149
|
+
[`CHANGELOG.md`](CHANGELOG.md).
|
|
150
|
+
|
|
151
|
+
## Status
|
|
152
|
+
|
|
153
|
+
Early (`v0.2.0`), single-author, research code (354 collected tests; Python 3.12–3.14, astral `ruff` + `ty`).
|
|
154
|
+
Working: hybrid dynamics + adjoint (discrete and adaptive `diffrax`), LQR, system ID (one-/multi-step),
|
|
155
|
+
causal identification (adjustment / IV / DML / sensitivity / refutation) plus the modern frontier —
|
|
156
|
+
Callaway–Sant'Anna staggered DiD, augmented synthetic control, R-learner CATE, E-values; **calibrated**
|
|
157
|
+
pessimism (deep ensemble + split conformal) and an LQ certainty-equivalence regret guarantee; MPC,
|
|
158
|
+
Strang–Marchuk splitting, off-policy gate, KAN/MLP/Graph residual backends; advanced control backends
|
|
159
|
+
(Koopman-LQR, mean-field, optimal transport, differentiable Stackelberg games, PMP time-optimal
|
|
160
|
+
bang-bang); dynamic-effect IRFs + structured Toeplitz/Levinson/Gohberg–Semencul operators; lagged
|
|
161
|
+
structure discovery; five benchmark tasks (pricing, inventory, support-shift, model-uncertainty,
|
|
162
|
+
confounding-robust) with multi-seed bootstrap CIs; two flagships (pricing, epidemic); 1D/2D Galerkin FEM + a mesh-free Deep
|
|
163
|
+
Galerkin neural Poisson solver; and step-response quality metrics. Both halves are now validated on
|
|
164
|
+
**real** targets, not just synthetic ones: the **causal identification core** on real data with an
|
|
165
|
+
experimental ground truth (notebook 07, LaLonde NSW: the naive estimate flips sign, Double ML recovers
|
|
166
|
+
the randomised benchmark), and the **control loop** on a real building emulator — the identification +
|
|
167
|
+
forecast-MPC of this library, run via `causaldyn-bench` against a live **BOPTEST**
|
|
168
|
+
`bestest_hydronic_heat_pump`, beats the tuned built-in baseline on *every* KPI at once (thermal
|
|
169
|
+
discomfort 8.01→7.32, energy 0.393→0.354, cost 0.100→0.090, emissions 0.066→0.059 — a clean Pareto
|
|
170
|
+
win). Roadmap: more real tasks, the Medium/paper writeups, and — only if a real-time/edge deployment
|
|
171
|
+
target appears — a compiled runtime.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|