integrate_module 0.99.6__tar.gz → 0.99.9__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 (152) hide show
  1. {integrate_module-0.99.6/integrate_module.egg-info → integrate_module-0.99.9}/PKG-INFO +25 -4
  2. {integrate_module-0.99.6 → integrate_module-0.99.9}/README.md +17 -0
  3. integrate_module-0.99.9/frontend/__init__.py +10 -0
  4. integrate_module-0.99.9/frontend/__main__.py +4 -0
  5. integrate_module-0.99.9/frontend/app.py +61 -0
  6. integrate_module-0.99.9/frontend/components.py +256 -0
  7. integrate_module-0.99.9/frontend/config.py +48 -0
  8. integrate_module-0.99.9/frontend/pages/__init__.py +33 -0
  9. integrate_module-0.99.9/frontend/pages/_forms.py +72 -0
  10. integrate_module-0.99.9/frontend/pages/_jobs_ui.py +125 -0
  11. integrate_module-0.99.9/frontend/pages/borehole.py +94 -0
  12. integrate_module-0.99.9/frontend/pages/files.py +189 -0
  13. integrate_module-0.99.9/frontend/pages/forward.py +91 -0
  14. integrate_module-0.99.9/frontend/pages/geoprior.py +556 -0
  15. integrate_module-0.99.9/frontend/pages/inversion.py +139 -0
  16. integrate_module-0.99.9/frontend/pages/plotting.py +142 -0
  17. integrate_module-0.99.9/frontend/pages/prior.py +241 -0
  18. integrate_module-0.99.9/frontend/pages/profile.py +141 -0
  19. integrate_module-0.99.9/frontend/pages/query.py +217 -0
  20. integrate_module-0.99.9/frontend/pages/results.py +142 -0
  21. integrate_module-0.99.9/frontend/pages/stubs.py +38 -0
  22. integrate_module-0.99.9/frontend/pages/workflow.py +255 -0
  23. integrate_module-0.99.9/frontend/pages/workspace_ui.py +35 -0
  24. integrate_module-0.99.9/frontend/services/__init__.py +9 -0
  25. integrate_module-0.99.9/frontend/services/figures.py +173 -0
  26. integrate_module-0.99.9/frontend/services/files.py +196 -0
  27. integrate_module-0.99.9/frontend/services/integrate_api.py +1053 -0
  28. integrate_module-0.99.9/frontend/services/jobs.py +125 -0
  29. integrate_module-0.99.9/frontend/services/session.py +24 -0
  30. integrate_module-0.99.9/frontend/services/worker.py +330 -0
  31. integrate_module-0.99.9/frontend/services/workspace.py +59 -0
  32. integrate_module-0.99.9/frontend/services/xlsx.py +104 -0
  33. integrate_module-0.99.9/frontend/static/app.css +250 -0
  34. integrate_module-0.99.9/frontend/static/figures/006b874a67148744.json +1 -0
  35. integrate_module-0.99.9/frontend/static/figures/01c54329eb934b92.png +0 -0
  36. integrate_module-0.99.9/frontend/static/figures/0454af65d7dea5f1.png +0 -0
  37. integrate_module-0.99.9/frontend/static/figures/061d5cecc1a2f677.png +0 -0
  38. integrate_module-0.99.9/frontend/static/figures/066bb5c9da4d0c0b.png +0 -0
  39. integrate_module-0.99.9/frontend/static/figures/0baffb84416e0aa5.png +0 -0
  40. integrate_module-0.99.9/frontend/static/figures/0bddf00c6308d581.png +0 -0
  41. integrate_module-0.99.9/frontend/static/figures/0d0c87a2b0317423.png +0 -0
  42. integrate_module-0.99.9/frontend/static/figures/0e020d8b85920d7e.png +0 -0
  43. integrate_module-0.99.9/frontend/static/figures/105023459d1a8567.png +0 -0
  44. integrate_module-0.99.9/frontend/static/figures/12846323d657202c.json +1 -0
  45. integrate_module-0.99.9/frontend/static/figures/12846323d657202c.png +0 -0
  46. integrate_module-0.99.9/frontend/static/figures/1a9964bb2736785c.png +0 -0
  47. integrate_module-0.99.9/frontend/static/figures/1acd3021d782ccc2.json +1 -0
  48. integrate_module-0.99.9/frontend/static/figures/1acd3021d782ccc2.png +0 -0
  49. integrate_module-0.99.9/frontend/static/figures/1c84cb9595fbcb9b.png +0 -0
  50. integrate_module-0.99.9/frontend/static/figures/1ec73b329d1e30c1-0.png +0 -0
  51. integrate_module-0.99.9/frontend/static/figures/29dc879298fdfa8a.png +0 -0
  52. integrate_module-0.99.9/frontend/static/figures/2b167d8b53dd646f.png +0 -0
  53. integrate_module-0.99.9/frontend/static/figures/2b764aca24a6fd44.json +1 -0
  54. integrate_module-0.99.9/frontend/static/figures/2d78ec085ac2bbef.png +0 -0
  55. integrate_module-0.99.9/frontend/static/figures/3bf8957c95d8dc58.png +0 -0
  56. integrate_module-0.99.9/frontend/static/figures/3f50930542de8196.png +0 -0
  57. integrate_module-0.99.9/frontend/static/figures/41ef6a32da0ec924.png +0 -0
  58. integrate_module-0.99.9/frontend/static/figures/426adc78c42b1a27.png +0 -0
  59. integrate_module-0.99.9/frontend/static/figures/427dd675638f594d.json +1 -0
  60. integrate_module-0.99.9/frontend/static/figures/427dd675638f594d.png +0 -0
  61. integrate_module-0.99.9/frontend/static/figures/487ff0ef8da5a2ec.png +0 -0
  62. integrate_module-0.99.9/frontend/static/figures/4dc6542b7b0f149f.png +0 -0
  63. integrate_module-0.99.9/frontend/static/figures/4ec72f25a8ec038c.png +0 -0
  64. integrate_module-0.99.9/frontend/static/figures/4feb7f1a8281a0bd.png +0 -0
  65. integrate_module-0.99.9/frontend/static/figures/525d39d8ea618a41.png +0 -0
  66. integrate_module-0.99.9/frontend/static/figures/53da1cb8aa8a6cb1.png +0 -0
  67. integrate_module-0.99.9/frontend/static/figures/55d5162d81b64a36.png +0 -0
  68. integrate_module-0.99.9/frontend/static/figures/588f962e19adecff.png +0 -0
  69. integrate_module-0.99.9/frontend/static/figures/5a5b4b0457603714.png +0 -0
  70. integrate_module-0.99.9/frontend/static/figures/5d84b1ae75e96c16.png +0 -0
  71. integrate_module-0.99.9/frontend/static/figures/5fdc8164998fe564.png +0 -0
  72. integrate_module-0.99.9/frontend/static/figures/64cdac3bedd0cecd.png +0 -0
  73. integrate_module-0.99.9/frontend/static/figures/65cf5a549f2f1b33.png +0 -0
  74. integrate_module-0.99.9/frontend/static/figures/689bb914194a8d64.png +0 -0
  75. integrate_module-0.99.9/frontend/static/figures/68bdea6008a16e42.png +0 -0
  76. integrate_module-0.99.9/frontend/static/figures/6a7883226a8a477d.png +0 -0
  77. integrate_module-0.99.9/frontend/static/figures/6c85602ebffb533e.json +1 -0
  78. integrate_module-0.99.9/frontend/static/figures/6c85602ebffb533e.png +0 -0
  79. integrate_module-0.99.9/frontend/static/figures/75c488b1f85e3369.png +0 -0
  80. integrate_module-0.99.9/frontend/static/figures/78a160053c2cac33.png +0 -0
  81. integrate_module-0.99.9/frontend/static/figures/79d91b91323697cd.png +0 -0
  82. integrate_module-0.99.9/frontend/static/figures/7a64c69b35eaf912.png +0 -0
  83. integrate_module-0.99.9/frontend/static/figures/7bb22ced65184b59.png +0 -0
  84. integrate_module-0.99.9/frontend/static/figures/7e7213e8f8247031.png +0 -0
  85. integrate_module-0.99.9/frontend/static/figures/7fd95f0d2c9595c9.png +0 -0
  86. integrate_module-0.99.9/frontend/static/figures/994f10f602985978.png +0 -0
  87. integrate_module-0.99.9/frontend/static/figures/9a6b1d1f0a7ffd11.png +0 -0
  88. integrate_module-0.99.9/frontend/static/figures/9a8729c07d09bd5a.png +0 -0
  89. integrate_module-0.99.9/frontend/static/figures/9ddc30d24404a155-0.png +0 -0
  90. integrate_module-0.99.9/frontend/static/figures/a6f07ace7e42da09.png +0 -0
  91. integrate_module-0.99.9/frontend/static/figures/ab655c40d7fffc4f.png +0 -0
  92. integrate_module-0.99.9/frontend/static/figures/b12e125404b2aea1.png +0 -0
  93. integrate_module-0.99.9/frontend/static/figures/b2f3a4ce9f535f45.png +0 -0
  94. integrate_module-0.99.9/frontend/static/figures/b605415c314c6cef.png +0 -0
  95. integrate_module-0.99.9/frontend/static/figures/b7d8d18a140c0073.png +0 -0
  96. integrate_module-0.99.9/frontend/static/figures/b829df1a78718dd6.png +0 -0
  97. integrate_module-0.99.9/frontend/static/figures/b8dbb12675172fda.png +0 -0
  98. integrate_module-0.99.9/frontend/static/figures/b99e09ec69df4967.png +0 -0
  99. integrate_module-0.99.9/frontend/static/figures/be438f0ac7f59a5c.png +0 -0
  100. integrate_module-0.99.9/frontend/static/figures/c9030d3cf5e7b94c.png +0 -0
  101. integrate_module-0.99.9/frontend/static/figures/d20e6de716ab4454.png +0 -0
  102. integrate_module-0.99.9/frontend/static/figures/d2295c0eaa723af8.png +0 -0
  103. integrate_module-0.99.9/frontend/static/figures/d5925defec62f763.png +0 -0
  104. integrate_module-0.99.9/frontend/static/figures/d7daade6e2618161.png +0 -0
  105. integrate_module-0.99.9/frontend/static/figures/e029eb6cb62868dd.png +0 -0
  106. integrate_module-0.99.9/frontend/static/figures/e4000e24e61bcefc.png +0 -0
  107. integrate_module-0.99.9/frontend/static/figures/e58009ee8354d189.png +0 -0
  108. integrate_module-0.99.9/frontend/static/figures/e5b501e77c7db114.png +0 -0
  109. integrate_module-0.99.9/frontend/static/figures/e8b65132706e5437.png +0 -0
  110. integrate_module-0.99.9/frontend/static/figures/f015d3f025e93a71.png +0 -0
  111. integrate_module-0.99.9/frontend/static/figures/f3cf99660141b008.png +0 -0
  112. integrate_module-0.99.9/frontend/static/figures/f4a3311840de58e2-0.png +0 -0
  113. integrate_module-0.99.9/frontend/static/figures/f5a08d8db05edb9c.png +0 -0
  114. integrate_module-0.99.9/frontend/static/figures/f8907379e981be33-0.png +0 -0
  115. integrate_module-0.99.9/frontend/static/figures/fb3b9efeef379e15.png +0 -0
  116. integrate_module-0.99.9/frontend/static/fonts/archivo-latin-ext.woff2 +0 -0
  117. integrate_module-0.99.9/frontend/static/fonts/archivo-latin.woff2 +0 -0
  118. integrate_module-0.99.9/frontend/static/fonts/archivo-vietnamese.woff2 +0 -0
  119. integrate_module-0.99.9/frontend/static/htmx.min.js +1 -0
  120. integrate_module-0.99.9/frontend/static/modernist.css +253 -0
  121. integrate_module-0.99.9/frontend/static/picker.js +90 -0
  122. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/__init__.py +168 -147
  123. integrate_module-0.99.9/integrate/anemone_forward.py +728 -0
  124. integrate_module-0.99.9/integrate/em_system.py +295 -0
  125. integrate_module-0.99.9/integrate/gaaem_forward.py +697 -0
  126. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate.py +542 -628
  127. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_io.py +35 -11
  128. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_plot.py +843 -290
  129. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_query.py +337 -4
  130. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection.py +75 -16
  131. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection_cli.py +9 -3
  132. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection_jax.py +55 -6
  133. integrate_module-0.99.9/integrate/simpeg_forward.py +557 -0
  134. {integrate_module-0.99.6 → integrate_module-0.99.9/integrate_module.egg-info}/PKG-INFO +25 -4
  135. integrate_module-0.99.9/integrate_module.egg-info/SOURCES.txt +147 -0
  136. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/entry_points.txt +1 -1
  137. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/requires.txt +7 -3
  138. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/top_level.txt +1 -0
  139. {integrate_module-0.99.6 → integrate_module-0.99.9}/pyproject.toml +31 -6
  140. integrate_module-0.99.9/tests/test_likelihood_nan.py +46 -0
  141. integrate_module-0.99.9/tests/test_simpeg_forward.py +362 -0
  142. integrate_module-0.99.6/integrate/integrate_www_cli.py +0 -8
  143. integrate_module-0.99.6/integrate_module.egg-info/SOURCES.txt +0 -24
  144. integrate_module-0.99.6/tests/test_likelihood_multinomial.py +0 -260
  145. {integrate_module-0.99.6 → integrate_module-0.99.9}/LICENSE +0 -0
  146. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/gex.py +0 -0
  147. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_borehole.py +0 -0
  148. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_hdf5_info_cli.py +0 -0
  149. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_timing_cli.py +0 -0
  150. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/mlmapping.py +0 -0
  151. {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/dependency_links.txt +0 -0
  152. {integrate_module-0.99.6 → integrate_module-0.99.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: integrate_module
3
- Version: 0.99.6
3
+ Version: 0.99.9
4
4
  Summary: Localized probabilistic data integration
5
5
  Author-email: Thomas Mejer Hansen <tmeha@geo.au.dk>
6
6
  License: MIT
@@ -21,6 +21,7 @@ Requires-Dist: psutil
21
21
  Requires-Dist: tqdm
22
22
  Requires-Dist: requests
23
23
  Requires-Dist: geoprior1d>=0.9
24
+ Requires-Dist: shapely
24
25
  Requires-Dist: ga-aem-forward-win; platform_system == "Windows"
25
26
  Requires-Dist: jupyter>=1.0.0
26
27
  Requires-Dist: jupytext
@@ -28,9 +29,12 @@ Requires-Dist: ipykernel
28
29
  Requires-Dist: pandas
29
30
  Requires-Dist: pyvista
30
31
  Requires-Dist: litellm
31
- Requires-Dist: streamlit
32
32
  Requires-Dist: libaarhusxyz
33
33
  Requires-Dist: jax
34
+ Requires-Dist: uvicorn[standard]
35
+ Requires-Dist: python-fasthtml
36
+ Requires-Dist: openpyxl
37
+ Requires-Dist: httpx
34
38
  Provides-Extra: dev
35
39
  Requires-Dist: pytest; extra == "dev"
36
40
  Requires-Dist: black; extra == "dev"
@@ -43,15 +47,15 @@ Requires-Dist: geopandas; extra == "examples"
43
47
  Requires-Dist: shapely; extra == "examples"
44
48
  Provides-Extra: docs
45
49
  Requires-Dist: sphinx; extra == "docs"
46
- Requires-Dist: nbsphinx; extra == "docs"
47
50
  Requires-Dist: sphinx-gallery; extra == "docs"
48
51
  Requires-Dist: pandoc; extra == "docs"
49
52
  Requires-Dist: myst-parser; extra == "docs"
50
53
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
51
54
  Requires-Dist: furo; extra == "docs"
55
+ Requires-Dist: ghp-import; extra == "docs"
52
56
  Requires-Dist: tomli; python_version < "3.11" and extra == "docs"
53
57
  Provides-Extra: jax-cuda
54
- Requires-Dist: jax[cuda12]; extra == "jax-cuda"
58
+ Requires-Dist: jax[cuda13]; extra == "jax-cuda"
55
59
  Dynamic: license-file
56
60
 
57
61
  # INTEGRATE Python Module
@@ -221,6 +225,23 @@ First install Homebrew, then run:
221
225
  pip install .
222
226
 
223
227
 
228
+ ## Alternative EM forward backends (anemone, SimPEG)
229
+
230
+ GA-AEM is the default TDEM forward modeller. Two pure-Python alternatives can be
231
+ selected with `method=` in `ig.forward_em()` / `ig.prior_data_em()` (or via the
232
+ `EM_FORWARD_METHOD` environment variable):
233
+
234
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='ga-aem') # default
235
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='anemone') # PyTorch, GPU-capable
236
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='simpeg') # SimPEG 1D layered TDEM
237
+
238
+ * **anemone** (`pip install "integrate[anemone]"`): see `ANEMONE_VS_GAAEM_VS_AI.md`.
239
+ * **SimPEG** (`pip install "integrate[simpeg]"`, or `uv pip install -e ../simpeg` from a
240
+ checkout): `ig.forward_simpeg()` / `ig.prior_data_simpeg()`. The `.gex` low-pass filters
241
+ and gate integration are applied by INTEGRATE on top of SimPEG's
242
+ `Simulation1DLayered`; agreement with GA-AEM is ~1 % (LM) / ~0.2 % (HM) on Daugaard tTEM,
243
+ see `SIMPEG_VS_GAAEM.md`.
244
+
224
245
  ## Development
225
246
 
226
247
  The `main` branch is the most stable, with less frequent updates but larger changes.
@@ -165,6 +165,23 @@ First install Homebrew, then run:
165
165
  pip install .
166
166
 
167
167
 
168
+ ## Alternative EM forward backends (anemone, SimPEG)
169
+
170
+ GA-AEM is the default TDEM forward modeller. Two pure-Python alternatives can be
171
+ selected with `method=` in `ig.forward_em()` / `ig.prior_data_em()` (or via the
172
+ `EM_FORWARD_METHOD` environment variable):
173
+
174
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='ga-aem') # default
175
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='anemone') # PyTorch, GPU-capable
176
+ ig.prior_data_em(f_prior_h5, file_gex=gex, method='simpeg') # SimPEG 1D layered TDEM
177
+
178
+ * **anemone** (`pip install "integrate[anemone]"`): see `ANEMONE_VS_GAAEM_VS_AI.md`.
179
+ * **SimPEG** (`pip install "integrate[simpeg]"`, or `uv pip install -e ../simpeg` from a
180
+ checkout): `ig.forward_simpeg()` / `ig.prior_data_simpeg()`. The `.gex` low-pass filters
181
+ and gate integration are applied by INTEGRATE on top of SimPEG's
182
+ `Simulation1DLayered`; agreement with GA-AEM is ~1 % (LM) / ~0.2 % (HM) on Daugaard tTEM,
183
+ see `SIMPEG_VS_GAAEM.md`.
184
+
168
185
  ## Development
169
186
 
170
187
  The `main` branch is the most stable, with less frequent updates but larger changes.
@@ -0,0 +1,10 @@
1
+ """INTEGRATE — a FastHTML front end for the ``integrate`` module.
2
+
3
+ Self-contained: nothing here is imported by the core ``integrate`` package,
4
+ and every heavy computation is delegated to ``integrate`` via
5
+ ``frontend.services.integrate_api``. See ``frontend/FRONTEND.md`` for the
6
+ full plan and TODO checklist.
7
+ """
8
+
9
+ __all__ = ["__version__"]
10
+ __version__ = "0.0.1"
@@ -0,0 +1,4 @@
1
+ from frontend.app import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,61 @@
1
+ """FastHTML application factory + ``integrate`` entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import secrets
6
+
7
+ from fasthtml.common import FastHTML, Link, Meta, Script
8
+ from starlette.staticfiles import StaticFiles
9
+
10
+ from frontend.config import (
11
+ APP_TITLE, DEFAULT_HOST, DEFAULT_PORT, STATIC_DIR, get_workspace, set_workspace,
12
+ )
13
+ from frontend.pages import register_all
14
+
15
+ _HDRS = (
16
+ Meta(charset="utf-8"),
17
+ Meta(name="viewport", content="width=device-width, initial-scale=1"),
18
+ Link(rel="stylesheet", href="/static/modernist.css"),
19
+ Link(rel="stylesheet", href="/static/app.css"),
20
+ Script(src="/static/htmx.min.js"),
21
+ Script(src="/static/picker.js", defer=True),
22
+ )
23
+
24
+
25
+ def create_app() -> FastHTML:
26
+ app = FastHTML(
27
+ hdrs=_HDRS,
28
+ default_hdrs=False,
29
+ pico=False,
30
+ surreal=False,
31
+ htmx=False,
32
+ title=APP_TITLE,
33
+ secret_key=secrets.token_hex(16),
34
+ )
35
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
36
+ register_all(app.route)
37
+ return app
38
+
39
+
40
+ app = create_app()
41
+
42
+
43
+ def main(argv: list[str] | None = None) -> None:
44
+ import argparse
45
+
46
+ import uvicorn
47
+
48
+ p = argparse.ArgumentParser(prog="integrate", description=APP_TITLE)
49
+ p.add_argument("--host", default=DEFAULT_HOST)
50
+ p.add_argument("--port", type=int, default=DEFAULT_PORT)
51
+ p.add_argument("--workspace", default=None, help="Folder with the project .h5 files (default: CWD).")
52
+ args = p.parse_args(argv)
53
+
54
+ set_workspace(args.workspace)
55
+ print(f"{APP_TITLE} — workspace: {get_workspace()}")
56
+ print(f" http://{args.host}:{args.port}")
57
+ uvicorn.run(app, host=args.host, port=args.port)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,256 @@
1
+ """Design-system component helpers.
2
+
3
+ Thin wrappers that emit the classes defined in ``static/modernist.css``
4
+ (and a little chrome from ``static/app.css``; the look mirrors the React UI
5
+ in ``ui/``). Pages compose these; they do
6
+ not hand-write class strings.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ from fasthtml.common import (
14
+ A, B, Button, Div, Form, H2, H4, Img, Input, Label, Main, Nav, Option, P,
15
+ Script, Select, Span, Table, Tbody, Td, Th, Thead, Title, Tr,
16
+ )
17
+
18
+ from frontend.config import APP_TITLE, get_workspace
19
+
20
+ # (slug, number, label, children) — order defines the nav and the URL map.
21
+ # `children` = ((slug, label), …); a parent with children is a section label,
22
+ # not itself a link.
23
+ PAGES: list[tuple[str, str, str, tuple]] = [
24
+ ("files", "01", "Data files", ()),
25
+ ("workflow", "00", "Workflow", (("workflow", "Simple Workflow"),)),
26
+ ("prior", "02", "Prior model",
27
+ (("prior", "Generic"), ("prior-wb", "WB"), ("geoprior", "geoprior1d"))),
28
+ ("forward", "03", "Forward", (("forward", "GA-AEM"), ("forward-bh", "Boreholes"))),
29
+ ("inversion", "04", "Inversion", ()),
30
+ ("results", "05", "Results", ()),
31
+ ("plotting", "06", "Plotting", (("plotting", "Library"), ("profile", "Profile line"))),
32
+ ("query", "—", "Query", ()),
33
+ ]
34
+
35
+ # every reachable leaf slug -> (number, header label)
36
+ _SLUG_META: dict[str, tuple[str, str]] = {}
37
+ for _slug, _num, _label, _children in PAGES:
38
+ if _children:
39
+ for _cslug, _clabel in _children:
40
+ _SLUG_META[_cslug] = (_num, f"{_label} — {_clabel}")
41
+ else:
42
+ _SLUG_META[_slug] = (_num, _label)
43
+
44
+
45
+ def leaf_slugs() -> list[str]:
46
+ out: list[str] = []
47
+ for slug, _num, _label, children in PAGES:
48
+ out.extend(c[0] for c in children) if children else out.append(slug)
49
+ return out
50
+
51
+
52
+ def _href(slug: str) -> str:
53
+ return "/" if slug == "files" else f"/{slug}"
54
+
55
+
56
+ def nav(active: str):
57
+ items = []
58
+ for slug, num, label, children in PAGES:
59
+ if not children:
60
+ items.append(A(Span(num, cls="num"), Span(label), href=_href(slug),
61
+ cls="active" if slug == active else None))
62
+ continue
63
+ active_here = any(active == c[0] for c in children)
64
+ items.append(Div(
65
+ Div(Span(num, cls="num"), Span(label),
66
+ cls="wb-navgroup" + (" open" if active_here else "")),
67
+ *[A(Span(clabel), href=_href(cslug),
68
+ cls="wb-navchild" + (" active" if active == cslug else ""))
69
+ for cslug, clabel in children],
70
+ ))
71
+ return Nav(
72
+ Div(Div("ig", cls="logo-tile"),
73
+ Div(Div("INTEGRATE", cls="logo-name"),
74
+ Div("probabilistic integration", cls="logo-sub")),
75
+ cls="wordmark"),
76
+ *items,
77
+ Div(cls="spacer"),
78
+ Span(str(get_workspace()), cls="ws", title=str(get_workspace())),
79
+ cls="wb-nav",
80
+ )
81
+
82
+
83
+ def _subdirs(path: Path) -> list[Path]:
84
+ try:
85
+ return sorted(
86
+ (d for d in path.iterdir() if d.is_dir() and not d.name.startswith(".")),
87
+ key=lambda d: d.name.lower(),
88
+ )[:16]
89
+ except OSError:
90
+ return []
91
+
92
+
93
+ def _has_h5(path: Path) -> bool:
94
+ try:
95
+ return any(p.suffix.lower() == ".h5" for p in path.iterdir())
96
+ except OSError:
97
+ return False
98
+
99
+
100
+ def workspace_bar(*, editing: bool = False, error: str | None = None):
101
+ """Top strip: current working folder + a Change control.
102
+
103
+ Not editing -> path + "Change…". Editing -> a plain POST form to
104
+ ``/workspace`` (full reload on success) with a text field and quick-pick
105
+ buttons for the parent and sub-folders.
106
+ """
107
+ cur = get_workspace()
108
+
109
+ if not editing:
110
+ return Div(
111
+ Span("WORKING FOLDER", cls="wb-eyebrow"),
112
+ Span(str(cur), cls="path"),
113
+ btn("Change…", kind="ghost",
114
+ hx_get="/workspace/edit", hx_target="#wb-ws-bar", hx_swap="outerHTML"),
115
+ id="wb-ws-bar", cls="wb-wsbar",
116
+ )
117
+
118
+ picks = []
119
+ parent = cur.parent
120
+ if parent != cur:
121
+ picks.append(Button("../ " + parent.name, name="path", value=str(parent),
122
+ type="submit", cls="pick", title=str(parent)))
123
+ for d in _subdirs(cur):
124
+ mark = " ●" if _has_h5(d) else ""
125
+ picks.append(Button(d.name + mark, name="path", value=str(d),
126
+ type="submit", cls="pick", title=str(d)))
127
+
128
+ return Div(
129
+ Form(
130
+ Span("WORKING FOLDER", cls="wb-eyebrow"),
131
+ Input(name="path_text", value=str(cur), cls="input", type="text",
132
+ placeholder="/absolute/path/to/folder"),
133
+ btn("Use folder", kind="primary", type="submit"),
134
+ btn("Cancel", kind="secondary",
135
+ hx_get="/workspace/cancel", hx_target="#wb-ws-bar", hx_swap="outerHTML"),
136
+ (error_box(error) if error else Span()),
137
+ Div(*picks, cls="picks") if picks else Span(),
138
+ method="post", action="/workspace",
139
+ hx_post="/workspace", hx_target="#wb-ws-bar", hx_swap="outerHTML",
140
+ ),
141
+ id="wb-ws-bar", cls="wb-wsbar editing",
142
+ )
143
+
144
+
145
+ def page_header(slug: str):
146
+ num, label = _SLUG_META.get(slug, ("—", slug.title()))
147
+ counter = label if num == "—" else f"{num} — {label}"
148
+ return Div(
149
+ Span("INTEGRATE", cls="mark"),
150
+ Span(counter, cls="counter"),
151
+ cls="wb-head",
152
+ )
153
+
154
+
155
+ def shell(active: str, *body):
156
+ """Full page: (Title, shell div). Return this from a GET route."""
157
+ _slug_num, label = _SLUG_META.get(active, ("—", active.title()))
158
+ return (
159
+ Title(f"{label} · {APP_TITLE}"),
160
+ Div(
161
+ nav(active),
162
+ Main(workspace_bar(), page_header(active), *body, cls="wb-main"),
163
+ cls="wb-shell",
164
+ ),
165
+ )
166
+
167
+
168
+ # --------------------------------------------------------------------------- #
169
+ # atoms
170
+ # --------------------------------------------------------------------------- #
171
+ def btn(label, *, kind: str = "secondary", block: bool = False, **kw):
172
+ cls = f"btn btn-{kind}" + (" btn-block" if block else "")
173
+ return Button(label, cls=cls, type=kw.pop("type", "button"), **kw)
174
+
175
+
176
+ def tag(text: str):
177
+ kind = {
178
+ "DATA": "tag-warn",
179
+ "PRIOR": "tag-info",
180
+ "POSTERIOR": "tag-accent",
181
+ "UNKNOWN": "tag-neutral",
182
+ "UNREADABLE": "tag-danger",
183
+ }.get(str(text).upper(), "tag-neutral")
184
+ return Span(text, cls=f"tag {kind}")
185
+
186
+
187
+ def field(label: str, control):
188
+ return Div(Label(label), control, cls="field")
189
+
190
+
191
+ def text_input(name: str, value: str = "", **kw):
192
+ return Input(name=name, value=value, cls="input", type="text", **kw)
193
+
194
+
195
+ def select(name: str, options, value: str | None = None, **kw):
196
+ opts = [
197
+ Option(o, value=o, selected=(o == value)) if not isinstance(o, tuple)
198
+ else Option(o[1], value=o[0], selected=(o[0] == value))
199
+ for o in options
200
+ ]
201
+ return Select(*opts, name=name, cls="input", **kw)
202
+
203
+
204
+ def seg(name: str, options: list[tuple[str, str]], value: str, **kw):
205
+ """Segmented control. ``options`` = [(value, label), ...]."""
206
+ parts = []
207
+ for val, lbl in options:
208
+ parts.append(
209
+ Label(
210
+ Input(type="radio", name=name, value=val, checked=(val == value)),
211
+ Span(lbl),
212
+ cls="seg-opt",
213
+ )
214
+ )
215
+ return Div(*parts, cls="seg", **kw)
216
+
217
+
218
+ def stat(label: str, value):
219
+ return Div(Div(label, cls="l"), Div(str(value), cls="v"), cls="wb-stat")
220
+
221
+
222
+ def stat_grid(*stats, cols: int | None = None):
223
+ n = cols or len(stats) or 1
224
+ return Div(*stats, cls="wb-stats",
225
+ style=f"grid-template-columns:repeat({n},minmax(0,1fr));")
226
+
227
+
228
+ def eyebrow(text: str):
229
+ return Div(text, cls="wb-eyebrow")
230
+
231
+
232
+ def figure_panel(eyebrow_text: str, img_url: str | None, placeholder: str, action=None, *, tall: bool = False):
233
+ head = Div(
234
+ eyebrow(eyebrow_text),
235
+ action if action is not None else Span(),
236
+ style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;",
237
+ )
238
+ from fasthtml.common import Img
239
+
240
+ # `tall`: show the image at its natural size (scrolls) rather than fitting
241
+ # a fixed 260px box — for multi-panel plots like plot_profile.
242
+ body = Img(src=img_url, alt=eyebrow_text) if img_url else Span(placeholder, cls="wb-empty")
243
+ return Div(head, Div(body, cls="wb-figure-tall" if tall else "wb-figure"), cls="wb-panel")
244
+
245
+
246
+ def error_box(message: str):
247
+ return Div(message, cls="wb-err")
248
+
249
+
250
+ __all__ = [
251
+ "PAGES", "shell", "page_header", "nav", "workspace_bar", "btn", "tag", "field",
252
+ "text_input", "select", "seg", "stat", "stat_grid", "eyebrow", "figure_panel", "error_box",
253
+ # re-export common FT for pages
254
+ "Div", "Span", "P", "H2", "H4", "Form", "Table", "Thead", "Tbody", "Tr",
255
+ "Th", "Td", "A", "Script",
256
+ ]
@@ -0,0 +1,48 @@
1
+ """Paths and runtime configuration for the frontend.
2
+
3
+ The one knob that matters is the *workspace* — the directory holding the
4
+ project's ``.h5`` (and ``.gex`` / ``.xlsx``) files. Everything the UI reads or
5
+ writes is resolved relative to it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ PKG_DIR = Path(__file__).resolve().parent
14
+ STATIC_DIR = PKG_DIR / "static"
15
+ FIGURES_DIR = STATIC_DIR / "figures"
16
+ # Scratch area for throwaway artefacts (geoprior1d live-preview .xlsx / .h5).
17
+ # Never the workspace — previews must not touch the user's real files.
18
+ SCRATCH_DIR = PKG_DIR / "_scratch"
19
+
20
+ APP_TITLE = "INTEGRATE"
21
+ DEFAULT_PORT = 8051
22
+ DEFAULT_HOST = "127.0.0.1"
23
+
24
+
25
+ def resolve_workspace(explicit: str | os.PathLike | None = None) -> Path:
26
+ """Resolve the workspace directory.
27
+
28
+ Priority: explicit argument > ``WB_WORKSPACE`` > ``INTEGRATE_WORKSPACE`` >
29
+ current working directory.
30
+ """
31
+ for cand in (explicit, os.environ.get("WB_WORKSPACE"), os.environ.get("INTEGRATE_WORKSPACE")):
32
+ if cand:
33
+ return Path(cand).expanduser().resolve()
34
+ return Path.cwd().resolve()
35
+
36
+
37
+ # Set once by app.main() / create_app(); read via get_workspace().
38
+ _WORKSPACE: Path | None = None
39
+
40
+
41
+ def set_workspace(path: str | os.PathLike | None) -> Path:
42
+ global _WORKSPACE
43
+ _WORKSPACE = resolve_workspace(path)
44
+ return _WORKSPACE
45
+
46
+
47
+ def get_workspace() -> Path:
48
+ return _WORKSPACE if _WORKSPACE is not None else resolve_workspace(None)
@@ -0,0 +1,33 @@
1
+ """Page modules. Each exposes ``register(rt)`` which adds its routes.
2
+
3
+ Order and slugs are defined by ``frontend.components.PAGES``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from frontend.pages import (
9
+ borehole, files, forward, geoprior, inversion, plotting, prior, profile,
10
+ query, results, stubs, workflow, workspace_ui,
11
+ )
12
+
13
+ # slugs with a real page module (stubs.py covers the rest)
14
+ IMPLEMENTED = {
15
+ "files", "prior", "prior-wb", "geoprior", "forward", "forward-bh", "inversion",
16
+ "results", "plotting", "profile", "query", "workflow",
17
+ }
18
+
19
+
20
+ def register_all(rt) -> None:
21
+ workspace_ui.register(rt)
22
+ files.register(rt)
23
+ prior.register(rt)
24
+ geoprior.register(rt)
25
+ forward.register(rt)
26
+ borehole.register(rt)
27
+ inversion.register(rt)
28
+ results.register(rt)
29
+ plotting.register(rt)
30
+ profile.register(rt)
31
+ query.register(rt)
32
+ workflow.register(rt)
33
+ stubs.register(rt, skip=IMPLEMENTED)
@@ -0,0 +1,72 @@
1
+ """Tiny declarative parameter-form helper shared by the job pages.
2
+
3
+ ``F(name, label, kind, default)`` where ``kind`` is one of:
4
+ "int" | "float" | "str" | "bool" | ("select", [options]) | ("h5", [classes]) | "gex"
5
+ ``field_grid`` renders a ``.wb-fields`` grid; ``cast`` coerces one submitted value.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from fasthtml.common import Div, Input, Label, Span
13
+
14
+ from frontend.components import field, select, text_input
15
+ from frontend.services import integrate_api as api
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class F:
20
+ name: str
21
+ label: str
22
+ kind: object
23
+ default: str = ""
24
+
25
+
26
+ def _control(f: F, value: str):
27
+ k = f.kind
28
+ if isinstance(k, tuple) and k[0] == "select":
29
+ return select(f.name, list(k[1]), value=value)
30
+ if isinstance(k, tuple) and k[0] == "h5":
31
+ opts = api.list_h5_by_class(*k[1]) or ["(none)"]
32
+ return select(f.name, opts, value=value or opts[0])
33
+ if k in ("gex", "system", "json", "priordata"):
34
+ opts = {
35
+ "gex": api.list_gex,
36
+ "system": api.list_system_files,
37
+ "json": api.list_json,
38
+ "priordata": api.list_prior_with_data,
39
+ }[k]() or ["(none)"]
40
+ return select(f.name, opts, value=value or opts[0])
41
+ if k == "bool":
42
+ checked = str(value).lower() in ("1", "true", "on", "yes")
43
+ return Label(
44
+ Input(type="checkbox", name=f.name, value="1", checked=checked),
45
+ Span(cls="dot"),
46
+ " " + f.label, cls="radio",
47
+ )
48
+ return text_input(f.name, value)
49
+
50
+
51
+ def field_grid(specs: list[F], values: dict | None = None):
52
+ values = values or {}
53
+ cells = []
54
+ for f in specs:
55
+ v = values.get(f.name, f.default)
56
+ if f.kind == "bool":
57
+ cells.append(Div(_control(f, v), cls="wb-boolfield"))
58
+ else:
59
+ cells.append(field(f.label, _control(f, v)))
60
+ return Div(*cells, cls="wb-fields")
61
+
62
+
63
+ def cast(f: F, raw) -> object:
64
+ s = str(raw).strip()
65
+ try:
66
+ if f.kind == "int":
67
+ return int(float(s or f.default or "0"))
68
+ if f.kind == "float":
69
+ return float(s or f.default or "0")
70
+ except ValueError:
71
+ return f.default
72
+ return s