<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://tomaskozak.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tomaskozak.com/" rel="alternate" type="text/html" /><updated>2026-10-05T20:36:11+00:00</updated><id>https://tomaskozak.com/feed.xml</id><title type="html">Tomáš Kozák - Senior AI / ML Engineer</title><subtitle>Personal website and portfolio for Tomáš Kozák - Senior AI / ML Engineer</subtitle><author><name>Tomáš Kozák</name></author><entry><title type="html">MLOps Series L1: a simple lightweight intro to ‘From IPython Notebook to Prod’</title><link href="https://tomaskozak.com/blog/mlops-grid-anomaly-detection/" rel="alternate" type="text/html" title="MLOps Series L1: a simple lightweight intro to ‘From IPython Notebook to Prod’" /><published>2025-09-09T00:00:00+00:00</published><updated>2025-09-09T00:00:00+00:00</updated><id>https://tomaskozak.com/blog/mlops-grid-anomaly-detection</id><content type="html" xml:base="https://tomaskozak.com/blog/mlops-grid-anomaly-detection/"><![CDATA[<p>This post is a deep-dive, hands‑on guide to turning an experimental ML project ideas into a production‑grade service you can deploy, scale, and observe. We will build a real‑time anomaly detection microservice for grid sensor data, expose a friendly UI, containerize both services, deploy them to Kubernetes with autoscaling, and reason about SLOs, performance, and extensibility.</p>

<p>The full codebase lives in this repository: <a href="https://github.com/Tomas-Kozak/mlops-series-l1">https://github.com/Tomas-Kozak/mlops-series-l1</a></p>

<h2 id="contents">Contents</h2>

<ul>
  <li>A FastAPI service that serves an IsolationForest model for detecting anomalies in 3 sensor features: <code class="language-plaintext highlighter-rouge">voltage</code>, <code class="language-plaintext highlighter-rouge">current</code>, <code class="language-plaintext highlighter-rouge">frequency</code>.</li>
  <li>A Streamlit UI that simulates grid readings, sends them in batches to the backend, and visualizes anomalies and tail latencies.</li>
  <li>Prometheus metrics for end‑to‑end observability (<code class="language-plaintext highlighter-rouge">/metrics</code>).</li>
  <li>Docker images for both services; Docker Compose for local multi‑service dev.</li>
  <li>Kubernetes manifests with a Horizontal Pod Autoscaler (HPA) driven by CPU, deployable to a local kind cluster.</li>
</ul>

<p>Patterns and code you can reuse: request/response schemas, model artifact handling, latency instruments, container hardening, HPA configuration, and a realistic load test harness.</p>

<h2 id="architecture-overview">Architecture Overview</h2>

<p>Data flow:</p>

<p>1) Simulator emits grid readings with occasional injected anomalies.
2) UI assembles readings into request batches and calls the <code class="language-plaintext highlighter-rouge">/predict</code> endpoint.
3) Backend validates payloads (Pydantic), converts to feature matrix, runs IsolationForest inference.
4) Backend returns per‑reading anomaly flags and scores, while emitting Prometheus metrics.
5) UI renders charts, overlays anomalies, and generates burst/sustained load to trigger autoscaling.</p>

<p>Services and deployment:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/backend/*</code>: containerized FastAPI app, scaled by HPA in Kubernetes.</li>
  <li><code class="language-plaintext highlighter-rouge">src/ui/*</code>: containerized Streamlit UI, pointed to backend via <code class="language-plaintext highlighter-rouge">BACKEND_URL</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">docker/compose.yaml</code>: local multi‑service run with shared network.</li>
  <li><code class="language-plaintext highlighter-rouge">k8s/*</code>: <code class="language-plaintext highlighter-rouge">Deployment</code> + <code class="language-plaintext highlighter-rouge">Service</code> for both apps and CPU‑based <code class="language-plaintext highlighter-rouge">HorizontalPodAutoscaler</code> for backend.</li>
</ul>

<h3 id="diagram-highlevel-architecture">Diagram: High‑Level Architecture</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +----------------+         HTTP (JSON)          +-------------------+
 |  Streamlit UI  |  -------------------------&gt;  |  FastAPI Backend  |
 |  (src/ui)      |                              |  (src/backend)    |
 +--------+-------+                              +----+--------------+
          ^                                           |
          |                                           | Prometheus exposition
          |                                           v
          |                                     +-----+------+
 Simulation (local)                             |  /metrics  |
   └─ stream_readings()                         +------------+
</code></pre></div></div>

<p>In Kubernetes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>          +------------------+                     +------------------+
          |  anomaly-ui Pod  |  ---&gt; Service ---&gt;  | anomaly-backend  |
          |  (Deployment)    |        (ClusterIP)  |   Pods (HPA)     |
          +---------+--------+                     +---------+--------+
                    |                                        |
             Port-forward                                   HPA
                    |                              (CPU utilization target)
                    v                                        v
               http://localhost:8501                    Scales 1..N pods
</code></pre></div></div>

<h3 id="diagram-request-path-and-timers">Diagram: Request Path and Timers</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UI batch -&gt; HTTP POST /predict -&gt; [FastAPI]
                                     |
                                     +-- parse JSON (Pydantic)
                                     |
                                     +-- as_feature_matrix()
                                     |
                                     +-- [T_infer] IsolationForest.predict
                                     |
                                     +-- build response
                                     |
                                     `-- observe REQUEST_LATENCY (total)
</code></pre></div></div>

<h3 id="diagram-sequence-ui-burst">Diagram: Sequence (UI burst)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UI(ThreadPool)       Kubernetes SVC        Pod A (backend)       Pod B (backend)
    |                      |                     |                      |
    |-- N requests -------&gt;|-- distribute ------&gt;|-- handle (A)         |
    |                      |                     |                      |
    |-- N requests -------&gt;|-- distribute -----------------------------&gt;|-- handle (B)
    |                      |                     |                      |
    |&lt;- responses (A,B) ---|&lt;--------------------|&lt;---------------------|
</code></pre></div></div>

<h2 id="data-and-modeling">Data and Modeling</h2>

<p>Core modeling is not the focus of this post, but we’ll briefly go over it to get a sense of what we’re working with. Problem: detect anomalous grid operating points using only point‑in‑time readings for three features: voltage (V), current (A), and frequency (Hz).</p>

<p>We use an IsolationForest (scikit‑learn) trained on synthetically generated “normal” operating points; assigns higher anomaly scores to outliers (inverted). This keeps the focus on the MLOps systems work rather than modeling intricacies, but we still make the model deterministic, reproducible, and artifacted.</p>

<p>Feature schema and defaults (source: <code class="language-plaintext highlighter-rouge">src/backend/model.py:11</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DEFAULT_FEATURES</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">voltage</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">current</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">frequency</span><span class="sh">"</span><span class="p">]</span>
</code></pre></div></div>

<p>Model config and training (source: <code class="language-plaintext highlighter-rouge">src/backend/model.py:15</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ModelConfig</span><span class="p">:</span>
    <span class="n">feature_names</span><span class="p">:</span> <span class="n">Tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="p">...]</span> <span class="o">=</span> <span class="nf">tuple</span><span class="p">(</span><span class="n">DEFAULT_FEATURES</span><span class="p">)</span>
    <span class="n">random_state</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">42</span>
    <span class="n">contamination</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.05</span>
    <span class="n">n_estimators</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">200</span>

<span class="k">def</span> <span class="nf">train_isolation_forest</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="n">ModelConfig</span><span class="p">,</span> <span class="n">n_samples</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5000</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">IsolationForest</span><span class="p">:</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">default_rng</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">random_state</span><span class="p">)</span>
    <span class="n">X</span> <span class="o">=</span> <span class="nf">_generate_synthetic_normal</span><span class="p">(</span><span class="n">n_samples</span><span class="p">,</span> <span class="n">rng</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nc">IsolationForest</span><span class="p">(</span>
        <span class="n">n_estimators</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">n_estimators</span><span class="p">,</span>
        <span class="n">contamination</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">contamination</span><span class="p">,</span>
        <span class="n">random_state</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">random_state</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">model</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">model</span>
</code></pre></div></div>

<p>Theory notes:</p>

<ul>
  <li>IsolationForest isolates points by random splits; outliers require fewer splits to isolate, yielding larger anomaly scores.</li>
  <li><code class="language-plaintext highlighter-rouge">contamination</code> approximates the expected fraction of anomalies. For energy datasets with non‑stationarity, you’d calibrate this offline (ROC/PR curves) and monitor precision/recall drop‑offs over time.</li>
  <li>Determinism and reproducibility come from fixed <code class="language-plaintext highlighter-rouge">random_state</code> and artifacting the trained model to <code class="language-plaintext highlighter-rouge">models/iso_forest.joblib</code>.</li>
</ul>

<p>Advanced modeling guidance tailored to this service:</p>

<ul>
  <li>If your grid has diurnal/seasonal cycles, consider adding time‑of‑day or temperature context features or training per‑segment models (substations/regions). Keep <code class="language-plaintext highlighter-rouge">feature_names</code> ordered and versioned.</li>
  <li>Track input drift via PSI/KL on voltage/current/frequency; alert when drift exceeds thresholds and kick off retraining jobs.</li>
  <li>Calibrate a score threshold to a target precision/recall on validation data and make it a dynamic config value.</li>
</ul>

<h2 id="api-design-and-contracts">API Design and Contracts</h2>

<p>We model the request/response with Pydantic. The contract is explicit and versionable (good for compatibility testing and schema drift detection).</p>

<p>Schemas (source: <code class="language-plaintext highlighter-rouge">src/backend/api.py:1</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SensorReading</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">voltage</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">current</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">frequency</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">timestamp</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">BatchPredictRequest</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">readings</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">SensorReading</span><span class="p">]</span>

<span class="k">class</span> <span class="nc">Prediction</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">anomaly</span><span class="p">:</span> <span class="nb">bool</span>
    <span class="n">score</span><span class="p">:</span> <span class="nb">float</span>

<span class="k">class</span> <span class="nc">BatchPredictResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">predictions</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">Prediction</span><span class="p">]</span>
    <span class="n">anomalies</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">total</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">served_by</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
</code></pre></div></div>

<p>Endpoints (source: <code class="language-plaintext highlighter-rouge">src/backend/app.py</code>):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GET /health</code>: quick readiness and instance identity.</li>
  <li><code class="language-plaintext highlighter-rouge">POST /predict</code>: batch inference. Returns per‑reading anomaly booleans and scores, plus metadata.</li>
  <li><code class="language-plaintext highlighter-rouge">GET /metrics</code>: Prometheus exposition for scraping.</li>
</ul>

<p>Example request:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST /predict
Content-Type: application/json

{
  "readings": [
    {"voltage": 230.1, "current": 9.8, "frequency": 50.02},
    {"voltage": 271.0, "current": 28.5, "frequency": 50.7}
  ]
}
</span></code></pre></div></div>

<p>Example response:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"predictions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="nl">"anomaly"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nl">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.06</span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"anomaly"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nl">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.79</span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"anomalies"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"served_by"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pod-xyz:12345"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Validation and invariants:</p>

<ul>
  <li>Pydantic enforces types; reject empty batches with 400.</li>
  <li><code class="language-plaintext highlighter-rouge">as_feature_matrix</code> ensures strict feature ordering to match the model’s training order.</li>
  <li>On model unavailability, return <code class="language-plaintext highlighter-rouge">503</code> to signal readiness gates.</li>
</ul>

<h2 id="backend-serving-metrics-and-error-semantics">Backend: Serving, Metrics, and Error Semantics</h2>

<p>Core app (abridged; source: <code class="language-plaintext highlighter-rouge">src/backend/app.py:1</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">REQUEST_COUNT</span> <span class="o">=</span> <span class="nc">Counter</span><span class="p">(</span><span class="sh">"</span><span class="s">requests_total</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Total HTTP requests</span><span class="sh">"</span><span class="p">,</span> <span class="p">[</span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">method</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">status</span><span class="sh">"</span><span class="p">])</span>
<span class="n">REQUEST_LATENCY</span> <span class="o">=</span> <span class="nc">Histogram</span><span class="p">(</span><span class="sh">"</span><span class="s">request_latency_seconds</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Request latency (s)</span><span class="sh">"</span><span class="p">,</span> <span class="p">[</span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">method</span><span class="sh">"</span><span class="p">])</span>
<span class="n">INFERENCE_LATENCY</span> <span class="o">=</span> <span class="nc">Histogram</span><span class="p">(</span><span class="sh">"</span><span class="s">inference_latency_seconds</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Model inference latency (s)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">ANOMALY_COUNT</span> <span class="o">=</span> <span class="nc">Counter</span><span class="p">(</span><span class="sh">"</span><span class="s">anomalies_total</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Total anomalies predicted</span><span class="sh">"</span><span class="p">)</span>

<span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/predict</span><span class="sh">"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">BatchPredictResponse</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">predict</span><span class="p">(</span><span class="n">req</span><span class="p">:</span> <span class="n">BatchPredictRequest</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">BatchPredictResponse</span><span class="p">:</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">req</span><span class="p">.</span><span class="n">readings</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">No readings provided</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">model_instance</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">503</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Model not ready</span><span class="sh">"</span><span class="p">)</span>

    <span class="n">start</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">perf_counter</span><span class="p">()</span>
    <span class="n">status_label</span> <span class="o">=</span> <span class="sh">"</span><span class="s">200</span><span class="sh">"</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">X</span> <span class="o">=</span> <span class="nf">as_feature_matrix</span><span class="p">([</span><span class="n">r</span><span class="p">.</span><span class="nf">model_dump</span><span class="p">()</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">req</span><span class="p">.</span><span class="n">readings</span><span class="p">],</span> <span class="n">model_instance</span><span class="p">.</span><span class="n">feature_names</span><span class="p">)</span>
        <span class="n">infer_t0</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">perf_counter</span><span class="p">()</span>
        <span class="n">flags</span><span class="p">,</span> <span class="n">scores</span> <span class="o">=</span> <span class="n">model_instance</span><span class="p">.</span><span class="nf">predict_batch</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
        <span class="n">INFERENCE_LATENCY</span><span class="p">.</span><span class="nf">observe</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">infer_t0</span><span class="p">)</span>
        <span class="n">preds</span> <span class="o">=</span> <span class="p">[</span><span class="nc">Prediction</span><span class="p">(</span><span class="n">anomaly</span><span class="o">=</span><span class="nf">bool</span><span class="p">(</span><span class="n">flags</span><span class="p">[</span><span class="n">i</span><span class="p">]),</span> <span class="n">score</span><span class="o">=</span><span class="nf">float</span><span class="p">(</span><span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">]))</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">readings</span><span class="p">))]</span>
        <span class="n">anomalies</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">flags</span><span class="p">.</span><span class="nf">sum</span><span class="p">())</span>
        <span class="n">ANOMALY_COUNT</span><span class="p">.</span><span class="nf">inc</span><span class="p">(</span><span class="n">anomalies</span><span class="p">)</span>
        <span class="k">return</span> <span class="nc">BatchPredictResponse</span><span class="p">(</span><span class="n">predictions</span><span class="o">=</span><span class="n">preds</span><span class="p">,</span> <span class="n">anomalies</span><span class="o">=</span><span class="n">anomalies</span><span class="p">,</span> <span class="n">total</span><span class="o">=</span><span class="nf">len</span><span class="p">(</span><span class="n">preds</span><span class="p">),</span> <span class="n">served_by</span><span class="o">=</span><span class="n">INSTANCE_ID</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">HTTPException</span> <span class="k">as</span> <span class="n">he</span><span class="p">:</span>
        <span class="n">status_label</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">he</span><span class="p">.</span><span class="n">status_code</span><span class="p">);</span> <span class="k">raise</span>
    <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
        <span class="n">status_label</span> <span class="o">=</span> <span class="sh">"</span><span class="s">500</span><span class="sh">"</span><span class="p">;</span> <span class="k">raise</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">dt</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">start</span>
        <span class="n">REQUEST_LATENCY</span><span class="p">.</span><span class="nf">labels</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="sh">"</span><span class="s">/predict</span><span class="sh">"</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">).</span><span class="nf">observe</span><span class="p">(</span><span class="n">dt</span><span class="p">)</span>
        <span class="n">REQUEST_COUNT</span><span class="p">.</span><span class="nf">labels</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="sh">"</span><span class="s">/predict</span><span class="sh">"</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">,</span> <span class="n">status</span><span class="o">=</span><span class="n">status_label</span><span class="p">).</span><span class="nf">inc</span><span class="p">()</span>
</code></pre></div></div>

<p>Notes on instrumentation and semantics:</p>

<ul>
  <li>We separate total request latency from model inference latency; this allows you to spot overhead from JSON parsing, data marshaling, or network.</li>
  <li>We label counts by path/method/status to power RED/USE dashboards and simple SLOs (e.g., error rate &lt; 1%).</li>
  <li>A global exception handler ensures 500s are counted even on unexpected code paths.</li>
</ul>

<p>Threading and CPU:</p>

<ul>
  <li>scikit‑learn inference is CPU‑bound. We scale using Uvicorn workers (multiprocessing) and Kubernetes replicas; the GIL isn’t the bottleneck when workers are processes.</li>
  <li>Suggested env for predictable CPU usage: set <code class="language-plaintext highlighter-rouge">OMP_NUM_THREADS=1</code> and <code class="language-plaintext highlighter-rouge">MKL_NUM_THREADS=1</code> in production to avoid oversubscription when running multiple workers/pods.</li>
</ul>

<p>Histogram buckets and cardinality:</p>

<ul>
  <li>Customize histogram buckets to your SLOs to make <code class="language-plaintext highlighter-rouge">histogram_quantile</code> stable. Keep label sets small to control time‑series cardinality and Prometheus load.</li>
</ul>

<p>Example bucket customization:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">REQUEST_LATENCY</span> <span class="o">=</span> <span class="nc">Histogram</span><span class="p">(</span>
    <span class="sh">"</span><span class="s">request_latency_seconds</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Request latency (s)</span><span class="sh">"</span><span class="p">,</span> <span class="p">[</span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">method</span><span class="sh">"</span><span class="p">],</span>
    <span class="n">buckets</span><span class="o">=</span><span class="p">(</span><span class="mf">0.01</span><span class="p">,</span> <span class="mf">0.02</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">,</span> <span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="model-artifact-management">Model Artifact Management</h2>

<p>Artifacts live under <code class="language-plaintext highlighter-rouge">MODEL_DIR</code> (<code class="language-plaintext highlighter-rouge">models</code> locally, <code class="language-plaintext highlighter-rouge">/models</code> in containers). On first run, the service trains and persists an artifact, making cold‑start deterministic.</p>

<p>Artifact handling (source: <code class="language-plaintext highlighter-rouge">src/backend/model.py:39</code>):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ensure_model</span><span class="p">(</span><span class="n">model_path</span><span class="p">:</span> <span class="n">Path</span> <span class="o">|</span> <span class="nb">str</span><span class="p">,</span> <span class="n">config</span><span class="p">:</span> <span class="n">ModelConfig</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span> <span class="n">train_if_missing</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">AnomalyModel</span><span class="p">:</span>
    <span class="n">path</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">model_path</span><span class="p">)</span>
    <span class="n">cfg</span> <span class="o">=</span> <span class="n">config</span> <span class="ow">or</span> <span class="nc">ModelConfig</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">path</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">joblib</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
        <span class="k">return</span> <span class="nc">AnomalyModel</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">cfg</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">train_if_missing</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nc">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Model artifact not found at </span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">path</span><span class="p">.</span><span class="n">parent</span><span class="p">.</span><span class="nf">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nf">train_isolation_forest</span><span class="p">(</span><span class="n">cfg</span><span class="p">)</span>
    <span class="n">joblib</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>
    <span class="k">return</span> <span class="nc">AnomalyModel</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">cfg</span><span class="p">)</span>
</code></pre></div></div>

<p>Production variants:</p>

<ul>
  <li>Swap the local filesystem for a PVC in Kubernetes or a remote artifact store (S3/GCS) with a digest‑addressed path and integrity check.</li>
  <li>Gate startup on model availability (<code class="language-plaintext highlighter-rouge">TRAIN_IF_MISSING=0</code>) and fail fast if artifact is missing.</li>
  <li>Record model metadata (config, hash, training data hash) next to the artifact for lineage.</li>
</ul>

<p>Artifact promotion workflow:</p>

<ul>
  <li>Train offline; store artifact with immutable digest and metadata JSON (config, data hashes, metrics).</li>
  <li>Canary new artifact with 1 replica; compare error/latency and anomaly base rate vs. baseline.</li>
  <li>Promote by scaling up canary and scaling down baseline or using traffic splitting with Argo Rollouts.</li>
</ul>

<h2 id="the-ui-streaming-visualization-and-load-generation">The UI: Streaming, Visualization, and Load Generation</h2>

<p>The UI lets you explore the system’s behavior and also stress test it. It simulates readings, batches requests, and displays rolling windows with anomaly overlays and server instance distribution.</p>

<p>Highlights (source: <code class="language-plaintext highlighter-rouge">src/ui/main.py</code>):</p>

<ul>
  <li>Sidebar controls: backend URL, batch size, interval, anomaly rate, and “new connection each request” to improve load distribution across pods (disables HTTP keep‑alive pinning).</li>
  <li>Actions: Generate Batch, Start/Stop Streaming, Inject Extreme Reading, Concurrent Burst, and Sustained Bursts (with workers and cycles).</li>
  <li>Charts: line chart for signals; red triangle markers for anomalies; bar chart of “served_by” instance to visualize request spread across replicas.</li>
</ul>

<p>Why “new connection each request” matters: HTTP keep‑alive can pin a Streamlit (single process) client to a single pod behind a ClusterIP/Service due to 5‑tuple hashing. Setting <code class="language-plaintext highlighter-rouge">Connection: close</code> on each request approximates a fairer fan‑out across pods at the cost of extra TCP overhead. For production, consider a proper load balancer with connection‑level balancing or a service mesh.</p>

<p>Additional UI internals:</p>

<ul>
  <li>Maintains a rolling deque (<code class="language-plaintext highlighter-rouge">WINDOW=200</code>) of recent points with anomaly overlay; aggregates <code class="language-plaintext highlighter-rouge">served_by</code> to visualize load distribution.</li>
  <li>Burst features use <code class="language-plaintext highlighter-rouge">ThreadPoolExecutor</code>; when pushing very high RPS from one host, watch ephemeral port exhaustion and OS limits.</li>
  <li>“Inject Extreme Reading” posts a synthetic outlier to validate end‑to‑end behavior and verify alerting/visualization.</li>
</ul>

<h2 id="local-development-and-tooling">Local Development and Tooling</h2>

<p>We use <code class="language-plaintext highlighter-rouge">uv</code> with <code class="language-plaintext highlighter-rouge">pyproject.toml</code> to pin dependencies and streamline local runs.</p>

<p>Key commands:</p>

<ul>
  <li>Install: <code class="language-plaintext highlighter-rouge">uv sync --extra dev</code></li>
  <li>Run API: <code class="language-plaintext highlighter-rouge">uv run python -m src.backend.main</code> or <code class="language-plaintext highlighter-rouge">uv run uvicorn src.backend.app:app --host 0.0.0.0 --port 8000</code></li>
  <li>Run UI: <code class="language-plaintext highlighter-rouge">uv run streamlit run src/ui/main.py</code></li>
  <li>Lint/format: <code class="language-plaintext highlighter-rouge">uv run python scripts/dev.py fix</code> (optional helper)</li>
</ul>

<p>Typing and linting:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mypy</code> enforces type safety on <code class="language-plaintext highlighter-rouge">src/</code>; gradually tighten in critical paths (API surface, model I/O).</li>
  <li><code class="language-plaintext highlighter-rouge">ruff</code> provides fast lint + formatting; keep consistent style to reduce PR churn.</li>
</ul>

<h2 id="containerization">Containerization</h2>

<p>We build two containers — a full backend image with the ML stack and a lean UI image that depends only on Streamlit + Requests.</p>

<p>Backend Dockerfile (source: <code class="language-plaintext highlighter-rouge">docker/Dockerfile.backend</code>):</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># syntax=docker/dockerfile:1.7</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">python:3.13-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span>

<span class="k">ENV</span><span class="s"> PYTHONDONTWRITEBYTECODE=1 \</span>
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PORT=8000 \
    MODEL_DIR=/models \
    MODEL_NAME=iso_forest.joblib \
    TRAIN_IF_MISSING=1 \
    VIRTUAL_ENV=/app/.venv \
    PATH="/app/.venv/bin:$PATH"

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">--upgrade</span> pip <span class="se">\
</span>    <span class="o">&amp;&amp;</span> pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> uv

<span class="k">COPY</span><span class="s"> pyproject.toml README.md uv.lock ./</span>
<span class="k">COPY</span><span class="s"> src ./src</span>

<span class="k">RUN </span>uv <span class="nt">--version</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> uv <span class="nb">sync</span> <span class="nt">--frozen</span>

<span class="k">RUN </span>groupadd <span class="nt">-r</span> app <span class="o">&amp;&amp;</span> useradd <span class="nt">-r</span> <span class="nt">-g</span> app app <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /models <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">chown</span> <span class="nt">-R</span> app:app /app /models

<span class="k">USER</span><span class="s"> app</span>
<span class="k">EXPOSE</span><span class="s"> 8000</span>

<span class="k">CMD</span><span class="s"> ["sh", "-c", "uvicorn src.backend.app:app --host 0.0.0.0 --port 8000 --workers ${WORKERS:-1}"]</span>
</code></pre></div></div>

<p>UI Dockerfile (source: <code class="language-plaintext highlighter-rouge">docker/Dockerfile.ui</code>):</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># syntax=docker/dockerfile:1.7</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">python:3.13-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span>

<span class="k">ENV</span><span class="s"> PYTHONDONTWRITEBYTECODE=1 \</span>
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    VIRTUAL_ENV=/app/.venv \
    PATH="/app/.venv/bin:$PATH" \
    BACKEND_URL=http://localhost:8000 \
    STREAMLIT_SERVER_PORT=8501

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">--upgrade</span> pip <span class="se">\
</span>    <span class="o">&amp;&amp;</span> pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> uv

<span class="k">COPY</span><span class="s"> src/__init__.py ./src/__init__.py</span>
<span class="k">COPY</span><span class="s"> src/ui ./src/ui</span>

<span class="k">RUN </span>uv pip <span class="nb">install</span> <span class="nt">--system</span> <span class="nv">streamlit</span><span class="o">==</span>1.49.1 <span class="nv">requests</span><span class="o">==</span>2.32.4

<span class="k">EXPOSE</span><span class="s"> 8501</span>

<span class="k">CMD</span><span class="s"> ["streamlit", "run", "src/ui/main.py", "--server.port=8501", "--server.address=0.0.0.0"]</span>
</code></pre></div></div>

<p>Notes:</p>

<ul>
  <li>Non‑root user (<code class="language-plaintext highlighter-rouge">app</code>) and a writable <code class="language-plaintext highlighter-rouge">/models</code> directory for artifacts in backend.</li>
  <li>The UI image avoids installing the heavy ML stack, reducing build time and attack surface.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">WORKERS</code> env to tune Uvicorn worker processes for CPU cores.</li>
</ul>

<p>Supply‑chain and runtime hardening:</p>

<ul>
  <li>Pin base image digests; generate SBOMs; scan images in CI.</li>
  <li>Set <code class="language-plaintext highlighter-rouge">OMP_NUM_THREADS=1</code> and <code class="language-plaintext highlighter-rouge">MKL_NUM_THREADS=1</code> in the backend image to avoid CPU oversubscription.</li>
  <li>Consider <code class="language-plaintext highlighter-rouge">readOnlyRootFilesystem: true</code> and dropping Linux capabilities where possible.</li>
</ul>

<h2 id="docker-compose">Docker Compose</h2>

<p>Local multi‑service development (source: <code class="language-plaintext highlighter-rouge">docker/compose.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">backend</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">..</span>
      <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">docker/Dockerfile.backend</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">TRAIN_IF_MISSING=1</span>
      <span class="pi">-</span> <span class="s">MODEL_DIR=/models</span>
      <span class="pi">-</span> <span class="s">POD_NAME=compose-backend</span>
      <span class="pi">-</span> <span class="s">WORKERS=2</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">../models:/models</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8000:8000"</span>

  <span class="na">ui</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">..</span>
      <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">docker/Dockerfile.ui</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">BACKEND_URL=http://backend:8000</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8501:8501"</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">backend</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">default</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-net</span>
</code></pre></div></div>

<p>Run both: <code class="language-plaintext highlighter-rouge">cd docker &amp;&amp; docker compose up --build</code>.</p>

<p>Testing variations locally:</p>

<ul>
  <li>Scale <code class="language-plaintext highlighter-rouge">WORKERS=4</code> and compare p95 at fixed RPS.</li>
  <li>Delete <code class="language-plaintext highlighter-rouge">./models/iso_forest.joblib</code> to exercise cold‑start training path.</li>
</ul>

<h2 id="kubernetes-deploy-observe-and-scale">Kubernetes: Deploy, Observe, and Scale</h2>

<p>We provide <code class="language-plaintext highlighter-rouge">Deployment</code> + <code class="language-plaintext highlighter-rouge">Service</code> for both apps and an HPA for the backend. Apply via Kustomize: <code class="language-plaintext highlighter-rouge">kubectl apply -k k8s/</code>.</p>

<p>Backend deployment (source: <code class="language-plaintext highlighter-rouge">k8s/backend-deployment.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
    <span class="na">rollingUpdate</span><span class="pi">:</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="m">0</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">anomaly-backend:latest</span>
          <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8000</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TRAIN_IF_MISSING</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MODEL_DIR</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/models"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WORKERS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">POD_NAME</span>
              <span class="na">valueFrom</span><span class="pi">:</span>
                <span class="na">fieldRef</span><span class="pi">:</span>
                  <span class="na">fieldPath</span><span class="pi">:</span> <span class="s">metadata.name</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">NODE_NAME</span>
              <span class="na">valueFrom</span><span class="pi">:</span>
                <span class="na">fieldRef</span><span class="pi">:</span>
                  <span class="na">fieldPath</span><span class="pi">:</span> <span class="s">spec.nodeName</span>
          <span class="na">readinessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/health</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8000</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
            <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">2</span>
          <span class="na">livenessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/health</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8000</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">10</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">20</span>
            <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">2</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">200m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">512Mi"</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">model-store</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/models</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">model-store</span>
          <span class="na">emptyDir</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>

<p>Horizontal Pod Autoscaler (source: <code class="language-plaintext highlighter-rouge">k8s/backend-hpa.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-backend-hpa</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
      <span class="na">resource</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
        <span class="na">target</span><span class="pi">:</span>
          <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
          <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">60</span>
</code></pre></div></div>

<p>Backend service (source: <code class="language-plaintext highlighter-rouge">k8s/backend-service.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-backend</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">8000</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8000</span>
</code></pre></div></div>

<p>UI deployment (source: <code class="language-plaintext highlighter-rouge">k8s/ui-deployment.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
    <span class="na">rollingUpdate</span><span class="pi">:</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="m">0</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ui</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">anomaly-ui:latest</span>
          <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8501</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">BACKEND_URL</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">http://anomaly-backend:8000</span>
          <span class="na">readinessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8501</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
            <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">2</span>
          <span class="na">livenessProbe</span><span class="pi">:</span>
            <span class="na">tcpSocket</span><span class="pi">:</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8501</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">10</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">20</span>
            <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">2</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">100m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">128Mi"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
</code></pre></div></div>

<p>UI service (source: <code class="language-plaintext highlighter-rouge">k8s/ui-service.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">anomaly-ui</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">8501</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8501</span>
</code></pre></div></div>

<p>Notes and production tips:</p>

<ul>
  <li>On Kubernetes, prefer a single Uvicorn worker per container and let HPA/KEDA add pods; use multiple workers per pod only with a clear reason (startup cost/bin-packing) and cap OMP_NUM_THREADS/MKL_NUM_THREADS to 1 to avoid CPU oversubscription.</li>
  <li>HPA on CPU is simple and effective for CPU‑bound inference. For request‑driven scaling (QPS or queue length), consider KEDA with event sources or custom metrics (requests in flight).</li>
  <li>Ensure <code class="language-plaintext highlighter-rouge">metrics-server</code> is installed; for kind, patch insecure TLS as in the README.</li>
  <li>Tune <code class="language-plaintext highlighter-rouge">requests</code>/<code class="language-plaintext highlighter-rouge">limits</code> to achieve a useful utilization target. Too low limits can cause throttling; too high requests can starve scheduling.</li>
  <li>Prefer <code class="language-plaintext highlighter-rouge">readinessProbe</code> on <code class="language-plaintext highlighter-rouge">/health</code> over <code class="language-plaintext highlighter-rouge">/metrics</code>; treat model availability as a readiness condition.</li>
</ul>

<p>Diagram: K8s Objects and Flow</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +-------------+     +-------------------+     +------------------------+
 |  Deployment | --&gt; | ReplicaSet (Pods) | --&gt; | HPA observes CPU usage |
 +------+------+     +---------+---------+     +-----------+------------+
        |                        |                           |
        v                        v                           |
  Pods expose :8000        ClusterIP Service                 |
        |                        |                           |
        +-------&gt; kube-proxy/iptables (hash)  &lt;--------------+
</code></pre></div></div>

<p>Kustomize overlays (source: <code class="language-plaintext highlighter-rouge">k8s/kustomization.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.config.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">anomaly</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">namespace.yaml</span>
  <span class="pi">-</span> <span class="s">backend-deployment.yaml</span>
  <span class="pi">-</span> <span class="s">backend-service.yaml</span>
  <span class="pi">-</span> <span class="s">backend-hpa.yaml</span>
  <span class="pi">-</span> <span class="s">ui-deployment.yaml</span>
  <span class="pi">-</span> <span class="s">ui-service.yaml</span>
</code></pre></div></div>

<p>Namespace (source: <code class="language-plaintext highlighter-rouge">k8s/namespace.yaml</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Namespace</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">anomaly</span>
</code></pre></div></div>

<h2 id="observability-metrics-slislos-and-promql">Observability: Metrics, SLI/SLOs, and PromQL</h2>

<p>The backend emits Prometheus metrics at <code class="language-plaintext highlighter-rouge">/metrics</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">requests_total{path,method,status}</code>: RED metrics (rate/errors/duration).</li>
  <li><code class="language-plaintext highlighter-rouge">request_latency_seconds_bucket{path,method}</code> (+ <code class="language-plaintext highlighter-rouge">_sum</code>, <code class="language-plaintext highlighter-rouge">_count</code>): request latency histograms.</li>
  <li><code class="language-plaintext highlighter-rouge">inference_latency_seconds_bucket</code> (+ <code class="language-plaintext highlighter-rouge">_sum</code>, <code class="language-plaintext highlighter-rouge">_count</code>): pure model time.</li>
  <li><code class="language-plaintext highlighter-rouge">anomalies_total</code>: running count of predicted anomalies.</li>
</ul>

<p>Example SLOs:</p>

<ul>
  <li>Rule of thumb: for a CPU-bound sync service, keep average CPU ≲70% to protect p95/p99 from queuing blow-ups; we target 60% here to leave headroom.</li>
  <li>Bound synchronous server, keep utilization under ~70–75% to avoid runaway tail latencies; HPA targets 60% CPU here.</li>
</ul>

<h2 id="reliability-and-failure-modes">Reliability and Failure Modes</h2>

<ul>
  <li>Empty batches 400 error; invalid types 422 (Pydantic).</li>
  <li>Model missing 503 until artifact present or auto‑trained.</li>
  <li>Global exception handler increments <code class="language-plaintext highlighter-rouge">requests_total{status="500"}</code> and returns a stable JSON body for clients.</li>
  <li>Timeouts: client defaults to 5s; adjust based on batch sizes and latency budgets.</li>
</ul>

<p>Resilience extensions:</p>

<ul>
  <li>Add circuit breaking/retries at the client or service mesh layer.</li>
  <li>Expose a lightweight <code class="language-plaintext highlighter-rouge">/live</code> probe separate from <code class="language-plaintext highlighter-rouge">/health</code> readiness (for faster restarts).</li>
  <li>Emit structured logs (already using <code class="language-plaintext highlighter-rouge">structlog</code>); attach <code class="language-plaintext highlighter-rouge">instance</code>, <code class="language-plaintext highlighter-rouge">pod</code>, and <code class="language-plaintext highlighter-rouge">node</code> for correlation.</li>
</ul>

<p>Structured logging example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">predict</span><span class="sh">"</span><span class="p">,</span> <span class="n">instance</span><span class="o">=</span><span class="n">INSTANCE_ID</span><span class="p">,</span> <span class="n">pod</span><span class="o">=</span><span class="n">POD_NAME</span><span class="p">,</span> <span class="n">total</span><span class="o">=</span><span class="nf">len</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">readings</span><span class="p">),</span> <span class="n">anomalies</span><span class="o">=</span><span class="n">anomalies</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="security-and-hardening">Security and Hardening</h2>

<ul>
  <li>Non‑root containers; explicit resource requests/limits; probes.</li>
  <li>Avoid including the ML stack in the UI image.</li>
  <li>For production: ingress with TLS, authentication/authorization, rate limiting, and secrets management for external artifact stores.</li>
  <li>Image scanning and SBOM generation in CI.</li>
</ul>

<p>Secrets and config management:</p>

<ul>
  <li>Use <code class="language-plaintext highlighter-rouge">Secret</code> for credentials (e.g., remote artifact store) and <code class="language-plaintext highlighter-rouge">ConfigMap</code> for non‑sensitive configs (thresholds, feature lists).</li>
  <li>Mount via env or volumes; avoid embedding secrets in images.</li>
</ul>

<h2 id="cicd-and-testing-strategy">CI/CD and Testing Strategy</h2>

<p>While this project focuses on systems integration, a robust setup would include:</p>

<ul>
  <li>Unit tests around schema conversions (<code class="language-plaintext highlighter-rouge">as_feature_matrix</code>), model scoring sign, and endpoint error paths.</li>
  <li>Contract tests that POST known payloads and assert response schema and monotonicity of scores.</li>
  <li>Performance tests gated on p95 thresholds per commit.</li>
  <li>CI pipeline: lint (ruff), typecheck (mypy), test (pytest), build images, push to registry, deploy to a staging namespace via Kustomize overlays, smoke test, then promote.</li>
</ul>

<p>Testing pyramid tailored to this service:</p>

<ul>
  <li>Unit: <code class="language-plaintext highlighter-rouge">as_feature_matrix</code>, score polarity, simulator generation bounds.</li>
  <li>API: schema conformance, empty payloads, large batch behavior.</li>
  <li>Load: enforce p95/p99 targets at representative RPS.</li>
  <li>E2E: UI - backend flow; anomaly injection path.</li>
  <li>Upgrade: canary two backends with different model digests and verify parity.</li>
</ul>

<h2 id="extensibility-beyond-the-minimal-service">Extensibility: Beyond the Minimal Service</h2>

<ul>
  <li>Model registry and lineage: back your artifacts by a registry (MLflow, WandB, or custom) with immutable digests and promotion flows.</li>
  <li>Data drift and concept drift: track input feature distributions and anomaly rate; trigger alerts or shadow retraining.</li>
  <li>Event‑driven streaming: swap the synchronous HTTP flow for Kafka/NATS ingestion + consumer workers; scale with KEDA on lag.</li>
  <li>Inference servers: the same contract can be implemented with BentoML, Ray Serve, or LitServe while preserving schema and metrics.</li>
  <li>Real thresholds: convert scores to probabilities via calibration; expose thresholds per segment or dynamic thresholds from control charts.</li>
</ul>

<p>Streaming architectures:</p>

<ul>
  <li>Replace synchronous HTTP with Kafka ingestion and consumer workers; scale with KEDA on lag; UI subscribes via WebSocket/SSE.</li>
</ul>

<h2 id="endtoend-run">End‑to‑End Run:</h2>

<p>Bare metal (no containers):</p>

<ol>
  <li>Install deps: <code class="language-plaintext highlighter-rouge">uv sync --extra dev</code>.</li>
  <li>Backend: <code class="language-plaintext highlighter-rouge">uv run python -m src.backend.main</code>.</li>
  <li>UI: <code class="language-plaintext highlighter-rouge">uv run streamlit run src/ui/main.py</code>.</li>
  <li>Generate batches or start streaming from the UI; optionally run <code class="language-plaintext highlighter-rouge">uv run python scripts/load.py ...</code> for load.</li>
</ol>

<p>Docker Compose:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">cd docker &amp;&amp; docker compose up --build</code>.</li>
  <li>UI at <code class="language-plaintext highlighter-rouge">http://localhost:8501</code> (points to backend).</li>
  <li>Optionally run load against <code class="language-plaintext highlighter-rouge">http://localhost:8000</code>.</li>
</ol>

<p>Kubernetes (kind):</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">kind create cluster --name anomaly</code>.</li>
  <li>Install metrics‑server (and patch for kind); verify <code class="language-plaintext highlighter-rouge">kubectl top nodes</code> works.</li>
  <li>Build and load images into kind.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl apply -k k8s/</code>.</li>
  <li>Port‑forward UI: <code class="language-plaintext highlighter-rouge">kubectl -n anomaly port-forward svc/anomaly-ui 8501:8501</code>.</li>
  <li>Optionally port‑forward backend and run the load generator.</li>
  <li>Watch <code class="language-plaintext highlighter-rouge">kubectl -n anomaly get hpa -w</code> and <code class="language-plaintext highlighter-rouge">kubectl -n anomaly get pods -w</code> while increasing RPS.</li>
</ol>

<p>Smoke test checklist:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/health</code> returns <code class="language-plaintext highlighter-rouge">status=ok</code>; features match <code class="language-plaintext highlighter-rouge">DEFAULT_FEATURES</code>.</li>
  <li>First run creates model artifact; subsequent runs load it (no retrain).</li>
  <li>UI shows anomalies with non‑zero anomaly rate or after “Inject Extreme Reading”.</li>
  <li><code class="language-plaintext highlighter-rouge">/metrics</code> counters and histograms move when generating load.</li>
</ul>

<h2 id="appendix">Appendix</h2>

<p>Environment variables:</p>

<ul>
  <li>Backend: <code class="language-plaintext highlighter-rouge">WORKERS</code>, <code class="language-plaintext highlighter-rouge">MODEL_DIR</code>, <code class="language-plaintext highlighter-rouge">MODEL_NAME</code>, <code class="language-plaintext highlighter-rouge">TRAIN_IF_MISSING</code>, <code class="language-plaintext highlighter-rouge">POD_NAME</code>, <code class="language-plaintext highlighter-rouge">NODE_NAME</code>.</li>
  <li>UI: <code class="language-plaintext highlighter-rouge">BACKEND_URL</code>, <code class="language-plaintext highlighter-rouge">STREAMLIT_SERVER_PORT</code>.</li>
</ul>

<p>Key files to explore:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/backend/app.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/api.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/model.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/sim.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/ui/main.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/load.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">docker/Dockerfile.backend</code>, <code class="language-plaintext highlighter-rouge">docker/Dockerfile.ui</code>, <code class="language-plaintext highlighter-rouge">docker/compose.yaml</code></li>
  <li><code class="language-plaintext highlighter-rouge">k8s/*</code></li>
</ul>

<h2 id="closing">Closing</h2>

<p>This project demonstrates the full lifecycle from a simple notebook‑idea model to a production‑ready microservice with observability and autoscaling. The same patterns scale to richer models and larger fleets: keep contracts explicit, instrument everything, design for scale‑out, and treat artifacts and configuration as first‑class citizens.</p>]]></content><author><name>Tomáš Kozák</name></author><category term="mlops" /><category term="fastapi" /><category term="kubernetes" /><summary type="html"><![CDATA[Build a simple, observable, autoscaled microservice: FastAPI backend with IsolationForest, Streamlit UI, Docker, and Kubernetes HPA]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tomaskozak.com/assets/blog/mlops_l1/kubernetes.svg" /><media:content medium="image" url="https://tomaskozak.com/assets/blog/mlops_l1/kubernetes.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Neural Nets for Combinatorial Optimization</title><link href="https://tomaskozak.com/blog/neural-nets-combinatorial-optimization/" rel="alternate" type="text/html" title="Neural Nets for Combinatorial Optimization" /><published>2025-06-21T00:00:00+00:00</published><updated>2025-06-21T00:00:00+00:00</updated><id>https://tomaskozak.com/blog/neural-nets-combinatorial-optimization</id><content type="html" xml:base="https://tomaskozak.com/blog/neural-nets-combinatorial-optimization/"><![CDATA[<h1 id="coming-soon">Coming Soon!</h1>]]></content><author><name>Tomáš Kozák</name></author><category term="pointer-neural-nets" /><category term="graph-neural-nets" /><category term="combinatorial-optimization" /><category term="tsp" /><summary type="html"><![CDATA[Pointer Neural Nets and Graph Neural Nets with Beam Search to construct approximate TSP tours]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tomaskozak.com/assets/blog/tsp/gnn.png" /><media:content medium="image" url="https://tomaskozak.com/assets/blog/tsp/gnn.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Neural Self-Organization: Growing Patterns with Neural Cellular Automata</title><link href="https://tomaskozak.com/blog/neural-cellular-automata/" rel="alternate" type="text/html" title="Neural Self-Organization: Growing Patterns with Neural Cellular Automata" /><published>2025-06-19T00:00:00+00:00</published><updated>2025-06-19T00:00:00+00:00</updated><id>https://tomaskozak.com/blog/neural-cellular-automata</id><content type="html" xml:base="https://tomaskozak.com/blog/neural-cellular-automata/"><![CDATA[<p><strong>Context:</strong> Morphogenesis ≈ programmable self-assembly.  A Neural Cellular Automaton (NCA) is a discrete-time, convolutional dynamical system whose transition operator is differentiable and therefore amenable to gradient-based system identification.</p>

<p><strong>Scope:</strong> re-implementation of the Mordvintsev et al. (2020) NCA in PyTorch and instrument it with research-grade tooling: dynamic roll-outs, gradient clipping, Fourier/PCA diagnostics, regeneration stress-tests, and weight-space tracking.</p>

<p><strong>Road-map:</strong></p>

<ul>
  <li>Formalisation of the CA’s state-space and update equations (incl. Sobel perception)</li>
  <li>Derivation of the training objective $\mathcal L(\theta)$ and optimisation protocol</li>
  <li>Sample-pool curriculum and why it yields an attractor basin around the target pattern</li>
  <li>Quantitative diagnostics: SSIM/PSNR, radially-averaged power spectra, hidden-state PCA</li>
  <li>Ablations: dropout level ↦ robustness, channel budget ↦ expressivity</li>
</ul>

<p>Designed to run on a single GPU.</p>

<h2 id="1-foundation">1. Foundation</h2>

<h3 id="cell-state-representation">Cell State Representation</h3>

<p>Each cell $(i,j)$ maintains a <strong>16-dimensional state vector</strong> $\mathbf{s}_{i,j}^{(t)} \in \mathbb{R}^{16}$:</p>

<ul>
  <li><strong>RGB channels (0-2):</strong> Visual appearance $\in [0,1]$</li>
  <li><strong>Alpha channel (3):</strong> “Alive” marker; $\alpha &gt; 0.1$ indicates living cell</li>
  <li><strong>Hidden channels (4-15):</strong> Latent variables for coordination</li>
</ul>

<h3 id="update-dynamics">Update Dynamics</h3>

<p>At each time step, cells update via:</p>

<ol>
  <li>
    <p><strong>Perception:</strong> Sobel gradient sensing:
\(\mathbf{p}_{i,j}^{(t)} = [\, \mathbf{s}_{i,j}^{(t)},\; \partial_x \mathbf{s}_{i,j}^{(t)},\; \partial_y \mathbf{s}_{i,j}^{(t)}\,] \in \mathbb{R}^{48}\)</p>
  </li>
  <li>
    <p><strong>Neural update rule:</strong> small MLP produces increment:
\(\Delta \mathbf{s}_{i,j}^{(t)} = f_\theta(\mathbf{p}_{i,j}^{(t)})\)</p>
  </li>
  <li>
    <p><strong>Stochastic application:</strong> asynchronous updates with probability $p=0.5$:
\(\mathbf{s}_{i,j}^{(t+1)} = \mathbf{s}_{i,j}^{(t)} + m_{i,j}^{(t)} \cdot \Delta \mathbf{s}_{i,j}^{(t)}\)</p>
  </li>
  <li>
    <p><strong>Life/death masking:</strong> cells without living neighbors are zeroed</p>
  </li>
</ol>

<p>This creates a <strong>differentiable dynamical system</strong> where local rules can be learned via gradient descent to achieve global morphological objectives.</p>

<h4 id="formal-objective--gradient-flow">Formal Objective &amp; Gradient Flow</h4>

<p>Let $g_{\boldsymbol\theta}:\mathbb{R}^{C\times H\times W}\to\mathbb{R}^{C\times H\times W}$ denote <strong>one</strong> CA update parameterised by weights $\boldsymbol\theta$. A $T$-step rollout is the composition $g_{\boldsymbol\theta}^{\circ T}=g_{\boldsymbol\theta}\circ\cdots\circ g_{\boldsymbol\theta}$ applied $T$ times. For a distribution of initial states $\mathcal I$ (here the distribution over pool states during training) and a horizon distribution $\mathcal T$ (uniform over the schedule described later), the learning signal is the mean-squared reconstruction error between the rendered <strong>RGBA</strong> projection $\pi_{\text{rgba}}\bigl(g_{\boldsymbol\theta}^{\circ T}(\mathbf S_0)\bigr)$ and a fixed target image $\mathbf X^\star$:</p>

\[\mathcal L(\boldsymbol\theta)\;=\;
\underset{\mathbf S_0\sim\mathcal I}{\mathbb E}\;
\underset{T\sim\mathcal T}{\mathbb E}
\bigl\|
\pi_{\text{rgba}}\!\bigl(g_{\boldsymbol\theta}^{\circ T}(\mathbf S_0)\bigr)-\mathbf X^\star
\bigr\|_2^2.\]

<p>Every operation in $g_{\boldsymbol\theta}$ (perception, the small MLP, masking, and the stochastic Bernoulli update mask) is differentiable almost everywhere (the Bernoulli mask is not differentiable w.r.t. its own random variable, gradients are conditioned on the sample and are a Monte Carlo estimate of the true expectation), the gradient $\nabla_{\boldsymbol\theta}\mathcal L$ is unbiased but stochastic gradient estimate, computed by back-propagating through the unrolled computational graph. In practice it is recommended to clip the total gradient norm to 1 to avoid exploding gradients and rely on <strong>Adam</strong> with a decayed learning rate schedule. The stochastic update mask acts as a form of <strong>spatial dropout</strong> which empirically reduces co-adaptation of neighbouring cells and encourages the emergence of truly local rules.</p>

<p>A useful way to interpret the CA is as a <em>space- and time-evolving residual network</em>. Each cell implements a residual block $\mathbf s^{(t+1)}=\mathbf s^{(t)}+\Delta\mathbf s^{(t)}$ and the alive-mask pooling enforces a soft <strong>domain constraint</strong> that prunes isolated activations, akin to morphological erosion in mathematical morphology.</p>

<h2 id="2-implementation-setup">2. Implementation Setup</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pathlib</span>
<span class="kn">import</span> <span class="n">time</span>
<span class="kn">import</span> <span class="n">json</span>
<span class="kn">import</span> <span class="n">random</span>
<span class="kn">import</span> <span class="n">io</span>
<span class="kn">import</span> <span class="n">urllib.request</span>
<span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">List</span>
<span class="kn">from</span> <span class="n">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span><span class="p">,</span> <span class="n">asdict</span>

<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="n">seaborn</span> <span class="k">as</span> <span class="n">sns</span>
<span class="kn">import</span> <span class="n">PIL.Image</span>

<span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">from</span> <span class="n">skimage.metrics</span> <span class="kn">import</span> <span class="n">structural_similarity</span> <span class="k">as</span> <span class="n">ssim</span><span class="p">,</span> <span class="n">peak_signal_noise_ratio</span> <span class="k">as</span> <span class="n">psnr</span>
<span class="kn">from</span> <span class="n">sklearn.decomposition</span> <span class="kn">import</span> <span class="n">PCA</span>
<span class="kn">from</span> <span class="n">scipy.fft</span> <span class="kn">import</span> <span class="n">fft2</span><span class="p">,</span> <span class="n">fftshift</span>
<span class="kn">import</span> <span class="n">imageio.v2</span> <span class="k">as</span> <span class="n">imageio</span>


<span class="n">torch</span><span class="p">.</span><span class="nf">manual_seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
<span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>

<span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">device</span><span class="p">(</span><span class="sh">"</span><span class="s">cuda</span><span class="sh">"</span> <span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="nf">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="sh">"</span><span class="s">cpu</span><span class="sh">"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">backends</span><span class="p">.</span><span class="n">mps</span><span class="p">.</span><span class="nf">is_available</span><span class="p">():</span>
    <span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">device</span><span class="p">(</span><span class="sh">"</span><span class="s">mps</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Running on: </span><span class="si">{</span><span class="n">device</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="n">FIG_DIR</span> <span class="o">=</span> <span class="n">pathlib</span><span class="p">.</span><span class="nc">Path</span><span class="p">(</span><span class="sh">"</span><span class="s">figures</span><span class="sh">"</span><span class="p">)</span>
<span class="n">FIG_DIR</span><span class="p">.</span><span class="nf">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="3-target-pattern--data-preparation">3. Target Pattern &amp; Data Preparation</h2>

<p>The implementation will train the NCA to grow a 🐣 emoji from a single seed cell.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">GRID_SIZE</span> <span class="o">=</span> <span class="mi">64</span>
<span class="n">TARGET_PATH</span> <span class="o">=</span> <span class="n">pathlib</span><span class="p">.</span><span class="nc">Path</span><span class="p">(</span><span class="sh">"</span><span class="s">target.png</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="ow">not</span> <span class="n">TARGET_PATH</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Downloading target emoji...</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">url</span> <span class="o">=</span> <span class="sh">"</span><span class="s">https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/128/emoji_u1f423.png</span><span class="sh">"</span>
    <span class="k">with</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="nf">urlopen</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">as</span> <span class="n">resp</span><span class="p">:</span>
        <span class="n">PIL</span><span class="p">.</span><span class="n">Image</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">io</span><span class="p">.</span><span class="nc">BytesIO</span><span class="p">(</span><span class="n">resp</span><span class="p">.</span><span class="nf">read</span><span class="p">())).</span><span class="nf">save</span><span class="p">(</span><span class="n">TARGET_PATH</span><span class="p">)</span>


<span class="n">target_img</span> <span class="o">=</span> <span class="n">PIL</span><span class="p">.</span><span class="n">Image</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">TARGET_PATH</span><span class="p">).</span><span class="nf">convert</span><span class="p">(</span><span class="sh">"</span><span class="s">RGBA</span><span class="sh">"</span><span class="p">)</span>
<span class="n">canvas</span> <span class="o">=</span> <span class="n">PIL</span><span class="p">.</span><span class="n">Image</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="sh">"</span><span class="s">RGBA</span><span class="sh">"</span><span class="p">,</span> <span class="p">(</span><span class="n">GRID_SIZE</span><span class="p">,</span> <span class="n">GRID_SIZE</span><span class="p">))</span>
<span class="n">resized</span> <span class="o">=</span> <span class="n">target_img</span><span class="p">.</span><span class="nf">resize</span><span class="p">((</span><span class="mi">40</span><span class="p">,</span> <span class="mi">40</span><span class="p">),</span> <span class="n">resample</span><span class="o">=</span><span class="n">PIL</span><span class="p">.</span><span class="n">Image</span><span class="p">.</span><span class="n">BILINEAR</span><span class="p">)</span>
<span class="n">canvas</span><span class="p">.</span><span class="nf">paste</span><span class="p">(</span><span class="n">resized</span><span class="p">,</span> <span class="p">((</span><span class="n">GRID_SIZE</span> <span class="o">-</span> <span class="mi">40</span><span class="p">)</span> <span class="o">//</span> <span class="mi">2</span><span class="p">,)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">resized</span><span class="p">)</span>

<span class="n">target_rgba</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">canvas</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span> <span class="o">/</span> <span class="mf">255.0</span>
<span class="n">TARGET_TENSOR</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">from_numpy</span><span class="p">(</span><span class="n">target_rgba</span><span class="p">).</span><span class="nf">permute</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">).</span><span class="nf">unsqueeze</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">imshow</span><span class="p">(</span><span class="n">target_rgba</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Target Pattern (🐣)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/target.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="4-neural-cellular-automaton-model">4. Neural Cellular Automaton Model</h2>

<p>The core of our system is a small convolutional network that acts as the local update rule.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">NeuralCA</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Neural Cellular Automaton with Sobel perception and residual updates.</span><span class="sh">"""</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">channels</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">16</span><span class="p">,</span> <span class="n">hidden</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">128</span><span class="p">,</span> <span class="n">dropout_p</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.5</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">channels</span> <span class="o">=</span> <span class="n">channels</span>
        <span class="n">self</span><span class="p">.</span><span class="n">dropout_p</span> <span class="o">=</span> <span class="n">dropout_p</span>
        
        <span class="n">sobel_x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">tensor</span><span class="p">([[</span><span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">],</span> <span class="p">[</span><span class="o">-</span><span class="mf">2.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">],</span> <span class="p">[</span><span class="o">-</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">]])</span>
        <span class="n">sobel_y</span> <span class="o">=</span> <span class="n">sobel_x</span><span class="p">.</span><span class="nf">t</span><span class="p">()</span>
        <span class="n">kx</span> <span class="o">=</span> <span class="n">sobel_x</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">repeat</span><span class="p">(</span><span class="n">channels</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">ky</span> <span class="o">=</span> <span class="n">sobel_y</span><span class="p">.</span><span class="nf">view</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">repeat</span><span class="p">(</span><span class="n">channels</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
        
        <span class="n">self</span><span class="p">.</span><span class="n">perception</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Conv2d</span><span class="p">(</span><span class="n">channels</span><span class="p">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">channels</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">groups</span><span class="o">=</span><span class="n">channels</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">perception</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="nf">copy_</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">kx</span><span class="p">,</span> <span class="n">ky</span><span class="p">],</span> <span class="mi">0</span><span class="p">))</span>
        <span class="n">self</span><span class="p">.</span><span class="n">perception</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="nf">requires_grad_</span><span class="p">(</span><span class="bp">False</span><span class="p">)</span>
        
        <span class="n">self</span><span class="p">.</span><span class="n">update_net</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="nc">Conv2d</span><span class="p">(</span><span class="mi">3</span> <span class="o">*</span> <span class="n">channels</span><span class="p">,</span> <span class="n">hidden</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="nc">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="nc">Conv2d</span><span class="p">(</span><span class="n">hidden</span><span class="p">,</span> <span class="n">channels</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
        <span class="p">)</span>
        
        <span class="n">nn</span><span class="p">.</span><span class="n">init</span><span class="p">.</span><span class="nf">zeros_</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">update_net</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">weight</span><span class="p">)</span>
        <span class="n">nn</span><span class="p">.</span><span class="n">init</span><span class="p">.</span><span class="nf">zeros_</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">update_net</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">bias</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">alive_mask</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">Compute mask of cells that should remain alive.</span><span class="sh">"""</span>
        <span class="n">alpha</span> <span class="o">=</span> <span class="n">x</span><span class="p">[:,</span> <span class="mi">3</span><span class="p">:</span><span class="mi">4</span><span class="p">]</span>
        <span class="n">neighborhood_alive</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">max_pool2d</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">stride</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="nf">return </span><span class="p">(</span><span class="n">neighborhood_alive</span> <span class="o">&gt;</span> <span class="mf">0.1</span><span class="p">).</span><span class="nf">float</span><span class="p">()</span>
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">training</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
        <span class="n">gradients</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">perception</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">perception</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">x</span><span class="p">,</span> <span class="n">gradients</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">delta</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">update_net</span><span class="p">(</span><span class="n">perception</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">training</span><span class="p">:</span>
            <span class="n">mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">rand_like</span><span class="p">(</span><span class="n">delta</span><span class="p">[:,</span> <span class="p">:</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">-</span> <span class="n">self</span><span class="p">.</span><span class="n">dropout_p</span><span class="p">)).</span><span class="nf">float</span><span class="p">()</span>
            <span class="n">delta</span> <span class="o">=</span> <span class="n">delta</span> <span class="o">*</span> <span class="n">mask</span>
        
        <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">delta</span>
        <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="nf">alive_mask</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">x</span><span class="p">[:,</span> <span class="p">:</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">clamp</span><span class="p">(</span><span class="n">x</span><span class="p">[:,</span> <span class="p">:</span><span class="mi">4</span><span class="p">],</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">x</span>


<span class="k">def</span> <span class="nf">seed_state</span><span class="p">(</span><span class="n">channels</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">16</span><span class="p">,</span> <span class="n">batch</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Create initial state with single alive cell at center.</span><span class="sh">"""</span>
    <span class="n">state</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">channels</span><span class="p">,</span> <span class="n">GRID_SIZE</span><span class="p">,</span> <span class="n">GRID_SIZE</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)</span>
    <span class="n">center</span> <span class="o">=</span> <span class="n">GRID_SIZE</span> <span class="o">//</span> <span class="mi">2</span>
    <span class="n">state</span><span class="p">[:,</span> <span class="mi">3</span><span class="p">:,</span> <span class="n">center</span><span class="p">,</span> <span class="n">center</span><span class="p">]</span> <span class="o">=</span> <span class="mf">1.0</span>
    <span class="k">return</span> <span class="n">state</span>


<span class="k">def</span> <span class="nf">to_rgba</span><span class="p">(</span><span class="n">state_tensor</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Convert state tensor to RGBA image.</span><span class="sh">"""</span>
    <span class="n">rgba</span> <span class="o">=</span> <span class="n">state_tensor</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:</span><span class="mi">4</span><span class="p">].</span><span class="nf">detach</span><span class="p">().</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
    <span class="n">rgba</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">moveaxis</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">clip</span><span class="p">(</span><span class="n">rgba</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">rgba</span>


<span class="n">model</span> <span class="o">=</span> <span class="nc">NeuralCA</span><span class="p">().</span><span class="nf">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
<span class="n">test_state</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">()</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Model parameters: </span><span class="si">{</span><span class="nf">sum</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="nf">numel</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">())</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Seed state shape: </span><span class="si">{</span><span class="n">test_state</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">test_state</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Initial Seed State</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/seed.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="5-training-strategy-the-sample-pool-method">5. Training Strategy: The Sample Pool Method</h2>

<p>Traditional RNN training on long sequences is memory-intensive. Instead, let’s use a “sample pool” approach:</p>

<ol>
  <li>Maintain a pool of 256 CA states at various stages of development</li>
  <li>Each training step: sample a batch, evolve for 8-128 steps, compute loss</li>
  <li>Replace worst-performing sample with fresh seed to maintain diversity</li>
  <li>This implicitly trains the pattern to be a <strong>stable attractor</strong></li>
</ol>

<p><strong>Why does the pool help?</strong> Without it, back-propagation would always start from the <em>same</em> point on the trajectory (typically the pristine seed) making the optimisation highly myopic and leading to attractors that only look correct at a single time stamp. The pool therefore acts as a replay buffer $\mathcal D={\mathbf S^{(k)}}_{k=1}^{N}$ that continually covers a <em>diverse orbit</em> of the current policy $g_{\boldsymbol\theta}$. Sampling from $\mathcal D$ yields the Monte Carlo estimator</p>

\[\hat{\mathcal L}(\boldsymbol\theta)=\frac{1}{B}\sum_{b=1}^{B}
\bigl\|
\pi_{\text{rgba}}\!\bigl(g_{\boldsymbol\theta}^{\circ T_b}(\mathbf S_b)\bigr)-\mathbf X^\star
\bigr\|_2^2,\qquad
(\mathbf S_b,T_b)\sim\mathrm{Uniform}(\mathcal D)\times\mathcal T,\]

<p>where $B=16$ in our experiments. After every gradient step we <em>refresh</em> the worst performer (identified by the highest loss) with a clean seed. This simple heuristic prevents the buffer from collapsing to trivial or dead states and provides an ever-present <em>exploration pressure</em>.</p>

<p>From a control-theoretic perspective the optimisation shapes $g_{\boldsymbol\theta}$ such that the target image becomes an <em>asymptotically stable fixed point</em>. Small perturbations (Section 7) are equivalent to bounded disturbances, and the observed convergence empirically demonstrates that the learned CA implements a <strong>robust attractor basin</strong> around $\mathbf X^\star$.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">TrainingConfig</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Configuration for NCA training experiment.</span><span class="sh">"""</span>
    <span class="n">channels</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">16</span>
    <span class="n">hidden</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">128</span>
    <span class="n">dropout_p</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.5</span>
    <span class="n">lr</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e-3</span>
    <span class="n">pool_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">256</span>
    <span class="n">batch_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">16</span>
    <span class="n">steps</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">8000</span>
    <span class="n">rollout_start</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">8</span>
    <span class="n">rollout_growth</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">16</span>
    <span class="n">rollout_cap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">128</span>
    <span class="n">growth_frequency</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2000</span>

    <span class="k">def</span> <span class="nf">rollout_length</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">iteration</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">Progressive rollout schedule: start short, grow longer for stability.</span><span class="sh">"""</span>
        <span class="k">return</span> <span class="nf">min</span><span class="p">(</span>
            <span class="n">self</span><span class="p">.</span><span class="n">rollout_cap</span><span class="p">,</span>
            <span class="n">self</span><span class="p">.</span><span class="n">rollout_start</span> <span class="o">+</span> <span class="p">(</span><span class="n">iteration</span> <span class="o">//</span> <span class="n">self</span><span class="p">.</span><span class="n">growth_frequency</span><span class="p">)</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">rollout_growth</span>
        <span class="p">)</span>


<span class="k">def</span> <span class="nf">train_nca</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="n">TrainingConfig</span><span class="p">,</span> <span class="n">save_artifacts</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Train Neural CA with comprehensive diagnostics.</span><span class="sh">"""</span>
    
    <span class="n">model</span> <span class="o">=</span> <span class="nc">NeuralCA</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">channels</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">hidden</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">dropout_p</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
    <span class="n">optimizer</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="nc">Adam</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">(),</span> <span class="n">lr</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">lr</span><span class="p">)</span>
    <span class="n">scheduler</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="nc">MultiStepLR</span><span class="p">(</span>
        <span class="n">optimizer</span><span class="p">,</span> <span class="p">[</span><span class="nf">int</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">steps</span> <span class="o">*</span> <span class="mf">0.4</span><span class="p">),</span> <span class="nf">int</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">steps</span> <span class="o">*</span> <span class="mf">0.7</span><span class="p">)],</span> <span class="n">gamma</span><span class="o">=</span><span class="mf">0.3</span>
    <span class="p">)</span>
    
    <span class="n">pool</span> <span class="o">=</span> <span class="p">[</span><span class="nf">seed_state</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">channels</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">pool_size</span><span class="p">)]</span>
    
    <span class="n">loss_history</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">grad_history</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">alive_history</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">channel_variance_history</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">weight_snapshots</span> <span class="o">=</span> <span class="p">{}</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Training for </span><span class="si">{</span><span class="n">config</span><span class="p">.</span><span class="n">steps</span><span class="si">}</span><span class="s"> iterations...</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span>
    
    <span class="k">for</span> <span class="n">iteration</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">steps</span><span class="p">):</span>
        <span class="n">indices</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">choice</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">pool_size</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">replace</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">batch</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">pool</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">indices</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
        
        <span class="n">rollout_steps</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randint</span><span class="p">(</span>
            <span class="n">config</span><span class="p">.</span><span class="nf">rollout_length</span><span class="p">(</span><span class="n">iteration</span><span class="p">)</span> <span class="o">//</span> <span class="mi">2</span><span class="p">,</span>
            <span class="n">config</span><span class="p">.</span><span class="nf">rollout_length</span><span class="p">(</span><span class="n">iteration</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="p">)</span>
        
        <span class="n">model</span><span class="p">.</span><span class="nf">train</span><span class="p">()</span>
        <span class="n">state</span> <span class="o">=</span> <span class="n">batch</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">rollout_steps</span><span class="p">):</span>
            <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        
        <span class="n">loss</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">mse_loss</span><span class="p">(</span><span class="n">state</span><span class="p">[:,</span> <span class="p">:</span><span class="mi">4</span><span class="p">],</span> <span class="n">TARGET_TENSOR</span><span class="p">.</span><span class="nf">expand_as</span><span class="p">(</span><span class="n">state</span><span class="p">[:,</span> <span class="p">:</span><span class="mi">4</span><span class="p">]))</span>
        
        <span class="n">optimizer</span><span class="p">.</span><span class="nf">zero_grad</span><span class="p">()</span>
        <span class="n">loss</span><span class="p">.</span><span class="nf">backward</span><span class="p">()</span>
        <span class="n">grad_norm</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">nn</span><span class="p">.</span><span class="n">utils</span><span class="p">.</span><span class="nf">clip_grad_norm_</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">(),</span> <span class="n">max_norm</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="n">optimizer</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span>
        <span class="n">scheduler</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span>
        
        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">pool_idx</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">indices</span><span class="p">):</span>
            <span class="n">pool</span><span class="p">[</span><span class="n">pool_idx</span><span class="p">]</span> <span class="o">=</span> <span class="n">state</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">].</span><span class="nf">detach</span><span class="p">()</span>
        
        <span class="n">worst_idx</span> <span class="o">=</span> <span class="n">indices</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="nf">argmax</span><span class="p">(</span><span class="n">loss</span><span class="p">.</span><span class="nf">detach</span><span class="p">())]</span>
        <span class="n">pool</span><span class="p">[</span><span class="n">worst_idx</span><span class="p">]</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">channels</span><span class="p">)</span>
        
        <span class="n">loss_history</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">loss</span><span class="p">.</span><span class="nf">item</span><span class="p">())</span>
        <span class="n">grad_history</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">grad_norm</span><span class="p">.</span><span class="nf">item</span><span class="p">())</span>
        <span class="n">alive_count</span> <span class="o">=</span> <span class="p">(</span><span class="n">state</span><span class="p">[:,</span> <span class="mi">3</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mf">0.1</span><span class="p">).</span><span class="nf">float</span><span class="p">().</span><span class="nf">sum</span><span class="p">().</span><span class="nf">item</span><span class="p">()</span>
        <span class="n">alive_history</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">alive_count</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">iteration</span> <span class="o">%</span> <span class="mi">1000</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">channel_var</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="nf">var</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]).</span><span class="nf">detach</span><span class="p">().</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
            <span class="n">channel_variance_history</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">channel_var</span><span class="p">)</span>

        <span class="n">progress</span> <span class="o">=</span> <span class="n">iteration</span> <span class="o">/</span> <span class="n">config</span><span class="p">.</span><span class="n">steps</span>
        <span class="k">if</span> <span class="nf">any</span><span class="p">(</span><span class="nf">abs</span><span class="p">(</span><span class="n">progress</span> <span class="o">-</span> <span class="n">p</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mf">1e-3</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.4</span><span class="p">,</span> <span class="mf">0.7</span><span class="p">])</span> <span class="ow">or</span> <span class="n">iteration</span> <span class="o">==</span> <span class="n">config</span><span class="p">.</span><span class="n">steps</span> <span class="o">-</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">weights</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">p</span><span class="p">.</span><span class="nf">detach</span><span class="p">().</span><span class="nf">flatten</span><span class="p">()</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">()]).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
            <span class="n">weight_snapshots</span><span class="p">[</span><span class="nf">int</span><span class="p">(</span><span class="n">progress</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)]</span> <span class="o">=</span> <span class="n">weights</span>
        
        <span class="nf">if </span><span class="p">(</span><span class="n">iteration</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="mi">1000</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">elapsed</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Step </span><span class="si">{</span><span class="n">iteration</span> <span class="o">+</span> <span class="mi">1</span><span class="si">:</span><span class="mi">5</span><span class="n">d</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">config</span><span class="p">.</span><span class="n">steps</span><span class="si">}</span><span class="s"> | </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">Loss: </span><span class="si">{</span><span class="n">loss</span><span class="p">.</span><span class="nf">item</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s"> | </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">Grad: </span><span class="si">{</span><span class="n">grad_norm</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s"> | </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">Rollout: </span><span class="si">{</span><span class="n">rollout_steps</span><span class="si">:</span><span class="mi">2</span><span class="n">d</span><span class="si">}</span><span class="s"> | </span><span class="sh">"</span>
                  <span class="sa">f</span><span class="sh">"</span><span class="s">Time: </span><span class="si">{</span><span class="n">elapsed</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">total_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Training completed in </span><span class="si">{</span><span class="n">total_time</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">s</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">results</span> <span class="o">=</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">loss_history</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">loss_history</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">grad_history</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">grad_history</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">alive_history</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">alive_history</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">channel_variance_history</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">channel_variance_history</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">weight_snapshots</span><span class="sh">"</span><span class="p">:</span> <span class="n">weight_snapshots</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">config</span><span class="sh">"</span><span class="p">:</span> <span class="n">config</span>
    <span class="p">}</span>
    
    <span class="k">if</span> <span class="n">save_artifacts</span><span class="p">:</span>
        <span class="n">np</span><span class="p">.</span><span class="nf">savez</span><span class="p">(</span>
            <span class="n">FIG_DIR</span> <span class="o">/</span> <span class="sh">"</span><span class="s">training_metrics.npz</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">loss</span><span class="o">=</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">loss_history</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">grad_norm</span><span class="o">=</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">grad_history</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">alive_cells</span><span class="o">=</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">alive_history</span><span class="sh">"</span><span class="p">]</span>
        <span class="p">)</span>
        
        <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
        
        <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">semilogy</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">loss_history</span><span class="sh">"</span><span class="p">])</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Training Loss</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Iteration</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">MSE Loss</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">semilogy</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">grad_history</span><span class="sh">"</span><span class="p">])</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Gradient Norm</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Iteration</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">L2 Norm</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">plot</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">alive_history</span><span class="sh">"</span><span class="p">])</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Alive Cells</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Iteration</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Count</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/training_curves.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="k">return</span> <span class="n">results</span>

<span class="n">config</span> <span class="o">=</span> <span class="nc">TrainingConfig</span><span class="p">(</span><span class="n">steps</span><span class="o">=</span><span class="mi">8000</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="nf">train_nca</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
<span class="n">trained_model</span> <span class="o">=</span> <span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">]</span>
</code></pre></div></div>

<h2 id="6-growth-dynamics-analysis">6. Growth Dynamics Analysis</h2>

<p>Let’s analyze how the pattern develops from seed to target over time.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">analyze_growth_dynamics</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span> <span class="n">steps</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">128</span><span class="p">,</span> <span class="n">save_gif</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Analyze and visualize growth dynamics.</span><span class="sh">"""</span>
    
    <span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>
    <span class="n">states</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">metrics</span> <span class="o">=</span> <span class="p">{</span><span class="sh">"</span><span class="s">ssim</span><span class="sh">"</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">"</span><span class="s">psnr</span><span class="sh">"</span><span class="p">:</span> <span class="p">[],</span> <span class="sh">"</span><span class="s">alive_cells</span><span class="sh">"</span><span class="p">:</span> <span class="p">[]}</span>
    
    <span class="n">state</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">()</span>
    <span class="n">target_np</span> <span class="o">=</span> <span class="n">TARGET_TENSOR</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">permute</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
    
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">steps</span><span class="p">):</span>
            <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
            <span class="n">states</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">())</span>
            
            <span class="n">current_img</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">clamp</span><span class="p">(</span><span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:</span><span class="mi">4</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">).</span><span class="nf">permute</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
            <span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">ssim</span><span class="sh">"</span><span class="p">].</span><span class="nf">append</span><span class="p">(</span><span class="nf">ssim</span><span class="p">(</span><span class="n">current_img</span><span class="p">,</span> <span class="n">target_np</span><span class="p">,</span> <span class="n">channel_axis</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">data_range</span><span class="o">=</span><span class="mf">1.0</span><span class="p">))</span>
            <span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">psnr</span><span class="sh">"</span><span class="p">].</span><span class="nf">append</span><span class="p">(</span><span class="nf">psnr</span><span class="p">(</span><span class="n">target_np</span><span class="p">,</span> <span class="n">current_img</span><span class="p">,</span> <span class="n">data_range</span><span class="o">=</span><span class="mf">1.0</span><span class="p">))</span>
            <span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">alive_cells</span><span class="sh">"</span><span class="p">].</span><span class="nf">append</span><span class="p">((</span><span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mf">0.1</span><span class="p">).</span><span class="nf">float</span><span class="p">().</span><span class="nf">sum</span><span class="p">().</span><span class="nf">item</span><span class="p">())</span>
    
    <span class="n">n_frames</span> <span class="o">=</span> <span class="mi">16</span>
    <span class="n">frame_indices</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">states</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">n_frames</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    
    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
    <span class="n">axes</span> <span class="o">=</span> <span class="n">axes</span><span class="p">.</span><span class="nf">flatten</span><span class="p">()</span>
    
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">frame_indices</span><span class="p">):</span>
        <span class="n">axes</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">states</span><span class="p">[</span><span class="n">idx</span><span class="p">]))</span>
        <span class="n">axes</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">axes</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Step </span><span class="si">{</span><span class="n">idx</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">suptitle</span><span class="p">(</span><span class="sh">"</span><span class="s">Growth Progression</span><span class="sh">"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/growth_montage.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">plot</span><span class="p">(</span><span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">ssim</span><span class="sh">"</span><span class="p">])</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Structural Similarity (SSIM)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Step</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">SSIM</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">plot</span><span class="p">(</span><span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">psnr</span><span class="sh">"</span><span class="p">])</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Peak Signal-to-Noise Ratio</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Step</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">PSNR (dB)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">plot</span><span class="p">(</span><span class="n">metrics</span><span class="p">[</span><span class="sh">"</span><span class="s">alive_cells</span><span class="sh">"</span><span class="p">])</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Living Cell Count</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Step</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Cells</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/convergence_metrics.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="k">if</span> <span class="n">save_gif</span><span class="p">:</span>
        <span class="n">gif_frames</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">states</span><span class="p">),</span> <span class="mi">2</span><span class="p">):</span>
            <span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">states</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="o">*</span> <span class="mi">255</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
            <span class="n">gif_frames</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span>
        
        <span class="n">imageio</span><span class="p">.</span><span class="nf">mimsave</span><span class="p">(</span><span class="n">FIG_DIR</span> <span class="o">/</span> <span class="sh">"</span><span class="s">growth_animation.gif</span><span class="sh">"</span><span class="p">,</span> <span class="n">gif_frames</span><span class="p">,</span> <span class="n">fps</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Saved growth animation: </span><span class="si">{</span><span class="n">FIG_DIR</span> <span class="o">/</span> <span class="sh">'</span><span class="s">growth_animation.gif</span><span class="sh">'</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">states</span><span class="p">,</span> <span class="n">metrics</span>


<span class="n">growth_states</span><span class="p">,</span> <span class="n">growth_metrics</span> <span class="o">=</span> <span class="nf">analyze_growth_dynamics</span><span class="p">(</span><span class="n">trained_model</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="7-regeneration-capability">7. Regeneration Capability</h2>

<p>A hallmark of biological systems is the ability to regenerate damaged tissue. Let’s test our NCA’s regenerative properties.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_regeneration</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span> <span class="n">damage_radius</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">12</span><span class="p">,</span> <span class="n">healing_steps</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">96</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Test the model</span><span class="sh">'</span><span class="s">s ability to regenerate after damage.</span><span class="sh">"""</span>
    
    <span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>
    
    <span class="n">state</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">()</span>
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">80</span><span class="p">):</span>
            <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    
    <span class="n">pre_damage</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">()</span>
    
    <span class="n">center</span> <span class="o">=</span> <span class="n">GRID_SIZE</span> <span class="o">//</span> <span class="mi">2</span>
    <span class="n">y</span><span class="p">,</span> <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">meshgrid</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">GRID_SIZE</span><span class="p">),</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">GRID_SIZE</span><span class="p">),</span> <span class="n">indexing</span><span class="o">=</span><span class="sh">"</span><span class="s">ij</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">damage_mask</span> <span class="o">=</span> <span class="p">((</span><span class="n">x</span> <span class="o">-</span> <span class="n">center</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span> <span class="o">+</span> <span class="p">(</span><span class="n">y</span> <span class="o">-</span> <span class="n">center</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">damage_radius</span><span class="o">**</span><span class="mi">2</span>
    <span class="n">state</span><span class="p">[:,</span> <span class="p">:,</span> <span class="n">damage_mask</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>
    
    <span class="n">damaged</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">()</span>
    
    <span class="n">healing_states</span> <span class="o">=</span> <span class="p">[</span><span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">()]</span>
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">healing_steps</span><span class="p">):</span>
            <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
            <span class="n">healing_states</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">())</span>
    
    <span class="n">healed</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="nf">clone</span><span class="p">()</span>
    
    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">pre_damage</span><span class="p">))</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Before Damage</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">damaged</span><span class="p">))</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">After Damage (r=</span><span class="si">{</span><span class="n">damage_radius</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">healed</span><span class="p">))</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">After Healing (</span><span class="si">{</span><span class="n">healing_steps</span><span class="si">}</span><span class="s"> steps)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">diff</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="n">pre_damage</span> <span class="o">-</span> <span class="n">healed</span><span class="p">).</span><span class="nf">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">3</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="n">diff</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">(),</span> <span class="n">cmap</span><span class="o">=</span><span class="sh">"</span><span class="s">hot</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">3</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Recovery Difference</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">3</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">suptitle</span><span class="p">(</span><span class="sh">"</span><span class="s">Regeneration Test</span><span class="sh">"</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/regeneration_test.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="n">gif_frames</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">state</span> <span class="ow">in</span> <span class="n">healing_states</span><span class="p">[::</span><span class="mi">2</span><span class="p">]:</span>
        <span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> <span class="o">*</span> <span class="mi">255</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
        <span class="n">gif_frames</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span>
    
    <span class="n">imageio</span><span class="p">.</span><span class="nf">mimsave</span><span class="p">(</span><span class="n">FIG_DIR</span> <span class="o">/</span> <span class="sh">"</span><span class="s">healing_animation.gif</span><span class="sh">"</span><span class="p">,</span> <span class="n">gif_frames</span><span class="p">,</span> <span class="n">fps</span><span class="o">=</span><span class="mi">6</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Saved healing animation: </span><span class="si">{</span><span class="n">FIG_DIR</span> <span class="o">/</span> <span class="sh">'</span><span class="s">healing_animation.gif</span><span class="sh">'</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">pre_damage</span><span class="p">,</span> <span class="n">damaged</span><span class="p">,</span> <span class="n">healed</span><span class="p">,</span> <span class="n">healing_states</span>

<span class="n">pre_dmg</span><span class="p">,</span> <span class="n">damaged</span><span class="p">,</span> <span class="n">healed</span><span class="p">,</span> <span class="n">healing_sequence</span> <span class="o">=</span> <span class="nf">test_regeneration</span><span class="p">(</span><span class="n">trained_model</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="8-nca-dynamics-analysis">8. NCA Dynamics Analysis</h2>

<p>Let’s conduct deeper analysis of the learned dynamics.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">analyze_nca_dynamics</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span> <span class="n">growth_states</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">],</span> <span class="n">results</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Perform analysis of NCA dynamics.</span><span class="sh">"""</span>
    
    <span class="n">channel_vars</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">channel_variance_history</span><span class="sh">"</span><span class="p">])</span>
    <span class="k">if</span> <span class="n">channel_vars</span><span class="p">.</span><span class="n">size</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
        <span class="n">sns</span><span class="p">.</span><span class="nf">heatmap</span><span class="p">(</span><span class="n">channel_vars</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="sh">"</span><span class="s">magma</span><span class="sh">"</span><span class="p">,</span> <span class="n">cbar_kws</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">label</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Variance</span><span class="sh">"</span><span class="p">})</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Channel Activity During Training</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Training Epoch (×1000)</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Channel Index</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/channel_activity.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="k">def</span> <span class="nf">radial_power_spectrum</span><span class="p">(</span><span class="n">image</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">Compute radially-averaged power spectrum.</span><span class="sh">"""</span>
        <span class="n">gray</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="k">if</span> <span class="n">image</span><span class="p">.</span><span class="n">ndim</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="n">image</span>
        <span class="n">F</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="nf">fftshift</span><span class="p">(</span><span class="nf">fft2</span><span class="p">(</span><span class="n">gray</span><span class="p">)))</span><span class="o">**</span><span class="mi">2</span>
        
        <span class="n">h</span><span class="p">,</span> <span class="n">w</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">shape</span>
        <span class="n">y</span><span class="p">,</span> <span class="n">x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">indices</span><span class="p">((</span><span class="n">h</span><span class="p">,</span> <span class="n">w</span><span class="p">))</span>
        <span class="n">r</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">((</span><span class="n">x</span> <span class="o">-</span> <span class="n">w</span><span class="o">//</span><span class="mi">2</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span> <span class="o">+</span> <span class="p">(</span><span class="n">y</span> <span class="o">-</span> <span class="n">h</span><span class="o">//</span><span class="mi">2</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
        
        <span class="n">r_max</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">w</span><span class="p">)</span> <span class="o">//</span> <span class="mi">2</span>
        <span class="n">power_profile</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">r_max</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">radius</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">r_max</span><span class="p">):</span>
            <span class="n">mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">r</span> <span class="o">==</span> <span class="n">radius</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">mask</span><span class="p">.</span><span class="nf">any</span><span class="p">():</span>
                <span class="n">power_profile</span><span class="p">[</span><span class="n">radius</span><span class="p">]</span> <span class="o">=</span> <span class="n">F</span><span class="p">[</span><span class="n">mask</span><span class="p">].</span><span class="nf">mean</span><span class="p">()</span>
        
        <span class="k">return</span> <span class="n">power_profile</span>
    
    <span class="n">spectra</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">state</span> <span class="ow">in</span> <span class="n">growth_states</span><span class="p">[::</span><span class="mi">8</span><span class="p">]:</span>
        <span class="n">rgb</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">clamp</span><span class="p">(</span><span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:</span><span class="mi">3</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
        <span class="n">spectrum</span> <span class="o">=</span> <span class="nf">radial_power_spectrum</span><span class="p">(</span><span class="n">rgb</span><span class="p">)</span>
        <span class="n">spectra</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">spectrum</span><span class="p">)</span>
    
    <span class="n">spectra_matrix</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">spectra</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">imshow</span><span class="p">(</span><span class="n">spectra_matrix</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">aspect</span><span class="o">=</span><span class="sh">"</span><span class="s">auto</span><span class="sh">"</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="sh">"</span><span class="s">viridis</span><span class="sh">"</span><span class="p">,</span> <span class="n">origin</span><span class="o">=</span><span class="sh">"</span><span class="s">lower</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">colorbar</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="sh">"</span><span class="s">Log Power</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Fourier Spectrum Evolution</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Growth Step (×8)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Spatial Frequency</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/spectrum_evolution.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="n">hidden_means</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">state</span> <span class="ow">in</span> <span class="n">growth_states</span><span class="p">:</span>
        <span class="n">hidden</span> <span class="o">=</span> <span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">:].</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
        <span class="n">mean_hidden</span> <span class="o">=</span> <span class="n">hidden</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">hidden</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">-</span><span class="mi">1</span><span class="p">).</span><span class="nf">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">hidden_means</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">mean_hidden</span><span class="p">)</span>
    
    <span class="n">hidden_array</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">array</span><span class="p">(</span><span class="n">hidden_means</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="n">hidden_array</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mi">2</span><span class="p">:</span>
        <span class="n">X</span> <span class="o">=</span> <span class="n">hidden_array</span><span class="p">.</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float64</span><span class="p">)</span>
        <span class="n">X</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">nan_to_num</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">nan</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">posinf</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">neginf</span><span class="o">=</span><span class="mf">0.0</span><span class="p">)</span>
        <span class="n">X</span> <span class="o">=</span> <span class="p">(</span><span class="n">X</span> <span class="o">-</span> <span class="n">X</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span> <span class="o">/</span> <span class="p">(</span><span class="n">X</span><span class="p">.</span><span class="nf">std</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-6</span><span class="p">)</span>
        <span class="n">X</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">clip</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="o">-</span><span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
        
        <span class="k">try</span><span class="p">:</span>
            <span class="n">pca</span> <span class="o">=</span> <span class="nc">PCA</span><span class="p">(</span><span class="n">n_components</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">trajectory</span> <span class="o">=</span> <span class="n">pca</span><span class="p">.</span><span class="nf">fit_transform</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
            
            <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">trajectory</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">trajectory</span><span class="p">[:,</span> <span class="mi">1</span><span class="p">],</span> <span class="sh">"</span><span class="s">b-</span><span class="sh">"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.7</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">scatter</span><span class="p">(</span><span class="n">trajectory</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">trajectory</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="n">c</span><span class="o">=</span><span class="sh">"</span><span class="s">green</span><span class="sh">"</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sh">"</span><span class="s">Start</span><span class="sh">"</span><span class="p">,</span> <span class="n">zorder</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">scatter</span><span class="p">(</span><span class="n">trajectory</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">trajectory</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="n">c</span><span class="o">=</span><span class="sh">"</span><span class="s">red</span><span class="sh">"</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sh">"</span><span class="s">End</span><span class="sh">"</span><span class="p">,</span> <span class="n">zorder</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Hidden State Trajectory (PCA)</span><span class="sh">"</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">PC1 (</span><span class="si">{</span><span class="n">pca</span><span class="p">.</span><span class="n">explained_variance_ratio_</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="o">%</span><span class="si">}</span><span class="s"> variance)</span><span class="sh">"</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">PC2 (</span><span class="si">{</span><span class="n">pca</span><span class="p">.</span><span class="n">explained_variance_ratio_</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="o">%</span><span class="si">}</span><span class="s"> variance)</span><span class="sh">"</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">legend</span><span class="p">()</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/hidden_trajectory.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
            <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">PCA analysis failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">label</span><span class="p">,</span> <span class="n">weights</span> <span class="ow">in</span> <span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">weight_snapshots</span><span class="sh">"</span><span class="p">].</span><span class="nf">items</span><span class="p">():</span>
        <span class="n">sns</span><span class="p">.</span><span class="nf">kdeplot</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s">% training</span><span class="sh">"</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.7</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Weight Distribution Evolution</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Weight Value</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Density</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">legend</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/weight_evolution.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
    
    <span class="n">damage_radii</span> <span class="o">=</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">]</span>
    <span class="n">recovery_times</span> <span class="o">=</span> <span class="p">[]</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Testing recovery performance...</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>
    <span class="n">target_np</span> <span class="o">=</span> <span class="n">TARGET_TENSOR</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">permute</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
    
    <span class="k">for</span> <span class="n">radius</span> <span class="ow">in</span> <span class="n">damage_radii</span><span class="p">:</span>
        <span class="n">state</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">()</span>
        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
            <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">80</span><span class="p">):</span>
                <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        
        <span class="n">center</span> <span class="o">=</span> <span class="n">GRID_SIZE</span> <span class="o">//</span> <span class="mi">2</span>
        <span class="n">y</span><span class="p">,</span> <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">meshgrid</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">GRID_SIZE</span><span class="p">),</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">GRID_SIZE</span><span class="p">),</span> <span class="n">indexing</span><span class="o">=</span><span class="sh">"</span><span class="s">ij</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">mask</span> <span class="o">=</span> <span class="p">((</span><span class="n">x</span> <span class="o">-</span> <span class="n">center</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span> <span class="o">+</span> <span class="p">(</span><span class="n">y</span> <span class="o">-</span> <span class="n">center</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">radius</span><span class="o">**</span><span class="mi">2</span>
        <span class="n">state</span><span class="p">[:,</span> <span class="p">:,</span> <span class="n">mask</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>
        
        <span class="n">steps</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">max_steps</span> <span class="o">=</span> <span class="mi">200</span>
        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
            <span class="k">while</span> <span class="n">steps</span> <span class="o">&lt;</span> <span class="n">max_steps</span><span class="p">:</span>
                <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
                <span class="n">steps</span> <span class="o">+=</span> <span class="mi">1</span>
                
                <span class="n">current_img</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">clamp</span><span class="p">(</span><span class="n">state</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:</span><span class="mi">4</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">).</span><span class="nf">permute</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">cpu</span><span class="p">().</span><span class="nf">numpy</span><span class="p">()</span>
                <span class="k">if</span> <span class="nf">ssim</span><span class="p">(</span><span class="n">current_img</span><span class="p">,</span> <span class="n">target_np</span><span class="p">,</span> <span class="n">channel_axis</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">data_range</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mf">0.9</span><span class="p">:</span>
                    <span class="k">break</span>
        
        <span class="n">recovery_times</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">steps</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  Damage radius </span><span class="si">{</span><span class="n">radius</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">steps</span><span class="si">}</span><span class="s"> steps to recover</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">damage_radii</span><span class="p">,</span> <span class="n">recovery_times</span><span class="p">,</span> <span class="sh">"</span><span class="s">o-</span><span class="sh">"</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">markersize</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Regeneration Performance</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Damage Radius (pixels)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Steps to Recover (SSIM &gt; 0.9)</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/recovery_performance.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>

<span class="nf">analyze_nca_dynamics</span><span class="p">(</span><span class="n">trained_model</span><span class="p">,</span> <span class="n">growth_states</span><span class="p">,</span> <span class="n">results</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="9-multiple-experimental-configurations">9. Multiple Experimental Configurations</h2>

<p>Let’s compare different training configurations to understand the impact of various hyperparameters.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">experiments</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nc">TrainingConfig</span><span class="p">(</span><span class="n">dropout_p</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">steps</span><span class="o">=</span><span class="mi">4000</span><span class="p">),</span>
    <span class="nc">TrainingConfig</span><span class="p">(</span><span class="n">dropout_p</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">steps</span><span class="o">=</span><span class="mi">4000</span><span class="p">),</span>
    <span class="nc">TrainingConfig</span><span class="p">(</span><span class="n">channels</span><span class="o">=</span><span class="mi">32</span><span class="p">,</span> <span class="n">hidden</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">steps</span><span class="o">=</span><span class="mi">4000</span><span class="p">),</span>
<span class="p">]</span>

<span class="n">experiment_names</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Baseline</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">High Dropout</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Large Model</span><span class="sh">"</span><span class="p">]</span>
<span class="n">experiment_results</span> <span class="o">=</span> <span class="p">[]</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Running comparative experiments...</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nf">zip</span><span class="p">(</span><span class="n">experiments</span><span class="p">,</span> <span class="n">experiment_names</span><span class="p">)):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">--- Experiment </span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> ---</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">results</span> <span class="o">=</span> <span class="nf">train_nca</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="n">save_artifacts</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">experiment_results</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span>

<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">results</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nf">zip</span><span class="p">(</span><span class="n">experiment_results</span><span class="p">,</span> <span class="n">experiment_names</span><span class="p">)):</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">semilogy</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">loss_history</span><span class="sh">"</span><span class="p">],</span> <span class="n">label</span><span class="o">=</span><span class="n">name</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Training Loss Comparison</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Iteration</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">MSE Loss</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">legend</span><span class="p">()</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>

<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">results</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nf">zip</span><span class="p">(</span><span class="n">experiment_results</span><span class="p">,</span> <span class="n">experiment_names</span><span class="p">)):</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">semilogy</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">grad_history</span><span class="sh">"</span><span class="p">],</span> <span class="n">label</span><span class="o">=</span><span class="n">name</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.8</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sh">"</span><span class="s">Gradient Norm Comparison</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">set_xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Iteration</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">set_ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">L2 Norm</span><span class="sh">"</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">legend</span><span class="p">()</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">grid</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>

<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">results</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nf">zip</span><span class="p">(</span><span class="n">experiment_results</span><span class="p">,</span> <span class="n">experiment_names</span><span class="p">)):</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">results</span><span class="p">[</span><span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>
    
    <span class="n">state</span> <span class="o">=</span> <span class="nf">seed_state</span><span class="p">()</span>
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">96</span><span class="p">):</span>
            <span class="n">state</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">training</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="p">].</span><span class="nf">imshow</span><span class="p">(</span><span class="nf">to_rgba</span><span class="p">(</span><span class="n">state</span><span class="p">))</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="p">].</span><span class="nf">set_title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Final Pattern: </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="n">i</span><span class="p">].</span><span class="nf">axis</span><span class="p">(</span><span class="sh">"</span><span class="s">off</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">experiments</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">:</span>
    <span class="n">fig</span><span class="p">.</span><span class="nf">delaxes</span><span class="p">(</span><span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">])</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">FIG_DIR</span><span class="si">}</span><span class="s">/experiment_comparison.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="10-experiment-results">10. Experiment Results</h2>

<div class="experiment-section">

<h3>Baseline</h3>

<div class="experiment-results-grid">
<img src="/assets/blog/nca/baseline/growth.gif" alt="Baseline Growth" />
<img src="/assets/blog/nca/baseline/channel_activity.png" alt="Baseline Channel Activity" />
<img src="/assets/blog/nca/baseline/hidden_pca.png" alt="Baseline Hidden PCA" />
<img src="/assets/blog/nca/baseline/hidden.gif" alt="Baseline Hidden" />
<img src="/assets/blog/nca/baseline/psnr_curve.png" alt="Baseline PSNR" />
<img src="/assets/blog/nca/baseline/regen.gif" alt="Baseline Regeneration" />
<img src="/assets/blog/nca/baseline/spectrum.png" alt="Baseline Spectrum" />
<img src="/assets/blog/nca/baseline/ssim_curve.png" alt="Baseline SSIM" />
<img src="/assets/blog/nca/baseline/weight_hist_evo.png" alt="Baseline Weight Evolution" />
</div>

</div>

<div class="experiment-section">

<h3>High Dropout</h3>

<div class="experiment-results-grid">
<img src="/assets/blog/nca/high_dropout/growth.gif" alt="High Dropout Growth" />
<img src="/assets/blog/nca/high_dropout/channel_activity.png" alt="High Dropout Channel Activity" />
<img src="/assets/blog/nca/high_dropout/hidden_pca.png" alt="High Dropout Hidden PCA" />
<img src="/assets/blog/nca/high_dropout/hidden.gif" alt="High Dropout Hidden" />
<img src="/assets/blog/nca/high_dropout/psnr_curve.png" alt="High Dropout PSNR" />
<img src="/assets/blog/nca/high_dropout/regen.gif" alt="High Dropout Regeneration" />
<img src="/assets/blog/nca/high_dropout/spectrum.png" alt="High Dropout Spectrum" />
<img src="/assets/blog/nca/high_dropout/ssim_curve.png" alt="High Dropout SSIM" />
<img src="/assets/blog/nca/high_dropout/weight_hist_evo.png" alt="High Dropout Weight Evolution" />
</div>

</div>

<div class="experiment-section">

<h3>Large Channels</h3>

<div class="experiment-results-grid">
<img src="/assets/blog/nca/large_channels/growth.gif" alt="Large Channels Growth" />
<img src="/assets/blog/nca/large_channels/channel_activity.png" alt="Large Channels Channel Activity" />
<img src="/assets/blog/nca/large_channels/hidden_pca.png" alt="Large Channels Hidden PCA" />
<img src="/assets/blog/nca/large_channels/hidden.gif" alt="Large Channels Hidden" />
<img src="/assets/blog/nca/large_channels/psnr_curve.png" alt="Large Channels PSNR" />
<img src="/assets/blog/nca/large_channels/regen.gif" alt="Large Channels Regeneration" />
<img src="/assets/blog/nca/large_channels/spectrum.png" alt="Large Channels Spectrum" />
<img src="/assets/blog/nca/large_channels/ssim_curve.png" alt="Large Channels SSIM" />
<img src="/assets/blog/nca/large_channels/weight_hist_evo.png" alt="Large Channels Weight Evolution" />
</div>

</div>

<p><strong>References:</strong></p>

<p>Mordvintsev, et al. “Growing Neural Cellular Automata.” Distill, 2020.</p>]]></content><author><name>Tomáš Kozák</name></author><category term="neural-cellular-automata" /><category term="sobel-gradient" /><category term="morphogenesis" /><summary type="html"><![CDATA[Intro to differentiable morphogenesis and emergent self-repair]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tomaskozak.com/assets/blog/nca/baseline/growth.gif" /><media:content medium="image" url="https://tomaskozak.com/assets/blog/nca/baseline/growth.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>