mantis-rtm 0.1.1.dev0__tar.gz → 0.1.2__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 (60) hide show
  1. mantis_rtm-0.1.2/.github/workflows/publish-pypi.yml +47 -0
  2. mantis_rtm-0.1.2/.github/workflows/tests.yml +35 -0
  3. {mantis_rtm-0.1.1.dev0/src/mantis_rtm.egg-info → mantis_rtm-0.1.2}/PKG-INFO +1 -1
  4. mantis_rtm-0.1.2/changelog.md +67 -0
  5. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/docs/tutorial.md +130 -28
  6. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/docs/tutorial.org +139 -3
  7. mantis_rtm-0.1.2/examples/velde_et_al_2016/kinetic_constants_fast.py +108 -0
  8. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/examples/velde_et_al_2016/kinetic_constants_slow.py +1 -1
  9. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/examples/velde_et_al_2016/plot_layout.py +1 -1
  10. mantis_rtm-0.1.2/examples/velde_et_al_2016/run_velde_fast.py +75 -0
  11. mantis_rtm-0.1.1.dev0/examples/velde_et_al_2016/run_velde_model.py → mantis_rtm-0.1.2/examples/velde_et_al_2016/run_velde_slow.py +2 -1
  12. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/_version.py +3 -3
  13. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/diff_lib.py +71 -25
  14. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/generate_equation_system.py +165 -143
  15. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/model.py +124 -19
  16. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/monolithic_solver.py +16 -0
  17. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/plot_data_new.py +11 -11
  18. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2/src/mantis_rtm.egg-info}/PKG-INFO +1 -1
  19. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/SOURCES.txt +6 -1
  20. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/scm_file_list.json +6 -1
  21. mantis_rtm-0.1.2/src/mantis_rtm.egg-info/scm_version.json +8 -0
  22. mantis_rtm-0.1.2/tests/test_diagenetic_model.py +179 -0
  23. mantis_rtm-0.1.1.dev0/src/mantis_rtm.egg-info/scm_version.json +0 -8
  24. mantis_rtm-0.1.1.dev0/tests/test_diagenetic_model.py +0 -65
  25. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/.gitignore +0 -0
  26. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/.python-version +0 -0
  27. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/LICENSE +0 -0
  28. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/README.md +0 -0
  29. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/examples/velde_et_al_2016/00readme.md +0 -0
  30. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/examples/velde_et_al_2016/fes_reactions.py +0 -0
  31. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/examples/velde_et_al_2016/species.py +0 -0
  32. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/pyproject.toml +0 -0
  33. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/pyrightconfig.json +0 -0
  34. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/requirements.txt +0 -0
  35. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/setup.cfg +0 -0
  36. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/.vscode/settings.json +0 -0
  37. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/__init__.py +0 -0
  38. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/live_plot_lib.py +0 -0
  39. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/monitor_plots.py +0 -0
  40. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/parallel_video.py +0 -0
  41. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis/solvers_numba.py +0 -0
  42. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/dependency_links.txt +0 -0
  43. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/entry_points.txt +0 -0
  44. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/requires.txt +0 -0
  45. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/src/mantis_rtm.egg-info/top_level.txt +0 -0
  46. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_direct_solver_numpy.py +0 -0
  47. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_equivalence.py +0 -0
  48. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_fes_flux_limited.py +0 -0
  49. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_fes_predictor_corrector.py +0 -0
  50. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_fes_smooth_transition.py +0 -0
  51. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_fes_symmetrical_picard.py +0 -0
  52. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_generate_equation_system.py +0 -0
  53. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_log_compression.py +0 -0
  54. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_monod_linearization.py +0 -0
  55. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_parallel_video.py +0 -0
  56. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_plot_data_options.py +0 -0
  57. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_reactant_coexistence.py +0 -0
  58. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_s0_disproportionation.py +0 -0
  59. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_solvers_numba.py +0 -0
  60. {mantis_rtm-0.1.1.dev0 → mantis_rtm-0.1.2}/tests/test_video_queue_and_logging.py +0 -0
@@ -0,0 +1,47 @@
1
+ name: Build and Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ name: Build distribution and publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/p/mantis-rtm
15
+ permissions:
16
+ id-token: write # Required for PyPI Trusted Publishing (OIDC)
17
+ contents: read # Required to checkout repository
18
+
19
+ steps:
20
+ - name: Check out repository
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0 # Required for setuptools_scm to extract version from tags
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install build tools
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ python -m pip install build twine
34
+
35
+ - name: Build source distribution and wheel
36
+ run: python -m build
37
+
38
+ - name: Verify distribution archives
39
+ run: twine check dist/*
40
+
41
+ - name: Publish to PyPI
42
+ uses: pypa/gh-action-pypi-publish@release/v1
43
+ # PyPI Trusted Publishing (OIDC) is used automatically when permissions.id-token is set.
44
+ # If you prefer using a classic PyPI API token instead of Trusted Publishing,
45
+ # add a repository secret named PYPI_API_TOKEN and uncomment the line below:
46
+ # with:
47
+ # password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,35 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Run test suite (${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.12", "3.13"]
17
+
18
+ steps:
19
+ - name: Check out repository
20
+ uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: Set up Python ${{ matrix.python-version }}
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install -e ".[test]"
33
+
34
+ - name: Run pytest
35
+ run: pytest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mantis-rtm
3
- Version: 0.1.1.dev0
3
+ Version: 0.1.2
4
4
  Summary: MANTIS: A Monolithic Assembly of Nonlinear Transport Interactions Solver
5
5
  Author-email: Uli Wortmann <uli.wortmann@utoronto.ca>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.2] - 2026-10-12
9
+
10
+ ### Added
11
+
12
+ - **File Path Resolution & Diagnostic Error Handling**:
13
+ - Enhanced file path resolution in `DiageneticModel` (`_resolve`), searching the current working directory, the model script directory (`base_dir`), and fallback naming patterns (such as `<experiment>_reactions.py`).
14
+ - Implemented early file validation (`validate_simulation_files()`) during `DiageneticModel` initialization and setup to catch missing simulation files before simulation execution.
15
+ - Added informative error diagnostics (`[MANTIS ERROR]`) detailing exact search locations and resolution guidance when species, reactions, constants, limiters, or plot layout files are missing.
16
+ - Improved plot layout file resolution in `plot_data_new.load_layout_from_file` and `plot_summary` with clear error reporting and search path feedback.
17
+ - Added unit test suite in `tests/test_diagenetic_model.py` testing missing file scenarios, exit behavior, and directory resolution from both CWD and model base directories.
18
+
19
+ ## [0.1.1] - 2026-10-12
20
+
21
+ ### Added
22
+
23
+ - **Steady-State Detection & Convergence**:
24
+ - Implemented a grid-independent spatial $L_2$ steady-state convergence check in `run_non_steady_state_solver_monolithic`. Computes a volume-weighted root-mean-square (RMS) change across grid cells per species and checks against `steady_state_tolerance` (or falls back to `dt_tolerance` if set).
25
+ - Added a minimum step requirement (step > 5) to prevent premature steady-state termination.
26
+ - **Model Parameters & Unit Conversions**:
27
+ - Added support for grid spacing parameters with unit conversion in `DiageneticModel`: `initial_spacing`, `reaction_zone_spacing`, and `max_spacing`.
28
+ - Added support for the `reaction_zone` tuple/list interval parameter with unit conversion.
29
+ - Added support for `steady_state_tolerance` and `dt_tolerance` solver tolerance parameters.
30
+ - Added `check_reactant_coexistence` boolean option and enabled the post-simulation reactant coexistence diagnostic by default.
31
+ - **Examples**:
32
+ - Added fast and slow simulation setups for the Velde et al. (2016) sediment diagenesis benchmark:
33
+ - `examples/velde_et_al_2016/run_velde_fast.py` & `kinetic_constants_fast.py`: fast configuration with pyrite precipitation.
34
+ - `examples/velde_et_al_2016/run_velde_slow.py` & `kinetic_constants_slow.py`: slow configuration tracking sulfur isotopes.
35
+ - **CI/CD & Automation**:
36
+ - Added GitHub Actions workflow `.github/workflows/tests.yml` to run automated test suites across Python 3.12, 3.13, and 3.14.
37
+ - Added GitHub Actions workflow `.github/workflows/publish-pypi.yml` for automated PyPI package publishing upon GitHub releases.
38
+ - **Documentation**:
39
+ - Added comprehensive documentation on physical units (`pint` integration) and non-uniform grid generation to `docs/tutorial.md` and `docs/tutorial.org`.
40
+
41
+ ### Changed
42
+
43
+ - **Rate Laws & Equation System Generation**:
44
+ - Simplified particulate organic carbon (POC) respiration rate equations by removing redundant coefficient calculations.
45
+ - Simplified FeS precipitation and dissolution rate formulation by removing unnecessary Michaelis-Menten terms.
46
+ - Adjusted default smoothing parameter `fes_smooth_epsilon` from `0.05` to `0.01` for sharper, smoother reaction transitions.
47
+ - Dynamically extract and handle active POC species and kinetic parameter pairs instead of using hardcoded POC fast/slow loops.
48
+ - **Differential Operators Library (`diff_lib`)**:
49
+ - Added type annotations across numerical and differential operator functions.
50
+ - Improved formatting, line wrapping, indentation, and visual structure.
51
+ - **Project Packaging & Metadata**:
52
+ - Updated package distribution name to `mantis-rtm` and enabled dynamic versioning using `setuptools_scm`.
53
+ - Added `pint` as an explicit core dependency in `pyproject.toml` and `requirements.txt`.
54
+ - Added AGPL-3.0-or-later license configuration and classifiers.
55
+ - Configured project URLs (Homepage, Repository, Issues) and updated build ignore patterns in `.gitignore`.
56
+ - **Examples**:
57
+ - Corrected FeS dissolution rate constant in `examples/velde_et_al_2016/kinetic_constants_slow.py`.
58
+ - Adjusted broken axis limits in `examples/velde_et_al_2016/plot_layout.py` for clearer concentration profile visualization.
59
+
60
+ ### Removed
61
+
62
+ - Removed obsolete `examples/velde_et_al_2016/run_velde_model.py` script.
63
+ - Removed legacy table of contents from tutorial documentation.
64
+
65
+ ## [0.1.0] - 2026-10-11
66
+
67
+ - Initial tagged release with core MANTIS reactive transport solver and tutorial documentation.
@@ -1,31 +1,3 @@
1
- - [Introduction: What is MANTIS?](#org5dbc758)
2
- - [Overall Model Architecture: What is Where?](#org4417d27)
3
- - [Step 1: Defining Chemical Species (`species.py`)](#orgc16f49a)
4
- - [Understanding Phases: Dissolved vs. Solid vs. Mixed](#orgba057ca)
5
- - [Example: Standard Species Configuration](#org10d01d0)
6
- - [Handling Diffusion Coefficients](#orgd0812cc)
7
- - [Custom Diffusion Functions (e.g., Dissolved Oxygen)](#org69559bb)
8
- - [Step 2: Defining Kinetic Constants (`kinetic_constants_*.py`)](#org809df99)
9
- - [Structure of `kinetic_constants_slow.py`](#org3c64ffa)
10
- - [Step 3: Defining Reactions & Limiters (`fes_reactions.py`)](#org1fecbeb)
11
- - [Understanding Limiters & Inhibitors](#org739ad2b)
12
- - [Declaring Chemical Reactions](#org5336c1e)
13
- - [Step 4: The Master Run Script (`run_velde_model.py`)](#org21fe2f4)
14
- - [Step 5: Designing Plot Layouts (`plot_layout.py`)](#orgf185086)
15
- - [Converting Model Units to Geochemical Units](#orgaefde21)
16
- - [Defining Multi-Panel Profiles](#orgf5ff558)
17
- - [Step 6: Running the Model & Producing Time-Lapse Videos](#orge7acd7b)
18
- - [Standard Execution](#orgee831e4)
19
- - [Generating a Time-Lapse Video](#orgcf4fea3)
20
- - [Command-Line Options Reference](#orgf872aa8)
21
- - [Solver Mechanics & Physical Realism (Geochemist's Alert!)](#orgfeddb58)
22
- - [How to Ensure Physical Realism:](#org9a13232)
23
- - [Summary & Next Steps](#org0cb5c1f)
24
-
25
-
26
-
27
- <a id="org5dbc758"></a>
28
-
29
1
  # Introduction: What is MANTIS?
30
2
 
31
3
  Welcome to **MANTIS** (\*M\*onolithic \*A\*ssembly of \*N\*onlinear \*T\*ransport \*I\*nteractions \*S\*olver)! If you are a geochemist, sedimentologist, or marine geologist, you are likely familiar with the classic picture of early diagenesis in marine sediments:
@@ -194,6 +166,85 @@ species = {
194
166
  }
195
167
  ```
196
168
 
169
+ # Concentration Units & Conversions: Inputs, Internal Engine, and Outputs
170
+
171
+ A common source of confusion in diagenetic modeling is the distinction between porewater solute concentrations, solid sediment mineral concentrations, and geochemical laboratory units (such as $\text{wt}\%$ or $\mu\text{mol/g}$). MANTIS handles physical units transparently while maintaining strict mass conservation across phases.
172
+
173
+ ## 1. Default Input Units
174
+
175
+ When defining your model setup and boundary conditions:
176
+
177
+ - **Driver Script Parameters (`run_velde_model.py`)**:
178
+ MANTIS supports human-readable strings parsed via Python's `pint` physical unit library:
179
+ - Lengths and depths: `"0.5 m"`, `"50 cm"`
180
+ - Times and time steps: `"0.5 kyr"`, `"1 year"`, `"24 hours"`
181
+ - Sedimentation rates: `"0.2 cm/yr"`, `"1 mm/a"`
182
+ - Bioturbation coefficients: `"4 cm^2/year"`, `"1e-9 m^2/s"`
183
+
184
+ - **Boundary Conditions (`species.py`)**:
185
+ - **Dissolved Solutes (`type: "dissolved"`)**:
186
+ Upper boundary concentrations can be specified as a numeric float or a unit string. The default numeric input is **$\text{mmol/L}$** (which is identical to $\text{mol/m}^3$ or $\text{mM}$).
187
+ ```text
188
+ "bc": {"top": 28.2, "type": "dissolved"} # 28.2 mmol/L (seawater sulfate)
189
+ "bc": {"top": "280 umol/L", "type": "dissolved"} # parsed automatically to 0.28 mmol/L
190
+ ```
191
+ - **Particulate Solids (`type: "particulate"`)**:
192
+ Deposition at the sediment-water interface is specified as a depositional surface flux:
193
+ ```text
194
+ "bc": {"top": "12 umol/(cm^2 * yr)", "type": "particulate"}
195
+ ```
196
+ MANTIS automatically converts this to internal flux units ($\text{mol}/(\text{m}^2 \cdot \text{s})$) during initialization.
197
+
198
+ ## 2. How Units Are Handled Internally by the Solver
199
+
200
+ Under the hood, MANTIS standardizes all transport and reaction PDEs into SI-derived volumetric units:
201
+ $$\text{Internal Concentration Unit} = \mathbf{mol/m^3} \quad (\equiv 1\ \text{mmol/L} \equiv 1\ \text{mM} \equiv 1000\ \mu\text{mol/L} \equiv 1\ \mu\text{mol/cm}^3)$$
202
+
203
+ Crucially, concentrations are defined **per unit volume of their respective phase**:
204
+ 1. **Liquid Solutes**: Expressed per volume of porewater ($\text{mol / m}^3_{\text{liquid}} = \text{mmol / L}_{\text{porewater}}$). The bulk sediment inventory is $\phi \cdot C_{\text{liquid}}$.
205
+ 2. **Solid Minerals**: Expressed per volume of pure solid sediment ($\text{mol / m}^3_{\text{solid}} = \text{mmol / L}_{\text{solid}}$). The bulk sediment inventory is $(1 - \phi) \cdot C_{\text{solid}}$.
206
+ 3. **Phase Transfer & Reactions**: Whenever a reaction couples a solid phase and a liquid phase (for example, $\text{Fe}^{2+} + \text{HS}^- \to \text{FeS}$), MANTIS scales the source and sink terms with the appropriate phase ratio ($(1-\phi)/\phi$ or $\phi/(1-\phi)$) so that mass is strictly conserved.
207
+ 4. **Reaction Rates**: Solved internally in $\text{mol} / (\text{m}^3_{\text{phase}} \cdot \text{s})$.
208
+ 5. **Depths and Transport**: Depth $z$ is in meters ($\text{m}$), time $t$ in seconds ($\text{s}$), and diffusion coefficients in $\text{m}^2/\text{s}$.
209
+
210
+ ## 3. Output Units in Result Files (`.csv`)
211
+
212
+ When the simulation completes or exports data (`<model_name>.csv`):
213
+ - `c_<species>`: Reported directly in **$\text{mmol/L}$** (numerically equivalent to $\text{mol/m}^3$):
214
+ - Solutes (e.g., `c_SO4`, `c_O2`, `c_TS2`) are in **$\text{mmol / L of porewater}$**.
215
+ - Solids (e.g., `c_FeS`, `c_Fe3`, `c_POC_slow`) are in **$\text{mmol / L of pure solid}$**.
216
+ - `f_<species>`: Net production/consumption reaction rates in **$\text{mol} / (\text{m}^3 \cdot \text{s})$**.
217
+ - `d_<species>`: Isotope delta values (e.g., `d_SO4`, `d_FeS`) in **$\permil$** (per mil relative to VCDT).
218
+ - `z`: Depth below sediment-water interface in **meters** ($\text{m}$).
219
+ - `phi` and `w`: Porosity (fraction, dimensionless) and burial velocity ($\text{m/s}$).
220
+
221
+ ## 4. Converting Solid Concentrations to Standard Geochemical Units
222
+
223
+ Geochemists typically report solid mineral abundances as dry-sediment mass fractions ($\text{wt}\%$) or mass-normalized molar concentrations ($\mu\text{mol/g}$). MANTIS includes built-in conversion utilities in `mantis.diff_lib`:
224
+
225
+ ### A. Converting to $\mu\text{mol / g}$ (micromoles per gram of dry sediment)
226
+ To convert a solid concentration $C_{\text{solid}}$ ($\text{mmol/L of solid} = \mu\text{mol/cm}^3_{\text{solid}}$) to $\mu\text{mol/g}$ of dry sediment, divide by the mineral grain density $\rho_{\text{sed}}$ (typically $2.6\ \text{g/cm}^3$ for siliciclastic sediments):
227
+ $$C_{\mu\text{mol/g}} = \frac{C_{\text{solid}}}{\rho_{\text{sed}}}$$
228
+
229
+ In Python:
230
+ ```python
231
+ from mantis.diff_lib import solid_conc_to_umol_per_g
232
+
233
+ # df.c_FeS is in mmol/L of solid; density in g/cm^3
234
+ c_fes_umol_g = solid_conc_to_umol_per_g(df.c_FeS, d=2.6)
235
+ ```
236
+
237
+ ### B. Converting to Dry Weight Percent ($\text{wt}\%$)
238
+ To convert $C_{\text{solid}}$ to weight percent of dry sediment matrix given molecular weight $M_w$ ($\text{g/mol}$):
239
+ $$\text{wt}\% = \frac{C_{\text{solid}} \cdot M_w}{\rho_{\text{sed}} \cdot 10^4}$$
240
+
241
+ In Python:
242
+ ```python
243
+ from mantis.diff_lib import solid_conc_to_wt_percent
244
+
245
+ # Example: FeS (Mw = 87.91 g/mol, density = 2.6 g/cm^3)
246
+ fes_wt_pct = solid_conc_to_wt_percent(df.c_FeS, mw=87.91, d=2.6, phi=df.phi)
247
+ ```
197
248
 
198
249
  <a id="org809df99"></a>
199
250
 
@@ -403,6 +454,12 @@ if __name__ == "__main__":
403
454
  "BI0": 0.0, # Set to 0.0 to disable; or e.g. "20 /year"
404
455
  "depth": "0 cm",
405
456
  },
457
+
458
+ # Spatial grid & reaction zone refinement (optional, defaults provided)
459
+ initial_spacing="0.1 mm", # Spacing at sediment-water interface (z=0)
460
+ reaction_zone=("0.1 cm", "1.0 cm"), # Critical depth window of sharp redox fronts
461
+ reaction_zone_spacing="0.05 mm", # Dense, uniform spacing in the reaction zone
462
+ max_spacing="1.0 cm", # Coarser spacing in deep sediment
406
463
  )
407
464
 
408
465
  # 3. Allow CLI overrides (e.g. running --video from the terminal)
@@ -423,6 +480,51 @@ if __name__ == "__main__":
423
480
  ```
424
481
 
425
482
 
483
+ ## Spatial Grid Configuration & Reaction Zone Resolution
484
+
485
+ Diagenetic models must resolve spatial scales ranging from sub-millimeter oxygen penetration depths to deep sediment columns spanning decimeters to hundreds of meters. A uniform fine grid across a 50-meter column would require millions of cells and slow down calculations unnecessarily.
486
+
487
+ MANTIS solves this using an adaptive geometric-mesh builder (`make_grid2`) that refines where steep gradients occur and expands into the deep sediment.
488
+
489
+ ### 1. How Grid Generation Works (`make_grid2`)
490
+ The grid structure is divided into four seamless stages from the sediment surface to depth:
491
+ 1. **Surface Section**: Starts at `initial_spacing` and geometrically refines cells downward toward `reaction_zone[0]`.
492
+ 2. **Reaction Zone Section**: Maintains a constant, ultra-fine cell size (`reaction_zone_spacing`) across the depth interval `reaction_zone = (z_start, z_end)`.
493
+ 3. **Coarsening Section**: Below the reaction zone, cell size expands geometrically by factor $r = 1.05$ per cell.
494
+ 4. **Deep Uniform Section**: Once cell size reaches `max_spacing`, the remainder of the domain down to `max_depth` uses uniform large cells.
495
+
496
+ ### 2. The Geochemical Importance of Reaction Zone Resolution
497
+ Why does grid spacing in the reaction zone matter so much?
498
+ Fast bimolecular redox reactions—such as ferrous iron oxidation ($\text{Fe}^{2+} + \text{O}_2 \to \text{Fe}^{3+}$) or sulfide oxidation—occur across exceedingly narrow interfaces. The physical reaction front width $\delta_{\text{phys}}$ is governed by the balance between molecular diffusion and reaction kinetics:
499
+ $$\delta_{\text{phys}} \sim \sqrt{\frac{D_{\text{eff}}}{k \cdot C_{\text{scale}}}}$$
500
+ For transport-controlled reactions with large kinetic constants ($k = 10^7\ \text{M}^{-1}\text{s}^{-1}$), $\delta_{\text{phys}}$ is often **under $0.05\text{ mm}$**.
501
+
502
+ If your numerical grid cell size $\Delta z$ is larger than $2 \delta_{\text{phys}}$, the local Damköhler number is large ($Da_{II} \gg 1$). The finite-volume discretization cannot resolve the sharp counter-gradients, producing a numerical artifact called **artificial reactant coexistence**:
503
+ - $\text{Fe}^{2+}$ and $\text{O}_2$ artificially overlap in the same cell.
504
+ - Reactants diffuse past each other instead of reacting completely at the interface.
505
+ - Apparent reaction rates become smeared over several millimeters to centimeters.
506
+
507
+ ### 3. Automated Coexistence Diagnostic
508
+ MANTIS automatically runs a post-simulation front diagnostic (`check_reactant_coexistence`) at the end of every run. If any reaction front is under-resolved, MANTIS outputs a clear warning:
509
+ ```text
510
+ [WARNING: Numerical Reactant Co-existence Detected]
511
+ Reaction: 'Fe2 Oxidation by O2' (fe2 + O2)
512
+ Front location (peak): z = 0.21 cm (depth cell index: 21)
513
+ Local grid resolution: dz = 0.10 mm
514
+ Estimated physical front width: 0.01 mm
515
+ Front cell span (N_cells): 6 cell(s)
516
+ Reasons: Local grid cell dz (0.10 mm) > 2x physical front width (0.01 mm)
517
+ --> Action recommended: In make_grid2(), refine the mesh around z = [0.19, 0.23] cm to reaction_zone_spacing <= 0.01 mm.
518
+ ```
519
+
520
+ When you see this warning, simply update your driver script's `model.configure()` with the recommended reaction zone window and spacing:
521
+ ```python
522
+ model.configure(
523
+ reaction_zone=("0.15 cm", "0.30 cm"),
524
+ reaction_zone_spacing="0.01 mm",
525
+ )
526
+ ```
527
+
426
528
  <a id="orgf185086"></a>
427
529
 
428
530
  # Step 5: Designing Plot Layouts (`plot_layout.py`)
@@ -1,8 +1,7 @@
1
- #+TITLE: MANTIS: A Geoscientist's Guide to 1D Reactive-Transport Modeling
2
1
  #+AUTHOR: MANTIS Development Team
3
2
  #+DATE: [2026-10-11]
4
- #+OPTIONS: toc:3 num:t ^:{}
5
- #+STARTUP: showeverything
3
+ #+OPTIONS: toc:nil
4
+
6
5
 
7
6
  * Introduction: What is MANTIS?
8
7
 
@@ -155,6 +154,90 @@ species = {
155
154
  }
156
155
  #+END_SRC
157
156
 
157
+ * Concentration Units & Conversions: Inputs, Internal Engine, and Outputs
158
+
159
+ A common source of confusion in diagenetic modeling is the distinction between porewater solute concentrations, solid sediment mineral concentrations, and geochemical laboratory units (such as $\text{wt}\%$ or $\mu\text{mol/g}$). MANTIS handles physical units transparently while maintaining strict mass conservation across phases.
160
+
161
+ ** 1. Default Input Units
162
+
163
+ When defining your model setup and boundary conditions:
164
+
165
+ - *Driver Script Parameters (=run_velde_model.py=)*:
166
+ MANTIS supports human-readable strings parsed via Python's =pint= physical unit library:
167
+ - Lengths and depths: ="0.5 m"=, ="50 cm"=
168
+ - Times and time steps: ="0.5 kyr"=, ="1 year"=, ="24 hours"=
169
+ - Sedimentation rates: ="0.2 cm/yr"=, ="1 mm/a"=
170
+ - Bioturbation coefficients: ="4 cm^2/year"=, ="1e-9 m^2/s"=
171
+
172
+ - *Boundary Conditions (=species.py=)*:
173
+ - *Dissolved Solutes (=type: "dissolved"=)*:
174
+ Upper boundary concentrations can be specified as a numeric float or a unit string. The default numeric input is *$\text{mmol/L}$* (which is identical to $\text{mol/m}^3$ or $\text{mM}$).
175
+ #+BEGIN_EXAMPLE
176
+ "bc": {"top": 28.2, "type": "dissolved"} # 28.2 mmol/L (seawater sulfate)
177
+ "bc": {"top": "280 umol/L", "type": "dissolved"} # parsed automatically to 0.28 mmol/L
178
+ #+END_EXAMPLE
179
+ - *Particulate Solids (=type: "particulate"=)*:
180
+ Deposition at the sediment-water interface is specified as a depositional surface flux:
181
+ #+BEGIN_EXAMPLE
182
+ "bc": {"top": "12 umol/(cm^2 * yr)", "type": "particulate"}
183
+ #+END_EXAMPLE
184
+ MANTIS automatically converts this to internal flux units ($\text{mol}/(\text{m}^2 \cdot \text{s})$) during initialization.
185
+
186
+ ** 2. How Units Are Handled Internally by the Solver
187
+
188
+ Under the hood, MANTIS standardizes all transport and reaction PDEs into SI-derived volumetric units:
189
+ \[
190
+ \text{Internal Concentration Unit} = \mathbf{mol/m^3} \quad (\equiv 1\ \text{mmol/L} \equiv 1\ \text{mM} \equiv 1000\ \mu\text{mol/L} \equiv 1\ \mu\text{mol/cm}^3)
191
+ \]
192
+
193
+ Crucially, concentrations are defined *per unit volume of their respective phase*:
194
+ 1. *Liquid Solutes*: Expressed per volume of porewater ($\text{mol / m}^3_{\text{liquid}} = \text{mmol / L}_{\text{porewater}}$). The bulk sediment inventory is $\phi \cdot C_{\text{liquid}}$.
195
+ 2. *Solid Minerals*: Expressed per volume of pure solid sediment ($\text{mol / m}^3_{\text{solid}} = \text{mmol / L}_{\text{solid}}$). The bulk sediment inventory is $(1 - \phi) \cdot C_{\text{solid}}$.
196
+ 3. *Phase Transfer & Reactions*: Whenever a reaction couples a solid phase and a liquid phase (for example, $\text{Fe}^{2+} + \text{HS}^- \to \text{FeS}$), MANTIS scales the source and sink terms with the appropriate phase ratio ($(1-\phi)/\phi$ or $\phi/(1-\phi)$) so that mass is strictly conserved.
197
+ 4. *Reaction Rates*: Solved internally in $\text{mol} / (\text{m}^3_{\text{phase}} \cdot \text{s})$.
198
+ 5. *Depths and Transport*: Depth $z$ is in meters ($\text{m}$), time $t$ in seconds ($\text{s}$), and diffusion coefficients in $\text{m}^2/\text{s}$.
199
+
200
+ ** 3. Output Units in Result Files (=.csv=)
201
+
202
+ When the simulation completes or exports data (=<model_name>.csv=):
203
+ - =c_<species>=: Reported directly in *$\text{mmol/L}$* (numerically equivalent to $\text{mol/m}^3$):
204
+ - Solutes (e.g., =c_SO4=, =c_O2=, =c_TS2=) are in *$\text{mmol / L of porewater}$*.
205
+ - Solids (e.g., =c_FeS=, =c_Fe3=, =c_POC_slow=) are in *$\text{mmol / L of pure solid}$*.
206
+ - =f_<species>=: Net production/consumption reaction rates in *$\text{mol} / (\text{m}^3 \cdot \text{s})$*.
207
+ - =d_<species>=: Isotope delta values (e.g., =d_SO4=, =d_FeS=) in *$\permil$* (per mil relative to VCDT).
208
+ - =z=: Depth below sediment-water interface in *meters* ($\text{m}$).
209
+ - =phi= and =w=: Porosity (fraction, dimensionless) and burial velocity ($\text{m/s}$).
210
+
211
+ ** 4. Converting Solid Concentrations to Standard Geochemical Units
212
+
213
+ Geochemists typically report solid mineral abundances as dry-sediment mass fractions ($\text{wt}\%$) or mass-normalized molar concentrations ($\mu\text{mol/g}$). MANTIS includes built-in conversion utilities in =mantis.diff_lib=:
214
+
215
+ *** A. Converting to $\mu\text{mol / g}$ (micromoles per gram of dry sediment)
216
+ To convert a solid concentration $C_{\text{solid}}$ ($\text{mmol/L of solid} = \mu\text{mol/cm}^3_{\text{solid}}$) to $\mu\text{mol/g}$ of dry sediment, divide by the mineral grain density $\rho_{\text{sed}}$ (typically $2.6\ \text{g/cm}^3$ for siliciclastic sediments):
217
+ \[
218
+ C_{\mu\text{mol/g}} = \frac{C_{\text{solid}}}{\rho_{\text{sed}}}
219
+ \]
220
+ In Python:
221
+ #+BEGIN_SRC python
222
+ from mantis.diff_lib import solid_conc_to_umol_per_g
223
+
224
+ # df.c_FeS is in mmol/L of solid; density in g/cm^3
225
+ c_fes_umol_g = solid_conc_to_umol_per_g(df.c_FeS, d=2.6)
226
+ #+END_SRC
227
+
228
+ *** B. Converting to Dry Weight Percent ($\text{wt}\%$)
229
+ To convert $C_{\text{solid}}$ to weight percent of dry sediment matrix given molecular weight $M_w$ ($\text{g/mol}$):
230
+ \[
231
+ \text{wt}\% = \frac{C_{\text{solid}} \cdot M_w}{\rho_{\text{sed}} \cdot 10^4}
232
+ \]
233
+ In Python:
234
+ #+BEGIN_SRC python
235
+ from mantis.diff_lib import solid_conc_to_wt_percent
236
+
237
+ # Example: FeS (Mw = 87.91 g/mol, density = 2.6 g/cm^3)
238
+ fes_wt_pct = solid_conc_to_wt_percent(df.c_FeS, mw=87.91, d=2.6, phi=df.phi)
239
+ #+END_SRC
240
+
158
241
  * Step 2: Defining Kinetic Constants (=kinetic_constants_*.py=)
159
242
 
160
243
  The kinetic constants file defines how fast reactions occur and the affinity thresholds of microbial communities.
@@ -349,6 +432,12 @@ if __name__ == "__main__":
349
432
  "BI0": 0.0, # Set to 0.0 to disable; or e.g. "20 /year"
350
433
  "depth": "0 cm",
351
434
  },
435
+
436
+ # Spatial grid & reaction zone refinement (optional, defaults provided)
437
+ initial_spacing="0.1 mm", # Spacing at sediment-water interface (z=0)
438
+ reaction_zone=("0.1 cm", "1.0 cm"), # Critical depth window of sharp redox fronts
439
+ reaction_zone_spacing="0.05 mm", # Dense, uniform spacing in the reaction zone
440
+ max_spacing="1.0 cm", # Coarser spacing in deep sediment
352
441
  )
353
442
 
354
443
  # 3. Allow CLI overrides (e.g. running --video from the terminal)
@@ -368,6 +457,53 @@ if __name__ == "__main__":
368
457
  result.plot_summary()
369
458
  #+END_SRC
370
459
 
460
+ ** Spatial Grid Configuration & Reaction Zone Resolution
461
+
462
+ Diagenetic models must resolve spatial scales ranging from sub-millimeter oxygen penetration depths to deep sediment columns spanning decimeters to hundreds of meters. A uniform fine grid across a 50-meter column would require millions of cells and slow down calculations unnecessarily.
463
+
464
+ MANTIS solves this using an adaptive geometric-mesh builder (=make_grid2=) that refines where steep gradients occur and expands into the deep sediment.
465
+
466
+ *** 1. How Grid Generation Works (=make_grid2=)
467
+ The grid structure is divided into four seamless stages from the sediment surface to depth:
468
+ 1. *Surface Section*: Starts at =initial_spacing= and geometrically refines cells downward toward =reaction_zone[0]=.
469
+ 2. *Reaction Zone Section*: Maintains a constant, ultra-fine cell size (=reaction_zone_spacing=) across the depth interval =reaction_zone = (z_start, z_end)=.
470
+ 3. *Coarsening Section*: Below the reaction zone, cell size expands geometrically by factor $r = 1.05$ per cell.
471
+ 4. *Deep Uniform Section*: Once cell size reaches =max_spacing=, the remainder of the domain down to =max_depth= uses uniform large cells.
472
+
473
+ *** 2. The Geochemical Importance of Reaction Zone Resolution
474
+ Why does grid spacing in the reaction zone matter so much?
475
+ Fast bimolecular redox reactions—such as ferrous iron oxidation ($\text{Fe}^{2+} + \text{O}_2 \to \text{Fe}^{3+}$) or sulfide oxidation—occur across exceedingly narrow interfaces. The physical reaction front width $\delta_{\text{phys}}$ is governed by the balance between molecular diffusion and reaction kinetics:
476
+ \[
477
+ \delta_{\text{phys}} \sim \sqrt{\frac{D_{\text{eff}}}{k \cdot C_{\text{scale}}}}
478
+ \]
479
+ For transport-controlled reactions with large kinetic constants ($k = 10^7\ \text{M}^{-1}\text{s}^{-1}$), $\delta_{\text{phys}}$ is often *under $0.05\text{ mm}$*.
480
+
481
+ If your numerical grid cell size $\Delta z$ is larger than $2 \delta_{\text{phys}}$, the local Damk\ouml{}hler number is large ($Da_{II} \gg 1$). The finite-volume discretization cannot resolve the sharp counter-gradients, producing a numerical artifact called *artificial reactant coexistence*:
482
+ - $\text{Fe}^{2+}$ and $\text{O}_2$ artificially overlap in the same cell.
483
+ - Reactants diffuse past each other instead of reacting completely at the interface.
484
+ - Apparent reaction rates become smeared over several millimeters to centimeters.
485
+
486
+ *** 3. Automated Coexistence Diagnostic
487
+ MANTIS automatically runs a post-simulation front diagnostic (=check_reactant_coexistence=) at the end of every run. If any reaction front is under-resolved, MANTIS outputs a clear warning:
488
+ #+BEGIN_EXAMPLE
489
+ [WARNING: Numerical Reactant Co-existence Detected]
490
+ Reaction: 'Fe2 Oxidation by O2' (fe2 + O2)
491
+ Front location (peak): z = 0.21 cm (depth cell index: 21)
492
+ Local grid resolution: dz = 0.10 mm
493
+ Estimated physical front width: 0.01 mm
494
+ Front cell span (N_cells): 6 cell(s)
495
+ Reasons: Local grid cell dz (0.10 mm) > 2x physical front width (0.01 mm)
496
+ --> Action recommended: In make_grid2(), refine the mesh around z = [0.19, 0.23] cm to reaction_zone_spacing <= 0.01 mm.
497
+ #+END_EXAMPLE
498
+
499
+ When you see this warning, simply update your driver script's =model.configure()= with the recommended reaction zone window and spacing:
500
+ #+BEGIN_SRC python
501
+ model.configure(
502
+ reaction_zone=("0.15 cm", "0.30 cm"),
503
+ reaction_zone_spacing="0.01 mm",
504
+ )
505
+ #+END_SRC
506
+
371
507
  * Step 5: Designing Plot Layouts (=plot_layout.py=)
372
508
 
373
509
  Geochemists usually want to see:
@@ -0,0 +1,108 @@
1
+ """Define reaction constants.
2
+
3
+ Note, Velde et al likely have a typo in the FeS equilibrium constant.
4
+ Rickard (2006) defines two reaction constants, one with Ksp = 3.5
5
+ and the other with Ksp = - 3.5. Here we use the latter since otherwise
6
+ we would never precipitate FeS unless H2S > 1 mmol/l
7
+ """
8
+
9
+ from typing import Any
10
+
11
+
12
+ def get_reaction_constants(
13
+ pH: float, phi: float | Any, k_values: dict = {}
14
+ ) -> dict:
15
+ """Convert reaction_constants into model units.
16
+
17
+ It is assumed that the reaction constants are in phase specific units.
18
+ This can be tricky, as Velde et al, state that their k-values are in
19
+ bulk units, but then they convert it before using (see Table 3). E.g.
20
+ they write:
21
+
22
+ (1−ϕ)⋅kSIR⋅[FeOOH]⋅[HS−]
23
+ ​
24
+ which suggests that the (1−ϕ) is used to convert FeOOH from solid
25
+ phase concentration to bulk concentration. Since this code
26
+ tracks all concentrations in phase specific units, converting the
27
+ k-value by deviding by (1−ϕ), would cancel out, and we can use the
28
+ k-value as published.
29
+ """
30
+ import pint
31
+
32
+ ureg = pint.UnitRegistry()
33
+ Q_ = ureg.Quantity
34
+
35
+ if hasattr(phi, "value"):
36
+ phi = phi.value
37
+
38
+ velde: dict = {
39
+ "POC_fast": [10, "1/year", "1/second"], # highly reactive OM
40
+ "POC_slow": [0.1, "1/year", "1/second"], # reactive OM, Velde et al
41
+ "Fe2_p_eq": [
42
+ 696,
43
+ "dimensionless",
44
+ "dimensionless",
45
+ ], # sorbed vs Fe2+ Fe2+ liq.
46
+ "FeS2_O2": [
47
+ 1e-10,
48
+ "m^3/(mol*second)",
49
+ "m^3/(mol*second)",
50
+ ], # FeS2 + O2 -> SO4, Halevy et al
51
+ "FeS_S0": [ # FeS + S0 -> FeS2
52
+ 5e-8,
53
+ "m^3/(mol*second)",
54
+ "m^3/(mol*second)",
55
+ ], # FeS + HS -> FeS2
56
+ "FeS_TS2": [
57
+ 5e-8,
58
+ "m^3/(mol*second)",
59
+ "m^3/(mol*second)",
60
+ ], # FeS + H2S -> FeS2, at 10C -> notes.org
61
+ "Hplus": [10 ** (-pH), "mol/l", "mol/m^3"],
62
+ # Oxidation reactions after Velde at eal 2016
63
+ "TS2_O2": [1e7, "cm^3/(umol*year)", "m^3/(mol*second)"],
64
+ "Fe3_hs": [
65
+ 494,
66
+ "cm^3/(umol*year)",
67
+ "m^3/(mol*second)",
68
+ ], # check with halevy
69
+ "FeS_O2": [1e7, "cm^3/(umol*year)", "m^3/(mol*second)"],
70
+ "Fe2_O2": [1e7, "cm^3/(umol*year)", "m^3/(mol*second)"],
71
+ # "Fe2p_ox": [1e73, "cm^3/(umol*year)", "m^3/(mol*second)"],
72
+ "S0_O2": [4e7, "cm^3/(umol*year)", "m^3/(mol*second)"],
73
+ # Iron sulfide reactions after Velde at eal 2016
74
+ "FeS_isp": [
75
+ 1e4,
76
+ "umol/((cm^3*year))",
77
+ "mol/(m^3 *second)",
78
+ ], # FeS precipitation
79
+ "FeS_isd": [3, "1/year", "1/second"], # FeS dissolution
80
+ "FeS_sp": [ # FeS equilibrium constant
81
+ 10**-3.5,
82
+ "mol/l",
83
+ "mol/m^3 ",
84
+ ],
85
+ # S0 disproportionation rate (first-order in S0)
86
+ "S0_dispro": [50, "1/year", "1/second"],
87
+ }
88
+
89
+ for k, v in velde.items():
90
+ k_values[k] = Q_(f"{v[0]} {v[1]}").to(f"{v[2]}").magnitude
91
+
92
+ return velde, k_values
93
+
94
+
95
+ if __name__ == "__main__":
96
+ import pint
97
+
98
+ ureg = pint.UnitRegistry()
99
+ Q_ = ureg.Quantity
100
+
101
+ velde, k_values = get_reaction_constants(7.5, 0.8)
102
+ ureg = pint.UnitRegistry()
103
+ for k, v in velde.items():
104
+ n = Q_(f"{v[0]} {v[1]}").to(f"{v[2]}")
105
+ print(f"{k} = {n.magnitude:.2e} [{n.units:~P}]")
106
+
107
+ print(f"k.FeS_sp * k.Hplus: {k_values['FeS_sp'] * k_values['Hplus']:.2e}")
108
+ print(f"k.Hplus: {k_values['Hplus']:.2e}")
@@ -76,7 +76,7 @@ def get_reaction_constants(
76
76
  "umol/((cm^3*year))",
77
77
  "mol/(m^3 *second)",
78
78
  ], # FeS precipitation
79
- "FeS_isd": [3/10, "1/year", "1/second"], # FeS dissolution
79
+ "FeS_isd": [3, "1/year", "1/second"], # FeS dissolution
80
80
  "FeS_sp": [ # FeS equilibrium constant
81
81
  10**-3.5,
82
82
  "mol/l",
@@ -168,7 +168,7 @@ def get_layout(df):
168
168
  "xlim": (1e-4, None),
169
169
  # "ylim": (-15, 75),
170
170
  # "right": [[df.d_h2s, "d_h2s", {"color": "C1"}]],
171
- "options-left": ["broken_axis(-45, -40, 20, 25)"],
171
+ "options-left": ["broken_axis(-45, -37, 20, 30)"],
172
172
  },
173
173
  })
174
174