<?xml version='1.0' encoding='utf-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us"><title>Hessam's blog</title><link href="https://hessammehr.github.io/blog/" rel="alternate" /><link href="https://hessammehr.github.io/feed.xml" rel="self" /><id>https://hessammehr.github.io/</id><author><name>Hessam Mehr</name></author><updated>2026-07-16T00:00:00+00:00</updated><entry><title>Ancestral sampling with variational relaxation</title><link href="https://hessammehr.github.io/blog/posts/2026-07-16-ancestral-sampling-with-variational-relaxation.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-07-16-ancestral-sampling-with-variational-relaxation.html</id><published>2026-07-16T00:00:00+00:00</published><updated>2026-07-16T00:00:00+00:00</updated><content type="html">&lt;p&gt;This post is an interactive HTML document. &lt;a href="https://hessammehr.github.io/blog/posts/2026-07-16-ancestral-sampling-with-variational-relaxation.html"&gt;Read it on Hessam's blog.&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>Exploration: Flow matching, NeuTraHMC and normalizing flow guides in NumPyro</title><link href="https://hessammehr.github.io/blog/posts/2026-05-06-flows.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-05-06-flows.html</id><published>2026-05-06T00:00:00+00:00</published><updated>2026-05-06T00:00:00+00:00</updated><content type="html">&lt;p&gt;This post is an interactive HTML document. &lt;a href="https://hessammehr.github.io/blog/posts/2026-05-06-flows.html"&gt;Read it on Hessam's blog.&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>First bee sighting of 2026</title><link href="https://hessammehr.github.io/blog/posts/2026-02-25-first-bee-of-2026.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-02-25-first-bee-of-2026.html</id><published>2026-02-25T00:00:00+00:00</published><updated>2026-02-25T00:00:00+00:00</updated><content type="html">&lt;h1 id="first-bee-sighting-of-2026"&gt;First bee sighting of 2026&lt;/h1&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/first-bee-2026.jpeg" class="hero"
alt="Honey bee on Waterloo bridge" /&gt;
&lt;figcaption aria-hidden="true"&gt;Honey bee on Waterloo bridge&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I was crossing the Waterloo bridge to go to a seminar when I saw my
first bee of 2026. Most years this would be a big fuzzy bumble bee but
this time it’s a honey bee. What a wonderful little friend to see; it
made my day. :)&lt;/p&gt;
</content></entry><entry><title>Nice strange attractor visualisation</title><link href="https://hessammehr.github.io/blog/posts/2026-02-24-attractor.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-02-24-attractor.html</id><published>2026-02-24T00:00:00+00:00</published><updated>2026-02-24T00:00:00+00:00</updated><content type="html">&lt;h1 id="nice-strange-attractor-visualisation"&gt;Nice strange attractor
visualisation&lt;/h1&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/attractor.png" class="hero"
alt="Strange attractor visualisation using THREE.js" /&gt;
&lt;figcaption aria-hidden="true"&gt;Strange attractor visualisation using
THREE.js&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;a href="https://zensical.org/"&gt;Zensical&lt;/a&gt; is a new static site
generator implemented in Python that I heard mentioned on the &lt;em&gt;Talk
Python to Me&lt;/em&gt; podcast. The website features a stunning mathematical
visualisation that I was able to extract into a GitHub pages &lt;a
href="https://hessammehr.github.io/attractor"&gt;website&lt;/a&gt;, using Claude
Opus 4.6 (low) via &lt;a href="https://github.com/badlogic/pi-mono"&gt;Pi&lt;/a&gt;.
The parameters involved in the differential equation can be modified and
the default flythrough effect can be interrupted to allow
zooming/panning/rotating to explore the geometry.&lt;/p&gt;
</content></entry><entry><title>Draft: Evolving interactions with AI in 2026</title><link href="https://hessammehr.github.io/blog/posts/2026-01-19-ai-in-2026.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-01-19-ai-in-2026.html</id><published>2026-01-19T00:00:00+00:00</published><updated>2026-01-19T00:00:00+00:00</updated><content type="html">&lt;h1 id="draft-evolving-interactions-with-ai-in-2026"&gt;Draft: Evolving
interactions with AI in 2026&lt;/h1&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/hessam-chatgpt.png" class="hero"
alt="My 2025 year in summary according to ChatGPT" /&gt;
&lt;figcaption aria-hidden="true"&gt;My 2025 year in summary according to
ChatGPT&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Living through the first couple of weeks of 2026 it is clear, whether
I like it or not, that it will be yet another year dominated by AI. I
have to be honest, looking back, this growing presence is something I’ve
had mixed feelings about, having gone through the usual cycles of
excitement, epiphany, frustration and despair that countless of us seem
to have experienced. It’s been a technical and emotional rollercoaster
like discovering, falling in love with, then good old times&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Up #1:&lt;/strong&gt; Before GitHub Copilot, there was GitHub
Codex, the predictive autocomplete for VS Code. This was great for those
obscure pandas incantations that never seemed to make it into muscle
memory. A function signature, maybe a comment or the beginning of the
line &lt;code&gt;combined_df =&lt;/code&gt; and more likely than not the right
completion would show up as ghost text.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Down #1:&lt;/strong&gt; Over the Christmas break of 2023, I set
myself two challenges: use SQL to reason about molecular assembly; and
write a Raspberry Pi camera image acquisition GUI to make life easier
for my students who had to use the command line to capture images and a
text file for documents. With the camera challenge specially Codex
seemed completely clueless. This was around the &lt;code&gt;raspicam&lt;/code&gt; to
&lt;code&gt;libcamera&lt;/code&gt; transition and there just didn’t seem to be a way
to prompt it to use the more modern stack (in fairness to Codex, there
was probably next to nothing in its training data about this).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Up #2:&lt;/strong&gt; In 2024, when GitHub Copilot’s &lt;em&gt;Edit&lt;/em&gt;
mode popped up in my editor one day, there was a feeling of immense
power as I prompted Claude Sonnet 3.5 to create &lt;a
href="https://github.com/MehrResearch/microbit_sensornet"&gt;interactive
web UIs&lt;/a&gt; that I could never dream of having the time to learn coding
on my own. I even prototyped an &lt;a
href="https://github.com/MehrResearch/Delphi2"&gt;IDE for probabilistic
modelling&lt;/a&gt;, even though I have little frontend coding experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Down #2:&lt;/strong&gt; Then the code (and dead code) started to
grow, I lost track of how and where things were implemented, what was
still in use vs left over from the last prompt. You had to be sure that
the right source files were added to context or the model would
ignore/reimplement existing functionality, blissfully unaware that it’s
right there. Things worked, but not quite the way I wanted them to, and
prompting the model to go from working to correct seemed almost as hard
as implementing them myself, since I was no longer able to wrap my head
around all the react code and API endpoints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Up #3:&lt;/strong&gt; The first taste of Claude Code and the
incredible feeling of power, entire projects written from scratch,
handling git features that I didn’t even know about (worktrees).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Down #3:&lt;/strong&gt; With more difficult challenges Claude’s
tendency to hack its way to success became annoyingly apparent. It would
move goalposts, keep celebrating trivial partial successes, growing the
number of celebratory emojis used to hype up what it’s achieved whilst
in reality it had made no progress, running around making a mess until
it would run out of tokens. My last experiment was &lt;em&gt;autoclaude&lt;/em&gt;,
a script that would call &lt;code&gt;claude -p&lt;/code&gt; with a set of
instructions that included how to keep track of progress and gave it the
task of implementing a &lt;a
href="https://github.com/hessammehr/nucms"&gt;WordPress-like content
management system&lt;/a&gt; with a Python rather than PHP backend. Sonnet 3.7
running some 80 “awakenings” made some good initial progress but it was
hard to keep it on track as it kept marking things done that weren’t
really done at all.&lt;/p&gt;
</content></entry><entry><title>Draft: Thinking about aerosols probabilistically — Part 1: Evaporation</title><link href="https://hessammehr.github.io/blog/posts/2026-01-16-thinking-about-aerosols-probabilistically-part-1.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-01-16-thinking-about-aerosols-probabilistically-part-1.html</id><published>2026-01-16T00:00:00+00:00</published><updated>2026-01-16T00:00:00+00:00</updated><content type="html">&lt;p&gt;This post is an interactive HTML document. &lt;a href="https://hessammehr.github.io/blog/posts/2026-01-16-thinking-about-aerosols-probabilistically-part-1.html"&gt;Read it on Hessam's blog.&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>A little snippet for nicer plots using matplotlib</title><link href="https://hessammehr.github.io/blog/posts/2026-01-09-nicer-plots.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2026-01-09-nicer-plots.html</id><published>2026-01-09T00:00:00+00:00</published><updated>2026-01-09T00:00:00+00:00</updated><content type="html">&lt;h1 id="a-little-snippet-for-nicer-plots-using-matplotlib"&gt;A little
snippet for nicer plots using matplotlib&lt;/h1&gt;
&lt;p&gt;If you’ve seen previous blog posts (or read any of our &lt;a
href="https://scholar.google.com/citations?user=HeyhCHEAAAAJ"&gt;group
papers&lt;/a&gt;) you may have noticed the plots have a consistent look.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import seaborn as sns
from matplotlib import pyplot as plt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default matplotlib look&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.plot([1,2,5,3])&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[&amp;lt;matplotlib.lines.Line2D at 0x11530e960&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2026-01-09-nicer-plots_files/2026-01-09-nicer-plots_3_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not bad but try the following now&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;from matplotlib_inline.backend_inline import set_matplotlib_formats
set_matplotlib_formats(&amp;#39;svg&amp;#39;)

sns.set_theme(&amp;#39;talk&amp;#39;, &amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;, font_scale=1.0, rc={&amp;#39;svg.fonttype&amp;#39;: &amp;#39;none&amp;#39;})&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.plot([1,2,5,3])&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[&amp;lt;matplotlib.lines.Line2D at 0x1154e8ce0&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2026-01-09-nicer-plots_files/2026-01-09-nicer-plots_6_1.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;An added perk is that the plot is now embedded in your notebook as
SVG so if you export to Markdown/HTML they will stay nice and crisp.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; If you’re a marimo user (and if you’re
reading this you &lt;em&gt;have to&lt;/em&gt; try it), it’s even easier:&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import seaborn as sns

sns.set_theme(
    context=&amp;quot;notebook&amp;quot;,
    style=&amp;quot;ticks&amp;quot;,
    font=&amp;quot;Inter&amp;quot;,
    rc={&amp;quot;svg.fonttype&amp;quot;: &amp;quot;none&amp;quot;, &amp;quot;savefig.format&amp;quot;: &amp;quot;svg&amp;quot;},
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has the unintended consequence of making all figures fill up the
available space. There’s a little trick to keep things just the right
size:&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import marimo as mo

def fig(f):
    return mo.as_html(f).style({&amp;quot;width&amp;quot;: &amp;quot;max-content&amp;quot;, &amp;quot;display&amp;quot;: &amp;quot;block&amp;quot;})

fig(plt.gcf())&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a Jupyter notebook so I can’t show you the output but trust
me, it looks good.&lt;/p&gt;
</content></entry><entry><title>3D-printed mathematical lampshades</title><link href="https://hessammehr.github.io/blog/posts/2025-12-24-maths-to-lampshade.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-12-24-maths-to-lampshade.html</id><published>2025-12-24T00:00:00+00:00</published><updated>2025-12-24T00:00:00+00:00</updated><content type="html">&lt;h1 id="d-printed-mathematical-lampshades"&gt;3D-printed mathematical
lampshades&lt;/h1&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/lampshade-printed.jpeg" class="hero"
alt="Finished lampshade" /&gt;
&lt;figcaption aria-hidden="true"&gt;Finished lampshade&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I’ve been playing with translucent PLA for lighting projects at home.
Earlier this year, I built a diffuser/housing for our Lidl LED strip,
since the light from the exposed LEDs was too cool and jarring. The
design files are on &lt;a
href="https://cad.onshape.com/documents/e51f793089189f80c2f7aa1f/w/94e173d3c5332d1ee7327e7a/e/50d37d0328225b89bb78e092?renderMode=0&amp;amp;uiState=697a5c68c73367c11cdd50ff"&gt;OnShape&lt;/a&gt;,
although they might need tweaking for the exact strip and required
installation area.&lt;/p&gt;
&lt;div class="gallery"&gt;
&lt;p&gt;&lt;img src="https://hessammehr.github.io/blog/images/livarno-led.png" alt="alt text" /&gt; &lt;img
src="https://hessammehr.github.io/blog/images/rail-design.png" alt="alt text" /&gt; &lt;img
src="https://hessammehr.github.io/blog/images/kitchen-lights.jpeg" alt="alt text" /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;A few viral videos have recently popped up in my YouTube feed showing
bespoke 3D-printed lampshades. These ondulating membranes struck me as a
surface that would arise from sweeping 2D profile as it’s raised and
rotated through space. Making one from scratch seemed like a fun
challenge (not to mention a nice Christmas present!).&lt;/p&gt;
&lt;h2 id="d-profile"&gt;2D profile&lt;/h2&gt;
&lt;p&gt;I started by graphing the following equation in &lt;a
href="https://www.desmos.com/calculator/ramwof22yn"&gt;Desmos&lt;/a&gt; (link
will take you to the formula).&lt;/p&gt;
&lt;p&gt;&lt;span
class="math display"&gt;&lt;em&gt;r&lt;/em&gt; = cos (5&lt;em&gt;θ&lt;/em&gt;) + 0.2cos (9&lt;em&gt;θ&lt;/em&gt;) + 0.05cos (200&lt;em&gt;θ&lt;/em&gt;) + 4&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Something like this with big gentle ondulations on top of which small
jittery ridges are overlapped.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/graph.png" alt="alt text" /&gt;
&lt;figcaption aria-hidden="true"&gt;alt text&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="trying-and-failing-3d-with-onshape"&gt;Trying (and failing) 3D with
OnShape&lt;/h2&gt;
&lt;p&gt;Desmos helpfully allows export to SVG, which I then converted to DXF
in Inkscape so it can be imported into OnShape. DXF files can be added
to OnShape sketches directly. So far so good. Creating the loft object
in OnShape proved challenging though. A loft is pretty much what I
described earlier, the volume or surface created as one 2D profile is
interpolated to another. In this case, the second profile is just a
point placed above the initial one. Something like this:&lt;/p&gt;
&lt;div class="gallery"&gt;
&lt;p&gt;&lt;img src="https://hessammehr.github.io/blog/images/profiles.png" alt="alt text" /&gt;&lt;img
src="https://hessammehr.github.io/blog/images/onshape-loft.png" alt="alt text" /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The loft tool provide a few nice settings, specifically the initial
and final angle of approach to each profile. I want the shape to rise
straight from the 2D contour and form a tangent to the final point. This
way the top will form a nice dome rather than a sharp spike.&lt;/p&gt;
&lt;p&gt;This complex organic-looking profile, made up of 1047 points, doesn’t
really seem to fit OnShape’s main usecase. The loft operation kept
failing to process (although it looks nice) and froze a couple of times
too.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/onshape-error.png" alt="alt text" /&gt;
&lt;figcaption aria-hidden="true"&gt;alt text&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At this point, I had another idea. Since the point coordinates are
known, it shouldn’t be too hard to create the shape in Python. This is
not something I had done before, so I asked ChatGPT for advice. It gave
me a script, which I was able to modify (&lt;a
href="https://github.com/hessammehr/lampshades"&gt;repo here&lt;/a&gt;) to create
the transition to a single point at the top (essentially scaling the
profile depending on height). The results is a massive 70+MB STL file,
which is currently printing …&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/images/lampshade-printing.jpeg"
alt="Lampshade being 3D printed" /&gt;
&lt;figcaption aria-hidden="true"&gt;Lampshade being 3D printed&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Update: &lt;/strong&gt; I’ve converted the Jupyter notebook to
marimo so it’s easier to visualise the cross section and profile of the
generated lampshade. You don’t even need to install Python to run this
code, it will run directly in the browser using WebAssembly (&lt;a
href="https://marimo.app/#code/JYWwDg9gTgLgBCAhlUEBQaD6mDmBTAOzykRjwBNMB3YGACzgF44AiABgDoBGADg4BYWaRGDBMEyVBwCCogBQBKDGgACIsBwDGeADY605PADM4mRQC40ca3FCRYcKHis270eAQCu4AJ5xEAM5wBGAu1m4OAB4gOhx4ME54HACiOngghDAAKon+QclZYXBGUBAgcAF0Iro+HPhl8VB+EfAAChA6PjgQBEUtcAmgeJV94O7B8VTQANaRo-bwAZrAYD7z4wF4iABG0AR5FQQBGDbFpeVIMGA6EDA6wNu2Yw6r17cH1zDrDkgoIBAHf4nGwBI4cTYwTD0dJ4OQsGCIHTTFgAGlYMGAmmmAVRxR6MEYLGkKERuKM+MwS0ReEYXA4bDRUE0jAA3iwAgA3HAcckEGAwHxgPAscysAg9YUAXyUpycME8UH2BTR7U63QIaP+aJCaM+jLwaMGGRGGDUoi0un0hhMZi1wTAup0MAUllO1sccmhCJdRVOAGJHIAQAkYgc0EACgDACOQAVkAHcAKQCgBABqBOcABMYcjcgAnPHk6n6dHM1G02w2HmU-xfTY5Qr9iEtOGY3AAFQDOjxRAKOBJuDp1v2xsBHMDr1dnt9wsDhuZuSltijjveif8enKN3GOBgUpGYBpOTkH2nU4sABK8TrQWhFU01OKiE0MGgeKgcB0iB8xDgVA7Tjg5FsIJxSgJB7k2ADyGAAIEQIbQBgBJ8xAgExKmqDghGPGsL0VQcAgAR1gA9uzbOR+17BsdmHMjJ2jacNACYACAPOiODAYAFA49cbDHIJmAokgCHwUi0TTFi2LRTg2C4GUbHIXjB3uI4wAfWEGTgLg0S4MsZOsKB5KgT0l0CHS4EifSgjbGcmx4kyfHMliGKYmyikiOzxAbaCH2mOQAG0zLROyAF00UQSIoMYABaaSiiMNE5EQDT-DTbtmE+cFPG2N4YGHWCOgCRg0zRXccAYgAvGkczRfhONOBLWJuGA5H8uA7JMxA03q24DwCXUdz3WE5Jqmx-g4cBYkYsgSEfYAOVhNKcE0IxFBM2scO3ZD+rRVzjlNdQLT0AxNzMZV7X1I9ZM3G5EEoTkcEwZT6HuiAJuHW77tIOhRWgqBuwigA+QcCHIZASB8V1MNYFgMIh5JIgSB9svbPBimAPSPB6CL0jAAU4AAHgehhyEJDgSZYAHECBxxsKOfx9jkAA5FFkv8KBQeraxkLgAANABNLdnr5IJGLgABlAA1ABxOBPE2V9PAIWgAg4dm4GkAIAm8YYka3D7AP8HQnGuvxIKWOUoIoOA5AaoJOYAGTgMMQCQIGAgUNEf0xBhLk0Dsgh8CAFRRtJlYhyHocw0p3mYApWOQTY5DegmFDqRoIC6pQVYA5h6YlFXyVfXRbH2SOYA4WhiAsFXTmAExdA4BFuQCa5aDhSUWAUHyosCphmBYAmRSriHb0p5g69IQZthTxqWHIXEoZM0Pq5MNImOHw84ABthwcXxes4dinyEHnftkN6YVZr-89Zzoht8XkhzbgMXEU8PBklZ6A4Rz4IMaxnH8d1omAByEmHBAEA3JPLACwtrziwluhDOocAyw3ho+fWOhgjeG2MQAIABubWu40h6xYAAWVMi1OA9tIjkMoeQkBuIAgAi8BkFAmgKgJC2OUKCpkGQ+AZJEDSPgNIgJVkw+SYFGpIDAHIIwV0YD6h5IxYGeg5BQBYJ3JMgUAD8AAdcgLZtEcF0UmAAPuorRRjcSHiGphC+K85CiO7AAUjgKJAAhMwLeR9HCIAfk-HQL836lAMiwAA8uQACTCsGvk5mGaAkECCkC1ngOGU0yAARKGUUWksdb0HgSrbG8l+Kg3sd4Hq-4BRCkYDIiApBk5OFQkKOQUURIIIhgGc8-xZoOx6JsTQngMRdPIJ4Zut4yBBAPlTTpyNkkjNoA7G4jkcD8wmrYEw25hiZEzjXIwhSNCKURNyYCIB7EaEgkYJaBSQphXymwN2-hrm0gXjYaYeA8BiD4hoOgnksS+R8jkF+wV-zbKCADLgeAIrZkCk86wBTxAFJ8i8t5gVz4mAbPsnQhzoDHPhWwbuEUtzZU7lwKFeN1LguzLfUOsLUqEvMF3YEEdqYEuOLKamygzQaG0Add0ZhVRdB6NqB0W4+ppENH8YYdAtqtVvu6SAaoeiYGQFsTA2g+SlGAOQOQBTRQNiBiDD8v0AbymuHgHy1TSCCo4Hq1mH5AqUpsFQmlAQfLmAksi0OfgnUus0u6iG-D3IaFKMoyIaIorQpalwANHAg06DkD4UN0VQ6IHEJwWill6LeCagOQRcB8X+rbD4cNmhHWWzpAuAA9JbAAbPSAcXZiK4UzVm3s-CG1ZoLZGvNkaC3WOPJoT1pba2VrkDWhcbZ60OSbX4Xsgi21UI7bm0y3aWq9tZfKHCiBLX6p8L5YtaJ+2AvIBUmk5rnRcWsO6epiAxp4BVQsigT1OiKTmtlHVGhrWg21KKCahrAbAxtWDFWUNw7HgAKoK3zuUZAmgV44HoFTAI17m6CTgJzZNMHwwWzlT4Z9IcIbastozZmNB6AB3gEMkZiSAKEAApACaeHMLnnXTTexTNk5AahirAMmwcAZD5G+QgcGvkqx41G9FmKQInI4GcpaDYOTfO8j5S5zKfK4qhVc8KtyNP5UTRDXZHAvkIh+T5VT9JAUzlKZmnjCgoUqyfAidBzBnX0tDgiKA+B4AfI4IpJuKlSL0kNLcRE2oJzSRdfSrjRdJpysSfrHoSzrzYefSrMjUbf47tYy4u5R7BQntkeGgAVim-J2U72Yb3nJhTvllM4sCrZ0O+dbCGiLnAQgmsSBkE9MgDz50d4e0IeKeACcfIFe7rjZg8BcYVBG6FqF9qd5FaTMwLgXil5wCK39ZgdjrPzZ3jYIrW3CAJzwDgX66lVsQxPlsM+e3pbiE9Iu51o3uzDqe7Nx7I3u69jBVFaM4bThkZ8sAbuzA5Cdulg2gpZXwKfYnJ4AcUOMMw8W+pX1jLmOof6ee-8m5SgIjIJgVyTUwZ-u3YaIyopT2-t1f+0Gu3NBon07OMcdyPKMUMp2cNp4o3bt+Qz3NARAU+TKZoOraIcuVKpyrVa+xXJwBUHAU8HBCgMpxyYGANBoIPsqJaTAxo6ByBS-04ghOSc07J+fAgRAoCm7fVa2nH44DGLgNfA0KsOzABwHQSETtKeyJRCIzA75Px6W-XyAPrmgs6ChJryEJByB+5qXIoov6jQSuV+Kyou3gMqwloQYgiTxnfkSbAT33uEASt2-isjX4NZQCMCpL6RmXkATx1RxwjESrdKt3gR8Ftth+Dwp4a6QRPQkkEp4d8aTw3V+xsAHoiIi7W4qAqBvcEVF4FmnpZG0BgCZFIPPgg3YJnQVoD7C2fxxnwEQuW3Y-Iyiq4B8bm3cuJmMWt6blmyMGZsZZqDMpdg9wywdwfgSOFA+6mQTgAEOwqWwktyDGx4OemcuOnemAW+GI2gw460BCt6rkduH6BquaAM5uAGu2pw9McKIquBtQDSpquKXipCfEQeH42CXipU+mPmyk2g-makHuXuPuIAaIxC-23ERkHBjEvm3BkkgWDmMeUEce10QhIh1g6B+m6WcgcgpCbYjMcAAAzNlselUvll4o1sAHFKVOTt2MLG1swoknIKVCsPYWUjZL1rdkCp5LBMjKDqQvisAIutJHAMOj4epMoceK5JgHjuIG3gTkTtgf1KbpYRdiocQISn4doXAKKHIH4d9g2joVvGmCDqZD4BEe8G2HEfuCbDBNoKEacKoUDgOBQZkdkSEQ0SJIUaVF4jLnAKoY-hdChBqremvsMAzGHnInAMQqMWiMhDsvEJMcUPcGAKKLsB0L+uIj5GsRNHVmQTYEMfJD5GjovKYS1gJEJMEdJNsZhNsIEHgAuMwNMRCBOGkS7kkXAFcZsJGncecg8b2FkaFrkS8Y1kVsLCcbCPTK4W4ftvsKDijgEc4vTC8bVOIG8TcROAVgiTYI8MwMiQuL2AVr0BCZhKwlidcZGrifiQSW6EiSSaieieEKiu8DIisBcW4bsRwOoDRr5Juq8fuvVhSRDKyeyUDJyfuuLryXya1joJsMybdgGE4FvpsN+Iop3rSceAKaIByT5FyfztsGKeKTsSpErIKZqpqeLjyaEV0bsb0RzM-p-l5oELzjXi-vGuUrlkYUnuGhQXcTaa5OCFUEKKpgcacIwcEMwSHjtKHKoZgI6VEagegZiMMY6abuGrseIAxIYJgLsQzIwDocQowMQlMV8fEIwGpIyWAIwAAGKIibDhqqHiCRmOlWmrJL4m5y5cKDYu4SjMnv4tkDoUSIYAZZE95Oni6GFS63YXzdlOm+nVABlwDuIu7Sk1g+IKl+IBLvzBIACSQ5tMAE0Z5RWsIAMs8AVQXS14iGGQyy-GYY8s8AiARgk0CGSGik3I7cXikZk5MZgkaBKR8Zw4k5SZjZAOhZkIH5h2TEqhoRtZXm8mzevkqhaI75Q5upDWBpPYTmAxGZBpWZOZeZBZMxBI9x8QmAk5RUCxjA-yeArSO83GGIMAPsrxtwT45QfwlspUxZLomOD5KAKGC4T4zZr43FSybAJhz4QJxcFMQkYJi51geJ92MJTizxFJEAtx62LxEAHx625JEJwAKlhFIF25uJLxwAGlelxFBlmlAJBpbJ6pQpPkylaIxlDluKNR+pmB1lQotl9lqGiUxltmXiNFZ+DAiEHe5Q9hjAAAEhxdGYJeMVFPBPxR3ihsQlFF4ohFGc-vdmcf8bdmlaBahsBWZcvj8VlQ0SJa+GJd4oJKCeCXtrJdCX8XAHCWpSpWldGYZUpRpa1RlWSUZS1RAGAIVV+O1QScZeILleZXiZZW5Uab5F5TpQ5cSi5dYGqR5caXNYlOpX5aHPrmNZnnQBnkMJUHIHGZgYwPBfeKdUUh+NIgaSOa6T+r1BAJgflJWZKZRSrPrqNMQPgN+aXpgYoB9enruJEJgEclWQDdtenutM9cdYiBqokuRVAC-OGl0frtju6EgOzjVdYInLrL3IYAEP8AEBFDgCQGAPtbdEIJFhFDTdLLLDrCQBkJNEEDTRFO7ngGXgIeIPOGuJHrIRrvIVCJeCmtwIHsHtguIFpAuHAAGIxJoIbAqY1oTWnNCK+ALdBHgoYPLVsIrc+EYHgFQF+IMBJWkCyhDAQPdFQU9C9OINGGWNYAGGGGqnlB3qIIQruIYPcDjBMsbYJIQteXyHZt1kRYmZBNevECbk7NzaWLWjLTeHeDAtknxfQFwiEgACIqxUCIjR4p1YhEDqx67lDLYcCiRx0PGu4vj+AVCd6EJ15DEqwFYCyUgCiELMCl0BhcCMClCQIiSMAgC0BOBoi6GMBYKzToJyAix+k1BUXHgBis1vg1IAQABkDskByM+5uaNNKsSqWpkBz0e82G6oiqhsKqe9GqxOrsJW8k20i6qqCQ+9jZSCRwCoO+GVh9PQesAAwp-QAOqV1O0MRa78Zg2Skopf7JpTaeK3bUoqbmB0rEqNirAA3U201UiELJ3B3kbADh0Pms3S5RpICRBSbiZWpYparZTaaPLhpoNeGWypqjiYPpWTSYBh1M2R0gAvaOBX2thOa3hpCP2b34pXo3rxVO333oJ0ZXkBz8Z4OhzRnMDCMmrQ4PpJaMQvplIW37nW2Cwz3VzbnZy5yhwXxZ16BQh0CYjTD50BCF16ztkV0TImM53mN53DDWNR2bzMnYbiB8rqhyCOmhEBiT3VCdCvGeDnJfgkYMBEA4AH6DLyEUxwTa2F60xFxZ1QC7nAUIFGNDmPoDrYYcDbBhP60GQRSONmMWNWOF1oiN2MTN04Y0g1MW3QT1O6MQwTk5P5NQSYDpbSXeK+LPyvzrlwhblpMZP4XdJ6AiDgTaz7l4JQGeBwRlO52WOuM2PPgMRjQ1x+DXhka4avn+VwAbkmCFPhMCXUxBCHlOgrCELv1HCGiIAvLazvjubDDwAD7gNZNtMmD-n5P1AgBQi5Y9ysDEKT4Yg+M9ADwUk-MdADqEODkf6-N4BlBlIvI+CMDvggDbDAxwA4Cijcg72tM2I5OxLpP6a87QudBxBwzEDz5QCNhxLOrwOHqjn5YO3-ilBiAUZAHt5I4v0HOKOEIfl8XewMCSPwAB1fDjn6OPk3rKOUCqNEDwsm4ktyTaiW0bRpDaPZSEvwT82x4RHXTc21rpqsRPH2aIhyHQRC2Khm2YQ7UTax7a4dimP66G63aJmuSMCOkR57b-meskVeJ8He6F2MBBsCE+s7wW3i16SMBRssF6QRuLzms536vx6MDJuWsKHkCJunA6vbgTRwg-0l4Yj8HmC4ifVdNZ2TQlve7hr5t8hwg5DYN+3DBltoh2KfW7GrpYQY5o1rp1iWyY0agZyqB7RcpWhHRyBDuagQCuH2sSBY1FDbAwBQkIAQAcCeDAAFP9JPhMTvhYI6CEgiyIBdIixZC2y4gKowYWPovXpYvJqYCiifUzLuBwgYtgCoSGDgh3DtwrRMrLsagV4mijvmjjuHQ2hyAAeagSoztzvrswVeS+SfWVAQBUCKBogAe6ky7spjuWjgemCVz9uKjKAXzYAJIZDYBAssDYBDvYCQs2B7RI1MRKBAA"&gt;link&lt;/a&gt;).&lt;/p&gt;
</content></entry><entry><title>Using `uv` projects on a network share</title><link href="https://hessammehr.github.io/blog/posts/2025-12-17-uv-on-network-shares.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-12-17-uv-on-network-shares.html</id><published>2025-12-17T00:00:00+00:00</published><updated>2025-12-17T00:00:00+00:00</updated><content type="html">&lt;h1 id="using-uv-projects-on-a-network-share"&gt;Using &lt;code&gt;uv&lt;/code&gt;
projects on a network share&lt;/h1&gt;
&lt;p&gt;The wondeful &lt;a href="https://astral.sh/uv"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; has a
strong preference for virtual environments that are placed within a
project tree rather than kept centrally &lt;em&gt;a la&lt;/em&gt; conda. One
unfortunate consequence of this is for projects stored on network
shares, for us usually data analysis scripts and Jupyter/marimo
notebooks for visualisation and as dashboards. Simply
&lt;code&gt;uv add&lt;/code&gt;ing &lt;code&gt;numpy&lt;/code&gt; alone will dump heaps of tiny
files into the folder and completely overwhelm our network share
(normally OneDrive since we get 1 TB free as part of 365).&lt;/p&gt;
&lt;p&gt;A little trick to avoid this situation, while still being able to use
&lt;code&gt;pyproject.toml&lt;/code&gt; to specify dependencies via &lt;code&gt;uv&lt;/code&gt;.
Here it goes.&lt;/p&gt;
&lt;ol type="1"&gt;
&lt;li&gt;&lt;code&gt;uv init --bare&lt;/code&gt; to initialise your
&lt;code&gt;pyproject.toml&lt;/code&gt; if it doesn’t already exist.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uv add --no-sync &amp;lt;dependencies&amp;gt;&lt;/code&gt;, the key is
&lt;code&gt;--no-sync&lt;/code&gt; so &lt;code&gt;uv&lt;/code&gt; doesn’t make a
&lt;code&gt;.venv&lt;/code&gt; folder and install packages there.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx --with-requirements pyproject.toml &amp;lt;program that you want to run&amp;gt;&lt;/code&gt;.
Here are a few examples:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;uvx --with-requirements pyproject.toml marimo edit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx --with-requirements pyproject.toml jupyter lab&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx --with-requirements pyproject.toml ipython&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx --with-requirements pyproject.toml python script.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You might be tempted to run &lt;code&gt;uv run script.py&lt;/code&gt;. Don’t!
That’s going to create a virtual environment in the project folder.
Either use the last example above or the nifty &lt;code&gt;--isolated&lt;/code&gt;
flag: &lt;code&gt;uv run --isolated script.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And just like that, the most frustration-free Python workflow that I
am aware of.&lt;/p&gt;
</content></entry><entry><title>Reparameterizing distributions in numpyro</title><link href="https://hessammehr.github.io/blog/posts/2025-11-26-reparam.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-11-26-reparam.html</id><published>2025-11-26T00:00:00+00:00</published><updated>2025-11-26T00:00:00+00:00</updated><content type="html">&lt;h1 id="reparameterizing-distributions-in-numpyro"&gt;Reparameterizing
distributions in numpyro&lt;/h1&gt;
&lt;p&gt;Every single time I’ve tried to “chance” it with probabilistic
methods I am once again reminded that you can’t treat them like a black
box. The following is a self-contained example of a pathological case so
simple it almost feels like it should just work, and yet it fails
miserably without intervention (in this case by reparameterizing the
distributions). Part of me wonders if this all just means we need better
tools or whether we just have to accept the fundamental complexity and
model defensively.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import numpy as np
import pandas as pd
import seaborn as sns
from jax import lax
from jax import numpy as jnp
from jax.random import PRNGKey
from matplotlib import pyplot as plt
from numpyro import deterministic, handlers, sample
from numpyro import distributions as dist
from numpyro.infer import MCMC, NUTS, SVI, Trace_ELBO, autoguide, reparam
from numpyro.optim import Adam
from matplotlib_inline.backend_inline import set_matplotlib_formats

set_matplotlib_formats(&amp;#39;svg&amp;#39;)

sns.set_theme(&amp;#39;talk&amp;#39;, &amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;, font_scale=1.0, rc={&amp;#39;svg.fonttype&amp;#39;: &amp;#39;none&amp;#39;})&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="problem-setting"&gt;Problem setting&lt;/h2&gt;
&lt;p&gt;Consider a catalyzed reaction &lt;span class="math inline"&gt;$A
\xrightarrow{\mathrm{cat}} \cdots$&lt;/span&gt; where the catalyst itself is
slowly undergoing decomposition &lt;span
class="math inline"&gt;cat → ⋯&lt;/span&gt;. Initial concentrations &lt;span
class="math inline"&gt;[&lt;em&gt;A&lt;/em&gt;]&lt;sub&gt;0&lt;/sub&gt;&lt;/span&gt; and&lt;/p&gt;
&lt;p&gt;&lt;span class="math inline"&gt;$\frac{d [A]}{d t} =
-k[A][\mathrm{cat}]$&lt;/span&gt;&lt;br /&gt;
&lt;span class="math inline"&gt;$\frac{d [\mathrm{cat}]}{d t} =
-k_d[\mathrm{cat}]$&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;A couple of simple helper functions to integrate these differential
equations.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def solve(a_0, k, cats, dt):
    return lax.scan(lambda a_n, cat: (a_n - dt * k * cat * a_n, a_n - dt * k * cat * a_n), a_0, cats)[1]

def solve_cat(cat_0, k_d, dt, n):
    return lax.scan(lambda cat_n, _: (cat_n - dt * k_d * cat_n, cat_n - dt * k_d * cat_n), cat_0, length=n)[1]
    

plt.plot(solve(1.0, 1.0, jnp.ones((1000,)), 0.01))
plt.plot(solve_cat(1.0, 1.0, 0.02, 1000))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[&amp;lt;matplotlib.lines.Line2D at 0x1185b9f40&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_3_1.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now a simple model, first without worrying about measurement error.
We’ll use both MCMC and SVI.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model(a_0, cat_0, dt, n):
    k = sample(&amp;#39;k&amp;#39;, dist.LogNormal(0.0, 1.0))
    k_d = sample(&amp;#39;k_d&amp;#39;, dist.LogNormal(-2.0, 1.0))
    cats = deterministic(&amp;#39;cat&amp;#39;, solve_cat(cat_0, k_d, dt, n))
    a_n = deterministic(&amp;#39;a_n&amp;#39;, solve(a_0, k, cats, dt))

args = (1.0, 0.1, 0.01, 500)
mcmc = MCMC(NUTS(model), num_warmup=1000, num_samples=100)
mcmc.run(PRNGKey(0), *args)
mcmc_samples = mcmc.get_samples()

guide = autoguide.AutoNormal(model)
svi = SVI(model, guide, Adam(0.01), Trace_ELBO())
svi_result = svi.run(PRNGKey(1), 2000, *args)
svi_samples = guide.sample_posterior(PRNGKey(0), svi_result.params, sample_shape=(100,))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:00&amp;lt;00:00, 1791.63it/s, 3 steps of size 9.31e-01. acc. prob=0.92]
100%|██████████| 2000/2000 [00:00&amp;lt;00:00, 7085.99it/s, init loss: 7.5166, avg. loss [1901-2000]: 0.0118]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;f, (a1, a2) = plt.subplots(ncols=2, sharey=True, figsize=(10, 4))
a1.set_title(&amp;#39;MCMC&amp;#39;)
a2.set_title(&amp;#39;SVI&amp;#39;)

a_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1, label=&amp;#39;[A]&amp;#39;)
a_line.set_alpha(1)
cat_line, *_ = a1.plot(mcmc_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1, label=&amp;#39;[cat]&amp;#39;)
cat_line.set_alpha(1)
a2.plot(svi_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1)
a2.plot(svi_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1)
f.legend(handles=[a_line, cat_line])
a_line.set_alpha(0.1)
cat_line.set_alpha(0.1)&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_6_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Very nice, we get a range of &lt;span
class="math inline"&gt;[&lt;em&gt;A&lt;/em&gt;]&lt;/span&gt; trajectories based on possible
&lt;span class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt;’s but also &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;sub&gt;&lt;em&gt;d&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;’s. But now
let’s just sample simulate adding measurement error as a
&lt;code&gt;LogNormal&lt;/code&gt;. The previous variables shouldn’t be affected
because we are not making an observation. What’s notable about this
&lt;code&gt;a_draw&lt;/code&gt; variable is that it has a very narrow distribution
(&lt;span class="math inline"&gt;&lt;em&gt;σ&lt;/em&gt;&lt;/span&gt; = 0.02).&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model(a_0, cat_0, dt, n, err):
    k = sample(&amp;#39;k&amp;#39;, dist.LogNormal(0.0, 1.0))
    k_d = sample(&amp;#39;k_d&amp;#39;, dist.LogNormal(-2.0, 1.0))
    cats = deterministic(&amp;#39;cat&amp;#39;, solve_cat(cat_0, k_d, dt, n))
    a_n = deterministic(&amp;#39;a_n&amp;#39;, solve(a_0, k, cats, dt))
    a_draw = sample(&amp;#39;a_draw&amp;#39;, dist.LogNormal(jnp.log(a_n), err))

args = (1.0, 0.1, 0.01, 500, 0.05)
mcmc = MCMC(NUTS(model), num_warmup=1000, num_samples=100)
mcmc.run(PRNGKey(0), *args)
mcmc_samples = mcmc.get_samples()

guide = autoguide.AutoNormal(model)
svi = SVI(model, guide, Adam(0.01), Trace_ELBO())
svi_result = svi.run(PRNGKey(1), 2000, *args)
svi_samples = guide.sample_posterior(PRNGKey(0), svi_result.params, sample_shape=(100,))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:02&amp;lt;00:00, 549.16it/s, 63 steps of size 4.88e-02. acc. prob=0.92]
100%|██████████| 2000/2000 [00:00&amp;lt;00:00, 3172.51it/s, init loss: 139124.0000, avg. loss [1901-2000]: 26.2503]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;f, (a1, a2) = plt.subplots(ncols=2, sharey=True, figsize=(10, 4))
a1.set_title(&amp;#39;MCMC&amp;#39;)
a2.set_title(&amp;#39;SVI&amp;#39;)

a_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1, label=&amp;#39;[A]&amp;#39;)
a_line.set_alpha(1)
cat_line, *_ = a1.plot(mcmc_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1, label=&amp;#39;[cat]&amp;#39;)
cat_line.set_alpha(1)
meas_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_draw&amp;#39;].T, c=&amp;#39;k&amp;#39;, alpha=0.02, label=&amp;#39;[A] measured&amp;#39;)
meas_line.set_alpha(1)
a2.plot(svi_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1)
a2.plot(svi_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1)
a2.plot(svi_samples[&amp;#39;a_draw&amp;#39;].T, c=&amp;#39;k&amp;#39;, alpha=0.05)
f.legend(handles=[a_line, cat_line, meas_line])
a_line.set_alpha(0.1)
cat_line.set_alpha(0.1)
meas_line.set_alpha(0.02)&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_9_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Yes, not looking good at all. We were expecting the exact sample
plots! I tried increasing the number of samples, warmup and SVI steps
and those didn’t help either. The culprit is clearly the narrow
distribution of &lt;code&gt;a_draw&lt;/code&gt; as increasing its std to 1.0
everything goes back to normal.&lt;/p&gt;
&lt;p&gt;After a bit of head scratching, it turns out there is a way to fix
this without rewriting the model and manually de-centering/scaling. This
happens on the level of the &lt;code&gt;Normal&lt;/code&gt; distributions being
sampled with (&lt;span class="math inline"&gt;&lt;em&gt;μ&lt;/em&gt; = 0&lt;/span&gt; and &lt;span
class="math inline"&gt;&lt;em&gt;σ&lt;/em&gt; = 1.0&lt;/span&gt;) but the
&lt;code&gt;LogNormal&lt;/code&gt; distribution is a further exponential transform
away from so that’s where &lt;code&gt;TransformReparam&lt;/code&gt; comes in.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;reparam_config = {
    &amp;#39;k&amp;#39;: reparam.TransformReparam(),
    &amp;#39;k_d&amp;#39;: reparam.TransformReparam(),
    &amp;#39;a_draw&amp;#39;: reparam.TransformReparam(),
    &amp;#39;k_base&amp;#39;: reparam.LocScaleReparam(0),
    &amp;#39;k_d_base&amp;#39;: reparam.LocScaleReparam(0),
    &amp;#39;a_draw_base&amp;#39;: reparam.LocScaleReparam(0),
}

def model(a_0, cat_0, dt, n, err):
    k = sample(&amp;#39;k&amp;#39;, dist.LogNormal(0.0, 1.0))
    k_d = sample(&amp;#39;k_d&amp;#39;, dist.LogNormal(-2.0, 1.0))
    cats = deterministic(&amp;#39;cat&amp;#39;, solve_cat(cat_0, k_d, dt, n))
    a_n = deterministic(&amp;#39;a_n&amp;#39;, solve(a_0, k, cats, dt))
    a_draw = sample(&amp;#39;a_draw&amp;#39;, dist.LogNormal(jnp.log(a_n), err))

model = handlers.reparam(model, reparam_config)

args = (1.0, 0.1, 0.01, 500, 0.05)
mcmc = MCMC(NUTS(model), num_warmup=1000, num_samples=100)
mcmc.run(PRNGKey(0), *args)
mcmc_samples = mcmc.get_samples()

guide = autoguide.AutoNormal(model)
svi = SVI(model, guide, Adam(0.01), Trace_ELBO())
svi_result = svi.run(PRNGKey(1), 2000, *args)
svi_samples = guide.sample_posterior(PRNGKey(0), svi_result.params, sample_shape=(100,))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:00&amp;lt;00:00, 1576.36it/s, 15 steps of size 3.03e-01. acc. prob=0.89]
100%|██████████| 2000/2000 [00:00&amp;lt;00:00, 4285.20it/s, init loss: 1233.4703, avg. loss [1901-2000]: 2.5142]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;f, (a1, a2) = plt.subplots(ncols=2, sharey=True, figsize=(10, 4))
a1.set_title(&amp;#39;MCMC&amp;#39;)
a2.set_title(&amp;#39;SVI&amp;#39;)

a_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1, label=&amp;#39;[A]&amp;#39;)
a_line.set_alpha(1)
cat_line, *_ = a1.plot(mcmc_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1, label=&amp;#39;[cat]&amp;#39;)
cat_line.set_alpha(1)
meas_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_draw&amp;#39;].T, c=&amp;#39;k&amp;#39;, alpha=0.05, label=&amp;#39;[A] measured&amp;#39;)
meas_line.set_alpha(1)
a2.plot(svi_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1)
a2.plot(svi_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1)
a2.plot(svi_samples[&amp;#39;a_draw&amp;#39;].T, c=&amp;#39;k&amp;#39;, alpha=0.05)
f.legend(handles=[a_line, cat_line, meas_line])
a_line.set_alpha(0.1)
cat_line.set_alpha(0.1)
meas_line.set_alpha(0.02)&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_12_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Beautiful! The latent concentrations are now exactly as before and we
also have a nice extra variable showing measurement with error. How
about a quick look at the sampled values of &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; and &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;sub&gt;&lt;em&gt;d&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;, which should
ideally be uncorrelated and have a lognormal marginal, i.e. each is
lognormally distributed irrespective of the other one.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def posterior_df(samples):
    dfs = []

    for var, vals in samples.items():
        dfs.append(pd.DataFrame(samples[var]).melt().rename(columns={&amp;quot;value&amp;quot;: var})[[var]])
    
    return pd.concat([dfs[0], *[df.iloc[:, 0] for df in dfs[1:]]], axis=1)

sns.jointplot(posterior_df(mcmc_samples), x=&amp;#39;k&amp;#39;, y=&amp;#39;k_d&amp;#39;, kind=&amp;#39;kde&amp;#39;, fill=True)
plt.gca().set(ylabel=&amp;#39;$k_d$&amp;#39;, xlabel=&amp;#39;$k$&amp;#39;)
plt.gca().set_xlim(left=0.0)
plt.gca().set_ylim(bottom=0.0);&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_14_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Nice, normal as expected!&lt;/p&gt;
&lt;p&gt;You will notice a quirk: I had to know that the name of the
underlying &lt;code&gt;Normal&lt;/code&gt; sites. And one downside of using this
automatic reparameterisation is that &lt;code&gt;TransformReparam&lt;/code&gt;
doesn’t currently support observations. That’s fine though, you can
split the sampled and observed parts (the observed part won’t need
reparameterising as it won’t need to be sampled).&lt;/p&gt;
&lt;h2 id="adding-support-for-observations"&gt;Adding support for
observations&lt;/h2&gt;
&lt;p&gt;Visualising the prior predictive, i.e. what’s predicted solely from
our priors without observing anything is nice but not that exciting. It
took some trial and error to find the right combination of types and
effect handlers to do this but the solution is to use a masked
distribution. Here is how it works:&lt;/p&gt;
&lt;ol type="1"&gt;
&lt;li&gt;For any unobserved values in &lt;code&gt;a_draw&lt;/code&gt; that need imputing
(i.e. drawing from the prior predictive rather than observing), we can
continue to use our reparamterized &lt;code&gt;TransformedDistribution&lt;/code&gt;,
but masked so the log probability of the any observed sites is ignored.
Essentially, you are still sampling them but not accounting for their
log probability, since we are later going to observe them.&lt;/li&gt;
&lt;li&gt;For any observed values in &lt;code&gt;a_draw_obs&lt;/code&gt;, reparameterizing
is not necessary, since we are not going to sample them. We apply the
opposite mask: observed values simply add to our overall joint log
probability without having to be sampled; unobserved values are also
observed from the imputed values but their log probs are ignored because
we mask them. This is because we already calculated their log prob in
the &lt;code&gt;a_draw&lt;/code&gt; site.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I have to confess that this took a while to get right. Especially
because some of the types don’t compose very well, so for example
&lt;code&gt;TransformedDistribution(MaskedDistribution(...))&lt;/code&gt; is not
supported but using a &lt;code&gt;mask&lt;/code&gt; handler works.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;reparam_config = {
    &amp;#39;k&amp;#39;: reparam.TransformReparam(),
    &amp;#39;k_d&amp;#39;: reparam.TransformReparam(),
    &amp;#39;a_draw&amp;#39;: reparam.TransformReparam(),
    &amp;#39;k_base&amp;#39;: reparam.LocScaleReparam(0),
    &amp;#39;k_d_base&amp;#39;: reparam.LocScaleReparam(0),
    &amp;#39;a_draw_base&amp;#39;: reparam.LocScaleReparam(0),
}

def model(a_0, cat_0, dt, n, err, obs):
    has_obs = ~jnp.isnan(obs)
    k = sample(&amp;#39;k&amp;#39;, dist.LogNormal(0.0, 1.0))
    k_d = sample(&amp;#39;k_d&amp;#39;, dist.LogNormal(-2.0, 1.0))
    cats = deterministic(&amp;#39;cat&amp;#39;, solve_cat(cat_0, k_d, dt, n))
    a_n = deterministic(&amp;#39;a_n&amp;#39;, solve(a_0, k, cats, dt))
    with handlers.mask(mask=~has_obs):
        d = dist.Normal(jnp.log(a_n), err)
    a_draw = sample(&amp;#39;a_draw&amp;#39;, dist.TransformedDistribution(d, dist.transforms.ExpTransform()))
    obs = jnp.where(has_obs, obs, a_draw)
    a_draw_combined = deterministic(&amp;#39;a_draw_combined&amp;#39;, obs)
    a_draw_obs = sample(&amp;#39;a_draw_obs&amp;#39;, dist.LogNormal(jnp.log(a_n), err).mask(has_obs), obs=obs)

def infer(model, args, guide=None):
    mcmc = MCMC(NUTS(model), num_warmup=2000, num_samples=100)
    mcmc.run(PRNGKey(0), *args)
    mcmc_samples = mcmc.get_samples()

    guide = guide or autoguide.AutoNormal(model)
    svi = SVI(model, guide, Adam(0.01), Trace_ELBO())
    svi_result = svi.run(PRNGKey(1), 10000, *args)
    svi_samples = guide.sample_posterior(PRNGKey(0), svi_result.params, sample_shape=(100,))
    return locals()


model = handlers.reparam(model, reparam_config)

obs = np.full((500,), np.nan)
args = (1.0, 0.1, 0.01, 500, 0.05, obs)

results = infer(model, args)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 2100/2100 [00:01&amp;lt;00:00, 1359.92it/s, 15 steps of size 3.03e-01. acc. prob=0.85]
100%|██████████| 10000/10000 [00:01&amp;lt;00:00, 5709.20it/s, init loss: 1233.4703, avg. loss [9501-10000]: 2.4468]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sampling from the prior predictive, i.e. when the observations are
all &lt;code&gt;np.nan&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def plot_samples(mcmc_samples, svi_samples, obs):
    f, (a1, a2) = plt.subplots(ncols=2, sharey=True, figsize=(12, 5))
    a1.set_title(&amp;#39;MCMC&amp;#39;)
    a2.set_title(&amp;#39;SVI&amp;#39;)
    has_obs = ~jnp.isnan(obs)


    a_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1, label=&amp;#39;[A]&amp;#39;)
    a_line.set_alpha(1)
    cat_line, *_ = a1.plot(mcmc_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1, label=&amp;#39;[cat]&amp;#39;)
    cat_line.set_alpha(1)
    meas_line, *_ = a1.plot(mcmc_samples[&amp;#39;a_draw_combined&amp;#39;].T, c=&amp;#39;y&amp;#39;, alpha=0.05, label=&amp;#39;[A] measured&amp;#39;, zorder=-1)
    meas_line.set_alpha(1)
    a2.plot(svi_samples[&amp;#39;a_n&amp;#39;].T, c=&amp;#39;b&amp;#39;, alpha=0.1)
    a2.plot(svi_samples[&amp;#39;cat&amp;#39;].T, c=&amp;#39;r&amp;#39;, alpha=0.1)
    a2.plot(svi_samples[&amp;#39;a_draw_combined&amp;#39;].T, c=&amp;#39;y&amp;#39;, alpha=0.05, zorder=-1)
    f.legend(handles=[a_line, cat_line, meas_line])
    a1.scatter(jnp.nonzero(has_obs)[0], obs[has_obs], c=&amp;#39;k&amp;#39;, zorder=10)
    a2.scatter(jnp.nonzero(has_obs)[0], obs[has_obs], c=&amp;#39;k&amp;#39;, zorder=10)
    a1.set(ylim=(-0.1,1.1))
    a_line.set_alpha(0.1)
    cat_line.set_alpha(0.1)
    meas_line.set_alpha(0.05)
    return f

plot_samples(results[&amp;#39;mcmc_samples&amp;#39;], results[&amp;#39;svi_samples&amp;#39;], obs)
plt.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_20_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now let’s supply a single observation, specifically one that suggests
the reaction is faster than predicted by most trajectories. In the
posterior we would expect to see either a higher than anticipated &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; or a lower than anticipated &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;sub&gt;&lt;em&gt;d&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; and the two
should be positively &lt;strong&gt;correlated&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;obs[100] = 0.6

args = (1.0, 0.1, 0.01, 500, 0.05, obs)

results = infer(model, args)

plot_samples(results[&amp;#39;mcmc_samples&amp;#39;], results[&amp;#39;svi_samples&amp;#39;], obs)
plt.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 2100/2100 [00:01&amp;lt;00:00, 1255.81it/s, 15 steps of size 3.08e-01. acc. prob=0.85]
100%|██████████| 10000/10000 [00:01&amp;lt;00:00, 5971.33it/s, init loss: 1234.0802, avg. loss [9501-10000]: 3.8389]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_22_1.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;all_data = pd.concat([
    posterior_df(results[&amp;#39;mcmc_samples&amp;#39;])[[&amp;#39;k&amp;#39;, &amp;#39;k_d&amp;#39;]].assign(Method=&amp;#39;MCMC&amp;#39;),
    posterior_df(results[&amp;#39;svi_samples&amp;#39;])[[&amp;#39;k&amp;#39;, &amp;#39;k_d&amp;#39;]].assign(Method=&amp;#39;SVI&amp;#39;),
    ], ignore_index=True)
sns.kdeplot(all_data, x=&amp;#39;k&amp;#39;, y=&amp;#39;k_d&amp;#39;, hue=&amp;quot;Method&amp;quot;, fill=True)
plt.gca().set(xlabel=&amp;#39;$k$&amp;#39;, ylabel=&amp;#39;$k_d$&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[Text(0.5, 0, &amp;#39;$k$&amp;#39;), Text(0, 0.5, &amp;#39;$k_d$&amp;#39;)]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_23_1.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And here we go, a very different picture now, and as expected the
&lt;code&gt;AutoNormal&lt;/code&gt; SVI guide fails to capture the correlation
between &lt;span class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; and &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;sub&gt;&lt;em&gt;d&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt;. A full rank
multivariate normal should be able to handle this, albeit taking about
10x as long as MCMC. For some reason
&lt;code&gt;autoguide.AutoLowRankMultivariateNormal(rank=2)&lt;/code&gt; doesn’t
seem to do the trick.&lt;/p&gt;
&lt;p&gt;A more advanced option to get the best of both worlds would be
creating a separate full-rank guide for &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;/span&gt; and &lt;span
class="math inline"&gt;&lt;em&gt;k&lt;/em&gt;&lt;sub&gt;&lt;em&gt;d&lt;/em&gt;&lt;/sub&gt;&lt;/span&gt; then using a
simple &lt;code&gt;AutoNormal&lt;/code&gt; for the rest (specifically
&lt;code&gt;a_draw&lt;/code&gt;) using &lt;code&gt;AutoGuideList&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;# &amp;quot;Easy&amp;quot; but slow and doesn&amp;#39;t really scale
# results = infer(model, args, autoguide.AutoMultivariateNormal(model))

# More involved but very fast
guide = autoguide.AutoGuideList(model)
guide.append(autoguide.AutoNormal(handlers.block(model, expose=[&amp;#39;a_draw_base_decentered&amp;#39;])))
guide.append(autoguide.AutoMultivariateNormal(handlers.block(model, expose=[&amp;#39;k_base_decentered&amp;#39;, &amp;#39;k_d_base_decentered&amp;#39;])))
results = infer(model, args, guide)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 2100/2100 [00:01&amp;lt;00:00, 1313.56it/s, 15 steps of size 3.08e-01. acc. prob=0.85]
100%|██████████| 10000/10000 [00:01&amp;lt;00:00, 5945.90it/s, init loss: 1272.4036, avg. loss [9501-10000]: 3.7564]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is more involved and it seems like &lt;code&gt;AutoGuideList&lt;/code&gt;
doesn’t deal with deterministic sites, so we have derive them manually.
I had to read a lot of numpyro’s source code to figure out how.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;from numpyro.infer.util import soft_vmap

def predictive(sample):
    with handlers.seed(rng_seed=PRNGKey(0)):
        with handlers.substitute(data=sample):
            return {k: v[&amp;#39;value&amp;#39;] for k, v in handlers.trace(model).get_trace(*args).items()}

results[&amp;#39;svi_samples&amp;#39;].update(soft_vmap(predictive, results[&amp;#39;svi_samples&amp;#39;]))&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;all_data = pd.concat([
    posterior_df(results[&amp;#39;mcmc_samples&amp;#39;])[[&amp;#39;k&amp;#39;, &amp;#39;k_d&amp;#39;]].assign(Method=&amp;#39;MCMC&amp;#39;),
    posterior_df(results[&amp;#39;svi_samples&amp;#39;])[[&amp;#39;k&amp;#39;, &amp;#39;k_d&amp;#39;]].assign(Method=&amp;#39;SVI&amp;#39;),
    ], ignore_index=True)
sns.kdeplot(all_data, x=&amp;#39;k&amp;#39;, y=&amp;#39;k_d&amp;#39;, hue=&amp;quot;Method&amp;quot;, fill=True)
plt.gca().set(xlabel=&amp;#39;$k$&amp;#39;, ylabel=&amp;#39;$k_d$&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[Text(0.5, 0, &amp;#39;$k$&amp;#39;), Text(0, 0.5, &amp;#39;$k_d$&amp;#39;)]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_28_1.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And here we go!&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;plot_samples(results[&amp;#39;mcmc_samples&amp;#39;], results[&amp;#39;svi_samples&amp;#39;], obs)
plt.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-11-26-reparam_files/2025-11-26-reparam_30_0.svg"
alt="svg" /&gt;
&lt;figcaption aria-hidden="true"&gt;svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I hope this was fun to explore together. Very happy to chat if you
have any suggestions or questions. This experiment was motivated by my
upcoming RSC book on digital chemistry, as part of which I am hoping to
present probabilistic programming as a powerful data interpretation aid
for experimentalists working with automated platforms that can generate
a wealth of experimental data.&lt;/p&gt;
</content></entry><entry><title>Live variational inference</title><link href="https://hessammehr.github.io/blog/posts/2025-10-15-live-svi.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-10-15-live-svi.html</id><published>2025-10-15T00:00:00+00:00</published><updated>2025-10-15T00:00:00+00:00</updated><content type="html">&lt;h1 id="live-variational-inference"&gt;Live variational inference&lt;/h1&gt;
&lt;p&gt;This is mostly to show two possibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The ability to update the data going into a model in the course of
running variational inference. Imagine you are accumulating data as you
go, or (this is more technical/hacky) you find that your multimodal
distributions get locked into a specific mode that the optimiser cannot
escape and are looking for a way to “ease into” the right mode by
e.g. tightening your Dirichlet concentration factor gradually as you
go.&lt;/li&gt;
&lt;li&gt;Inspect model predictions and loss in real time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is using the &lt;code&gt;SVI.update&lt;/code&gt; call, which I have
avoided so far for the convenience of &lt;code&gt;SVI.run&lt;/code&gt;. A batch of
steps can be take in a jitted loop for performance and the loss
monitored for convergence every time a parameter/input data is
changed.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;matplotlib&lt;/code&gt; you need to use an interactive backend
like &lt;code&gt;qt&lt;/code&gt; or &lt;code&gt;osx&lt;/code&gt; and add a
&lt;code&gt;plt.pause(...)&lt;/code&gt; call in the loop to make sure there is a
chance to re-render the figure. Super useful and something I didn’t know
about (thanks ChatGPT).&lt;/p&gt;
&lt;p&gt;A minimal example below, where the concentration parameter for a
Dirichlet prior to a categorical observation is varied smoothly from 1.0
(i.e. flat) to 0.1 (pretty pointy at the extremes).&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;%matplotlib osx&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;UsageError: Invalid GUI request &amp;#39;macosx&amp;#39;, valid ones are:dict_keys([&amp;#39;inline&amp;#39;, &amp;#39;nbagg&amp;#39;, &amp;#39;webagg&amp;#39;, &amp;#39;notebook&amp;#39;, &amp;#39;ipympl&amp;#39;, &amp;#39;widget&amp;#39;, None, &amp;#39;qt&amp;#39;, &amp;#39;qt5&amp;#39;, &amp;#39;qt6&amp;#39;, &amp;#39;wx&amp;#39;, &amp;#39;tk&amp;#39;, &amp;#39;gtk&amp;#39;, &amp;#39;gtk3&amp;#39;, &amp;#39;osx&amp;#39;, &amp;#39;asyncio&amp;#39;])&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;import jax
import jax.numpy as jnp
import numpy as np
import numpyro
import numpyro.distributions as dist
import seaborn as sns
from matplotlib import pyplot as plt
from numpyro.infer import SVI, Trace_ELBO, autoguide
from numpyro.optim import Adam
from tqdm import tqdm

sns.set_theme(
    &amp;quot;talk&amp;quot;, &amp;quot;ticks&amp;quot;, font=&amp;quot;Arial&amp;quot;, font_scale=1.0, rc={&amp;quot;svg.fonttype&amp;quot;: &amp;quot;none&amp;quot;}
)

num_steps = 100


def model(alpha_scale):
    alpha = jnp.ones(12) * alpha_scale
    theta = numpyro.sample(&amp;quot;theta&amp;quot;, dist.Dirichlet(alpha))
    numpyro.sample(&amp;quot;obs1&amp;quot;, dist.Categorical(theta), obs=jnp.array([2,2,2,2,2,5,5,5]))

guide = autoguide.AutoNormal(model)

optimizer = Adam(1e-3)
svi = SVI(model, guide, optimizer, loss=Trace_ELBO())

rng_key = jax.random.PRNGKey(0)
state = svi.init(
    rng_key,
    alpha_scale=1.0,
)


@jax.jit
def run_stage(state, init_loss, alpha_scale):
    def body_fn(i, val):
        return svi.update(val[0], alpha_scale)

    return jax.lax.fori_loop(0, 100, body_fn, (state, init_loss))


f, (a1, a2) = plt.subplots(nrows=2, sharex=True)
a1.set(ylabel=&amp;quot;ELBO loss&amp;quot;)
a2.set(xlabel=&amp;quot;SVI batch&amp;quot;, ylabel=r&amp;quot;$\theta$ (posterior)&amp;quot;)
f.tight_layout()
for i in tqdm(range(num_steps)):
    alpha_scale = 0.02 ** (i / num_steps)
    a1.set_title(f&amp;quot;$\\alpha$: {alpha_scale:.2f}&amp;quot;)
    while True:
        # Run SVI steps until convergence
        init_loss = svi.evaluate(state, alpha_scale)
        state, loss = run_stage(state, init_loss, alpha_scale)
        if jnp.abs(loss - init_loss) / np.abs(init_loss) &amp;lt; 0.02:
            break
    params = svi.get_params(state)
    a1.scatter([i], loss, c=&amp;quot;k&amp;quot;, s=5)
    posterior = guide.sample_posterior(rng_key, params)
    theta = posterior[&amp;quot;theta&amp;quot;]
    a2.scatter(i * np.ones_like(theta), theta, c=np.arange(12), s=5)
    # This is really important to get live updates.
    plt.pause(0.01)

f.savefig(&amp;quot;2025-10-15-live-svi_result.svg&amp;quot;)
print(&amp;quot;Posterior theta mean:&amp;quot;, posterior[&amp;quot;theta&amp;quot;])
plt.close()&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;100%|██████████| 100/100 [00:06&amp;lt;00:00, 14.36it/s]


Posterior theta mean: [1.26911415e-08 7.69821611e-08 6.65301085e-01 7.23654193e-06
 1.26773830e-06 2.86240101e-01 2.82157103e-10 9.47567692e-04
 2.23408958e-10 4.75026183e-02 6.67153069e-11 5.59603919e-09]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure if the result will show up as expected when this notebook is
converted to HTML but it looks very nice and it’s good to be able to see
it converge.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://hessammehr.github.io/blog/posts/2025-10-15-live-svi_plot.svg" alt="The result" /&gt;
&lt;figcaption aria-hidden="true"&gt;The result&lt;/figcaption&gt;
&lt;/figure&gt;
</content></entry><entry><title>Amortized probabilistic models for chemical microscopy</title><link href="https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3.html</id><published>2025-07-21T00:00:00+00:00</published><updated>2025-07-21T00:00:00+00:00</updated><content type="html">&lt;h1
id="amortized-probabilistic-models-for-chemical-microscopy"&gt;Amortized
probabilistic models for chemical microscopy&lt;/h1&gt;
&lt;h3
id="can-we-used-an-amortized-model-to-speed-up-inference-in-our-droplet-microscopy-model"&gt;Can
we used an amortized model to speed up inference in our droplet
microscopy model?&lt;/h3&gt;
&lt;p&gt;My last go using a probabilistic model to analyze a microscope image
seemed to work well enough, but I wanted to take a more flexible
approach to modelling the appearance of droplets without having to roll
out a more sophisticated physical model. Also, it seemed impractical to
require a beefy GPU and minutes of compute for a single image.&lt;/p&gt;
&lt;p&gt;I’ve been meaning to experiment with &lt;em&gt;amortized&lt;/em&gt; inference a
bit more recently. The idea is that instead of all latent variables
being inferred, a small model (think a miniature multi-layer perceptron)
is trained to predict a subset of these variables. We are interested in
inferring droplet locations and compositions, not so much about
rendering the droplets themselves. This approach can give the best of
both worlds, a mechanistic interpretable model for the parts that we
care about or are easy to reason about, and an data-driven, learned
representation for the complex, inherently introspectable parts.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import jax
import jax.numpy as jnp
import jax.nn as jnn
import flax.linen as nn
import matplotlib.pyplot as plt
import numpy as np
import numpyro.distributions as dist
import seaborn as sns
from numpyro import deterministic, plate, sample
from numpyro.handlers import seed, trace, substitute
from numpyro.infer import SVI, Trace_ELBO, MCMC, NUTS
from numpyro.infer.autoguide import AutoNormal
from numpyro.optim import Adam
from PIL import Image

plt.rcParams[&amp;#39;figure.dpi&amp;#39;] = 200

sns.set_theme(context=&amp;#39;paper&amp;#39;, style=&amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’ll use the same delightful microscope image as last time, part of
the experiments that went into our &lt;a
href="https://doi.org/10.1039/D5DD00100E"&gt;latest paper&lt;/a&gt;.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;img = Image.open(&amp;#39;data/example.jpg&amp;#39;)
img = img.resize((img.width // 4, img.height // 4))
img&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3_files/2025-07-21-droplet-generative-process-3_3_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;img = np.array(img) / 255.0&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;class DropletOpticsModel(nn.Module):
    hidden_dims: tuple = (32, 16, 8)
    
    @nn.compact
    def __call__(self, background, dx, dy, radius, composition):
        &amp;quot;&amp;quot;&amp;quot;
        Args:
            background: (batch, n_channels) - existing/background pixel values
            dx: (batch, 1) - normalized distance from droplet center in x-direction
            dy: (batch, 1) - normalized distance from droplet center in y-direction
            radius: (batch, 1) - droplet radius
            composition: (batch, n_composition_features) - droplet composition vector
        
        Returns:
            new_pixel_value: (batch, n_channels) - predicted new pixel values
        &amp;quot;&amp;quot;&amp;quot;
        # Concatenate all input features
        x = jnp.concatenate([background, dx, dy, radius, composition], axis=-1)

        for i, dim in enumerate(self.hidden_dims):
            x = nn.Dense(dim, name=f&amp;#39;hidden_{i}&amp;#39;)(x)
            x = nn.LayerNorm(name=f&amp;#39;ln_{i}&amp;#39;)(x)
            x = jnn.relu(x)
        
        # Output layer - predict change in pixel values
        delta = nn.Dense(background.shape[-1], name=&amp;#39;output&amp;#39;)(x)
        
        # Add residual connection and apply sigmoid to keep values in [0, 1]
        new_pixel_value = jnn.sigmoid(background + delta)
        
        return new_pixel_value

# Initialize model
model = DropletOpticsModel()

# Test with dummy data
key = jax.random.PRNGKey(42)
batch_size, n_channels, n_composition = 32, 3, 10

dummy_background = jax.random.uniform(key, (batch_size, n_channels))
dummy_dx = jax.random.uniform(key, (batch_size, 1), minval=-1.0, maxval=1.0)
dummy_dy = jax.random.uniform(key, (batch_size, 1), minval=-1.0, maxval=1.0)
dummy_radius = jax.random.uniform(key, (batch_size, 1)) * 0.1  # Small radii
dummy_composition = jax.random.uniform(key, (batch_size, n_composition))

# Initialize parameters
print(f&amp;quot;{dummy_background.shape=}, {dummy_dx.shape=}, {dummy_dy.shape=}, {dummy_radius.shape=}, {dummy_composition.shape=}&amp;quot;)
params = model.init(key, dummy_background, dummy_dx, dummy_dy, dummy_radius, dummy_composition)

# Test forward pass
output = model.apply(params, dummy_background, dummy_dx, dummy_dy, dummy_radius, dummy_composition)
print(f&amp;quot;Input background shape: {dummy_background.shape}&amp;quot;)
print(f&amp;quot;Output pixel values shape: {output.shape}&amp;quot;)
print(f&amp;quot;Output range: [{output.min():.3f}, {output.max():.3f}]&amp;quot;)

# Print model summary
print(f&amp;quot;\nModel summary:&amp;quot;)
print(model.tabulate(key, dummy_background, dummy_dx, dummy_dy, dummy_radius, dummy_composition))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;dummy_background.shape=(32, 3), dummy_dx.shape=(32, 1), dummy_dy.shape=(32, 1), dummy_radius.shape=(32, 1), dummy_composition.shape=(32, 10)
Input background shape: (32, 3)
Output pixel values shape: (32, 3)
Output range: [0.260, 0.897]

Model summary:

                           DropletOpticsModel Summary                           
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ path     ┃ module         ┃ inputs         ┃ outputs        ┃ params         ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│          │ DropletOptics… │ -              │ float32[32,3]  │                │
│          │                │ float32[32,3]  │                │                │
│          │                │ -              │                │                │
│          │                │ float32[32,1]  │                │                │
│          │                │ -              │                │                │
│          │                │ float32[32,1]  │                │                │
│          │                │ -              │                │                │
│          │                │ float32[32,1]  │                │                │
│          │                │ -              │                │                │
│          │                │ float32[32,10] │                │                │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ hidden_0 │ Dense          │ float32[32,16] │ float32[32,32] │ bias:          │
│          │                │                │                │ float32[32]    │
│          │                │                │                │ kernel:        │
│          │                │                │                │ float32[16,32] │
│          │                │                │                │                │
│          │                │                │                │ 544 (2.2 KB)   │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ ln_0     │ LayerNorm      │ float32[32,32] │ float32[32,32] │ bias:          │
│          │                │                │                │ float32[32]    │
│          │                │                │                │ scale:         │
│          │                │                │                │ float32[32]    │
│          │                │                │                │                │
│          │                │                │                │ 64 (256 B)     │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ hidden_1 │ Dense          │ float32[32,32] │ float32[32,16] │ bias:          │
│          │                │                │                │ float32[16]    │
│          │                │                │                │ kernel:        │
│          │                │                │                │ float32[32,16] │
│          │                │                │                │                │
│          │                │                │                │ 528 (2.1 KB)   │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ ln_1     │ LayerNorm      │ float32[32,16] │ float32[32,16] │ bias:          │
│          │                │                │                │ float32[16]    │
│          │                │                │                │ scale:         │
│          │                │                │                │ float32[16]    │
│          │                │                │                │                │
│          │                │                │                │ 32 (128 B)     │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ hidden_2 │ Dense          │ float32[32,16] │ float32[32,8]  │ bias:          │
│          │                │                │                │ float32[8]     │
│          │                │                │                │ kernel:        │
│          │                │                │                │ float32[16,8]  │
│          │                │                │                │                │
│          │                │                │                │ 136 (544 B)    │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ ln_2     │ LayerNorm      │ float32[32,8]  │ float32[32,8]  │ bias:          │
│          │                │                │                │ float32[8]     │
│          │                │                │                │ scale:         │
│          │                │                │                │ float32[8]     │
│          │                │                │                │                │
│          │                │                │                │ 16 (64 B)      │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│ output   │ Dense          │ float32[32,8]  │ float32[32,3]  │ bias:          │
│          │                │                │                │ float32[3]     │
│          │                │                │                │ kernel:        │
│          │                │                │                │ float32[8,3]   │
│          │                │                │                │                │
│          │                │                │                │ 27 (108 B)     │
├──────────┼────────────────┼────────────────┼────────────────┼────────────────┤
│          │                │                │          Total │ 1,347 (5.4 KB) │
└──────────┴────────────────┴────────────────┴────────────────┴────────────────┘
                                                                                
                        Total Parameters: 1,347 (5.4 KB)                        &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What ended up working in the end is when each pixel refers to its
closest droplet for colour. Not ideal but it keeps the memory
requirement manageable.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def tree_to_dists(tree, path=&amp;#39;&amp;#39;):
    if isinstance(tree, dict):
        return {k: tree_to_dists(v, path + &amp;#39;/&amp;#39; + k) for k, v in tree.items()}
    else:
        # print(f&amp;quot;Sampling {path} with shape {tree.shape}&amp;quot;)
        return sample(path, dist.Normal().expand(tree.shape))&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model_with_nn(img, n_droplets, types=10):
    h, w, n_channels = img.shape
    
    # Create coordinate grids
    y_coords, x_coords = jnp.mgrid[:h, :w]
    
    # Sample background per channel
    with plate(&amp;quot;channels&amp;quot;, n_channels):
        bg = sample(&amp;quot;bg&amp;quot;, dist.Uniform(0, 1).expand((n_channels,)))

    # Sample droplet parameters
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(0, 0.5))
        with plate(&amp;quot;types&amp;quot;, types):
            composition = sample(&amp;quot;composition&amp;quot;, dist.Uniform(0, 1)).T

    model = DropletOpticsModel()

    # Initialize background image
    prediction = jnp.broadcast_to(bg, (h, w, n_channels))
    nn_params = model.init(key, 
                           jnp.zeros((h * w, n_channels)), 
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, types)))    
    nn_params = tree_to_dists(nn_params, path=&amp;#39;nn_params&amp;#39;)

    distance = ((x_coords[..., None] - x) / r)**2 + ((y_coords[..., None] - y) / r)**2
    nearest = jnp.argmin(distance, axis=-1)

    # Calculate relative distances from droplet center
    dx = (x_coords - x[nearest]) / r[nearest]  # Normalized by radius
    dy = (y_coords - y[nearest]) / r[nearest]  # Normalized by radius


    # Flatten spatial dimensions for neural network processing
    dx_flat = dx.flatten()[:, None]
    dy_flat = dy.flatten()[:, None]
    r_flat = r[nearest].flatten()[:, None]

    
    # Repeat background and composition for all pixels
    bg_flat = prediction.reshape(-1, n_channels)
    comp_flat = composition[nearest, :].reshape(-1, types)
    
    # Apply neural network to get new pixel values
    prediction = model.apply(nn_params, bg_flat, dx_flat, dy_flat, r_flat, comp_flat)
    
    # Reshape back to image dimensions
    prediction = prediction.reshape(h, w, n_channels)
    
    prediction = jnp.clip(prediction, 0, 1)
    prediction = deterministic(&amp;#39;prediction&amp;#39;, prediction)
    diff = deterministic(&amp;#39;diff&amp;#39;, img - prediction)
    sample(&amp;#39;obs&amp;#39;, dist.Normal(scale=0.05), obs=diff)
    # print(f&amp;quot;{x_coords.shape=}, {y_coords.shape=}, {x.shape=}, {y.shape=}, {r.shape=}, {composition.shape=}, {distance.shape=}, {nearest.shape=}, {dx.shape=}, {dy.shape=}, {dx_flat.shape=}, {dy_flat.shape=}, {r_flat.shape=}, {bg_flat.shape=}, {comp_flat.shape=}, {prediction.shape=}&amp;quot;)
    return nn_params&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;tr = trace(seed(model_with_nn, 0)).get_trace(img, 1000, types=5)
nn_params = seed(model_with_nn, 0)(img, 1000, types=5)
{k: v[&amp;#39;value&amp;#39;].shape for k, v in tr.items() if &amp;#39;value&amp;#39; in v}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{&amp;#39;channels&amp;#39;: (3,),
 &amp;#39;bg&amp;#39;: (3,),
 &amp;#39;droplets&amp;#39;: (1000,),
 &amp;#39;x&amp;#39;: (1000,),
 &amp;#39;y&amp;#39;: (1000,),
 &amp;#39;r&amp;#39;: (1000,),
 &amp;#39;types&amp;#39;: (5,),
 &amp;#39;composition&amp;#39;: (5, 1000),
 &amp;#39;nn_params/params/hidden_0/kernel&amp;#39;: (11, 32),
 &amp;#39;nn_params/params/hidden_0/bias&amp;#39;: (32,),
 &amp;#39;nn_params/params/ln_0/scale&amp;#39;: (32,),
 &amp;#39;nn_params/params/ln_0/bias&amp;#39;: (32,),
 &amp;#39;nn_params/params/hidden_1/kernel&amp;#39;: (32, 16),
 &amp;#39;nn_params/params/hidden_1/bias&amp;#39;: (16,),
 &amp;#39;nn_params/params/ln_1/scale&amp;#39;: (16,),
 &amp;#39;nn_params/params/ln_1/bias&amp;#39;: (16,),
 &amp;#39;nn_params/params/hidden_2/kernel&amp;#39;: (16, 8),
 &amp;#39;nn_params/params/hidden_2/bias&amp;#39;: (8,),
 &amp;#39;nn_params/params/ln_2/scale&amp;#39;: (8,),
 &amp;#39;nn_params/params/ln_2/bias&amp;#39;: (8,),
 &amp;#39;nn_params/params/output/kernel&amp;#39;: (8, 3),
 &amp;#39;nn_params/params/output/bias&amp;#39;: (3,),
 &amp;#39;prediction&amp;#39;: (380, 507, 3),
 &amp;#39;diff&amp;#39;: (380, 507, 3),
 &amp;#39;obs&amp;#39;: (380, 507, 3)}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;guide = AutoNormal(model_with_nn)
svi = SVI(model_with_nn, guide, Adam(0.01), Trace_ELBO())

svi_result = svi.run(jax.random.PRNGKey(0), 100000, img, 800, types=3)
samples_svi = guide.sample_posterior(jax.random.PRNGKey(0), svi_result.params, sample_shape=(100,))
fig, ax = plt.subplots(figsize=(5, 2))
ax.plot(svi_result.losses)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;100%|██████████| 100000/100000 [08:35&amp;lt;00:00, 193.87it/s, init loss: 6371727.5000, avg. loss [95001-100000]: -854501.1250]





[&amp;lt;matplotlib.lines.Line2D at 0x721bf0302960&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3_files/2025-07-21-droplet-generative-process-3_10_2.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.imshow(samples_svi[&amp;#39;prediction&amp;#39;].mean(axis=0))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;matplotlib.image.AxesImage at 0x721cd022ff80&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3_files/2025-07-21-droplet-generative-process-3_11_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not a bad reconstruction, and this time we capture color as well.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;samples_svi[&amp;#39;composition&amp;#39;][:1].shape&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(1, 3, 800)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.imshow(img)

plt.scatter(samples_svi[&amp;#39;x&amp;#39;][0], samples_svi[&amp;#39;y&amp;#39;][0], s=4, alpha=1.0, c=samples_svi[&amp;#39;composition&amp;#39;][0].T, marker=&amp;#39;x&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;matplotlib.collections.PathCollection at 0x721c10238f80&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-07-21-droplet-generative-process-3_files/2025-07-21-droplet-generative-process-3_14_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Droplet composition seems nicely consistent with the image.&lt;/p&gt;
&lt;h2 id="other-attempts"&gt;Other attempts&lt;/h2&gt;
&lt;p&gt;A couple of other approaches that didn’t quite work.&lt;/p&gt;
&lt;h3 id="fully-flattened-model-with-aggregation"&gt;Fully flattened model
with aggregation&lt;/h3&gt;
&lt;p&gt;Two issues: memory use and how to aggregate the results at the
end.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model_with_nn(img, n_droplets, types=10):
    h, w, n_channels = img.shape
    
    # Create coordinate grids
    y_coords, x_coords = jnp.mgrid[:h, :w]
    
    # Sample background per channel
    with plate(&amp;quot;channels&amp;quot;, n_channels):
        bg = sample(&amp;quot;bg&amp;quot;, dist.Uniform(0, 1).expand((n_channels,)))

    # Sample droplet parameters
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(0, 0.5))
        with plate(&amp;quot;types&amp;quot;, types):
            composition = sample(&amp;quot;composition&amp;quot;, dist.Uniform(0, 1)).T

    model = DropletOpticsModel()

    # Initialize background image
    bg = jnp.broadcast_to(bg, (h, w, n_channels))
    nn_params = model.init(key, 
                           jnp.zeros((h * w, n_channels)), 
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, types)))    
    nn_params = tree_to_dists(nn_params, path=&amp;#39;nn_params&amp;#39;)
    
    # Calculate relative distances from droplet center
    dx = (x_coords[..., None] - x) / r  # Normalized by radius
    dy = (y_coords[..., None] - y) / r  # Normalized by radius

    # Flatten spatial dimensions for neural network processing
    dx_flat = dx.flatten()[:, None]
    dy_flat = dy.flatten()[:, None]
    r_flat = jnp.broadcast_to(r, (h, w, n_droplets)).flatten()[:, None]
    
    # Repeat background and composition for all pixels
    bg_flat = jnp.broadcast_to(bg[:, :, None, :], (h, w, n_droplets, n_channels)).reshape(-1, n_channels)
    comp_flat = jnp.broadcast_to(composition, (h * w, n_droplets, types)).reshape(-1, types)
    
    # Apply neural network to get new pixel values
    new_pixels = model.apply(nn_params, bg_flat, dx_flat, dy_flat, r_flat, comp_flat)
    
    # Reshape back to image dimensions
    new_pixels = new_pixels.reshape(h, w, n_channels)
    
    # Update prediction (could be additive or replacement - using replacement here)
    prediction = new_pixels
    
    prediction = jnp.clip(prediction, 0, 1)
    prediction = deterministic(&amp;#39;prediction&amp;#39;, prediction)
    diff = deterministic(&amp;#39;diff&amp;#39;, img - prediction)
    sample(&amp;#39;obs&amp;#39;, dist.Normal(scale=0.05), obs=diff)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="iterative-with-lax.scan"&gt;Iterative with
&lt;code&gt;lax.scan&lt;/code&gt;&lt;/h3&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model_with_nn(img, n_droplets, types=10):
    h, w, n_channels = img.shape
    
    # Create coordinate grids
    y_coords, x_coords = jnp.mgrid[:h, :w]
    
    # Sample background per channel
    with plate(&amp;quot;channels&amp;quot;, n_channels):
        bg = sample(&amp;quot;bg&amp;quot;, dist.Uniform(0, 1).expand((n_channels,)))

    # Sample droplet parameters
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(0, 0.5))
        with plate(&amp;quot;types&amp;quot;, types):
            composition = sample(&amp;quot;composition&amp;quot;, dist.Uniform(0, 1))

    model = DropletOpticsModel()

    # Initialize background image
    prediction = jnp.broadcast_to(bg, (h, w, n_channels))
    nn_params = model.init(key, 
                           jnp.zeros((h * w, n_channels)), 
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, 1)),
                           jnp.zeros((h * w, types)))    
    nn_params = tree_to_dists(nn_params, path=&amp;#39;nn_params&amp;#39;)
    # For each droplet, compute its effect using the neural network
    def apply_droplet(carry, droplet_params):
        current_prediction = carry
        x_i, y_i, r_i, comp_i = droplet_params
        
        # Calculate relative distances from droplet center
        dx = (x_coords - x_i) / r_i  # Normalized by radius
        dy = (y_coords - y_i) / r_i  # Normalized by radius
        
        # Flatten spatial dimensions for neural network processing
        dx_flat = dx.flatten()[:, None]
        dy_flat = dy.flatten()[:, None]
        r_flat = jnp.full((h * w, 1), r_i)
        
        # Repeat background and composition for all pixels
        bg_flat = current_prediction.reshape(-1, n_channels)
        comp_flat = jnp.broadcast_to(comp_i, (h * w, types))
        
        # Apply neural network to get new pixel values
        new_pixels = model.apply(nn_params, bg_flat, dx_flat, dy_flat, r_flat, comp_flat)
        
        # Reshape back to image dimensions
        new_pixels = new_pixels.reshape(h, w, n_channels)
        
        return new_pixels, None
    
    prediction, _ = jax.lax.scan(apply_droplet, prediction, (x, y, r, composition.T))
    prediction = jnp.clip(prediction, 0, 1)
    prediction = deterministic(&amp;#39;prediction&amp;#39;, prediction)
    diff = deterministic(&amp;#39;diff&amp;#39;, img - prediction)
    sample(&amp;#39;obs&amp;#39;, dist.Normal(scale=0.05), obs=diff)
    return nn_params&lt;/code&gt;&lt;/pre&gt;
</content></entry><entry><title>Trialing generative processes for chemical microscopy (part 2)</title><link href="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2.html</id><published>2025-02-23T00:00:00+00:00</published><updated>2025-02-23T00:00:00+00:00</updated><content type="html">&lt;h1
id="trialing-generative-processes-for-chemical-microscopy-part-2"&gt;Trialing
generative processes for chemical microscopy (part 2)&lt;/h1&gt;
&lt;p&gt;Last time we used a very rigid generative model (droplets modeled as
gaussians). This time we’ll use a learned representation of
droplets.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import numpyro.distributions as dist
import seaborn as sns
from numpyro import deterministic, plate, sample
from numpyro.infer import SVI, Trace_ELBO
from numpyro.infer.autoguide import AutoNormal
from numpyro.optim import Adam
from PIL import Image

plt.rcParams[&amp;#39;figure.dpi&amp;#39;] = 200

sns.set_theme(context=&amp;#39;paper&amp;#39;, style=&amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;img = Image.open(&amp;#39;data/example.jpg&amp;#39;)
img = img.resize((img.width // 4, img.height // 4))
img&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_2_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For simplicity, we’ll focus on modeling the H (hue) channel of the
image.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;img_hsv = np.array(img.convert(&amp;#39;HSV&amp;#39;)) / 255.0

plt.imshow(img_hsv[..., 0], cmap=&amp;#39;gray&amp;#39;)
plt.colorbar()&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;matplotlib.colorbar.Colorbar at 0x7861b29ea210&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_4_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model(w, h, n_droplets, channel, types=10, mask_shape=(15, 15)):
    # Sample droplet locations and sizes
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, 1)) * (w - mask_shape[1] - 1)
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, 1)) * (h - mask_shape[0] - 1)
        with plate(&amp;quot;types&amp;quot;, types):
            composition = sample(&amp;quot;composition&amp;quot;, dist.Uniform(0, 1))
    mask = sample(&amp;#39;mask&amp;#39;, dist.Uniform(0, 1).expand((types, *mask_shape)))

    # Instead of round, use floor and linear interpolation
    x_floor = jnp.floor(x)
    y_floor = jnp.floor(y)
    x_frac = x - x_floor
    y_frac = y - y_floor

    dx = jnp.arange(mask_shape[1])[:, None, None]
    dy = jnp.arange(mask_shape[0])[None, :, None]

    # Generate coordinates for bilinear interpolation
    x1 = x_floor[None, None, :].astype(int) + dx
    x2 = x1 + 1
    y1 = y_floor[None, None, :].astype(int) + dy
    y2 = y1 + 1

    # Calculate weights for bilinear interpolation
    wx2 = x_frac[None, None, :]
    wx1 = 1 - wx2
    wy2 = y_frac[None, None, :]
    wy1 = 1 - wy2

    # Calculate droplet masks
    droplet_masks = jnp.einsum(&amp;#39;tn,thw-&amp;gt;hwn&amp;#39;, composition, mask)

    # Initialize background
    bg = sample(&amp;quot;bg&amp;quot;, dist.Uniform(0, 1))
    img = jnp.full((h, w), bg)

    # Add droplets using bilinear interpolation
    img = img.at[y1, x1].add(droplet_masks * (wx1 * wy1))
    img = img.at[y1, x2].add(droplet_masks * (wx2 * wy1))
    img = img.at[y2, x1].add(droplet_masks * (wx1 * wy2))
    img = img.at[y2, x2].add(droplet_masks * (wx2 * wy2))

    img = jnp.clip(img, 0, 1)
    img = deterministic(&amp;#39;img&amp;#39;, img)
    diff = deterministic(&amp;#39;diff&amp;#39;, channel - img)
    sample(&amp;#39;obs&amp;#39;, dist.Normal(scale=0.05), obs=diff)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;guide = AutoNormal(model)
svi = SVI(model, guide, Adam(0.01), Trace_ELBO())

svi_result = svi.run(jax.random.PRNGKey(0), 100000, img.width, img.height, 2000, img_hsv[..., 0])
samples_svi = guide.sample_posterior(jax.random.PRNGKey(0), svi_result.params, sample_shape=(100,))
fig, ax = plt.subplots(figsize=(5, 2))
ax.plot(svi_result.losses)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;100%|██████████| 100000/100000 [01:38&amp;lt;00:00, 1015.79it/s, init loss: 13266128.0000, avg. loss [95001-100000]: 447068.1562]





[&amp;lt;matplotlib.lines.Line2D at 0x78611a185820&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_6_2.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.imshow(samples_svi[&amp;#39;img&amp;#39;].mean(axis=0), cmap=&amp;#39;gray&amp;#39;)
plt.colorbar()&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;matplotlib.colorbar.Colorbar at 0x78610cf92bd0&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_7_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Looks quite good!&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;plt.imshow(img_hsv[:, :, 0]/255.0, cmap=&amp;#39;gray&amp;#39;)
plt.colorbar()
plt.scatter(samples_svi[&amp;#39;x&amp;#39;][:100] * img_hsv.shape[1], samples_svi[&amp;#39;y&amp;#39;][:100] * img_hsv.shape[0], s=4, alpha=0.01, c=&amp;#39;red&amp;#39;, marker=&amp;#39;x&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;matplotlib.collections.PathCollection at 0x786118d06030&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_9_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Most droplets are now detected — very nice!&lt;/p&gt;
&lt;p&gt;Let’s have a look at the inferred droplet masks:&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;fig, axes = plt.subplots(1, samples_svi[&amp;#39;mask&amp;#39;].shape[1], figsize=(samples_svi[&amp;#39;mask&amp;#39;].shape[1], 1), sharey=True)
for i, ax in enumerate(axes):
    ax.imshow(samples_svi[&amp;#39;mask&amp;#39;].mean(axis=0)[i], cmap=&amp;#39;gray&amp;#39;, vmin=0, vmax=1)&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2025-02-23-droplet-generative-process-2_files/2025-02-23-droplet-generative-process-2_12_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This model took about 90 seconds to fit on a rusty RTX 2080. Still
there is much that could be improved. If new samples include the same
droplet types, only at different locations, the masks could be “frozen”.
Even better, we could use amortised inference for almost instant
results. Definitely something to explore in the future.&lt;/p&gt;
</content></entry><entry><title>Trialing generative processes for chemical microscopy (part 1)</title><link href="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process.html</id><published>2024-12-28T00:00:00+00:00</published><updated>2024-12-28T00:00:00+00:00</updated><content type="html">&lt;h1
id="trialing-generative-processes-for-chemical-microscopy-part-1"&gt;Trialing
generative processes for chemical microscopy (part 1)&lt;/h1&gt;
&lt;p&gt;Is it possible to use a generative process to model microscope images
like this (and is it worth the effort?)&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import numpyro.distributions as dist
import seaborn as sns
from numpyro import deterministic, plate, sample
from numpyro.infer import MCMC, NUTS, SVI, Trace_ELBO
from numpyro.infer.autoguide import AutoNormal
from numpyro.optim import Adam
from PIL import Image

sns.set_theme(&amp;#39;notebook&amp;#39;, &amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;)

plt.rcParams[&amp;#39;figure.dpi&amp;#39;] = 200&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;img = Image.open(&amp;#39;data/example.jpg&amp;#39;)
img = img.resize((img.width // 2, img.height // 2))
img&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_2_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Try a couple of different colour spaces in case something interesting
stands out.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;from skimage import color

img_array = np.array(img)
lab_img = color.rgb2lab(img_array)

fig, axes = plt.subplots(3, 1, figsize=(5, 10), sharex=True)

for i, (ax, title) in enumerate(zip(axes, [&amp;#39;L channel&amp;#39;, &amp;#39;a channel&amp;#39;, &amp;#39;b channel&amp;#39;])):
    im = ax.imshow(lab_img[:,:,i], cmap=&amp;#39;gray&amp;#39;)
    ax.set_title(title)
    fig.colorbar(im, ax=ax, fraction=0.03, pad=0.04)

plt.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_4_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;hsv_img = color.rgb2hsv(img_array)

fig, axes = plt.subplots(3, 1, figsize=(5, 10), sharex=True)

for i, (ax, title) in enumerate(zip(axes, [&amp;#39;H channel&amp;#39;, &amp;#39;S channel&amp;#39;, &amp;#39;V channel&amp;#39;])):
    im = ax.imshow(hsv_img[:,:,i], cmap=&amp;#39;gray&amp;#39;)
    ax.set_title(title)
    fig.colorbar(im, ax=ax, fraction=0.03, pad=0.04)

plt.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_5_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;First approach, fixed number of droplets; model centres and radii&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model1(w, h, n_droplets):
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(1.5, 0.75))


mcmc = MCMC(NUTS(model1), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0), w=img.width, h=img.height, n_droplets=100)
samples = mcmc.get_samples()

fig, ax = plt.subplots()
ax.imshow(np.ones_like(np.array(img)) * 255, cmap=&amp;quot;gray&amp;quot;)
for sample_no in range(5):
    for i in range(100):
        circle = plt.Circle(
            (samples[&amp;quot;x&amp;quot;][sample_no][i], samples[&amp;quot;y&amp;quot;][sample_no][i]),
            samples[&amp;quot;r&amp;quot;][sample_no][i],
            color=plt.cm.tab10(sample_no),
        )

        ax.add_artist(circle)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:09&amp;lt;00:00, 112.09it/s, 15 steps of size 3.08e-01. acc. prob=0.84]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_7_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model2(w, h, n_droplets):
    # Sample droplet locations and sizes
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(1.5, 0.75))
        
        # Sample HSV values for each droplet
        h_val = sample(&amp;quot;h&amp;quot;, dist.Uniform(0, 1))
        s_val = sample(&amp;quot;s&amp;quot;, dist.Beta(2, 2))
        v_val = sample(&amp;quot;v&amp;quot;, dist.Beta(5, 2))  # Biased towards brighter values

mcmc = MCMC(NUTS(model2), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0), w=img.width, h=img.height, n_droplets=100)
samples = mcmc.get_samples()
samples = {k: np.array(v) for k, v in samples.items()}

# Visualize with HSV colors
fig, ax = plt.subplots()
ax.imshow(np.ones_like(np.array(img)) * 255, cmap=&amp;quot;gray&amp;quot;)
for i in range(100):
    circle = plt.Circle(
        (samples[&amp;quot;x&amp;quot;][0][i], samples[&amp;quot;y&amp;quot;][0][i]),
        samples[&amp;quot;r&amp;quot;][0][i],
        color=color.hsv2rgb(
            np.array(
                [
                    samples[&amp;quot;h&amp;quot;][0][i],
                    samples[&amp;quot;s&amp;quot;][0][i],
                    samples[&amp;quot;v&amp;quot;][0][i],
                ]
            )
        ),
    )
    ax.add_artist(circle)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:14&amp;lt;00:00, 74.39it/s, 15 steps of size 2.52e-01. acc. prob=0.86] &lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_8_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model2(w, h, n_droplets):
    # Sample droplet locations and sizes
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, w))
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, h))
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(1.5, 0.75))
        
        with plate(&amp;quot;pixels&amp;quot;, w * h):
            x_dist = jnp.abs(x - jnp.arange(w)[:, None])
            y_dist = jnp.abs(y - jnp.arange(h)[:, None])
            distance = jnp.sqrt(x_dist ** 2 + y_dist[:, None] ** 2)
            val = deterministic(&amp;#39;val&amp;#39;, jnp.sum(jnp.exp(-distance ** 2 / (2 * r ** 2)), axis=-1))


mcmc = MCMC(NUTS(model2), num_warmup=500, num_samples=10)
mcmc.run(jax.random.PRNGKey(0), w=img.width, h=img.height, n_droplets=100)
samples = mcmc.get_samples()
samples = {k: np.array(v) for k, v in samples.items()}

# show the first 3 samples
fig, axes = plt.subplots(3, 1, figsize=(5, 10), sharex=True)
for i, ax in enumerate(axes):
    ax.imshow(1 - samples[&amp;#39;val&amp;#39;][i], cmap=&amp;#39;gray&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 510/510 [00:06&amp;lt;00:00, 78.12it/s, 15 steps of size 3.10e-01. acc. prob=0.85] &lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_9_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not a bad generative process to start with. Now let’s just fit the
hue channel …&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model3(w, h, n_droplets, channel, error_scale):
    # Sample droplet locations and sizes
    bg = sample(&amp;quot;bg&amp;quot;, dist.Uniform(0, 1))
    with plate(&amp;quot;droplets&amp;quot;, n_droplets):
        x = sample(&amp;quot;x&amp;quot;, dist.Uniform(0, 1))*w
        y = sample(&amp;quot;y&amp;quot;, dist.Uniform(0, 1))*h
        r = sample(&amp;quot;r&amp;quot;, dist.LogNormal(1.5, 0.75))
        amplitude = sample(&amp;quot;amplitude&amp;quot;, dist.Uniform(0, 1))
        
        x_dist = jnp.abs(x - jnp.arange(w)[:, None])
        y_dist = jnp.abs(y - jnp.arange(h)[:, None])
        distance = jnp.sqrt(x_dist ** 2 + y_dist[:, None] ** 2)
        val = deterministic(&amp;#39;val&amp;#39;, bg + jnp.sum(amplitude[None, None, :] * jnp.exp(-distance ** 2 / (2 * r ** 2)), axis=-1))
        diff = deterministic(&amp;#39;diff&amp;#39;, val - channel)
    sample(&amp;#39;obs&amp;#39;, dist.Normal(0, error_scale), obs=val - channel)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fitting, instead of sampling&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;guide = AutoNormal(model3)
svi = SVI(model3, guide, Adam(0.01), Trace_ELBO())
svi_result = svi.run(jax.random.PRNGKey(0), 20000, img.width, img.height, 500, hsv_img[:,:,0], 0.05)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;100%|██████████| 20000/20000 [04:40&amp;lt;00:00, 71.43it/s, init loss: 19718028.0000, avg. loss [19001-20000]: 5279295.0000]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;svi_result = svi.run(jax.random.PRNGKey(0), 50000, img.width, img.height, 500, hsv_img[:,:,0], 0.05, init_state=svi_result.state)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  0%|          | 0/50000 [00:00&amp;lt;?, ?it/s]

100%|██████████| 50000/50000 [11:36&amp;lt;00:00, 71.82it/s, init loss: 5101927.5000, avg. loss [47501-50000]: 5046932.5000]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;samples = guide.sample_posterior(jax.random.PRNGKey(0), svi_result.params, sample_shape=(5,))&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="python"&gt;&lt;code&gt;fig, axes = plt.subplots(3, 1, figsize=(5, 10), sharex=True)
fig.colorbar(axes[0].imshow(samples[&amp;#39;val&amp;#39;][0], cmap=&amp;#39;gray&amp;#39;), ax=axes[0], fraction=0.03, pad=0.04)
axes[0].set_title(&amp;#39;Prediction&amp;#39;)
fig.colorbar(axes[1].imshow(jnp.abs(samples[&amp;#39;diff&amp;#39;][0]), cmap=&amp;#39;gray&amp;#39;), ax=axes[1], fraction=0.03, pad=0.04)
axes[1].set_title(&amp;#39;Difference&amp;#39;)
fig.colorbar(axes[-1].imshow(hsv_img[:,:,0], cmap=&amp;#39;gray&amp;#39;), ax=axes[-1], fraction=0.03, pad=0.04)
axes[-1].set_title(&amp;#39;Ground truth&amp;#39;)
fig.tight_layout()&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_16_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not a bad start. Most of the brightests spots have been fitted. I am
surprised thought that a lot of the amplitudes are almost zero. At the
end of the day, optimisations where pieces have to move into the right
place first are tricky and I have no reason to believe that this is a
global optimum, despite having spent a while trying to tease out a
better outcome.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;sns.histplot(samples[&amp;#39;amplitude&amp;#39;].flatten(), bins=50);&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-28-droplet-generative-process_files/2024-12-28-droplet-generative-process_18_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
</content></entry><entry><title>Simple generation of locally constrained values in `numpyro`</title><link href="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained.html</id><published>2024-12-26T00:00:00+00:00</published><updated>2024-12-26T00:00:00+00:00</updated><content type="html">&lt;h1
id="simple-generation-of-locally-constrained-values-in-numpyro"&gt;Simple
generation of locally constrained values in &lt;code&gt;numpyro&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Just a simple experiment using &lt;code&gt;numpyro&lt;/code&gt; to simulate
values from a function with locally constrained values. This is often
achieved using Gaussian processed but I thought it would be interesting
to try something a bit more intuitive, plus priors on the points
themselves and their interdependence can be anything, not just normal
distributions.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import numpyro.distributions as dist
import seaborn as sns
from numpyro import sample
from numpyro.infer import MCMC, NUTS

sns.set_theme(&amp;#39;notebook&amp;#39;, &amp;#39;ticks&amp;#39;, font=&amp;#39;Arial&amp;#39;)

plt.rcParams[&amp;#39;figure.dpi&amp;#39;] = 200&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, baseline: Independent draws from a normal distribution.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model1():
    x = sample(&amp;quot;x&amp;quot;, dist.Normal().expand([100]))


mcmc = MCMC(NUTS(model1), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0))
samples = mcmc.get_samples()

x_points = np.repeat(np.arange(100)[None, :], samples[&amp;quot;x&amp;quot;].shape[0], axis=0)
plt.scatter(
    x_points.flatten(), samples[&amp;quot;x&amp;quot;].flatten(), color=&amp;quot;darkblue&amp;quot;, alpha=0.01, s=10
)
plt.gca().set(xlabel=&amp;quot;Spatial/temporal dimension&amp;quot;, ylabel=&amp;quot;Observable&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:00&amp;lt;00:00, 1656.79it/s, 7 steps of size 4.46e-01. acc. prob=0.86]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_3_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Adding some point observations …&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;obs_vals = jnp.array([3.3, -2.5, 4.9])
obs_idx = jnp.array([20, 40, 75])

def model2(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, dist.Normal().expand([100]))
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.1), obs=x[obs_idx])


mcmc = MCMC(NUTS(model2), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10);
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:00&amp;lt;00:00, 1624.28it/s, 15 steps of size 4.02e-01. acc. prob=0.90]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_5_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;“Observing” each point to be the average of previous and following
points. This would be equivalent to adjusting the log-likelihood or
adding a potential in other packages.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model3(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, dist.Normal().expand([100]))
    sample(&amp;#39;x_dependence&amp;#39;, dist.Normal(loc=(x[:-2]+x[2:])/2.0, scale=0.1), obs=x[1:-1])
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.1), obs=x[obs_idx])


mcmc = MCMC(NUTS(model3), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10)
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:01&amp;lt;00:00, 903.87it/s, 63 steps of size 8.62e-02. acc. prob=0.84] &lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_7_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Slightly fancier — weighted relation to next/previous 2 points. I
don’t notice a dramatic change with this 1:2:2:1 weighting but it would
be interesting to add asymmetric constraints, etc. I suppose this could
be useful for probabilistic low-pass filtering with sinc weights.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model4(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, dist.Normal().expand([100]))
    sample(&amp;#39;x_pre_obs&amp;#39;, dist.Normal(loc=(x[:-4]+2.0*x[1:-3]+2.0*x[3:-1]+x[4:])/6.0, scale=0.1), obs=x[2:-2])
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.1), obs=x[obs_idx])


mcmc = MCMC(NUTS(model4), num_warmup=1000, num_samples=100)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

plt.plot(samples[&amp;#39;x&amp;#39;].T, color=&amp;#39;darkblue&amp;#39;, alpha=0.01)
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1100/1100 [00:01&amp;lt;00:00, 1011.04it/s, 63 steps of size 9.88e-02. acc. prob=0.89]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_9_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Something a bit more interesting. Let’s change the “base”
distribution to be bimodal and asymmetric. I have picked fairly broad
humps so we have a fair shot at achieving good mixing.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;mixing_distribution = dist.Categorical(jnp.array([0.3, 0.7]))
component_distribution = dist.Normal(
    loc=jnp.array([-3.0, 3.5]), scale=jnp.array([1.0, 1.5])
)

d = dist.MixtureSameFamily(mixing_distribution, component_distribution)

x = jnp.linspace(-10, 10, 200)
y = np.exp(d.log_prob(x))

plt.fill_between(x, y, alpha=0.3, color=&amp;#39;darkblue&amp;#39;)
plt.gca().set(xlabel=&amp;#39;x&amp;#39;, ylabel=&amp;#39;PDF&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_11_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model5(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, d.expand([100]))

mcmc = MCMC(NUTS(model5), num_warmup=1000, num_samples=200)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

x_points = np.repeat(np.arange(100)[None, :], samples[&amp;#39;x&amp;#39;].shape[0], axis=0)
plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10);&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1200/1200 [00:00&amp;lt;00:00, 1610.28it/s, 15 steps of size 1.68e-01. acc. prob=0.83]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_12_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Adding back the observation constraints without dependence.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model6(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, d.expand([100]))
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.1), obs=x[obs_idx])

mcmc = MCMC(NUTS(model6), num_warmup=1000, num_samples=200)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

x_points = np.repeat(np.arange(100)[None, :], samples[&amp;#39;x&amp;#39;].shape[0], axis=0)
plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10)
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1200/1200 [00:00&amp;lt;00:00, 1463.46it/s, 15 steps of size 1.48e-01. acc. prob=0.87]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_14_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Finally, bringing back dependency on adjacent points …&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model6(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, d.expand([100]))
    sample(&amp;#39;x_dependence&amp;#39;, dist.Normal(loc=(x[:-2]+x[2:])/2.0, scale=0.1), obs=x[1:-1])
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.1), obs=x[obs_idx])

mcmc = MCMC(NUTS(model6), num_warmup=1000, num_samples=200)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

x_points = np.repeat(np.arange(100)[None, :], samples[&amp;#39;x&amp;#39;].shape[0], axis=0)
# plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10)
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20, zorder=10)
plt.plot(samples[&amp;#39;x&amp;#39;].T, color=&amp;#39;darkblue&amp;#39;, alpha=0.01)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1200/1200 [00:03&amp;lt;00:00, 356.29it/s, 255 steps of size 2.30e-02. acc. prob=0.88]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_16_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And now just for fun, let’s try a bimodal dependency distribution,
here essentially saying that each point is likely to be larger than its
neighbor (or linger in the same ballpark).&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;d_dep = dist.MixtureSameFamily(
    mixing_distribution,
    dist.Normal(loc=jnp.array([0, 0.5]), scale=0.1)
)

x = jnp.linspace(-1, 1, 200)
y = np.exp(d_dep.log_prob(x))

plt.fill_between(x, y, alpha=0.3, color=&amp;#39;darkblue&amp;#39;)
plt.gca().set(xlabel=&amp;#39;x&amp;#39;, ylabel=&amp;#39;PDF&amp;#39;, title=&amp;#39;$x_i - x_{i-1}$ prior&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_18_0.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model7(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, d.expand([100]))
    sample(&amp;#39;x_dependence&amp;#39;, d_dep, obs=x[1:] - x[:-1])

mcmc = MCMC(NUTS(model7), num_warmup=1000, num_samples=200)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10)
plt.plot(x_points[0], samples[&amp;#39;x&amp;#39;].mean(axis=0), color=&amp;#39;crimson&amp;#39;, lw=2, label=&amp;#39;Mean&amp;#39;)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1200/1200 [00:03&amp;lt;00:00, 396.09it/s, 127 steps of size 6.49e-02. acc. prob=0.84]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_19_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;With the observations added back in you can see how the function
finds it easier to catch up with sudden rises than falls due to the
prior on adjacent values.&lt;/p&gt;
&lt;pre class="python"&gt;&lt;code&gt;def model8(obs_vals, obs_idx):
    x = sample(&amp;#39;x&amp;#39;, d.expand([100]))
    sample(&amp;#39;x_dependence&amp;#39;, d_dep, obs=x[1:] - x[:-1])
    sample(&amp;#39;x_point_obs&amp;#39;, dist.Normal(loc=obs_vals, scale=0.25), obs=x[obs_idx])

mcmc = MCMC(NUTS(model8), num_warmup=1000, num_samples=200)
mcmc.run(jax.random.PRNGKey(0), obs_vals, obs_idx)
samples = mcmc.get_samples()

plt.scatter(x_points.flatten(), samples[&amp;#39;x&amp;#39;].flatten(), color=&amp;#39;darkblue&amp;#39;, alpha=0.01, s=10)
plt.plot(x_points[0], samples[&amp;#39;x&amp;#39;].mean(axis=0), color=&amp;#39;crimson&amp;#39;, lw=2, label=&amp;#39;Mean&amp;#39;)
plt.scatter(obs_idx, obs_vals, label=&amp;#39;Observed values&amp;#39;, color=&amp;#39;crimson&amp;#39;, s=20)
plt.legend();&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sample: 100%|██████████| 1200/1200 [00:02&amp;lt;00:00, 497.90it/s, 255 steps of size 5.76e-02. acc. prob=0.88]&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;img
src="https://hessammehr.github.io/blog/posts/2024-12-26-locally-constrained_files/2024-12-26-locally-constrained_21_1.png"
alt="png" /&gt;
&lt;figcaption aria-hidden="true"&gt;png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Very cool and I think quite useful for modelling chromatograms.&lt;/p&gt;
</content></entry><entry><title>A rant against macros</title><link href="https://hessammehr.github.io/blog/posts/2020-06-14-rant-against-macros.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2020-06-14-rant-against-macros.html</id><published>2020-06-14T00:00:00+00:00</published><updated>2020-06-14T00:00:00+00:00</updated><content type="html">&lt;h1 id="a-rant-against-macros"&gt;A rant against macros&lt;/h1&gt;
&lt;p&gt;I used to be a huge fan of macros. I remember reading SICP and being
amazed that you could use the language to generate and transform code.
How cool is that? First a couple of examples: Clojure’s
&lt;code&gt;core.async&lt;/code&gt; library includes a &lt;code&gt;go&lt;/code&gt; macro that
lets you launch goroutine-like tasks without having to change the
language.&lt;/p&gt;
&lt;pre class="clojure"&gt;&lt;code&gt;; https://github.com/clojure/core.async/blob/master/examples/walkthrough.clj
(let [c1 (chan)
      c2 (chan)]
  (go (while true
        (let [[v ch] (alts! [c1 c2])]
          (println &amp;quot;Read&amp;quot; v &amp;quot;from&amp;quot; ch))))
  (go (&amp;gt;! c1 &amp;quot;hi&amp;quot;))
  (go (&amp;gt;! c2 &amp;quot;there&amp;quot;)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Turing library lets you write probabilistic programs in Julia as
if you’re using a dedicated probabilistic programming language
(PPL):&lt;/p&gt;
&lt;pre class="julia"&gt;&lt;code&gt;@model gdemo(x, y) = begin
    # Assumptions
    σ ~ InverseGamma(2,3)
    μ ~ Normal(0,sqrt(σ))
    # Observations
    x ~ Normal(μ, sqrt(σ))
    y ~ Normal(μ, sqrt(σ))
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fast forward to 2018 when I sat down with Chris Rackauckas before
JuliaCon and he mentioned he’d been in touch with the Turing developers.
I thought he bring up their PPL syntax and how it’s so wonderful that
Julia lets you mold the language, but when I prompted him he said the
macros have gotten in the way of using Turing as a library. He said
functions and types were the way forward if you want things to
compose.&lt;/p&gt;
&lt;p&gt;Since then, I’ve written a couple of macros of my own and, powerful
as they are, I have come to the conclusion that the problems I used them
for were better handled by i) new or more expressive data structures,
ii) plain old functions, iii) accepting a small amount of extra
verbosity. In return you get, i) better interoperability, ii) code that
is more explicit and easier to undestand, iii) much easier debugging,
iv) a more robust design, v) much better support from your tools
(&lt;em&gt;e.g.&lt;/em&gt; IDE, REPL).&lt;/p&gt;
&lt;p&gt;Let’s look at a simpler model:&lt;/p&gt;
&lt;pre class="julia"&gt;&lt;code&gt;using Distributions

@model normal_model(x) = begin
    # just a simple transformation; z is still observed, just like x
    z = 2x
    # sample y
    y ~ Normal(0.0, 1.0)
    # observe z
    z ~ Normal(y, 1.0)
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what it expands to&lt;/p&gt;
&lt;pre class="julia"&gt;&lt;code&gt;quote
    #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:348 =#
    function var&amp;quot;##evaluator#371&amp;quot;(_rng::Random.AbstractRNG, _model::DynamicPPL.Model, _varinfo::DynamicPPL.AbstractVarInfo, _sampler::AbstractMCMC.AbstractSampler, _context::DynamicPPL.AbstractContext)
        #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:355 =#
        begin
            x = (DynamicPPL.matchingvalue)(_sampler, _varinfo, _model.args.x)
        end
        #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:356 =#
        begin
            #= REPL[22]:1 =#
            #= REPL[22]:2 =#
            z = 2x
            #= REPL[22]:3 =#
            begin
                var&amp;quot;##tmpright#363&amp;quot; = Normal(0.0, 1.0)
                var&amp;quot;##tmpright#363&amp;quot; isa Union{Distribution, AbstractVector{&amp;lt;:Distribution}} || throw(ArgumentError(&amp;quot;Right-hand side of a ~ must be subtype of Distribution or a vector of Distributions.&amp;quot;))
                var&amp;quot;##vn#365&amp;quot; = y
                var&amp;quot;##inds#366&amp;quot; = ()
                y = (DynamicPPL.tilde_assume)(_rng, _context, _sampler, var&amp;quot;##tmpright#363&amp;quot;, var&amp;quot;##vn#365&amp;quot;, var&amp;quot;##inds#366&amp;quot;, _varinfo)
            end
            #= REPL[22]:4 =#
            begin
                var&amp;quot;##tmpright#367&amp;quot; = Normal(y, 1.0)
                var&amp;quot;##tmpright#367&amp;quot; isa Union{Distribution, AbstractVector{&amp;lt;:Distribution}} || throw(ArgumentError(&amp;quot;Right-hand side of a ~ must be subtype of Distribution or a vector of Distributions.&amp;quot;))
                var&amp;quot;##vn#369&amp;quot; = z
                var&amp;quot;##inds#370&amp;quot; = ()
                z = (DynamicPPL.tilde_assume)(_rng, _context, _sampler, var&amp;quot;##tmpright#367&amp;quot;, var&amp;quot;##vn#369&amp;quot;, var&amp;quot;##inds#370&amp;quot;, _varinfo)
            end
        end
    end
    #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:359 =#
    var&amp;quot;##generator#372&amp;quot;(x) = begin
            #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:359 =#
            (DynamicPPL.Model)(var&amp;quot;##evaluator#371&amp;quot;, (DynamicPPL.namedtuple)(NamedTuple{(:x,), Tuple{Core.Typeof(x)}}, (x,)), (DynamicPPL.ModelGen){(:x,)}(var&amp;quot;##generator#372&amp;quot;, NamedTuple()))
        end
    #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:360 =#
    var&amp;quot;##generator#372&amp;quot;(; x) = begin
            #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:344 =#
            var&amp;quot;##generator#372&amp;quot;(x)
        end
    #= /home/group/.julia/packages/DynamicPPL/9OFG0/src/compiler.jl:362 =#
    begin
        $(Expr(:meta, :doc))
        normal_model = (DynamicPPL.ModelGen){(:x,)}(var&amp;quot;##generator#372&amp;quot;, NamedTuple())
    end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now to sample it:&lt;/p&gt;
&lt;pre class="julia"&gt;&lt;code&gt;sample(normal_model(3.0), NUTS(), 1000)

# Summary Statistics
#   parameters    mean     std  naive_se    mcse       ess   r_hat
#   ──────────  ──────  ──────  ────────  ──────  ────────  ──────
#            y  0.0096  1.0146    0.0454  0.0978  168.5831  0.9986
#            z  0.0169  1.4692    0.0657  0.1204  158.7592  0.9992&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we see that Turing has sampled both &lt;code&gt;y&lt;/code&gt; and
&lt;code&gt;z&lt;/code&gt;, where &lt;code&gt;z&lt;/code&gt; should have been marked as
deterministic and observed rather than sampled. Now, I’m sure this is
well-documented somewhere but the point is that when you use a macro,
your Julia code no longer functions the way you would expect. Worse, yet
finding out why means being able to navigate the mess of generated
symbols in the expanded version. And yes, the authors can fix this (if
it’s actually a bug) but it doesn’t change the problem that the language
inside that block is no longer Julia. You keep having to second guess
yourself every time you reach for a new language feature.&lt;/p&gt;
&lt;p&gt;Increasingly, macros, even nice hygienic ones remind me of the
horrible mess that’s C/C++ macros: an untamed partial language with its
own semantics that you need to learn and use, and how people have
created whole programming languages in part to escape this ugly
metalangauge problem. It’s true that homoiconic languages mostly get rid
of the macro/preprocessor language, but the semantics of how language
constucts behave within the macro and how they compose with other
langauge features is still completely up to the programmer and, in my
experience, quite hard to get right.&lt;/p&gt;
&lt;p&gt;I see macros used in places that I find really troubling. I was
writing a toy GTK application in Rust earlier today and learned that you
need to use these weird macros to get memory management to play nicely
with Rust.&lt;/p&gt;
&lt;pre class="rust"&gt;&lt;code&gt;use glib::clone;

let window = Rc::new(ApplicationWindow::new(app));

# moving a weak reference to `window` into the closure
butten.connect_activate(clone!(@weak window =&amp;gt; move |_| {
    window.close(&amp;amp;button);
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I really don’t think introducing this metalanguage is a good idea at
all. Also, how is this custom syntax supposed to be understood by the
editor? Before &lt;code&gt;rust-analyzer&lt;/code&gt; my editor (VSCode + RLS) would
give up with the macro and I would have to guess my way out. Things are
better now that we have &lt;code&gt;rust-analyzer&lt;/code&gt; but I’m not even sure
the Rust tooling is ever supposed to be able to make sense of this.&lt;/p&gt;
&lt;p&gt;Bottom line (and I’m happy to be proven wrong): macros are an
unsustainable convenience. They are never good enough to justify the
readability/maintainability/tooling headaches.&lt;/p&gt;
</content></entry><entry><title>The case for lazy computation and interactive optimization</title><link href="https://hessammehr.github.io/blog/posts/2020-05-24-lazy-interactive-optimization.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2020-05-24-lazy-interactive-optimization.html</id><published>2020-05-24T00:00:00+00:00</published><updated>2020-05-24T00:00:00+00:00</updated><content type="html">&lt;h1 id="the-case-for-lazy-computation-and-interactive-optimization"&gt;The
case for lazy computation and interactive optimization&lt;/h1&gt;
&lt;p&gt;Since 2018, my colleague &lt;a
href="https://twitter.com/DarioCaramelli"&gt;Dario&lt;/a&gt; and I have been
working on a probabilistic model of chemical reactivity. In a nutshell,
this model can take reactivity observations between a bunch of compounds
and interpret them as manifestations of properties and their mutual
reactivities. The &lt;a
href="https://en.wikipedia.org/wiki/Markov_chain_Monte_Carlo"&gt;Markov
chain Monte carlo&lt;/a&gt; implementation of this model in &lt;a
href="https://docs.pymc.io"&gt;PyMC3&lt;/a&gt; was quite fast to start but over
time we have expanded the model and now with all the bells and whistles
sampling can take close to a day.&lt;/p&gt;
&lt;p&gt;Given these performance issues it was only natural to look at
alternative implementations in my high performance language of choice,
Julia. So, over the past year I have done maybe a dozen experiments,
from a more or less equivalent high-level description in &lt;a
href="https://turing.ml"&gt;Turing.jl&lt;/a&gt;, to encoding the log probability
manually (on the CPU and the GPU) and using &lt;a
href="https://github.com/tpapp/DynamicHMC.jl"&gt;DynamicHMC.jl&lt;/a&gt; and
friends to sample, to using the HMC implementation in &lt;a
href="https://github.com/TuringLang/AdvancedHMC.jl"&gt;AdvancedHMC.jl&lt;/a&gt;
and hand coding the log probability + domain transformations. What has
been surprising is that the performance gain hasn’t been nearly as large
as I imagined it would be. In fact, my fastest implementation using
AdvancedHMC is still only half as fast as the straightforward PyMC3
implementation. There may be differences in the formulation of NUTS
between the different packages, of course, so I don’t think this should
be taken as representative. Just that it got me thinking about
performance.&lt;/p&gt;
&lt;p&gt;Yesterday, I started thinking about PyMC3’s backend, &lt;a
href="http://www.deeplearning.net/software/theano/"&gt;Theano&lt;/a&gt;, and how
it allows PyMC to be fast and expressive. Theano (and newer systems like
TensorFlow) express computations as executable graphs of operations
(&lt;em&gt;ops&lt;/em&gt; for short). In principle, evaluation of these graphs can
entail any of the following:&lt;/p&gt;
&lt;ol type="1"&gt;
&lt;li&gt;Simply running an interpreted implementation of each
&lt;em&gt;op&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Native code generation for all or part of the ops before
execution.&lt;/li&gt;
&lt;li&gt;Graph optimization, e.g. removing redundant ops or rewriting
sequences of ops them with faster equivalents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Libraries like Theano and TensorFlow have a fairly limited scope
(numerical code) but I believe that borrowing certain of the above
elements can make a great DSL for high performance computing in other
domains. Specifically:&lt;/p&gt;
&lt;ol type="1"&gt;
&lt;li&gt;Computations described as graphs of ops.&lt;/li&gt;
&lt;li&gt;Op-graph transformations, themselves described using #1. These
transformations can be applied to a certain op or to all ops in a given
scope (&lt;em&gt;e.g.&lt;/em&gt; children of a certain op, or ops matching a certain
pattern).&lt;/li&gt;
&lt;li&gt;Interpretive execution of the op graph.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Using this design, optimizations will be part of libraries that can
be imported and applied to existing code &lt;em&gt;a la carte&lt;/em&gt; as opposed
to hard-coded in the compiler/JIT. This model is somewhat similar to
what Julia does, &lt;em&gt;i.e.&lt;/em&gt; interpretation and JIT compilation of
code based on inferred type, but goes beyond building everything around
the type system. Moreover, it can be implemented as a library in Julia,
Python, or any language with bindings to codegen backend being targetted
(if any).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (2020-05-26):&lt;/strong&gt; Relevant discussion on the &lt;a
href="https://discourse.julialang.org/t/idea-scope-rather-than-type-centric-composable-optimizations"&gt;Julia
discourse&lt;/a&gt;. Forum users pointed out some of the promising
developments in the area, e.g. &lt;a
href="https://github.com/MikeInnes/Mjolnir.jl"&gt;Mjolnir&lt;/a&gt; and being
able to &lt;a
href="https://github.com/JuliaLang/julia/pull/33955"&gt;customize the
compilation pipeline&lt;/a&gt; through parameterized interpretation.&lt;/p&gt;
</content></entry><entry><title>Fertile land at the confluence of staged programming and (typed) logic programming — Part 1</title><link href="https://hessammehr.github.io/blog/posts/2019-04-07-fertile-land-at-the-confluence-of-staged-programming-and-typed-logic-programming.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2019-04-07-fertile-land-at-the-confluence-of-staged-programming-and-typed-logic-programming.html</id><published>2019-04-07T00:00:00+00:00</published><updated>2019-04-07T00:00:00+00:00</updated><content type="html">&lt;h1
id="fertile-land-at-the-confluence-of-staged-programming-and-typed-logic-programming-part-1"&gt;Fertile
land at the confluence of staged programming and (typed) logic
programming — Part 1&lt;/h1&gt;
&lt;p&gt;I recently started sketching out a logic programming library called
&lt;a href="https://github.com/hessammehr/Logic.jl"&gt;Logic.jl&lt;/a&gt; (it really
is very much a sketch at the moment). As the name might imply, the
library is implemented in Julia, which seems like an odd choice of
implementation language. Choosing Julia and the twist on logic
programming that I am aiming for are motivated by a particularly
interesting confluence of ideas that I have become aware of in the past
year and hope to describe in this blog post. I would love to hear your
thoughts of course; just keep in mind that I am not a computer
scientist.&lt;/p&gt;
&lt;h2
id="idea-1-staged-programming-and-languages-with-first-class-jit-compilation-machinery"&gt;Idea
#1: Staged programming and languages with first-class JIT compilation
machinery&lt;/h2&gt;
&lt;p&gt;There is no shortage of programming languages with just-in-time (JIT)
accelerated virtual machines. Typical JITs are opaque: they step in at
run time and speed up your code without you having to tell them
anything. Increasingly, however, there are JITs of a different breed
that, rather than stay hidden and opaque, work &lt;em&gt;in conjunction&lt;/em&gt;
with the program, giving rise to a class of programming languages where
application code can inspect and influence the various stages of code
lowering as a &lt;em&gt;precise&lt;/em&gt; mechanism for on- demand code generation
and behaviour adaptation. Using the notion of &lt;em&gt;staged
programming&lt;/em&gt; , the various components of the runtime monolith (type
checker, GC, codegen) can also be used in an &lt;em&gt;à la carte&lt;/em&gt;
fashion, further blurring the line between compile and run time. I find
this paradigm superior to the AST- level metaprogramming often
encountered with LISP family languages. Where conventional JITs like
HotSpot are often used to bridge the performance gap between
bytecode-interpreted dynamically-typed languages like Python and
statically-typed compiled languages like OCaml, staged programming,
e.g. in Julia, also recovers some of the safety and expressive power of
a sophisticated type system in an otherwise dynamically-typed
environment.&lt;/p&gt;
&lt;p&gt;The immediate trade-off is the overhead of including the full
compiler toolchain with application code. The run time memory footprint
would then resemble the equivalent of an AST/bytecode interpreter,
libraries, and any compiler passes, including [possibly] the type system
and codegen (e.g. LLVM). This is non-trivial and impractical in
memory-constrained applications such as embedded systems. In principle
at least, one can ahead-of-time compile the application and discard
unused toolchain code as long as it can be inferred as unnecessary. That
said, annotating/inferring dependencies between application code and the
various pieces of compiler/runtime might require some effort.&lt;/p&gt;
&lt;h3 id="staged-programming-in-the-wild"&gt;Staged programming in the
wild&lt;/h3&gt;
&lt;p&gt;Before we move on to Julia, let’s look at a few examples of
programming langauges/environments that feature elements of staged
programming. I suspect that there are many more such systems out there
that I am not aware of.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://okmij.org/ftp/ML/MetaOCaml.html"&gt;MetaOCaml&lt;/a&gt; is
described as an OCaml dialect for multi-stage programming. OCaml seems
like a particularly good choice of language in my opinion, because a lot
of the power afforded by its type system is challenged when dealing with
data the structure of which is unknown until run time. Here for example
is a (simplified) definition of the &lt;code&gt;json&lt;/code&gt; type from the
OCaml package Yojson.&lt;/p&gt;
&lt;pre class="ocaml"&gt;&lt;code&gt;type json = [
  | `Assoc of (string * json) list
  | `Bool of bool
  | `Float of float
  | `Int of int
  | `List of json list
  | `Null
  | `String of string
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the Yojson package in use&lt;/p&gt;
&lt;pre class="ocaml"&gt;&lt;code&gt;let doc = &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="http://terralang.org/"&gt;Terra&lt;/a&gt;, essentially a
metaprogramming system using Lua and LLVM aimed at low-level system
programming.&lt;/p&gt;
&lt;pre class="lua"&gt;&lt;code&gt;-- Terra allows the Lua interpreter and LLVM to interact.
-- Lua code can invoke LLVM for code generation.
-- LLVM can also call Lua, here to partially evaluate an expression.
local a = 5
terra sin5()
    return [ math.sin(a) ]
end

-- output bitcode
sin5:printpretty() 
&amp;gt; output:
&amp;gt; sin50 = terra() : {double}
&amp;gt;    return -0.95892427466314
&amp;gt; end

-- example code from terralang.org&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="staged-programming-in-julia"&gt;Staged programming in Julia&lt;/h3&gt;
&lt;p&gt;In my opinion Julia is the most successful implementation of the
staged programming paradigm today. I say this based on the number of
Julia users as well as how far it has taken the paradigm.&lt;/p&gt;
&lt;p&gt;In Julia functions act as the basic unit of JIT compilation, with
types guiding the process through a language feature called &lt;em&gt;multiple
dispatch&lt;/em&gt;. Take the following simple function, for instance.&lt;/p&gt;
&lt;pre class="julia"&gt;&lt;code&gt;&amp;quot;&amp;quot;&amp;quot; sum(col)
Return the sum of the elements of collection `col`
&amp;quot;&amp;quot;&amp;quot;
function sum(col)
    result = zero(eltype(col))
    for elem in col
        result += elem
    end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No code generation happens for this function until it is invoked,
e.g. &lt;code&gt;sum([1,2,3])&lt;/code&gt;, at which point the type of its argument
&lt;code&gt;Array{Int64, 1}&lt;/code&gt; recursively propagates through the body of
the function. Much of the logic inside the function can be
constant-folded given this concrete type. far from perfect.&lt;/p&gt;
&lt;p&gt;The use of multiple dispatch as the primary driver of code generation
in Julia seems to have worked out really well for the language. Still, I
wonder if there are mechanisms that allow more expressive
programming.&lt;/p&gt;
&lt;p&gt;Julia was conceived as a programming language for high performance
numerical calculations but, unlike other languages in its league like
Matlab and Mathematica, its type system and compilation machinery appear
to be applicable well beyond the realm of scientific computing, as
demonstrated by projects like &lt;a
href="https://github.com/jamii/imp"&gt;imp&lt;/a&gt; and &lt;a
href="https://github.com/rbt-lang/DataKnots.jl"&gt;DataKnots.jl&lt;/a&gt; and &lt;a
href="https://github.com/BioJulia/Automa.jl"&gt;Automa.jl&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Staged-programming is also known as multi&lt;/p&gt;
&lt;h2 id="first-class-embedding-of-logic-programming"&gt;First-class
embedding of logic programming&lt;/h2&gt;
&lt;p&gt;Having learned the functional paradigm, many people find it hard to
go back to their old imperitive tools, which feel verbose and
error-prone in comparison. My brief exposure to logic programming has
left me with a similar feeling towards functional programming, namely
the sense that logic programming allows solving the problem in a more
direct and natural way. Just as functional programming seems removed
from the physical reality of the computer because of its pervasive use
of the function as a layer of indirection, logic programming’s symbolic
variables and predicates provide a further layer of abstraction that
allow the solution to be described by its properties rather than its
realization from a given set of inputs.&lt;/p&gt;
&lt;p&gt;Many Prolog users are familiar with the less-than-ideal
interoperability of typical Prolog implementations with real-world code
written in a language like Python. In a follow-up blog post I will try
to explain how Julia’s multiple dispatch and staged programming
facilitate embedding logic programming to bring most of its expressive
power to Julia.&lt;/p&gt;
</content></entry><entry><title>Funny</title><link href="https://hessammehr.github.io/blog/posts/2013-12-28-funny.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2013-12-28-funny.html</id><published>2013-12-28T00:00:00+00:00</published><updated>2013-12-28T00:00:00+00:00</updated><content type="html">&lt;h1 id="funny"&gt;Funny&lt;/h1&gt;
&lt;p&gt;… Likewise, claiming JS is homoiconic because you can eval strings of
code is nonsense. If that’s the only criteria for homoiconicity, then C
is too, since you can treat an array of bytes as code and jump to
it.&lt;/p&gt;
&lt;p&gt;From &lt;a
href="http://journal.stuffwithstuff.com/2013/07/18/javascript-isnt-scheme"&gt;here&lt;/a&gt;.&lt;/p&gt;
</content></entry><entry><title>A new way of doing chemistry</title><link href="https://hessammehr.github.io/blog/posts/2013-12-23-a-new-way-of-doing-chemistry.html" rel="alternate" /><id>https://hessammehr.github.io/blog/posts/2013-12-23-a-new-way-of-doing-chemistry.html</id><published>2013-12-23T00:00:00+00:00</published><updated>2013-12-23T00:00:00+00:00</updated><content type="html">&lt;h1 id="a-new-way-of-doing-chemistry"&gt;A new way of doing chemistry&lt;/h1&gt;
&lt;p&gt;I’ve been thinking about a new way of doing chemistry. Take for
example the case of growing crystals. In the more difficult cases, this
involves trial and error using a number of educated guesses about the
solvents and conditions. What if we had an algorithm that would decide
the best procedure based on a database of molecular metrics, e.g.,
dipole moment, molecular weight, melting point, decomposition
temperature, functional groups, hydrogen bonding, etc. for solutes as
well as solvents, trained on a series of successful/unsuccessful
combinations, e.g., naphthalene from hot ethanol, macrocycle x from DMSO
+ ether by diffusion at room temperature, compound y from slow
evaporation of hexanes solution?&lt;/p&gt;
&lt;p&gt;This of course can be generalized to any chemical transformation.
Publicly available databases like Org. Syn. can be mined for information
that no one chemist can hope to memorize, significantly reducing the
amount of trial and error in day-to-day chemistry.&lt;/p&gt;
</content></entry></feed>