exege-core 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. exege_core-0.6.0/.gitignore +26 -0
  2. exege_core-0.6.0/LICENSE +28 -0
  3. exege_core-0.6.0/NOTICE +40 -0
  4. exege_core-0.6.0/PKG-INFO +120 -0
  5. exege_core-0.6.0/packages/exege/pyproject.toml +44 -0
  6. exege_core-0.6.0/pyproject.toml +112 -0
  7. exege_core-0.6.0/src/exege/README.md +69 -0
  8. exege_core-0.6.0/src/exege/__init__.py +17 -0
  9. exege_core-0.6.0/src/exege/_cli.py +89 -0
  10. exege_core-0.6.0/src/exege/_render.py +31 -0
  11. exege_core-0.6.0/src/exege/adapters/__init__.py +6 -0
  12. exege_core-0.6.0/src/exege/adapters/bundle_dir.py +315 -0
  13. exege_core-0.6.0/src/exege/adapters/latent_archive.py +561 -0
  14. exege_core-0.6.0/src/exege/adapters/toy_dynamics.py +168 -0
  15. exege_core-0.6.0/src/exege/app/__init__.py +14 -0
  16. exege_core-0.6.0/src/exege/app/cli.py +68 -0
  17. exege_core-0.6.0/src/exege/app/config.py +35 -0
  18. exege_core-0.6.0/src/exege/app/latent.py +722 -0
  19. exege_core-0.6.0/src/exege/app/main.py +19 -0
  20. exege_core-0.6.0/src/exege/app/record.py +228 -0
  21. exege_core-0.6.0/src/exege/app/theme.py +11 -0
  22. exege_core-0.6.0/src/exege/core/__init__.py +21 -0
  23. exege_core-0.6.0/src/exege/core/errors.py +25 -0
  24. exege_core-0.6.0/src/exege/core/extras.py +77 -0
  25. exege_core-0.6.0/src/exege/core/registry.py +138 -0
  26. exege_core-0.6.0/src/exege/figures/__init__.py +28 -0
  27. exege_core-0.6.0/src/exege/figures/bars.py +137 -0
  28. exege_core-0.6.0/src/exege/figures/grids.py +149 -0
  29. exege_core-0.6.0/src/exege/figures/maps.py +234 -0
  30. exege_core-0.6.0/src/exege/figures/series.py +91 -0
  31. exege_core-0.6.0/src/exege/latents/__init__.py +315 -0
  32. exege_core-0.6.0/src/exege/latents/analysis.py +402 -0
  33. exege_core-0.6.0/src/exege/latents/basis.py +521 -0
  34. exege_core-0.6.0/src/exege/latents/cli.py +1137 -0
  35. exege_core-0.6.0/src/exege/latents/evaluate.py +851 -0
  36. exege_core-0.6.0/src/exege/latents/features.py +318 -0
  37. exege_core-0.6.0/src/exege/latents/grid.py +190 -0
  38. exege_core-0.6.0/src/exege/latents/record.py +324 -0
  39. exege_core-0.6.0/src/exege/latents/samples.py +188 -0
  40. exege_core-0.6.0/src/exege/latents/source.py +480 -0
  41. exege_core-0.6.0/src/exege/latents/steering.py +530 -0
  42. exege_core-0.6.0/src/exege/latents/through.py +770 -0
  43. exege_core-0.6.0/src/exege/latents/toy.py +276 -0
  44. exege_core-0.6.0/src/exege/nn/__init__.py +14 -0
  45. exege_core-0.6.0/src/exege/nn/cli.py +81 -0
  46. exege_core-0.6.0/src/exege/nn/sae.py +192 -0
  47. exege_core-0.6.0/src/exege/nn/train.py +200 -0
  48. exege_core-0.6.0/tests/conftest.py +273 -0
  49. exege_core-0.6.0/tests/test_adapter_contracts.py +525 -0
  50. exege_core-0.6.0/tests/test_app.py +617 -0
  51. exege_core-0.6.0/tests/test_basis.py +600 -0
  52. exege_core-0.6.0/tests/test_bundle_dir.py +182 -0
  53. exege_core-0.6.0/tests/test_cli.py +68 -0
  54. exege_core-0.6.0/tests/test_extras.py +89 -0
  55. exege_core-0.6.0/tests/test_figures.py +258 -0
  56. exege_core-0.6.0/tests/test_grid.py +111 -0
  57. exege_core-0.6.0/tests/test_latent_analysis.py +317 -0
  58. exege_core-0.6.0/tests/test_latent_archive.py +148 -0
  59. exege_core-0.6.0/tests/test_latent_evaluate.py +529 -0
  60. exege_core-0.6.0/tests/test_latent_features.py +191 -0
  61. exege_core-0.6.0/tests/test_latent_hub.py +169 -0
  62. exege_core-0.6.0/tests/test_latent_record.py +250 -0
  63. exege_core-0.6.0/tests/test_latent_samples.py +131 -0
  64. exege_core-0.6.0/tests/test_latent_steering.py +571 -0
  65. exege_core-0.6.0/tests/test_latent_through.py +490 -0
  66. exege_core-0.6.0/tests/test_latent_toy.py +326 -0
  67. exege_core-0.6.0/tests/test_latent_validation.py +123 -0
  68. exege_core-0.6.0/tests/test_latent_views.py +251 -0
  69. exege_core-0.6.0/tests/test_nn.py +314 -0
  70. exege_core-0.6.0/tests/test_packaging.py +45 -0
  71. exege_core-0.6.0/tests/test_purity.py +185 -0
  72. exege_core-0.6.0/tests/test_registry.py +88 -0
@@ -0,0 +1,26 @@
1
+ # the site directory
2
+ site
3
+
4
+ # any cache
5
+ .cache
6
+
7
+ # python
8
+ .venv/
9
+ __pycache__/
10
+ *.py[cod]
11
+ *.egg-info/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+
15
+ # throwaway output: toy archives, fitted bases, whatever is made while trying things
16
+ scratch/
17
+
18
+ # lock files and tool environments: nothing is pinned here, and CI resolves from
19
+ # pyproject.toml, so a lock is one machine's answer and not the repo's
20
+ uv.lock
21
+ pixi.lock
22
+ .pixi/
23
+ poetry.lock
24
+ pdm.lock
25
+ Pipfile.lock
26
+ conda-lock.yml
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, E3SM AI Group
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,40 @@
1
+ exege
2
+ ====
3
+
4
+ This product is licensed under the BSD 3-Clause License; see LICENSE.
5
+
6
+ Third-party notices
7
+ -------------------
8
+
9
+ exege.latents, exege.figures and exege.app grew out of the latent space
10
+ visualiser for weather models,
11
+
12
+ https://github.com/ktempestuous/latent_space_visualiser_weather_models
13
+ Tempest, Beylich & Craig (2026), doi:10.1007/978-3-032-29915-4_10
14
+
15
+ whose region selection, channel ranking, cosine-similarity and PCA views they
16
+ reimplement, and whose SamudrACE exporter defines the latent-archive layout that
17
+ exege.adapters.latent_archive reads. That work is distributed under the MIT
18
+ License, reproduced here as it requires:
19
+
20
+ MIT License
21
+
22
+ Copyright (c) 2026 Kirsten
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.5
2
+ Name: exege-core
3
+ Version: 0.6.0
4
+ Summary: Tools for understanding and evaluating scientific machine-learning models: the code, and only Click. `exege` is the full install.
5
+ Project-URL: Homepage, https://mahf708.github.io/exege
6
+ Project-URL: Documentation, https://mahf708.github.io/exege/
7
+ Project-URL: Source, https://github.com/mahf708/exege
8
+ Project-URL: Issues, https://github.com/mahf708/exege/issues
9
+ Author: E3SM AI Group
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Keywords: climate,e3sm,emulator,interpretability,machine-learning
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: click>=8.1
23
+ Provides-Extra: app
24
+ Requires-Dist: cartopy>=0.23; extra == 'app'
25
+ Requires-Dist: matplotlib>=3.7; extra == 'app'
26
+ Requires-Dist: netcdf4>=1.6; extra == 'app'
27
+ Requires-Dist: numpy>=1.24; extra == 'app'
28
+ Requires-Dist: streamlit>=1.50; extra == 'app'
29
+ Requires-Dist: xarray>=2023.1; extra == 'app'
30
+ Provides-Extra: figures
31
+ Requires-Dist: cartopy>=0.23; extra == 'figures'
32
+ Requires-Dist: matplotlib>=3.7; extra == 'figures'
33
+ Requires-Dist: netcdf4>=1.6; extra == 'figures'
34
+ Requires-Dist: numpy>=1.24; extra == 'figures'
35
+ Requires-Dist: xarray>=2023.1; extra == 'figures'
36
+ Provides-Extra: hf
37
+ Requires-Dist: huggingface-hub>=0.23; extra == 'hf'
38
+ Requires-Dist: netcdf4>=1.6; extra == 'hf'
39
+ Requires-Dist: numpy>=1.24; extra == 'hf'
40
+ Requires-Dist: xarray>=2023.1; extra == 'hf'
41
+ Provides-Extra: latents
42
+ Requires-Dist: netcdf4>=1.6; extra == 'latents'
43
+ Requires-Dist: numpy>=1.24; extra == 'latents'
44
+ Requires-Dist: xarray>=2023.1; extra == 'latents'
45
+ Provides-Extra: nn
46
+ Requires-Dist: netcdf4>=1.6; extra == 'nn'
47
+ Requires-Dist: numpy>=1.24; extra == 'nn'
48
+ Requires-Dist: torch>=2.0; extra == 'nn'
49
+ Requires-Dist: xarray>=2023.1; extra == 'nn'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # exege
53
+
54
+ Tools for understanding and evaluating scientific machine-learning models: what an
55
+ emulator holds inside, sparse autoencoders trained on it, figures, and a local app over
56
+ them. The name comes from the Greek stem *exēgē-*, associated with explanation and
57
+ interpretation.
58
+
59
+ > **Research tool.** `exege` is early. What is described here works; expect it to change.
60
+
61
+ ## Install
62
+
63
+ This is the light install: the code, and only Click. Anything heavier sits behind an
64
+ extra named after the subpackage that needs it:
65
+
66
+ ```console
67
+ $ uv pip install 'exege-core[latents]' # or: pip install 'exege-core[latents]'
68
+ ```
69
+
70
+ For everything at once, install [exege](https://pypi.org/project/exege/) instead: it is
71
+ this with every extra but `nn` (`pip install exege`, or `'exege[nn]'` to add torch). Both
72
+ provide the same `import exege` and the same `exege` command.
73
+
74
+ | Extra | Pulls | Gets you |
75
+ | --- | --- | --- |
76
+ | `latents` | numpy, xarray, netCDF4 | `exege.latents`: latent diagnostics on a grid |
77
+ | `figures` | matplotlib, cartopy | `exege.figures`: maps and series figures (brings `latents`) |
78
+ | `app` | streamlit | `exege app`: a local web app (brings `figures`) |
79
+ | `nn` | torch | `exege.nn` and `exege nn`: sparse autoencoders (brings `latents`) |
80
+ | `hf` | huggingface_hub | archives read from a Hugging Face repository, `hf://datasets/...` (brings `latents`) |
81
+
82
+ A missing extra says so, with the command that fits how `exege` was installed.
83
+
84
+ ## Try it, with no model and no data
85
+
86
+ A toy emulator writes a latent archive with nothing but numpy; everything else reads it
87
+ like any other:
88
+
89
+ ```console
90
+ $ exege latents toy scratch/toy/control
91
+ $ exege latents info scratch/toy/control --mask-variable sst
92
+ $ exege latents region scratch/toy/control --lat 10 --lon -114 --time 2 --centered --pcs 2
93
+ $ exege latents fields scratch/toy/control --field precipitation --top 3
94
+ $ exege nn sae scratch/toy/control --features 64 --k 4 --out scratch/toy/sae.npz # needs exege-core[nn]
95
+ $ exege app --latents scratch/toy # needs exege-core[app]
96
+ ```
97
+
98
+ The CLI is a thin client of the Python API; anything it can do, a notebook can:
99
+
100
+ ```python
101
+ from exege.latents import Region, analyze_region, open_source
102
+
103
+ source = open_source("scratch/toy/control", mask_variable="sst")
104
+ result = analyze_region(
105
+ source, time=2, layer=3, region=Region(lat=10, lon=-114, radius_km=1500), centered=True
106
+ )
107
+ result.ranking.channels # the channels that respond most strongly there
108
+ ```
109
+
110
+ ## More
111
+
112
+ - Documentation: <https://mahf708.github.io/exege/>
113
+ - Source and issues: <https://github.com/mahf708/exege>
114
+
115
+ BSD-3-Clause. `exege.latents`, `exege.figures` and `exege.app` grew out of
116
+ the [latent space visualiser for weather models](https://github.com/ktempestuous/latent_space_visualiser_weather_models)
117
+ (Tempest, Beylich & Craig 2026, arXiv:2604.20467, doi:10.1007/978-3-032-29915-4_10); see
118
+ `NOTICE`. The sparse autoencoders follow MacMillan & Ouellette (2025, arXiv:2512.24440);
119
+ the B-spline autoencoder of Cheon (2026, arXiv:2605.17493) is what `exege.nn` is
120
+ heading for and does not implement yet.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ # The full install: no code of its own, only exege-core with every extra but nn.
6
+ # `import exege` and the `exege` command come from exege-core either way. The version
7
+ # and the pin below move together with `exege.__version__`; tests/test_packaging.py
8
+ # and the release workflow refuse any disagreement.
9
+ [project]
10
+ name = "exege"
11
+ version = "0.6.0"
12
+ description = "Tools for understanding and evaluating scientific machine-learning models: latent-space diagnostics, sparse autoencoders, figures, a local app"
13
+ readme = "README.md"
14
+ requires-python = ">=3.11"
15
+ license = "BSD-3-Clause"
16
+ authors = [{ name = "E3SM AI Group" }]
17
+ keywords = ["interpretability", "emulator", "climate", "e3sm", "machine-learning"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Science/Research",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
26
+ ]
27
+ dependencies = ["exege-core[app,hf]==0.6.0"]
28
+
29
+ [project.optional-dependencies]
30
+ # torch is left out of the default: its build (CPU, CUDA) is the machine's business.
31
+ nn = ["exege-core[nn]==0.6.0"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://mahf708.github.io/exege"
35
+ Documentation = "https://mahf708.github.io/exege/"
36
+ Source = "https://github.com/mahf708/exege"
37
+ Issues = "https://github.com/mahf708/exege/issues"
38
+
39
+ [tool.uv.sources]
40
+ exege-core = { workspace = true }
41
+
42
+ # Nothing to package but the metadata.
43
+ [tool.hatch.build.targets.wheel]
44
+ bypass-selection = true
@@ -0,0 +1,112 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "exege-core"
7
+ dynamic = ["version"]
8
+ description = "Tools for understanding and evaluating scientific machine-learning models: the code, and only Click. `exege` is the full install."
9
+ # The repository's README is about the repository; this one is about the package, and is
10
+ # the page PyPI shows.
11
+ readme = "src/exege/README.md"
12
+ requires-python = ">=3.11"
13
+ license = "BSD-3-Clause"
14
+ license-files = ["LICENSE", "NOTICE"]
15
+ authors = [{ name = "E3SM AI Group" }]
16
+ keywords = ["interpretability", "emulator", "climate", "e3sm", "machine-learning"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Science/Research",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
25
+ ]
26
+
27
+ # Base tier stays deliberately tiny: see src/exege/AGENTS.md
28
+ dependencies = ["click>=8.1"]
29
+
30
+ # One extra per subpackage that needs more than the base tier, named after it, and
31
+ # no others: "to use exege.latents, install exege-core[latents]" is the whole rule.
32
+ # The `exege` distribution (packages/exege) is this with every extra but nn.
33
+ [project.optional-dependencies]
34
+ # xarray reads what the adapters read, and needs a backend to open NetCDF with;
35
+ # netCDF4 is the one the rest of the E3SM/ACE stack already uses.
36
+ latents = ["numpy>=1.24", "xarray>=2023.1", "netCDF4>=1.6"]
37
+ # cartopy is for coastlines; without its data (no network) maps are still drawn.
38
+ figures = ["matplotlib>=3.7", "cartopy>=0.23", "exege-core[latents]"]
39
+ app = ["streamlit>=1.50", "exege-core[figures]"]
40
+ # It trains on latents, so it brings that extra as figures does.
41
+ nn = ["torch>=2.0", "exege-core[latents]"]
42
+ # Reads a latent archive straight from a Hugging Face dataset repository (hf://datasets/...),
43
+ # a file at a time.
44
+ hf = ["huggingface_hub>=0.23", "exege-core[latents]"]
45
+
46
+ # What a checkout gets and a wheel does not. uv installs the `dev` and `full` groups by
47
+ # default, so `uv run exege ...`, `uv run pytest` and `uv run ruff check` work with no
48
+ # flags, while an installed exege-core stays on the base tier until an extra is asked for.
49
+ # `full` is every extra but torch, which is large and whose build (CPU, CUDA) is the
50
+ # machine's business: ask for it with `uv sync --extra nn`. The base tier alone:
51
+ # uv sync --no-default-groups --group dev
52
+ # and the guide site: `uv run --group docs mkdocs build --strict`.
53
+ [dependency-groups]
54
+ dev = ["pytest>=7.4", "ruff>=0.5"]
55
+ full = ["exege-core[app]", "exege-core[hf]"]
56
+ # mkdocs 2 drops the plugin system this site is built on.
57
+ docs = ["mkdocs>=1.6,<2", "mkdocs-material[imaging]>=9.5"]
58
+
59
+ [tool.uv]
60
+ default-groups = ["dev", "full"]
61
+
62
+ # packages/exege is the full install: no code, only this distribution and its extras.
63
+ [tool.uv.workspace]
64
+ members = ["packages/exege"]
65
+
66
+ [project.scripts]
67
+ exege = "exege._cli:main"
68
+
69
+ # The only way an adapter is found, shipped or third-party alike, so one can ship in a
70
+ # separate distribution without editing exege at all. After adding an entry here, run
71
+ # `uv sync` so the installed metadata is refreshed.
72
+ [project.entry-points."exege.adapters"]
73
+ latent-archive = "exege.adapters.latent_archive:LatentArchive"
74
+ bundle-dir = "exege.adapters.bundle_dir:BundleDir"
75
+ toy-dynamics = "exege.adapters.toy_dynamics:ToyDynamics"
76
+
77
+ [project.urls]
78
+ Homepage = "https://mahf708.github.io/exege"
79
+ Documentation = "https://mahf708.github.io/exege/"
80
+ Source = "https://github.com/mahf708/exege"
81
+ Issues = "https://github.com/mahf708/exege/issues"
82
+
83
+ [tool.hatch.version]
84
+ path = "src/exege/__init__.py"
85
+
86
+ # A release holds the package, its tests and its licences: not the guide site, the run
87
+ # configs or the notes for contributors that share this repository with it.
88
+ [tool.hatch.build.targets.sdist]
89
+ include = ["src/exege", "tests", "LICENSE", "NOTICE", "pyproject.toml"]
90
+ exclude = ["AGENTS.md"]
91
+
92
+ [tool.hatch.build.targets.wheel]
93
+ packages = ["src/exege"]
94
+ exclude = ["AGENTS.md", "src/exege/README.md"]
95
+
96
+ [tool.ruff]
97
+ line-length = 100
98
+ src = ["src", "tests"]
99
+
100
+ [tool.ruff.lint]
101
+ # TID252 bans relative imports: the architecture tests read absolute ones.
102
+ select = ["E", "F", "I", "UP", "B", "SIM", "TID252"]
103
+
104
+ [tool.ruff.lint.flake8-tidy-imports]
105
+ ban-relative-imports = "all"
106
+
107
+ [tool.pytest.ini_options]
108
+ testpaths = ["tests"]
109
+ addopts = "-q"
110
+ # A compiled wheel (netCDF4, cftime) built against older numpy headers says this on
111
+ # import. numpy silences it itself; pytest resets the filters, so it is restated here.
112
+ filterwarnings = ["ignore:numpy.ndarray size changed:RuntimeWarning"]
@@ -0,0 +1,69 @@
1
+ # exege
2
+
3
+ Tools for understanding and evaluating scientific machine-learning models: what an
4
+ emulator holds inside, sparse autoencoders trained on it, figures, and a local app over
5
+ them. The name comes from the Greek stem *exēgē-*, associated with explanation and
6
+ interpretation.
7
+
8
+ > **Research tool.** `exege` is early. What is described here works; expect it to change.
9
+
10
+ ## Install
11
+
12
+ This is the light install: the code, and only Click. Anything heavier sits behind an
13
+ extra named after the subpackage that needs it:
14
+
15
+ ```console
16
+ $ uv pip install 'exege-core[latents]' # or: pip install 'exege-core[latents]'
17
+ ```
18
+
19
+ For everything at once, install [exege](https://pypi.org/project/exege/) instead: it is
20
+ this with every extra but `nn` (`pip install exege`, or `'exege[nn]'` to add torch). Both
21
+ provide the same `import exege` and the same `exege` command.
22
+
23
+ | Extra | Pulls | Gets you |
24
+ | --- | --- | --- |
25
+ | `latents` | numpy, xarray, netCDF4 | `exege.latents`: latent diagnostics on a grid |
26
+ | `figures` | matplotlib, cartopy | `exege.figures`: maps and series figures (brings `latents`) |
27
+ | `app` | streamlit | `exege app`: a local web app (brings `figures`) |
28
+ | `nn` | torch | `exege.nn` and `exege nn`: sparse autoencoders (brings `latents`) |
29
+ | `hf` | huggingface_hub | archives read from a Hugging Face repository, `hf://datasets/...` (brings `latents`) |
30
+
31
+ A missing extra says so, with the command that fits how `exege` was installed.
32
+
33
+ ## Try it, with no model and no data
34
+
35
+ A toy emulator writes a latent archive with nothing but numpy; everything else reads it
36
+ like any other:
37
+
38
+ ```console
39
+ $ exege latents toy scratch/toy/control
40
+ $ exege latents info scratch/toy/control --mask-variable sst
41
+ $ exege latents region scratch/toy/control --lat 10 --lon -114 --time 2 --centered --pcs 2
42
+ $ exege latents fields scratch/toy/control --field precipitation --top 3
43
+ $ exege nn sae scratch/toy/control --features 64 --k 4 --out scratch/toy/sae.npz # needs exege-core[nn]
44
+ $ exege app --latents scratch/toy # needs exege-core[app]
45
+ ```
46
+
47
+ The CLI is a thin client of the Python API; anything it can do, a notebook can:
48
+
49
+ ```python
50
+ from exege.latents import Region, analyze_region, open_source
51
+
52
+ source = open_source("scratch/toy/control", mask_variable="sst")
53
+ result = analyze_region(
54
+ source, time=2, layer=3, region=Region(lat=10, lon=-114, radius_km=1500), centered=True
55
+ )
56
+ result.ranking.channels # the channels that respond most strongly there
57
+ ```
58
+
59
+ ## More
60
+
61
+ - Documentation: <https://mahf708.github.io/exege/>
62
+ - Source and issues: <https://github.com/mahf708/exege>
63
+
64
+ BSD-3-Clause. `exege.latents`, `exege.figures` and `exege.app` grew out of
65
+ the [latent space visualiser for weather models](https://github.com/ktempestuous/latent_space_visualiser_weather_models)
66
+ (Tempest, Beylich & Craig 2026, arXiv:2604.20467, doi:10.1007/978-3-032-29915-4_10); see
67
+ `NOTICE`. The sparse autoencoders follow MacMillan & Ouellette (2025, arXiv:2512.24440);
68
+ the B-spline autoencoder of Cheon (2026, arXiv:2605.17493) is what `exege.nn` is
69
+ heading for and does not implement yet.
@@ -0,0 +1,17 @@
1
+ """exege -- tools for understanding and evaluating scientific machine-learning models.
2
+
3
+ Domains, and the presentation downstream of them:
4
+
5
+ - ``exege.latents`` what emulators hold inside: the latent space, on a grid
6
+ - ``exege.nn`` torch modules trained on those latents: a sparse autoencoder
7
+ - ``exege.figures`` figures of what ``latents`` computes, with no web framework
8
+ - ``exege.app`` a local web app over all of the above; nothing imports it
9
+
10
+ Everything framework-specific lives in ``exege.adapters``. See ``AGENTS.md``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.6.0"
16
+
17
+ __all__ = ["__version__"]
@@ -0,0 +1,89 @@
1
+ """Top-level ``exege`` command.
2
+
3
+ Subcommands load lazily, so ``exege --help`` and one command never pay for (or
4
+ require) what stands behind another.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import sys
11
+ from importlib import import_module
12
+ from importlib.metadata import entry_points
13
+
14
+ import click
15
+
16
+ from exege import __version__
17
+ from exege.core.errors import ExegeError
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+ # Shipped commands, by import path. Listing them for --help imports every cli
22
+ # module, so each must stay importable on the base tier: anything heavier is
23
+ # imported inside the command that needs it (tests/test_purity.py holds them to it).
24
+ _COMMANDS = {
25
+ "latents": "exege.latents.cli:latents",
26
+ "nn": "exege.nn.cli:nn",
27
+ "app": "exege.app.cli:app",
28
+ }
29
+
30
+ # A separate distribution adds a command by registering a ``click.Command``
31
+ # under this entry-point group. Shipped names win a clash.
32
+ _GROUP = "exege.commands"
33
+
34
+
35
+ class _LazyGroup(click.Group):
36
+ def list_commands(self, ctx: click.Context) -> list[str]:
37
+ return sorted(set(_COMMANDS) | {ep.name for ep in entry_points(group=_GROUP)})
38
+
39
+ def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
40
+ if name in _COMMANDS:
41
+ module, _, attr = _COMMANDS[name].partition(":")
42
+ return getattr(import_module(module), attr)
43
+ for ep in entry_points(group=_GROUP):
44
+ if ep.name == name:
45
+ try:
46
+ return ep.load()
47
+ except Exception as exc: # one broken plugin must not take --help down
48
+ log.warning("could not load command %r from %s: %s", name, ep.value, exc)
49
+ return None
50
+
51
+ def invoke(self, ctx: click.Context) -> object:
52
+ """Deliberate errors read as one clear line, not a traceback -- here, so
53
+ that a test runner or an embedding program sees what a terminal does.
54
+ ``--debug`` keeps the traceback."""
55
+ try:
56
+ return super().invoke(ctx)
57
+ except ExegeError as exc:
58
+ if ctx.params.get("debug"):
59
+ raise
60
+ raise click.ClickException(str(exc)) from exc
61
+
62
+
63
+ @click.group(cls=_LazyGroup)
64
+ @click.version_option(__version__, prog_name="exege")
65
+ @click.option("--debug", is_flag=True, help="Verbose logging.")
66
+ def cli(debug: bool) -> None:
67
+ """Tools for understanding and evaluating scientific machine-learning models."""
68
+ logging.basicConfig(
69
+ level=logging.DEBUG if debug else logging.WARNING,
70
+ format="%(levelname)s %(name)s: %(message)s",
71
+ )
72
+
73
+
74
+ def main() -> None:
75
+ try:
76
+ # Not standalone, so that an interrupt exits 130 without click's "Aborted!".
77
+ # click then *returns* the code of a `ctx.exit(n)` instead of exiting with it.
78
+ code = cli.main(standalone_mode=False)
79
+ except click.ClickException as exc:
80
+ exc.show()
81
+ sys.exit(exc.exit_code)
82
+ except click.Abort:
83
+ sys.exit(130)
84
+ if isinstance(code, int) and code:
85
+ sys.exit(code)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,31 @@
1
+ """Plain-text rendering. Presentation only, and stdlib only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping, Sequence
6
+ from typing import Any
7
+
8
+
9
+ def _cell(value: Any) -> str:
10
+ return "-" if value is None or value == "" else str(value)
11
+
12
+
13
+ def table(rows: Sequence[Mapping[str, Any]], columns: Iterable[str] | None = None) -> str:
14
+ """Left-aligned fixed-width table. Returns '' for no rows so callers can
15
+ distinguish 'nothing found' from 'a header with nothing under it'."""
16
+ if not rows:
17
+ return ""
18
+ cols = list(columns) if columns is not None else list(rows[0].keys())
19
+ widths = {c: max(len(c), *(len(_cell(r.get(c))) for r in rows)) for c in cols}
20
+ out = [" ".join(c.upper().ljust(widths[c]) for c in cols).rstrip()]
21
+ for row in rows:
22
+ out.append(" ".join(_cell(row.get(c)).ljust(widths[c]) for c in cols).rstrip())
23
+ return "\n".join(out)
24
+
25
+
26
+ def pairs(mapping: Mapping[str, Any]) -> str:
27
+ """Aligned ``key: value`` block."""
28
+ if not mapping:
29
+ return ""
30
+ width = max(len(k) for k in mapping)
31
+ return "\n".join(f"{k.ljust(width)} {_cell(v)}" for k, v in mapping.items())
@@ -0,0 +1,6 @@
1
+ """Adapters: every assumption about a specific framework lives here.
2
+
3
+ Adapters are resolved through the ``exege.adapters`` entry-point group, so a new
4
+ system ships as a new module (or a separate distribution) and core never changes.
5
+ Heavy dependencies belong here, behind an extra -- never in the base tier.
6
+ """