FastLSQ 0.2.5__tar.gz → 0.2.6__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 (142) hide show
  1. {fastlsq-0.2.5 → fastlsq-0.2.6}/FastLSQ.egg-info/PKG-INFO +1 -1
  2. {fastlsq-0.2.5 → fastlsq-0.2.6}/FastLSQ.egg-info/SOURCES.txt +33 -0
  3. {fastlsq-0.2.5 → fastlsq-0.2.6}/PKG-INFO +1 -1
  4. fastlsq-0.2.6/examples/digital_twins/darcy_heat.py +583 -0
  5. fastlsq-0.2.6/examples/digital_twins/pendulum.py +242 -0
  6. fastlsq-0.2.6/examples/digital_twins/pendulum_benchmark.py +281 -0
  7. fastlsq-0.2.6/examples/digital_twins/plasma_wakefield.py +339 -0
  8. fastlsq-0.2.6/examples/digital_twins/plasma_wakefield_2D_1.py +863 -0
  9. fastlsq-0.2.6/examples/digital_twins/plasma_wakefield_2D_2.py +769 -0
  10. fastlsq-0.2.6/examples/digital_twins/plasma_wakefield_2d_3.py +818 -0
  11. fastlsq-0.2.6/examples/digital_twins/plasma_wakefield_parameteric.py +475 -0
  12. fastlsq-0.2.6/examples/digital_twins/plot_utils.py +42 -0
  13. fastlsq-0.2.6/examples/digital_twins/structural_health_simple.py +343 -0
  14. fastlsq-0.2.6/examples/digital_twins/turbulence_gravity_cooling.py +387 -0
  15. fastlsq-0.2.6/examples/inverse/aero_.py +362 -0
  16. fastlsq-0.2.6/examples/inverse/denoising_parameter_estimation.py +297 -0
  17. fastlsq-0.2.6/examples/inverse/elastic_wave_animation.py +300 -0
  18. fastlsq-0.2.6/examples/inverse/heat_from_video.py +394 -0
  19. fastlsq-0.2.6/examples/inverse/inverse_turbulence.py +811 -0
  20. fastlsq-0.2.6/examples/inverse/shape_ns.py +1426 -0
  21. fastlsq-0.2.6/examples/inverse/subsurface_imaging.py +547 -0
  22. fastlsq-0.2.6/examples/inverse/wing_optimize_simple.py +283 -0
  23. fastlsq-0.2.6/examples/sindy/compare_sindy_methods.py +760 -0
  24. fastlsq-0.2.6/examples/sindy/sindy_benchmarks.py +351 -0
  25. fastlsq-0.2.6/examples/sindy/sindy_differentiable.py +418 -0
  26. fastlsq-0.2.6/examples/sindy/sindy_minimal_diff.py +716 -0
  27. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/linalg.py +7 -2
  28. fastlsq-0.2.6/misc/fastlsq_teaser.png +0 -0
  29. fastlsq-0.2.6/misc/ideal_quadrupole.png +0 -0
  30. fastlsq-0.2.6/misc/inverse_heat_source.gif +0 -0
  31. fastlsq-0.2.6/misc/inverse_heat_source.png +0 -0
  32. fastlsq-0.2.6/misc/inverse_magnetostatics.png +0 -0
  33. fastlsq-0.2.6/misc/inverse_magnetostatics_convergence.png +0 -0
  34. fastlsq-0.2.6/misc/quadrupole_convergence.png +0 -0
  35. fastlsq-0.2.6/misc/quadrupole_optimization.png +0 -0
  36. fastlsq-0.2.6/misc/tutorial_nlpoisson_convergence.png +0 -0
  37. fastlsq-0.2.6/misc/tutorial_nlpoisson_solution.png +0 -0
  38. {fastlsq-0.2.5 → fastlsq-0.2.6}/pyproject.toml +1 -1
  39. {fastlsq-0.2.5 → fastlsq-0.2.6}/CHANGELOG.md +0 -0
  40. {fastlsq-0.2.5 → fastlsq-0.2.6}/FastLSQ.egg-info/dependency_links.txt +0 -0
  41. {fastlsq-0.2.5 → fastlsq-0.2.6}/FastLSQ.egg-info/requires.txt +0 -0
  42. {fastlsq-0.2.5 → fastlsq-0.2.6}/FastLSQ.egg-info/top_level.txt +0 -0
  43. {fastlsq-0.2.5 → fastlsq-0.2.6}/LICENSE +0 -0
  44. {fastlsq-0.2.5 → fastlsq-0.2.6}/MANIFEST.in +0 -0
  45. {fastlsq-0.2.5 → fastlsq-0.2.6}/README.md +0 -0
  46. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/add_your_own_pde.py +0 -0
  47. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/benchmark_comparison.py +0 -0
  48. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/custom_features.py +0 -0
  49. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/fred_sde.py +0 -0
  50. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/fred_sde_fastlsq.py +0 -0
  51. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/gaia_potential.py +0 -0
  52. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/gaia_potential_fastlsq.py +0 -0
  53. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/horizons_ephemeris.py +0 -0
  54. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/numerai_alpha.py +0 -0
  55. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/numerai_alpha_fastlsq.py +0 -0
  56. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/run_all_fastlsq.py +0 -0
  57. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/__init__.py +0 -0
  58. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/_alsu_lattice.py +0 -0
  59. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/_common.py +0 -0
  60. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/run_all.py +0 -0
  61. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
  62. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
  63. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_green_fff.py +0 -0
  64. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
  65. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
  66. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
  67. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_passive_loco.py +0 -0
  68. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
  69. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
  70. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
  71. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
  72. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_tides_3months.py +0 -0
  73. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
  74. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s01_visualize.py +0 -0
  75. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
  76. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
  77. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s04_sunspots.py +0 -0
  78. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s05_helioseismology.py +0 -0
  79. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s06_tides.py +0 -0
  80. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
  81. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
  82. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
  83. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
  84. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
  85. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
  86. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
  87. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s14_eeg.py +0 -0
  88. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/scenarios/s15_circadian.py +0 -0
  89. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/extras/spectral_expansion.py +0 -0
  90. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/grad_shafranov.py +0 -0
  91. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/grid_inverse.py +0 -0
  92. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/grid_rl_control.py +0 -0
  93. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/grid_swing.py +0 -0
  94. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/gs_inverse.py +0 -0
  95. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/gs_rl_control.py +0 -0
  96. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/inverse_heat_source.py +0 -0
  97. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/inverse_magnetostatics.py +0 -0
  98. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/inverse_source_position.py +0 -0
  99. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/learnable_helmholtz.py +0 -0
  100. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/orbit_hill.py +0 -0
  101. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/orbit_inverse.py +0 -0
  102. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/orbit_rl.py +0 -0
  103. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/pde_discovery.py +0 -0
  104. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/run_all_extensions.py +0 -0
  105. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/run_linear.py +0 -0
  106. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/run_nonlinear.py +0 -0
  107. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/tutorial_basic.py +0 -0
  108. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/tutorial_nonlinear.py +0 -0
  109. {fastlsq-0.2.5 → fastlsq-0.2.6}/examples/vector_basis_stream_vorticity.py +0 -0
  110. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/__init__.py +0 -0
  111. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/api.py +0 -0
  112. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/basis.py +0 -0
  113. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/block.py +0 -0
  114. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/device.py +0 -0
  115. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/diagnostics.py +0 -0
  116. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/export.py +0 -0
  117. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/geometry.py +0 -0
  118. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/learnable.py +0 -0
  119. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/lightning.py +0 -0
  120. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/newton.py +0 -0
  121. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/plotting.py +0 -0
  122. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/problems/__init__.py +0 -0
  123. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/problems/linear.py +0 -0
  124. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/problems/nonlinear.py +0 -0
  125. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/problems/regression.py +0 -0
  126. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/solvers.py +0 -0
  127. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/tuning.py +0 -0
  128. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/utils.py +0 -0
  129. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/vector.py +0 -0
  130. {fastlsq-0.2.5 → fastlsq-0.2.6}/fastlsq/viz.py +0 -0
  131. {fastlsq-0.2.5 → fastlsq-0.2.6}/requirements.txt +0 -0
  132. {fastlsq-0.2.5 → fastlsq-0.2.6}/setup.cfg +0 -0
  133. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_basic.py +0 -0
  134. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_benchmarks_inverse.py +0 -0
  135. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_block.py +0 -0
  136. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_derivatives.py +0 -0
  137. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_device.py +0 -0
  138. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_grad_shafranov.py +0 -0
  139. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_grid_swing.py +0 -0
  140. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_learnable.py +0 -0
  141. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_orbit_hill.py +0 -0
  142. {fastlsq-0.2.5 → fastlsq-0.2.6}/tests/test_vector_basis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: One-shot PDE solving via Fourier features with exact analytical derivatives; rank-revealing solvers, learnable anisotropic bandwidth, and CPU/CUDA/MPS support
5
5
  Author: Antonin Sulc
6
6
  License-Expression: MIT
@@ -32,6 +32,17 @@ examples/run_nonlinear.py
32
32
  examples/tutorial_basic.py
33
33
  examples/tutorial_nonlinear.py
34
34
  examples/vector_basis_stream_vorticity.py
35
+ examples/digital_twins/darcy_heat.py
36
+ examples/digital_twins/pendulum.py
37
+ examples/digital_twins/pendulum_benchmark.py
38
+ examples/digital_twins/plasma_wakefield.py
39
+ examples/digital_twins/plasma_wakefield_2D_1.py
40
+ examples/digital_twins/plasma_wakefield_2D_2.py
41
+ examples/digital_twins/plasma_wakefield_2d_3.py
42
+ examples/digital_twins/plasma_wakefield_parameteric.py
43
+ examples/digital_twins/plot_utils.py
44
+ examples/digital_twins/structural_health_simple.py
45
+ examples/digital_twins/turbulence_gravity_cooling.py
35
46
  examples/extras/fred_sde.py
36
47
  examples/extras/fred_sde_fastlsq.py
37
48
  examples/extras/gaia_potential.py
@@ -73,6 +84,18 @@ examples/extras/scenarios/s12_mems_resonator.py
73
84
  examples/extras/scenarios/s13_variable_stars_kepler.py
74
85
  examples/extras/scenarios/s14_eeg.py
75
86
  examples/extras/scenarios/s15_circadian.py
87
+ examples/inverse/aero_.py
88
+ examples/inverse/denoising_parameter_estimation.py
89
+ examples/inverse/elastic_wave_animation.py
90
+ examples/inverse/heat_from_video.py
91
+ examples/inverse/inverse_turbulence.py
92
+ examples/inverse/shape_ns.py
93
+ examples/inverse/subsurface_imaging.py
94
+ examples/inverse/wing_optimize_simple.py
95
+ examples/sindy/compare_sindy_methods.py
96
+ examples/sindy/sindy_benchmarks.py
97
+ examples/sindy/sindy_differentiable.py
98
+ examples/sindy/sindy_minimal_diff.py
76
99
  fastlsq/__init__.py
77
100
  fastlsq/api.py
78
101
  fastlsq/basis.py
@@ -95,6 +118,16 @@ fastlsq/problems/__init__.py
95
118
  fastlsq/problems/linear.py
96
119
  fastlsq/problems/nonlinear.py
97
120
  fastlsq/problems/regression.py
121
+ misc/fastlsq_teaser.png
122
+ misc/ideal_quadrupole.png
123
+ misc/inverse_heat_source.gif
124
+ misc/inverse_heat_source.png
125
+ misc/inverse_magnetostatics.png
126
+ misc/inverse_magnetostatics_convergence.png
127
+ misc/quadrupole_convergence.png
128
+ misc/quadrupole_optimization.png
129
+ misc/tutorial_nlpoisson_convergence.png
130
+ misc/tutorial_nlpoisson_solution.png
98
131
  tests/test_basic.py
99
132
  tests/test_benchmarks_inverse.py
100
133
  tests/test_block.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: One-shot PDE solving via Fourier features with exact analytical derivatives; rank-revealing solvers, learnable anisotropic bandwidth, and CPU/CUDA/MPS support
5
5
  Author: Antonin Sulc
6
6
  License-Expression: MIT
@@ -0,0 +1,583 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Antonin Sulc
3
+ # Licensed under the MIT License. See LICENSE file for details.
4
+
5
+ """
6
+ Digital Twin: Darcy Flow + Heat (Porous Media).
7
+
8
+ Coupled PDEs:
9
+ 1. Darcy: ∇·(K∇p) = 0 → pressure p, velocity u = −K∇p
10
+ 2. Heat: −k∇²T + u·∇T = Q → advection by Darcy flow
11
+
12
+ Heat as tracer: Temperature observations reveal flow structure. Infer permeability
13
+ K(x) from T—where flow is blocked (clog) or channeled, heat distribution changes.
14
+
15
+ Domain [0,1]²: fluid flows left→right. Multiple heat sources; flow mixes/transports.
16
+ Spatially varying K: low-K "clog" or high-K "channel" changes flow paths.
17
+
18
+ USAGE
19
+ -----
20
+ python darcy_heat.py
21
+ python darcy_heat.py --quick
22
+ python darcy_heat.py --inverse # infer constant K from T
23
+ python darcy_heat.py --clog # spatially varying K (clog)
24
+ python darcy_heat.py --clog --inverse # infer clog params from T
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import os
31
+ import sys
32
+
33
+ import numpy as np
34
+ import torch
35
+ from scipy.optimize import minimize_scalar, minimize
36
+
37
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
38
+ from fastlsq.solvers import FastLSQSolver
39
+ from fastlsq.linalg import solve_lstsq
40
+ from fastlsq.geometry import sample_box
41
+ from fastlsq.utils import device, setup
42
+
43
+ try:
44
+ import matplotlib
45
+ matplotlib.use("Agg")
46
+ import matplotlib.pyplot as plt
47
+ from plot_utils import paper_style, panel_label, COLORS
48
+ HAS_MPL = True
49
+ except ImportError:
50
+ HAS_MPL = False
51
+
52
+
53
+ # =============================================================================
54
+ # Configuration
55
+ # =============================================================================
56
+
57
+ K_DARCY = 1.0 # permeability (constant or base value)
58
+ K_THERMAL = 1.0 # thermal diffusivity
59
+ P_IN = 1.0 # pressure at inlet (x=0)
60
+ P_OUT = 0.0 # pressure at outlet (x=1)
61
+ SOURCE_SIGMA = 0.08
62
+ # Multiple sources: (cx, cy, strength) — flow mixes and transports heat from each
63
+ SOURCES = [(0.25, 0.4, 2.5), (0.5, 0.7, 2.0), (0.75, 0.35, 1.5)]
64
+ # Clog: K(x) = K0 * (1 - A*exp(-r²/(2σ²))), r²=(x-cx)²+(y-cy)²
65
+ CLOG_CX, CLOG_CY = 0.5, 0.5
66
+ CLOG_A = 0.75 # strength of clog (K drops to 0.25*K0 at center)
67
+ CLOG_SIGMA = 0.15 # extent of clog
68
+ N_FEAT = 400
69
+ SIGMA_BASIS = 6.0
70
+ M_PDE_DARCY = 2000
71
+ M_BC_DARCY = 400
72
+ M_PDE_HEAT = 2000
73
+ M_BC_HEAT = 400
74
+
75
+
76
+ # =============================================================================
77
+ # Domain sampling
78
+ # =============================================================================
79
+
80
+ def sample_domain(n_int, n_in, n_out, n_top, n_bot):
81
+ """Sample [0,1]²: interior, inlet (x=0), outlet (x=1), top (y=1), bottom (y=0)."""
82
+ x_int = sample_box(n_int, 2)
83
+ x_in = torch.rand(n_in, 2, device=device)
84
+ x_in[:, 0] = 0.0
85
+ x_out = torch.rand(n_out, 2, device=device)
86
+ x_out[:, 0] = 1.0
87
+ x_top = torch.rand(n_top, 2, device=device)
88
+ x_top[:, 1] = 1.0
89
+ x_bot = torch.rand(n_bot, 2, device=device)
90
+ x_bot[:, 1] = 0.0
91
+ return {"x_int": x_int, "x_in": x_in, "x_out": x_out, "x_top": x_top, "x_bot": x_bot}
92
+
93
+
94
+ # =============================================================================
95
+ # Permeability: constant or spatially varying (clog)
96
+ # =============================================================================
97
+
98
+ def permeability(x, K0=K_DARCY, clog_params=None):
99
+ """K(x) = K0 for constant; K0*(1 - A*exp(-r²/(2σ²))) for clog."""
100
+ if clog_params is None:
101
+ return K0 * torch.ones(x.shape[0], 1, device=x.device, dtype=x.dtype)
102
+ cx, cy, A, sig = clog_params
103
+ xv, yv = x[:, 0:1], x[:, 1:2]
104
+ r2 = (xv - cx) ** 2 + (yv - cy) ** 2
105
+ return K0 * (1.0 - A * torch.exp(-r2 / (2 * sig ** 2)))
106
+
107
+
108
+ def permeability_grad(x, K0=K_DARCY, clog_params=None):
109
+ """∇K at x. Returns (dK/dx, dK/dy) each shape (n,1)."""
110
+ if clog_params is None:
111
+ return torch.zeros(x.shape[0], 1, device=x.device, dtype=x.dtype), \
112
+ torch.zeros(x.shape[0], 1, device=x.device, dtype=x.dtype)
113
+ cx, cy, A, sig = clog_params
114
+ xv, yv = x[:, 0:1], x[:, 1:2]
115
+ r2 = (xv - cx) ** 2 + (yv - cy) ** 2
116
+ g = K0 * A * torch.exp(-r2 / (2 * sig ** 2)) / (sig ** 2)
117
+ dK_dx = -g * (xv - cx)
118
+ dK_dy = -g * (yv - cy)
119
+ return dK_dx, dK_dy
120
+
121
+
122
+ # =============================================================================
123
+ # Darcy flow: ∇·(K∇p) = 0, u = −K∇p
124
+ # =============================================================================
125
+
126
+ def solve_darcy(solver_p, K, pts, clog_params=None):
127
+ """
128
+ Solve ∇·(K∇p) = 0. For constant K: −K∇²p = 0.
129
+ For variable K: K∇²p + ∇K·∇p = 0.
130
+ BC: p = P_IN at inlet, p = P_OUT at outlet, ∂p/∂y = 0 on top/bottom.
131
+ """
132
+ basis = solver_p.basis
133
+ x_int = pts["x_int"]
134
+ cache = basis.cache(x_int)
135
+ lap = basis.laplacian(x_int, cache=cache)
136
+ dH = basis.gradient(x_int, cache=cache)
137
+ dH_dx, dH_dy = dH[:, 0, :], dH[:, 1, :]
138
+
139
+ if clog_params is None:
140
+ A_pde = -K * lap
141
+ else:
142
+ K_val = permeability(x_int, K, clog_params)
143
+ dK_dx, dK_dy = permeability_grad(x_int, K, clog_params)
144
+ # K∇²p + ∇K·∇p = 0 => K*lap + dK_dx*dH_dx + dK_dy*dH_dy
145
+ A_pde = K_val * lap + dK_dx * dH_dx + dK_dy * dH_dy
146
+ b_pde = torch.zeros(len(x_int), 1, device=device, dtype=x_int.dtype)
147
+
148
+ # BC: inlet p = P_IN
149
+ H_in = basis.evaluate(pts["x_in"])
150
+ A_in = H_in * 150.0
151
+ b_in = torch.full((len(pts["x_in"]), 1), P_IN * 150.0, device=device, dtype=pts["x_int"].dtype)
152
+
153
+ # BC: outlet p = P_OUT
154
+ H_out = basis.evaluate(pts["x_out"])
155
+ A_out = H_out * 150.0
156
+ b_out = torch.full((len(pts["x_out"]), 1), P_OUT * 150.0, device=device, dtype=pts["x_int"].dtype)
157
+
158
+ # BC: top/bottom ∂p/∂y = 0 (no flow through walls)
159
+ dH_top = basis.gradient(pts["x_top"], cache=basis.cache(pts["x_top"]))[:, 1, :]
160
+ dH_bot = basis.gradient(pts["x_bot"], cache=basis.cache(pts["x_bot"]))[:, 1, :]
161
+ A_top = dH_top * 100.0
162
+ A_bot = dH_bot * 100.0
163
+ b_top = torch.zeros(len(pts["x_top"]), 1, device=device, dtype=pts["x_int"].dtype)
164
+ b_bot = torch.zeros(len(pts["x_bot"]), 1, device=device, dtype=pts["x_int"].dtype)
165
+
166
+ A = torch.cat([A_pde, A_in, A_out, A_top, A_bot])
167
+ b = torch.cat([b_pde, b_in, b_out, b_top, b_bot])
168
+ solver_p.beta = solve_lstsq(A, b, mu=1e-10)
169
+ return solver_p.beta
170
+
171
+
172
+ def get_velocity_at(solver_p, K, pts, clog_params=None):
173
+ """Darcy velocity: u = −K∇p (K constant or K(x))."""
174
+ dH = solver_p.basis.gradient(pts, cache=solver_p.basis.cache(pts))
175
+ grad_p_x = dH[:, 0, :] @ solver_p.beta
176
+ grad_p_y = dH[:, 1, :] @ solver_p.beta
177
+ K_val = permeability(pts, K, clog_params)
178
+ u = (-K_val * grad_p_x).squeeze(-1)
179
+ v = (-K_val * grad_p_y).squeeze(-1)
180
+ return u, v
181
+
182
+
183
+ # =============================================================================
184
+ # Heat equation: −k∇²T + u·∇T = Q
185
+ # =============================================================================
186
+
187
+ def heat_source_multi(x, sources=SOURCES, sigma=SOURCE_SIGMA):
188
+ """Sum of Gaussian sources. sources = [(cx, cy, strength), ...]."""
189
+ xv, yv = x[:, 0:1], x[:, 1:2]
190
+ f = torch.zeros_like(xv)
191
+ for cx, cy, strength in sources:
192
+ f = f + strength * torch.exp(-((xv - cx) ** 2 + (yv - cy) ** 2) / (2 * sigma ** 2))
193
+ return f
194
+
195
+
196
+ def solve_heat_advection(solver_T, solver_p, K, x_pde, x_bc_dir, x_bc_outflow,
197
+ sources=SOURCES, clog_params=None):
198
+ """
199
+ Solve −k∇²T + u·∇T = Q.
200
+ u from Darcy. Dirichlet T=0 on 3 edges, outflow (zero gradient) at outlet.
201
+ """
202
+ x_all = torch.cat([x_pde, x_bc_dir, x_bc_outflow])
203
+ u_all, v_all = get_velocity_at(solver_p, K, x_all, clog_params)
204
+ n_pde = len(x_pde)
205
+ u_pde = u_all[:n_pde]
206
+ v_pde = v_all[:n_pde]
207
+
208
+ basis = solver_T.basis
209
+ cache = basis.cache(x_pde)
210
+ lap = basis.laplacian(x_pde, cache=cache)
211
+ dH = basis.gradient(x_pde, cache=cache)
212
+ dH_dx = dH[:, 0, :]
213
+ dH_dy = dH[:, 1, :]
214
+
215
+ # PDE: −k∇²T + u·∇T = Q
216
+ # u·∇T = u_x * dT/dx + u_y * dT/dy
217
+ adv = u_pde.unsqueeze(1) * dH_dx + v_pde.unsqueeze(1) * dH_dy
218
+ A_pde = -K_THERMAL * lap + adv
219
+ b_pde = heat_source_multi(x_pde, sources)
220
+
221
+ As, bs = [A_pde], [b_pde]
222
+
223
+ # Dirichlet T=0 on left, top, bottom
224
+ H_dir = basis.evaluate(x_bc_dir)
225
+ As.append(H_dir * 100.0)
226
+ bs.append(torch.zeros(len(x_bc_dir), 1, device=device, dtype=x_pde.dtype))
227
+
228
+ # Outflow at outlet: convective BC or zero gradient
229
+ # Use ∂T/∂x + (u/k)*T = 0 for advection-dominated outflow (simplified)
230
+ # Simpler: zero gradient ∂T/∂x = 0
231
+ dH_out = basis.gradient(x_bc_outflow, cache=basis.cache(x_bc_outflow))[:, 0, :]
232
+ As.append(dH_out * 100.0)
233
+ bs.append(torch.zeros(len(x_bc_outflow), 1, device=device, dtype=x_pde.dtype))
234
+
235
+ A = torch.cat(As)
236
+ b = torch.cat(bs)
237
+ solver_T.beta = solve_lstsq(A, b, mu=1e-10)
238
+ return solver_T.beta
239
+
240
+
241
+ # =============================================================================
242
+ # Inverse: infer permeability from temperature (heat as tracer)
243
+ # =============================================================================
244
+
245
+ def run_inverse_constant_K(solver_p, solver_T, pts_darcy, x_pde, x_bc_dir, x_bc_outflow,
246
+ K_true, sources=SOURCES, n_obs=80, noise=0.01):
247
+ """Infer constant K from temperature observations (heat as tracer)."""
248
+ solve_darcy(solver_p, K_true, pts_darcy, None)
249
+ solve_heat_advection(solver_T, solver_p, K_true, x_pde, x_bc_dir, x_bc_outflow, sources, None)
250
+
251
+ x_obs = sample_box(n_obs, 2)
252
+ T_obs = solver_T.predict(x_obs).detach().cpu().numpy()
253
+ T_obs = T_obs + noise * np.std(T_obs) * np.random.randn(*T_obs.shape)
254
+ x_obs_np = x_obs.detach().cpu().numpy()
255
+
256
+ def loss(K_val):
257
+ K = float(np.clip(K_val, 0.2, 5.0))
258
+ solve_darcy(solver_p, K, pts_darcy, None)
259
+ solve_heat_advection(solver_T, solver_p, K, x_pde, x_bc_dir, x_bc_outflow, sources, None)
260
+ T_pred = solver_T.predict(torch.tensor(x_obs_np, device=device, dtype=torch.float64)).detach().cpu().numpy()
261
+ return float(np.mean((T_pred - T_obs) ** 2))
262
+
263
+ res = minimize_scalar(loss, bounds=(0.2, 5.0), method="bounded", options={"maxiter": 100})
264
+ K_inferred = float(np.clip(res.x, 0.2, 5.0))
265
+ return K_true, K_inferred, x_obs_np, T_obs
266
+
267
+
268
+ def sample_obs_with_interior_density(n_uniform, n_interior, box=(0.25, 0.75)):
269
+ """Uniform samples + dense sampling in interior [box, box]² where clog effects are strongest."""
270
+ x_uni = sample_box(n_uniform, 2)
271
+ lo, hi = box
272
+ x_int = torch.rand(n_interior, 2, device=device, dtype=torch.float64)
273
+ x_int[:, 0] = lo + (hi - lo) * x_int[:, 0]
274
+ x_int[:, 1] = lo + (hi - lo) * x_int[:, 1]
275
+ return torch.cat([x_uni, x_int], dim=0)
276
+
277
+
278
+ def run_inverse_clog(solver_p, solver_T, pts_darcy, x_pde, x_bc_dir, x_bc_outflow,
279
+ clog_true, sources=SOURCES, n_obs=200, n_near=80, noise=0.01,
280
+ n_restarts=5):
281
+ """Infer clog params (cx, cy, A, σ) from T. Heat reveals where flow is blocked."""
282
+ cx_t, cy_t, A_t, sig_t = clog_true
283
+ solve_darcy(solver_p, K_DARCY, pts_darcy, clog_true)
284
+ solve_heat_advection(solver_T, solver_p, K_DARCY, x_pde, x_bc_dir, x_bc_outflow, sources, clog_true)
285
+
286
+ # More observations: uniform + dense in interior [0.25, 0.75]² where clog perturbs flow
287
+ x_obs = sample_obs_with_interior_density(n_obs - n_near, n_near, box=(0.25, 0.75))
288
+ T_obs = solver_T.predict(x_obs).detach().cpu().numpy()
289
+ T_obs = T_obs + noise * np.std(T_obs) * np.random.randn(*T_obs.shape)
290
+ x_obs_np = x_obs.detach().cpu().numpy()
291
+
292
+ def loss(params):
293
+ cx, cy, A, sig = np.clip(params[0], 0.2, 0.8), np.clip(params[1], 0.2, 0.8), \
294
+ np.clip(params[2], 0.3, 0.95), np.clip(params[3], 0.06, 0.25)
295
+ cp = (float(cx), float(cy), float(A), float(sig))
296
+ solve_darcy(solver_p, K_DARCY, pts_darcy, cp)
297
+ solve_heat_advection(solver_T, solver_p, K_DARCY, x_pde, x_bc_dir, x_bc_outflow, sources, cp)
298
+ T_pred = solver_T.predict(torch.tensor(x_obs_np, device=device, dtype=torch.float64)).detach().cpu().numpy()
299
+ return float(np.mean((T_pred - T_obs) ** 2))
300
+
301
+ bounds = [(0.2, 0.8), (0.2, 0.8), (0.3, 0.95), (0.06, 0.25)]
302
+ starts = [
303
+ [0.5, 0.5, 0.7, 0.14],
304
+ [0.4, 0.5, 0.6, 0.12],
305
+ [0.6, 0.5, 0.6, 0.12],
306
+ [0.5, 0.4, 0.5, 0.15],
307
+ [0.5, 0.6, 0.8, 0.10],
308
+ [0.5, 0.5, 0.8, 0.12],
309
+ [0.5, 0.5, 0.6, 0.18],
310
+ [0.3, 0.3, 0.8, 0.15],
311
+ [0.7, 0.7, 0.5, 0.10],
312
+ [0.4, 0.6, 0.7, 0.20],
313
+ [0.6, 0.4, 0.9, 0.08],
314
+ [0.5, 0.5, 0.5, 0.10],
315
+ ][:n_restarts]
316
+ best_res, best_loss = None, float("inf")
317
+ for x0 in starts:
318
+ res = minimize(loss, x0, method="L-BFGS-B", bounds=bounds, options={"maxiter": 120})
319
+ L = loss(res.x)
320
+ if L < best_loss:
321
+ best_loss = L
322
+ best_res = res
323
+ res = best_res
324
+ cx_i = float(np.clip(res.x[0], 0.2, 0.8))
325
+ cy_i = float(np.clip(res.x[1], 0.2, 0.8))
326
+ A_i = float(np.clip(res.x[2], 0.3, 0.95))
327
+ sig_i = float(np.clip(res.x[3], 0.06, 0.25))
328
+ clog_inferred = (cx_i, cy_i, A_i, sig_i)
329
+ return clog_true, clog_inferred, x_obs_np, T_obs
330
+
331
+
332
+ # =============================================================================
333
+ # Main
334
+ # =============================================================================
335
+
336
+ def run_darcy_heat(quick=False, inverse=False, clog=False, seed=42):
337
+ setup(dtype=torch.float64)
338
+ np.random.seed(seed)
339
+ torch.manual_seed(seed)
340
+
341
+ m_darcy = M_PDE_DARCY // 2 if quick else M_PDE_DARCY
342
+ m_bc_d = M_BC_DARCY // 2 if quick else M_BC_DARCY
343
+ m_heat = M_PDE_HEAT // 2 if quick else M_PDE_HEAT
344
+ m_bc_h = M_BC_HEAT // 2 if quick else M_BC_HEAT
345
+
346
+ pts_darcy = sample_domain(m_darcy, m_bc_d, m_bc_d, m_bc_d, m_bc_d)
347
+
348
+ solver_p = FastLSQSolver(2, normalize=False)
349
+ solver_p.add_block(hidden_size=N_FEAT, scale=SIGMA_BASIS)
350
+
351
+ x_pde = sample_box(m_heat, 2)
352
+ x_left = torch.rand(m_bc_h // 3, 2, device=device)
353
+ x_left[:, 0] = 0.0
354
+ x_right = torch.rand(m_bc_h // 3, 2, device=device)
355
+ x_right[:, 0] = 1.0
356
+ x_bot_h = torch.rand(m_bc_h // 3, 2, device=device)
357
+ x_bot_h[:, 1] = 0.0
358
+ x_top_h = torch.rand(m_bc_h // 3, 2, device=device)
359
+ x_top_h[:, 1] = 1.0
360
+ x_bc_dir = torch.cat([x_left, x_bot_h, x_top_h])
361
+ x_bc_outflow = torch.rand(m_bc_h // 3, 2, device=device)
362
+ x_bc_outflow[:, 0] = 1.0
363
+
364
+ solver_T = FastLSQSolver(2, normalize=False)
365
+ solver_T.add_block(hidden_size=N_FEAT, scale=SIGMA_BASIS)
366
+
367
+ K = K_DARCY
368
+ clog_params = None
369
+ clog_true = None
370
+ clog_inferred = None
371
+
372
+ if clog:
373
+ clog_params = (CLOG_CX, CLOG_CY, CLOG_A, CLOG_SIGMA)
374
+
375
+ solve_darcy(solver_p, K, pts_darcy, clog_params)
376
+ solve_heat_advection(solver_T, solver_p, K, x_pde, x_bc_dir, x_bc_outflow, SOURCES, clog_params)
377
+
378
+ print("Darcy + Heat (Porous Media) - Digital Twin")
379
+ print("=" * 60)
380
+ print("Heat as tracer: T reveals flow structure → infer K(x)")
381
+ print("1. Darcy: ∇·(K∇p) = 0, u = −K∇p")
382
+ print("2. Heat: −k∇²T + u·∇T = Q (multiple sources)")
383
+ print("3. Coupling: flow distributes heat; T encodes where flow is blocked/channeled")
384
+ print()
385
+
386
+ if inverse:
387
+ if clog:
388
+ clog_true = (CLOG_CX, CLOG_CY, CLOG_A, CLOG_SIGMA)
389
+ n_r = 3 if quick else 12
390
+ clog_true, clog_inferred, x_obs, T_obs = run_inverse_clog(
391
+ solver_p, solver_T, pts_darcy, x_pde, x_bc_dir, x_bc_outflow,
392
+ clog_true, SOURCES, n_obs=300, n_near=120, noise=0.01, n_restarts=n_r
393
+ )
394
+ solve_darcy(solver_p, K, pts_darcy, clog_inferred)
395
+ solve_heat_advection(solver_T, solver_p, K, x_pde, x_bc_dir, x_bc_outflow, SOURCES, clog_inferred)
396
+ print(f"Inverse (clog): cx {clog_true[0]:.2f}→{clog_inferred[0]:.2f}, "
397
+ f"cy {clog_true[1]:.2f}→{clog_inferred[1]:.2f}, "
398
+ f"A {clog_true[2]:.2f}→{clog_inferred[2]:.2f}, σ {clog_true[3]:.2f}→{clog_inferred[3]:.2f}")
399
+ else:
400
+ K_true = 1.5
401
+ K_true, K_inferred, x_obs, T_obs = run_inverse_constant_K(
402
+ solver_p, solver_T, pts_darcy, x_pde, x_bc_dir, x_bc_outflow, K_true,
403
+ SOURCES, n_obs=80, noise=0.01
404
+ )
405
+ solve_darcy(solver_p, K_inferred, pts_darcy, None)
406
+ solve_heat_advection(solver_T, solver_p, K_inferred, x_pde, x_bc_dir, x_bc_outflow, SOURCES, None)
407
+ print(f"Inverse: K_true = {K_true:.3f}, K_inferred = {K_inferred:.3f}")
408
+ else:
409
+ K_inferred = None
410
+
411
+ # Evaluation grid
412
+ xx = np.linspace(0.05, 0.95, 40)
413
+ yy = np.linspace(0.05, 0.95, 40)
414
+ X, Y = np.meshgrid(xx, yy)
415
+ pts = torch.tensor(np.c_[X.ravel(), Y.ravel()], device=device, dtype=torch.float64)
416
+
417
+ eff_K = K_inferred if inverse and not clog else K
418
+ eff_clog = clog_inferred if inverse and clog else clog_params
419
+ u, v = get_velocity_at(solver_p, eff_K, pts, eff_clog)
420
+ U = u.detach().cpu().numpy().reshape(X.shape)
421
+ V = v.detach().cpu().numpy().reshape(X.shape)
422
+ P = solver_p.predict(pts).detach().cpu().numpy().reshape(X.shape)
423
+ T = solver_T.predict(pts).detach().cpu().numpy().reshape(X.shape)
424
+
425
+ print(f"Pressure range: [{P.min():.3f}, {P.max():.3f}]")
426
+ print(f"Velocity |u|_max: {np.sqrt(U**2 + V**2).max():.4f}")
427
+ print(f"Temperature range: [{T.min():.3f}, {T.max():.3f}]")
428
+ print()
429
+
430
+ if HAS_MPL:
431
+ paper_style()
432
+ fig, axes = plt.subplots(2, 2, figsize=(8, 6.5))
433
+
434
+ # (a) Darcy flow (pressure + streamlines)
435
+ ax = axes[0, 0]
436
+ speed = np.sqrt(U ** 2 + V ** 2)
437
+ ax.contourf(X, Y, P, levels=20, cmap="viridis")
438
+ ax.streamplot(X, Y, U, V, color="white", density=1.2, linewidth=0.8)
439
+ plt.colorbar(ax.collections[0], ax=ax, label="p")
440
+ ax.set_xlabel("x")
441
+ ax.set_ylabel("y")
442
+ ax.set_title("Darcy: pressure & flow")
443
+ ax.set_aspect("equal")
444
+ panel_label(ax, "a")
445
+
446
+ # (b) Temperature (heat as tracer: reveals flow paths)
447
+ ax = axes[0, 1]
448
+ im = ax.contourf(X, Y, T, levels=25, cmap="hot")
449
+ for cx, cy, _ in SOURCES:
450
+ ax.scatter([cx], [cy], c="cyan", s=80, marker="*", edgecolors="white")
451
+ plt.colorbar(im, ax=ax, label="T")
452
+ ax.set_xlabel("x")
453
+ ax.set_ylabel("y")
454
+ ax.set_title("Heat as tracer (flow distributes sources)")
455
+ ax.set_aspect("equal")
456
+ panel_label(ax, "b")
457
+
458
+ # (c) Velocity magnitude or K(x) for clog
459
+ ax = axes[1, 0]
460
+ if clog or clog_inferred:
461
+ cp = eff_clog
462
+ K_field = permeability(pts, K_DARCY, cp).detach().cpu().numpy().reshape(X.shape)
463
+ im = ax.contourf(X, Y, K_field, levels=20, cmap="YlOrBr")
464
+ ax.streamplot(X, Y, U, V, color="black", density=1.0, linewidth=0.5)
465
+ plt.colorbar(im, ax=ax, label="K(x)")
466
+ ax.set_title("Permeability K(x) & flow")
467
+ else:
468
+ speed_flat = np.sqrt(U ** 2 + V ** 2)
469
+ im = ax.contourf(X, Y, speed_flat, levels=20, cmap="Blues")
470
+ ax.streamplot(X, Y, U, V, color="black", density=1.0, linewidth=0.5)
471
+ plt.colorbar(im, ax=ax, label="|u|")
472
+ ax.set_title("Darcy velocity")
473
+ ax.set_xlabel("x")
474
+ ax.set_ylabel("y")
475
+ ax.set_aspect("equal")
476
+ panel_label(ax, "c")
477
+
478
+ # (d) Inferred K(x) or velocity magnitude
479
+ ax = axes[1, 1]
480
+ if inverse and clog and clog_inferred is not None:
481
+ K_inf_field = permeability(pts, K_DARCY, clog_inferred)
482
+ K_inf_2d = K_inf_field.detach().cpu().numpy().reshape(X.shape)
483
+ im = ax.contourf(X, Y, K_inf_2d, levels=20, cmap="YlOrBr")
484
+ u_inf, v_inf = get_velocity_at(solver_p, K, pts, clog_inferred)
485
+ U_inf = u_inf.detach().cpu().numpy().reshape(X.shape)
486
+ V_inf = v_inf.detach().cpu().numpy().reshape(X.shape)
487
+ ax.streamplot(X, Y, U_inf, V_inf, color="black", density=1.0, linewidth=0.5)
488
+ plt.colorbar(im, ax=ax, label="$K(x)$")
489
+ ax.set_title("Inferred permeability $K(x)$")
490
+ else:
491
+ speed_flat = np.sqrt(U ** 2 + V ** 2)
492
+ im = ax.contourf(X, Y, speed_flat, levels=20, cmap="Blues")
493
+ ax.streamplot(X, Y, U, V, color="black", density=1.0, linewidth=0.5)
494
+ plt.colorbar(im, ax=ax, label="$|u|$")
495
+ ax.set_title("Darcy velocity $|u|$")
496
+ ax.set_xlabel("x")
497
+ ax.set_ylabel("y")
498
+ ax.set_aspect("equal")
499
+ panel_label(ax, "d")
500
+
501
+ plt.tight_layout()
502
+ out_dir = os.path.join(os.path.dirname(__file__), "output")
503
+ os.makedirs(out_dir, exist_ok=True)
504
+ if inverse:
505
+ fname = "darcy_heat_clog_inverse.pdf" if clog else "darcy_heat_inverse.pdf"
506
+ else:
507
+ fname = "darcy_heat_clog.pdf" if clog else "darcy_heat.pdf"
508
+ plt.savefig(os.path.join(out_dir, fname), dpi=300, bbox_inches="tight")
509
+ plt.close()
510
+ print(f"Saved {out_dir}/{fname}")
511
+
512
+ return {"K": K, "clog": clog_inferred if (inverse and clog) else clog_params,
513
+ "P": P, "T": T, "U": U, "V": V}
514
+
515
+
516
+ def run_paper_benchmark(n_runs=5, base_seed=0, quick=False):
517
+ """Run clog inverse multiple times with different seeds; save JSON results."""
518
+ import json
519
+ all_results = []
520
+ for i in range(n_runs):
521
+ seed = base_seed + i
522
+ print(f"\n--- Run {i+1}/{n_runs} (seed={seed}) ---")
523
+ result = run_darcy_heat(quick=quick, inverse=True, clog=True, seed=seed)
524
+ clog_rec = result["clog"]
525
+ all_results.append({
526
+ "seed": seed,
527
+ "cx": round(clog_rec[0], 4),
528
+ "cy": round(clog_rec[1], 4),
529
+ "A": round(clog_rec[2], 4),
530
+ "sigma": round(clog_rec[3], 4),
531
+ })
532
+
533
+ cxs = [r["cx"] for r in all_results]
534
+ cys = [r["cy"] for r in all_results]
535
+ As = [r["A"] for r in all_results]
536
+ sigs = [r["sigma"] for r in all_results]
537
+ summary = {
538
+ "true": {"cx": CLOG_CX, "cy": CLOG_CY, "A": CLOG_A, "sigma": CLOG_SIGMA},
539
+ "runs": all_results,
540
+ "mean": {
541
+ "cx": round(float(np.mean(cxs)), 4),
542
+ "cy": round(float(np.mean(cys)), 4),
543
+ "A": round(float(np.mean(As)), 4),
544
+ "sigma": round(float(np.mean(sigs)), 4),
545
+ },
546
+ "std": {
547
+ "cx": round(float(np.std(cxs)), 4),
548
+ "cy": round(float(np.std(cys)), 4),
549
+ "A": round(float(np.std(As)), 4),
550
+ "sigma": round(float(np.std(sigs)), 4),
551
+ },
552
+ }
553
+ out_dir = os.path.join(os.path.dirname(__file__), "output")
554
+ os.makedirs(out_dir, exist_ok=True)
555
+ out_path = os.path.join(out_dir, "darcy_heat_benchmark.json")
556
+ with open(out_path, "w") as f:
557
+ json.dump(summary, f, indent=2)
558
+
559
+ print("\n" + "=" * 60)
560
+ print("Darcy--heat clog benchmark results:")
561
+ for param in ["cx", "cy", "A", "sigma"]:
562
+ t = summary["true"][param]
563
+ m = summary["mean"][param]
564
+ s = summary["std"][param]
565
+ print(f" {param}: true={t:.4f}, recovered={m:.4f} ± {s:.4f}")
566
+ print(f"Saved to {out_path}")
567
+ return summary
568
+
569
+
570
+ if __name__ == "__main__":
571
+ parser = argparse.ArgumentParser()
572
+ parser.add_argument("--quick", action="store_true")
573
+ parser.add_argument("--inverse", action="store_true", help="Infer K from T (heat as tracer)")
574
+ parser.add_argument("--clog", action="store_true", help="Spatially varying K (clog); infer clog params")
575
+ parser.add_argument("--seed", type=int, default=42, help="Random seed")
576
+ parser.add_argument("--n-runs", type=int, default=1,
577
+ help="Number of runs for benchmark (implies --clog --inverse)")
578
+ args = parser.parse_args()
579
+
580
+ if args.n_runs > 1:
581
+ run_paper_benchmark(n_runs=args.n_runs, base_seed=args.seed, quick=args.quick)
582
+ else:
583
+ run_darcy_heat(quick=args.quick, inverse=args.inverse, clog=args.clog, seed=args.seed)