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.
Files changed (231) hide show
  1. causal_hybrid_control-0.2.0/.github/workflows/ci.yml +41 -0
  2. causal_hybrid_control-0.2.0/.github/workflows/release.yml +41 -0
  3. causal_hybrid_control-0.2.0/.gitignore +43 -0
  4. causal_hybrid_control-0.2.0/.python-version +1 -0
  5. causal_hybrid_control-0.2.0/CHANGELOG.md +211 -0
  6. causal_hybrid_control-0.2.0/LICENSE +21 -0
  7. causal_hybrid_control-0.2.0/PKG-INFO +175 -0
  8. causal_hybrid_control-0.2.0/README.md +141 -0
  9. causal_hybrid_control-0.2.0/notebooks/01_causal_vs_predictive_control.ipynb +433 -0
  10. causal_hybrid_control-0.2.0/notebooks/01_causal_vs_predictive_control.py +121 -0
  11. causal_hybrid_control-0.2.0/notebooks/02_learn_hidden_physics.ipynb +404 -0
  12. causal_hybrid_control-0.2.0/notebooks/02_learn_hidden_physics.py +165 -0
  13. causal_hybrid_control-0.2.0/notebooks/03_causal_inference_toolkit.ipynb +593 -0
  14. causal_hybrid_control-0.2.0/notebooks/03_causal_inference_toolkit.py +140 -0
  15. causal_hybrid_control-0.2.0/notebooks/04_epidemic_and_pessimism.ipynb +314 -0
  16. causal_hybrid_control-0.2.0/notebooks/04_epidemic_and_pessimism.py +106 -0
  17. causal_hybrid_control-0.2.0/notebooks/05_benchmark_scoreboard.ipynb +426 -0
  18. causal_hybrid_control-0.2.0/notebooks/05_benchmark_scoreboard.py +127 -0
  19. causal_hybrid_control-0.2.0/notebooks/05_confounding_robust_control.ipynb +325 -0
  20. causal_hybrid_control-0.2.0/notebooks/05_confounding_robust_control.py +128 -0
  21. causal_hybrid_control-0.2.0/notebooks/06_cruise_control_confounded.ipynb +352 -0
  22. causal_hybrid_control-0.2.0/notebooks/06_cruise_control_confounded.py +140 -0
  23. causal_hybrid_control-0.2.0/notebooks/07_real_data_lalonde.ipynb +435 -0
  24. causal_hybrid_control-0.2.0/notebooks/07_real_data_lalonde.py +151 -0
  25. causal_hybrid_control-0.2.0/proofs/adaptive_exploration.v +109 -0
  26. causal_hybrid_control-0.2.0/proofs/bandit_causal.v +37 -0
  27. causal_hybrid_control-0.2.0/proofs/barrier_feasibility.v +197 -0
  28. causal_hybrid_control-0.2.0/proofs/box_projection.v +38 -0
  29. causal_hybrid_control-0.2.0/proofs/c2_end_to_end.v +93 -0
  30. causal_hybrid_control-0.2.0/proofs/causal_mpc.v +52 -0
  31. causal_hybrid_control-0.2.0/proofs/causal_pathway.v +99 -0
  32. causal_hybrid_control-0.2.0/proofs/clustered_van_trees.v +93 -0
  33. causal_hybrid_control-0.2.0/proofs/composition_transfer.v +37 -0
  34. causal_hybrid_control-0.2.0/proofs/confounded_turnpike.v +40 -0
  35. causal_hybrid_control-0.2.0/proofs/confounding_lq_regret.v +58 -0
  36. causal_hybrid_control-0.2.0/proofs/confounding_robust_control.v +162 -0
  37. causal_hybrid_control-0.2.0/proofs/confounding_robust_cvar.v +71 -0
  38. causal_hybrid_control-0.2.0/proofs/congestion_contraction.v +91 -0
  39. causal_hybrid_control-0.2.0/proofs/constrained_ce_regret.v +60 -0
  40. causal_hybrid_control-0.2.0/proofs/contractive_euler.v +42 -0
  41. causal_hybrid_control-0.2.0/proofs/doubly_robust.v +34 -0
  42. causal_hybrid_control-0.2.0/proofs/dynamic_causal_mpc.v +54 -0
  43. causal_hybrid_control-0.2.0/proofs/ensemble_control.v +50 -0
  44. causal_hybrid_control-0.2.0/proofs/equilibrium_transfer.v +98 -0
  45. causal_hybrid_control-0.2.0/proofs/exposure_map_c2.v +62 -0
  46. causal_hybrid_control-0.2.0/proofs/highprob_regret.v +37 -0
  47. causal_hybrid_control-0.2.0/proofs/hinf_robust_regret.v +66 -0
  48. causal_hybrid_control-0.2.0/proofs/information_lower_bound.v +35 -0
  49. causal_hybrid_control-0.2.0/proofs/interference_convexity.v +34 -0
  50. causal_hybrid_control-0.2.0/proofs/interference_orthogonal.v +40 -0
  51. causal_hybrid_control-0.2.0/proofs/interference_regret.v +56 -0
  52. causal_hybrid_control-0.2.0/proofs/lipschitz_rollout.v +168 -0
  53. causal_hybrid_control-0.2.0/proofs/multichannel_control.v +55 -0
  54. causal_hybrid_control-0.2.0/proofs/nonlinear_regret.v +53 -0
  55. causal_hybrid_control-0.2.0/proofs/optimal_exploration.v +56 -0
  56. causal_hybrid_control-0.2.0/proofs/orthogonal_control.v +40 -0
  57. causal_hybrid_control-0.2.0/proofs/partial_id_control.v +31 -0
  58. causal_hybrid_control-0.2.0/proofs/pessimistic_optimality.v +90 -0
  59. causal_hybrid_control-0.2.0/proofs/port_hamiltonian_lyapunov.v +69 -0
  60. causal_hybrid_control-0.2.0/proofs/transportability_regret.v +42 -0
  61. causal_hybrid_control-0.2.0/proofs/van_trees.v +51 -0
  62. causal_hybrid_control-0.2.0/pyproject.toml +96 -0
  63. causal_hybrid_control-0.2.0/scripts/confounding_demo.py +29 -0
  64. causal_hybrid_control-0.2.0/scripts/epidemic_demo.py +55 -0
  65. causal_hybrid_control-0.2.0/scripts/flagship_demo.py +46 -0
  66. causal_hybrid_control-0.2.0/scripts/reachability_demo.py +110 -0
  67. causal_hybrid_control-0.2.0/scripts/run_benchmark.py +47 -0
  68. causal_hybrid_control-0.2.0/scripts/run_causal_bench.py +17 -0
  69. causal_hybrid_control-0.2.0/scripts/run_confounding_control_demo.py +40 -0
  70. causal_hybrid_control-0.2.0/scripts/run_dynamic_confounding_demo.py +38 -0
  71. causal_hybrid_control-0.2.0/scripts/run_marketplace_demo.py +17 -0
  72. causal_hybrid_control-0.2.0/scripts/spine_demo.py +43 -0
  73. causal_hybrid_control-0.2.0/src/chc/__init__.py +552 -0
  74. causal_hybrid_control-0.2.0/src/chc/adjoint.py +109 -0
  75. causal_hybrid_control-0.2.0/src/chc/barrier.py +401 -0
  76. causal_hybrid_control-0.2.0/src/chc/benchmark.py +620 -0
  77. causal_hybrid_control-0.2.0/src/chc/causal.py +288 -0
  78. causal_hybrid_control-0.2.0/src/chc/causal_bench.py +138 -0
  79. causal_hybrid_control-0.2.0/src/chc/control.py +58 -0
  80. causal_hybrid_control-0.2.0/src/chc/cost.py +36 -0
  81. causal_hybrid_control-0.2.0/src/chc/deep_galerkin.py +79 -0
  82. causal_hybrid_control-0.2.0/src/chc/did.py +154 -0
  83. causal_hybrid_control-0.2.0/src/chc/discovery.py +196 -0
  84. causal_hybrid_control-0.2.0/src/chc/dynamics.py +59 -0
  85. causal_hybrid_control-0.2.0/src/chc/epidemic.py +86 -0
  86. causal_hybrid_control-0.2.0/src/chc/estimators.py +282 -0
  87. causal_hybrid_control-0.2.0/src/chc/flagship.py +91 -0
  88. causal_hybrid_control-0.2.0/src/chc/galerkin.py +95 -0
  89. causal_hybrid_control-0.2.0/src/chc/games.py +421 -0
  90. causal_hybrid_control-0.2.0/src/chc/gmethods.py +102 -0
  91. causal_hybrid_control-0.2.0/src/chc/independence.py +62 -0
  92. causal_hybrid_control-0.2.0/src/chc/integrate.py +36 -0
  93. causal_hybrid_control-0.2.0/src/chc/irf.py +103 -0
  94. causal_hybrid_control-0.2.0/src/chc/koopman.py +92 -0
  95. causal_hybrid_control-0.2.0/src/chc/lalonde.py +116 -0
  96. causal_hybrid_control-0.2.0/src/chc/lqr.py +104 -0
  97. causal_hybrid_control-0.2.0/src/chc/marketplace.py +180 -0
  98. causal_hybrid_control-0.2.0/src/chc/matching.py +164 -0
  99. causal_hybrid_control-0.2.0/src/chc/meanfield.py +110 -0
  100. causal_hybrid_control-0.2.0/src/chc/metrics.py +79 -0
  101. causal_hybrid_control-0.2.0/src/chc/mintime.py +100 -0
  102. causal_hybrid_control-0.2.0/src/chc/mpc.py +64 -0
  103. causal_hybrid_control-0.2.0/src/chc/network_causal.py +202 -0
  104. causal_hybrid_control-0.2.0/src/chc/offpolicy.py +70 -0
  105. causal_hybrid_control-0.2.0/src/chc/pathway.py +274 -0
  106. causal_hybrid_control-0.2.0/src/chc/plan.py +258 -0
  107. causal_hybrid_control-0.2.0/src/chc/py.typed +0 -0
  108. causal_hybrid_control-0.2.0/src/chc/reachability.py +294 -0
  109. causal_hybrid_control-0.2.0/src/chc/regret.py +2573 -0
  110. causal_hybrid_control-0.2.0/src/chc/residual.py +444 -0
  111. causal_hybrid_control-0.2.0/src/chc/scm.py +113 -0
  112. causal_hybrid_control-0.2.0/src/chc/sensitivity.py +155 -0
  113. causal_hybrid_control-0.2.0/src/chc/spine.py +158 -0
  114. causal_hybrid_control-0.2.0/src/chc/splitting.py +49 -0
  115. causal_hybrid_control-0.2.0/src/chc/support.py +120 -0
  116. causal_hybrid_control-0.2.0/src/chc/surrogate.py +90 -0
  117. causal_hybrid_control-0.2.0/src/chc/toeplitz.py +165 -0
  118. causal_hybrid_control-0.2.0/src/chc/train.py +107 -0
  119. causal_hybrid_control-0.2.0/src/chc/transport.py +167 -0
  120. causal_hybrid_control-0.2.0/src/chc/uncertainty.py +879 -0
  121. causal_hybrid_control-0.2.0/tests/conftest.py +5 -0
  122. causal_hybrid_control-0.2.0/tests/test_adjoint.py +49 -0
  123. causal_hybrid_control-0.2.0/tests/test_barrier.py +220 -0
  124. causal_hybrid_control-0.2.0/tests/test_benchmark.py +103 -0
  125. causal_hybrid_control-0.2.0/tests/test_benchmark_multiseed.py +53 -0
  126. causal_hybrid_control-0.2.0/tests/test_cate.py +36 -0
  127. causal_hybrid_control-0.2.0/tests/test_causal.py +106 -0
  128. causal_hybrid_control-0.2.0/tests/test_causal_bench.py +29 -0
  129. causal_hybrid_control-0.2.0/tests/test_confounding_lq_regret.py +101 -0
  130. causal_hybrid_control-0.2.0/tests/test_confounding_penalty.py +97 -0
  131. causal_hybrid_control-0.2.0/tests/test_confounding_robust.py +99 -0
  132. causal_hybrid_control-0.2.0/tests/test_confounding_robust_control.py +143 -0
  133. causal_hybrid_control-0.2.0/tests/test_control.py +35 -0
  134. causal_hybrid_control-0.2.0/tests/test_deep_galerkin.py +18 -0
  135. causal_hybrid_control-0.2.0/tests/test_did.py +75 -0
  136. causal_hybrid_control-0.2.0/tests/test_diffrax_adjoint.py +57 -0
  137. causal_hybrid_control-0.2.0/tests/test_discovery.py +58 -0
  138. causal_hybrid_control-0.2.0/tests/test_discovery_wiring.py +68 -0
  139. causal_hybrid_control-0.2.0/tests/test_epidemic.py +26 -0
  140. causal_hybrid_control-0.2.0/tests/test_estimators.py +84 -0
  141. causal_hybrid_control-0.2.0/tests/test_flagship.py +12 -0
  142. causal_hybrid_control-0.2.0/tests/test_galerkin.py +58 -0
  143. causal_hybrid_control-0.2.0/tests/test_games.py +209 -0
  144. causal_hybrid_control-0.2.0/tests/test_gmethods.py +55 -0
  145. causal_hybrid_control-0.2.0/tests/test_graph_residual.py +70 -0
  146. causal_hybrid_control-0.2.0/tests/test_hybrid.py +31 -0
  147. causal_hybrid_control-0.2.0/tests/test_independence.py +42 -0
  148. causal_hybrid_control-0.2.0/tests/test_irf.py +108 -0
  149. causal_hybrid_control-0.2.0/tests/test_kan.py +60 -0
  150. causal_hybrid_control-0.2.0/tests/test_koopman.py +51 -0
  151. causal_hybrid_control-0.2.0/tests/test_lalonde.py +39 -0
  152. causal_hybrid_control-0.2.0/tests/test_lipschitz_rollout.py +123 -0
  153. causal_hybrid_control-0.2.0/tests/test_lqr.py +72 -0
  154. causal_hybrid_control-0.2.0/tests/test_marketplace.py +72 -0
  155. causal_hybrid_control-0.2.0/tests/test_matching.py +49 -0
  156. causal_hybrid_control-0.2.0/tests/test_meanfield.py +9 -0
  157. causal_hybrid_control-0.2.0/tests/test_metrics.py +51 -0
  158. causal_hybrid_control-0.2.0/tests/test_mintime.py +55 -0
  159. causal_hybrid_control-0.2.0/tests/test_mpc.py +40 -0
  160. causal_hybrid_control-0.2.0/tests/test_network_causal.py +39 -0
  161. causal_hybrid_control-0.2.0/tests/test_offpolicy.py +55 -0
  162. causal_hybrid_control-0.2.0/tests/test_pathway.py +104 -0
  163. causal_hybrid_control-0.2.0/tests/test_plan.py +187 -0
  164. causal_hybrid_control-0.2.0/tests/test_reachability.py +266 -0
  165. causal_hybrid_control-0.2.0/tests/test_regret.py +476 -0
  166. causal_hybrid_control-0.2.0/tests/test_residual_backbones.py +99 -0
  167. causal_hybrid_control-0.2.0/tests/test_scm.py +78 -0
  168. causal_hybrid_control-0.2.0/tests/test_sensitivity.py +35 -0
  169. causal_hybrid_control-0.2.0/tests/test_sensitivity_properties.py +130 -0
  170. causal_hybrid_control-0.2.0/tests/test_spine.py +60 -0
  171. causal_hybrid_control-0.2.0/tests/test_splitting.py +61 -0
  172. causal_hybrid_control-0.2.0/tests/test_support.py +50 -0
  173. causal_hybrid_control-0.2.0/tests/test_surrogate.py +48 -0
  174. causal_hybrid_control-0.2.0/tests/test_tigramite_adapter.py +37 -0
  175. causal_hybrid_control-0.2.0/tests/test_toeplitz.py +92 -0
  176. causal_hybrid_control-0.2.0/tests/test_train.py +83 -0
  177. causal_hybrid_control-0.2.0/tests/test_transport.py +62 -0
  178. causal_hybrid_control-0.2.0/tests/test_uncertainty.py +125 -0
  179. causal_hybrid_control-0.2.0/tests/test_wasserstein.py +128 -0
  180. causal_hybrid_control-0.2.0/uv.lock +2526 -0
  181. causal_hybrid_control-0.2.0/validation/adaptive_exploration.mac +63 -0
  182. causal_hybrid_control-0.2.0/validation/bandit_causal.mac +30 -0
  183. causal_hybrid_control-0.2.0/validation/barrier_feasibility.mac +151 -0
  184. causal_hybrid_control-0.2.0/validation/barrier_feasibility.py +182 -0
  185. causal_hybrid_control-0.2.0/validation/c2_dml_remainder.mac +34 -0
  186. causal_hybrid_control-0.2.0/validation/c2_end_to_end.mac +34 -0
  187. causal_hybrid_control-0.2.0/validation/causal_mpc.mac +37 -0
  188. causal_hybrid_control-0.2.0/validation/causal_pathway.mac +37 -0
  189. causal_hybrid_control-0.2.0/validation/clustered_rate_check.R +52 -0
  190. causal_hybrid_control-0.2.0/validation/clustered_van_trees.mac +36 -0
  191. causal_hybrid_control-0.2.0/validation/composition_transfer.mac +36 -0
  192. causal_hybrid_control-0.2.0/validation/confounded_turnpike.mac +42 -0
  193. causal_hybrid_control-0.2.0/validation/confounding_lq_regret.mac +49 -0
  194. causal_hybrid_control-0.2.0/validation/confounding_robust_control.mac +57 -0
  195. causal_hybrid_control-0.2.0/validation/confounding_robust_cvar.mac +42 -0
  196. causal_hybrid_control-0.2.0/validation/congestion_contraction.mac +81 -0
  197. causal_hybrid_control-0.2.0/validation/congestion_contraction.py +97 -0
  198. causal_hybrid_control-0.2.0/validation/constrained_ce_regret.mac +50 -0
  199. causal_hybrid_control-0.2.0/validation/contractive_euler.mac +38 -0
  200. causal_hybrid_control-0.2.0/validation/doubly_robust.mac +25 -0
  201. causal_hybrid_control-0.2.0/validation/dynamic_causal_mpc.mac +26 -0
  202. causal_hybrid_control-0.2.0/validation/ensemble_control.mac +41 -0
  203. causal_hybrid_control-0.2.0/validation/equilibrium_transfer.mac +76 -0
  204. causal_hybrid_control-0.2.0/validation/equilibrium_transfer.py +83 -0
  205. causal_hybrid_control-0.2.0/validation/exposure_map_c2.mac +30 -0
  206. causal_hybrid_control-0.2.0/validation/hat_stiffness.mac +9 -0
  207. causal_hybrid_control-0.2.0/validation/highprob_regret.mac +33 -0
  208. causal_hybrid_control-0.2.0/validation/hinf_robust_regret.mac +63 -0
  209. causal_hybrid_control-0.2.0/validation/information_lower_bound.mac +34 -0
  210. causal_hybrid_control-0.2.0/validation/interference_convexity.mac +33 -0
  211. causal_hybrid_control-0.2.0/validation/interference_orthogonal.mac +28 -0
  212. causal_hybrid_control-0.2.0/validation/interference_regret.mac +33 -0
  213. causal_hybrid_control-0.2.0/validation/kantorovich_ot.m +42 -0
  214. causal_hybrid_control-0.2.0/validation/lipschitz_rollout.mac +43 -0
  215. causal_hybrid_control-0.2.0/validation/lqr_care.gp +8 -0
  216. causal_hybrid_control-0.2.0/validation/lqr_care.input +6 -0
  217. causal_hybrid_control-0.2.0/validation/lqr_care.m +23 -0
  218. causal_hybrid_control-0.2.0/validation/lqr_care.mac +31 -0
  219. causal_hybrid_control-0.2.0/validation/marketplace_interference.m +45 -0
  220. causal_hybrid_control-0.2.0/validation/matrix_exp.mac +10 -0
  221. causal_hybrid_control-0.2.0/validation/multichannel_control.mac +44 -0
  222. causal_hybrid_control-0.2.0/validation/nonlinear_regret.mac +27 -0
  223. causal_hybrid_control-0.2.0/validation/optimal_exploration.mac +48 -0
  224. causal_hybrid_control-0.2.0/validation/orthogonal_control.mac +46 -0
  225. causal_hybrid_control-0.2.0/validation/partial_id_control.mac +27 -0
  226. causal_hybrid_control-0.2.0/validation/pessimistic_optimality.mac +33 -0
  227. causal_hybrid_control-0.2.0/validation/port_hamiltonian_lyapunov.mac +50 -0
  228. causal_hybrid_control-0.2.0/validation/transportability_regret.mac +35 -0
  229. causal_hybrid_control-0.2.0/validation/van_trees.mac +38 -0
  230. causal_hybrid_control-0.2.0/validation/wasserstein_dro.m +46 -0
  231. 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