Skip to content

Instantly share code, notes, and snippets.

@davekennewell
Created February 21, 2026 09:13
Show Gist options
  • Select an option

  • Save davekennewell/3f0e25740c676df64d8b74f05f26b70e to your computer and use it in GitHub Desktop.

Select an option

Save davekennewell/3f0e25740c676df64d8b74f05f26b70e to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>run_anuga Architecture Review &amp; Recommendations</title>
<style>
:root {
--bg: #ffffff;
--bg-alt: #f7f8fa;
--bg-code: #f0f2f5;
--border: #d0d7de;
--text: #1f2328;
--text-muted: #59636e;
--heading: #0d1117;
--accent: #0969da;
--accent-light: #ddf4ff;
--green: #1a7f37;
--green-bg: #dafbe1;
--orange: #9a6700;
--orange-bg: #fff8c5;
--red: #cf222e;
--red-bg: #ffebe9;
--purple: #8250df;
--purple-bg: #fbefff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: var(--text);
background: var(--bg);
max-width: 960px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
h1 {
font-size: 2rem;
color: var(--heading);
border-bottom: 2px solid var(--border);
padding-bottom: 0.5rem;
margin-bottom: 0.25rem;
}
h2 {
font-size: 1.5rem;
color: var(--heading);
margin-top: 2.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.35rem;
border-bottom: 1px solid var(--border);
counter-increment: section;
}
h2::before {
content: counter(section) ". ";
color: var(--accent);
}
h3 {
font-size: 1.15rem;
color: var(--heading);
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
p { margin-bottom: 0.75rem; }
ul, ol {
margin-bottom: 0.75rem;
padding-left: 1.75rem;
}
li { margin-bottom: 0.35rem; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.9em;
background: var(--bg-code);
padding: 0.15em 0.35em;
border-radius: 4px;
}
pre {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.25rem;
overflow-x: auto;
margin-bottom: 1rem;
line-height: 1.5;
}
pre code {
background: none;
padding: 0;
font-size: 0.85rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 0.95rem;
}
th, td {
border: 1px solid var(--border);
padding: 0.5rem 0.75rem;
text-align: left;
vertical-align: top;
}
th {
background: var(--bg-alt);
font-weight: 600;
color: var(--heading);
}
tr:nth-child(even) { background: var(--bg-alt); }
.subtitle {
color: var(--text-muted);
font-size: 1rem;
margin-bottom: 2rem;
}
.callout {
border-left: 4px solid var(--accent);
background: var(--accent-light);
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border-radius: 0 6px 6px 0;
}
.callout-warn {
border-left-color: var(--orange);
background: var(--orange-bg);
}
.callout-danger {
border-left-color: var(--red);
background: var(--red-bg);
}
.callout-success {
border-left-color: var(--green);
background: var(--green-bg);
}
.tag {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
padding: 0.15em 0.5em;
border-radius: 12px;
vertical-align: middle;
}
.tag-tier0 { background: var(--green-bg); color: var(--green); }
.tag-tier1 { background: var(--accent-light); color: var(--accent); }
.tag-tier2 { background: var(--orange-bg); color: var(--orange); }
.tag-tier3 { background: var(--purple-bg); color: var(--purple); }
.phase-header {
display: inline-block;
font-weight: 700;
padding: 0.2em 0.6em;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.phase-1 { background: var(--green-bg); color: var(--green); }
.phase-2 { background: var(--accent-light); color: var(--accent); }
.phase-3 { background: var(--purple-bg); color: var(--purple); }
details {
margin-bottom: 1rem;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
details summary {
padding: 0.65rem 1rem;
background: var(--bg-alt);
cursor: pointer;
font-weight: 600;
user-select: none;
}
details summary:hover { background: #eef1f5; }
details[open] summary { border-bottom: 1px solid var(--border); }
details .details-body { padding: 1rem; }
.kw { color: #cf222e; font-weight: 600; }
.fn { color: #8250df; }
.str { color: #0a3069; }
.cmt { color: #6e7781; font-style: italic; }
.cls { color: #953800; }
.dec { color: #6639ba; }
.typ { color: #0550ae; }
.num { color: #0550ae; }
.toc {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.25rem;
margin-bottom: 2rem;
}
.toc h3 { margin-top: 0; }
.toc ol { margin-bottom: 0; }
.toc li { margin-bottom: 0.2rem; }
@media print {
body { max-width: 100%; padding: 1rem; font-size: 12pt; }
details { border: none; }
details[open] summary { display: none; }
details .details-body { padding: 0; }
h2 { page-break-before: auto; }
pre { white-space: pre-wrap; word-break: break-all; }
}
body { counter-reset: section; }
</style>
</head>
<body>
<h1>run_anuga Architecture Review &amp; Recommendations</h1>
<p class="subtitle">Restructuring plan for standalone distribution &mdash; February 2026</p>
<nav class="toc">
<h3>Contents</h3>
<ol>
<li><a href="#executive-summary">Executive Summary</a></li>
<li><a href="#current-problems">Current Architecture Problems</a></li>
<li><a href="#proposed-structure">Proposed Package Structure</a></li>
<li><a href="#dependency-tiers">Dependency Tiers</a></li>
<li><a href="#callbacks">Callback Architecture</a></li>
<li><a href="#cli">CLI Redesign</a></li>
<li><a href="#windows">Windows Distribution Strategy</a></li>
<li><a href="#testing">Testing Strategy</a></li>
<li><a href="#config">Configuration Improvements</a></li>
<li><a href="#roadmap">Implementation Roadmap</a></li>
</ol>
</nav>
<!-- ================================================================== -->
<h2 id="executive-summary">Executive Summary</h2>
<p>
The <code>run_anuga</code> package is a functional but tightly coupled simulation runner that grew organically inside the Hydrata web platform. Its two main files (<code>run.py</code> at ~300 lines and <code>run_utils.py</code> at ~1,100 lines) carry hard dependencies on Django, Celery, boto3, and the Hydrata REST API, making it impossible for a civil engineer to install and run a flood simulation without standing up the entire web stack. The monolithic <code>run_sim()</code> function interleaves mesh generation, domain setup, simulation execution, post-processing, and platform callbacks into a single 250-line control flow with no clear phase boundaries.
</p>
<p>
The proposed restructuring introduces <strong>four dependency tiers</strong> (Core, Geo, Sim, Platform), a <strong>callback protocol</strong> to decouple progress reporting from HTTP calls, and a <strong>subpackage layout</strong> that separates preprocessing, simulation, and postprocessing into independently importable modules. The result is a package that installs with <code>pip install run_anuga</code> for config validation, adds GDAL/shapely via <code>run_anuga[geo]</code> for mesh and GeoTIFF work, and only pulls in the Hydrata integration layer when explicitly requested. Distribution to Windows users without admin rights is achievable via <strong>conda-forge</strong> through miniforge.
</p>
<!-- ================================================================== -->
<h2 id="current-problems">Current Architecture Problems</h2>
<p>The following issues were identified across eight research threads examining the codebase, dependency graph, configuration schema, test suite, and distribution constraints.</p>
<table>
<thead>
<tr><th style="width:3rem">#</th><th>Problem</th><th>Severity</th><th>Impact</th></tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><strong>Monolithic <code>run_sim()</code></strong> &mdash; 250 lines, mixes mesh generation, domain config, boundary setup, simulation loop, post-processing, and web callbacks in a single function</td>
<td><span class="tag tag-tier3">High</span></td>
<td>Cannot unit-test stages independently; any change risks regression across all phases</td>
</tr>
<tr>
<td>2</td>
<td><strong>Hard coupling to Hydrata platform</strong> &mdash; celery, django, boto3, requests are unconditional imports; package cannot be imported without them installed</td>
<td><span class="tag tag-tier3">High</span></td>
<td>Standalone use impossible; <code>pip install run_anuga</code> pulls in Django</td>
</tr>
<tr>
<td>3</td>
<td><strong>All heavy deps required at import time</strong> &mdash; GDAL, numpy, anuga, opencv, rasterio, matplotlib imported at module level even to parse config</td>
<td><span class="tag tag-tier3">High</span></td>
<td>Cannot validate a scenario JSON without compiling GDAL from source</td>
</tr>
<tr>
<td>4</td>
<td><strong>No phase separation</strong> &mdash; preprocessing (input data, mesh), simulation, and postprocessing (GeoTIFF, video) are interleaved in <code>run_sim()</code></td>
<td><span class="tag tag-tier2">Medium</span></td>
<td>Cannot re-run post-processing without re-running the simulation</td>
</tr>
<tr>
<td>5</td>
<td><strong>Credentials as CLI positional args</strong> &mdash; username, password, API token passed as positional arguments, visible in <code>ps aux</code></td>
<td><span class="tag tag-tier3">High</span></td>
<td>Security risk on shared servers; violates best practice for credential handling</td>
</tr>
<tr>
<td>6</td>
<td><strong>Hardcoded server paths</strong> &mdash; <code>/opt/venv/hydrata/bin/python</code> baked into code</td>
<td><span class="tag tag-tier2">Medium</span></td>
<td>Breaks on any non-Hydrata server or on Windows</td>
</tr>
<tr>
<td>7</td>
<td><strong>Import side effects</strong> &mdash; <code>S3StacIO</code> instantiated at module load, establishes AWS session on import</td>
<td><span class="tag tag-tier2">Medium</span></td>
<td>Import fails without AWS credentials even if S3 is never used</td>
</tr>
<tr>
<td>8</td>
<td><strong><code>update_web_interface()</code> called 7 times</strong> &mdash; each call re-parses <code>scenario.json</code> from disk</td>
<td><span class="tag tag-tier1">Low</span></td>
<td>Unnecessary I/O; couples simulation flow to HTTP reporting</td>
</tr>
<tr>
<td>9</td>
<td><strong>Bug in <code>setup_logger</code></strong> &mdash; <code>if not username and password:</code> should be <code>if not username and not password:</code></td>
<td><span class="tag tag-tier2">Medium</span></td>
<td>Logger setup fails silently when only password is missing</td>
</tr>
<tr>
<td>10</td>
<td><strong>Missing test fixture</strong> &mdash; integration test references a zip file that does not exist in the repo</td>
<td><span class="tag tag-tier1">Low</span></td>
<td>Integration test always skips or fails</td>
</tr>
<tr>
<td>11</td>
<td><strong><code>store_mesh</code> field used but not in schema</strong> &mdash; code reads <code>scenario.get("store_mesh")</code> but JSON Schema does not define it</td>
<td><span class="tag tag-tier1">Low</span></td>
<td>Validation passes configs that may behave unexpectedly</td>
</tr>
<tr>
<td>12</td>
<td><strong>Platform IDs required for output naming</strong> &mdash; <code>id</code>, <code>project</code>, <code>run_id</code> used to construct output directory paths even in non-platform contexts</td>
<td><span class="tag tag-tier2">Medium</span></td>
<td>Standalone users must invent dummy IDs; directory names are meaningless without Hydrata</td>
</tr>
</tbody>
</table>
<!-- ================================================================== -->
<h2 id="proposed-structure">Proposed Package Structure</h2>
<pre><code>run_anuga/
__init__.py <span class="cmt"># Version string, Tier 0 exports only</span>
config.py <span class="cmt"># ScenarioConfig dataclass/pydantic, load/validate</span>
defaults.py <span class="cmt"># Named constants (already exists)</span>
schema.py <span class="cmt"># JSON Schema definition (already exists)</span>
callbacks.py <span class="cmt"># SimulationCallback protocol + LoggingCallback</span>
_compat.py <span class="cmt"># import_optional_dependency() helper</span>
preprocessing/
__init__.py
input_data.py <span class="cmt"># setup_input_data() - parse scenario + load files</span>
mesh.py <span class="cmt"># create_anuga_mesh(), create_mesher_mesh()</span>
boundaries.py <span class="cmt"># create_boundary_polygon_from_boundaries()</span>
simulation/
__init__.py
runner.py <span class="cmt"># run_sim() - orchestrate simulation phases</span>
domain.py <span class="cmt"># Domain setup, boundary conditions, inflows</span>
postprocessing/
__init__.py
geotiff.py <span class="cmt"># post_process_sww(), Make_Geotif calls</span>
video.py <span class="cmt"># make_video(), make_comparison_video()</span>
cli/
__init__.py
main.py <span class="cmt"># Click group: run, validate, info, post, capabilities</span>
integrations/
__init__.py
hydrata.py <span class="cmt"># HydrataCallback, update_web_interface,</span>
<span class="cmt"># S3StacIO, generate_stac</span></code></pre>
<h3>Design Principles</h3>
<ul>
<li><strong>Leaf imports only.</strong> Each subpackage imports only what it needs. <code>run_anuga/__init__.py</code> exposes Tier 0 objects only; Tier 1+ symbols require explicit subpackage imports.</li>
<li><strong>Lazy heavy imports.</strong> The <code>_compat.py</code> module provides <code>import_optional_dependency(name, tier, extra)</code> that raises a clear error message telling the user which pip extra to install.</li>
<li><strong>No import side effects.</strong> All module-level code is definitions only. No network calls, no file I/O, no object construction at import time.</li>
<li><strong>Phase boundaries.</strong> Preprocessing outputs a data structure that simulation consumes; simulation outputs SWW files that postprocessing consumes. Each phase can be run independently.</li>
</ul>
<!-- ================================================================== -->
<h2 id="dependency-tiers">Dependency Tiers</h2>
<table>
<thead>
<tr>
<th>Tier</th>
<th>Name</th>
<th>Dependencies</th>
<th>What It Enables</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="tag tag-tier0">Tier 0</span></td>
<td>Core</td>
<td>None (pure Python stdlib + <code>jsonschema</code>)</td>
<td>Config parsing, validation, schema, defaults, CLI <code>validate</code> and <code>info</code> commands</td>
</tr>
<tr>
<td><span class="tag tag-tier1">Tier 1</span></td>
<td>Geo</td>
<td>GDAL, shapely, rasterio, pyproj, fiona</td>
<td>Mesh generation, boundary polygon processing, GeoTIFF postprocessing</td>
</tr>
<tr>
<td><span class="tag tag-tier2">Tier 2</span></td>
<td>Sim</td>
<td>anuga, dill, psutil, numpy, pandas, matplotlib, opencv-python</td>
<td>Running ANUGA simulations, SWW output, video generation</td>
</tr>
<tr>
<td><span class="tag tag-tier3">Tier 3</span></td>
<td>Platform</td>
<td>requests, boto3, pystac, celery, django</td>
<td>Hydrata web integration, S3 uploads, STAC catalog generation</td>
</tr>
</tbody>
</table>
<h3>pyproject.toml Extras</h3>
<pre><code><span class="cmt"># pyproject.toml</span>
[project.optional-dependencies]
geo = [<span class="str">"GDAL"</span>, <span class="str">"shapely>=2.0"</span>, <span class="str">"rasterio"</span>, <span class="str">"pyproj"</span>, <span class="str">"fiona"</span>]
sim = [<span class="str">"run_anuga[geo]"</span>, <span class="str">"anuga_core"</span>, <span class="str">"dill"</span>, <span class="str">"psutil"</span>, <span class="str">"numpy"</span>,
<span class="str">"pandas"</span>, <span class="str">"matplotlib"</span>, <span class="str">"opencv-python-headless"</span>]
hydrata = [<span class="str">"run_anuga[sim]"</span>, <span class="str">"requests"</span>, <span class="str">"boto3"</span>, <span class="str">"pystac"</span>]
full = [<span class="str">"run_anuga[hydrata]"</span>]
dev = [<span class="str">"pytest"</span>, <span class="str">"ruff"</span>, <span class="str">"coverage"</span>, <span class="str">"mypy"</span>]</code></pre>
<h3>Installation Examples</h3>
<pre><code><span class="cmt"># Civil engineer: just want to validate a scenario file</span>
pip install run_anuga
run-anuga validate ./my_project/
<span class="cmt"># Researcher: run simulations locally</span>
pip install run_anuga[sim]
run-anuga run ./my_project/
<span class="cmt"># Hydrata server: full integration</span>
pip install run_anuga[full]</code></pre>
<div class="callout">
<strong>Why self-referencing extras?</strong> The <code>sim</code> extra includes <code>run_anuga[geo]</code>, so users never need to remember that simulation requires geo dependencies. pip resolves the chain automatically.
</div>
<!-- ================================================================== -->
<h2 id="callbacks">Callback Architecture</h2>
<p>The current codebase calls <code>update_web_interface()</code> seven times throughout <code>run.py</code>, each time re-reading <code>scenario.json</code> from disk and making an HTTP PATCH to the Hydrata control server. This tightly couples the simulation flow to the platform. The callback protocol replaces all seven call sites with a single injected dependency.</p>
<h3>Protocol Definition</h3>
<pre><code><span class="cmt"># run_anuga/callbacks.py</span>
<span class="kw">from</span> typing <span class="kw">import</span> Protocol, runtime_checkable
<span class="dec">@runtime_checkable</span>
<span class="kw">class</span> <span class="cls">SimulationCallback</span>(Protocol):
<span class="str">"""Interface for receiving simulation lifecycle events."""</span>
<span class="kw">def</span> <span class="fn">on_status</span>(self, status: <span class="typ">str</span>) -> <span class="typ">None</span>:
<span class="str">"""Phase change: 'preprocessing', 'meshing', 'running', 'postprocessing'."""</span>
...
<span class="kw">def</span> <span class="fn">on_progress</span>(self, pct: <span class="typ">float</span>, sim_time: <span class="typ">float</span>, wall_time: <span class="typ">float</span>) -> <span class="typ">None</span>:
<span class="str">"""Progress update during simulation. pct in [0, 100]."""</span>
...
<span class="kw">def</span> <span class="fn">on_mesh_created</span>(self, triangle_count: <span class="typ">int</span>) -> <span class="typ">None</span>:
<span class="str">"""Mesh generation complete."""</span>
...
<span class="kw">def</span> <span class="fn">on_complete</span>(self, metadata: <span class="typ">dict</span>) -> <span class="typ">None</span>:
<span class="str">"""Simulation finished successfully. metadata contains output paths."""</span>
...
<span class="kw">def</span> <span class="fn">on_error</span>(self, error: <span class="typ">Exception</span>) -> <span class="typ">None</span>:
<span class="str">"""Simulation failed."""</span>
...</code></pre>
<h3>Built-in Implementations</h3>
<table>
<thead>
<tr><th>Class</th><th>Location</th><th>Behavior</th></tr>
</thead>
<tbody>
<tr>
<td><code>LoggingCallback</code></td>
<td><code>run_anuga/callbacks.py</code></td>
<td>Default. Writes status and progress to Python <code>logging</code>. No external dependencies.</td>
</tr>
<tr>
<td><code>HydrataCallback</code></td>
<td><code>run_anuga/integrations/hydrata.py</code></td>
<td>HTTP PATCH to <code>control_server</code>. Replaces all 7 <code>update_web_interface()</code> calls. Requires <code>requests</code>.</td>
</tr>
<tr>
<td><em>User-defined</em></td>
<td>Any module</td>
<td>Users implement the protocol for custom dashboards, Slack notifications, database logging, etc.</td>
</tr>
</tbody>
</table>
<h3>Usage in Runner</h3>
<pre><code><span class="cmt"># run_anuga/simulation/runner.py</span>
<span class="kw">from</span> run_anuga.callbacks <span class="kw">import</span> SimulationCallback, LoggingCallback
<span class="kw">def</span> <span class="fn">run_sim</span>(
config: ScenarioConfig,
callback: SimulationCallback | <span class="typ">None</span> = <span class="typ">None</span>,
) -> SimulationResult:
cb = callback <span class="kw">or</span> LoggingCallback()
cb.on_status(<span class="str">"preprocessing"</span>)
input_data = setup_input_data(config)
cb.on_status(<span class="str">"meshing"</span>)
mesh = create_mesh(input_data)
cb.on_mesh_created(mesh.triangle_count)
cb.on_status(<span class="str">"running"</span>)
<span class="kw">for</span> progress <span class="kw">in</span> run_simulation(mesh, input_data):
cb.on_progress(progress.pct, progress.sim_time, progress.wall_time)
cb.on_status(<span class="str">"postprocessing"</span>)
result = post_process(config, input_data)
cb.on_complete(result.metadata)
<span class="kw">return</span> result</code></pre>
<div class="callout callout-success">
<strong>Key benefit:</strong> The simulation runner has zero knowledge of HTTP, Celery, or Django. All platform integration lives in <code>HydrataCallback</code>, which is injected by the Celery task wrapper, not by the runner itself.
</div>
<!-- ================================================================== -->
<h2 id="cli">CLI Redesign</h2>
<p>The current CLI accepts credentials as positional arguments, which are visible in process listings. The redesigned CLI uses <a href="https://click.palletsprojects.com/">Click</a> with subcommands, environment variables for credentials, and tiered availability so that commands gracefully degrade based on installed dependencies.</p>
<h3>Subcommand Structure</h3>
<table>
<thead>
<tr><th>Command</th><th>Tier</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>run-anuga validate &lt;package_dir&gt;</code></td>
<td><span class="tag tag-tier0">Tier 0</span></td>
<td>Validate scenario.json against schema. Reports errors with line numbers. Always works.</td>
</tr>
<tr>
<td><code>run-anuga info &lt;package_dir&gt;</code></td>
<td><span class="tag tag-tier0">Tier 0</span></td>
<td>Print config summary: extents, resolution, duration, boundary count, friction zones.</td>
</tr>
<tr>
<td><code>run-anuga run &lt;package_dir&gt;</code></td>
<td><span class="tag tag-tier2">Tier 2</span></td>
<td>Run the full simulation. Requires ANUGA and geo deps.</td>
</tr>
<tr>
<td><code>run-anuga post &lt;package_dir&gt;</code></td>
<td><span class="tag tag-tier1">Tier 1</span></td>
<td>Post-process existing SWW files to GeoTIFF. Does not re-run simulation.</td>
</tr>
<tr>
<td><code>run-anuga capabilities</code></td>
<td><span class="tag tag-tier0">Tier 0</span></td>
<td>Show installed tiers and available commands. Helps users diagnose missing deps.</td>
</tr>
</tbody>
</table>
<h3>Credential Handling</h3>
<pre><code><span class="cmt"># Environment variables (recommended)</span>
export RUN_ANUGA_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
export RUN_ANUGA_CONTROL_SERVER=https://hydrata.com
run-anuga run ./my_project/
<span class="cmt"># Config file (~/.config/run-anuga/config.toml)</span>
[hydrata]
api_token = "eyJhbGci..."
control_server = "https://hydrata.com"
<span class="cmt"># Precedence: CLI flags > env vars > config file > none (standalone mode)</span></code></pre>
<div class="callout callout-warn">
<strong>Never as positional args.</strong> Credentials passed as CLI arguments appear in <code>ps aux</code>, <code>/proc/*/cmdline</code>, shell history, and process monitoring tools. Environment variables are hidden from other users on the same system and are the standard approach for CI/CD and container deployments.
</div>
<h3>Capabilities Output Example</h3>
<pre><code>$ run-anuga capabilities
run_anuga 1.0.0
Installed tiers:
[x] Tier 0 - Core (config, validation, schema)
[x] Tier 1 - Geo (GDAL 3.8.4, shapely 2.0.3, rasterio 1.3.9)
[x] Tier 2 - Sim (anuga 3.2.0, numpy 1.26.4)
[ ] Tier 3 - Platform (not installed - pip install run_anuga[hydrata])
Available commands:
validate [x]
info [x]
run [x]
post [x]
capabilities [x]</code></pre>
<!-- ================================================================== -->
<h2 id="windows">Windows Distribution Strategy</h2>
<div class="callout callout-success">
<strong>Recommendation: conda-forge via miniforge.</strong> This is the only distribution channel that satisfies all constraints: no admin rights, native GDAL/ANUGA binaries, and a simple install path for non-technical users.
</div>
<h3>Why conda-forge</h3>
<table>
<thead>
<tr><th>Requirement</th><th>pip (PyPI)</th><th>conda-forge</th><th>Docker</th></tr>
</thead>
<tbody>
<tr>
<td>No admin rights needed</td>
<td>Yes</td>
<td>Yes (miniforge user-install)</td>
<td>No (Docker Desktop requires admin)</td>
</tr>
<tr>
<td>GDAL binary included</td>
<td>No (must compile from source)</td>
<td>Yes (pre-built)</td>
<td>Yes (in image)</td>
</tr>
<tr>
<td>anuga_core available</td>
<td>Partial (needs compiler)</td>
<td>Yes (conda-forge package)</td>
<td>Yes (in image)</td>
</tr>
<tr>
<td>Simple for civil engineers</td>
<td>No (C compiler, GDAL headers)</td>
<td>Yes (2 commands)</td>
<td>No (Docker concepts required)</td>
</tr>
<tr>
<td>Works behind corporate proxy</td>
<td>Yes (with pip config)</td>
<td>Yes (with conda config)</td>
<td>Difficult</td>
</tr>
<tr>
<td>Offline install possible</td>
<td>Difficult</td>
<td>Yes (conda-pack)</td>
<td>Yes (image export)</td>
</tr>
</tbody>
</table>
<h3>End-User Install Instructions</h3>
<pre><code><span class="cmt"># Step 1: Download miniforge installer (no admin needed)</span>
<span class="cmt"># https://github.com/conda-forge/miniforge/releases</span>
<span class="cmt"># Run the .exe installer, accept defaults (installs to %LOCALAPPDATA%)</span>
<span class="cmt"># Step 2: Open Miniforge Prompt (from Start Menu)</span>
<span class="cmt"># Step 3: Create environment and install</span>
conda create -n flood python=3.12 run_anuga -y
conda activate flood
<span class="cmt"># Step 4: Run a simulation</span>
run-anuga validate C:\Projects\my_scenario\
run-anuga run C:\Projects\my_scenario\</code></pre>
<h3>Fallback: Pre-built Docker Image</h3>
<p>For organizations that already have Docker Desktop deployed, provide a pre-built image:</p>
<pre><code>docker pull ghcr.io/hydrata/run-anuga:latest
docker run -v C:\Projects\my_scenario:/data run-anuga run /data</code></pre>
<p>This is a secondary option because Docker Desktop requires admin rights to install and introduces container concepts that are unfamiliar to most civil engineers.</p>
<!-- ================================================================== -->
<h2 id="testing">Testing Strategy</h2>
<h3>Test Pyramid by Tier</h3>
<table>
<thead>
<tr><th>Tier</th><th>Test Scope</th><th>Runner</th><th>CI Trigger</th><th>Est. Count</th></tr>
</thead>
<tbody>
<tr>
<td><span class="tag tag-tier0">Tier 0</span></td>
<td>Config parsing, schema validation, defaults, boundary math, CLI validate/info</td>
<td>pytest, no native deps</td>
<td>Every push, all platforms</td>
<td>~40 tests</td>
</tr>
<tr>
<td><span class="tag tag-tier1">Tier 1</span></td>
<td>Mesh generation, GeoTIFF output, boundary polygon creation</td>
<td>pytest + GDAL (conda CI)</td>
<td>Every push, Linux + Windows matrix</td>
<td>~20 tests</td>
</tr>
<tr>
<td><span class="tag tag-tier2">Tier 2</span></td>
<td>Full simulation integration, domain setup, SWW output</td>
<td>pytest + ANUGA (self-hosted or manual)</td>
<td>Nightly or manual trigger</td>
<td>~10 tests</td>
</tr>
<tr>
<td><span class="tag tag-tier3">Tier 3</span></td>
<td>Hydrata callback, S3 upload, STAC generation (mocked HTTP)</td>
<td>pytest + responses/moto</td>
<td>Every push (deps are mockable)</td>
<td>~15 tests</td>
</tr>
</tbody>
</table>
<h3>Current Gaps and Additions Needed</h3>
<ul>
<li><strong><code>setup_input_data()</code></strong> &mdash; no tests. Needs tests for valid scenario parsing, missing fields, malformed JSON, and file-not-found conditions.</li>
<li><strong><code>create_boundary_polygon_from_boundaries()</code></strong> &mdash; pure geometry function with no tests. Test with known polygons, edge cases (collinear points, self-intersecting).</li>
<li><strong><code>make_frictions()</code></strong> &mdash; no tests. Verify friction zone assignment from config.</li>
<li><strong><code>check_coordinates_are_in_polygon()</code></strong> &mdash; no tests. Test with points inside, outside, on boundary.</li>
<li><strong><code>correction_for_polar_quadrants()</code></strong> &mdash; math utility with no tests. Test each quadrant and boundary angles.</li>
<li><strong>Callback protocol compliance</strong> &mdash; test that <code>LoggingCallback</code> and <code>HydrataCallback</code> satisfy the protocol and handle edge cases (network failure, invalid progress values).</li>
</ul>
<h3>Test Fixtures</h3>
<div class="callout callout-warn">
<strong>Current problem:</strong> The integration test references a zip fixture file that does not exist in the repository. Large binary fixtures should not be committed to git.
</div>
<p><strong>Strategy:</strong> Use synthetic, minimal fixtures generated in code.</p>
<pre><code><span class="cmt"># conftest.py</span>
<span class="dec">@pytest.fixture</span>
<span class="kw">def</span> <span class="fn">tiny_dem</span>(tmp_path):
<span class="str">"""Create a 10x10 pixel GeoTIFF DEM for testing."""</span>
<span class="kw">import</span> numpy <span class="kw">as</span> np
dem = np.random.uniform(<span class="num">0</span>, <span class="num">10</span>, (<span class="num">10</span>, <span class="num">10</span>)).astype(np.float32)
path = tmp_path / <span class="str">"dem.tif"</span>
<span class="cmt"># Write with rasterio (Tier 1 test)</span>
<span class="kw">return</span> path
<span class="dec">@pytest.fixture</span>
<span class="kw">def</span> <span class="fn">minimal_scenario</span>(tmp_path, tiny_dem):
<span class="str">"""Create a valid scenario.json with minimal config."""</span>
scenario = {
<span class="str">"name"</span>: <span class="str">"test"</span>,
<span class="str">"dem"</span>: str(tiny_dem),
<span class="str">"duration"</span>: <span class="num">10</span>,
<span class="str">"resolution"</span>: <span class="num">5.0</span>,
<span class="str">"boundaries"</span>: [...]
}
<span class="cmt"># Write scenario.json to tmp_path</span>
<span class="kw">return</span> tmp_path</code></pre>
<h3>CI Configuration</h3>
<pre><code><span class="cmt"># .github/workflows/test.yml</span>
jobs:
tier0:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [<span class="str">"3.10"</span>, <span class="str">"3.11"</span>, <span class="str">"3.12"</span>]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install -e .[dev]
- run: pytest tests/tier0/ -v
tier1:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [<span class="str">"3.10"</span>, <span class="str">"3.12"</span>]
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
- run: conda install gdal shapely rasterio
- run: pip install -e .[geo,dev]
- run: pytest tests/tier1/ -v
tier2-integration:
runs-on: self-hosted
if: github.event_name == 'schedule'
steps:
- uses: actions/checkout@v4
- run: pip install -e .[sim,dev]
- run: pytest tests/tier2/ -v --timeout=300</code></pre>
<!-- ================================================================== -->
<h2 id="config">Configuration Improvements</h2>
<h3>Separate Platform Metadata</h3>
<p>Currently, fields like <code>id</code>, <code>project</code>, <code>run_id</code>, and <code>control_server</code> are mixed in with simulation parameters. These should be isolated into an optional section that standalone users never see.</p>
<pre><code><span class="cmt">// scenario.json - standalone mode (no platform section)</span>
{
<span class="str">"name"</span>: <span class="str">"Springfield Creek 100yr"</span>,
<span class="str">"dem"</span>: <span class="str">"dem.tif"</span>,
<span class="str">"duration_seconds"</span>: <span class="num">3600</span>,
<span class="str">"resolution_meters"</span>: <span class="num">5.0</span>,
<span class="str">"boundaries"</span>: [...],
<span class="str">"friction_zones"</span>: [...]
}
<span class="cmt">// scenario.json - platform mode (optional hydrata section)</span>
{
<span class="str">"name"</span>: <span class="str">"Springfield Creek 100yr"</span>,
<span class="str">"dem"</span>: <span class="str">"dem.tif"</span>,
<span class="str">"duration_seconds"</span>: <span class="num">3600</span>,
<span class="str">"resolution_meters"</span>: <span class="num">5.0</span>,
<span class="str">"boundaries"</span>: [...],
<span class="str">"hydrata"</span>: {
<span class="str">"id"</span>: <span class="num">42</span>,
<span class="str">"project"</span>: <span class="num">7</span>,
<span class="str">"run_id"</span>: <span class="num">123</span>,
<span class="str">"control_server"</span>: <span class="str">"https://hydrata.com"</span>
}
}</code></pre>
<h3>Output Directory Naming</h3>
<table>
<thead>
<tr><th>Mode</th><th>Current</th><th>Proposed</th></tr>
</thead>
<tbody>
<tr>
<td>Platform</td>
<td><code>output/{id}/{project}/{run_id}/</code></td>
<td><code>output/{id}/{project}/{run_id}/</code> (unchanged)</td>
</tr>
<tr>
<td>Standalone</td>
<td><code>output/0/0/0/</code> (dummy IDs)</td>
<td><code>output/{name}_{timestamp}/</code></td>
</tr>
</tbody>
</table>
<h3>Field Naming: Add Explicit Units</h3>
<table>
<thead>
<tr><th>Current</th><th>Proposed</th><th>Rationale</th></tr>
</thead>
<tbody>
<tr><td><code>duration</code></td><td><code>duration_seconds</code></td><td>Ambiguous (seconds? minutes?)</td></tr>
<tr><td><code>resolution</code></td><td><code>resolution_meters</code></td><td>DEM resolution in meters per cell</td></tr>
<tr><td><code>depth_threshold</code></td><td><code>depth_threshold_meters</code></td><td>Minimum water depth</td></tr>
<tr><td><code>velocity_threshold</code></td><td><code>velocity_threshold_mps</code></td><td>Minimum velocity (m/s)</td></tr>
</tbody>
</table>
<h3>Schema Fixes</h3>
<ul>
<li>Add <code>store_mesh</code> field to schema (boolean, used in code but not validated).</li>
<li>Mark <code>hydrata</code> section as <code>"required": false</code> with its own sub-schema.</li>
<li>Add <code>"additionalProperties": false</code> at the top level to catch typos.</li>
</ul>
<h3>Validation Library</h3>
<div class="callout">
<strong>Consider migrating from <code>jsonschema</code> to <code>pydantic</code>.</strong> Pydantic provides better error messages (field path + expected type vs. cryptic JSON Schema errors), runtime type coercion (string "5" to int 5), and generates the JSON Schema automatically from the dataclass. The existing <code>schema.py</code> can be kept for backward compatibility and auto-generated from the pydantic model.
</div>
<pre><code><span class="cmt"># run_anuga/config.py</span>
<span class="kw">from</span> pydantic <span class="kw">import</span> BaseModel, Field
<span class="kw">from</span> typing <span class="kw">import</span> Optional
<span class="kw">from</span> pathlib <span class="kw">import</span> Path
<span class="kw">class</span> <span class="cls">HydrataConfig</span>(BaseModel):
id: <span class="typ">int</span>
project: <span class="typ">int</span>
run_id: <span class="typ">int</span>
control_server: <span class="typ">str</span>
<span class="kw">class</span> <span class="cls">ScenarioConfig</span>(BaseModel):
name: <span class="typ">str</span>
dem: Path
duration_seconds: <span class="typ">float</span> = Field(gt=<span class="num">0</span>, description=<span class="str">"Simulation duration in seconds"</span>)
resolution_meters: <span class="typ">float</span> = Field(gt=<span class="num">0</span>, description=<span class="str">"Mesh resolution in meters"</span>)
store_mesh: <span class="typ">bool</span> = <span class="num">False</span>
boundaries: list[BoundaryConfig]
friction_zones: list[FrictionConfig] = []
hydrata: Optional[HydrataConfig] = <span class="typ">None</span>
<span class="dec">@property</span>
<span class="kw">def</span> <span class="fn">is_standalone</span>(self) -> <span class="typ">bool</span>:
<span class="kw">return</span> self.hydrata <span class="kw">is</span> <span class="typ">None</span></code></pre>
<!-- ================================================================== -->
<h2 id="roadmap">Implementation Roadmap</h2>
<p>The restructuring is divided into three phases. Each phase produces a working, releasable package. Phase boundaries are designed so that the Hydrata platform integration continues to work throughout &mdash; no big-bang migration.</p>
<!-- Phase 1 -->
<h3><span class="phase-header phase-1">Phase 1: Breaking the Coupling</span></h3>
<p><em>Estimated effort: 3&ndash;5 days. Goal: make <code>run_anuga</code> importable without Django/Celery.</em></p>
<table>
<thead>
<tr><th style="width:2.5rem">#</th><th>Task</th><th>Details</th></tr>
</thead>
<tbody>
<tr>
<td>1.1</td>
<td>Extract callback protocol</td>
<td>Create <code>callbacks.py</code> with <code>SimulationCallback</code> protocol and <code>LoggingCallback</code>. Replace all 7 <code>update_web_interface()</code> calls with <code>callback.on_*()</code>. Move <code>HydrataCallback</code> to <code>integrations/hydrata.py</code>.</td>
</tr>
<tr>
<td>1.2</td>
<td>Move platform deps to optional extras</td>
<td>Update <code>pyproject.toml</code> with <code>[geo]</code>, <code>[sim]</code>, <code>[hydrata]</code>, <code>[full]</code> extras. Remove hard requirements on requests, boto3, celery, django.</td>
</tr>
<tr>
<td>1.3</td>
<td>Guard all heavy imports</td>
<td>Create <code>_compat.py</code> with <code>import_optional_dependency()</code>. Replace all top-level imports of GDAL, anuga, rasterio, etc. with lazy imports at point of use. Error message tells user which extra to install.</td>
</tr>
<tr>
<td>1.4</td>
<td>Fix <code>setup_logger</code> bug</td>
<td>Change <code>if not username and password:</code> to <code>if not username and not password:</code>.</td>
</tr>
<tr>
<td>1.5</td>
<td>Remove hardcoded paths</td>
<td>Replace <code>/opt/venv/hydrata/bin/python</code> with <code>sys.executable</code>. Remove all absolute path assumptions.</td>
</tr>
<tr>
<td>1.6</td>
<td>Remove import side effects</td>
<td>Move <code>S3StacIO</code> instantiation into a function. Make it lazy-initialized on first use within <code>HydrataCallback</code>.</td>
</tr>
</tbody>
</table>
<div class="callout callout-success">
<strong>Phase 1 deliverable:</strong> <code>pip install run_anuga</code> succeeds with zero native dependencies. <code>from run_anuga import ScenarioConfig</code> works on any machine. Hydrata platform continues to work by installing <code>run_anuga[full]</code>.
</div>
<!-- Phase 2 -->
<h3><span class="phase-header phase-2">Phase 2: Restructure</span></h3>
<p><em>Estimated effort: 5&ndash;8 days. Goal: clean module boundaries, decomposed <code>run_sim()</code>, Click CLI.</em></p>
<table>
<thead>
<tr><th style="width:2.5rem">#</th><th>Task</th><th>Details</th></tr>
</thead>
<tbody>
<tr>
<td>2.1</td>
<td>Split <code>run_utils.py</code> into subpackages</td>
<td>Move functions into <code>preprocessing/</code> (input_data, mesh, boundaries), <code>postprocessing/</code> (geotiff, video). Maintain backward-compatible imports in <code>run_utils.py</code> via re-exports (deprecation warnings).</td>
</tr>
<tr>
<td>2.2</td>
<td>Decompose <code>run_sim()</code></td>
<td>Break the 250-line function into: <code>preprocess() -> PreprocessResult</code>, <code>simulate(PreprocessResult) -> SimResult</code>, <code>postprocess(SimResult) -> OutputPaths</code>. Each returns a typed data structure.</td>
</tr>
<tr>
<td>2.3</td>
<td>Add Click CLI</td>
<td>Implement <code>cli/main.py</code> with <code>validate</code>, <code>info</code>, <code>run</code>, <code>post</code>, <code>capabilities</code> subcommands. Register as <code>[project.scripts] run-anuga = "run_anuga.cli.main:cli"</code>.</td>
</tr>
<tr>
<td>2.4</td>
<td>Add unit tests for pure functions</td>
<td>Write tests for <code>create_boundary_polygon_from_boundaries()</code>, <code>make_frictions()</code>, <code>check_coordinates_are_in_polygon()</code>, <code>correction_for_polar_quadrants()</code>, <code>setup_input_data()</code>. Create synthetic fixtures.</td>
</tr>
<tr>
<td>2.5</td>
<td>Config improvements</td>
<td>Separate <code>hydrata</code> section in config. Add <code>store_mesh</code> to schema. Add explicit units to field names (backward-compat aliases). Consider pydantic migration.</td>
</tr>
<tr>
<td>2.6</td>
<td>Standalone output directory naming</td>
<td>When <code>hydrata</code> config section is absent, name output directory as <code>{scenario_name}_{YYYYMMDD_HHMMSS}/</code> instead of requiring dummy platform IDs.</td>
</tr>
</tbody>
</table>
<div class="callout">
<strong>Phase 2 deliverable:</strong> Clean package structure with independently testable modules. CLI works for standalone users. Hydrata integration via <code>HydrataCallback</code> injection in Celery task.
</div>
<!-- Phase 3 -->
<h3><span class="phase-header phase-3">Phase 3: Distribution</span></h3>
<p><em>Estimated effort: 3&ndash;5 days. Goal: civil engineers can install and run on Windows.</em></p>
<table>
<thead>
<tr><th style="width:2.5rem">#</th><th>Task</th><th>Details</th></tr>
</thead>
<tbody>
<tr>
<td>3.1</td>
<td>Publish to PyPI</td>
<td>Set up trusted publisher workflow in GitHub Actions. Publish <code>run_anuga</code> package with all extras. Tier 0 install works on PyPI alone.</td>
</tr>
<tr>
<td>3.2</td>
<td>Create conda-forge recipe</td>
<td>Submit feedstock to conda-forge. Define dependencies per tier. Test on Windows, macOS, Linux.</td>
</tr>
<tr>
<td>3.3</td>
<td>Write installation guide</td>
<td>Step-by-step guide for Windows users: download miniforge, create environment, install, run first simulation. Include screenshots.</td>
</tr>
<tr>
<td>3.4</td>
<td>Docker image (optional)</td>
<td>Multi-stage Dockerfile with all tiers pre-installed. Publish to ghcr.io. Document volume mounting for input/output.</td>
</tr>
<tr>
<td>3.5</td>
<td>Example scenarios</td>
<td>Ship 2&ndash;3 small example scenarios in the repo (<code>examples/</code> directory) with README. One tiny (10-second sim for testing), one realistic (demonstrates all features).</td>
</tr>
</tbody>
</table>
<div class="callout callout-success">
<strong>Phase 3 deliverable:</strong> A civil engineer on a corporate Windows laptop can run <code>conda install run_anuga</code> and execute a flood simulation without admin rights, without Docker, and without any knowledge of the Hydrata platform.
</div>
<!-- Timeline summary -->
<h3>Timeline Summary</h3>
<table>
<thead>
<tr><th>Phase</th><th>Duration</th><th>Key Milestone</th></tr>
</thead>
<tbody>
<tr>
<td><span class="phase-header phase-1">Phase 1</span></td>
<td>3&ndash;5 days</td>
<td><code>pip install run_anuga</code> works with zero native deps</td>
</tr>
<tr>
<td><span class="phase-header phase-2">Phase 2</span></td>
<td>5&ndash;8 days</td>
<td>Clean module boundaries, Click CLI, unit tests for pure functions</td>
</tr>
<tr>
<td><span class="phase-header phase-3">Phase 3</span></td>
<td>3&ndash;5 days</td>
<td>PyPI + conda-forge + Windows installation guide</td>
</tr>
<tr style="font-weight:600">
<td>Total</td>
<td>11&ndash;18 days</td>
<td>Standalone, distributable <code>run_anuga</code> package</td>
</tr>
</tbody>
</table>
<hr style="margin-top: 3rem; border: none; border-top: 1px solid var(--border);">
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 1rem;">
Generated February 2026. Based on analysis of the <code>run_anuga</code> package on the <code>5.x</code> branch across eight research threads covering architecture, dependencies, testing, configuration, distribution, and Windows compatibility.
</p>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment