<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://phisanti.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://phisanti.github.io/" rel="alternate" type="text/html" /><updated>2026-03-02T07:54:58+00:00</updated><id>https://phisanti.github.io/feed.xml</id><title type="html">Santiago Cano-Muniz</title><subtitle>personal description</subtitle><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><entry><title type="html">Making plotly labels behave: the story behind plotly.repel</title><link href="https://phisanti.github.io/posts/2026/03/plotly-repel/" rel="alternate" type="text/html" title="Making plotly labels behave: the story behind plotly.repel" /><published>2026-03-02T00:00:00+00:00</published><updated>2026-03-02T00:00:00+00:00</updated><id>https://phisanti.github.io/posts/2026/03/plotly-repel</id><content type="html" xml:base="https://phisanti.github.io/posts/2026/03/plotly-repel/"><![CDATA[<h1 id="making-plotly-labels-behave-the-story-behind-plotlyrepel">Making plotly labels behave: the story behind plotly.repel</h1>

<p>Today I am publishing <strong>plotly.repel</strong>, an R package that brings ggrepel-style label repulsion to interactive plotly charts.</p>

<p>If you have ever tried to annotate an interactive volcano plot, you know the moment you label more than a few points, everything overlaps and the plot turns into a block of text. With static ggplot2 charts, we can reach for <code class="language-plaintext highlighter-rouge">ggrepel</code>. Plotly users have been asking for an equivalent for a long time (see the plotly.js issue thread here): https://github.com/plotly/plotly.js/issues/4674</p>

<p>I recently joined BigOmics, and we rely heavily on plotly for interactive charts. Interactivity is a big part of how we give users context (hover tooltips, zooming into regions, selecting subsets of genes). But once the information density goes up, labels stop scaling. Manual annotation becomes a chore, and a repel layer becomes a must. The most egregious example we encountered at BigOmics was a UMAP of gene features: dozens of cluster labels stacking on top of each other the second you zoom in (Figure 1).</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_012/umap_standard.png" alt="UMAP plot with standard plotly text labels overlapping densely, making individual gene names unreadable." width="100%" />
  <p><em>Figure 1. Standard plotly labels on a dense UMAP of gene features: overlapping text makes the plot unreadable at scale.</em></p>
</div>

<p>At the same time, I wanted a contained project to experiment with agentic coding workflows: take a clear spec, turn it into a tested package, and iterate quickly.</p>

<h2 id="the-first-instinct-copy-ggrepel">The First Instinct: Copy ggrepel</h2>

<p>My first instinct was simple: take <code class="language-plaintext highlighter-rouge">ggrepel</code> and copy the idea into the plotly world. I cloned the repo, looked through the internals, and quickly realized that most of the hard logic lives in <code class="language-plaintext highlighter-rouge">C++</code>.</p>

<p>That was an excellent chance to test whether an agent could help me build an accurate mental model quickly. Since I am fluent in R, I asked for a high-level explanation in pseudo-R (made-up functions, but familiar syntax). I cannot claim it was a perfect reproduction, but it worked for its purpose.</p>

<p>The mental model I came away with: ggrepel is a <strong>force-directed solver</strong>. Each label is a body under two competing forces: repulsion from neighbouring labels and data points (proportional to 1/d², where d is the distance to a neighbour), and a restoring spring pulling it back toward its anchor point. Think of it as a small physics simulation where every label nudges every other, and the system relaxes toward equilibrium. ggrepel runs up to 10,000 of these iterations, with early passes handling large displacements and later passes fine-tuning positions.</p>

<p>With that base, I treated the expected behavior as a reference, added constraints and specs, and started iterating toward a plotly version. It worked. Then I hit the thing that makes plotly categorically different.</p>

<h2 id="the-reality-check-interactive-charts-change-under-you">The Reality Check: Interactive Charts Change Under You</h2>

<p>ggrepel solves an <strong>offline layout problem</strong>: the coordinate space is fixed, the plot will not change after render, and the solver can spend as long as it needs to find a good configuration.</p>

<p>Plotly turns that into an <strong>online, latency-constrained layout problem</strong>: the coordinate space is a function of viewport state. It shifts on every zoom, pan, and resize. The solver cannot run once; it is called continuously, and it has roughly 25 ms per call to feel instant to the user.</p>

<p>In plotly, the user changes the plot:</p>

<ul>
  <li>zoom in: the coordinate system changes</li>
  <li>pan: the viewport changes</li>
  <li>resize: the pixel geometry changes</li>
  <li>animate: the whole scene changes per frame</li>
</ul>

<p>This shifted what “good enough” meant. A solver that is perfect once and wrong after zoom is worthless. A solver that is “pretty good” but stable across reruns and fast enough to be transparent to the user is the right goal.</p>

<p>The naive port made this painfully concrete: at 50 labels on a volcano plot, the solver ran in 162 ms per call. On every zoom event. Effectively unusable.</p>

<p>The fix came from a direction I would not have reached on my own. Working with a search-and-retrieval agent that pulled in literature on spatial indexing, I landed on <strong>approximate nearest-neighbor collision detection</strong>, a technique from the world of vector databases and ML search that transfers cleanly to label geometry. Rather than checking every label against every other label on each iteration, the plot space is partitioned into a uniform grid and each label is indexed into a cell. Collision checks then touch only the 9 cells adjacent to a label, turning an operation that scales with the square of the label count into one that is effectively constant.</p>

<p>The result: 1.80 ms per solve at 50 labels. The solver now re-runs on every zoom, pan, and resize without the user noticing.</p>

<p>This is also where “agentic programming” paid off in a way I had not expected. It was not just about writing code faster. It was about having a research layer that could pull ideas from outside my usual reading and apply them to a concrete engineering problem.</p>

<h2 id="what-you-get">What You Get</h2>

<p>plotly.repel gives you:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">add_text_repel()</code> for repelled text labels</li>
  <li><code class="language-plaintext highlighter-rouge">add_label_repel()</code> for repelled labels with boxes</li>
  <li>optional rules like “label only these points” and “when space is tight, keep the most important labels”</li>
  <li>the ability to choose when relabeling happens (for example: on initial render and zoom)</li>
</ul>

<p>I am deliberately not going into the implementation details here. The goal is to show what you get and how to use it, not to require you to care about the internals.</p>

<p>Figure 2 shows the same UMAP after plotly.repel — labels pushed apart, connected to their points with leader lines, and stable across zoom and pan.</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_012/umap_repel.png" alt="The same UMAP plot with plotly.repel applied, showing non-overlapping labels with leader lines connecting each label to its data point." width="100%" />
  <p><em>Figure 2. The same plot with plotly.repel: labels are pushed apart and connected to their points with leader lines, remaining legible after zoom and pan.</em></p>
</div>

<h2 id="install">Install</h2>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">remotes</span><span class="o">::</span><span class="n">install_github</span><span class="p">(</span><span class="s2">"bigomics/plotly.repel"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h2 id="a-small-example">A Small Example</h2>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">library</span><span class="p">(</span><span class="n">plotly</span><span class="p">)</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">plotly.repel</span><span class="p">)</span><span class="w">

</span><span class="n">set.seed</span><span class="p">(</span><span class="m">1</span><span class="p">)</span><span class="w">
</span><span class="n">df</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">data.frame</span><span class="p">(</span><span class="w">
  </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rnorm</span><span class="p">(</span><span class="m">40</span><span class="p">),</span><span class="w">
  </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rnorm</span><span class="p">(</span><span class="m">40</span><span class="p">),</span><span class="w">
  </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">paste0</span><span class="p">(</span><span class="s2">"P"</span><span class="p">,</span><span class="w"> </span><span class="nf">seq_len</span><span class="p">(</span><span class="m">40</span><span class="p">))</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="n">plot_ly</span><span class="p">(</span><span class="n">df</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">add_markers</span><span class="p">()</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">add_text_repel</span><span class="p">(</span><span class="w">
    </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">id</span><span class="p">,</span><span class="w">
    </span><span class="c1"># Tuning knobs (start here when you need to tweak behavior):</span><span class="w">
    </span><span class="n">force</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1.6</span><span class="p">,</span><span class="w">          </span><span class="c1"># repulsion strength (higher = push labels apart more)</span><span class="w">
    </span><span class="n">force_pull</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.8</span><span class="p">,</span><span class="w">     </span><span class="c1"># pull back toward the point (higher = stay closer)</span><span class="w">
    </span><span class="n">box_padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.35</span><span class="p">,</span><span class="w">   </span><span class="c1"># extra space around labels</span><span class="w">
    </span><span class="n">point_padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.15</span><span class="p">,</span><span class="w"> </span><span class="c1"># extra space around points</span><span class="w">
    </span><span class="n">direction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"both"</span><span class="p">,</span><span class="w">   </span><span class="c1"># "both", "x", or "y"</span><span class="w">
    </span><span class="n">max_time_ms</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">25</span><span class="p">,</span><span class="w">     </span><span class="c1"># keep interaction responsive</span><span class="w">
    </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">42</span><span class="p">,</span><span class="w">            </span><span class="c1"># reproducible initial positions</span><span class="w">
    </span><span class="n">on</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"render"</span><span class="p">,</span><span class="w"> </span><span class="s2">"zoom"</span><span class="p">,</span><span class="w"> </span><span class="s2">"resize"</span><span class="p">)</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Even in this toy plot, the “feel” matters: labels shouldn’t fight you when you zoom in. If the layout becomes unreadable every time you interact, you stop interacting.</p>

<h2 id="a-pattern-i-use-constantly-label-the-outliers-not-everything">A Pattern I Use Constantly: “Label the Outliers, Not Everything”</h2>

<p>The right UX for most scientific plots is:</p>

<ul>
  <li>show all points</li>
  <li>label only the ones worth discussing</li>
</ul>

<p>Here is a common pattern where <code class="language-plaintext highlighter-rouge">visibility</code> selects which points are eligible for labeling, and <code class="language-plaintext highlighter-rouge">priority</code> decides which labels win when space is limited:</p>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">set.seed</span><span class="p">(</span><span class="m">2</span><span class="p">)</span><span class="w">
</span><span class="n">n</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">1500</span><span class="w">
</span><span class="n">df</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">data.frame</span><span class="p">(</span><span class="w">
  </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rnorm</span><span class="p">(</span><span class="n">n</span><span class="p">),</span><span class="w">
  </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rnorm</span><span class="p">(</span><span class="n">n</span><span class="p">),</span><span class="w">
  </span><span class="n">label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">paste0</span><span class="p">(</span><span class="s2">"G"</span><span class="p">,</span><span class="w"> </span><span class="nf">seq_len</span><span class="p">(</span><span class="n">n</span><span class="p">))</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="n">df</span><span class="o">$</span><span class="n">outlier</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">with</span><span class="p">(</span><span class="n">df</span><span class="p">,</span><span class="w"> </span><span class="nf">abs</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="m">2.5</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">abs</span><span class="p">(</span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="m">2.5</span><span class="p">)</span><span class="w">

</span><span class="n">plot_ly</span><span class="p">(</span><span class="n">df</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">add_markers</span><span class="p">(</span><span class="n">marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">6</span><span class="p">,</span><span class="w"> </span><span class="n">opacity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.5</span><span class="p">))</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">add_text_repel</span><span class="p">(</span><span class="w">
    </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">label</span><span class="p">,</span><span class="w">
    </span><span class="n">visibility</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">outlier</span><span class="p">,</span><span class="w">
    </span><span class="n">priority</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">abs</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nf">abs</span><span class="p">(</span><span class="n">y</span><span class="p">)),</span><span class="w">
    </span><span class="n">max_overlaps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">10</span><span class="p">,</span><span class="w">
    </span><span class="n">force</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1.4</span><span class="p">,</span><span class="w">
    </span><span class="n">force_pull</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1.0</span><span class="p">,</span><span class="w">
    </span><span class="n">box_padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.35</span><span class="p">,</span><span class="w">
    </span><span class="n">point_padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.2</span><span class="p">,</span><span class="w">
    </span><span class="n">max_time_ms</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">25</span><span class="p">,</span><span class="w">
    </span><span class="n">seed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h2 id="what-i-learned-building-this">What I Learned Building This</h2>

<p>Two things I would do the same way again:</p>

<ol>
  <li>Test driven design: This approach is really powerful with agents. If you have an “source of truth” that the agents can use to check their outputs, you can easily set a feedback loop.</li>
  <li>Using AI for coding is a really powerful tool, but you still need to do what you are doing. Otherwise, the agent will run in circles.</li>
</ol>

<h2 id="its-beta-please-stress-test-it">It’s Beta. Please Stress Test It.</h2>

<p>If you try plotly.repel, the best feedback is:</p>

<ul>
  <li>a plot that breaks it</li>
  <li>a plot where it is almost good but not quite</li>
  <li>a screenshot plus a minimal reproducible example</li>
</ul>

<p>Repository: <a href="https://github.com/bigomics/plotly.repel">bigomics/plotly.repel</a></p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><category term="microbiology" /><category term="membrane-potential" /><category term="antibiotics" /><category term="porins" /><summary type="html"><![CDATA[Today I am publishing **plotly.repel**, an R package that brings ggrepel-style label repulsion to interactive plotly charts...]]></summary></entry><entry><title type="html">The Membrane Potential Paradox: How Bacteria Balance Permeability and Energy</title><link href="https://phisanti.github.io/posts/2025/11/membrane-potential-paradox/" rel="alternate" type="text/html" title="The Membrane Potential Paradox: How Bacteria Balance Permeability and Energy" /><published>2025-11-25T00:00:00+00:00</published><updated>2025-11-25T00:00:00+00:00</updated><id>https://phisanti.github.io/posts/2025/11/membrane-potential-paradox</id><content type="html" xml:base="https://phisanti.github.io/posts/2025/11/membrane-potential-paradox/"><![CDATA[<h1 id="the-membrane-potential-paradox-how-bacteria-balance-permeability-and-energy">The Membrane Potential Paradox: How Bacteria Balance Permeability and Energy</h1>

<p>It took longer that I thought, but I’m thrilled to share that the core of this work—my main thesis—is now out in Nature Microbiology (<a href="https://www.nature.com/articles/s41564-025-02175-5">link to full paper here</a>). It’s surreal to move from debugging leaky microfluidics chips at 6 a.m. to seeing the story published. Now, let me tell you what it is about.</p>

<h2 id="the-leak-in-the-wall">The leak in the wall</h2>
<p>In the world of Gram-negative bacteria, the outer membrane serves as a protective wall, guarding the cell from external threats. Yet like all great walls, it has gates, these are protein channels called porins, that mediate the influx of essential nutrients for the cell and, incidentally, also antibiotics. Since the early studies of porins by Nikaido in the 1980s, these gates were largely considered to be wide open, allowing free passage for small molecules. This assumption, however, presented a fundamental paradox. Every biologist knows that bacteria rely on the proton motor force, that is, the accumulation of a proton gradient in the periplasmic space, as a source for ATP synthesis, motility, and transport. Thus, if porins as always open, then protons can simply leak out through these open gates? This question of energy conservation versus nutrient access led us to investigate what we call the membrane potential paradox.</p>

<h2 id="following-the-sugar">Following the sugar</h2>
<p>Our first step was to watch things flow. We used a fluorescently-tagged glucose molecule (2-NBDG), as a tracer to see how much was getting into the cells. As you’d expect, when we deleted the genes for the main porin proteins (ompF, ompC, ompG, nmpC, phoE), uptake dropped. No gates, no traffic. But then we got our first surprise. While screening a library of knockout mutants, we found one that had nothing to do with porins but still blocked uptake: a mutant for a potassium channel called Kch. [It was a strange and exciting clue.]
This pushed us to think about ions. Maybe the porins weren’t just passive pores but were instead sensitive to the ionic environment. We tested changing the pH and potassium levels outside the bacteria, but nothing happened. However, we reasoned, maybe it’s the inside that matters. We used chemicals called ionophores such as CCCP, which makes the membrane leaky to protons, and valinomycin, which does the same for potassium to mess with the periplasmic ion concentrations directly. And it worked! We saw a huge change in the uptake of our fluorescent tracer. This was a big moment, suggesting that porins were indeed regulated by internal ions.
But as a scientist, you’re trained to be skeptical of your own results. Ionophores are messy; they can have all sorts of side effects. We struggled with this idea. Using ionophores could have second-order effects. The solution, funnily enough, came from a completely different field: neuroscience. Neuroscientists care deeply about membrane voltage because neural is key for understanding neuron physiology. Thus, they developed this amazing toolbox of light-activated proteins that can be used to control the flow of ions across the membrane using light. We found one called ArchT, a proton pump originally discovered in algae, that you can switch on with a flash of green light. The thought was simple: what if we put this pump into our bacteria and used light to pump protons directly into the periplasm? We set up our bacteria in a microfluidic chip under the microscope. In the dark, the cells were happily swallowing up the fluorescent glucose. Then, we flipped the switch for the 561 nm laser. And just like that, the uptake stopped (Figure 1).</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_011/membrane-potential-fig1-permeability.png" alt="Porin-mediated 2NBDG uptake drops with porin and kch knockouts, increases with internal pH or K+ changes, and is shut down by optogenetic ArchT pumping under green light." width="100%" />
  <p><em>Figure 1. Internal H+ and K+ reshape porin permeability and light-driven ArchT pumping rapidly blocks 2NBDG uptake.</em></p>
</div>

<h2 id="where-the-pore-flexes">Where the pore flexes</h2>
<p>Now we had proof, but we needed a mechanism. Previous literature had focused on the inside L3 loop of porins because it formed a constriction zone. But our molecular dynamics simulations, run by a collaborator, pointed elsewhere; they suggested that protonating charged residues on the periplasmic side for neutral residues (E23A, K27A, D28A, E64A, D69A, D156A, D162A, D289A, K329A) of the porin would cause it to constrict. This was a new idea. To test it, we spent months creating an OmpC mutant where we replaced all those charged residues with neutral alanines. The phenotype of this mutant was the perfect confirmation: it was leakier than wild-type and, crucially, had lost its ability to be regulated by pH (Figure 2).</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_011/membrane-potential-fig1-structure.png" alt="OmpC periplasmic charged residues with pKa values and molecular dynamics cross-sections showing a wider unprotonated pore and a constricted protonated pore." width="100%" />
  <p><em>Figure 2. Periplasmic protonation of conserved charged residues narrows the OmpC pore, explaining the loss of regulation in charge-neutral mutants.</em></p>
</div>

<h2 id="the-metabolic-engine">The metabolic engine</h2>
<p>It was now logical to ask: Does bacterial physiology have a mechanism to carry out this kind of regulation spontaneously? To find out, we adapted a wide range of genetically encoded fluorescent sensors (pHluorin, pHuji, GcaMp, and Ginko1) to monitor ion concentrations.
Our most significant innovation was adapting these sensors for the periplasmic space. We initially thought that exporting them with pelB tags was sufficient, but quickly discovered that proteins do not tolerate the highly oxidative environment of the periplasm. For pHluorin, we found the dynamic range was not wide enough, so we switched to pHuji, which covers lower pH values. For Ginko1, we had multiple failed attempts because it relied on a GFP backbone anchored with a potassium-binding region. The issue with GFP is that it has disulfide bonds that do not form properly in the periplasmic space. Thus, we had to change the GFP for sfYFP while maintaining the potassium-binding region.
With these tools, we plugged the cells into our microfluidic devices and began monitoring ion dynamics. We observed spontaneous spikes in membrane potential—bacterial “action potentials,” if you wish—that were mirrored by changes in periplasmic H+ and K+. Crucially, these action potentials were absent in the Kch knockout cells.
The most exciting part came when we simultaneously imaged the membrane voltage alongside 2NBDG uptake. This revealed that moments of membrane depolarization correlated perfectly with bursts of 2NBDG uptake (Figure 3). Our mental model to explain this dynamic resembled a 4-piston engine cycle. The cycle begins in the “power stroke” or hyperpolarized state, with the periplasmic space full of protons, representing stored potential energy. These protons are then rapidly consumed to produce ATP or power other biological processes (Exhaust). As the proton gradient is consumed, the membrane begins to depolarize. This drop in potential acts as the “intake valve opening” (fuel intake), allowing nutrients (or 2-NBDG) to rush in through the now-open porin gates. Finally, the bacteria immediately triggers another metabolic cycle (Compression) to pump protons back into the periplasm, re-hyperpolarizing the membrane and closing the porin gates, ready to fire the next “power stroke.”
Returning to our original claim that bacteria use the PMF as a source of energy, we tested how different carbon sources would impact these bacterial “action potentials.” We provided the bacteria with sources that would lead to different levels of membrane potential. Interestingly, we observed a higher frequency of membrane voltage oscillations when E. coli was supplied with glucose compared with other carbon sources such as fumarate or lipids. More importantly, the observed synchrony, depolarization correlating with uptake and hyperpolarization with low uptake, was consistent. This was a fascinating discovery suggesting that bacteria could be using these membrane voltage oscillations to dynamically regulate the uptake of small molecules (Figure 4).</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_011/membrane-potential-fig2-voltage-dynamics.png" alt="Live traces of periplasmic and cytoplasmic pH and K+, membrane voltage spikes in wild type but not kch mutants, and the correlation between QuasAr2 voltage and 2NBDG uptake over time." width="100%" />
  <p><em>Figure 3. Ion-sensor imaging links periplasmic H+/K+ fluctuations with membrane depolarization and bursts of 2NBDG uptake.</em></p>
</div>

<div style="text-align: center;">
  <img src="/images/portfolio/post_011/membrane-potential-fig3-metabolism.png" alt="Membrane voltage oscillations across carbon sources and concentrations, action potential frequency versus substrate, and Hoechst uptake across minimal, glucose, and lipid media." width="100%" />
  <p><em>Figure 4. Metabolic state controls the frequency of membrane voltage oscillations and porin permeability across carbon sources.</em></p>
</div>

<h2 id="antibiotic-consequences">Antibiotic consequences</h2>
<p>Now the final question, why would you care about this? Well, it turns out that like sugars, many antibiotics rely on porin to get into the bacterium. Thus, the final phase of the project addressed whether the relationship between membrane voltage and uptake also applied to antibiotics, given that a significant portion of antibiotic resistance is due to changes in drug permeability.
We focused on quinolones, a class of naturally fluorescent antibiotics, which made tracking their uptake in single cells easy. First, we confirmed that porins were the main route for quinolone entry, showing that porin knockouts had reduced uptake. The Kch knockout also showed decreased permeability, reinforcing the role of membrane potential. We then measured the EC50 of quinolones in both porin and Kch knockouts compared to wild-type cells, finding that both knockouts were more resistant to quinolones but not to colistin, an antibiotic that enters cells independently of porins.
Finally, we revisited our ionophore experiments, treating cells with CCCP to see if altering the internal pH would affect ciprofloxacin uptake. Sure enough, we found that disrupting the proton gradient reduced antibiotic uptake. We hope this work helps explain why bacteria in certain metabolic states become naturally more resistant to drugs. It leaves us with a mechanistic bridge from metabolism to permeability. In lipid-rich environments—like certain intracellular niches—periplasmic acidification or slow bacterial “action potentials” likely close porins and reduce antibiotic entry. Therapeutically, activating Kch or otherwise depleting the proton gradient in the periplasm could, with adjutants, be a potential strategy to enhance antibiotic uptake and combat resistance (Figure 5).</p>

<div style="text-align: center;">
  <img src="/images/portfolio/post_011/membrane-potential-fig4-antibiotics.png" alt="Ciprofloxacin uptake and EC50 across porin and kch mutants, contrasted with colistin controls, and the effect of CCCP in glucose versus lipid media." width="100%" />
  <p><em>Figure 5. Porin and Kch control ciprofloxacin uptake and resistance, with proton gradient disruption dampening drug entry.</em></p>
</div>

<h2 id="few-thank-yous">Few thank yous:</h2>
<p>Before closing, just to note that this was a true team effort. Immense gratitude to Andres Floto—every success traces back to your ideas and leadership with Floto Lab. Grateful for the support from Cambridge University, the School of Clinical Medicine, and the Laboratory of Molecular Biology. Deep thanks to all co-authors: Stephen Trigg, Georgeos Hardo, Anja Hagting, Ieuan E. Evans, Chris Ruis, Ali F. Alsulami, David Summers, Felicity Crawshay-Williams, Tom L. Blundell, Lucas Boeck, and Somenath Bakshi.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><category term="microbiology" /><category term="membrane-potential" /><category term="antibiotics" /><category term="porins" /><summary type="html"><![CDATA[How internal proton and potassium gradients gate porins, tune nutrient uptake, and shape antibiotic entry in Gram-negative bacteria.]]></summary></entry><entry><title type="html">Announcing MCPR: The First AI Protocol for Live R Sessions</title><link href="https://phisanti.github.io/posts/2025/09/mcpr/" rel="alternate" type="text/html" title="Announcing MCPR: The First AI Protocol for Live R Sessions" /><published>2025-09-15T00:00:00+00:00</published><updated>2025-09-15T00:00:00+00:00</updated><id>https://phisanti.github.io/posts/2025/09/mcpr</id><content type="html" xml:base="https://phisanti.github.io/posts/2025/09/mcpr/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Every month brings new AI coding benchmarks showing superhuman performance. Yet any data scientist who has worked with Claude Code, GitHub Copilot, or similar tools discovers a maddening reality: these supposedly advanced agents must be kept on an impossibly short leash. Despite stellar demos and impressive performance scores, current AI assistants fail catastrophically at the iterative, stateful work that defines serious data analysis.</p>

<p>The problem manifests in two equally frustrating ways. First, we have the chatboxs, this involves asking questiosn and copying and pasting code snippets back and forth—transforms what should be seamless collaboration into a tedious exercise in manual context management. Then, we have the AI agents, but they only know bash and invoke Rscript for every operation, obliterating workspace state and forcing complete pipeline reconstruction for minor changes. Moreover, if you leave then running for long, their erors compound across steps as variables get redefined and assumptions change. Thus, neither approach supports the cumulative, hypothesis-driven exploration that characterizes sophisticated analytical work.</p>

<p>The deeper issue stems from architectural assumptions borrowed from software engineering. AI tools are fundamentally designed for code that lives in executable scripts and runs repeatedly. When you ask an AI agent to modify a plot color in your three-hour analysis session, it doesn’t adjust your existing visualization—it rebuilds your entire analytical pipeline from scratch. This approach is antithetical to exploratory data analysis, where insights emerge through cumulative investigation, hypothesis testing, and iterative refinement spanning hours or days.</p>

<h2 id="announcing-mcpr-the-first-ai-protocol-for-live-r-sessions-">Announcing MCPR: The First AI Protocol for Live R Sessions <a href="https://phisanti.github.io/MCPR/" alt="MCPR"><img src="/images/portfolio/post_010/mcpr_logo.png" alt="MCPR logo" align="right" width="120" /></a></h2>

<p>Today, I’m announcing MCPR, the first Model Context Protocol server designed specifically for interactive R sessions. MCPR solves this problem by enabling AI agents to operate within your live R environment. The breakthrough is treating the AI agent as a translator between Natural English intent to R’s syntax, operating in a persistent session where your workspace state is preserved.</p>

<p>Instead of generating isolated scripts, the agent becomes your “computational hands.” You direct the analytical strategy; the agent executes the code within your established context.</p>

<ol>
  <li>It Eliminates Overhead: Exploratory changes no longer require expensive pipeline rebuilds.</li>
  <li>It Amplifies Judgment: Your time shifts from managing code snippets to high-value tasks like hypothesis refinement and interpretation.</li>
  <li>It Manages Risk: By operating in a continuous environment, MCPR prevents the compounded errors that arise when context changes between isolated scripts.</li>
</ol>

<p>The rule for successful AI collaboration has always been to delegate the work, not the thinking. MCPR is the first tool that makes this a practical reality for data scientists.</p>

<h2 id="architecture-philosophy-simplicity-scales">Architecture Philosophy: Simplicity Scales</h2>

<p>MCPR’s design philosophy prioritizes minimal tool interfaces that compose into powerful workflows. Rather than overwhelming agents with dozens of specialized functions, MCPR provides four essential tools: session management, code execution, plot generation with intelligent token optimization, and environment introspection. The communication layer leverages JSON-RPC 2.0 over lightweight sockets, ensuring cross-platform compatibility and non-blocking interactions.</p>

<h2 id="getting-started-from-installation-to-insight">Getting Started: From Installation to Insight</h2>

<p>Setting up MCPR takes minutes. Install the package from GitHub:</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">remotes</span><span class="o">::</span><span class="n">install_github</span><span class="p">(</span><span class="s2">"phisanti/MCPR"</span><span class="p">)</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">MCPR</span><span class="p">)</span><span class="w">
</span><span class="n">install_mcpr</span><span class="p">(</span><span class="n">agent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"claude"</span><span class="p">)</span><span class="w">  </span><span class="c1"># Configures MCP integration for claude, copilot, or gemini</span><span class="w">
</span></code></pre></div></div>

<p>Next, start a listener in your R console. This makes your session available to the agent.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Make your current R session available</span><span class="w">
</span><span class="n">MCPR</span><span class="o">::</span><span class="n">mcpr_session</span><span class="p">()</span><span class="w">
</span></code></pre></div></div>

<p>That’s it. Now, you can simply ask your AI assistant to take over. Instruct your AI agent to connect, and watch the collaboration transform. Instead of explaining what you want and manually copying code, simply make request in plain English. The agent doesn’t generate a script for you to copy—it executes directly within your live session:
    You think: “Let’s filter this dataset for outliers and re-run the regression.”
    The agent works: It executes the dplyr and lm commands in your live session, using the objects you’ve already created.</p>

<div style="text-align: center;">
    <img src="/images/portfolio/post_010/mcpr_demo.gif" alt="MCPR in action - seamless AI collaboration in R" width="100%" />
</div>

<h2 id="the-take-home-message">The take home message</h2>

<p>This isn’t about replacing human analytical judgment. It’s about amplifying it through optimal task delegation. When mechanical execution happens automatically within maintained context, when exploratory changes don’t require expensive rebuilds, when AI agents become computational extensions of analytical thinking rather than separate tools requiring constant management—that’s when human-AI collaboration finally serves discovery rather than creating overhead.</p>

<p>The rule for successful AI collaboration has always been: delegate the work, not the thinking. MCPR is the first tool that makes this philosophy a practical reality.</p>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>We thank <a href="https://github.com/simonpcouch">Simon P. Couch</a>(<a href="https://github.com/posit-dev/mcptools">mcptools</a>) for the inspiration to use nanonext and <a href="https://github.com/dietrichson">Aleksander
Dietrichson</a> (<a href="https://github.com/chi2labs/mcpr">mcpr</a>) for the idea of using <code class="language-plaintext highlighter-rouge">roxygen2</code> for parsing tools.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><category term="R" /><category term="AI" /><category term="data-science" /><category term="programming" /><summary type="html"><![CDATA[MCPR solves the fundamental disconnect between AI agents and interactive data analysis by enabling direct execution within live R sessions, transforming AI from a code generator into a computational extension of analytical thinking.]]></summary></entry><entry><title type="html">The Great OOP Graveyard: Why R’s Class System Chaos Actually Makes Sense</title><link href="https://phisanti.github.io/r6-vs-s7/" rel="alternate" type="text/html" title="The Great OOP Graveyard: Why R’s Class System Chaos Actually Makes Sense" /><published>2025-09-01T00:00:00+00:00</published><updated>2025-09-01T00:00:00+00:00</updated><id>https://phisanti.github.io/r6-vs-s7</id><content type="html" xml:base="https://phisanti.github.io/r6-vs-s7/"><![CDATA[<h2 id="the-great-oop-graveyard-why-rs-class-system-chaos-actually-makes-sense">The Great OOP Graveyard: Why R’s Class System Chaos Actually Makes Sense</h2>

<h2 id="introduction">Introduction</h2>

<p>This summer I found myself in unfamiliar territory: building an R project that had absolutely nothing to do with statistical analysis. No regression models, no hypothesis tests, no data visualisations. For the first time in years, I had to venture beyond R’s comfortable functional world into its object-oriented wilderness. And what a wild, confusing, oddly fascinating wilderness it turned out to be.</p>

<p>R’s object-oriented programming landscape looks like the aftermath of some great taxonomic war. You’ve got S3 shambling around like a beloved but decrepit ancestor. S4 standing at attention with its formal methods and rigid protocols. R6 strutting around with its OOP swagger. S7 promising to be the chosen one who will finally bring balance to the Force. And scattered around them, the bones of the fallen: proto (used in the great ggplot2 but to be swapped by S7), R.oo, OOP, referenceClasses (R5?), mutateR, and probably a dozen others I’ve mercifully forgotten.</p>

<p>After some years building this kind of stuff in Python, I truly get the feeling. When you look at well-written Python code, it reads like prose. The syntax is clean, the object model is consistent, everything fits together with an almost zen-like harmony. R’s programming language can be so complex that even R-core developers occasionally get surprised by its behaviour. R treats <code class="language-plaintext highlighter-rouge">1</code> as a vector of length 1, which means there’s no difference between scalars and vectors except in your head. R has assignment operators that go both ways (<code class="language-plaintext highlighter-rouge">&lt;-</code> and <code class="language-plaintext highlighter-rouge">-&gt;</code>) because why choose? Lists will return <code class="language-plaintext highlighter-rouge">NULL</code>, not an error, if you use a name that hasn’t been defined. However, if you think about assigning <code class="language-plaintext highlighter-rouge">NULL</code> to an already existing list, it won’t replace the position with <code class="language-plaintext highlighter-rouge">NULL</code>. It will remove that entry. There’s no way to have a list containing <code class="language-plaintext highlighter-rouge">NULL</code> after creation. And don’t even get me started on environments: all your code is brought in by “sourcing” it and basically running the code in one single namespace. Compared to Python’s clean <code class="language-plaintext highlighter-rouge">import x from y</code>, in R it’s hard to organise routines in logical modules. There’s no hierarchy in organising individual R files when developing a package or project.</p>

<p>Python, meanwhile, was born from completely different philosophical soil. Python is the product of software engineering thinking applied with extraordinary discipline and care. When Guido van Rossum designed Python, he was thinking about how programmers think, how code should read, how systems should be structured. Python is gorgeous. Python makes sense. Python feels like programming should feel—clean, logical, consistent, and elegant. It’s the closest you get to programming in natural language English. But then you actually try to do data wrangling in Python, and something interesting happens:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python's methodical approach to data summarisation
</span><span class="n">quarterly_summary</span> <span class="o">=</span> <span class="p">(</span><span class="n">sales_data</span>
    <span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s">'transaction_date &gt;= "2023-01-01"'</span><span class="p">)</span>
    <span class="p">.</span><span class="n">groupby</span><span class="p">([</span><span class="s">'sales_rep'</span><span class="p">,</span> <span class="s">'product_line'</span><span class="p">])</span>
    <span class="p">.</span><span class="n">agg</span><span class="p">({</span>
        <span class="s">'gross_revenue'</span><span class="p">:</span> <span class="p">[</span><span class="s">'sum'</span><span class="p">,</span> <span class="s">'mean'</span><span class="p">,</span> <span class="s">'count'</span><span class="p">],</span>
        <span class="s">'net_profit'</span><span class="p">:</span> <span class="p">[</span><span class="s">'sum'</span><span class="p">,</span> <span class="s">'mean'</span><span class="p">],</span>
        <span class="s">'customer_satisfaction'</span><span class="p">:</span> <span class="s">'mean'</span>
    <span class="p">}))</span>

<span class="c1"># Navigate the multi-index column hell
</span><span class="n">quarterly_summary</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">col</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">_</span><span class="si">{</span><span class="n">col</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">"</span> <span class="k">if</span> <span class="n">col</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="s">''</span> <span class="k">else</span> <span class="n">col</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> 
                            <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="n">quarterly_summary</span><span class="p">.</span><span class="n">columns</span><span class="p">]</span>

<span class="c1"># Reshape into something actually usable
</span><span class="n">final_report</span> <span class="o">=</span> <span class="n">quarterly_summary</span><span class="p">.</span><span class="n">pivot_table</span><span class="p">(</span>
    <span class="n">index</span><span class="o">=</span><span class="s">'sales_rep'</span><span class="p">,</span>
    <span class="n">columns</span><span class="o">=</span><span class="s">'product_line'</span><span class="p">,</span>
    <span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s">'gross_revenue_sum'</span><span class="p">,</span> <span class="s">'net_profit_sum'</span><span class="p">]</span>
<span class="p">).</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nb">round</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<p>The beautiful systematic thinking that makes Python elegant for general programming becomes laborious and verbose when applied to the inherently mathematical world of data manipulation. Moreover, the fact that data science is one of the things amongst many that Python does means that if you jump into another framework, say Polars, then you have to learn another syntax from zero.</p>

<p>What we’re witnessing is the collision between two different ways of thinking about computational problems: engineering thinking and mathematical thinking.</p>

<h2 id="the-functional-sweet-spot-why-rs-domain-specific-design-is-actually-its-superpower">The Functional Sweet Spot: Why R’s Domain-Specific Design is Actually Its Superpower</h2>

<p>The worst of R is that it comes from statisticians who needed to crunch numbers (without paying a SAS licence) rather than build the perfect programming language. The early designers weren’t trying to build a “proper” programming language—they were trying to build a mathematical calculator that could handle real data. Thus, these aren’t design flaws—they’re design decisions made by people who prioritised mathematical intuition over computational orthodoxy. Thus, R has data as a first-class citizen. They naturally brought in the functional approach they were familiar with, because this is how mathematical education actually works.</p>

<p>I don’t know about you, but in my high-school math class didn’t learn about objects and methods and inheritance. I learnt about functions: \(f(x) = 2x + 3\), that functions are mappings (given an input, produce an output) and function composition: if you have \(f(x) = 2x + 3\) and \(g(x) = x^2\), then \((g \circ f)(x) = g(f(x)) = g(2x + 3) = (2x + 3)^2\). This is the cognitive foundation that every quantitatively-trained person carries. Mathematics is fundamentally about transformations, mappings, and compositions of functions. Not objects, not methods, not inheritance hierarchies—functions.</p>

<p>The same operation we struggled with in Python becomes natural when expressed in R’s mathematical idiom:</p>

<div class="language-r highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># R expressing the natural flow of mathematical thinking</span><span class="w">
</span><span class="n">quarterly_summary</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">sales_data</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">filter</span><span class="p">(</span><span class="n">transaction_date</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">as.Date</span><span class="p">(</span><span class="s2">"2023-01-01"</span><span class="p">))</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">group_by</span><span class="p">(</span><span class="n">sales_rep</span><span class="p">,</span><span class="w"> </span><span class="n">product_line</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">summarise</span><span class="p">(</span><span class="w">
    </span><span class="n">total_gross_revenue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">gross_revenue</span><span class="p">),</span><span class="w">
    </span><span class="n">avg_gross_revenue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mean</span><span class="p">(</span><span class="n">gross_revenue</span><span class="p">),</span><span class="w">
    </span><span class="n">total_net_profit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">net_profit</span><span class="p">),</span><span class="w">
    </span><span class="n">avg_customer_satisfaction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mean</span><span class="p">(</span><span class="n">customer_satisfaction</span><span class="p">),</span><span class="w">
    </span><span class="n">transaction_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">n</span><span class="p">(),</span><span class="w">
    </span><span class="n">.groups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"drop"</span><span class="w">
  </span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">pivot_wider</span><span class="p">(</span><span class="w">
    </span><span class="n">names_from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">product_line</span><span class="p">,</span><span class="w">
    </span><span class="n">values_from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="n">total_gross_revenue</span><span class="p">,</span><span class="w"> </span><span class="n">total_net_profit</span><span class="p">),</span><span class="w">
    </span><span class="n">names_sep</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"_"</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>The functional programming paradigm maps perfectly onto data analysis because data analysis <em>is</em> functional mathematical thinking applied to empirical problems. You transform data through sequences of well-defined mathematical operations. You compose simple functions to build sophisticated analytical procedures. You create computational pipelines because that’s how mathematical reasoning naturally flows from raw data to statistical conclusions.</p>

<p>R’s “weird” design choices aren’t weird—they’re the natural computational expression of mathematical thinking patterns that have been refined through centuries of pedagogical evolution.</p>

<h2 id="the-functional-breaking-point-when-mathematical-thinking-hits-its-limits">The Functional Breaking Point: When Mathematical Thinking Hits Its Limits</h2>

<p>But here’s what my summer project taught me: sometimes you need to build systems that exist beyond the elegant world of mathematical transformations. Sometimes you need persistent state that survives across multiple operations. Sometimes you need complex interactions between subsystems that each maintain their own internal logic and coordinate through well-defined interfaces.</p>

<p>Base R excels at transforming data, but it struggles with managing persistent connections, maintaining session state across time, coordinating real-time interactions, or building systems that need to remember complex relationships between their components. These aren’t data analysis problems—they’re systems architecture problems. Moreover, with modern data requirements with interactive dashboards, connections to third-party data sources. Think about what today’s R developers actually build: not just one-off statistical analyses, but living systems that researchers, and non-programmer savvy users (clinicians, wet-lab scientist,…) bio interact with daily. Hence, R needs to grow beyond its statistical and data analysis roots.</p>

<p>Now here comes the crucial insight: R developers currently do not agree on what “properties” should an “object” have—and this disagreement reflects two fundamentally different cognitive approaches to the same technical challenges.</p>
<div align="center">
    <img src="/images/portfolio/post_009/pre_blogpost_image.png" alt="The R6 vs S7 battle" width="80%" />
</div>

<h3 id="r6-the-engineering-solution">R6: The Engineering Solution</h3>

<p>R6 embodies classical object-oriented thinking. An R6 object is a self-contained entity with encapsulated state and behaviour. When you create an R6 object, you’re thinking like a software engineer: “What data does this object need to maintain? What operations should it support? How should it manage its internal consistency?” You tell the object what to do (<code class="language-plaintext highlighter-rouge">$connect()</code>, <code class="language-plaintext highlighter-rouge">$process_data()</code>), and it manages its own state internally. This is encapsulation in the classical sense—perfect for building complex internal components that need sophisticated state management.</p>

<p>The beauty of R6 lies in its unapologetic embrace of mutable state and familiar object-oriented patterns that will feel natural to programmers coming from other languages. When you need an object that remembers things between method calls, that maintains internal counters, that manages connection pools or caches expensive computations, R6 doesn’t fight you—it gives you the tools to build exactly what you need. This classical OOP approach is particularly valuable for attracting non-R programmers who bring solid architectural knowledge from computer science backgrounds, helping to elevate the engineering practices in R projects. Most R programmers transition from data science to programming rather than the other way around, often lacking the architectural know-how that comes from formal CS training. R6 bridges this gap by providing familiar patterns that encourage good software engineering practices whilst still being accessible within R’s ecosystem.</p>

<h3 id="s7-the-r-compromise">S7: The R Compromise</h3>
<p>S7 represents a different philosophical approach: maintaining functional separation whilst adding object-oriented capabilities. In S7, objects are still primarily data containers, but now they can have formal type definitions and method dispatch. The data (the object) remains separate from the operations (the generic functions), preserving the functional mindset that R users expect. This creates wonderfully consistent user-facing APIs that feel mathematical rather than computational.</p>

<p>The base idea in S7 is how it preserves R’s functional soul while adding the structure that complex systems need, making it an ideal stepping stone for traditional R programmers who want to move into more sophisticated project architectures without abandoning their functional thinking patterns. S7 objects are immutable by default—when you “modify” an S7 object, you get a new object back, just like traditional R operations. This immutability means S7 plays nicely with R’s copy-on-modify semantics and feels natural to R users who are accustomed to functional thinking. For statisticians and data scientists who’ve grown comfortable with generic functions like <code class="language-plaintext highlighter-rouge">summary()</code> and <code class="language-plaintext highlighter-rouge">plot()</code>, S7 extends this familiar paradigm into more complex domains, allowing them to write classes and build sophisticated systems without the cognitive leap required by classical OOP.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In my opinion, the choice between R6 and S7 isn’t about technical superiority, but rather about cognitive alignment with different problem domains. When building this summer project, I tried the S7 approach, but wasn’t very comfortable with the organisation. Switching to R6 helped me to organise the components and processes. I’m thus not sure if S7 will truly be “the chosen one”, but I think it makes sense to use R6 for the internals when building internal systems that require sophisticated state management and encapsulation. Something that the user will never touch. Then, use S7 when building user-facing APIs that need to feel like mathematical operations. I think this way helps maintain mathematical elegance for data analysis work whilst providing engineering power for systems development. Let’s see what the future of R has for us. I really look forward to the end of the OOP wars.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[This weekend I managed to finish a very fun project! I think there are two kinds of bioinformaticians: computer scientists that for one reason or another get into biology...]]></summary></entry><entry><title type="html">CodeDjinn: Engineering Simplicity in the Age of AI Complexity</title><link href="https://phisanti.github.io/codedjinn-ai-complexity/" rel="alternate" type="text/html" title="CodeDjinn: Engineering Simplicity in the Age of AI Complexity" /><published>2025-07-28T00:00:00+00:00</published><updated>2025-07-28T00:00:00+00:00</updated><id>https://phisanti.github.io/codedjinn-ai-complexity</id><content type="html" xml:base="https://phisanti.github.io/codedjinn-ai-complexity/"><![CDATA[<h1 id="codedjinn-engineering-simplicity-in-the-age-of-ai-complexity">CodeDjinn: Engineering Simplicity in the Age of AI Complexity</h1>

<p>This weekend I managed to finish a very fun project! I think there are two kinds of bioinformaticians: computer scientists that for one reason or another get into biology, or biologists that for necessity get into computer science. I belong to the second category. Thus, I find myself frequently asking for complex multiflag commands such as <code class="language-plaintext highlighter-rouge">find . -name "*.fastq.gz" -exec zcat {} \; | wc -l</code>, or <code class="language-plaintext highlighter-rouge">rsync -avz --progress --exclude='*.tmp' /source/data/ user@server:/backup/</code>, or <code class="language-plaintext highlighter-rouge">docker run -v $(pwd):/data -v /tmp:/tmp --rm biocontainers/samtools samtools view -bS input.sam | samtools sort -o output.bam</code>. This leads to seconds to minutes to search. The standard solution – web-based or IDE AI assistants – introduces its own inefficiency: context switching. Each lookup requires:</p>

<ol>
  <li>Mental context switch from problem-solving to information retrieval</li>
  <li>Physical context switch from terminal to browser</li>
  <li>Potential exposure to distractions (notifications, other tabs)</li>
  <li>Copy-paste operations that introduce error potential</li>
  <li>Context switch back to terminal and problem state</li>
</ol>

<p>Thus, in the age of LLMs, I thought, why not build a fast responsive app that is directly there where I need it. My goal was to make something fast, simple and responsive. I thus came up with the idea of CodeDjinn…</p>

<h2 id="codedjinn">CodeDjinn</h2>

<p>What is CodeDjinn, or rather, what is the main idea behind CodeDjinn? CodeDjinn is a lightweight-fast CLI assistant that generates shell commands. That’s it. The goal was to be there where you are and come up with quick and simple answers. Building something fast and responsive sounds simple, but it can be complex to implement. Indeed, it taught me a few profound ideas about the nature of tools and human-computer interaction.</p>

<p>First of all, I learned to prioritize – a meditation on essence versus excess. Any of the top-tier models can answer most of the questions that I was intending to address such as <code class="language-plaintext highlighter-rouge">code-djinn -a "count total reads in all fastq files"</code>. However, it raises a philosophical question: does it make any sense to summon the most powerful models for the simplest of queries? First of all, you’ll spend more resources (and money) than necessary . Second, you’ll spend also a few extra seconds waiting for an answer that smaller models can provide, which would defeat the initial purpose. Thus, I decided to work with the smallest, fastest models I could find, embracing the principle of “sufficient intelligence.”</p>

<ul>
  <li><strong>Mistral’s Codestral:</strong> Mistral has built a reputation for fast, efficient models, making it a natural choice for this use case.</li>
  <li><strong>Google’s Gemini Flash:</strong> This model demonstrated superior response times in public benchmarks, with Gemini Flash-lite consistently ranking as a top performer.</li>
  <li><strong>QwQ-32B:</strong> Last but not least, I wanted to explore the edges of the ecosystem. Given that Qwen has been making some noise, I thought, why not embrace the experimental spirit?</li>
</ul>

<p>The second thing that I learned was about optimizing Python code itself – a journey into the temporal dimension of software. I discovered that each time I called the tool from the CLI, I was “invoking a new CodeDjinn”, a fresh consciousness that had to reconstruct its entire reality from scratch. Thus, I implemented a cache system for fast responses – a form of digital memory. I also learned about lazy loading, which meant only the needed libraries were loaded each time.</p>

<p>This was interesting, because in this LLM age, many people have argued that they make us “lazy” and “stupid”. However, I see it differently. These tools are amplifiers of curiosity. I would probably never have learned about concepts like lazy loading if not for the fact that nowadays LLMs can make them easily accessible. They democratize knowledge, turning arcane wisdom into accessible understanding.</p>

<h2 id="real-world-magic-moments">Real-World Magic Moments</h2>

<p>So, what can CodeDjinn do for you? I focused on a feature set designed for maximum utility. The core functionality includes command generation (ask mode) and safe execution (execute mode). Additionally, I integrated an explanation feature for continuous learning and command understanding. The configuration system supports API key management, shell detection, and prompt customization, allowing users to specify preferred tools and command styles.</p>

<p>After getting done with the building, I started using it in my normal workflow and I was quite amazed about how much it could get done and how it truly helped me to stay in focus on the task I was doing. Here are practical examples demonstrating its capabilities:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Counting reads across all samples</span>
code-djinn <span class="nt">-x</span> <span class="s2">"count total reads in all fastq files"</span>
<span class="c"># Instantly: find . -name "*.fastq" -exec wc -l {} + | awk '{s+=$1} END {print s}'</span>
</code></pre></div></div>

<p>I used to spend minutes crafting that command, each flag a small decision, each pipe a connection between ideas. Now it emerges fully formed, like Athena from Zeus’s head.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># That moment when you need to check memory usage</span>
code-djinn <span class="nt">-x</span> <span class="s2">"show top 10 memory hungry processes with percentages"</span>
<span class="c"># Conjures: ps aux | sort -rk 4 | head -n 11 </span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code-djinn <span class="nt">-x</span> <span class="s2">"show commit history with file changes for last sprint"</span> 
<span class="c"># Magic: git log --oneline --graph --decorate --color --stat --since="2 weeks ago"</span>
</code></pre></div></div>

<p>And below you can see a quick plot of the execution time of some request. I indeed achieved the “sub-second” response time that I was aiming but note that some of the commands require a lot of time to execute, while others are very fast.</p>

<div align="center">
    <img src="/images/portfolio/post_008/execution_time_distribution.png" alt="Comprehensive Comparison" width="80%" />
</div>

<h2 id="conclusion-the-philosophy-of-simple-tools">Conclusion: The Philosophy of Simple Tools</h2>

<p>Here’s something I discovered while building this: Is this an agent? No, but I think it is really useful. I don’t think we need complex advanced multi-step agents for everything; sometimes getting quick answers can be very useful. CodeDjinn doesn’t try to write your entire analysis pipeline, manage your projects, or have philosophical discussions about the nature of genomics (though that would be fascinating). It translates thoughts into shell commands. That’s it. And that simplicity is its superpower. Indeed, I believe the future is full of small models and soon this will be a default feature of most OS. But this is just an opinion.</p>

<p>I played around with adding more features – “what if it could also edit files?” “what if it could chain multiple commands intelligently?” But each addition made it slower and more complex. The Unix philosophy remains relevant: do one thing well.</p>

<p>If you wanna try, the tool is available at <code class="language-plaintext highlighter-rouge">pip install code_djinn</code> – a small djinn in a digital bottle, ready to grant your command-line wishes. If you feel like complaining or asking for more, please open an issue on <a href="https://github.com/phisanti/code_djinn/">the GitHub repo</a>.</p>

<h2 id="bonus">Bonus</h2>

<p>The reason I called it CodeDjinn is because in Arabic mithology a djinn (“جن”) is a supernatural creature that can be summoned to do your bidding. In the same way, CodeDjinn is a tool that you can summon to help you with your coding tasks.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[This weekend I managed to finish a very fun project! I think there are two kinds of bioinformaticians: computer scientists that for one reason or another get into biology...]]></summary></entry><entry><title type="html">Down the Rabbit Hole: My Accidental Journey to PyPI</title><link href="https://phisanti.github.io/accidental-journey-pypi/" rel="alternate" type="text/html" title="Down the Rabbit Hole: My Accidental Journey to PyPI" /><published>2025-07-03T00:00:00+00:00</published><updated>2025-07-03T00:00:00+00:00</updated><id>https://phisanti.github.io/accidental-journey-pypi</id><content type="html" xml:base="https://phisanti.github.io/accidental-journey-pypi/"><![CDATA[<p>The other day, I was dragged into the rabbit hole of a bug that does not let you sleep. In my work, I am facing the task of processing a gigaton of microscope images while tracking a dizzy array of tiny objects, all complicated by a microscope camera that drifts unpredictably. The constant need for precise image registration to correct this drift quickly became a critical issue.</p>

<p>Initially, I leaned on a well-known Python package, PyStackReg, which operates with TurboReg’s fast C++ core to perform subpixel registration.  However, as I observed its performance under practical conditions, I recognized that the tool was not delivering the stability and speed that I was familiar when using Fiji. Looking at the images, two little quirks really bugged me: a jump in the very first frame of every movie, and a registration that seemed to circle around its equilibrium point. These small details pushed me to develop a new package.</p>

<p>So, I reached out to Tseng Qing, the creator of the TemplateMatching plugin, and floated the idea of porting his package to Python. I tinkered around his code and realised that OpenCV was the one moving the threads behind scenes. With Python’s excellent API for OpenCV, the solution practically wrote itself. After some trial and error, I succeeded in implementing a new registration algorithm.</p>

<p>My surprise came when I took some metrics and saw that it was crazy fast and very precise (see Figure 1). The key part was to focus on solving the Translation problem fast instead of trying to solve all possible cases. Indeed, after working for a while with MONAI, I think the complex registraction issues can be leave to Deep Learning modesl that do regression on the translation matrix, while the simpler ones can be solved with a more traditional approach. While PyStackReg has to build the pyramid features, try some warp -&gt; gradient -&gt; solve until converge -&gt; repeat, the OpenCV uses a template: it slides the template over the target image (like a 2D convolution), producing a grayscale match-map, and then finds the best match, and you are done.</p>

<div align="center">
    <img src="/images/portfolio/post_007/comprehensive_comparison.png" alt="Comprehensive Comparison" width="80%" />
</div>

<p>The end result was good (see the image below). First, I was very happy that the jump in frame 1 disappeared. Second, it was much faster, which really helped us in our analysis pipeline. Finally, the aligned images significantly improved our tracking. Given these good results, I decided to package it. You can download it from PyPI <a href="https://pypi.org/project/templatematchingpy/">here</a> or if you want to see more details, you can check the <a href="https://github.com/phisanti/TemplateMatchingPy">GitHub repository</a>. Life is good when you follow your instincts; you never know where the rabbit hole will lead next.</p>

<div align="center">
    <img src="/images/portfolio/post_007/comparison.gif" alt="Drift Correction Example" width="70%" />
</div>
<p><em>Figure: Comparison showing drift correction results before and after applying the new registration algorithm.</em></p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[The other day, I was dragged into the rabbit hole of a bug that does not let you sleep...]]></summary></entry><entry><title type="html">Focus restoration with restormer</title><link href="https://phisanti.github.io/focus-restoration-restormer/" rel="alternate" type="text/html" title="Focus restoration with restormer" /><published>2025-05-23T00:00:00+00:00</published><updated>2025-05-23T00:00:00+00:00</updated><id>https://phisanti.github.io/focus-restoration-restormer</id><content type="html" xml:base="https://phisanti.github.io/focus-restoration-restormer/"><![CDATA[<h2 id="intro">Intro</h2>

<p>I am very happy this week because, after some time working with it, my contribution to the <a href="https://github.com/Project-MONAI/MONAI">MONAI project</a> has been accepted! For those unfamiliar, MONAI is a PyTorch-based open-source framework for deep learning in healthcare imaging, and I am proud to be part of such a great community because I use their framework a lot.</p>

<p>My contribution is a flexible implementation of the <a href="https://arxiv.org/abs/2111.09881">Restormer model</a> for image restoration. I first stumbled upon this model because, in my current project, we acquire a lot of images (like… A LOT), and very fast. The project consists of acquiring microscopy images of bacteria under multiple conditions across time using well plates. To capture enough bacteria, we use a 40x objective that moves swiftly between positions, making out-of-focus objects really common. The image below (Figure 1) shows an example of the problem. I used a simple Sobel filter to detect the edges of bacteria, just to illustrate how the bacteria in the center are sharper than on the sides of the image:</p>

<div align="center">
    <img src="/images/portfolio/post_006/focus_issue.png" alt="Example image of uneven focus across the image" width="60%" />
</div>
<p><em>Figure 1: Source image (left), Sobel filter edge detection (middle), and focus map (right) showing uneven focus across the field.</em></p>

<p>Since manually focusing on each object is impossible at this scale, I needed an automated solution that could restore focus computationally.</p>
<h2 id="tackling-the-issue">Tackling the issue</h2>
<p>To tackle this, I came up with a simple idea: out-of-focus mainly happens because objects are in different planes. So, I take images at different Z-planes and use the model to infer the perfect focal plane from the out-of-focus images (see Figure 2). Given that our images are quite large and almost always contain some objects in focus and some out of focus, I split the image into quadrants and use as target only the quadrants with the most objects in focus.</p>

<div align="center">
    <img src="/images/portfolio/post_006/restoration_diagram.png" alt="Restoration diagram" width="70%" />
</div>
<p><em>Figure 2: Z-stack acquisition (left), off-focus slices as input (middle), and in-focus slices as target (right).</em></p>

<h2 id="the-restormer-model">The Restormer model</h2>
<p>The <a href="https://arxiv.org/abs/2111.09881">Restormer</a>, as described by Zamir et al. (2022), is a Vision Transformer specifically designed for high-resolution image restoration. It uses a multi-scale encoder-decoder architecture with skip connections, similar to U-Net, allowing it to capture both global and local image features. The key innovation is that, instead of using spatial attention as in the original Vision Transformer, it uses a combination of spatial and channel attention (what the authors call Multi-Dconv Head Transposed Attention, or MDTA), which allows it to focus on the most relevant features in the image. See Figure 3 for a schematic overview.</p>
<div align="center">
    <img src="/images/portfolio/post_006/restormer_arch_diagram.png" alt="Restormer Architecture diagram" width="100%" />
</div>

<p><em>Figure 3: Schematic overview of the Restormer architecture, including the UNet-like encoder/decoder architecture, MDTA, and GDFN blocks.</em></p>

<p>Notice also that one of the key parts is the fact that the original image is also passed to the end of the model. This way, the model can focus on the noise without having to learn the whole image again.</p>

<h2 id="implementation-details">Implementation Details</h2>

<p>This implementation extends the original 2D Restormer to support both 2D and 3D operations, making it particularly valuable for volumetric medical imaging. I thought this would be easy because the transformer block does not care about dimensions and the MONAI library already had an UpSample layer using pixel shuffle. However, there was no pixel unshuffle. Thus, I had to implement it myself. For this, I tackle the problem as permutation problem. If you think about it, it is just about moving the dimensions around:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">pixelunshuffle</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">spatial_dims</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">scale_factor</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">:</span>
    <span class="c1"># ...
</span>    <span class="n">output_size</span> <span class="o">=</span> <span class="p">[</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">new_channels</span><span class="p">]</span> <span class="o">+</span> <span class="p">[</span><span class="n">d</span> <span class="o">//</span> <span class="n">factor</span> <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">input_size</span><span class="p">[</span><span class="mi">2</span><span class="p">:]]</span>
    <span class="n">reshaped_size</span> <span class="o">=</span> <span class="p">[</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">channels</span><span class="p">]</span> <span class="o">+</span> <span class="nb">sum</span><span class="p">([[</span><span class="n">d</span> <span class="o">//</span> <span class="n">factor</span><span class="p">,</span> <span class="n">factor</span><span class="p">]</span> <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">input_size</span><span class="p">[</span><span class="mi">2</span><span class="p">:]],</span> <span class="p">[])</span>

    <span class="c1"># The eureka moment came when I realized the permutation pattern is just collecting all scale factors first,
</span>    <span class="c1"># followed by all spatial dimensions - it's like separating the "what" (features) from the "where" (locations)!
</span>    <span class="n">permute_indices</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="p">[(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">3</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">spatial_dims</span><span class="p">)]</span> <span class="o">+</span> <span class="p">[(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">spatial_dims</span><span class="p">)]</span>

    <span class="c1"># And then, pass everything to the channel dimension while keeping the spatial dimensions intact.
</span>    <span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">reshaped_size</span><span class="p">).</span><span class="n">permute</span><span class="p">(</span><span class="n">permute_indices</span><span class="p">)</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">output_size</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">x</span>
</code></pre></div></div>
<p>After this, everything else was quite simple because the transformer blocks were already dimension-agnostic by design. The next challenge was to give flexibility to the model and to add support for 3D images. The first point is important because the original Restormer was a generic model trained for all kinds of common RGB images. However, in the scientific and medical domain, it is more common to deal with N-channel images. Thus, the researcher should have space to contract or expand the encoder/decoder steps as required for their project. For example, in our case, we only had 2 steps because our image sampling space is quite homogeneous. The second point was relevant because, in the medical field, it is quite common to deal with 3D images (e.g., CT, MRI, etc.).</p>

<p>To give more flexibility to the Restormer, I only had to closely follow the calculation on how the spatial dimensions and channels are calculated at each step:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Restormer</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">spatial_dims</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
        <span class="n">in_channels</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
        <span class="n">out_channels</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
        <span class="n">dim</span> <span class="o">=</span> <span class="mi">48</span><span class="p">,</span>
        <span class="n">num_blocks</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
        <span class="n">heads</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
        <span class="n">num_refinement_blocks</span> <span class="o">=</span> <span class="mi">4</span><span class="p">,</span>
        <span class="n">ffn_expansion_factor</span> <span class="o">=</span> <span class="mf">2.66</span><span class="p">,</span>
        <span class="n">bias</span> <span class="o">=</span> <span class="bp">False</span><span class="p">,</span>
        <span class="n">layer_norm_use_bias</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
        <span class="n">dual_pixel_task</span> <span class="o">=</span> <span class="bp">False</span><span class="p">,</span>
        <span class="n">flash_attention</span><span class="o">=</span> <span class="bp">False</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="n">spatial_multiplier</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">**</span> <span class="p">(</span><span class="n">spatial_dims</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Define encoder levels
</span>        <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_steps</span><span class="p">):</span>
            <span class="n">current_dim</span> <span class="o">=</span> <span class="n">dim</span> <span class="o">*</span> <span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">**</span> <span class="p">(</span><span class="n">n</span><span class="p">)</span>
            <span class="n">next_dim</span> <span class="o">=</span> <span class="n">current_dim</span> <span class="o">//</span> <span class="n">spatial_multiplier</span>
            <span class="c1"># ...
</span>
        <span class="c1"># Define decoder levels
</span>        <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="n">num_steps</span><span class="p">)):</span>
            <span class="n">current_dim</span> <span class="o">=</span> <span class="n">dim</span> <span class="o">*</span> <span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">**</span> <span class="p">(</span><span class="n">n</span><span class="p">)</span>
            <span class="n">next_dim</span> <span class="o">=</span> <span class="n">dim</span> <span class="o">*</span> <span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">**</span> <span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
            <span class="c1"># In the decoder, it was also necessary to add an extra convolution step to reduce dimensions to make space for the skip connections.
</span></code></pre></div></div>
<p>For the encoder, it was very straightforward: the encoder systematically halves spatial dimensions while multiplying by $2^{(spatial_dims - 1)}$ channel dimensions at each step. This is because each spatial dimension contributes multiplicatively to the total channel increase. For the decoder, it was basically the same, but I also added an extra convolution step to reduce dimensions to make space for the skip connections.</p>

<h2 id="results">Results</h2>
<p>The results were nothing short of amazing. Here’s a before/after comparison (Figure 4):</p>

<div align="center">
    <img src="/images/portfolio/post_006/restoration_example.png" alt="Before and after focus restoration" width="70%" />
</div>
<p><em>Figure 4: Left—Input test image (top) and restored image (bottom). Right—Paired SSIM score comparison showing consistent improvement after restoration.</em></p>

<p>Not only do the restored images look visibly sharper and more defined, but the quantitative results speak for themselves. The SSIM (Structural Similarity Index) scores improved dramatically across the board, as shown in the paired plot. This improvement wasn’t just cosmetic—after restoration, the number of detected objects in our automated pipeline nearly doubled, making downstream analysis much more robust and reliable.</p>

<p>It’s genuinely rewarding to see how a well-designed model can breathe new life into challenging microscopy data. Watching those blurry bacteria snap into focus (both visually and statistically!) was one of those moments that makes all the late-night coding sessions worthwhile.</p>

<h2 id="things-that-i-learned">Things that I learned</h2>
<p>Implementing Restormer for MONAI taught me several valuable lessons:</p>
<ol>
  <li><strong>Channel attention is powerful</strong>: The transposed attention mechanism that operates across feature channels rather than spatial dimensions is remarkably effective while being computationally efficient.</li>
  <li><strong>Pixel unshuffle is elegant</strong>: Using pixel unshuffle/shuffle as downsampling/upsampling mechanisms preserves information by rearranging it between spatial and channel dimensions rather than discarding it.</li>
  <li><strong>Loss function choice is crucial</strong>: Since my goal was to restore images for subsequent segmentation, I used Structural Similarity Index (SSIM) as my loss function. This perceptual metric emphasizes preserving edges and contours rather than just pixel values, which was perfect for my use case. Importantly, I trained a separated model for restoring fluorescent singal, where PSNR was the best loss function because in this case we were only interested in the signal.</li>
  <li><strong>Transformers are data-hungry but efficient</strong>: While they require more training data than CNNs, they converge surprisingly quickly and generalize well to unseen data.</li>
  <li><strong>Code Quality</strong>: Given the fact that this was a constribution to a large-scale project, I had to pay extra attention to code quality. This meant writing unit tests, documentation, and following the MONAI coding standards. This was a great opportunity to learn about best practices in software development and how to write clean, maintainable code.</li>
</ol>

<h2 id="conclusion-and-future-work">Conclusion and Future Work</h2>

<p>Contributing to MONAI with this Restormer implementation has been one of the most satisfying projects I’ve tackled recently. The model now lives in the MONAI codebase where others can use it for various medical image restoration tasks beyond my application.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[Image restoration made easy]]></summary></entry><entry><title type="html">Building RMarkdown Reports from the CLI: A Tale of Persistence</title><link href="https://phisanti.github.io/rmarkdown-reports-cli/" rel="alternate" type="text/html" title="Building RMarkdown Reports from the CLI: A Tale of Persistence" /><published>2024-12-22T00:00:00+00:00</published><updated>2024-12-22T00:00:00+00:00</updated><id>https://phisanti.github.io/rmarkdown-reports-cli</id><content type="html" xml:base="https://phisanti.github.io/rmarkdown-reports-cli/"><![CDATA[<h1 id="r-the-good-the-bad-and-the-ugly">R, the Good, the Bad and the Ugly</h1>
<p>I’ve learned that R shines brightest in three areas: statistical analysis, data visualization, and report generation. Data visualization with <code class="language-plaintext highlighter-rouge">ggplot2</code> is a form of art, crafting charts so beautiful that they could bring a tear to a data scientist’s eye. Couple this with RMarkdown, and you have the undisputed champion for scientific reporting. It’s the Swiss Army knife of reproducible research, seamlessly weaving together narrative, code, and results into a single, elegant document. It’s the good, the beautiful, the stuff that makes you fall in love with data analysis.</p>

<p>But then there’s the other side, the bad, the ugly. Trying to make R speak with the rest of the OS, like behaving like a proper CLI tool? That is theoretically possible, but why would you do that to yourself? R wasn’t designed for this. It’s a language built by statisticians for interactive exploration. However, I recently wrote some scripts that made some nice reports from a given dataset, and I found it a bit cumbersome to first write a new Rmd where I was just changing the name of the data path, and second, running:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rscript <span class="nt">-e</span> <span class="s2">"rmarkdown::render('./path/to/report', params=list(file='./data.csv'), output_file='report.pdf')"</span>
</code></pre></div></div>
<p>was too ugly to bear. Thus, the challenge was born. I wanted to create an R package that, when my colleagues installed it, also installed a CLI tool to nicely assemble the automated reports.</p>
<h1 id="the-cli-challenge">The CLI Challenge</h1>

<p>The goal was simple: Create a tool where users could install the r package via <code class="language-plaintext highlighter-rouge">remotes::install_github('mypackage')</code>  and then run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>awesome_report <span class="nt">--input</span> data.csv <span class="nt">--output</span> report.pdf
</code></pre></div></div>

<p>However, the R’s standard approach feels more like a workaround:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rscript myscript.R
</code></pre></div></div>
<p>Then the classic shebang plus chmod approach:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Add shebang to the script</span>
<span class="c">#!/usr/bin/Rscript --vanilla</span>


<span class="c"># Then give permissions</span>
<span class="nb">chmod</span> +x awesome_report.R
</code></pre></div></div>
<p>Sure, it works on my machine™, but distributing this to colleagues? That’s where the fun begins.</p>

<h1 id="the-plot-thickens">The Plot Thickens</h1>
<p>Enter the R packaging system. Surely someone must have solved this? The <code class="language-plaintext highlighter-rouge">cli</code> package looks promising. <code class="language-plaintext highlighter-rouge">Littler</code> catches your eye. Even the mighty Dirk Eddelbuettel (yes, that Dirk) says it’s impossible. Here I quote from his (Stack Overflow answer)[https://stackoverflow.com/questions/44566100/installing-executable-scripts-with-r-package]:</p>

<blockquote>
  <p>Short (and very sad) answer: You cannot. But read on.
Reasoning: R will only ever write package content to its own .libPaths() directory (or the first in case several are given), or a directory given by the user.
So, say, /usr/local/bin/ is simply out of reach. That is a defensible strategy.</p>
</blockquote>

<p>Just when all hope seemed lost, like finding a bug in production code at 4:59 PM on a Friday, I stumbled upon (Rapp)[https://github.com/r-lib/Rapp]. Finally! They say in their intro page:</p>

<blockquote>
  <p>Rapp (short for “R application”) makes it fun to write and share command line applications in R.
It is an alternative front end to R, a drop-in replacement for Rscript that does automatic handling of command line arguments. It converts a simple R script into a command line application with a rich and robust support for command line arguments.
It aims to provides a seamless transition from interactive repl-driven development at the R console to non-interactive execution at the command line.</p>
</blockquote>

<p>Just what I wanted… But wait - it still needs manual PATH setup?</p>

<h1 id="the-eureka-moment">The Eureka Moment</h1>

<p>The solution came together like connecting dots in a complex puzzle. First, while exploring Rapp’s documentation, I found this gem:</p>

<blockquote>
  <p>Place your app in the exec folder in your package, e.g: exec/myapp. Apps are automatically installed as executable.</p>
</blockquote>

<p>Wait - automatic executable permissions? That solves the chmod headache!</p>

<p>Then, diving into R package development docs, I discovered the <code class="language-plaintext highlighter-rouge">configure</code> script - a hidden powerhouse that runs during package installation. Perfect timing to set up our PATH.</p>

<p>Finally, Rapp’s own installation instructions provided the missing piece:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="si">$(</span>Rscript <span class="nt">-e</span> <span class="s1">'cat(system.file("exec", package = "Rapp"))'</span><span class="si">)</span>:<span class="nv">$PATH</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="si">$(</span>Rscript <span class="nt">-e</span> <span class="s1">'cat(system.file("exec", package = "my.package.name"))'</span><span class="si">)</span>:<span class="nv">$PATH</span>
</code></pre></div></div>

<p>Combining these three insights, here’s the elegant solution:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Determine the user's shell configuration file</span>
<span class="nv">SHELL_RC</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">basename</span> <span class="nv">$SHELL</span><span class="si">)</span><span class="s2">rc"</span>
<span class="nv">USER_RC</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.</span><span class="nv">$SHELL_RC</span><span class="s2">"</span>

<span class="c"># Define color codes</span>
<span class="nv">GREEN</span><span class="o">=</span><span class="s1">'\033[0;32m'</span>
<span class="nv">YELLOW</span><span class="o">=</span><span class="s1">'\033[1;33m'</span>
<span class="nv">NC</span><span class="o">=</span><span class="s1">'\033[0m'</span>


<span class="c"># Add the exec directory of the package to the PATH</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="si">$(</span>Rscript <span class="nt">-e</span> <span class="s2">"cat(system.file(</span><span class="se">\"</span><span class="s2">exec</span><span class="se">\"</span><span class="s2">, package = </span><span class="se">\"</span><span class="s2">mypackage</span><span class="se">\"</span><span class="s2">))"</span><span class="si">)</span>:<span class="nv">$PATH</span>
<span class="nv">EXPORT_CMD</span><span class="o">=</span><span class="s1">'export PATH=$(Rscript -e "cat(system.file(\"exec\", package = \"mypackage\"))"):$PATH'</span>
<span class="k">if</span> <span class="o">!</span> <span class="nb">grep</span> <span class="nt">-Fxq</span> <span class="s2">"</span><span class="nv">$EXPORT_CMD</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$USER_RC</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$EXPORT_CMD</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$USER_RC</span><span class="s2">"</span>
    <span class="c"># These echo messages are just to inform the users</span>
    <span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">GREEN</span><span class="k">}</span><span class="s2">Added mypackage exec directory to PATH in </span><span class="nv">$USER_RC</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="k">else
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">mypackage exec directory already in PATH in </span><span class="nv">$USER_RC</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="k">fi
</span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Please run 'source </span><span class="nv">$USER_RC</span><span class="s2">' before using the command.</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="c"># Source the user's shell configuration file to apply changes</span>
<span class="nb">source</span> <span class="s2">"</span><span class="nv">$USER_RC</span><span class="s2">"</span>
</code></pre></div></div>
<p>Place your R scripts in the exec/ folder, and the package installation handles everything else. Now users just need to open his R terminal and run:</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">install.packages</span><span class="p">(</span><span class="s2">"mypackage"</span><span class="p">)</span><span class="w">
</span><span class="c1"># OR in my case</span><span class="w">
</span><span class="n">remotes</span><span class="o">::</span><span class="n">install_github</span><span class="p">(</span><span class="s1">'phisanti/BoeckLabRtools'</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p>And voilà! PDF reports can be buil with.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mypackage build_report data.csv
</code></pre></div></div>
<p>Is it elegant? Maybe not. Does it work? Absolutely. Remember folks, in R, persistence pays off. And if someone tells you it can’t be done, they probably just haven’t found the right hack yet! If you’re wondering why R makes CLI tools so complicated, remember it was created by statisticians, not programmers!</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[R, the good, the bad. I've learned that R shines brightest in three areas: statistical analysis, data visualization, and report generation.]]></summary></entry><entry><title type="html">Shiny strikes back!</title><link href="https://phisanti.github.io/shiny-strikes-back/" rel="alternate" type="text/html" title="Shiny strikes back!" /><published>2023-04-20T00:00:00+00:00</published><updated>2023-04-20T00:00:00+00:00</updated><id>https://phisanti.github.io/shiny-strikes-back</id><content type="html" xml:base="https://phisanti.github.io/shiny-strikes-back/"><![CDATA[<p>Shiny strikes back. A little bit over three weeks after publishing my first Shiny app, here comes my new creation freePrism! This app was inspired by the statistical analysis software Prism. The idea was to provide a GUI tool to my colleagues so that they could perform basic statistical analysis with relative ease. In the app, I included tools to run a two-sample comparison, one-way and two-way ANOVA and linear regression models. In addition, it is also possible to produce ready-made plots to show the statistical contrast for publication and download them as PDFs in case it is necessary to keep working on them. 
The app is neatly organized with different tabs for each statistical test, making it user-friendly and straightforward. All you have to do is input the test arguments and upload your data. Once you hit “run analysis,” the app generates a written report and a summary table. And if you’re not satisfied with the analysis, you can continue to produce different types of plots.
Take a look at these p-values:</p>

<p><img src="/images/freePrism_image.png" alt="image" /></p>

<p>But let’s take a moment to appreciate the magic behind the scenes - R is lifting all the weight. However, instead of using the ELISA analyzer, this time, I went with the Golem framework. The beauty of this framework is that the app is an R package, making it easier to split the different parts into auxiliary functions. Plus, the app is organized into separate modules. Shiny modules can be tricky at first, but you’ll get used to them. One thing that still amuses me about Shiny is that to pass reactive objects between modules; you have to pass them as functions! Why am I surprised when everything in R is a function?
In conclusion, you can find the app at <a href="https://phisanti.shinyapps.io/free-prism/">free-prism</a>, or you can check out the code directly at <a href="https://github.com/phisanti/freePrism">my github</a>. And because the app is an R package, you can even install it on your local device and try it out for yourself. Happy coding, everyone!</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[Shiny strikes back. A little bit over three weeks after publishing my first Shiny app, here comes my new creation freePrism! This app]]></summary></entry><entry><title type="html">The shiny milestone</title><link href="https://phisanti.github.io/shiny-milestone/" rel="alternate" type="text/html" title="The shiny milestone" /><published>2023-04-11T00:00:00+00:00</published><updated>2023-04-11T00:00:00+00:00</updated><id>https://phisanti.github.io/shiny-milestone</id><content type="html" xml:base="https://phisanti.github.io/shiny-milestone/"><![CDATA[<p>Ah, the sweet satisfaction of completing a (mini) project. Today, I publish my first R Shiny app. It is just a simple tool to automate data analysis for my colleagues and it took me  half a day, so I’m quite proud. But let’s be honest, working with Shiny is sometimes a walk in the park. In fact, at times, it can feel like you’re walking through a dense forest with no clear path in sight.
Don’t get me wrong, I love R, and I think it’s an incredibly powerful language. But when it comes to Shiny, things can get convoluted. What could be achieved with a few lines of code in other languages often requires function after function after function in Shiny. And don’t even get me started on the need to wrap everything in reactive() functions. It’s like trying to find your way through a labyrinth while blindfolded.
However, despite these frustrations, I must admit that the end result is worth it. Shiny is incredibly powerful and can do amazing things. It just takes a little extra effort and patience to get there. Just take a look at the picture:</p>

<p><img src="/images/clariostar_tool.png" alt="projec_image" /></p>

<p>If you’re struggling with Shiny, know that you’re not alone. It’s a tricky beast to tame, but with a bit of perseverance, you’ll get there. And when you finally do, the feeling of accomplishment is like no other.
So, to all the Shiny users, keep pushing through the frustration. The end result is worth it. And to our friend who just published their first Shiny app, congratulations! We’re all cheering you on and can’t wait to see what other amazing things you’ll create in the future.
For those who want to check out our friend’s shiny app and repo, be sure to follow the link they provided. Who knows, maybe it will inspire you to create your own Shiny masterpiece.
Link to the app <a href="https://phisanti.shinyapps.io/elisa_analiser/">here</a>
Link to the repo <a href="https://github.com/phisanti/elisa_analiser">here</a></p>

<p>PS, Also, I am working in a bigger Shiny project, so stary tune for the news.</p>]]></content><author><name>Santiago Cano-Muniz</name><email>santiago.e.canomuiz@gmail.com</email></author><summary type="html"><![CDATA[Ah, the sweet satisfaction of completing a (mini) project. Today, I publish]]></summary></entry></feed>