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.
- {integrate_module-0.99.6/integrate_module.egg-info → integrate_module-0.99.9}/PKG-INFO +25 -4
- {integrate_module-0.99.6 → integrate_module-0.99.9}/README.md +17 -0
- integrate_module-0.99.9/frontend/__init__.py +10 -0
- integrate_module-0.99.9/frontend/__main__.py +4 -0
- integrate_module-0.99.9/frontend/app.py +61 -0
- integrate_module-0.99.9/frontend/components.py +256 -0
- integrate_module-0.99.9/frontend/config.py +48 -0
- integrate_module-0.99.9/frontend/pages/__init__.py +33 -0
- integrate_module-0.99.9/frontend/pages/_forms.py +72 -0
- integrate_module-0.99.9/frontend/pages/_jobs_ui.py +125 -0
- integrate_module-0.99.9/frontend/pages/borehole.py +94 -0
- integrate_module-0.99.9/frontend/pages/files.py +189 -0
- integrate_module-0.99.9/frontend/pages/forward.py +91 -0
- integrate_module-0.99.9/frontend/pages/geoprior.py +556 -0
- integrate_module-0.99.9/frontend/pages/inversion.py +139 -0
- integrate_module-0.99.9/frontend/pages/plotting.py +142 -0
- integrate_module-0.99.9/frontend/pages/prior.py +241 -0
- integrate_module-0.99.9/frontend/pages/profile.py +141 -0
- integrate_module-0.99.9/frontend/pages/query.py +217 -0
- integrate_module-0.99.9/frontend/pages/results.py +142 -0
- integrate_module-0.99.9/frontend/pages/stubs.py +38 -0
- integrate_module-0.99.9/frontend/pages/workflow.py +255 -0
- integrate_module-0.99.9/frontend/pages/workspace_ui.py +35 -0
- integrate_module-0.99.9/frontend/services/__init__.py +9 -0
- integrate_module-0.99.9/frontend/services/figures.py +173 -0
- integrate_module-0.99.9/frontend/services/files.py +196 -0
- integrate_module-0.99.9/frontend/services/integrate_api.py +1053 -0
- integrate_module-0.99.9/frontend/services/jobs.py +125 -0
- integrate_module-0.99.9/frontend/services/session.py +24 -0
- integrate_module-0.99.9/frontend/services/worker.py +330 -0
- integrate_module-0.99.9/frontend/services/workspace.py +59 -0
- integrate_module-0.99.9/frontend/services/xlsx.py +104 -0
- integrate_module-0.99.9/frontend/static/app.css +250 -0
- integrate_module-0.99.9/frontend/static/figures/006b874a67148744.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/01c54329eb934b92.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/0454af65d7dea5f1.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/061d5cecc1a2f677.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/066bb5c9da4d0c0b.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/0baffb84416e0aa5.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/0bddf00c6308d581.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/0d0c87a2b0317423.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/0e020d8b85920d7e.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/105023459d1a8567.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/12846323d657202c.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/12846323d657202c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/1a9964bb2736785c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/1acd3021d782ccc2.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/1acd3021d782ccc2.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/1c84cb9595fbcb9b.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/1ec73b329d1e30c1-0.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/29dc879298fdfa8a.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/2b167d8b53dd646f.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/2b764aca24a6fd44.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/2d78ec085ac2bbef.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/3bf8957c95d8dc58.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/3f50930542de8196.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/41ef6a32da0ec924.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/426adc78c42b1a27.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/427dd675638f594d.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/427dd675638f594d.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/487ff0ef8da5a2ec.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/4dc6542b7b0f149f.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/4ec72f25a8ec038c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/4feb7f1a8281a0bd.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/525d39d8ea618a41.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/53da1cb8aa8a6cb1.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/55d5162d81b64a36.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/588f962e19adecff.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/5a5b4b0457603714.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/5d84b1ae75e96c16.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/5fdc8164998fe564.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/64cdac3bedd0cecd.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/65cf5a549f2f1b33.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/689bb914194a8d64.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/68bdea6008a16e42.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/6a7883226a8a477d.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/6c85602ebffb533e.json +1 -0
- integrate_module-0.99.9/frontend/static/figures/6c85602ebffb533e.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/75c488b1f85e3369.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/78a160053c2cac33.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/79d91b91323697cd.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/7a64c69b35eaf912.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/7bb22ced65184b59.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/7e7213e8f8247031.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/7fd95f0d2c9595c9.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/994f10f602985978.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/9a6b1d1f0a7ffd11.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/9a8729c07d09bd5a.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/9ddc30d24404a155-0.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/a6f07ace7e42da09.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/ab655c40d7fffc4f.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b12e125404b2aea1.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b2f3a4ce9f535f45.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b605415c314c6cef.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b7d8d18a140c0073.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b829df1a78718dd6.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b8dbb12675172fda.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/b99e09ec69df4967.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/be438f0ac7f59a5c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/c9030d3cf5e7b94c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/d20e6de716ab4454.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/d2295c0eaa723af8.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/d5925defec62f763.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/d7daade6e2618161.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/e029eb6cb62868dd.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/e4000e24e61bcefc.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/e58009ee8354d189.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/e5b501e77c7db114.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/e8b65132706e5437.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/f015d3f025e93a71.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/f3cf99660141b008.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/f4a3311840de58e2-0.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/f5a08d8db05edb9c.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/f8907379e981be33-0.png +0 -0
- integrate_module-0.99.9/frontend/static/figures/fb3b9efeef379e15.png +0 -0
- integrate_module-0.99.9/frontend/static/fonts/archivo-latin-ext.woff2 +0 -0
- integrate_module-0.99.9/frontend/static/fonts/archivo-latin.woff2 +0 -0
- integrate_module-0.99.9/frontend/static/fonts/archivo-vietnamese.woff2 +0 -0
- integrate_module-0.99.9/frontend/static/htmx.min.js +1 -0
- integrate_module-0.99.9/frontend/static/modernist.css +253 -0
- integrate_module-0.99.9/frontend/static/picker.js +90 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/__init__.py +168 -147
- integrate_module-0.99.9/integrate/anemone_forward.py +728 -0
- integrate_module-0.99.9/integrate/em_system.py +295 -0
- integrate_module-0.99.9/integrate/gaaem_forward.py +697 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate.py +542 -628
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_io.py +35 -11
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_plot.py +843 -290
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_query.py +337 -4
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection.py +75 -16
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection_cli.py +9 -3
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_rejection_jax.py +55 -6
- integrate_module-0.99.9/integrate/simpeg_forward.py +557 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9/integrate_module.egg-info}/PKG-INFO +25 -4
- integrate_module-0.99.9/integrate_module.egg-info/SOURCES.txt +147 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/entry_points.txt +1 -1
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/requires.txt +7 -3
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/top_level.txt +1 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/pyproject.toml +31 -6
- integrate_module-0.99.9/tests/test_likelihood_nan.py +46 -0
- integrate_module-0.99.9/tests/test_simpeg_forward.py +362 -0
- integrate_module-0.99.6/integrate/integrate_www_cli.py +0 -8
- integrate_module-0.99.6/integrate_module.egg-info/SOURCES.txt +0 -24
- integrate_module-0.99.6/tests/test_likelihood_multinomial.py +0 -260
- {integrate_module-0.99.6 → integrate_module-0.99.9}/LICENSE +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/gex.py +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_borehole.py +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_hdf5_info_cli.py +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/integrate_timing_cli.py +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate/mlmapping.py +0 -0
- {integrate_module-0.99.6 → integrate_module-0.99.9}/integrate_module.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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[
|
|
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,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
|