<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ertugrul</title>
    <description>The latest articles on DEV Community by Ertugrul (@ertugrulmutlu).</description>
    <link>https://dev.to/ertugrulmutlu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1342871%2F80fb281c-da45-4841-b3a7-762c28f06416.jpg</url>
      <title>DEV Community: Ertugrul</title>
      <link>https://dev.to/ertugrulmutlu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ertugrulmutlu"/>
    <language>en</language>
    <item>
      <title>From Lab to Code #4: Exploring Neural Network Efficiency with Python and PyTorch</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 09 Nov 2026 22:18:11 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch-1732</link>
      <guid>https://dev.to/ertugrulmutlu/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch-1732</guid>
      <description>&lt;p&gt;A larger neural network can often achieve better accuracy.&lt;/p&gt;

&lt;p&gt;But does that always make it the better model?&lt;/p&gt;

&lt;p&gt;When working with neural networks, it is easy to focus on a single metric: accuracy. We train a model, evaluate it on a test dataset, and compare the percentage of correct predictions.&lt;/p&gt;

&lt;p&gt;However, this leaves out an important engineering question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much computation do we need to achieve that accuracy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A small model might be slightly less accurate but require significantly fewer operations. Depending on the application, that trade-off can matter more than achieving the highest possible score.&lt;/p&gt;

&lt;p&gt;For the fourth project in my &lt;strong&gt;From Lab to Code&lt;/strong&gt; series, I wanted to explore this relationship by building an interactive environment for experimenting with convolutional neural networks.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Neural Network Efficiency Lab&lt;/strong&gt;, a Python and PyTorch application that compares three independently trained CNN models, visualizes their predictions, and shows how architectural decisions affect computational cost.&lt;/p&gt;

&lt;p&gt;Rather than building another digit-recognition demo, the goal was to investigate a more interesting question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How small can a neural network be while still delivering useful classification accuracy?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea: Accuracy Is Only Half the Story
&lt;/h2&gt;

&lt;p&gt;For this experiment, I used the MNIST handwritten-digit dataset.&lt;/p&gt;

&lt;p&gt;MNIST contains grayscale images of handwritten digits from 0 to 9. Each image has a resolution of 28 × 28 pixels.&lt;/p&gt;

&lt;p&gt;It is a relatively simple classification problem, which makes it useful for comparing small neural networks without requiring a large training infrastructure.&lt;/p&gt;

&lt;p&gt;I designed three convolutional neural networks with increasing capacity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Micro CNN:&lt;/strong&gt; A very small network designed for a restricted computation budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact CNN:&lt;/strong&gt; A moderately sized network with more convolutional channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wide CNN:&lt;/strong&gt; A larger network intended to investigate the accuracy gained from additional computation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three use the same basic architecture, dataset, and training procedure.&lt;/p&gt;

&lt;p&gt;The primary difference is the number of convolutional channels.&lt;/p&gt;

&lt;p&gt;This makes it easier to observe how increasing model capacity affects both accuracy and computational requirements.&lt;/p&gt;

&lt;p&gt;The project also includes an interactive interface where users can select trained models, draw digits, inspect predictions, visualize feature maps, and experiment with hypothetical CNN architectures.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Understanding the CNN Architecture
&lt;/h2&gt;

&lt;p&gt;A convolutional neural network processes an image by applying learned filters to local regions.&lt;/p&gt;

&lt;p&gt;Unlike a fully connected layer, where every output depends on every input, a convolutional layer reuses small filters across different image positions.&lt;/p&gt;

&lt;p&gt;These filters can learn patterns such as edges, strokes, or more complex arrangements of local features.&lt;/p&gt;

&lt;p&gt;For this project, I implemented a deliberately small architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Image
   1 × 28 × 28
        |
        v
Convolution 1
        |
       ReLU
        |
     MaxPool
        |
        v
Convolution 2
        |
       ReLU
        |
     MaxPool
        |
        v
      Flatten
        |
        v
   Linear Layer
        |
        v
    10 Logits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two convolutional layers extract features, while max pooling reduces the spatial resolution.&lt;/p&gt;

&lt;p&gt;Finally, a fully connected layer maps the extracted representation to ten output values, one for each digit.&lt;/p&gt;

&lt;p&gt;The implementation uses PyTorch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DigitCNN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kernel&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conv1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Conv2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxPool2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conv2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Conv2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxPool2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conv1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conv2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output consists of ten &lt;em&gt;logits&lt;/em&gt;, which are unnormalized classification scores.&lt;/p&gt;

&lt;p&gt;During training, these logits are passed to PyTorch's &lt;code&gt;CrossEntropyLoss&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During inference, softmax converts them into a probability distribution over the ten classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(class i) = exp(logit_i) / Σ exp(logit_j)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The predicted digit is the class with the highest probability.&lt;/p&gt;

&lt;p&gt;These probabilities are useful for inspecting the model's predictions, although they should not automatically be interpreted as perfectly calibrated confidence estimates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Change the Number of Channels?
&lt;/h3&gt;

&lt;p&gt;The number of convolutional channels determines how many filters a layer learns.&lt;/p&gt;

&lt;p&gt;A layer with more channels can represent a larger collection of features.&lt;/p&gt;

&lt;p&gt;However, more channels also mean additional learned parameters and arithmetic operations.&lt;/p&gt;

&lt;p&gt;This creates an interesting trade-off.&lt;/p&gt;

&lt;p&gt;A larger network may learn more useful features and improve classification accuracy, but the improvement might be relatively small compared with the additional computation.&lt;/p&gt;

&lt;p&gt;To investigate this, I kept the architecture structure consistent while changing its channel widths.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Measuring Computational Cost with MACs
&lt;/h2&gt;

&lt;p&gt;Accuracy is relatively easy to understand.&lt;/p&gt;

&lt;p&gt;We count the number of correct predictions and divide by the total number of evaluated examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accuracy = Correct Predictions / Total Predictions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But how should we measure the computational cost of a neural network?&lt;/p&gt;

&lt;p&gt;One useful approximation is the number of &lt;strong&gt;multiply-accumulate operations&lt;/strong&gt;, or MACs.&lt;/p&gt;

&lt;p&gt;A multiply-accumulate operation combines multiplication and addition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accumulator += weight × input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neural networks perform large numbers of these operations in convolutional and fully connected layers.&lt;/p&gt;

&lt;p&gt;Counting MACs provides a hardware-independent estimate of some of the arithmetic work required for a forward pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculating Convolutional MACs
&lt;/h3&gt;

&lt;p&gt;For a standard convolutional layer, the MAC count depends on the output dimensions, input channels, output channels, and kernel size.&lt;/p&gt;

&lt;p&gt;A simplified formula is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MACs = H_out × W_out × C_out × C_in × K²
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;H_out&lt;/code&gt; and &lt;code&gt;W_out&lt;/code&gt; are the output feature-map dimensions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C_in&lt;/code&gt; is the number of input channels.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C_out&lt;/code&gt; is the number of output channels.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;K&lt;/code&gt; is the width of a square convolution kernel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our architecture, the input has one channel and a resolution of 28 × 28.&lt;/p&gt;

&lt;p&gt;The first convolution preserves the spatial resolution through padding.&lt;/p&gt;

&lt;p&gt;After the first pooling operation, the image representation becomes 14 × 14.&lt;/p&gt;

&lt;p&gt;After the second pooling operation, it becomes 7 × 7.&lt;/p&gt;

&lt;p&gt;For convolutional channel counts &lt;code&gt;C1&lt;/code&gt; and &lt;code&gt;C2&lt;/code&gt;, and kernel size &lt;code&gt;K&lt;/code&gt;, the estimated costs are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conv1 = 28 × 28 × C1 × K²

Conv2 = 14 × 14 × C1 × C2 × K²

Dense = 7 × 7 × C2 × 10

Total MACs = Conv1 + Conv2 + Dense
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I implemented this directly in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mac_breakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;k2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kernel&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="n"&gt;conv1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k2&lt;/span&gt;
    &lt;span class="n"&gt;conv2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k2&lt;/span&gt;
    &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conv1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conv1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conv2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conv2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conv1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;conv2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to calculate the theoretical arithmetic cost of a model without executing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Important Limitation
&lt;/h3&gt;

&lt;p&gt;MACs are not the same as inference latency.&lt;/p&gt;

&lt;p&gt;The calculation does not include operations such as ReLU, pooling, softmax, memory access, or data movement.&lt;/p&gt;

&lt;p&gt;It also does not account for hardware-specific optimizations or parallel execution.&lt;/p&gt;

&lt;p&gt;Consequently, a model with fewer MACs is not guaranteed to be faster on every processor or GPU.&lt;/p&gt;

&lt;p&gt;For this project, MACs provide a transparent way to compare architectural complexity, not a benchmark of actual deployment performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Three Models, One Classification Task
&lt;/h2&gt;

&lt;p&gt;With the architecture and cost calculation in place, I created three CNN configurations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Conv Channels&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;MACs / Image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Micro CNN&lt;/td&gt;
&lt;td&gt;3 → 6&lt;/td&gt;
&lt;td&gt;3,148&lt;/td&gt;
&lt;td&gt;55,860&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compact CNN&lt;/td&gt;
&lt;td&gt;8 → 16&lt;/td&gt;
&lt;td&gt;9,098&lt;/td&gt;
&lt;td&gt;290,080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide CNN&lt;/td&gt;
&lt;td&gt;16 → 32&lt;/td&gt;
&lt;td&gt;20,490&lt;/td&gt;
&lt;td&gt;1,031,744&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three use 3 × 3 convolution kernels.&lt;/p&gt;

&lt;p&gt;The difference in computational cost is substantial.&lt;/p&gt;

&lt;p&gt;The Wide CNN requires approximately &lt;strong&gt;18.5 times as many MACs&lt;/strong&gt; as the Micro CNN.&lt;/p&gt;

&lt;p&gt;But does it achieve 18.5 times better classification performance?&lt;/p&gt;

&lt;p&gt;Of course, accuracy cannot scale in that way. Once a model is already performing well, each additional improvement becomes harder to achieve.&lt;/p&gt;

&lt;p&gt;The more useful question is how much accuracy we gain in exchange for that increased cost.&lt;/p&gt;

&lt;p&gt;To answer it, the models need to be trained and evaluated under comparable conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Training and Evaluating the Models
&lt;/h2&gt;

&lt;p&gt;For a fair comparison, I used the same training procedure for all three architectures.&lt;/p&gt;

&lt;p&gt;The official MNIST dataset was divided into:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Split&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Training&lt;/td&gt;
&lt;td&gt;54,000&lt;/td&gt;
&lt;td&gt;Learn model parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;Select the best training checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;Final evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The validation set was created from the official MNIST training split.&lt;/p&gt;

&lt;p&gt;The official test set was kept separate and used only for the final evaluation.&lt;/p&gt;

&lt;p&gt;This distinction matters because selecting a model based on its test accuracy would introduce information leakage into the evaluation process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Training Configuration
&lt;/h3&gt;

&lt;p&gt;The models were trained using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimizer:&lt;/strong&gt; Adam&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning rate:&lt;/strong&gt; 0.001&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch size:&lt;/strong&gt; 256&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss function:&lt;/strong&gt; Cross-entropy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training duration:&lt;/strong&gt; 4 epochs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random seed:&lt;/strong&gt; 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each model, I selected the checkpoint with the highest validation accuracy and then evaluated it on the official 10,000-image test set.&lt;/p&gt;

&lt;p&gt;The implementation uses the standard PyTorch training sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set_to_none&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cross-entropy provides a differentiable objective, while backpropagation calculates gradients for the trainable parameters.&lt;/p&gt;

&lt;p&gt;Adam uses these gradients to update the network weights.&lt;/p&gt;

&lt;p&gt;The key benefit of keeping the training procedure consistent is that we can focus on how changes to the architecture affect the observed results.&lt;/p&gt;

&lt;p&gt;However, the experiment uses only one training run per architecture. It is not a statistical comparison across multiple random seeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Experimental Results: More Computation, Smaller Gains
&lt;/h2&gt;

&lt;p&gt;After training, the three models achieved the following results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Test Accuracy&lt;/th&gt;
&lt;th&gt;MACs / Image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Micro CNN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.67%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55,860&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compact CNN&lt;/td&gt;
&lt;td&gt;97.92%&lt;/td&gt;
&lt;td&gt;290,080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide CNN&lt;/td&gt;
&lt;td&gt;98.63%&lt;/td&gt;
&lt;td&gt;1,031,744&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All accuracy values come from the held-out official MNIST test set.&lt;/p&gt;

&lt;p&gt;These results reveal the central trade-off behind the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro vs. Compact
&lt;/h3&gt;

&lt;p&gt;Moving from Micro to Compact increases the cost from 55,860 to 290,080 MACs.&lt;/p&gt;

&lt;p&gt;That is approximately &lt;strong&gt;5.2 times the computation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The test accuracy improves from 96.67% to 97.92% — a gain of &lt;strong&gt;1.25 percentage points&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro vs. Wide
&lt;/h3&gt;

&lt;p&gt;Moving from Micro to Wide requires approximately &lt;strong&gt;18.5 times the computation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yet accuracy improves by only &lt;strong&gt;1.96 percentage points&lt;/strong&gt;, from 96.67% to 98.63%.&lt;/p&gt;

&lt;p&gt;This does not mean the larger model is useless.&lt;/p&gt;

&lt;p&gt;For some applications, reducing the error rate can be extremely valuable, even if the improvement in accuracy appears small.&lt;/p&gt;

&lt;p&gt;But it does show why accuracy should not be considered in isolation.&lt;/p&gt;

&lt;p&gt;The best model depends on the available resources and the requirements of the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Constrained-Compute Experiment
&lt;/h3&gt;

&lt;p&gt;To make the trade-off more concrete, I introduced an illustrative budget of &lt;strong&gt;75,000 MACs per image&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Under this constraint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Micro CNN fits within the budget.&lt;/li&gt;
&lt;li&gt;Compact CNN exceeds the budget.&lt;/li&gt;
&lt;li&gt;Wide CNN exceeds the budget by a much larger margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Micro CNN achieves 96.67% test accuracy while requiring only 55,860 MACs.&lt;/p&gt;

&lt;p&gt;In this particular experiment, it provides a strong result under the selected arithmetic budget.&lt;/p&gt;

&lt;p&gt;The 75,000-MAC threshold is an educational design constraint rather than a limit derived from real hardware.&lt;/p&gt;

&lt;p&gt;The experiment demonstrates how a computational budget can influence architecture selection.&lt;/p&gt;

&lt;p&gt;It does not establish which network would be fastest on a particular CPU, GPU, or embedded device.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Making the Experiments Interactive
&lt;/h2&gt;

&lt;p&gt;After training the models, I wanted to make the results easier to explore.&lt;/p&gt;

&lt;p&gt;A table of numbers can show that one network is larger than another.&lt;/p&gt;

&lt;p&gt;But an interactive application can help explain what those differences mean.&lt;/p&gt;

&lt;p&gt;For the frontend, I created a browser interface connected to a local Python backend using FastAPI.&lt;/p&gt;

&lt;p&gt;The backend loads the saved PyTorch model checkpoints and performs inference.&lt;/p&gt;

&lt;p&gt;The browser displays the predictions, model metrics, and visualizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing the Models
&lt;/h3&gt;

&lt;p&gt;The first part of the application compares Micro, Compact, and Wide CNN.&lt;/p&gt;

&lt;p&gt;Users can inspect each model's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test accuracy.&lt;/li&gt;
&lt;li&gt;Number of parameters.&lt;/li&gt;
&lt;li&gt;Estimated MAC count.&lt;/li&gt;
&lt;li&gt;Architecture configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An accuracy-versus-computation visualization makes the trade-off easier to see.&lt;/p&gt;

&lt;p&gt;Instead of considering accuracy and model size separately, users can observe how much additional computation is associated with each improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drawing a Digit
&lt;/h3&gt;

&lt;p&gt;The application also includes an interactive drawing canvas.&lt;/p&gt;

&lt;p&gt;Users can draw a handwritten digit using their mouse and send it to one of the trained models.&lt;/p&gt;

&lt;p&gt;The image is converted into a 28 × 28 grayscale representation and normalized using the same convention as the training data.&lt;/p&gt;

&lt;p&gt;The selected model produces ten logits.&lt;/p&gt;

&lt;p&gt;Softmax converts them into class probabilities, allowing the interface to display the predicted digit and its probability distribution.&lt;/p&gt;

&lt;p&gt;The application also includes twenty attributed MNIST test examples for trying the models on actual dataset samples.&lt;/p&gt;

&lt;p&gt;An interesting limitation appears when using the drawing canvas.&lt;/p&gt;

&lt;p&gt;Handwritten digits produced with a mouse can look quite different from images in MNIST.&lt;/p&gt;

&lt;p&gt;Differences in stroke width, positioning, and drawing style can affect the prediction.&lt;/p&gt;

&lt;p&gt;A high accuracy on the MNIST test dataset therefore does not guarantee equally high accuracy on arbitrary user drawings.&lt;/p&gt;

&lt;p&gt;This is a small example of &lt;strong&gt;distribution shift&lt;/strong&gt;: a model is evaluated on inputs that differ from those it encountered during training.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking Inside the CNN
&lt;/h3&gt;

&lt;p&gt;I also wanted to show something beyond the final classification result.&lt;/p&gt;

&lt;p&gt;What does a convolutional layer actually produce?&lt;/p&gt;

&lt;p&gt;To investigate this, the application extracts intermediate feature maps from the first two convolutional layers.&lt;/p&gt;

&lt;p&gt;For a selected input, users can inspect visualizations of the learned activations.&lt;/p&gt;

&lt;p&gt;The model returns both its final classification logits and intermediate features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;f1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conv1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;f2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conv2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application displays a selection of feature channels from each layer.&lt;/p&gt;

&lt;p&gt;These visualizations provide a way to observe how the input is transformed as it passes through the network.&lt;/p&gt;

&lt;p&gt;However, the displayed feature maps are normalized individually for visualization.&lt;/p&gt;

&lt;p&gt;This means their brightness should not be interpreted as a directly comparable activation magnitude across channels.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Exploring Architectures Without Retraining
&lt;/h2&gt;

&lt;p&gt;One of the more useful features of the project is the &lt;strong&gt;Architecture Sandbox&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here, users can change the number of convolutional channels and the kernel size.&lt;/p&gt;

&lt;p&gt;The application immediately recalculates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of trainable parameters.&lt;/li&gt;
&lt;li&gt;Estimated MACs for each layer.&lt;/li&gt;
&lt;li&gt;Total arithmetic cost.&lt;/li&gt;
&lt;li&gt;Whether the hypothetical architecture fits the selected MAC budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the relationship between architecture and computation directly observable.&lt;/p&gt;

&lt;p&gt;For example, increasing the output channels of the second convolution affects the number of operations required by that layer.&lt;/p&gt;

&lt;p&gt;Increasing the kernel size also increases computation because each output value depends on more input positions.&lt;/p&gt;

&lt;p&gt;But there is an important separation between the architecture calculator and the trained model comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changing the architecture does not automatically tell us its accuracy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An untrained network has no measured classification performance.&lt;/p&gt;

&lt;p&gt;To evaluate a new architecture, we would need to train it, select an appropriate checkpoint using validation data, and measure its performance on held-out test data.&lt;/p&gt;

&lt;p&gt;Therefore, the Architecture Sandbox only reports properties that can be calculated analytically.&lt;/p&gt;

&lt;p&gt;It does not invent an accuracy score for hypothetical models.&lt;/p&gt;

&lt;p&gt;This distinction was important to preserve because the project is intended to demonstrate actual engineering trade-offs, not simulated performance claims.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. What the Experiment Does and Does Not Prove
&lt;/h2&gt;

&lt;p&gt;The results suggest that a small CNN can achieve strong MNIST classification accuracy with a relatively low arithmetic cost.&lt;/p&gt;

&lt;p&gt;However, several limitations are worth considering.&lt;/p&gt;

&lt;p&gt;First, the models were trained using a single random seed.&lt;/p&gt;

&lt;p&gt;A broader comparison would require repeated training runs to estimate performance variation.&lt;/p&gt;

&lt;p&gt;Second, the experiment uses only three configurations of the same basic architecture.&lt;/p&gt;

&lt;p&gt;It does not establish the optimal CNN for MNIST or the best architecture under a particular computation budget.&lt;/p&gt;

&lt;p&gt;Third, MACs estimate only part of the computational work.&lt;/p&gt;

&lt;p&gt;Actual inference speed depends on hardware, software libraries, memory behavior, and other implementation details.&lt;/p&gt;

&lt;p&gt;Finally, MNIST is a relatively simple classification benchmark.&lt;/p&gt;

&lt;p&gt;The relationship between model capacity and accuracy may look very different for larger images, more complex datasets, or real-world deployment conditions.&lt;/p&gt;

&lt;p&gt;The purpose of this project is therefore not to introduce a new neural network architecture or claim state-of-the-art performance.&lt;/p&gt;

&lt;p&gt;It is to build a transparent, reproducible experiment in which the trade-offs between network size, arithmetic cost, and classification performance can be explored.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;This project reinforced a few important ideas about machine learning engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, accuracy alone is not enough to evaluate a model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two models can have similar accuracy while requiring very different amounts of computation.&lt;/p&gt;

&lt;p&gt;Depending on the application, the smaller model may be the more practical choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the relationship between model capacity and accuracy is not linear.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this experiment, increasing computational cost substantially produced progressively smaller accuracy gains.&lt;/p&gt;

&lt;p&gt;The largest model performed best on the test set, but the smallest model was much more computationally economical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, mathematical cost estimates are useful, but they must be interpreted correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MAC counts make it possible to compare arithmetic complexity without hardware-dependent measurements.&lt;/p&gt;

&lt;p&gt;However, they should not be confused with actual runtime performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourth, intermediate visualizations make neural networks easier to understand.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inspecting convolutional feature maps helps connect the mathematical architecture to the representations produced during inference.&lt;/p&gt;

&lt;p&gt;And finally, &lt;strong&gt;a clear separation between measured results and hypothetical configurations is essential&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We can calculate the theoretical cost of a network architecture immediately.&lt;/p&gt;

&lt;p&gt;But we cannot know how accurately that architecture will classify images until it has been properly trained and evaluated.&lt;/p&gt;

&lt;p&gt;That difference is central to designing reliable machine learning experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;For this fourth From Lab to Code project, I wanted to investigate a question that extends beyond handwritten-digit recognition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we balance model accuracy against computational requirements?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By training three CNN architectures under comparable conditions, measuring their classification performance, and calculating their arithmetic costs, I built a small environment for exploring this trade-off.&lt;/p&gt;

&lt;p&gt;The interactive application adds another perspective by allowing users to compare actual models, classify their own drawings, inspect intermediate feature maps, and experiment with architectural changes.&lt;/p&gt;

&lt;p&gt;The most interesting result was that the smallest network achieved 96.67% test accuracy using only 55,860 estimated MACs per image.&lt;/p&gt;

&lt;p&gt;A significantly larger network improved accuracy to 98.63%, but required approximately 18.5 times more MACs.&lt;/p&gt;

&lt;p&gt;Neither model is universally better.&lt;/p&gt;

&lt;p&gt;The appropriate choice depends on the accuracy requirements, the computational constraints, and the environment in which the model will be used.&lt;/p&gt;

&lt;p&gt;That is the engineering idea I wanted to make visible.&lt;/p&gt;

&lt;p&gt;And it follows the same motivation behind &lt;strong&gt;From Lab to Code&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build something small, understand the underlying principles, make the results observable, and share the implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full source code, trained model checkpoints, reproducible experiment results, tests, and setup instructions are available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/04-neural-network-efficiency" rel="noopener noreferrer"&gt;Explore Neural Network Efficiency Lab on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The implementation and interface were developed independently. The project uses the public MNIST dataset with separate attribution for the included samples; no university course code or instructional materials are redistributed.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>pytorch</category>
    </item>
    <item>
      <title>From Lab to Code #3: Building an Interactive Robot Cell with Python and Three.js</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 09 Nov 2026 21:19:52 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/from-lab-to-code-3-building-an-interactive-robot-cell-with-python-and-threejs-2953</link>
      <guid>https://dev.to/ertugrulmutlu/from-lab-to-code-3-building-an-interactive-robot-cell-with-python-and-threejs-2953</guid>
      <description>&lt;p&gt;In an automated production line, a robotic arm might look like the most interesting part of the system.&lt;/p&gt;

&lt;p&gt;But moving the arm is only one part of the problem.&lt;/p&gt;

&lt;p&gt;How does the robot know when an object has arrived? How does it decide where to place it? And how do the conveyor, sensors, gripper, and controller coordinate their actions?&lt;/p&gt;

&lt;p&gt;More importantly, what happens when a sensor is disconnected or connected to the wrong input?&lt;/p&gt;

&lt;p&gt;These questions involve several fundamental concepts in robotics and industrial automation: &lt;strong&gt;kinematics, finite state machines, sensor feedback, and digital I/O communication.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the third project in my &lt;strong&gt;From Lab to Code&lt;/strong&gt; series, I decided to explore these concepts by building a small interactive robot-cell simulation.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Robot Cell Playground&lt;/strong&gt;, a Python-controlled robotic sorting cell with a 3D interface built using Flask, HTML, CSS, JavaScript, and Three.js.&lt;/p&gt;

&lt;p&gt;The interesting part is not just watching a robot sort colored boxes. Users can modify the digital connections between components, introduce faults, and observe how those changes affect the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea: A Small Automated Sorting Cell
&lt;/h2&gt;

&lt;p&gt;The simulation represents a simple production scenario.&lt;/p&gt;

&lt;p&gt;A conveyor transports colored boxes toward a robotic arm. Once a box reaches the pickup position, the controller uses simulated sensors to identify it and determine its destination.&lt;/p&gt;

&lt;p&gt;The robot then picks up the box, transfers it to the correct location, and returns to its initial position.&lt;/p&gt;

&lt;p&gt;There are three possible destinations: red, green, and blue.&lt;/p&gt;

&lt;p&gt;Under normal conditions, each box should reach the destination corresponding to its color.&lt;/p&gt;

&lt;p&gt;To make this possible, the application combines several components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A three-degree-of-freedom robotic arm.&lt;/li&gt;
&lt;li&gt;A conveyor and object generator.&lt;/li&gt;
&lt;li&gt;Idealized position and color sensors.&lt;/li&gt;
&lt;li&gt;A finite-state-machine controller.&lt;/li&gt;
&lt;li&gt;Configurable digital input/output connections.&lt;/li&gt;
&lt;li&gt;A 3D visualization of the complete cell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project uses a simplified geometric model rather than a full industrial physics simulator.&lt;/p&gt;

&lt;p&gt;This was a deliberate decision. I wanted to focus on understanding how the individual components interact without introducing the complexity of a complete robotics framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Robot Kinematics: From Coordinates to Joint Angles
&lt;/h2&gt;

&lt;p&gt;Before controlling a robot, we need a mathematical representation of its movement.&lt;/p&gt;

&lt;p&gt;In robotics, &lt;strong&gt;kinematics&lt;/strong&gt; describes positions, orientations, and motion without considering the forces that produce them.&lt;/p&gt;

&lt;p&gt;Two important problems are forward kinematics and inverse kinematics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forward Kinematics
&lt;/h3&gt;

&lt;p&gt;Forward kinematics answers a straightforward question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If we know the robot's joint angles, where is its end effector?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this project, I implemented a simplified robotic arm with three degrees of freedom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Base rotation (yaw).&lt;/li&gt;
&lt;li&gt;Shoulder rotation.&lt;/li&gt;
&lt;li&gt;Elbow rotation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The arm consists of two links moving in a vertical plane, while the base rotates that plane around the vertical axis.&lt;/p&gt;

&lt;p&gt;Let the upper-arm length be &lt;code&gt;L1&lt;/code&gt;, the forearm length be &lt;code&gt;L2&lt;/code&gt;, and the joint angles be &lt;code&gt;q1&lt;/code&gt;, &lt;code&gt;q2&lt;/code&gt;, and &lt;code&gt;q3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The horizontal distance from the base is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;r = L1*cos(q2) + L2*cos(q2 + q3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vertical position is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z = shoulder_height
    + L1*sin(q2)
    + L2*sin(q2 + q3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the base rotation, we obtain the horizontal coordinates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x = base_x + r*cos(q1)
y = base_y + r*sin(q1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding Python implementation is relatively compact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward_kinematics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;yaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shoulder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elbow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;angles&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UPPER_ARM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shoulder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FOREARM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shoulder&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;elbow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SHOULDER_HEIGHT&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;UPPER_ARM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shoulder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FOREARM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shoulder&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;elbow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;BASE_X&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;BASE_Y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the simulation to calculate the wrist position from the current joint configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inverse Kinematics
&lt;/h3&gt;

&lt;p&gt;The opposite problem is more useful when commanding the robot to move somewhere.&lt;/p&gt;

&lt;p&gt;Suppose a box has arrived at a known pickup coordinate.&lt;/p&gt;

&lt;p&gt;We know where the robot should move, but we do not yet know the joint angles required to reach that position.&lt;/p&gt;

&lt;p&gt;This is the inverse kinematics problem.&lt;/p&gt;

&lt;p&gt;For the two-link arm, we can calculate the elbow angle using the law of cosines.&lt;/p&gt;

&lt;p&gt;First, we determine the horizontal distance &lt;code&gt;r&lt;/code&gt; and height &lt;code&gt;h&lt;/code&gt; relative to the shoulder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;r = sqrt((x - base_x)^2 + (y - base_y)^2)

h = z - shoulder_height
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cos(q3) = (r^2 + h^2 - L1^2 - L2^2) / (2*L1*L2)

q3 = -acos(cos(q3))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the elbow angle is known, the shoulder angle can be calculated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q2 = atan2(h, r)
     - atan2(L2*sin(q3), L1 + L2*cos(q3))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q1 = atan2(y - base_y, x - base_x)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation selects one elbow configuration and checks whether the requested target lies within the robot's geometric workspace.&lt;/p&gt;

&lt;p&gt;A useful test is to apply inverse kinematics to a target and then reconstruct the position using forward kinematics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;angles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inverse_kinematics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reconstructed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forward_kinematics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reconstructed coordinates should match the target within floating-point precision.&lt;/p&gt;

&lt;p&gt;This gives us a simple mathematical consistency check.&lt;/p&gt;

&lt;p&gt;Of course, reaching a coordinate geometrically does not mean the movement is physically safe. Real robots must also consider collisions, mechanical joint limits, actuator dynamics, and other constraints.&lt;/p&gt;

&lt;p&gt;For this project, the geometric model is enough to explore the relationship between joint angles and target positions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Finite State Machines: Teaching the Robot What to Do Next
&lt;/h2&gt;

&lt;p&gt;Kinematics tells us how to reach a position.&lt;/p&gt;

&lt;p&gt;But it does not tell us &lt;strong&gt;when&lt;/strong&gt; the robot should move there.&lt;/p&gt;

&lt;p&gt;A sorting cell must coordinate multiple operations in a specific order.&lt;/p&gt;

&lt;p&gt;The robot should not attempt to pick up a box before it arrives. It should not transfer the box before activating the gripper. And it should not generate a new box while the previous sorting operation is still in progress.&lt;/p&gt;

&lt;p&gt;For this, I implemented a &lt;strong&gt;finite state machine (FSM)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An FSM represents a system using a finite number of states and transitions between them.&lt;/p&gt;

&lt;p&gt;In Robot Cell Playground, the normal operating sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDLE
  |
  v
CONVEYING
  |
  v
DETECTED
  |
  v
APPROACH
  |
  v
PICKING
  |
  v
LIFTING
  |
  v
TRANSFER
  |
  v
PLACING
  |
  v
RETRACTING
  |
  v
RETURNING
  |
  +------&amp;gt; IDLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state has a specific responsibility.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;CONVEYING&lt;/code&gt; advances the box toward the pickup point, while &lt;code&gt;DETECTED&lt;/code&gt; confirms that the necessary sensor information has been received.&lt;/p&gt;

&lt;p&gt;The movement states handle approaching, picking, lifting, transferring, and placing the box.&lt;/p&gt;

&lt;p&gt;A separate &lt;code&gt;FAULT&lt;/code&gt; state handles certain failed operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the State Machine
&lt;/h3&gt;

&lt;p&gt;The simulation is controlled by the &lt;code&gt;RobotCell&lt;/code&gt; class in Python.&lt;/p&gt;

&lt;p&gt;The controller maintains the current phase, box position, robot angles, sensor information, wiring configuration, and sorting statistics.&lt;/p&gt;

&lt;p&gt;A particularly interesting part is the conveyor logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONVEYING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inputs&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Belt.OnOff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;PICKUP_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inputs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.ItemAvailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;item_present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.Color&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capitalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detected_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DETECTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the conveyor does not move unconditionally.&lt;/p&gt;

&lt;p&gt;It moves only when the corresponding digital input is active.&lt;/p&gt;

&lt;p&gt;Similarly, the controller proceeds to the detection state only when an object is available and exactly one color input is active.&lt;/p&gt;

&lt;p&gt;These conditions become particularly important when we start modifying the wiring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the Motion Smooth
&lt;/h3&gt;

&lt;p&gt;For the robot's joint movements, I used a cubic smoothstep interpolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s(t) = 3t² - 2t³
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;t&lt;/code&gt; varies from 0 to 1 during a movement phase.&lt;/p&gt;

&lt;p&gt;Each joint angle is interpolated as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q(t) = q_start + s(t)*(q_target - q_start)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces smoother visual transitions than immediately jumping between configurations.&lt;/p&gt;

&lt;p&gt;It is not a physically constrained trajectory planner, but it provides a simple way to animate the robot while keeping the motion calculations inside the Python controller.&lt;/p&gt;

&lt;p&gt;The important distinction is that &lt;strong&gt;kinematics determines where the robot should move, while the state machine determines what the robot should do next.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Digital I/O: Connecting the Components
&lt;/h2&gt;

&lt;p&gt;This became one of the most interesting parts of the project.&lt;/p&gt;

&lt;p&gt;An automated robot cell consists of multiple components that need to communicate.&lt;/p&gt;

&lt;p&gt;The conveyor needs to receive a start command. The robot needs to know when a box arrives. The gripper must receive an activation signal, and the controller needs information from the color sensor.&lt;/p&gt;

&lt;p&gt;In this simulation, these interactions are represented using digital input/output signals.&lt;/p&gt;

&lt;p&gt;A digital signal has two possible logical values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOW  = 0
HIGH = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A signal travels from an output to an input.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Robot.StartStop  ------&amp;gt;  Belt.OnOff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the robot controller produces a command, and the conveyor receives it.&lt;/p&gt;

&lt;p&gt;The same principle applies to the simulated gripper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Robot.Grasp  ------&amp;gt;  Gripper.Vacuum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensor communication flows in the other direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PositionSensor.Detect
          |
          v
Robot.ItemAvailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the sensor provides the output, while the robot controller receives it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the Connections Editable
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding every connection directly into the controller, I implemented an independent digital I/O routing system.&lt;/p&gt;

&lt;p&gt;The application stores a mapping between device inputs and their connected outputs.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFAULT_CONNECTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source.Trigger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.Generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Belt.OnOff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.StartStop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gripper.Vacuum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.Grasp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.ItemAvailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PositionSensor.Detect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.ColorRed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ColorSensor.Red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.ColorGreen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ColorSensor.Green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Robot.ColorBlue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ColorSensor.Blue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a shortened version of the default configuration. The complete system also includes signals for the three sorting destinations.&lt;/p&gt;

&lt;p&gt;The key is that the controller does not directly read the physical source signals.&lt;/p&gt;

&lt;p&gt;Instead, it receives values through the current connection mapping.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IOBoard&lt;/code&gt; class resolves these connections using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;INPUTS&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each input, the function finds the connected output and retrieves its logical value.&lt;/p&gt;

&lt;p&gt;If the input has no valid connection, its value becomes &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This introduces an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A controller output can be HIGH while the corresponding device input remains LOW.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, the robot might send a command to start the conveyor, but the conveyor will not respond if the connection is missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Interactive Wiring Lab
&lt;/h3&gt;

&lt;p&gt;To make this behavior visible, I added an interactive wiring panel to the application.&lt;/p&gt;

&lt;p&gt;The panel contains ten output ports and ten input ports.&lt;/p&gt;

&lt;p&gt;Users can connect outputs to inputs, disconnect existing connections, restore the default configuration, and inspect the live digital signal levels.&lt;/p&gt;

&lt;p&gt;The connections are visualized as lines between ports.&lt;/p&gt;

&lt;p&gt;HIGH and LOW signals are displayed separately from the connection status, so an inactive signal is not confused with a disconnected wire.&lt;/p&gt;

&lt;p&gt;The Python controller also provides diagnostic messages explaining which signals are missing during particular operations.&lt;/p&gt;

&lt;p&gt;This is a simplified logical routing model, not a complete PLC programming environment.&lt;/p&gt;

&lt;p&gt;However, it gives users a way to explore a central principle of automation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The behavior of a system depends not only on the commands being generated, but also on how those commands and sensor signals are connected.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Fault Injection: What Happens When the Wiring Is Wrong?
&lt;/h2&gt;

&lt;p&gt;Once the connections became editable, I wanted to explore another question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens if we deliberately connect something incorrectly?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This turns the simulation into a small fault-injection environment.&lt;/p&gt;

&lt;p&gt;Instead of only observing the correct sorting sequence, we can investigate why the system stops working or produces incorrect results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 1: Disconnecting the Conveyor
&lt;/h3&gt;

&lt;p&gt;Under normal conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Robot.StartStop  ------&amp;gt;  Belt.OnOff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the controller enters the &lt;code&gt;CONVEYING&lt;/code&gt; state, it activates its &lt;code&gt;StartStop&lt;/code&gt; output.&lt;/p&gt;

&lt;p&gt;Now imagine disconnecting that connection.&lt;/p&gt;

&lt;p&gt;The output still becomes HIGH, but the conveyor input remains LOW.&lt;/p&gt;

&lt;p&gt;The result is straightforward: the box stays at the beginning of the conveyor.&lt;/p&gt;

&lt;p&gt;The controller remains in the conveying phase because the box never reaches the pickup position.&lt;/p&gt;

&lt;p&gt;The interesting part is that the controller still issues the correct command.&lt;/p&gt;

&lt;p&gt;The problem is the missing connection between the controller and the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 2: Disconnecting the Position Sensor
&lt;/h3&gt;

&lt;p&gt;Normally, the position sensor sends a detection signal to the robot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PositionSensor.Detect
          |
          v
Robot.ItemAvailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now disconnect this signal.&lt;/p&gt;

&lt;p&gt;The conveyor still transports the box toward the pickup point.&lt;/p&gt;

&lt;p&gt;But when the box arrives, the robot controller does not receive the corresponding detection signal.&lt;/p&gt;

&lt;p&gt;As a result, the controller cannot proceed to the pickup operation.&lt;/p&gt;

&lt;p&gt;The robot waits even though the box has physically reached the correct location in the simulated scene.&lt;/p&gt;

&lt;p&gt;This demonstrates how missing feedback can prevent an otherwise functioning system from progressing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 3: Disconnecting the Gripper
&lt;/h3&gt;

&lt;p&gt;The gripper is activated through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Robot.Grasp  ------&amp;gt;  Gripper.Vacuum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this connection is removed, the robot can still approach the box.&lt;/p&gt;

&lt;p&gt;However, when it attempts to pick up the object, the gripper does not receive the activation signal.&lt;/p&gt;

&lt;p&gt;In this implementation, the controller recognizes the missing signal and enters the &lt;code&gt;FAULT&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;The simulation must be reset before another sorting cycle can begin.&lt;/p&gt;

&lt;p&gt;This is a simplified fault-handling mechanism, but it clearly illustrates the dependency between control commands and actuator inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 4: Swapping the Color Sensor Connections
&lt;/h3&gt;

&lt;p&gt;This was my favorite experiment.&lt;/p&gt;

&lt;p&gt;Imagine that the simulated color sensor correctly identifies a red box.&lt;/p&gt;

&lt;p&gt;The sensor's red output becomes HIGH.&lt;/p&gt;

&lt;p&gt;Now suppose the red and blue sensor connections are swapped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ColorSensor.Red
          |
          v
Robot.ColorBlue

ColorSensor.Blue
          |
          v
Robot.ColorRed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual box is still red.&lt;/p&gt;

&lt;p&gt;The color sensor still detects red.&lt;/p&gt;

&lt;p&gt;But the controller receives that signal through its blue input.&lt;/p&gt;

&lt;p&gt;Consequently, it identifies the object as blue and selects the blue destination.&lt;/p&gt;

&lt;p&gt;The robot then picks up the red box and transfers it into the blue bin.&lt;/p&gt;

&lt;p&gt;The application records this as a mis-sort.&lt;/p&gt;

&lt;p&gt;This is particularly interesting because &lt;strong&gt;the robot's movement can be geometrically correct while its overall decision is wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The kinematics are still functioning. The state machine is still progressing. The robot reaches the destination selected by the controller.&lt;/p&gt;

&lt;p&gt;The error originates from the information entering the controller through an incorrect connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing the Fault Scenarios
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default wiring&lt;/td&gt;
&lt;td&gt;Boxes reach their matching bins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conveyor disconnected&lt;/td&gt;
&lt;td&gt;Box does not advance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Position sensor disconnected&lt;/td&gt;
&lt;td&gt;Controller waits for detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gripper disconnected&lt;/td&gt;
&lt;td&gt;Gripper fault is triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red/blue inputs swapped&lt;/td&gt;
&lt;td&gt;Boxes can reach the wrong destination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These scenarios are also covered by automated tests.&lt;/p&gt;

&lt;p&gt;For example, one integration test verifies that a red box incorrectly routed to the blue destination increments the mis-sort counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missorted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pick_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin_contents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of merely checking whether the application displays an error, the test verifies that the simulated sorting behavior actually changed.&lt;/p&gt;

&lt;p&gt;For me, this is one of the most valuable parts of the project.&lt;/p&gt;

&lt;p&gt;A small wiring error can affect the outcome of an entire automation process, even when the underlying movement algorithms are functioning correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Visualizing the System with Three.js
&lt;/h2&gt;

&lt;p&gt;After implementing the Python controller, I wanted to make its behavior visible and interactive.&lt;/p&gt;

&lt;p&gt;For the frontend, I used HTML, CSS, JavaScript, and Three.js.&lt;/p&gt;

&lt;p&gt;The backend uses Flask to expose the simulation state to the browser.&lt;/p&gt;

&lt;p&gt;One design decision was particularly important: &lt;strong&gt;Python remains responsible for the robot's behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend does not independently decide when the robot should move, when a box has been detected, or which destination should be selected.&lt;/p&gt;

&lt;p&gt;Instead, it renders the state calculated by Python.&lt;/p&gt;

&lt;p&gt;The architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser Interface
        |
        | HTTP / JSON
        v
     Flask API
        |
        v
 Python Controller
        |
        +-- Kinematics
        +-- State Machine
        +-- Sensors
        +-- Digital I/O Routing
        |
        v
 Simulation State
        |
        v
 Three.js Visualization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also inspect the &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/blob/main/03-robot-cell/results/system-overview.svg" rel="noopener noreferrer"&gt;system architecture diagram&lt;/a&gt; included in the repository.&lt;/p&gt;

&lt;p&gt;The browser periodically sends simulation-step requests to Flask.&lt;/p&gt;

&lt;p&gt;Python advances the controller and returns the updated state, including joint positions, box coordinates, sensor values, connection states, and sorting statistics.&lt;/p&gt;

&lt;p&gt;Three.js uses this information to update the scene.&lt;/p&gt;

&lt;p&gt;The user can observe the robotic arm, conveyor, colored boxes, and sorting destinations in 3D.&lt;/p&gt;

&lt;p&gt;The interface also allows camera rotation, zooming, simulation-speed adjustments, and selection of the next generated box color.&lt;/p&gt;

&lt;p&gt;Below the 3D scene, the Wiring Lab displays the digital connections and their live signal levels.&lt;/p&gt;

&lt;p&gt;An event stream provides additional information about state transitions, detection events, successful sorting operations, and faults.&lt;/p&gt;

&lt;p&gt;This separation between simulation and visualization has another advantage: the Python control logic can be tested independently of the browser.&lt;/p&gt;

&lt;p&gt;It also makes the system easier to understand. The 3D scene shows what happens, while the controller determines why it happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Reproducible Results and Testing
&lt;/h2&gt;

&lt;p&gt;An interactive simulation is useful for exploring individual scenarios.&lt;/p&gt;

&lt;p&gt;However, I also wanted to verify that the underlying controller behaves consistently without manually operating the interface.&lt;/p&gt;

&lt;p&gt;For this reason, I implemented a deterministic demonstration using a fixed random seed.&lt;/p&gt;

&lt;p&gt;The simulation generates and sorts twelve colored boxes using the default connections.&lt;/p&gt;

&lt;p&gt;The recorded result is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Successfully sorted boxes&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Green&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simulated elapsed time&lt;/td&gt;
&lt;td&gt;112.32 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The random seed for this run was &lt;code&gt;13&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These values represent simulated outcomes, not measurements from a physical production line.&lt;/p&gt;

&lt;p&gt;The elapsed time is measured in simulation time, so it should not be interpreted as an industrial throughput benchmark.&lt;/p&gt;

&lt;p&gt;Beyond this demonstration, the project includes unit and integration tests covering kinematics, sensor behavior, sorting cycles, input validation, digital wiring, and fault scenarios.&lt;/p&gt;

&lt;p&gt;An important test checks that forward and inverse kinematics produce consistent coordinates.&lt;/p&gt;

&lt;p&gt;Other tests verify that missing connections prevent operations from progressing, invalid connections are rejected, and swapped color inputs produce an actual mis-sort.&lt;/p&gt;

&lt;p&gt;These tests do not make the application hardware-ready, but they help ensure that the simplified model follows its intended rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What This Simulation Does Not Model
&lt;/h2&gt;

&lt;p&gt;Robot Cell Playground is an educational simulation, not a physics-accurate industrial robot controller.&lt;/p&gt;

&lt;p&gt;The robotic arm has three geometric degrees of freedom and uses analytical kinematics.&lt;/p&gt;

&lt;p&gt;The system does not model actuator forces, motor torque, realistic gripper contact, collisions, detailed joint constraints, or obstacle avoidance.&lt;/p&gt;

&lt;p&gt;The sensors are idealized. The color sensor returns a discrete color signal instead of processing real camera data.&lt;/p&gt;

&lt;p&gt;Similarly, the digital I/O connections model logical Boolean values, without electrical characteristics, signal propagation delays, or PLC scan cycles.&lt;/p&gt;

&lt;p&gt;The robot's motion uses smooth joint-angle interpolation rather than a complete industrial trajectory planner.&lt;/p&gt;

&lt;p&gt;These limitations are intentional.&lt;/p&gt;

&lt;p&gt;The objective is to make specific engineering concepts easier to observe and experiment with, not to reproduce the full complexity of a real manufacturing environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;This project helped me connect several engineering concepts that are often studied independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Robotic movement is only one part of automation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forward and inverse kinematics provide a mathematical way to position the robot, but they do not determine when a conveyor should stop, whether a box is available, or which destination the robot should select.&lt;/p&gt;

&lt;p&gt;That requires a control strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finite state machines make the behavior easier to understand.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breaking the sorting process into distinct states creates clear responsibilities and transition conditions.&lt;/p&gt;

&lt;p&gt;It also makes debugging more manageable because the controller's current state provides information about what it is trying to accomplish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Digital connections are part of the system's behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest takeaways came from the Wiring Lab.&lt;/p&gt;

&lt;p&gt;A controller may execute its programmed logic correctly and still produce the wrong result if its inputs contain incorrect information.&lt;/p&gt;

&lt;p&gt;The swapped-color experiment demonstrates this directly: the robot follows its controller's decision, but the decision itself is based on an incorrectly routed signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fault injection makes hidden dependencies visible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When everything works, the importance of individual sensor and actuator connections is easy to overlook.&lt;/p&gt;

&lt;p&gt;Disconnecting them reveals which operations depend on which signals.&lt;/p&gt;

&lt;p&gt;Finally, separating the Python simulation from the Three.js visualization made the project easier to test and reason about.&lt;/p&gt;

&lt;p&gt;The controller is responsible for the logic, while the frontend provides an interactive way to observe the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;With Robot Cell Playground, I wanted to explore how relatively simple mathematical and programming concepts can work together to form an automated system.&lt;/p&gt;

&lt;p&gt;The project began with a robotic arm and a conveyor, but the more interesting questions appeared when I started considering communication between components.&lt;/p&gt;

&lt;p&gt;What happens when the controller receives no signal? What if it receives the wrong one? Can the robot complete a movement correctly but still make an incorrect sorting decision?&lt;/p&gt;

&lt;p&gt;By combining kinematics, finite state machines, editable digital I/O connections, and fault injection, the simulation provides a small environment for investigating these questions.&lt;/p&gt;

&lt;p&gt;The project is intentionally limited in scope. It is not designed to replace industrial robotics software or operate physical machinery.&lt;/p&gt;

&lt;p&gt;Instead, it follows the idea behind &lt;strong&gt;From Lab to Code&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build something small, understand the engineering behind it, make the behavior observable, and share the implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The complete source code, technical documentation, setup instructions, and reproducible experiments are available in the GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/03-robot-cell" rel="noopener noreferrer"&gt;Explore Robot Cell Playground on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The implementation and visualizations are original and independently developed. No university handouts, CIROS assets, proprietary robot models, or restricted instructional materials are redistributed.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>python</category>
      <category>robotics</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Lab to Code #2: Exploring Spatial Audio with Python and SciPy</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:00:32 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/from-lab-to-code-2-exploring-spatial-audio-with-python-and-scipy-3kcj</link>
      <guid>https://dev.to/ertugrulmutlu/from-lab-to-code-2-exploring-spatial-audio-with-python-and-scipy-3kcj</guid>
      <description>&lt;p&gt;Most of us can recognize where a sound is coming from without consciously thinking about it.&lt;/p&gt;

&lt;p&gt;If someone speaks to our left, we naturally turn in that direction. If a car passes behind us, we can often estimate its movement without seeing it.&lt;/p&gt;

&lt;p&gt;But how does our auditory system determine where a sound is located?&lt;/p&gt;

&lt;p&gt;Interestingly, much of this ability depends on small differences between the signals arriving at our left and right ears.&lt;/p&gt;

&lt;p&gt;A sound may reach one ear slightly earlier, or with a different intensity, than the other.&lt;/p&gt;

&lt;p&gt;These differences can be analyzed using signal processing techniques such as &lt;strong&gt;cross-correlation, convolution, and impulse response analysis&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the second project in my &lt;strong&gt;From Lab to Code&lt;/strong&gt; series, I wanted to explore these concepts through an interactive Python application.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Spatial Audio Playground&lt;/strong&gt;: a small experimental application that lets users position virtual sound sources, compare mono and binaural audio, and visualize some of the signals responsible for directional hearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project 02 — Spatial Audio Playground:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are We Building?
&lt;/h2&gt;

&lt;p&gt;The application explores four main ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-correlation&lt;/strong&gt; to measure the relative delay between signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaural Time Difference (ITD)&lt;/strong&gt; and &lt;strong&gt;Interaural Level Difference (ILD)&lt;/strong&gt; to understand directional hearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binaural synthesis&lt;/strong&gt; to generate separate audio signals for the left and right ears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive spatial audio&lt;/strong&gt; using Python, SciPy, Streamlit, and a small JavaScript-based soundstage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users can move two virtual sound sources around a listener, adjust their positions and gains, and compare the resulting audio through headphones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd80gmonk5f4drumeln27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd80gmonk5f4drumeln27.png" alt="Spatial Audio Playground interface" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project uses procedurally generated audio and educational impulse-response models by default.&lt;/p&gt;

&lt;p&gt;No pretrained models, external audio datasets, or API calls are required.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Cross-Correlation: Detecting Delays Between Signals
&lt;/h2&gt;

&lt;p&gt;Before exploring spatial audio, we need a way to measure the relative timing of two signals.&lt;/p&gt;

&lt;p&gt;Consider a simple example.&lt;/p&gt;

&lt;p&gt;We have two discrete signals containing impulses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first signal has an impulse at sample index 5.&lt;/li&gt;
&lt;li&gt;The second signal has impulses at indices 9 and 14, with amplitudes of 1.0 and 0.5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corresponding differences in sample positions are +4 and +9.&lt;/p&gt;

&lt;p&gt;How can we detect these shifts automatically?&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Cross-Correlation
&lt;/h3&gt;

&lt;p&gt;Cross-correlation measures similarity between two signals as one is shifted relative to the other.&lt;/p&gt;

&lt;p&gt;For discrete signals, one convention is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;R_yx[k] = Σ y[n] · conjugate(x[n-k])&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The resulting function tells us how strongly the signals align at different sample lags.&lt;/p&gt;

&lt;p&gt;For simple impulse signals, alignment produces distinct correlation peaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Cross-Correlation with SciPy
&lt;/h3&gt;

&lt;p&gt;First, generate the signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;

&lt;span class="n"&gt;num_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

&lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unit_impulse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;shifted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unit_impulse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unit_impulse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, calculate the correlation and its corresponding lags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;correlation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;correlate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;shifted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;lags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;correlation_lags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shifted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correlation contains two nonzero peaks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lag&lt;/th&gt;
&lt;th&gt;Correlation amplitude&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+4&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+9&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first peak is larger because the corresponding impulse has a higher amplitude.&lt;/p&gt;

&lt;p&gt;An important detail is the &lt;strong&gt;order of the arguments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this example, &lt;code&gt;correlate(shifted, reference)&lt;/code&gt; produces positive lags for impulses occurring later in the shifted signal.&lt;/p&gt;

&lt;p&gt;Reversing the arguments changes the lag sign convention.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Sample Lag to Time Delay
&lt;/h3&gt;

&lt;p&gt;A sample shift can also be expressed as a time difference.&lt;/p&gt;

&lt;p&gt;If the sampling frequency is &lt;code&gt;fs&lt;/code&gt;, then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;delay = lag / fs&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example, at a sampling frequency of 44,100 Hz, a delay of four samples corresponds to approximately 0.091 milliseconds.&lt;/p&gt;

&lt;p&gt;This becomes important when comparing the arrival times of audio signals at our two ears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Cross-correlation gives us a practical way to estimate relative timing differences, provided the signals contain sufficiently identifiable patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. How Do We Hear Direction? ITD and ILD
&lt;/h2&gt;

&lt;p&gt;Our ears receive two slightly different versions of the same sound.&lt;/p&gt;

&lt;p&gt;The differences between them provide important information about the location of a sound source.&lt;/p&gt;

&lt;p&gt;Two particularly useful cues are:&lt;/p&gt;

&lt;h3&gt;
  
  
  Interaural Time Difference (ITD)
&lt;/h3&gt;

&lt;p&gt;Imagine a sound source located to your left.&lt;/p&gt;

&lt;p&gt;The sound generally reaches your left ear before your right ear because the path to the right ear is longer.&lt;/p&gt;

&lt;p&gt;This arrival-time difference is called the &lt;strong&gt;Interaural Time Difference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the application, I define:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ITD = t_right - t_left&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Positive ITD: the sound arrives at the left ear first.&lt;/li&gt;
&lt;li&gt;Negative ITD: the sound arrives at the right ear first.&lt;/li&gt;
&lt;li&gt;ITD near zero: both ears have approximately equal arrival times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To estimate ITD, the project applies a low-pass filter to the left and right impulse responses before calculating their cross-correlation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;butter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;btype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lowpass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;low_left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sosfilt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;low_right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sosfilt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;lags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;correlation_lags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low_right&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low_left&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;correlation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;correlate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;low_right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;low_left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation restricts the lag search to a physically relevant window of ±1 millisecond.&lt;/p&gt;

&lt;p&gt;The lag corresponding to the highest correlation value within this window provides an estimate of the timing difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interaural Level Difference (ILD)
&lt;/h3&gt;

&lt;p&gt;Time is not the only difference between our ears.&lt;/p&gt;

&lt;p&gt;The head also affects how much sound energy reaches each ear.&lt;/p&gt;

&lt;p&gt;For some directions, the head partially blocks the sound travelling toward the opposite ear.&lt;/p&gt;

&lt;p&gt;This effect is especially important at higher frequencies.&lt;/p&gt;

&lt;p&gt;The resulting difference in sound level is called the &lt;strong&gt;Interaural Level Difference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the project, a broadband ILD estimate is calculated using the energy of the two impulse responses:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ILD = 10 · log10(E_right / E_left)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;E = Σ h[n]²&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;energy_left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;energy_right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ild_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;energy_right&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;energy_left&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A negative ILD means the right channel contains less impulse-response energy than the left channel.&lt;/p&gt;

&lt;p&gt;This is a broadband estimate. In real acoustic systems, ILD depends strongly on frequency, so frequency-band analysis can provide more detailed information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Both Cues Matter
&lt;/h3&gt;

&lt;p&gt;ITD and ILD complement each other.&lt;/p&gt;

&lt;p&gt;ITD is particularly useful for understanding low-frequency directional cues, while ILD becomes especially relevant at higher frequencies.&lt;/p&gt;

&lt;p&gt;However, these two values alone cannot completely explain spatial hearing.&lt;/p&gt;

&lt;p&gt;They do not reliably resolve all front/back or elevation ambiguities.&lt;/p&gt;

&lt;p&gt;For that, we need to consider how the head, torso, and outer ears modify the sound spectrum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Spatial hearing depends on both when sound reaches each ear and how its level and spectrum change along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Head-Related Impulse Responses: Modeling the Path to Our Ears
&lt;/h2&gt;

&lt;p&gt;To simulate the sound arriving at each ear, we need to model how the human body modifies incoming audio.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Head-Related Transfer Functions (HRTFs)&lt;/strong&gt; and &lt;strong&gt;Head-Related Impulse Responses (HRIRs)&lt;/strong&gt; become useful.&lt;/p&gt;

&lt;p&gt;An HRTF describes the directional filtering of sound in the frequency domain.&lt;/p&gt;

&lt;p&gt;An HRIR represents the corresponding impulse response in the time domain.&lt;/p&gt;

&lt;p&gt;For each sound source direction, we need two impulse responses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;h_left[n]&lt;/code&gt; for the left ear.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;h_right[n]&lt;/code&gt; for the right ear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These responses describe how an incoming signal is transformed on its way to the two ears.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Educational HRIRs
&lt;/h3&gt;

&lt;p&gt;For this project, I implemented a small synthetic HRIR generator.&lt;/p&gt;

&lt;p&gt;Instead of relying on a measured dataset, the default model approximates three effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Arrival-time differences between the ears.&lt;/li&gt;
&lt;li&gt;Head-shadow attenuation.&lt;/li&gt;
&lt;li&gt;Mild spectral coloration related to elevation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lateral direction is calculated using the source angles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lateral&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;azimuth&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elevation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application uses the following coordinate convention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0°&lt;/code&gt;: front&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;+90°&lt;/code&gt;: left&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-90°&lt;/code&gt;: right&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;±180°&lt;/code&gt;: behind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The synthetic model then converts this lateral position into an approximate sample delay.&lt;/p&gt;

&lt;p&gt;It also reduces the far-ear amplitude to illustrate head-shadow effects.&lt;/p&gt;

&lt;p&gt;This provides a controllable environment for investigating spatial cues without requiring external recordings.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Important Limitation
&lt;/h3&gt;

&lt;p&gt;The synthetic HRIR model is intentionally simplified.&lt;/p&gt;

&lt;p&gt;Real HRIRs contain much more detailed information about reflections, diffraction, and resonances caused by an individual's anatomy.&lt;/p&gt;

&lt;p&gt;Therefore, the built-in model is most useful for understanding basic left/right directional cues.&lt;/p&gt;

&lt;p&gt;It should not be treated as a physically accurate or personalized 3D audio renderer.&lt;/p&gt;

&lt;p&gt;For more realistic experiments, the application can optionally load compatible measured HRIR data stored in the &lt;strong&gt;SOFA (Spatially Oriented Format for Acoustics)&lt;/strong&gt; format.&lt;/p&gt;

&lt;p&gt;The current implementation uses the nearest available measurement direction rather than interpolating between measurements.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Binaural Synthesis: Turning Mono into Directional Stereo
&lt;/h2&gt;

&lt;p&gt;Now we have the essential components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A mono audio signal.&lt;/li&gt;
&lt;li&gt;A left-ear impulse response.&lt;/li&gt;
&lt;li&gt;A right-ear impulse response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next step is to combine them.&lt;/p&gt;

&lt;p&gt;This is done using &lt;strong&gt;convolution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a mono input signal &lt;code&gt;x[n]&lt;/code&gt;, we calculate:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;y_left[n] = x[n] * h_left[n]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;y_right[n] = x[n] * h_right[n]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;*&lt;/code&gt; represents the convolution operation.&lt;/p&gt;

&lt;p&gt;Each output contains a filtered version of the original sound, corresponding to a different ear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Convolution with SciPy
&lt;/h3&gt;

&lt;p&gt;The actual project uses &lt;code&gt;scipy.signal.oaconvolve&lt;/code&gt;, which implements overlap-add convolution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gain_db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oaconvolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;hrir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ear&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;stereo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stereo&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mf"&gt;10.0&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gain_db&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a stereo array containing separate left and right waveforms.&lt;/p&gt;

&lt;p&gt;When these signals are played through headphones, the listener receives different directional information at each ear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combining Multiple Sound Sources
&lt;/h3&gt;

&lt;p&gt;The application supports up to two independent sound sources.&lt;/p&gt;

&lt;p&gt;Each source can have its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audio recording.&lt;/li&gt;
&lt;li&gt;Azimuth and elevation.&lt;/li&gt;
&lt;li&gt;Gain.&lt;/li&gt;
&lt;li&gt;HRIR pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mono source is processed separately.&lt;/p&gt;

&lt;p&gt;The resulting stereo signals are then summed to create a binaural mix.&lt;/p&gt;

&lt;p&gt;To make the mono/binaural comparison more consistent, the application applies one shared clipping-prevention gain to both outputs when necessary.&lt;/p&gt;

&lt;p&gt;This avoids independently peak-normalizing the two versions, which could introduce an unnecessary gain difference.&lt;/p&gt;

&lt;p&gt;It is important to note that a common gain does not guarantee equal perceived loudness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Convolution lets us transform ordinary mono audio into two direction-dependent ear signals using impulse responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Building the Interactive Spatial Audio Playground
&lt;/h2&gt;

&lt;p&gt;Understanding the equations is useful, but I also wanted a way to experiment with them directly.&lt;/p&gt;

&lt;p&gt;That became the main motivation behind the application's interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rt4wp9p60iu1qgjmerz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rt4wp9p60iu1qgjmerz.png" alt="Spatial audio and correlation results" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interface is built using &lt;strong&gt;Streamlit&lt;/strong&gt;, with an additional HTML, CSS, SVG, and JavaScript component for the interactive soundstage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving Virtual Sound Sources
&lt;/h3&gt;

&lt;p&gt;Instead of manually entering every angle, users can drag two sound-source markers around a listener.&lt;/p&gt;

&lt;p&gt;Each marker represents an independent virtual audio source.&lt;/p&gt;

&lt;p&gt;When a source is moved, its new azimuth is sent to the Python application.&lt;/p&gt;

&lt;p&gt;The application selects or generates the corresponding impulse responses and renders new audio clips.&lt;/p&gt;

&lt;p&gt;The soundstage also supports keyboard interaction and directional presets.&lt;/p&gt;

&lt;p&gt;Importantly, this is &lt;strong&gt;offline rendering&lt;/strong&gt;, not continuous real-time spatial audio.&lt;/p&gt;

&lt;p&gt;Moving the markers changes the next generated clip rather than streaming a moving sound source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing Mono and Binaural Playback
&lt;/h3&gt;

&lt;p&gt;One of the central features is a direct listening comparison.&lt;/p&gt;

&lt;p&gt;The application produces two outputs from the same input sources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centered mono:&lt;/strong&gt; A reference mix without separate left/right spatial filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binaural stereo:&lt;/strong&gt; A direction-dependent mix created using individual impulse responses for each ear.&lt;/p&gt;

&lt;p&gt;Users can listen to both versions and download the binaural WAV output.&lt;/p&gt;

&lt;p&gt;Headphones are recommended because they deliver the two channels separately to the listener's ears.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspecting the Signals
&lt;/h3&gt;

&lt;p&gt;The interface also displays:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Estimated ITD in milliseconds.&lt;/li&gt;
&lt;li&gt;Estimated ILD in decibels.&lt;/li&gt;
&lt;li&gt;Left and right HRIR waveforms.&lt;/li&gt;
&lt;li&gt;The beginning of the rendered stereo waveform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows users to connect the perceptual experience with the underlying signal-processing operations.&lt;/p&gt;

&lt;p&gt;The application also includes a separate cross-correlation experiment and a short theory section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Project Locally
&lt;/h2&gt;

&lt;p&gt;The complete implementation is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will need Python 3.11–3.13.&lt;/p&gt;

&lt;p&gt;Clone the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
&lt;span class="nb"&gt;cd &lt;/span&gt;from-lab-to-code/02-spatial-audio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application opens in your browser, typically at &lt;code&gt;http://localhost:8501&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can immediately experiment with the built-in audio examples.&lt;/p&gt;

&lt;p&gt;The project also supports uploading your own short WAV recordings and compatible SOFA datasets.&lt;/p&gt;

&lt;p&gt;To execute the automated tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project includes 18 automated unit and integration tests covering signal processing, audio rendering, and related functionality.&lt;/p&gt;

&lt;p&gt;The main implementation is organized into separate files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;correlation.py&lt;/code&gt; — cross-correlation and interaural cue estimation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;binaural.py&lt;/code&gt; — convolution, HRIR loading, and WAV handling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audio_pipeline.py&lt;/code&gt; — rendering and mixing multiple sound sources.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;synthetic.py&lt;/code&gt; — original demonstration sounds and educational HRIRs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app.py&lt;/code&gt; — the interactive Streamlit application.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend/index.html&lt;/code&gt; — the draggable soundstage component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation keeps the audio algorithms independent of the user interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Working on this project helped me connect several ideas that initially seemed independent.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;small timing differences can carry meaningful spatial information&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cross-correlation is not only a mathematical technique for comparing signals. It also provides a practical way to investigate the delays that contribute to directional hearing.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;impulse responses provide an elegant connection between physical systems and software&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once we represent the acoustic path to each ear as an impulse response, generating the corresponding output becomes a convolution problem.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;a signal can be mathematically correct without producing a perfectly realistic perceptual result&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Our synthetic HRIR model demonstrates timing and level differences, but human spatial hearing also depends on detailed anatomy-dependent spectral cues.&lt;/p&gt;

&lt;p&gt;Finally, I learned that making an engineering concept interactive can change how it is understood.&lt;/p&gt;

&lt;p&gt;Adjusting an azimuth angle, listening to the result, and examining the corresponding waveforms makes the relationship between theory and perception much more tangible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuing From Lab to Code
&lt;/h2&gt;

&lt;p&gt;In the first project of this series, I explored classical computer vision using filtering, Fourier analysis, thresholding, and clustering.&lt;/p&gt;

&lt;p&gt;This second project moves from image processing to audio signal processing.&lt;/p&gt;

&lt;p&gt;Both follow the same philosophy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take an engineering concept, build an independent implementation, make the results observable, and explain what is happening underneath.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not every engineering experiment needs to become a large software product.&lt;/p&gt;

&lt;p&gt;Sometimes, a small interactive application is enough to reveal how an interesting concept works.&lt;/p&gt;

&lt;p&gt;The goal of &lt;strong&gt;From Lab to Code&lt;/strong&gt; is to keep exploring these ideas through practical implementations.&lt;/p&gt;

&lt;p&gt;All bundled demonstration sounds in this project are generated programmatically, and no university course materials or proprietary recordings are redistributed.&lt;/p&gt;

&lt;p&gt;If you're interested in signal processing, spatial audio, or simply curious about how computers can simulate directional hearing, I hope this project gives you something interesting to experiment with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the full series:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore Spatial Audio Playground:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/02-spatial-audio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>From Lab to Code #1: Exploring Classical Computer Vision with Python and OpenCV</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:01:08 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv-3mcf</link>
      <guid>https://dev.to/ertugrulmutlu/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv-3mcf</guid>
      <description>&lt;p&gt;Most conversations about computer vision today revolve around deep learning, neural networks, and increasingly sophisticated AI models.&lt;/p&gt;

&lt;p&gt;But not every image processing problem requires a trained model.&lt;/p&gt;

&lt;p&gt;Sometimes, a Gaussian filter is enough to reduce noise. A threshold can separate objects from their background. A Fourier transform can reveal interference that is difficult to understand in the spatial domain.&lt;/p&gt;

&lt;p&gt;These techniques are relatively simple, mathematically interpretable, and still useful in practical applications.&lt;/p&gt;

&lt;p&gt;Recently, while exploring image and signal processing concepts in my computer engineering studies, I decided to start a small programming series called &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple: take engineering concepts I encounter, build independent implementations, visualize the results, and share what I learn.&lt;/p&gt;

&lt;p&gt;For the first project, I built a small interactive image processing toolkit using Python, OpenCV, NumPy, and Streamlit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project 01 — Image Processing:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are We Building?
&lt;/h2&gt;

&lt;p&gt;The application explores four classical computer vision techniques:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Image denoising&lt;/strong&gt; using Gaussian and median filters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object counting&lt;/strong&gt; using illumination correction, thresholding, and connected components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Periodic noise removal&lt;/strong&gt; using the Fast Fourier Transform (FFT).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color clustering&lt;/strong&gt; using K-means.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application provides interactive controls, generated example images, and visual comparisons.&lt;/p&gt;

&lt;p&gt;No pretrained models, API calls, or external datasets are required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mbg7nduir2ov3fzhuru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mbg7nduir2ov3fzhuru.png" alt="Image processing examples" width="720" height="1612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All bundled demonstration images are generated programmatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Image Denoising: Gaussian vs. Median Filtering
&lt;/h2&gt;

&lt;p&gt;Digital images are often affected by noise.&lt;/p&gt;

&lt;p&gt;Noise can originate from camera sensors, poor lighting conditions, signal transmission, or image acquisition hardware.&lt;/p&gt;

&lt;p&gt;Two common types are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gaussian noise:&lt;/strong&gt; Random variations in pixel intensity, often modeled using a normal distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salt-and-pepper noise:&lt;/strong&gt; Individual pixels unexpectedly taking very dark or very bright values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different noise distributions require different filtering strategies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gaussian Filtering
&lt;/h3&gt;

&lt;p&gt;A Gaussian filter performs a weighted average of neighboring pixels.&lt;/p&gt;

&lt;p&gt;Pixels closer to the center receive higher weights, following a Gaussian distribution.&lt;/p&gt;

&lt;p&gt;Mathematically, the two-dimensional Gaussian kernel can be written as:&lt;/p&gt;

&lt;p&gt;G(x, y) = (1 / 2πσ²) × exp(-(x² + y²) / 2σ²)&lt;/p&gt;

&lt;p&gt;The parameter σ controls the amount of smoothing.&lt;/p&gt;

&lt;p&gt;A larger σ produces stronger smoothing but can also remove fine image details.&lt;/p&gt;

&lt;p&gt;In OpenCV, Gaussian filtering is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;

&lt;span class="n"&gt;gaussian&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GaussianBlur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Median Filtering
&lt;/h3&gt;

&lt;p&gt;Unlike Gaussian filtering, a median filter replaces the center pixel with the median value of its neighborhood.&lt;/p&gt;

&lt;p&gt;Consider these pixel values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[10, 11, 12, 13, 255]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Their average is 60.2, but their median is 12.&lt;/p&gt;

&lt;p&gt;If the value 255 represents an isolated noise spike, the median is much closer to the surrounding pixel values.&lt;/p&gt;

&lt;p&gt;This makes median filtering particularly useful for salt-and-pepper noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;median&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;medianBlur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Comparing the Results
&lt;/h3&gt;

&lt;p&gt;The application applies both filters to the same noisy image.&lt;/p&gt;

&lt;p&gt;It also calculates the Mean Squared Error (MSE) against the original image when a reference is available.&lt;/p&gt;

&lt;p&gt;MSE = (1/N) × Σ(original - processed)²&lt;/p&gt;

&lt;p&gt;A lower MSE indicates a smaller average squared difference from the reference image.&lt;/p&gt;

&lt;p&gt;However, MSE alone does not fully describe perceptual image quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Gaussian filtering is useful for smoothing distributed noise, while median filtering is often more effective against isolated intensity spikes.&lt;/p&gt;

&lt;p&gt;Neither method is universally superior.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Object Counting: From Pixels to Objects
&lt;/h2&gt;

&lt;p&gt;Suppose we want to count small objects placed on a table.&lt;/p&gt;

&lt;p&gt;At first, this sounds like a simple thresholding problem.&lt;/p&gt;

&lt;p&gt;If the objects are darker than the background, we could classify pixels based on their intensity.&lt;/p&gt;

&lt;p&gt;But what happens when the background illumination is uneven?&lt;/p&gt;

&lt;p&gt;One side of the image might be noticeably darker than the other, making a single global threshold unreliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Estimate the Background
&lt;/h3&gt;

&lt;p&gt;We can approximate slowly changing illumination using a large Gaussian blur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GaussianBlur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sigmaX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subtracting the original image from the estimated background helps emphasize darker foreground objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;corrected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gray&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces the influence of gradual illumination changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Apply Otsu Thresholding
&lt;/h3&gt;

&lt;p&gt;Otsu's method automatically selects a threshold based on the image's intensity distribution.&lt;/p&gt;

&lt;p&gt;Instead of manually choosing a number such as 100 or 150, it searches for a threshold that separates the intensity classes according to their statistical variance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;corrected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;THRESH_BINARY&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;THRESH_OTSU&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a binary image containing foreground and background pixels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Identify Connected Components
&lt;/h3&gt;

&lt;p&gt;After thresholding, neighboring foreground pixels can be grouped into connected regions.&lt;/p&gt;

&lt;p&gt;OpenCV provides a useful function for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;centroids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectedComponentsWithStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returns component labels and information such as bounding boxes and pixel areas.&lt;/p&gt;

&lt;p&gt;By excluding the background and removing regions below a minimum area, we can estimate the number of objects.&lt;/p&gt;

&lt;p&gt;The project also applies a small morphological opening operation before counting to remove isolated foreground noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Can This Be Useful?
&lt;/h3&gt;

&lt;p&gt;This approach can be applied to controlled visual inspection tasks involving separate objects, such as small manufactured components.&lt;/p&gt;

&lt;p&gt;However, it comes with important assumptions.&lt;/p&gt;

&lt;p&gt;Objects should be sufficiently separated, and the contrast between objects and background should be reasonably clear.&lt;/p&gt;

&lt;p&gt;Touching objects may merge into a single connected component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Reliable object counting often requires preprocessing, not just thresholding.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Fourier Transform: Removing Periodic Noise
&lt;/h2&gt;

&lt;p&gt;This was one of the most interesting parts of the project.&lt;/p&gt;

&lt;p&gt;Images are normally represented in the spatial domain, where each pixel corresponds to a position.&lt;/p&gt;

&lt;p&gt;But some image artifacts are easier to identify in the frequency domain.&lt;/p&gt;

&lt;p&gt;For example, imagine an image affected by regularly repeating stripes.&lt;/p&gt;

&lt;p&gt;Those stripes represent periodic variations in pixel intensity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving to the Frequency Domain
&lt;/h3&gt;

&lt;p&gt;The two-dimensional Discrete Fourier Transform decomposes an image into spatial frequency components.&lt;/p&gt;

&lt;p&gt;Each component represents a particular frequency and orientation.&lt;/p&gt;

&lt;p&gt;Using NumPy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;spectrum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fft2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;shifted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fftshift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spectrum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;magnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log1p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shifted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fft2()&lt;/code&gt; computes the two-dimensional Fourier transform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fftshift()&lt;/code&gt; moves the zero-frequency component to the center.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;abs()&lt;/code&gt; obtains the magnitude.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;log1p()&lt;/code&gt; makes the magnitude spectrum easier to visualize.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Periodic interference may appear as distinct peaks in the Fourier spectrum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing Specific Frequencies
&lt;/h3&gt;

&lt;p&gt;If we know which frequencies correspond to interference, we can suppress them using a notch filter.&lt;/p&gt;

&lt;p&gt;A notch filter removes selected frequency regions while preserving much of the remaining spectrum.&lt;/p&gt;

&lt;p&gt;For real-valued images, the Fourier spectrum has conjugate symmetry.&lt;/p&gt;

&lt;p&gt;This means a sinusoidal interference component generally produces a symmetric pair of frequency peaks.&lt;/p&gt;

&lt;p&gt;Both should be handled to preserve the symmetry of the filtered spectrum.&lt;/p&gt;

&lt;p&gt;The basic operation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;filtered_spectrum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shifted&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;mask&lt;/code&gt; contains zeros at the frequencies we want to suppress and ones elsewhere.&lt;/p&gt;

&lt;p&gt;We then reconstruct the image using the inverse Fourier transform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifft2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifftshift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered_spectrum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  An Important Limitation
&lt;/h3&gt;

&lt;p&gt;In this project, the periodic noise is generated with known frequencies.&lt;/p&gt;

&lt;p&gt;Therefore, the notch filter already knows where the unwanted frequency components are located.&lt;/p&gt;

&lt;p&gt;This is intentional: it lets us isolate and study the effect of frequency-domain filtering.&lt;/p&gt;

&lt;p&gt;Automatically identifying unknown interference frequencies in arbitrary photographs is a separate problem and is not implemented here.&lt;/p&gt;

&lt;p&gt;A notch filter can also remove useful image information if the rejected frequencies overlap with actual image content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; The Fourier transform provides another perspective on image processing, allowing us to manipulate structures according to their spatial frequencies rather than their pixel positions.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. K-Means: Unsupervised Color Clustering
&lt;/h2&gt;

&lt;p&gt;The final technique explores unsupervised learning.&lt;/p&gt;

&lt;p&gt;Unlike classification models, K-means does not require predefined class labels.&lt;/p&gt;

&lt;p&gt;Instead, it groups observations based on similarity.&lt;/p&gt;

&lt;p&gt;For an RGB image, each pixel can be represented as a three-dimensional vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pixel = [Red, Green, Blue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;K-means attempts to divide these vectors into K clusters by minimizing the squared distances between pixels and their assigned cluster centers.&lt;/p&gt;

&lt;p&gt;The objective function is:&lt;/p&gt;

&lt;p&gt;J = Σ ||xᵢ - μ(clusterᵢ)||²&lt;/p&gt;

&lt;p&gt;where xᵢ is a pixel vector and μ is the corresponding cluster center.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing K-Means with OpenCV
&lt;/h3&gt;

&lt;p&gt;First, reshape the image into a collection of RGB vectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run K-means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;criteria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TERM_CRITERIA_EPS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TERM_CRITERIA_MAX_ITER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;centers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kmeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KMEANS_PP_CENTERS&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can reconstruct an image by replacing each pixel with the color of its assigned cluster center.&lt;/p&gt;

&lt;p&gt;This produces a simplified image containing a small number of representative colors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Color Clustering Is Not Semantic Segmentation
&lt;/h3&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;K-means only sees RGB values in this implementation.&lt;/p&gt;

&lt;p&gt;It does not understand objects, scenes, or semantic categories.&lt;/p&gt;

&lt;p&gt;Two unrelated objects with similar colors may belong to the same cluster.&lt;/p&gt;

&lt;p&gt;Likewise, one object with multiple colors may be split across several clusters.&lt;/p&gt;

&lt;p&gt;Still, color clustering is useful for exploring image structure, reducing color complexity, and understanding unsupervised learning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Project Locally
&lt;/h2&gt;

&lt;p&gt;The project is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will need Python 3.10 or newer.&lt;/p&gt;

&lt;p&gt;Clone the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
&lt;span class="nb"&gt;cd &lt;/span&gt;from-lab-to-code/01-image-processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it on Windows PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\.venv\Scripts\Activate.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streamlit will open an interactive application in your browser.&lt;/p&gt;

&lt;p&gt;You can switch between the four techniques, adjust processing parameters, and compare the results.&lt;/p&gt;

&lt;p&gt;Each mode includes a programmatically generated example image. You can also upload an image of your own.&lt;/p&gt;

&lt;p&gt;To regenerate the demonstration gallery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the included tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processing algorithms are implemented in &lt;code&gt;processing.py&lt;/code&gt;, while &lt;code&gt;synthetic.py&lt;/code&gt; generates the example images and &lt;code&gt;app.py&lt;/code&gt; contains the Streamlit interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building this project reinforced several important ideas.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;understanding the characteristics of a problem matters as much as selecting an algorithm&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Gaussian filtering and median filtering may look similar at first, but they behave differently depending on the noise distribution.&lt;/p&gt;

&lt;p&gt;Second, preprocessing can determine whether a simple computer vision algorithm succeeds or fails.&lt;/p&gt;

&lt;p&gt;In the object counting example, background illumination correction can be just as important as the thresholding operation itself.&lt;/p&gt;

&lt;p&gt;Third, the frequency domain provides a different way to reason about images.&lt;/p&gt;

&lt;p&gt;Some patterns that are difficult to isolate spatially become much easier to analyze after a Fourier transform.&lt;/p&gt;

&lt;p&gt;Finally, unsupervised clustering demonstrates that useful structure can be extracted from images without labeled training data, even though grouping colors is not the same as understanding a scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Started From Lab to Code
&lt;/h2&gt;

&lt;p&gt;I wanted a lightweight way to turn engineering concepts into practical software projects.&lt;/p&gt;

&lt;p&gt;Not every project needs to become a startup, a large framework, or a long-term product.&lt;/p&gt;

&lt;p&gt;Sometimes, a small, well-explained implementation is enough to explore an idea and demonstrate what you have learned.&lt;/p&gt;

&lt;p&gt;That is the philosophy behind &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each entry will focus on an engineering topic, build a small independent implementation, and document the underlying concepts and results.&lt;/p&gt;

&lt;p&gt;The projects are independently implemented, with original demonstration materials rather than redistributed university coursework.&lt;/p&gt;

&lt;p&gt;This first project focuses on classical image processing. Future entries may explore other engineering topics as I encounter them.&lt;/p&gt;

&lt;p&gt;If you are learning computer vision or signal processing, I hope the code and explanations help you experiment with these concepts yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the series:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the image processing project:&lt;/strong&gt; &lt;a href="https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>python</category>
      <category>computervision</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Behavioral Convergence Without Representational Convergence: Reproducing Training-History Dependence in Neural Networks</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Wed, 30 Sep 2026 14:34:04 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/behavioral-convergence-without-representational-convergence-reproducing-training-history-45e6</link>
      <guid>https://dev.to/ertugrulmutlu/behavioral-convergence-without-representational-convergence-reproducing-training-history-45e6</guid>
      <description>&lt;p&gt;I recently released a new preprint exploring a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If two neural networks eventually behave almost the same, does that mean they also become internally the same?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The experiments suggest that — at least under the protocol I tested — the answer can be &lt;strong&gt;no&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two neural networks can reach very similar predictive performance while still retaining measurable differences in their internal representations caused by their earlier training histories.&lt;/p&gt;

&lt;p&gt;The paper is now available on arXiv:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paper:&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2609.37836" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.37836&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code and reproducibility repository:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Ertugrulmutlu/hysteresis-neural-networks" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/hysteresis-neural-networks&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;The experiments use MNIST and a small convolutional neural network.&lt;/p&gt;

&lt;p&gt;Instead of training a model once on the full dataset, I split the learning history into stages.&lt;/p&gt;

&lt;p&gt;Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A:&lt;/strong&gt; digits 0–4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B:&lt;/strong&gt; digits 5–9&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C:&lt;/strong&gt; a balanced distribution containing digits 0–9&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two models start from &lt;strong&gt;identical initial weights&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The first model experiences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A -&amp;gt; B -&amp;gt; C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second experiences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;B -&amp;gt; A -&amp;gt; C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I call these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SABC
SBAC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the final stage.&lt;/p&gt;

&lt;p&gt;During &lt;strong&gt;C&lt;/strong&gt;, both models receive the same training distribution, the same deterministic batch sequence, and the same checkpoint schedule.&lt;/p&gt;

&lt;p&gt;So after their different histories, both networks are exposed to the same environment.&lt;/p&gt;

&lt;p&gt;The question becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does common training erase the effects of training history?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why accuracy alone is not enough
&lt;/h2&gt;

&lt;p&gt;Suppose both networks eventually reach around the same accuracy.&lt;/p&gt;

&lt;p&gt;It would be tempting to conclude that they have converged to essentially the same solution.&lt;/p&gt;

&lt;p&gt;But prediction accuracy only tells us about &lt;strong&gt;behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It does not tell us whether the internal features learned by the two networks are identical.&lt;/p&gt;

&lt;p&gt;To study this, I compare internal representations using CKA-based similarity measurements.&lt;/p&gt;

&lt;p&gt;The main representation-history score is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H_repr = 1 - mean(CKA_conv2, CKA_fc1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher values mean the two networks retain more representational difference.&lt;/p&gt;

&lt;p&gt;The interesting case is therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Behavioral difference -&amp;gt; small

while

Representational difference -&amp;gt; remains measurable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What happened?
&lt;/h2&gt;

&lt;p&gt;In the main experiment, I ran &lt;strong&gt;20 paired seeds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The networks often became closely matched in predictive performance after common relaxation, while measurable representation differences remained.&lt;/p&gt;

&lt;p&gt;This motivated a harder test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens if both models continue training on the same distribution for much longer?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Stress test: 50,000 shared updates
&lt;/h1&gt;

&lt;p&gt;I reran five paired seeds from initialization and extended the common-relaxation phase to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000 updates
25,000 updates
50,000 updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 50,000 common updates, the mean representation-history score was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;H_repr = 0.1902
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a 95% bootstrap confidence interval of approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.1611, 0.2193]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, predictive performance remained very close.&lt;/p&gt;

&lt;p&gt;At 50,000 updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mean SABC accuracy: 98.480%
Mean SBAC accuracy: 98.316%

Mean absolute accuracy gap: ~0.18 percentage points
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So over the measured horizon, behavioral convergence did &lt;strong&gt;not&lt;/strong&gt; imply representational convergence.&lt;/p&gt;

&lt;p&gt;Importantly, this does &lt;strong&gt;not&lt;/strong&gt; prove that the difference persists forever.&lt;/p&gt;

&lt;p&gt;The correct interpretation is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The internal representation difference did not disappear within the tested 50,000-update horizon.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  A second question: does the difference actually matter?
&lt;/h1&gt;

&lt;p&gt;Representation geometry can differ without having any practical consequence.&lt;/p&gt;

&lt;p&gt;So I added a fresh linear-probe experiment.&lt;/p&gt;

&lt;p&gt;The CNN backbone is frozen.&lt;/p&gt;

&lt;p&gt;A brand-new linear classifier is then trained on top of FC1 features.&lt;/p&gt;

&lt;p&gt;Only the new linear head is trained.&lt;/p&gt;

&lt;p&gt;The SABC and SBAC probes receive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identical initialization,&lt;/li&gt;
&lt;li&gt;identical examples,&lt;/li&gt;
&lt;li&gt;identical labels,&lt;/li&gt;
&lt;li&gt;identical minibatch ordering,&lt;/li&gt;
&lt;li&gt;identical optimization settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets us ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is class information equally easy to extract from both representations?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  An interesting low-data effect
&lt;/h2&gt;

&lt;p&gt;I tested several amounts of labeled training data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;25 examples / class
50 examples / class
100 examples / class
500 examples / class
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mean SABC-minus-SBAC accuracy differences were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;25/class  -&amp;gt; +1.005 percentage points
50/class  -&amp;gt; +0.580 percentage points
100/class -&amp;gt; +0.288 percentage points
500/class -&amp;gt; +0.090 percentage points
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference shrinks as more labeled data becomes available.&lt;/p&gt;

&lt;p&gt;The 50-examples-per-class condition was especially interesting, so I repeated the probe using five different fresh-head seeds.&lt;/p&gt;

&lt;p&gt;After averaging repeated probes within each of the 20 model seeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mean paired difference: +0.496 pp
95% t CI:              [+0.127, +0.866] pp
95% bootstrap CI:      [+0.160, +0.833] pp
paired t-test:         p = 0.0111
Wilcoxon:              p = 0.0083
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This suggests a useful distinction.&lt;/p&gt;

&lt;p&gt;The two representations can provide similar high-data performance while differing in &lt;strong&gt;low-data readout efficiency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, training history may change how easy it is for a downstream classifier to extract useful information when supervision is limited.&lt;/p&gt;




&lt;h1&gt;
  
  
  What happens with more probe data?
&lt;/h1&gt;

&lt;p&gt;At 500 examples per class, the mean difference was only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+0.090 percentage points
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also ran an exploratory equivalence analysis using a ±0.5 percentage-point margin.&lt;/p&gt;

&lt;p&gt;The 90% confidence interval was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-0.037, +0.217] pp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which falls completely inside the chosen equivalence interval.&lt;/p&gt;

&lt;p&gt;So under that declared margin, the high-data linear-probe endpoints are practically equivalent.&lt;/p&gt;

&lt;p&gt;That produces a two-regime picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Low labeled data:
training history affects linear readout efficiency.

High labeled data:
the difference becomes practically small.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Is this just caused by ReLU?
&lt;/h1&gt;

&lt;p&gt;One possible mechanism is reduced plasticity caused by sparse ReLU activation patterns.&lt;/p&gt;

&lt;p&gt;To explore this, I ran a matched-learning-rate comparison between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ReLU
vs.
LeakyReLU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both conditions were evaluated through 50,000 common-relaxation updates across five paired seeds.&lt;/p&gt;

&lt;p&gt;At the final endpoint, the LeakyReLU-minus-ReLU difference in the representation-history score was approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-0.0398
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five paired differences pointed in the same direction.&lt;/p&gt;

&lt;p&gt;The bootstrap interval was approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-0.0772, -0.0176]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But with only five model seeds, classical paired significance tests were still inconclusive.&lt;/p&gt;

&lt;p&gt;So I treat this as &lt;strong&gt;directional mechanism evidence&lt;/strong&gt;, not a causal proof.&lt;/p&gt;

&lt;p&gt;The result suggests that activation-mediated plasticity may contribute to the persistence of training-history effects.&lt;/p&gt;




&lt;h1&gt;
  
  
  Is the result only caused by different labels?
&lt;/h1&gt;

&lt;p&gt;The original A/B split separates digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A = 0–4
B = 5–9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That raises an obvious concern.&lt;/p&gt;

&lt;p&gt;Maybe the effect is simply caused by learning disjoint output classes in different orders.&lt;/p&gt;

&lt;p&gt;To test this, I added a &lt;strong&gt;same-label rotated-MNIST control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both histories use the same class labels, but the input domains differ through symmetric rotations.&lt;/p&gt;

&lt;p&gt;Across all five paired seeds, the models reached the predeclared behavioral-matching criterion while still retaining non-zero representation-history scores.&lt;/p&gt;

&lt;p&gt;So the phenomenon is not limited to the original disjoint-label setup.&lt;/p&gt;




&lt;h1&gt;
  
  
  Reproducing the experiments
&lt;/h1&gt;

&lt;p&gt;The full repository is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Ertugrulmutlu/hysteresis-neural-networks" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/hysteresis-neural-networks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ertugrulmutlu/hysteresis-neural-networks.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hysteresis-neural-networks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;venv&lt;/span&gt;&lt;span class="n"&gt;\Scripts\Activate.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Run the basic AB / BA experiment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.train configs/mnist_v0_sab.yaml
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.train configs/mnist_v0_sba.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then validate that the pair uses the intended matched initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.validate_pair &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-a&lt;/span&gt; results/mnist_v0_SAB_seed1337_normnone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-b&lt;/span&gt; results/mnist_v0_SBA_seed1337_normnone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json-out&lt;/span&gt; results/pair_validation.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Run representation analysis
&lt;/h2&gt;

&lt;p&gt;For example, CKA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.analysis.cka &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-sab&lt;/span&gt; results/mnist_v0_SAB_seed1337_normnone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-sba&lt;/span&gt; results/mnist_v0_SBA_seed1337_normnone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--samples-per-class&lt;/span&gt; 200 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--outdir&lt;/span&gt; plots/part2_seed1337/cka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository also includes analyses for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;training curves
weight trajectories
CKA
weight interpolation
activation health
common relaxation
paired multi-seed aggregation
linear probes
long-horizon relaxation
activation-function controls
rotated-MNIST controls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  A note on reproducibility
&lt;/h1&gt;

&lt;p&gt;I tried to make the pairwise experimental design explicit.&lt;/p&gt;

&lt;p&gt;The paired runs control factors such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialization
random seeds
evaluation data
common-relaxation batch order
checkpoint schedule
probe initialization
probe minibatch order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run metadata is stored alongside the generated experiment artifacts.&lt;/p&gt;

&lt;p&gt;The final paper-facing outputs are packaged under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;paper_artifacts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the underlying experiment outputs remain the source of truth.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I think is the interesting part
&lt;/h1&gt;

&lt;p&gt;The most interesting result to me is not simply that two networks end up with different weights.&lt;/p&gt;

&lt;p&gt;That is expected.&lt;/p&gt;

&lt;p&gt;The more interesting observation is the combination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same initialization

+ different training histories

+ long exposure to the same later distribution

+ almost identical predictive performance

+ persistently different internal representations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the linear-probe experiments suggest that those differences can sometimes affect how efficiently information is extracted downstream.&lt;/p&gt;

&lt;p&gt;This creates an interesting distinction between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;behavioral convergence

and

representational convergence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are not necessarily the same thing.&lt;/p&gt;




&lt;h1&gt;
  
  
  What this does NOT show
&lt;/h1&gt;

&lt;p&gt;There are several important limitations.&lt;/p&gt;

&lt;p&gt;This work does not establish that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all neural networks exhibit hysteresis,&lt;/li&gt;
&lt;li&gt;training-history effects persist permanently,&lt;/li&gt;
&lt;li&gt;the measured representation difference has a single causal mechanism,&lt;/li&gt;
&lt;li&gt;similar accuracy implies functionally identical networks,&lt;/li&gt;
&lt;li&gt;the result automatically generalizes beyond the tested architectures and datasets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The experiments currently focus mainly on a small CNN and MNIST-derived protocols.&lt;/p&gt;

&lt;p&gt;So I view the results as evidence of &lt;strong&gt;persistent training-history dependence under controlled experimental conditions&lt;/strong&gt;, rather than a universal theorem about neural networks.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where to go next
&lt;/h1&gt;

&lt;p&gt;There are several natural extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wider or deeper architectures,&lt;/li&gt;
&lt;li&gt;larger datasets,&lt;/li&gt;
&lt;li&gt;transformers,&lt;/li&gt;
&lt;li&gt;continual-learning benchmarks,&lt;/li&gt;
&lt;li&gt;longer common-relaxation horizons,&lt;/li&gt;
&lt;li&gt;interventions targeting plasticity,&lt;/li&gt;
&lt;li&gt;representation alignment methods,&lt;/li&gt;
&lt;li&gt;studying whether history dependence affects adaptation to entirely new downstream tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One question I find particularly interesting is whether similar effects appear in large models that are repeatedly fine-tuned or continually updated.&lt;/p&gt;

&lt;p&gt;If two systems behave similarly today, how much of their different histories is still encoded internally?&lt;/p&gt;

&lt;p&gt;That seems increasingly relevant as models become continuously trained, adapted, and deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paper and code
&lt;/h2&gt;

&lt;p&gt;📄 &lt;strong&gt;Paper&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2609.37836" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.37836&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;Code + reproducibility artifacts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Ertugrulmutlu/hysteresis-neural-networks" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/hysteresis-neural-networks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you reproduce the experiments, find an issue, or want to test the idea on another architecture, feel free to open an issue or discussion on GitHub.&lt;/p&gt;




&lt;h3&gt;
  
  
  Citation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bibtex"&gt;&lt;code&gt;&lt;span class="nc"&gt;@article&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;mutlu2026behavioral&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{Behavioral Convergence Without Representational Convergence: Persistent Training-History Dependence in Neural Networks}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{Mutlu, Ertuğrul}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{arXiv preprint arXiv:2609.37836}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{2026}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>computerscience</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What an Over-Engineered Parity Classifier Taught Me About Representation</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:23:53 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/what-an-over-engineered-parity-classifier-taught-me-about-representation-40c8</link>
      <guid>https://dev.to/ertugrulmutlu/what-an-over-engineered-parity-classifier-taught-me-about-representation-40c8</guid>
      <description>&lt;h1&gt;
  
  
  I Revisited My Parity Paper — and Found the Representation Was the Real Story
&lt;/h1&gt;

&lt;p&gt;A while ago, I built a deliberately over-engineered classifier for one of the easiest problems in computer science:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is an integer odd or even?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In binary, the answer is already sitting in the least significant bit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0&lt;/code&gt; means even.&lt;br&gt;&lt;br&gt;
&lt;code&gt;1&lt;/code&gt; means odd.&lt;/p&gt;

&lt;p&gt;No machine learning is needed.&lt;/p&gt;

&lt;p&gt;And yet I passed those binary representations through a wavelet transform, summarized the coefficients, clustered them with k-means, and tried to recover parity from the resulting feature space.&lt;/p&gt;

&lt;p&gt;The first version of the experiment looked interesting. It reported about &lt;strong&gt;69.67% accuracy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But when I came back to the project and started preparing a proper revision, I found something more important than the original result:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the experiment itself needed to be rethought.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That ended up making the project much more interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two problems I found in the original version
&lt;/h2&gt;

&lt;p&gt;The first issue was &lt;strong&gt;label leakage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I used parity labels to estimate whether each cluster was mostly odd or mostly even. But the evaluation was not cleanly separated from that calibration step.&lt;/p&gt;

&lt;p&gt;That meant information from the data being evaluated could influence the mapping from clusters to parity labels.&lt;/p&gt;

&lt;p&gt;The second issue was more conceptual.&lt;/p&gt;

&lt;p&gt;I had described the method as &lt;strong&gt;unsupervised&lt;/strong&gt; because k-means itself never receives parity labels.&lt;/p&gt;

&lt;p&gt;That is only partly true.&lt;/p&gt;

&lt;p&gt;The clustering step is unsupervised, but the final cluster-to-parity mapping uses labels. So the complete classifier is not fully unsupervised.&lt;/p&gt;

&lt;p&gt;Those two details changed how the result should be interpreted.&lt;/p&gt;

&lt;p&gt;Instead of trying to defend the original framing, I decided to rebuild the experiment around a stricter evaluation protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rebuilding the experiment
&lt;/h2&gt;

&lt;p&gt;The revised study uses all integers from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;10,000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each integer is converted into a fixed-width 32-bit binary signal.&lt;/p&gt;

&lt;p&gt;The primary pipeline is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Integer
   ↓
32-bit binary representation
   ↓
level-3 db2 wavelet transform
   ↓
mean absolute coefficient magnitude
   ↓
k-means per wavelet subband
   ↓
training-only cluster calibration
   ↓
odd / even prediction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The data is split into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;6,000 training samples&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2,000 validation samples&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2,001 held-out test samples&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Representation and model choices are made using only training and validation data.&lt;/p&gt;

&lt;p&gt;Once the configuration is frozen, the model is re-fit on the combined train and validation sets and evaluated once on the held-out test set.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;84.26% held-out test accuracy&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;with a 95% Wilson confidence interval of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;82.60%–85.79%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Across 20 different stratified train/test splits, the same representation achieved:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;84.20% ± 0.57%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the revised result is numerically stronger than the original one.&lt;/p&gt;

&lt;p&gt;But that is not the part I find most interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The important result is what happens when the representation changes
&lt;/h2&gt;

&lt;p&gt;Parity is determined by one bit.&lt;/p&gt;

&lt;p&gt;So the cleanest test is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;remove that bit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I mask the natural least significant bit and keep the rest of the wavelet pipeline unchanged, validation accuracy drops to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;48.15%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is essentially chance.&lt;/p&gt;

&lt;p&gt;This is important because it rules out the strongest interpretation of the model.&lt;/p&gt;

&lt;p&gt;The pipeline is not discovering the abstract arithmetic rule of parity.&lt;/p&gt;

&lt;p&gt;It is using information that is already present in the binary representation.&lt;/p&gt;

&lt;p&gt;The interesting question becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is that information so easy to recover in some representations and almost impossible to recover in others?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A tiny layout change can have a huge effect
&lt;/h2&gt;

&lt;p&gt;The standard representation uses left-zero padding.&lt;/p&gt;

&lt;p&gt;If I keep the same 32-bit signal but switch to right padding, validation accuracy drops to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;65.20%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The parity rule did not change.&lt;/p&gt;

&lt;p&gt;The bits did not contain less information.&lt;/p&gt;

&lt;p&gt;Only their &lt;strong&gt;position inside the signal&lt;/strong&gt; changed.&lt;/p&gt;

&lt;p&gt;That was the first strong hint that the wavelet transform was creating a geometry that depends heavily on spatial layout.&lt;/p&gt;




&lt;h2&gt;
  
  
  The coarse wavelet band carries almost all of the signal
&lt;/h2&gt;

&lt;p&gt;A level-3 wavelet decomposition produces one approximation band and three detail bands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subband&lt;/th&gt;
&lt;th&gt;Validation accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83.20%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3&lt;/td&gt;
&lt;td&gt;52.35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D2&lt;/td&gt;
&lt;td&gt;50.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;50.40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This surprised me.&lt;/p&gt;

&lt;p&gt;Parity depends on a single bit, so I initially expected the fine-scale detail coefficients to matter most.&lt;/p&gt;

&lt;p&gt;Instead, the approximation band &lt;code&gt;A3&lt;/code&gt; contains almost the entire predictive signal.&lt;/p&gt;

&lt;p&gt;I then compared it with simpler coarse representations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Representation&lt;/th&gt;
&lt;th&gt;Validation accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;db2 A3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83.20%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haar A3&lt;/td&gt;
&lt;td&gt;61.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw MAV&lt;/td&gt;
&lt;td&gt;61.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-level average pooling&lt;/td&gt;
&lt;td&gt;61.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triangular low-pass&lt;/td&gt;
&lt;td&gt;51.40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So this is not just an averaging effect.&lt;/p&gt;

&lt;p&gt;Something specific about the interaction between the db2 filters, downsampling, signal layout, and boundary handling makes the parity bit unusually accessible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I moved the parity bit
&lt;/h2&gt;

&lt;p&gt;This was probably the most revealing experiment.&lt;/p&gt;

&lt;p&gt;I kept exactly the same 32 bits.&lt;/p&gt;

&lt;p&gt;I did not add information.&lt;/p&gt;

&lt;p&gt;I did not remove information.&lt;/p&gt;

&lt;p&gt;I only moved the parity-carrying bit to different positions in the signal.&lt;/p&gt;

&lt;p&gt;The result changed dramatically.&lt;/p&gt;

&lt;p&gt;At the natural position, validation accuracy is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;83.20%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At some positions, it falls much closer to chance.&lt;/p&gt;

&lt;p&gt;At the best tested position, it reaches:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;98.60%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing about the underlying parity information changed.&lt;/p&gt;

&lt;p&gt;Only its position changed.&lt;/p&gt;

&lt;p&gt;That makes the interpretation much clearer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the model is not learning a representation-independent rule.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is exploiting a representation-dependent structure created by the transform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Even boundary handling changes the result
&lt;/h2&gt;

&lt;p&gt;Wavelet transforms need a rule for what happens at the edges of a finite signal.&lt;/p&gt;

&lt;p&gt;I tested several boundary-extension modes while keeping the rest of the model fixed.&lt;/p&gt;

&lt;p&gt;The resulting validation accuracy ranged from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;54.45% to 83.20%&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a huge swing from what might look like a low-level implementation choice.&lt;/p&gt;

&lt;p&gt;In this experiment, boundary handling is not a minor detail.&lt;/p&gt;

&lt;p&gt;It is part of the mechanism.&lt;/p&gt;




&lt;h2&gt;
  
  
  Generalization exposes the weakness
&lt;/h2&gt;

&lt;p&gt;I also froze the model trained on &lt;code&gt;0–10,000&lt;/code&gt; and evaluated it on increasingly distant numerical ranges without recalibration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test range&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10,001–20,000&lt;/td&gt;
&lt;td&gt;79.98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20,001–50,000&lt;/td&gt;
&lt;td&gt;71.51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,001–100,000&lt;/td&gt;
&lt;td&gt;66.32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,001–1,000,000&lt;/td&gt;
&lt;td&gt;59.69%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Performance steadily degrades as the magnitude distribution moves away from the training range.&lt;/p&gt;

&lt;p&gt;At first, that might suggest that larger integers are inherently harder.&lt;/p&gt;

&lt;p&gt;But when separate models are trained and tested inside fixed bit-length bands, accuracy remains roughly between &lt;strong&gt;78% and 88%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the main problem is not magnitude itself.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;representation shift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The geometry that works in one numerical regime does not stay stable in another.&lt;/p&gt;




&lt;h2&gt;
  
  
  More data does not solve it either
&lt;/h2&gt;

&lt;p&gt;On a wider &lt;code&gt;0–100,000&lt;/code&gt; distribution, I increased the training set from 500 examples all the way to 80,000.&lt;/p&gt;

&lt;p&gt;The performance ceiling barely moved.&lt;/p&gt;

&lt;p&gt;That suggests the bottleneck is not the amount of data.&lt;/p&gt;

&lt;p&gt;The bottleneck is the representation and the very simple clustering model operating on top of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I changed my mind about
&lt;/h2&gt;

&lt;p&gt;The first version of this project was mostly about a surprising classifier result.&lt;/p&gt;

&lt;p&gt;The revised version is not.&lt;/p&gt;

&lt;p&gt;The more interesting story is that the &lt;strong&gt;same symbolic information can become easy, difficult, or almost impossible to recover depending on how it is represented&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That changed how I think about the experiment.&lt;/p&gt;

&lt;p&gt;The question is no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can wavelets classify parity?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course parity can be solved exactly with one bit.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“What does the representation make accessible to a simple model?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more general machine-learning question.&lt;/p&gt;

&lt;p&gt;A model can perform well without learning the abstract rule we think it learned.&lt;/p&gt;

&lt;p&gt;Sometimes preprocessing creates a useful proxy.&lt;/p&gt;

&lt;p&gt;Sometimes position matters more than expected.&lt;/p&gt;

&lt;p&gt;Sometimes a boundary condition changes the geometry enough to alter the result completely.&lt;/p&gt;

&lt;p&gt;And sometimes the right experiment is not another accuracy benchmark, but an ablation that tells you &lt;strong&gt;where the accuracy came from&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I am glad I revisited the paper
&lt;/h2&gt;

&lt;p&gt;Finding problems in an earlier experiment is uncomfortable.&lt;/p&gt;

&lt;p&gt;But I think revisiting it made the work much stronger.&lt;/p&gt;

&lt;p&gt;The revised version now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separates training, validation, and test data cleanly&lt;/li&gt;
&lt;li&gt;describes the supervised and unsupervised parts accurately&lt;/li&gt;
&lt;li&gt;includes repeated-split robustness checks&lt;/li&gt;
&lt;li&gt;tests out-of-distribution generalization&lt;/li&gt;
&lt;li&gt;isolates the role of the LSB&lt;/li&gt;
&lt;li&gt;studies subbands, padding, bit position, and boundary conditions&lt;/li&gt;
&lt;li&gt;includes frozen splits, predictions, metrics, and reproducibility artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final claim is narrower than the original one.&lt;/p&gt;

&lt;p&gt;I am much more comfortable with it because of that.&lt;/p&gt;

&lt;p&gt;The wavelet pipeline does &lt;strong&gt;not&lt;/strong&gt; discover parity as an abstract arithmetic rule.&lt;/p&gt;

&lt;p&gt;Instead, it shows how a classical signal-processing transform can make symbolic information that is already present in the input more or less statistically accessible.&lt;/p&gt;

&lt;p&gt;For me, that ended up being the real result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducibility
&lt;/h2&gt;

&lt;p&gt;The code, frozen experiment artifacts, prediction outputs, and analysis results are available here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Ertugrulmutlu/Using-Wavelets-and-Clustering-to-Predict-Odd-or-Even-Numbers" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/Using-Wavelets-and-Clustering-to-Predict-Odd-or-Even-Numbers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the exact manuscript snapshot, use the Git tag:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;paper-v2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The revised arXiv version is scheduled to become public on September 29, 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paper:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://arxiv.org/abs/2511.00071" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2511.00071&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;This project started as an unnecessarily complicated way to answer a one-bit question.&lt;/p&gt;

&lt;p&gt;The revision taught me something more useful than the original accuracy number:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before asking what a model learned, ask what the representation made easy to learn.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the part of this experiment I will probably remember.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>datascience</category>
      <category>signalprocessing</category>
    </item>
    <item>
      <title>PromptLedger v0.7 — Turning prompt evaluation into local regression gates</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 13 Jul 2026 14:14:40 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/promptledger-v07-turning-prompt-evaluation-into-local-regression-gates-4ano</link>
      <guid>https://dev.to/ertugrulmutlu/promptledger-v07-turning-prompt-evaluation-into-local-regression-gates-4ano</guid>
      <description>&lt;h2&gt;
  
  
  Devlog — Part 6
&lt;/h2&gt;

&lt;p&gt;PromptLedger v0.7 is out.&lt;/p&gt;

&lt;p&gt;The previous release made prompt history easier to inspect.&lt;/p&gt;

&lt;p&gt;This release makes prompt changes easier to evaluate.&lt;/p&gt;

&lt;p&gt;Until now, PromptLedger could answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed?&lt;/li&gt;
&lt;li&gt;Which version is in production?&lt;/li&gt;
&lt;li&gt;Which version was marked stable?&lt;/li&gt;
&lt;li&gt;Why was a new version created?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But one important question was still missing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the new prompt actually perform better?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A text diff can show that a prompt changed.&lt;/p&gt;

&lt;p&gt;It cannot tell you whether accuracy improved, latency increased, cost became unacceptable, or an important behavior regressed.&lt;/p&gt;

&lt;p&gt;PromptLedger v0.7 introduces evaluation runs, metric comparisons, and policy-based regression gates.&lt;/p&gt;

&lt;p&gt;The workflow is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version → diff → review → evaluate → gate → promote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Evaluation runs
&lt;/h2&gt;

&lt;p&gt;PromptLedger can now store benchmark results produced by external tools.&lt;/p&gt;

&lt;p&gt;Each evaluation run is attached to a concrete prompt version and can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;benchmark suite&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;dataset hash&lt;/li&gt;
&lt;li&gt;numeric metrics&lt;/li&gt;
&lt;li&gt;run metadata&lt;/li&gt;
&lt;li&gt;creation time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accuracy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.018&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sample_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A prompt version can have multiple evaluation runs.&lt;/p&gt;

&lt;p&gt;This is important because the same prompt may be tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on multiple models&lt;/li&gt;
&lt;li&gt;against different datasets&lt;/li&gt;
&lt;li&gt;with different seeds&lt;/li&gt;
&lt;li&gt;across several benchmark suites&lt;/li&gt;
&lt;li&gt;multiple times as the surrounding system changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Evaluation history is stored separately from prompt history, so repeated runs are preserved instead of overwriting each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recording results
&lt;/h2&gt;

&lt;p&gt;PromptLedger does not run the benchmark itself.&lt;/p&gt;

&lt;p&gt;External tools produce the evaluation result, and PromptLedger records it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger &lt;span class="nb"&gt;eval &lt;/span&gt;record &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ref&lt;/span&gt; staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; evaluation-result.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reference may be a concrete version or a label such as &lt;code&gt;prod&lt;/code&gt; or &lt;code&gt;staging&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even when a moving label is used, the evaluation is stored against the concrete version the label currently points to.&lt;/p&gt;

&lt;p&gt;This keeps the historical record stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing prompt versions
&lt;/h2&gt;

&lt;p&gt;Evaluation runs can be compared between two prompt versions or labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger &lt;span class="nb"&gt;eval &lt;/span&gt;compare &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt; prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to&lt;/span&gt; staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--suite&lt;/span&gt; support-v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; test-model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Evaluation comparison: onboarding
Suite: support-v1
Model: test-model

Metric          prod/v1     staging/v2    Delta
accuracy        0.84        0.91          +0.07
latency_ms      380         410           +30
cost_usd        0.016       0.018         +0.002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PromptLedger selects compatible runs deterministically.&lt;/p&gt;

&lt;p&gt;It does not silently compare results from unrelated benchmark suites or different models.&lt;/p&gt;

&lt;p&gt;A comparison is only useful when both sides describe the same experiment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Regression gates
&lt;/h2&gt;

&lt;p&gt;Comparisons show what changed.&lt;/p&gt;

&lt;p&gt;Regression gates decide whether the change is acceptable.&lt;/p&gt;

&lt;p&gt;A gate policy defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether a metric should be higher or lower&lt;/li&gt;
&lt;li&gt;how much absolute regression is allowed&lt;/li&gt;
&lt;li&gt;how much percentage regression is allowed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accuracy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"higher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"max_regression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lower"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"max_regression_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lower"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"max_regression_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the gate with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger &lt;span class="nb"&gt;eval &lt;/span&gt;gate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt; prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to&lt;/span&gt; staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy&lt;/span&gt; promptledger-gate.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS accuracy: candidate improved by 0.07
PASS latency_ms: regression within allowed threshold
PASS cost_usd: regression within allowed threshold

Gate passed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a regression exceeds the policy limit, the command returns a non-zero exit code.&lt;/p&gt;

&lt;p&gt;That makes evaluation gates suitable for CI workflows.&lt;/p&gt;

&lt;p&gt;A prompt change can now fail a build in the same way as a failed test or performance regression.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dashboard evaluation history
&lt;/h2&gt;

&lt;p&gt;The local dashboard has also been updated.&lt;/p&gt;

&lt;p&gt;Prompt details can now show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;evaluation run history&lt;/li&gt;
&lt;li&gt;benchmark suite&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;dataset information&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;metric differences between versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This connects prompt history and evaluation history in the same interface.&lt;/p&gt;

&lt;p&gt;A developer can inspect what changed in the prompt and how the measured behavior changed alongside it.&lt;/p&gt;

&lt;p&gt;The dashboard remains local-first, and prompt content remains read-only.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sequence-aware prompt diffs
&lt;/h2&gt;

&lt;p&gt;The dashboard comparison system was also improved.&lt;/p&gt;

&lt;p&gt;The previous implementation compared lines mainly by their array position.&lt;/p&gt;

&lt;p&gt;That works for simple replacements, but inserting one new line could make every following line appear changed.&lt;/p&gt;

&lt;p&gt;The new comparison uses a sequence-aware diff based on Python’s &lt;code&gt;difflib.SequenceMatcher&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It now handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserted lines&lt;/li&gt;
&lt;li&gt;removed lines&lt;/li&gt;
&lt;li&gt;replaced lines&lt;/li&gt;
&lt;li&gt;unchanged sections&lt;/li&gt;
&lt;li&gt;aligned line numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This produces a much more accurate view of how a prompt changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed in v0.7
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dedicated evaluation history
&lt;/h3&gt;

&lt;p&gt;Benchmark results now live in their own evaluation-run records instead of being treated as static prompt metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version and label comparisons
&lt;/h3&gt;

&lt;p&gt;Evaluation results can be compared through concrete versions or release labels such as &lt;code&gt;prod&lt;/code&gt; and &lt;code&gt;staging&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy-based regression gates
&lt;/h3&gt;

&lt;p&gt;Teams can define acceptable quality, latency, and cost regressions through JSON policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI-friendly exit codes
&lt;/h3&gt;

&lt;p&gt;Passing gates, detected regressions, invalid input, and operational failures return distinct exit codes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard evaluation visibility
&lt;/h3&gt;

&lt;p&gt;Evaluation runs and metric differences are now visible alongside prompt history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved visual diffs
&lt;/h3&gt;

&lt;p&gt;Prompt comparisons now use a sequence-aware algorithm instead of simple line-position matching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-destructive migration
&lt;/h3&gt;

&lt;p&gt;Existing PromptLedger databases are migrated to the new schema without removing prompt versions, labels, markers, or metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design boundary
&lt;/h2&gt;

&lt;p&gt;PromptLedger still does not execute prompts.&lt;/p&gt;

&lt;p&gt;It does not call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI&lt;/li&gt;
&lt;li&gt;Anthropic&lt;/li&gt;
&lt;li&gt;Gemini&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;any other model provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also does not include an automatic LLM judge.&lt;/p&gt;

&lt;p&gt;That separation is intentional.&lt;/p&gt;

&lt;p&gt;PromptLedger should remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local-first&lt;/li&gt;
&lt;li&gt;provider-independent&lt;/li&gt;
&lt;li&gt;deterministic&lt;/li&gt;
&lt;li&gt;lightweight&lt;/li&gt;
&lt;li&gt;compatible with external benchmark systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your evaluation framework runs the experiment.&lt;/p&gt;

&lt;p&gt;PromptLedger keeps the history, compares the results, and applies the release policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The v0.7 release includes 119 passing tests covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;li&gt;repeated evaluation runs&lt;/li&gt;
&lt;li&gt;label resolution&lt;/li&gt;
&lt;li&gt;malformed metrics&lt;/li&gt;
&lt;li&gt;non-finite values&lt;/li&gt;
&lt;li&gt;comparison compatibility&lt;/li&gt;
&lt;li&gt;deterministic ordering&lt;/li&gt;
&lt;li&gt;missing metrics&lt;/li&gt;
&lt;li&gt;zero baselines&lt;/li&gt;
&lt;li&gt;regression policies&lt;/li&gt;
&lt;li&gt;CLI exit codes&lt;/li&gt;
&lt;li&gt;dashboard endpoints&lt;/li&gt;
&lt;li&gt;sequence-aware diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PowerShell file-based evaluation workflow was also tested end to end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; promptledger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize a local database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Prompt version control is useful, but version history alone cannot tell you whether a release is safe.&lt;/p&gt;

&lt;p&gt;A new prompt may look cleaner while producing worse answers.&lt;/p&gt;

&lt;p&gt;It may improve accuracy while increasing latency or cost.&lt;/p&gt;

&lt;p&gt;It may pass one benchmark while silently regressing another.&lt;/p&gt;

&lt;p&gt;PromptLedger v0.7 connects prompt changes to measurable results.&lt;/p&gt;

&lt;p&gt;The goal is not to build another evaluation framework.&lt;/p&gt;

&lt;p&gt;The goal is to provide the missing release layer between prompt experimentation and production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What changed?
How did performance change?
Is the regression acceptable?
Should this version be promoted?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PromptLedger is moving from a tool that stores prompt history to a tool that helps control prompt releases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/promptledger/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Ertugrulmutlu/promptledger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LinkedIn: &lt;a href="https://www.linkedin.com/in/ertugrul-mutlu/?locale=en" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://ertugrulmutlu.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>showdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>One Gradient Spike, Six Batches to NaN: Debugging a Deterministic Neural-Network Training Failure</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 13 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/one-gradient-spike-six-batches-to-nan-debugging-a-deterministic-neural-network-training-failure-4bdh</link>
      <guid>https://dev.to/ertugrulmutlu/one-gradient-spike-six-batches-to-nan-debugging-a-deterministic-neural-network-training-failure-4bdh</guid>
      <description>&lt;p&gt;The first visible NaN appeared in the final linear layer at batch 46.&lt;/p&gt;

&lt;p&gt;By then, the model was already effectively destroyed.&lt;/p&gt;

&lt;p&gt;Six batches earlier, every tensor was still finite. The loss was finite. The gradients were finite. The momentum buffers were finite. Nothing had crossed the clean boundary between a valid floating-point value and NaN.&lt;/p&gt;

&lt;p&gt;But the training state was already in a runaway regime.&lt;/p&gt;

&lt;p&gt;This is how an aggregate-analysis failure led me backward—from a corrupted checkpoint, to one reproducible batch, to the finite gradient spike that began the collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;I was running a sequential-training experiment on MNIST using a small CNN.&lt;/p&gt;

&lt;p&gt;The experiment compared two training histories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SABC:&lt;/strong&gt; digits 0–4, then digits 5–9, followed by a shared balanced 0–9 relaxation phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SBAC:&lt;/strong&gt; digits 5–9, then digits 0–4, followed by the same relaxation phase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a mechanism-control experiment, I replaced the existing ReLU activation with LeakyReLU while keeping the rest of the protocol fixed.&lt;/p&gt;

&lt;p&gt;The setup was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SimpleCNN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;no normalization&lt;/li&gt;
&lt;li&gt;LeakyReLU slope &lt;code&gt;0.01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SGD&lt;/li&gt;
&lt;li&gt;learning rate &lt;code&gt;0.05&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;momentum &lt;code&gt;0.9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;no weight decay&lt;/li&gt;
&lt;li&gt;deterministic seeds: &lt;code&gt;101&lt;/code&gt;, &lt;code&gt;202&lt;/code&gt;, &lt;code&gt;303&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;505&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;two histories per seed&lt;/li&gt;
&lt;li&gt;ten total runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four of the ten runs became numerically invalid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SABC seed303&lt;/li&gt;
&lt;li&gt;SBAC seed101&lt;/li&gt;
&lt;li&gt;SBAC seed202&lt;/li&gt;
&lt;li&gt;SBAC seed303&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all four cases, the first known bad checkpoint was epoch 11.&lt;/p&gt;

&lt;p&gt;Epoch 10 was the end of the first training domain. Epoch 11 was the first epoch after switching to the second domain.&lt;/p&gt;

&lt;p&gt;That timing was suspicious, but not enough to establish a cause. The instability could depend on the interaction between the activation function, learning rate, momentum, domain switch, model state, task order, and random seed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validator found it first
&lt;/h2&gt;

&lt;p&gt;I did not discover the problem because the training loop printed &lt;code&gt;NaN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A later aggregate-analysis validator rejected two checkpoints that should have represented the same model state: the final sequential-training checkpoint and the step-zero checkpoint of the relaxation phase.&lt;/p&gt;

&lt;p&gt;At first, this looked like a serialization or bookkeeping issue.&lt;/p&gt;

&lt;p&gt;A direct checkpoint audit showed that the affected files contained NaN and Inf values.&lt;/p&gt;

&lt;p&gt;For three failed runs, all 421,642 parameters were NaN in the first bad checkpoint. In SABC seed303, the checkpoint contained 421,610 NaNs and 32 positive infinities.&lt;/p&gt;

&lt;p&gt;The validator had not caused the problem. It had prevented corrupted runs from entering the scientific analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I initially got wrong
&lt;/h2&gt;

&lt;p&gt;The original runs had been launched in parallel on one GPU.&lt;/p&gt;

&lt;p&gt;My first suspicion was therefore environmental:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPU contention&lt;/li&gt;
&lt;li&gt;multiprocessing interference&lt;/li&gt;
&lt;li&gt;nondeterministic CUDA behavior&lt;/li&gt;
&lt;li&gt;a transient scheduling or hardware issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To test that, I reran one known failing configuration—seed101-SBAC—alone with a detailed numerical tracer.&lt;/p&gt;

&lt;p&gt;It failed again at exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;epoch 11&lt;/li&gt;
&lt;li&gt;phase 2&lt;/li&gt;
&lt;li&gt;phase data A&lt;/li&gt;
&lt;li&gt;batch 46&lt;/li&gt;
&lt;li&gt;after the forward pass&lt;/li&gt;
&lt;li&gt;first failing tensor: &lt;code&gt;fc2_logits&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The isolated repeat also used the same input hash, label hash, label histogram, and non-finite counts.&lt;/p&gt;

&lt;p&gt;The failing logits contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;128 NaNs&lt;/li&gt;
&lt;li&gt;128 positive infinities&lt;/li&gt;
&lt;li&gt;384 negative infinities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reproducing the same failure in isolation, on the same batch and at the same network stage, makes a one-off GPU collision very unlikely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The first non-finite tensor appeared at batch 46.&lt;/p&gt;

&lt;p&gt;But the failure began earlier.&lt;/p&gt;

&lt;p&gt;At batch 39, the system still looked ordinary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loss: about &lt;code&gt;3.03&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;global gradient norm: about &lt;code&gt;9.11&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;parameter magnitudes still small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At batch 40, the trajectory changed sharply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loss jumped to about &lt;code&gt;44.59&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;global gradient norm jumped to about &lt;code&gt;483.74&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;maximum gradient reached about &lt;code&gt;266.7&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything was still finite.&lt;/p&gt;

&lt;p&gt;After the optimizer step, the largest momentum-buffer value rose from about &lt;code&gt;9.13&lt;/code&gt; to about &lt;code&gt;269.93&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following batches did not recover.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Batch&lt;/th&gt;
&lt;th&gt;Loss&lt;/th&gt;
&lt;th&gt;Global gradient norm&lt;/th&gt;
&lt;th&gt;Max FC1 activation&lt;/th&gt;
&lt;th&gt;Interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.03&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ordinary-looking state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;&lt;code&gt;44.59&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;483.74&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;17.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First large finite gradient event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.48 × 10^3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;988&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;913&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runaway growth becomes visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.04 × 10^5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7.82 × 10^4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5.92 × 10^4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strong amplification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4.71 × 10^8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6.25 × 10^7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.02 × 10^6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extreme but finite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.23 × 10^18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.19 × 10^15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6.87 × 10^15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model effectively destroyed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.97 × 10^36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Final logits overflow float32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Immediately before the failing forward pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fc1.weight&lt;/code&gt; magnitude was around &lt;code&gt;10^12&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fc2.weight&lt;/code&gt; magnitude was around &lt;code&gt;10^13&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;momentum buffers were around &lt;code&gt;10^14–10^15&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the maximum FC1 activation was about &lt;code&gt;1.97 × 10^36&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final linear layer then overflowed float32 and produced NaN and signed Inf values.&lt;/p&gt;

&lt;p&gt;The trace supports the following sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;large but finite gradient event
→ rapid growth in momentum and parameter scale
→ rapidly growing activations
→ float32 overflow in the final linear layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Momentum appears to have amplified the instability, but the trace does not establish what caused the initial gradient spike.&lt;/p&gt;

&lt;p&gt;Likewise, &lt;code&gt;fc2_logits&lt;/code&gt; was the first non-finite tensor, but it was the terminal symptom, not necessarily the origin of the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finite is not healthy
&lt;/h2&gt;

&lt;p&gt;A standard safeguard is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;stop_training&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That check is necessary, but late.&lt;/p&gt;

&lt;p&gt;At batch 45, the model had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loss around &lt;code&gt;10^18&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;gradient norm around &lt;code&gt;10^15&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;momentum buffers around &lt;code&gt;10^14&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;activations around &lt;code&gt;10^15&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every value was still finite.&lt;/p&gt;

&lt;p&gt;A binary finite/non-finite check would classify the state as valid. In practice, the model was already beyond recovery.&lt;/p&gt;

&lt;p&gt;This suggests a more useful distinction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;numerically finite&lt;/li&gt;
&lt;li&gt;numerically extreme&lt;/li&gt;
&lt;li&gt;numerically non-finite&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The transition from the first state to the second is where the most useful debugging information exists.&lt;/p&gt;

&lt;p&gt;By the time NaN appears, the original trigger may be several optimizer steps in the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the debugging workflow mattered
&lt;/h2&gt;

&lt;p&gt;The investigation followed a simple sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aggregate analysis failed.&lt;/li&gt;
&lt;li&gt;A pair validator caught a checkpoint inconsistency.&lt;/li&gt;
&lt;li&gt;A checkpoint audit found NaN and Inf values.&lt;/li&gt;
&lt;li&gt;A fail-fast numerical tracer was added.&lt;/li&gt;
&lt;li&gt;A known failure was reproduced in isolation.&lt;/li&gt;
&lt;li&gt;The first non-finite tensor was localized to one batch and one network stage.&lt;/li&gt;
&lt;li&gt;Earlier batches revealed the actual runaway dynamics.&lt;/li&gt;
&lt;li&gt;A finite control run showed that the tracer did not automatically cause failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That workflow is reusable.&lt;/p&gt;

&lt;p&gt;For numerical debugging, I would now monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;global gradient norm&lt;/li&gt;
&lt;li&gt;maximum absolute gradient&lt;/li&gt;
&lt;li&gt;parameter norms&lt;/li&gt;
&lt;li&gt;momentum-buffer norms&lt;/li&gt;
&lt;li&gt;activation magnitudes&lt;/li&gt;
&lt;li&gt;model state before and after the optimizer step&lt;/li&gt;
&lt;li&gt;deterministic hashes of the input and labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hashes were especially useful.&lt;/p&gt;

&lt;p&gt;“It failed at batch 46 again” is weaker evidence than:&lt;/p&gt;

&lt;p&gt;“It failed at batch 46 again on the exact same input and label tensors.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What the evidence supports
&lt;/h2&gt;

&lt;p&gt;The careful conclusion is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Under this sequential-training protocol, LeakyReLU with SGD at learning rate 0.05 and momentum 0.9 showed deterministic, seed-dependent numerical instability in 4 of 10 runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The evidence does not show that LeakyReLU universally causes gradient explosions.&lt;/p&gt;

&lt;p&gt;It also does not show that momentum, the domain switch, the task order, or the final linear layer independently caused the failure.&lt;/p&gt;

&lt;p&gt;The instability may depend on an interaction between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;activation function&lt;/li&gt;
&lt;li&gt;learning rate&lt;/li&gt;
&lt;li&gt;momentum&lt;/li&gt;
&lt;li&gt;domain switch&lt;/li&gt;
&lt;li&gt;model state&lt;/li&gt;
&lt;li&gt;random seed&lt;/li&gt;
&lt;li&gt;task order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A control run, seed404-SBAC, completed the same requested epoch range without non-finite values.&lt;/p&gt;

&lt;p&gt;So the protocol was not universally unstable.&lt;/p&gt;

&lt;p&gt;But a 4/10 failure rate made this LeakyReLU branch unreliable for paired scientific analysis under the tested configuration.&lt;/p&gt;

&lt;p&gt;The existing ReLU results were not affected by this failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The most useful artifact from this experiment was not the NaN checkpoint.&lt;/p&gt;

&lt;p&gt;It was the finite trace leading into it.&lt;/p&gt;

&lt;p&gt;At batch 39, the system looked ordinary.&lt;/p&gt;

&lt;p&gt;At batch 40, the gradient norm jumped from roughly &lt;code&gt;9&lt;/code&gt; to roughly &lt;code&gt;484&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At batch 45, the model was still technically finite but already had gradients around &lt;code&gt;10^15&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At batch 46, a hidden activation reached approximately &lt;code&gt;10^36&lt;/code&gt;, and the final linear layer overflowed float32.&lt;/p&gt;

&lt;p&gt;The main lesson is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A finite tensor is not necessarily evidence of a healthy training state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;NaN checks are essential, but they are late alarms.&lt;/p&gt;

&lt;p&gt;The final NaN showed where the computation stopped being representable.&lt;/p&gt;

&lt;p&gt;The six finite batches before it showed how the model got there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>learning</category>
    </item>
    <item>
      <title>OpenAnima v1 — Open-Source Desktop Overlay Engine for Windows</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Fri, 15 May 2026 06:15:20 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/openanima-v1-open-source-desktop-overlay-engine-for-windows-3m9c</link>
      <guid>https://dev.to/ertugrulmutlu/openanima-v1-open-source-desktop-overlay-engine-for-windows-3m9c</guid>
      <description>&lt;h1&gt;
  
  
  OpenAnima v1 — Open-Source Desktop Overlay Engine for Windows
&lt;/h1&gt;

&lt;p&gt;After months of building, experimenting, rewriting systems, debugging strange desktop issues, and testing different asset formats, OpenAnima v1 is finally out.&lt;/p&gt;

&lt;p&gt;OpenAnima is an open-source desktop overlay engine for Windows that lets you place animated assets directly onto your desktop.&lt;/p&gt;

&lt;p&gt;You can use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIFs&lt;/li&gt;
&lt;li&gt;Static images&lt;/li&gt;
&lt;li&gt;Sprite strips&lt;/li&gt;
&lt;li&gt;Spritesheets&lt;/li&gt;
&lt;li&gt;Frame-folder animations&lt;/li&gt;
&lt;li&gt;RPG-style HUD elements&lt;/li&gt;
&lt;li&gt;Transparent animated assets&lt;/li&gt;
&lt;li&gt;Pixel-art characters&lt;/li&gt;
&lt;li&gt;Desktop companions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of the project was simple:&lt;/p&gt;

&lt;p&gt;Create a lightweight desktop overlay system that feels flexible, customizable, and fun to experiment with.&lt;/p&gt;




&lt;h2&gt;
  
  
  What OpenAnima Can Do
&lt;/h2&gt;

&lt;p&gt;OpenAnima allows you to spawn movable overlay windows directly on top of your desktop.&lt;/p&gt;

&lt;p&gt;Each overlay can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dragged freely&lt;/li&gt;
&lt;li&gt;resized&lt;/li&gt;
&lt;li&gt;locked in place&lt;/li&gt;
&lt;li&gt;made click-through&lt;/li&gt;
&lt;li&gt;set always-on-top&lt;/li&gt;
&lt;li&gt;adjusted for opacity&lt;/li&gt;
&lt;li&gt;animated with custom FPS settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application also restores overlay states between sessions, so your desktop setup persists after restarting the app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Supported Asset Types
&lt;/h2&gt;

&lt;p&gt;One of the biggest goals of the project was supporting multiple animation workflows instead of only GIFs.&lt;/p&gt;

&lt;p&gt;Currently supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIF animations&lt;/li&gt;
&lt;li&gt;Static images (.png, .jpg, .webp)&lt;/li&gt;
&lt;li&gt;Frame-folder animations&lt;/li&gt;
&lt;li&gt;Horizontal sprite strips&lt;/li&gt;
&lt;li&gt;Vertical sprite strips&lt;/li&gt;
&lt;li&gt;Spritesheets with metadata&lt;/li&gt;
&lt;li&gt;Basic HUD/UI overlay assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine also includes an asset analyzer system that tries to detect asset types automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built With
&lt;/h2&gt;

&lt;p&gt;OpenAnima is primarily built using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;PyQt6&lt;/li&gt;
&lt;li&gt;QMovie / QPixmap rendering systems&lt;/li&gt;
&lt;li&gt;Custom animation parsing logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of time went into handling edge cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden overlays&lt;/li&gt;
&lt;li&gt;broken configs&lt;/li&gt;
&lt;li&gt;off-screen windows&lt;/li&gt;
&lt;li&gt;click-through recovery&lt;/li&gt;
&lt;li&gt;corrupted animation states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1 focuses heavily on stability and recovery tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Features Added During Development
&lt;/h2&gt;

&lt;p&gt;Some systems added throughout development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent overlay state saving&lt;/li&gt;
&lt;li&gt;Safer config recovery&lt;/li&gt;
&lt;li&gt;Recovery tools for invisible overlays&lt;/li&gt;
&lt;li&gt;Logging system&lt;/li&gt;
&lt;li&gt;Diagnostics panel&lt;/li&gt;
&lt;li&gt;Asset metadata support&lt;/li&gt;
&lt;li&gt;Import wizard&lt;/li&gt;
&lt;li&gt;Sprite animation handling&lt;/li&gt;
&lt;li&gt;Basic layered UI rendering&lt;/li&gt;
&lt;li&gt;System tray controls&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;I always liked desktop customization tools, animated desktop companions, game HUD overlays, and lightweight desktop effects.&lt;/p&gt;

&lt;p&gt;Most existing solutions were either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too limited&lt;/li&gt;
&lt;li&gt;too complicated&lt;/li&gt;
&lt;li&gt;too specific&lt;/li&gt;
&lt;li&gt;abandoned&lt;/li&gt;
&lt;li&gt;or focused on only one asset type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I started experimenting with building my own system.&lt;/p&gt;

&lt;p&gt;The project slowly evolved from “just display GIFs on desktop” into a more general desktop overlay engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Plans
&lt;/h2&gt;

&lt;p&gt;Some ideas planned for future versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better transparent video support&lt;/li&gt;
&lt;li&gt;More advanced animation systems&lt;/li&gt;
&lt;li&gt;Improved UI tools&lt;/li&gt;
&lt;li&gt;Better performance optimizations&lt;/li&gt;
&lt;li&gt;Asset marketplace/import improvements&lt;/li&gt;
&lt;li&gt;More overlay interaction systems&lt;/li&gt;
&lt;li&gt;Possible 3D asset support in the future&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;Website / Download:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAnima Website: &lt;a href="https://ertugrulmutlu.github.io/OpenAnima" rel="noopener noreferrer"&gt;https://ertugrulmutlu.github.io/OpenAnima/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAnima GitHub Repository: &lt;a href="https://github.com/Ertugrulmutlu/OpenAnima" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/OpenAnima&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;itch.io Page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAnima itch.io Page: &lt;a href="https://ertugrulmutlu.itch.io/openanima" rel="noopener noreferrer"&gt;https://ertugrulmutlu.itch.io/openanima&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instagram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAnima Instagram: &lt;a href="https://www.instagram.com/openanimaengine" rel="noopener noreferrer"&gt;https://www.instagram.com/openanimaengine&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;OpenAnima started as a small side project and slowly became a much larger system than I originally expected.&lt;/p&gt;

&lt;p&gt;There is still a lot to improve, but releasing v1 feels like a huge milestone for the project.&lt;/p&gt;

&lt;p&gt;Feedback, ideas, bug reports, and experiments are always welcome.&lt;/p&gt;

&lt;p&gt;I’m excited to see what people create with it.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>PromptLedger v0.6 — Turning prompt history into a local workspace dashboard</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 04 May 2026 16:23:46 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/promptledger-v06-turning-prompt-history-into-a-local-workspace-dashboard-3cen</link>
      <guid>https://dev.to/ertugrulmutlu/promptledger-v06-turning-prompt-history-into-a-local-workspace-dashboard-3cen</guid>
      <description>&lt;h2&gt;
  
  
  Devlog — Part 5
&lt;/h2&gt;

&lt;p&gt;PromptLedger v0.6 is out.&lt;/p&gt;

&lt;p&gt;This release changes how PromptLedger feels to use.&lt;/p&gt;

&lt;p&gt;Until now, PromptLedger was primarily a terminal-first tool with a small read-only viewer. The core workflow already existed: store prompt versions, compare changes, label releases, mark important versions, and keep everything local in SQLite.&lt;/p&gt;

&lt;p&gt;That worked.&lt;/p&gt;

&lt;p&gt;But once a prompt library grows, a simple viewer stops being enough.&lt;/p&gt;

&lt;p&gt;Prompt iteration is not just about storing text. It is about navigating decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which version worked best?&lt;/li&gt;
&lt;li&gt;Which version became stable?&lt;/li&gt;
&lt;li&gt;What changed between versions?&lt;/li&gt;
&lt;li&gt;Which prompt belongs to which workflow?&lt;/li&gt;
&lt;li&gt;Which prompt should be reused?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are easier to answer when the history is not only stored, but also visible and interactive.&lt;/p&gt;

&lt;p&gt;So v0.6 turns the old viewer into a local prompt workspace dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workspace dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard now starts with a card-based workspace instead of a single prompt view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpruubjud6h477k1pzua.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpruubjud6h477k1pzua.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each prompt appears as a card showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latest version&lt;/li&gt;
&lt;li&gt;collection and role&lt;/li&gt;
&lt;li&gt;markers such as stable or milestone&lt;/li&gt;
&lt;li&gt;short preview of the prompt&lt;/li&gt;
&lt;li&gt;last updated time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easier to scan a prompt library and understand what exists without opening each item.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prompt detail view
&lt;/h2&gt;

&lt;p&gt;Clicking a card opens a detailed view of the prompt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1ukgab6e3ixuvfr39js.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1ukgab6e3ixuvfr39js.png" alt=" " width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This view includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full prompt text&lt;/li&gt;
&lt;li&gt;version metadata&lt;/li&gt;
&lt;li&gt;markers and labels&lt;/li&gt;
&lt;li&gt;version timeline&lt;/li&gt;
&lt;li&gt;side-by-side comparison&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to make it easy to understand how a prompt evolved over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed in v0.6
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Workspace instead of viewer
&lt;/h3&gt;

&lt;p&gt;The dashboard is no longer just a viewer. It is a workspace where prompts can be explored and organized visually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Card-based interaction
&lt;/h3&gt;

&lt;p&gt;Prompts are now treated as objects instead of rows in a list. Cards provide quick context and actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marker actions in the UI
&lt;/h3&gt;

&lt;p&gt;Stable and milestone markers can now be applied directly from the dashboard. These actions use the same underlying marker system as the CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare workflow
&lt;/h3&gt;

&lt;p&gt;The compare view has been improved to clearly show differences between versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usability improvements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;keyboard shortcuts for faster navigation&lt;/li&gt;
&lt;li&gt;copy actions with feedback&lt;/li&gt;
&lt;li&gt;better empty states&lt;/li&gt;
&lt;li&gt;improved hover and selection behavior&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Design direction
&lt;/h2&gt;

&lt;p&gt;PromptLedger remains intentionally limited in scope.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local-first&lt;/li&gt;
&lt;li&gt;SQLite-backed&lt;/li&gt;
&lt;li&gt;CLI-driven for write operations&lt;/li&gt;
&lt;li&gt;dashboard-driven for inspection and workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cloud services&lt;/li&gt;
&lt;li&gt;telemetry&lt;/li&gt;
&lt;li&gt;external APIs&lt;/li&gt;
&lt;li&gt;AI features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This boundary is important.&lt;/p&gt;

&lt;p&gt;The goal is not to build another platform, but to provide a reliable tool for working with prompt history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; promptledger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run the dashboard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;v0.6 is a shift from storing prompt history to working with it.&lt;/p&gt;

&lt;p&gt;The dashboard is not meant to replace the CLI, but to complement it by making prompt iteration easier to inspect, compare, and organize.&lt;/p&gt;

&lt;p&gt;There is still a lot to improve, but this version establishes a clearer direction:&lt;/p&gt;

&lt;h2&gt;
  
  
  PromptLedger is becoming a tool for thinking about prompts, not just storing them.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/promptledger/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Ertugrulmutlu/promptledger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
LinkedIn: &lt;a href="https://www.linkedin.com/in/ertugrul-mutlu/?locale=en" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://ertugrulmutlu.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

</description>
      <category>promptengineering</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>OpenAnima v0.2 Preview: Turning the Windows Desktop into a Living Canvas</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Fri, 01 May 2026 10:31:23 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/openanima-v02-preview-turning-the-windows-desktop-into-a-living-canvas-11lh</link>
      <guid>https://dev.to/ertugrulmutlu/openanima-v02-preview-turning-the-windows-desktop-into-a-living-canvas-11lh</guid>
      <description>&lt;h1&gt;
  
  
  OpenAnima v0.2 Preview: Turning the Windows Desktop into a Living Canvas
&lt;/h1&gt;

&lt;p&gt;I recently published &lt;strong&gt;OpenAnima v0.2 Preview&lt;/strong&gt;, and this release is a big step for the project.&lt;/p&gt;

&lt;p&gt;OpenAnima started as a small experiment: what if I could place animated GIFs directly on my Windows desktop as movable overlay objects?&lt;/p&gt;

&lt;p&gt;That simple idea quickly became more interesting.&lt;/p&gt;

&lt;p&gt;Instead of only supporting GIFs, OpenAnima is now moving toward a more general goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An open-source desktop asset overlay engine for Windows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The idea is to make the desktop feel less static. OpenAnima lets you place animated assets, sprites, frame animations, HUD elements, and game-style visual assets directly on your desktop, then control how they behave.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://ertugrulmutlu.github.io/OpenAnima/" rel="noopener noreferrer"&gt;https://ertugrulmutlu.github.io/OpenAnima/&lt;/a&gt;&lt;br&gt;
Itch.io: &lt;a href="https://ertugrulmutlu.itch.io/openanima" rel="noopener noreferrer"&gt;https://ertugrulmutlu.itch.io/openanima&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Ertugrulmutlu/OpenAnima" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/OpenAnima&lt;/a&gt;&lt;br&gt;
Release: &lt;a href="https://github.com/Ertugrulmutlu/OpenAnima/releases/tag/v0.2.0-preview" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/OpenAnima/releases/tag/v0.2.0-preview&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What OpenAnima does
&lt;/h2&gt;

&lt;p&gt;OpenAnima is a Windows desktop application that lets users add visual assets on top of their desktop.&lt;/p&gt;

&lt;p&gt;These assets can be moved around, configured, and used as lightweight desktop overlays.&lt;/p&gt;

&lt;p&gt;Some example use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop companions or animated mascots&lt;/li&gt;
&lt;li&gt;Game-style HUD elements&lt;/li&gt;
&lt;li&gt;Stream or recording overlays&lt;/li&gt;
&lt;li&gt;Sprite and animation preview experiments&lt;/li&gt;
&lt;li&gt;Ambient desktop widgets&lt;/li&gt;
&lt;li&gt;Weird little visual experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is still early, but the direction is becoming clearer: OpenAnima is not just a GIF player. It is becoming a small visual layer above the desktop.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I like projects that feel small at first, but slowly reveal a larger design space.&lt;/p&gt;

&lt;p&gt;At the beginning, OpenAnima was simply about putting GIFs on the desktop. That was already fun, but it also felt limited.&lt;/p&gt;

&lt;p&gt;Once I started thinking about game assets, HUD elements, sprites, and layered UI assets, the project became more like a desktop rendering playground.&lt;/p&gt;

&lt;p&gt;I wanted to explore questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can desktop overlays be treated like reusable visual assets?&lt;/li&gt;
&lt;li&gt;Can a normal desktop become a small interactive canvas?&lt;/li&gt;
&lt;li&gt;Can game-style assets live outside a game engine?&lt;/li&gt;
&lt;li&gt;Can asset packs become something users can import, configure, and reuse?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the v0.2 release comes in.&lt;/p&gt;


&lt;h2&gt;
  
  
  What changed in v0.2
&lt;/h2&gt;

&lt;p&gt;The v0.2 preview expands OpenAnima from a GIF overlay tool into a more metadata-driven desktop asset engine.&lt;/p&gt;

&lt;p&gt;The biggest changes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic asset analyzer and import wizard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;asset.json&lt;/code&gt; metadata support&lt;/li&gt;
&lt;li&gt;Support for multiple asset formats&lt;/li&gt;
&lt;li&gt;Sprite strip setup workflow&lt;/li&gt;
&lt;li&gt;Spritesheet rendering with named animations&lt;/li&gt;
&lt;li&gt;Composite UI/HUD assets&lt;/li&gt;
&lt;li&gt;Runtime sliders for layered UI assets&lt;/li&gt;
&lt;li&gt;Improved Editor tab and Asset Setup dialog layouts&lt;/li&gt;
&lt;li&gt;Safer metadata validation and error handling&lt;/li&gt;
&lt;li&gt;Backward compatibility for existing GIF/static/frame-folder workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This release is still a preview, but the foundation is much stronger than the first version.&lt;/p&gt;


&lt;h2&gt;
  
  
  Supported asset types
&lt;/h2&gt;

&lt;p&gt;OpenAnima v0.2 supports several asset types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIF&lt;/li&gt;
&lt;li&gt;Static image&lt;/li&gt;
&lt;li&gt;Frame-folder animation&lt;/li&gt;
&lt;li&gt;Sprite strip&lt;/li&gt;
&lt;li&gt;Spritesheet&lt;/li&gt;
&lt;li&gt;Composite UI / HUD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first version was mostly focused on simple animated overlays. v0.2 starts building the system needed for more structured assets.&lt;/p&gt;


&lt;h2&gt;
  
  
  Metadata-driven assets
&lt;/h2&gt;

&lt;p&gt;One of the most important changes in v0.2 is &lt;code&gt;asset.json&lt;/code&gt; support.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding every asset type, OpenAnima can now use metadata to understand how an asset should be loaded and rendered.&lt;/p&gt;

&lt;p&gt;A simplified example could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo_hud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"composite_ui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"layers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bar.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the project more flexible. Instead of treating every asset as just an image or GIF, OpenAnima can start understanding assets as structured objects.&lt;/p&gt;

&lt;p&gt;That opens the door for asset packs, richer UI overlays, and more advanced configuration later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sprite strips and spritesheets
&lt;/h2&gt;

&lt;p&gt;Another focus of v0.2 was better support for game-style assets.&lt;/p&gt;

&lt;p&gt;Sprite strips and spritesheets are common in game development, but they usually need some setup before they can be used correctly.&lt;/p&gt;

&lt;p&gt;OpenAnima now includes workflows for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frame count&lt;/li&gt;
&lt;li&gt;Frame size&lt;/li&gt;
&lt;li&gt;Crop fields&lt;/li&gt;
&lt;li&gt;Preview grid&lt;/li&gt;
&lt;li&gt;Frame export&lt;/li&gt;
&lt;li&gt;Named animations for spritesheets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is still not perfect, especially for unusual sprite layouts, but it is a good step toward making OpenAnima useful for more than simple GIF overlays.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composite UI and HUD assets
&lt;/h2&gt;

&lt;p&gt;I also added support for composite UI/HUD assets.&lt;/p&gt;

&lt;p&gt;The idea is simple: an overlay does not have to be one image. It can be made from multiple layers.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A health bar background&lt;/li&gt;
&lt;li&gt;A fill layer&lt;/li&gt;
&lt;li&gt;A frame layer&lt;/li&gt;
&lt;li&gt;A text or icon layer&lt;/li&gt;
&lt;li&gt;Runtime sliders controlling values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it possible to experiment with game-like desktop HUDs.&lt;/p&gt;

&lt;p&gt;The current Composite UI editor is functional, but it is not meant to be a professional layout tool yet. It is more of a foundation for future versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The control panel
&lt;/h2&gt;

&lt;p&gt;OpenAnima has a small control panel with three main areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Library&lt;/li&gt;
&lt;li&gt;Active&lt;/li&gt;
&lt;li&gt;Editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Library tab is used to import and manage assets.&lt;/p&gt;

&lt;p&gt;The Active tab is used to manage overlays currently placed on the desktop.&lt;/p&gt;

&lt;p&gt;The Editor tab is used to adjust selected assets with controls like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scale&lt;/li&gt;
&lt;li&gt;Opacity&lt;/li&gt;
&lt;li&gt;Speed&lt;/li&gt;
&lt;li&gt;Always on top&lt;/li&gt;
&lt;li&gt;Click-through&lt;/li&gt;
&lt;li&gt;Locked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to keep the interface practical and lightweight. I do not want OpenAnima to become a huge desktop suite. I want it to stay small, hackable, and focused.&lt;/p&gt;




&lt;h2&gt;
  
  
  Website and distribution
&lt;/h2&gt;

&lt;p&gt;For this release, I also created a small website for the project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ertugrulmutlu.github.io/OpenAnima/" rel="noopener noreferrer"&gt;https://ertugrulmutlu.github.io/OpenAnima/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The website explains the idea, shows the demo, links to the GitHub repository, and provides access to the Windows executable through GitHub Releases.&lt;/p&gt;

&lt;p&gt;I also published the project on itch.io as a free tool:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ertugrulmutlu.itch.io/openanima" rel="noopener noreferrer"&gt;https://ertugrulmutlu.itch.io/openanima&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was mainly to make the project feel more like a small product rather than only a repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;p&gt;This is still a preview release, so some limitations are expected.&lt;/p&gt;

&lt;p&gt;Known issues include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sprite strips may require manual frame count or frame size correction.&lt;/li&gt;
&lt;li&gt;Some sprite strips with unusual padding may need manual crop values.&lt;/li&gt;
&lt;li&gt;Spritesheets require metadata or setup through the import wizard.&lt;/li&gt;
&lt;li&gt;Composite UI assets may require manual layer alignment.&lt;/li&gt;
&lt;li&gt;The Composite UI editor is functional but not a full professional layout tool.&lt;/li&gt;
&lt;li&gt;3D model support is not included yet.&lt;/li&gt;
&lt;li&gt;Some unusual asset packs may still need manual &lt;code&gt;asset.json&lt;/code&gt; editing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I prefer being clear about this because v0.2 is not a polished final product. It is a foundation release.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I want to explore next
&lt;/h2&gt;

&lt;p&gt;There are several directions I want to explore after v0.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asset packs&lt;/li&gt;
&lt;li&gt;Better first-run experience&lt;/li&gt;
&lt;li&gt;More polished installer/distribution flow&lt;/li&gt;
&lt;li&gt;Better preview and import tools&lt;/li&gt;
&lt;li&gt;More reliable spritesheet workflows&lt;/li&gt;
&lt;li&gt;Simple 3D overlay experiments&lt;/li&gt;
&lt;li&gt;Linux experiments later&lt;/li&gt;
&lt;li&gt;Better documentation for custom assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 3D direction is especially interesting, but I do not want to rush it before the 2D asset foundation is stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;This project reminded me that even small desktop tools can become surprisingly deep.&lt;/p&gt;

&lt;p&gt;At first, the problem looked simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put an animated object on the desktop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But once I started supporting different asset types, the project became about importing, validating, describing, previewing, rendering, and controlling assets.&lt;/p&gt;

&lt;p&gt;That means the real challenge is not only drawing something on the desktop. The real challenge is creating a flexible system around desktop assets.&lt;/p&gt;

&lt;p&gt;OpenAnima v0.2 is my first serious step in that direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://ertugrulmutlu.github.io/OpenAnima/" rel="noopener noreferrer"&gt;https://ertugrulmutlu.github.io/OpenAnima/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Ertugrulmutlu/OpenAnima" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/OpenAnima&lt;/a&gt;&lt;br&gt;
Release: &lt;a href="https://github.com/Ertugrulmutlu/OpenAnima/releases/tag/v0.2.0-preview" rel="noopener noreferrer"&gt;https://github.com/Ertugrulmutlu/OpenAnima/releases/tag/v0.2.0-preview&lt;/a&gt;&lt;br&gt;
itch.io: &lt;a href="https://ertugrulmutlu.itch.io/openanima" rel="noopener noreferrer"&gt;https://ertugrulmutlu.itch.io/openanima&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, bug reports, weird desktop overlay ideas, and asset workflow suggestions are very welcome.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>ui</category>
    </item>
    <item>
      <title>PromptLedger v0.4 — Faster prompt logging, lightweight markers, and better prompt organization</title>
      <dc:creator>Ertugrul</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:19:18 +0000</pubDate>
      <link>https://dev.to/ertugrulmutlu/promptledger-v04-faster-prompt-logging-lightweight-markers-and-better-prompt-organization-2b2g</link>
      <guid>https://dev.to/ertugrulmutlu/promptledger-v04-faster-prompt-logging-lightweight-markers-and-better-prompt-organization-2b2g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Devlog — Part 4&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PromptLedger v0.4 is mostly about making prompt versioning easier to use repeatedly: faster logging, clearer organization, and small release signals for versions worth remembering.&lt;/p&gt;

&lt;p&gt;In the earlier parts of this series, PromptLedger started as a deliberately small local-first prompt version control tool. Then it gained labels, status semantics, better diffs, review workflows, semantic summaries, warnings, and Markdown export.&lt;/p&gt;

&lt;p&gt;Those additions made the history more useful after prompts had already been logged.&lt;/p&gt;

&lt;p&gt;Some of the direction for v0.4 also came from feedback and from watching where the workflow still felt a bit too manual. So before going into the details: thank you to everyone who tried the earlier versions, shared thoughts, pointed out rough edges, or simply asked practical questions about how the tool should behave during real prompt iteration.&lt;/p&gt;

&lt;p&gt;v0.4 focuses more on the moment before review: the actual day-to-day act of adding, organizing, and revisiting prompt versions.&lt;/p&gt;

&lt;p&gt;Because in practice, a prompt version control tool only works if logging a prompt is cheap enough that you actually keep doing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this part was needed
&lt;/h2&gt;

&lt;p&gt;PromptLedger has always been intentionally limited in scope.&lt;/p&gt;

&lt;p&gt;It is SQLite-backed. It is terminal-first. It does not need a hosted backend. It does not try to execute prompts for you. It does not try to become an evaluation platform.&lt;/p&gt;

&lt;p&gt;The core job is still simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store prompt versions&lt;/li&gt;
&lt;li&gt;compare them&lt;/li&gt;
&lt;li&gt;review changes&lt;/li&gt;
&lt;li&gt;organize prompt history&lt;/li&gt;
&lt;li&gt;make the history inspectable later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But once I started using it more like a real prompt library, a small problem became obvious.&lt;/p&gt;

&lt;p&gt;Adding a prompt version was technically easy, but repetitive.&lt;/p&gt;

&lt;p&gt;During actual prompt iteration, the prompt text changes often, while the surrounding metadata usually stays the same. The same author. The same environment. The same tags. The same library grouping. The same role.&lt;/p&gt;

&lt;p&gt;Having to retype that metadata every time creates friction.&lt;/p&gt;

&lt;p&gt;And friction matters. If logging is annoying, the history becomes incomplete. If the history is incomplete, review becomes less useful. And if review becomes less useful, the tool stops doing its main job.&lt;/p&gt;

&lt;p&gt;So v0.4 is not a big architectural release.&lt;/p&gt;

&lt;p&gt;It is a workflow release.&lt;/p&gt;

&lt;p&gt;It makes repeated prompt logging faster, adds better organization primitives, and introduces lightweight markers for important versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick add: less typing during real iteration
&lt;/h2&gt;

&lt;p&gt;The main usability change in v0.4 is the new &lt;code&gt;add --quick&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;A normal add can still be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger add &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--collection&lt;/span&gt; support &lt;span class="nt"&gt;--role&lt;/span&gt; system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful when creating a new prompt or when metadata should be stated clearly.&lt;/p&gt;

&lt;p&gt;But once a prompt already exists, most iterations do not need all metadata to be typed again. With &lt;code&gt;--quick&lt;/code&gt;, PromptLedger can reuse safe metadata defaults from the latest version of the same prompt id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger add &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--quick&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the latest &lt;code&gt;onboarding&lt;/code&gt; version already had metadata like author, tags, env, collection, and role, those values can be reused unless explicitly overridden.&lt;/p&gt;

&lt;p&gt;That means the common workflow becomes much smaller:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;edit the prompt&lt;/li&gt;
&lt;li&gt;add the new version&lt;/li&gt;
&lt;li&gt;keep moving&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a flashy feature, but it changes the feel of the tool.&lt;/p&gt;

&lt;p&gt;Prompt logging becomes closer to a habit than a chore.&lt;/p&gt;

&lt;p&gt;That matters because PromptLedger is not useful because one perfect prompt was saved once. It is useful because a sequence of changes becomes reviewable later.&lt;/p&gt;

&lt;p&gt;Quick add is there to protect that sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Collection and role are now first-class metadata
&lt;/h2&gt;

&lt;p&gt;v0.4 also adds two first-class metadata fields on prompt versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;collection&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;role&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason is simple: once a prompt library grows, ids and tags are not always enough.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;collection&lt;/code&gt; gives prompts a lightweight grouping. It can represent a product area, a project, a use case, a customer support flow, an internal tool, or just a personal folder-like grouping.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger add &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--collection&lt;/span&gt; support &lt;span class="nt"&gt;--role&lt;/span&gt; system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easier to ask questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which prompts belong to the support collection?&lt;/li&gt;
&lt;li&gt;Which prompts are part of an onboarding workflow?&lt;/li&gt;
&lt;li&gt;Which versions were written for a specific environment or use case?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second field, &lt;code&gt;role&lt;/code&gt;, is about what kind of prompt artifact this version represents.&lt;/p&gt;

&lt;p&gt;Built-in roles are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;system&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modelfile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eval&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because prompt libraries usually contain different kinds of artifacts.&lt;/p&gt;

&lt;p&gt;A system instruction is not the same thing as a reusable template. An evaluation prompt is not the same thing as a user-facing message. A model file prompt is not the same thing as an onboarding assistant instruction.&lt;/p&gt;

&lt;p&gt;Before v0.4, these differences could be represented with tags, but tags are free-form and tend to become messy over time.&lt;/p&gt;

&lt;p&gt;Making &lt;code&gt;role&lt;/code&gt; first-class gives PromptLedger a small amount of structure without turning it into a large framework.&lt;/p&gt;

&lt;p&gt;That is the balance I wanted here: enough organization to be useful, but not so much that the tool starts dictating how every prompt library must be designed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Markers: small signals for important versions
&lt;/h2&gt;

&lt;p&gt;v0.4 introduces a new marker system for prompt versions.&lt;/p&gt;

&lt;p&gt;The core commands are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger marker &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--version&lt;/span&gt; 8 &lt;span class="nt"&gt;--name&lt;/span&gt; stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger marker remove &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--version&lt;/span&gt; 8 &lt;span class="nt"&gt;--name&lt;/span&gt; stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger marker list &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger marker show &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding &lt;span class="nt"&gt;--version&lt;/span&gt; 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also convenience commands for the built-in markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger stable &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger milestone &lt;span class="nt"&gt;--id&lt;/span&gt; onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built-in markers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;milestone&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markers are intentionally lighter than labels.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;p&gt;Labels in PromptLedger are useful when you want release-like semantics or named pointers. They can represent states such as a current production prompt, a reviewed version, or a version used in a specific workflow.&lt;/p&gt;

&lt;p&gt;Markers are smaller than that.&lt;/p&gt;

&lt;p&gt;A marker says: this version is worth noticing.&lt;/p&gt;

&lt;p&gt;Maybe it was the first version that worked well. Maybe it was a milestone during a rewrite. Maybe it was stable enough to revisit later. Maybe it is just a checkpoint that should not get lost in the version list.&lt;/p&gt;

&lt;p&gt;Not every important prompt version needs full label semantics.&lt;/p&gt;

&lt;p&gt;Sometimes you only need a small flag attached directly to a version.&lt;/p&gt;

&lt;p&gt;That is what markers are for.&lt;/p&gt;

&lt;p&gt;They are deliberately simple. They do not try to encode deployment state, environment ownership, evaluation results, or production routing. They are just lightweight release signals inside the local history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Search, list, and show became more useful
&lt;/h2&gt;

&lt;p&gt;Organization only helps if the tools expose it.&lt;/p&gt;

&lt;p&gt;So v0.4 updates &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, and &lt;code&gt;search&lt;/code&gt; to surface and use collection, role, and marker information.&lt;/p&gt;

&lt;p&gt;For example, searching by metadata becomes more natural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptledger search &lt;span class="nt"&gt;--collection&lt;/span&gt; support &lt;span class="nt"&gt;--role&lt;/span&gt; system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search can also work with an empty &lt;code&gt;--contains&lt;/code&gt;, which means metadata-only filtering is now possible.&lt;/p&gt;

&lt;p&gt;That sounds small, but it changes how PromptLedger can be used.&lt;/p&gt;

&lt;p&gt;Before, search was mostly about finding prompt text. Now it can also be used to navigate the prompt library itself.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;show all system prompts in a collection&lt;/li&gt;
&lt;li&gt;find evaluation prompts across a project&lt;/li&gt;
&lt;li&gt;inspect versions marked as stable&lt;/li&gt;
&lt;li&gt;separate templates from user prompts&lt;/li&gt;
&lt;li&gt;review one slice of the library without relying on text search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This moves PromptLedger a little further from “prompt version storage” toward “prompt library workflow”.&lt;/p&gt;

&lt;p&gt;Still local. Still small. Still inspectable.&lt;/p&gt;

&lt;p&gt;But more practical once the number of prompt artifacts grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Streamlit UI is still read-only
&lt;/h2&gt;

&lt;p&gt;PromptLedger also has a small Streamlit UI for inspecting prompt history.&lt;/p&gt;

&lt;p&gt;In v0.4, the UI now surfaces collection, role, and markers in timeline, detail, and comparison views.&lt;/p&gt;

&lt;p&gt;That makes the UI more useful when browsing a prompt library. You can see not only how a prompt changed, but also what kind of prompt it is, which collection it belongs to, and whether a version was marked as stable or as a milestone.&lt;/p&gt;

&lt;p&gt;The important part: the UI is still read-only.&lt;/p&gt;

&lt;p&gt;That is intentional.&lt;/p&gt;

&lt;p&gt;For now, PromptLedger keeps editing and logging in the CLI, while the UI remains focused on inspection. This keeps the implementation smaller and avoids turning the viewer into a second source of write behavior.&lt;/p&gt;

&lt;p&gt;The terminal is where versions are created.&lt;/p&gt;

&lt;p&gt;The UI is where history can be reviewed more comfortably.&lt;/p&gt;

&lt;p&gt;That split still feels right for the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database changes stayed local and simple
&lt;/h2&gt;

&lt;p&gt;v0.4 moves the schema version from 3 to 5.&lt;/p&gt;

&lt;p&gt;The main database changes are straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new &lt;code&gt;markers&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;new &lt;code&gt;collection&lt;/code&gt; and &lt;code&gt;role&lt;/code&gt; columns on &lt;code&gt;prompt_versions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;migrations for the local SQLite database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no remote migration service. No hosted registry. No account state. No backend coordination.&lt;/p&gt;

&lt;p&gt;The migration story stayed aligned with the rest of the project: local, inspectable, and boring in a good way.&lt;/p&gt;

&lt;p&gt;That matters because PromptLedger should remain easy to understand. A small SQLite-backed tool should not require infrastructure thinking just to store prompt history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tests expanded around the new workflows
&lt;/h2&gt;

&lt;p&gt;v0.4 also expands the test coverage substantially around the areas that changed most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add workflows&lt;/li&gt;
&lt;li&gt;quick add behavior&lt;/li&gt;
&lt;li&gt;marker commands&lt;/li&gt;
&lt;li&gt;search and metadata filtering&lt;/li&gt;
&lt;li&gt;related list/show behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not the kind of testing expansion that makes for a dramatic release note, but it is important for this project.&lt;/p&gt;

&lt;p&gt;PromptLedger deals with history. If history is stored incorrectly, the tool loses trust quickly.&lt;/p&gt;

&lt;p&gt;So the goal is practical correctness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metadata should be reused only when expected&lt;/li&gt;
&lt;li&gt;explicit overrides should still win&lt;/li&gt;
&lt;li&gt;markers should attach to the right versions&lt;/li&gt;
&lt;li&gt;search should return the intended slice of the prompt library&lt;/li&gt;
&lt;li&gt;existing workflows should not become harder to use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a local-first developer tool, confidence matters more than feature count.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design tradeoffs
&lt;/h2&gt;

&lt;p&gt;The main tradeoff in v0.4 was deciding how much structure to add.&lt;/p&gt;

&lt;p&gt;PromptLedger could have gone further.&lt;/p&gt;

&lt;p&gt;It could have introduced nested collections, custom role registries, marker categories, richer release channels, or project configuration files.&lt;/p&gt;

&lt;p&gt;I avoided that for now.&lt;/p&gt;

&lt;p&gt;The project works best when the concepts stay small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ids identify prompt histories&lt;/li&gt;
&lt;li&gt;versions preserve changes over time&lt;/li&gt;
&lt;li&gt;labels provide stronger named semantics&lt;/li&gt;
&lt;li&gt;markers provide lightweight version signals&lt;/li&gt;
&lt;li&gt;collection groups prompt versions&lt;/li&gt;
&lt;li&gt;role explains what kind of prompt artifact a version is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough structure to make a growing prompt library easier to navigate, without making the tool feel like a platform.&lt;/p&gt;

&lt;p&gt;This is also why markers are not labels with another name.&lt;/p&gt;

&lt;p&gt;Labels are useful when you want something closer to a maintained pointer or status. Markers are useful when you want to annotate a version as notable.&lt;/p&gt;

&lt;p&gt;Both can exist because they answer different workflow questions.&lt;/p&gt;

&lt;p&gt;A label asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does this version represent in a larger workflow?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A marker asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this version worth noticing later?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference is small, but it keeps the model clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  What did not change
&lt;/h2&gt;

&lt;p&gt;v0.4 does not change the basic philosophy of PromptLedger.&lt;/p&gt;

&lt;p&gt;It is still a small, local-first prompt version control tool.&lt;/p&gt;

&lt;p&gt;It still does not add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hosted registry&lt;/li&gt;
&lt;li&gt;prompt execution APIs&lt;/li&gt;
&lt;li&gt;agent tooling&lt;/li&gt;
&lt;li&gt;telemetry pipelines&lt;/li&gt;
&lt;li&gt;cloud sync&lt;/li&gt;
&lt;li&gt;automatic scoring&lt;/li&gt;
&lt;li&gt;evaluation harnesses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not bad ideas in general. They are just outside the current scope of this project.&lt;/p&gt;

&lt;p&gt;PromptLedger is not trying to run prompts, score prompts, deploy prompts, or orchestrate agents.&lt;/p&gt;

&lt;p&gt;It is trying to make prompt changes easier to store, compare, review, and organize.&lt;/p&gt;

&lt;p&gt;That boundary is useful.&lt;/p&gt;

&lt;p&gt;Small scope is not a missing feature here. It is part of the design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;PromptLedger v0.4 is a usability and organization release.&lt;/p&gt;

&lt;p&gt;It does not radically change what the tool is. Instead, it makes the existing workflow smoother.&lt;/p&gt;

&lt;p&gt;Quick add reduces friction during iteration. Collection and role make prompt libraries easier to navigate. Markers create a lightweight way to remember important versions. Search, list, show, and the read-only UI now expose more of that structure.&lt;/p&gt;

&lt;p&gt;The result is still intentionally modest.&lt;/p&gt;

&lt;p&gt;A local SQLite database. A CLI. A read-only inspection UI. Deterministic exports and reviewable history where possible.&lt;/p&gt;

&lt;p&gt;But the day-to-day workflow feels better now.&lt;/p&gt;

&lt;p&gt;And for a tool like this, that matters.&lt;/p&gt;

&lt;p&gt;Because prompt version control only becomes useful when it is easy enough to use consistently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/promptledger/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Ertugrulmutlu/promptledger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
LinkedIn: &lt;a href="https://www.linkedin.com/in/ertugrul-mutlu/?locale=en" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://ertugrulmutlu.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

</description>
      <category>promptengineering</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
