Created
February 21, 2026 09:13
-
-
Save davekennewell/3f0e25740c676df64d8b74f05f26b70e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 & 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 & Recommendations</h1> | |
| <p class="subtitle">Restructuring plan for standalone distribution — 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> — 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> — 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> — 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> — 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> — 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> — <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> — <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> — 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> — <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> — 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> — 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> — <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 <package_dir></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 <package_dir></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 <package_dir></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 <package_dir></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> — 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> — pure geometry function with no tests. Test with known polygons, edge cases (collinear points, self-intersecting).</li> | |
| <li><strong><code>make_frictions()</code></strong> — no tests. Verify friction zone assignment from config.</li> | |
| <li><strong><code>check_coordinates_are_in_polygon()</code></strong> — no tests. Test with points inside, outside, on boundary.</li> | |
| <li><strong><code>correction_for_polar_quadrants()</code></strong> — math utility with no tests. Test each quadrant and boundary angles.</li> | |
| <li><strong>Callback protocol compliance</strong> — 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 — 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–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–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–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–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–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–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–5 days</td> | |
| <td>PyPI + conda-forge + Windows installation guide</td> | |
| </tr> | |
| <tr style="font-weight:600"> | |
| <td>Total</td> | |
| <td>11–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