Latest Blog Postshttp://localhost/latest.atom/2023-02-11T12:00:00Zhttp://localhost/WerkzeugVector graphics wallpapers with pythonhttp://localhost/blog/svg-wallpapers-with-python/2023-02-11T12:00:00Z2023-02-11T12:00:00Z<p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/hexsin.svg" alt="sin wave on hexagonal lattice"></p>
<p class="text-muted small text-end">Graphic by author
</p>
<hr>
<h1 id="tldr">TL;DR</h1>
<p>If you are not interested in any of the code or math but only want to see pretty graphics - jump straight to the
<a href="#graphics">graphics</a> section.</p>
<h1 id="introduction">Introduction</h1>
<p>In the past months I've been working with a lot with vector graphics. Although I wouldn't call myself
a graphic designer, I was mistaken for one a couple of times at tech events. I quite enjoy creative design work since
it's a really refreshing mode change from writing code or looking at data. I've always enjoyed making things
look aesthetically pleasing but in my work that was mainly limited around creating charts and data
visualizations, arranging those into dashboards and laying it out in some app. Occasionally I'm creating
diagrams, but that's mostly for technical purposes.</p>
<p>Most of the work with vector graphics I've done to date was in <a href="https://inkscape.org/">Inkscape</a> which
I fell most comfortable with since I've been using on and off for a few good years. Although I've tried
more modern, web based tools like Canva or Figma but whenever I had something a bit more advanced I always went back
to Inkscape. All tools have their strengths, but they all require some learning investment before becoming
productive. Inkscape turned out to be a great investment for me, but I feel I could do much more with
vector graphics if I could tap into my python skills. </p>
<p>For some time I wanted to learn how to create vector graphics with python. I've watched a few conference
talks on creative coding and the whole concept resonated with me instantly. I started exploring what's
possible around the stack I'm familiar with. A good place to start was learning a bit more about
<a href="https://developer.mozilla.org/en-US/docs/Web/SVG">SVG</a> itself which stands for <strong>Scalable Vector Graphics</strong>, and it
is an XML-based markup language that describes 2D vector graphics. For our purposes we won't need to go
very deep and only explore a few <code>elements</code> and <code>attributes</code> and that'll be sufficient to start creating
graphics.</p>
<p>What I would like to do is to be able to create repeated patterns (or tilings) of simple geometric shapes
and apply a few effects to make it a bit less boring. I am aware that that Inkscape has a tool to create
tilings based on repeating patterns, but I couldn't fully do what I had in mind and I wanted to try
creating graphics programmatically. There is also a practical aspect to this project
since I would like to come up with a design to cover a few windows with in a restaurant to increase privacy
and comfort for guests.</p>
<p>I'll start with a simple square lattice<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup> and insert simple shapes like squares on the lattice positions.
The plan is to start modifying the shapes to create interesting effects. In the end I'll repeat the whole
set of designs on a hexagonal to compare the results.</p>
<h2 id="creating-svg-graphics-with-python">Creating SVG graphics with python</h2>
<p>As you might've expected, there are python packages out there, that can help you create SVGs without
having to write raw XML and worrying
about matching tags and debugging typos. I'll use <a href="https://svgwrite.readthedocs.io/en/latest/">svgwrite</a> since
it works out of the box with JupyterLab. Alternatively you can try <a href="https://github.com/cduck/drawSvg">drawSvg</a>,
they both seem to have a very similar API.</p>
<p>I'm doing most of my coding here in a jupyter notebook it might be worth mentioning that notebooks can
display SVGs natively. They can display SVG content directly in a cell either from a file or
from SVG source with the handy <code>%%SVG</code> cell magic</p>
<div class="codehilite"><pre><span></span><code>%%SVG
<span class="nt"><svg</span> <span class="na">height=</span><span class="s">"210"</span> <span class="na">width=</span><span class="s">"500"</span><span class="nt">></span>
<span class="nt"><polygon</span>
<span class="na">points=</span><span class="s">"200,10 300,180 100,180"</span>
<span class="na">style=</span><span class="s">"fill:darkcyan;stroke:tomato;stroke-width:2"</span>
<span class="nt">/></span>
<span class="nt"></svg></span>
</code></pre></div>
<p>should give you the following in the output cell:</p>
<p><svg height="210" width="500">
<polygon points="200,10 300,180 100,180" style="fill:darkcyan;stroke:tomato;stroke-width:2" />
</svg> </p>
<p>Alternatively you can also use the builtin display system of IPython:</p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">IPython.core.display</span> <span class="kn">import</span> <span class="n">SVG</span>
<span class="n">SVG</span><span class="p">(</span><span class="s2">"""<svg height="210" width="500"></span>
<span class="s2"><polygon</span>
<span class="s2"> points="100,200 200,200 150.0,113.39745962155614"</span>
<span class="s2"> style="fill:darkcyan;stroke:tomato;stroke-width:2"</span>
<span class="s2">/></span>
<span class="s2"><polygon</span>
<span class="s2"> points="200,200 300,200 250.0,113.39745962155614"</span>
<span class="s2"> style="fill:darkcyan;stroke:tomato;stroke-width:2"</span>
<span class="s2">/></span>
<span class="s2"></svg>"""</span><span class="p">)</span>
</code></pre></div>
<p>should in turn produce:</p>
<p><svg height="110" width="500">
<polygon points="100,100 200,100 150.0,13.39745962155614" style="fill:darkcyan;stroke:tomato;stroke-width:2" />
<polygon points="200,100 300,100 250.0,13.39745962155614" style="fill:darkcyan;stroke:tomato;stroke-width:2" />
</svg></p>
<p>As you can see SVG is quite straightforward. The drawing in enclosed in the <code><svg>...</svg></code> tags, and I'm using
the <code>polygon</code> element to draw the shapes by specifying the coordinates of all vertices. The appearance is then
modifies with the <code>style</code> attribute in a CSS-like syntax.</p>
<p>Turning to python and making graphics with <code>svgwrite</code> requires creating python representation of the drawing and
adding shapes to it. I'm only going to use <code>rect</code>, <code>circle</code> and <code>polygon</code> elements with a few standard attributes like: <code>fill</code>, <code>stroke</code> and <code>opacity</code>. Here's an example that illustrates basic functionality:</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span> <span class="nn">svgwrite</span>
<span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">filename</span><span class="o">=</span><span class="s2">"example.svg"</span><span class="p">,</span> <span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">140</span><span class="p">))</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span>
<span class="n">insert</span><span class="o">=</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">20</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">50</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="s2">"darkcyan"</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">"none"</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="mf">0.7</span><span class="p">))</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span>
<span class="n">insert</span><span class="o">=</span><span class="p">(</span><span class="mi">180</span><span class="p">,</span> <span class="mi">20</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="s2">"darkcyan"</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">"none"</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="mf">1.0</span><span class="p">))</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span>
<span class="n">center</span><span class="o">=</span><span class="p">(</span><span class="mi">290</span><span class="p">,</span> <span class="mi">60</span><span class="p">),</span> <span class="n">r</span><span class="o">=</span><span class="mi">40</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="s2">"tomato"</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">"none"</span><span class="p">))</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">polygon</span><span class="p">(</span>
<span class="n">points</span><span class="o">=</span><span class="p">[(</span><span class="mi">350</span><span class="p">,</span> <span class="mi">120</span><span class="p">),</span> <span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">120</span><span class="p">),</span> <span class="p">(</span><span class="mf">375.0</span><span class="p">,</span> <span class="mf">13.5</span><span class="p">)],</span> <span class="n">fill</span><span class="o">=</span><span class="s2">"tomato"</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">"none"</span><span class="p">,</span>
<span class="n">opacity</span><span class="o">=</span><span class="mf">0.8</span><span class="p">))</span>
</code></pre></div>
<p><svg baseProfile="full" height="140" version="1.1" width="400" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><rect fill="darkcyan" height="50" opacity="0.7" stroke="none" width="100" x="50" y="20" /><rect fill="darkcyan" height="100" opacity="1.0" stroke="none" width="50" x="180" y="20" /><circle cx="290" cy="60" fill="tomato" r="40" stroke="none" /><polygon fill="tomato" opacity="0.8" points="350,120 400,120 375.0,13.5" stroke="none" /></svg></p>
<p>That's it! We now know enough about SVG to start producing more advanced designs programmatically.</p>
<h2 id="square-lattice">Square lattice</h2>
<p>Square lattice is the best place to start since it's the simplest. What I want, specifically is a regular
grid of points whose coordinates will be expressed as the SVG canvas coordinates<sup id="fnref:2"><a class="footnote-ref" href="#fn:2">2</a></sup>. Once we have the coordinates
we can start putting shapes in those locations. To define a square lattice it is sufficient to define a single parameter
<code>a</code> which is the lattice parameter<sup id="fnref:3"><a class="footnote-ref" href="#fn:3">3</a></sup>. Defining the lattice and size of our graphics will allow us to determine how
many tiles we'll be able to fit on the graphic of a given size. Here's how it might look in code:</p>
<div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">SquareLattice</span><span class="p">:</span>
<span class="s2">"Square Lattice in 2D"</span>
<span class="n">a</span><span class="p">:</span> <span class="nb">float</span>
<span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">row</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">col</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">center</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">True</span><span class="p">)</span> <span class="o">-></span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span>
<span class="s2">"Coordinates for a lattice position (row, col)"</span>
<span class="k">if</span> <span class="n">center</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span>
<span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="p">(</span><span class="n">row</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">),</span>
<span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="p">(</span><span class="n">col</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">)],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">row</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">col</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">size</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span>
<span class="s2">"Size of the lattice in lattice postions"</span>
<span class="n">s</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">width</span> <span class="o">/</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">s</span><span class="p">,</span> <span class="n">s</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>
</code></pre></div>
<p><code>SquareLatttice</code> takes a single parameter <code>a</code> and with the <code>coords</code> method will generate <code>(x, y)</code>
coordinates for each of the integer lattice positions <code>(i, j)</code>. To see how it works let's generate a few coordinate pairs:</p>
<div class="codehilite"><pre><span></span><code><span class="n">lat</span> <span class="o">=</span> <span class="n">SquareLattice</span><span class="p">(</span><span class="n">a</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">5.</span> <span class="mf">5.</span><span class="p">]</span>
<span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">15.</span> <span class="mf">5.</span><span class="p">]</span>
<span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span> <span class="mf">5.</span> <span class="mf">15.</span><span class="p">]</span>
<span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">25.</span> <span class="mf">25.</span><span class="p">]</span>
</code></pre></div>
<p>By default, we get coordinates for the center of each cell. It will only be important later on,
but it's good to notice. </p>
<p>Now that we have the lattice positions we can put some elements there. For a square lattice, squares
are a good starting point. Here's how we can create the drawing and repeat
squares on the predefined lattice:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span>
<span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span>
<span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span>
<span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"darkcyan"</span><span class="p">,</span>
<span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">,</span>
<span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
<span class="p">):</span>
<span class="s2">"Draw SVG squares on a lattice"</span>
<span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span>
<span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span>
<span class="n">insert</span><span class="o">=</span><span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span>
<span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">side</span><span class="p">,</span> <span class="n">side</span><span class="p">),</span>
<span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span>
<span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span>
<span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">dwg</span>
</code></pre></div>
<p>Since we can independently control the lattice spacing and size of the squares let's see a few variants:</p>
<div class="row justify-content-center py-3">
<div class="col-4 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a40-square-5.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Lattice a=40, square size=5</figcaption>
</figure>
</div>
<div class="col-4 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a40-square-20.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Lattice a=40, square size=20</figcaption>
</figure>
</div>
<div class="col-4 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a90-square-80.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Lattice a=90, square size=80</figcaption>
</figure>
</div>
</div>
<p>Nothing spectacular yet but we need this foundation to be able to quickly modify the designs.</p>
<h3 id="making-things-more-interesting">Making things more interesting</h3>
<p>There are many ways to modify the grids we've produced so far to make it more interesting. Obvious
example is probably changing the color channel but before going there I will try changing the size of
the squares. The effect that I think it will have to imitate brightness where smaller shapes will
be less intense and bigger elements more intense. Alternatively I would try to modify <em>opacity</em>
in a similar fashion, but I think encoding brightness with size will be a nicer aesthetic. </p>
<p>I want to simulate a fade effect with a square size gradient. I would
like to try out a few different gradient functions, but I'll start with the linear gradient along the <em>y</em> axis,
that we'll use to modify elements.</p>
<p>With the function below I'm going to be able to create an array with coefficients <code>[0.0, 1.0]</code> to scale
the squares by. Since the gradient will be applied along the <em>y</em> axis the size of the gradient array
is the same as the number of lattice points in the <em>y</em> direction.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">linear_gradient</span><span class="p">(</span>
<span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">offset</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span>
<span class="n">geom</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span>
<span class="n">start</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.01</span>
<span class="p">):</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">floor</span><span class="p">(</span><span class="n">size</span> <span class="o">*</span> <span class="n">offset</span><span class="p">)</span>
<span class="k">if</span> <span class="n">geom</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span>
<span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">,</span> <span class="n">endpoint</span><span class="o">=</span><span class="kc">False</span><span class="p">),</span>
<span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
<span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span>
<span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">,</span> <span class="n">endpoint</span><span class="o">=</span><span class="kc">False</span><span class="p">),</span>
<span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
<span class="p">))</span>
</code></pre></div>
<p>To make it work we'll need a small modification to the drawing function to take <code>gradient</code> argument
and scale the squares with it. Here's the updated function:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span>
<span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span>
<span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span>
<span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"darkcyan"</span><span class="p">,</span>
<span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">,</span>
<span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
<span class="hll"> <span class="n">gradient</span><span class="p">:</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
</span><span class="p">):</span>
<span class="s2">"Draw SVG squares on a lattice with gradient"</span>
<span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span>
<span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span>
<span class="hll"> <span class="n">size</span> <span class="o">=</span> <span class="n">side</span> <span class="o">*</span> <span class="n">gradient</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span>
<span class="n">insert</span><span class="o">=</span><span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span>
<span class="hll"> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span>
</span> <span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span>
<span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span>
<span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">dwg</span>
</code></pre></div>
<p>Putting it all together and generating the grid with the gradient gives the following result</p>
<div class="row justify-content-center py-3">
<div class="col text-center">
<figure class="figure">
<img src="../../static/img/square-lattice-a40-square-15-gradient.svg" width="75%" class="mb-2">
<figcaption class="figure-caption">Lattice a=40, square size=15 with a linear gradient</figcaption>
</figure>
</div>
</div>
<p>If we draw squares that are a bit larger we'll notice a slight problem with the current approach since
the default <code>rect</code> element is drawn with respect to top left corner which results in an alignment to the left.</p>
<div class="row justify-content-center py-3">
<div class="col text-center">
<figure class="figure">
<img src="../../static/img/square-lattice-a80-square-75-gradient-left.svg" width="75%" class="mb-2">
<figcaption class="figure-caption">Lattice a=80, square size=75 with a linear gradient</figcaption>
</figure>
</div>
</div>
<p>Although it might be desirable is some cases I would rather have all the squares centered with respect
to the lattice positions. To make that happen I created a custom <code>Square</code> class that I'll use with
the <code>polygon</code> element instead of <code>rect</code>. Here's the <code>Square</code> class and updated <code>draw_squares</code> function</p>
<div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">Square</span><span class="p">:</span>
<span class="s2">"Square"</span>
<span class="n">side</span><span class="p">:</span> <span class="nb">int</span>
<span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s2">"Vertex coordinates of a square with repect to its center (0, 0)"</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span>
<span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">])</span>
<span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span>
<span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span>
<span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span>
<span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span>
<span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"darkcyan"</span><span class="p">,</span>
<span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">,</span>
<span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
<span class="n">gradient</span><span class="p">:</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">):</span>
<span class="s2">"Draw SVG squares on a lattice"</span>
<span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span>
<span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span>
<span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span>
<span class="n">square</span> <span class="o">=</span> <span class="n">Square</span><span class="p">(</span><span class="n">side</span><span class="o">=</span><span class="n">side</span> <span class="o">*</span> <span class="n">gradient</span><span class="p">[</span><span class="n">j</span><span class="p">])</span>
<span class="n">points</span> <span class="o">=</span> <span class="n">square</span><span class="o">.</span><span class="n">coords</span><span class="p">()</span> <span class="o">+</span> <span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span>
<span class="n">dwg</span><span class="o">.</span><span class="n">polygon</span><span class="p">(</span>
<span class="n">points</span> <span class="o">=</span> <span class="n">points</span><span class="p">,</span>
<span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span>
<span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span>
<span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">dwg</span>
</code></pre></div>
<p>Redrawing the previous graphic with the updates gives the expected result where all the squares are now
centered around lattice positions.</p>
<div class="row justify-content-center py-3">
<div class="col text-center">
<figure class="figure">
<img src="../../static/img/square-lattice-a80-square-75-gradient-center.svg" width="75%" class="mb-2">
<figcaption class="figure-caption">Lattice a=80, square size=75 with a linear gradient centered</figcaption>
</figure>
</div>
</div>
<h3 id="ridge">Ridge</h3>
<p>With a slight modification to the <code>linear_gradient</code> function we can make the gradient symmetric with respect
to the center. I'm calling it <code>ridge</code> gradient, and it should create a fade effect going both up and down.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">ridge</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">geom</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.01</span><span class="p">):</span>
<span class="n">c</span> <span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
<span class="k">if</span> <span class="n">geom</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span>
<span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">),</span>
<span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
<span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span>
<span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">),</span>
<span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
<span class="p">))</span>
</code></pre></div>
<p>Applying the ridge gradient give a nicely symmetric pattern.</p>
<div class="row justify-content-center py-3">
<div class="col text-center">
<figure class="figure">
<img src="../../static/img/square-lattice-a20-square-15-gradient-ridge.svg" width="75%" class="mb-2">
<figcaption class="figure-caption">Lattice a=20, square size=15 with a ridge gradient</figcaption>
</figure>
</div>
</div>
<p>I guess it's easy to see how to modify the current code to change the direction or produce a different
fade pattern. In the next step we're going to update the gradient from 1D to 2D to allow for more
sophisticated patterns.</p>
<h3 id="radial-gradient">Radial gradient</h3>
<p>So far we've applied gradient along a single axis, but there is nothing preventing
us from extending the gradient to 2D. One familiar concept we can try to implement is radial gradient.
We'll need to modify the function for drawing the squares to modify the size of squares
based on both <code>i</code> and <code>j</code> lattice positions, and we'll need a 2D array with sizes
representing brightness. The <code>radial_gradient</code> function evaluates the distance between
the <code>origin</code> of the gradient and given lattice position and scales the value to <code>[0.0, 1.0]</code>
range within the specified <code>radius</code>. </p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">radial_gradient</span><span class="p">(</span>
<span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span>
<span class="n">origin</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">,</span>
<span class="n">radius</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
<span class="n">width</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">height</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span>
<span class="s2">"Compute brightness for a radial gradient"</span>
<span class="k">def</span> <span class="nf">radial_distance</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">coords</span><span class="p">,</span> <span class="n">radius</span><span class="p">):</span>
<span class="n">distance</span> <span class="o">=</span> <span class="n">euclidean</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">coords</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">min</span><span class="p">(</span><span class="n">distance</span> <span class="o">/</span> <span class="n">radius</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
<span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="n">gradient</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span><span class="p">))</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span>
<span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span>
<span class="n">gradient</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span>
<span class="n">gradient</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">],</span>
<span class="n">radial_distance</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="n">radius</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">gradient</span>
</code></pre></div>
<p>Now the returned array is 2D and can be applied to our grid. The modification to <code>draw_squares</code>
is straightforward, so I'll not repeat the code here. Since the number in the gradient array
are scaled to <code>[0.0, 1.0]</code> we can also compute the inverse gradient by subtracting the gradient from 1.
Here are the two variants we get:</p>
<div class="row justify-content-center py-3">
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a20-square-15-radial-grad.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Lattice a=20, square size=15, radial gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a20-square-15-radial-grad-inverse.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Lattice a=20, square size=15, inverse radial gradient</figcaption>
</figure>
</div>
</div>
<p>A nice effect here is that side by side these look like the right hand side one is "taken out" from the left hand
side one. </p>
<h2 id="hexagonal-lattice">Hexagonal lattice</h2>
<p>Square lattice is not the only choice for a lattice. It's the most symmetric looking, and therefore
it has an artificial or dry aesthetic. To check the effect that a lattice might have on the overall
graphic I'll try also a hexagonal lattice also known as honeycomb structure.</p>
<p>Just as in the case of a square lattice we need only a single parameter <code>a</code> to define the lattice.
Since for the hexagonal lattice the translation vectors are no longer parallel we need to take that
into account when computing lattice coordiantes<sup id="fnref:4"><a class="footnote-ref" href="#fn:4">4</a></sup>.</p>
<div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">HexLattice</span><span class="p">:</span>
<span class="s2">"Hexagonal lattice"</span>
<span class="n">a</span><span class="p">:</span> <span class="nb">float</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">vectors</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">]])</span>
<span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">row</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">col</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span>
<span class="s2">"Coordinates for a lattice position (row, col)"</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">row</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">vectrors</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">col</span>
</code></pre></div>
<p>With the lattice defined I would like to have regular hexagons in cell positions
in analogy to having squares on a square lattice. For a regular hexagon with the
side <code>a</code> here is a diagram with the coordinates of the vertices with respect to the center of
the hexagon<sup id="fnref:5"><a class="footnote-ref" href="#fn:5">5</a></sup>.</p>
<div class="row justify-content-center py-3">
<div class="col text-center">
<figure class="figure">
<img src="../../static/img/hexagon.svg" width="500px" class="mb-2">
<figcaption class="figure-caption">Coordinates of the hexagon vertices with respect to its center</figcaption>
</figure>
</div>
</div>
<p>Based on that we can code up the <code>Hexagon</code> class to be drawn on the canvas as the
<code>polygon</code> element.</p>
<div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">Hexagon</span><span class="p">:</span>
<span class="s2">"Regular Hexagon"</span>
<span class="n">side</span><span class="p">:</span> <span class="nb">int</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s2">"Height of a flat top regular hexagon"</span>
<span class="k">return</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span>
<span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s2">"Coordinates of vertices with repect to hexagon center (0, 0)"</span>
<span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span>
<span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
<span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span>
<span class="p">])</span>
</code></pre></div>
<p>With that we should be able to recreate the analogues of the graphics we created
with squares on a square lattice. We'll also need a <code>draw_hexagons</code> function, but
that again is pretty analogous to <code>draw_squares</code>, so I'll leave it for you to figure adapt.</p>
<h2 id="graphics">Graphics</h2>
<p>With all things in place here is a side by side comparison of the graphics with and
without different gradient effects. You might notice that there is an extra wavy pattern
generated which is also at the head of this post. It's essentially a sin wave with a
fade based on distance from the curve. If you're curious about it let me in the comments
and I'll more.</p>
<div class="row justify-content-center py-3">
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Square lattice a=40, square side=20</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice padding=5, hex side=20</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, linear gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice padding=5, hex side=20, linear gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25-gradient-ridge.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, ridge gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5-ridge-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, ridge gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25-sin-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Sq Lattice a=33, square size=25, sin gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5-sin-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, sin gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25-radial-grad.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, radial gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5-radial-gradient.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, radial gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/square-lattice-a33-square-25-radial-grad-inverse.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, inverse radial gradient</figcaption>
</figure>
</div>
<div class="col-6 py-2">
<figure class="figure">
<img src="../../static/img/hex-lattice-side20-padding5-radial-gradient-inverse.svg" width="100%" class="mb-2">
<figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, inverse radial gradient</figcaption>
</figure>
</div>
</div>
<h2 id="summary">Summary</h2>
<p>Generating vector graphics programmatically was a lot more exciting than I expected
and I was surprised how much I got sucked into it. Once it was clear how to create
shapes and position them on the canvas create variations on the designs was quite easy.
I am quite happy with the results so far and definitely prefer the hex-on-hex variant,
but I would like to explore further and try out a few more things to make the designs
a bit more organic and less perfect.</p>
<p>To take it further I would probably explore ways of introducing randomness in various
channels and to encode different attributes. So far all the designs were quite simple
and deterministic and randomness would
take them to next level. Breaking the perfect symmetries either by deforming the lattice
of making the shapes a bit irregular (or both) might also produce an interesting result.</p>
<p>On top of that I think that introducing various kinds of defects and imperfections
inspired by real crystal structures where those phenomena occur would add yet another dimension
to explore. </p>
<h2 id="footnotes">Footnotes</h2>
<div class="footnote">
<hr />
<ol>
<li id="fn:1">
<p>This is going to be a nice journey back in to my crystallgraphy lectures. <a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:2">
<p>By default SVG coordinates start in the top-left corner at <code>(0, 0)</code> and go to <code>(width, height)</code> with the pixels as unit. <a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:3">
<p>In general there are 5 types <a href="https://en.wikipedia.org/wiki/Bravais_lattice">Bravais lattices</a> in 2D, and they can be
specified by defining the parameters of the unit cell. <a class="footnote-backref" href="#fnref:3" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:4">
<p>If you want to know learn more about hexagonal coordinates check out <a href="https://www.redblobgames.com/grids/hexagons/">this post</a>. <a class="footnote-backref" href="#fnref:4" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:5">
<p>I choose the flat top orientation of the hexagon, an alternative would be the pointy
top hexagon orientation. They differ by a 90-degree rotation of all vertices, so it's quite
straightforward to switch between them. <a class="footnote-backref" href="#fnref:5" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>Better select forms with django and Bootstrap 5http://localhost/blog/better-select-form-django/2022-10-16T23:55:00Z2022-10-16T23:55:00Z<p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/kelly-sikkema-hLit2zL-Dhk-unsplash-small.jpg" alt="web design"></p>
<p class="text-muted small text-end">Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kelly Sikkema</a> on <a href="https://unsplash.com/s/photos/user-interaction?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
</p>
<hr>
<p>I enjoy working with <a href="https://www.djangoproject.com/">Django</a> for many
reasons but mostly since I can focus on what I want my app to do
and django happily takes care of the rest. In 2022 it's probably not
the sexiest choice when it comes to web frameworks, but there are good reasons it is still as popular as it is.
There is a recent wave of excitement about server side rendering frameworks thanks
to a new batch of tools like <a href="https://htmx.org/">HTMX</a>, <a href="https://www.django-unicorn.com/">django-unicorn</a> and
<a href="https://github.com/jonathan-s/django-sockpuppet">django-sockpuppet</a> that give django
superpowers and put into question the <code>React</code> based paradigm for building apps.
But that's a digression for another time.</p>
<p>Django takes care of a lot of stuff behind the scenes and that is a solid productivity
boost. Being a mature framework means that is has opinions (mostly for very good reasons) and
provides you with defaults that are sensible in most o the cases. There are
however cases when it's worth to make some changes to improve end-user experience.</p>
<p>One such aspect is the default UI widget for selecting items. I am of course
talking about <a href="https://en.wikipedia.org/wiki/Drop-down_list">dropdowns</a>.
They have been around since forever and although being familiar they are usually
not the best choice when it comes to modern UX standards. Out in the wild
you can find professionals suggesting <a href="https://www.nngroup.com/articles/drop-down-menus/">caution when using dropdowns</a>
and calls to <a href="https://adrianroselli.com/2020/03/stop-using-drop-down.html">stop using drop-downs</a> at all. </p>
<p>Personally I would like to adhere to good UX practices as far as I can
and although my level of comfort increases with
proximity to backend side of things I cannot be annoyed by all the low hanging
fruit that sometimes plague otherwise good products. Wanting things to look good
without a massive design effort is one of the reasons I usually reach for a
frontend framework, most often <a href="https://getbootstrap.com/">bootstrap</a>.
With django that means using <a href="https://github.com/django-crispy-forms/django-crispy-forms">django crispy forms</a>
to get the forms looking sharp and consistent with an extra line in the template.</p>
<h2 id="making-choices">Making choices</h2>
<p>One of the things I was recently working on is a django-based dashboard where
I wanted to have the option to choose the aggregation level between: <code>daily</code>, <code>weekly</code>, <code>monthly</code> aggregates.
Of course my first pass was the default <code>forms.Select</code> widget that is a dropdown.
Once I saw the result on the screen though, I felt something is off, and that this should probably be improved.
Usually I try to move on until I have a working prototype that I can iterate over. Following the
Kent Beck's principle</p>
<blockquote class="blockquote text-center py-5">make it work, make it right, make it fast</blockquote>
<p>When I got everything wired up and working it seemed like a good time get back to the select widget and scratch that itch. </p>
<p>On the django side I had a form to represent the choices </p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span>
<span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">choices</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s2">"1D"</span><span class="p">,</span> <span class="s2">"Daily"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"1W"</span><span class="p">,</span> <span class="s2">"Weekly),</span>
<span class="p">(</span><span class="s2">"1M"</span><span class="p">,</span> <span class="s2">"Monthly"</span><span class="p">)</span>
<span class="p">]</span>
<span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">"Frequency"</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">)</span>
</code></pre></div>
<p>which was being rendered in a template with pretty standard django</p>
<div class="codehilite"><pre><span></span><code>{% load crispy_forms_tags %}
...
{{ form|crispy }}
</code></pre></div>
<p>The effect isn't surprising either</p>
<div class="row mb-5">
<div class="col-md-4 mx-auto">
<div id="div_id_freq" class="mb-3"> <label for="id_freq" class="form-label requiredField">
Frequency<span class="asteriskField">*</span> </label> <select name="freq" class="select form-select" id="id_freq">
<option value="1D">Daily</option> <option value="1W" selected="">Weekly</option>
<option value="1M">Monthly</option>
</select>
</div>
</div>
</div>
<p>I didn't want for the options to be hidden, so the next best thing was to switch to a radio select
by changing the for widget to <code>RadioSelect</code> and keeping the template as is</p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span>
<span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">choices</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s2">"1D"</span><span class="p">,</span> <span class="s2">"Daily"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"1W"</span><span class="p">,</span> <span class="s2">"Weekly),</span>
<span class="p">(</span><span class="s2">"1M"</span><span class="p">,</span> <span class="s2">"Monthly"</span><span class="p">)</span>
<span class="p">]</span>
<span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span>
<span class="n">label</span><span class="o">=</span><span class="s2">"Frequency"</span><span class="p">,</span>
<span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span>
<span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(),</span>
<span class="p">)</span>
</code></pre></div>
<p>This gives</p>
<div class="row mb-5">
<div class="col-md-4 mx-auto">
<div id="div_id_freq" class="mb-3">
<label class="form-label requiredField">Frequency<span class="asteriskField">*</span></label>
<div>
<div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="1D" id="id_freq_0" required=""> <label for="id_freq_0" class="form-check-label">
Daily
</label> </div>
<div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="W" id="id_freq_1" required="" checked=""> <label for="id_freq_1" class="form-check-label">
Weekly
</label> </div>
<div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="MS" id="id_freq_2" required=""> <label for="id_freq_2" class="form-check-label">
Monthly
</label> </div>
</div>
</div>
</div>
</div>
<p>Bettter but I still wasn't happy with the result. When looking for inspiration I
found <a href="https://www.learnui.design/blog/4-rules-intuitive-ux.html">this post</a>
suggesting a segmented button that reminded me of what I saw in latest
<a href="https://getbootstrap.com/docs/5.0/components/button-group/#checkbox-and-radio-button-groups">Bootstrap 5 docs</a>
so I decided to give it a go since I was already using Bootstrap.</p>
<p>Looking at the example from Bootstrap it became clear that I would have to render
the form manually. The example I started from was this:</p>
<div class="codehilite"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn-group"</span> <span class="na">role</span><span class="o">=</span><span class="s">"group"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Basic radio toggle button group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"radio"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn-check"</span> <span class="na">name</span><span class="o">=</span><span class="s">"btnradio"</span> <span class="na">id</span><span class="o">=</span><span class="s">"btnradio1"</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">"off"</span> <span class="na">checked</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-outline-primary"</span> <span class="na">for</span><span class="o">=</span><span class="s">"btnradio1"</span><span class="p">></span>Radio 1<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"radio"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn-check"</span> <span class="na">name</span><span class="o">=</span><span class="s">"btnradio"</span> <span class="na">id</span><span class="o">=</span><span class="s">"btnradio2"</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">"off"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-outline-primary"</span> <span class="na">for</span><span class="o">=</span><span class="s">"btnradio2"</span><span class="p">></span>Radio 2<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"radio"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn-check"</span> <span class="na">name</span><span class="o">=</span><span class="s">"btnradio"</span> <span class="na">id</span><span class="o">=</span><span class="s">"btnradio3"</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">"off"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-outline-primary"</span> <span class="na">for</span><span class="o">=</span><span class="s">"btnradio3"</span><span class="p">></span>Radio 3<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>which looks like </p>
<div class="row justify-content-center py-5">
<div class="col text-center">
<div class="btn-group" role="group" aria-label="Basic radio toggle button group">
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="btnradio1">Radio 1</label>
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off">
<label class="btn btn-outline-primary" for="btnradio2">Radio 2</label>
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off">
<label class="btn btn-outline-primary" for="btnradio3">Radio 3</label>
</div>
</div>
</div>
<p>Pretty nice! I went ahead and updated my template and instead of the <code>{{ form|crispy }}</code>
elements I went ahead and unfolded the form and added the classes and attributes to render
the button group:</p>
<div class="codehilite"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn-group"</span> <span class="na">role</span><span class="o">=</span><span class="s">"group"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Basic radio toggle button group"</span><span class="p">></span>
{% for choice in form.freq %}
{{ choice.tag }}
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-outline-primary"</span> <span class="na">for</span><span class="o">=</span><span class="s">"{{ choice.id_for_label }}"</span><span class="p">></span>{{ choice.choice_label }}<span class="p"></</span><span class="nt">label</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>Everything looked nice when rendered however when clicking on the buttons they wouldn't stay selected as they should. They would
change back to unselected which was annoying. I had to compare the original HTML with my rendered version to
realize that the <code><input></code> tags in my case were missing s <code>class="btn-check"</code> attribute, so I modified
the form to have that added </p>
<div class="codehilite"><pre><span></span><code><span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">choices</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s2">"1D"</span><span class="p">,</span> <span class="s2">"Daily"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"1W"</span><span class="p">,</span> <span class="s2">"Weekly),</span>
<span class="p">(</span><span class="s2">"1M"</span><span class="p">,</span> <span class="s2">"Monthly"</span><span class="p">)</span>
<span class="p">]</span>
<span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span>
<span class="n">label</span><span class="o">=</span><span class="s1">'Category'</span><span class="p">,</span>
<span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span>
<span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s1">'class'</span><span class="p">:</span><span class="s1">'btn-check'</span><span class="p">}),</span>
<span class="p">)</span>
</code></pre></div>
<p>Adding that attribute to the form did the trick. With that little tweak I was able to achieve way better UX and eliminate
and unnecessary dropdown. </p>
<div class="row justify-content-center py-5">
<div class="col text-center">
<div class="btn-group" role="group" aria-label="Basic radio toggle button group">
<input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio1" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="freqbtnradio1">Daily</label>
<input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio2" autocomplete="off">
<label class="btn btn-outline-primary" for="freqbtnradio2">Weekly</label>
<input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio3" autocomplete="off">
<label class="btn btn-outline-primary" for="freqbtnradio3">Monthly</label>
</div>
</div>
</div>
<p>This was it for me but in case you have a few more choices to select from (maybe 5-9 items) and you still
would like to present them to the user this might not be a good choice since button group will
most likely be too wide for small screens and overflow. </p>
<p>In this case you can switch to splitting choices into individual buttons and arranging them in a grid
rather than a single row. Then your CSS framework can take over and manage the responsive layout for
smaller screens where you probably want to either switch to a more vertical layout or a grid layout.</p>
<p>Let me add two more choices to my original form to illustrate how could it look like</p>
<div class="codehilite"><pre><span></span><code><span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">choices</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s2">"1D"</span><span class="p">,</span> <span class="s2">"Daily"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"1W"</span><span class="p">,</span> <span class="s2">"Weekly),</span>
<span class="p">(</span><span class="s2">"1M"</span><span class="p">,</span> <span class="s2">"Monthly"</span><span class="p">)</span>
<span class="p">(</span><span class="s2">"1Q"</span><span class="p">,</span> <span class="s2">"Quarterly"</span><span class="p">)</span>
<span class="p">(</span><span class="s2">"1Y"</span><span class="p">,</span> <span class="s2">"Yearly"</span><span class="p">)</span>
<span class="p">]</span>
<span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span>
<span class="n">label</span><span class="o">=</span><span class="s1">'Category'</span><span class="p">,</span>
<span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span>
<span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s1">'class'</span><span class="p">:</span><span class="s1">'btn-check'</span><span class="p">}),</span>
<span class="p">)</span>
</code></pre></div>
<p>In the template we need to wrap all choice fields in a <code>row</code> and each input + label
tag in a <code>col</code> with appropriate widths for each display breakpoint. This is bootstrap
specific so check out <a href="https://getbootstrap.com/docs/5.2/layout/breakpoints/">bootstrap layout</a>
if you want to know more. </p>
<div class="codehilite"><pre><span></span><code><span class="p"><</span><span class="nt">form</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"row"</span><span class="p">></span>
{% for choice in form.freq %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"col-4 col-md-3 col-lg-2"</span><span class="p">></span>
{{ choice.tag }}
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-outline-primary"</span> <span class="na">for</span><span class="o">=</span><span class="s">"{{ choice.id_for_label }}"</span><span class="p">></span>{{ choice.choice_label }}<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>This is now rendered as independent radio buttons </p>
<div class="row justify-content-center py-5">
<div class="col text-center">
<form>
<div class="row justify-content-center">
<div class="col-4 col-md-3 col-lg-2 py-2">
<input type="radio" class="btn-check" name="choice" id="radio-1" value="1D">
<label class="btn btn-outline-primary btn-lg" for="radio-1">Daily</label>
</div>
<div class="col-4 col-md-3 col-lg-2 py-2">
<input type="radio" class="btn-check" name="choice" id="radio-2" value="1W">
<label class="btn btn-outline-primary btn-lg" for="radio-2">Weekly</label>
</div>
<div class="col-4 col-md-3 col-lg-2 py-2">
<input type="radio" class="btn-check" name="choice" id="radio-3" value="1M">
<label class="btn btn-outline-primary btn-lg" for="radio-3">Monthly</label>
</div>
<div class="col-4 col-md-3 col-lg-2 py-2">
<input type="radio" class="btn-check" name="choice" id="radio-4" value="1Q">
<label class="btn btn-outline-primary btn-lg" for="radio-4">Quartely</label>
</div>
<div class="col-4 col-md-3 col-lg-2 py-2">
<input type="radio" class="btn-check" name="choice" id="radio-5" value="1Y">
<label class="btn btn-outline-primary btn-lg" for="radio-5">Yearly</label>
</div>
</div>
</form>
</div>
</div>
<h2 id="summary">Summary</h2>
<p>If you're using django and a css framework like bootstrap it does not take much
effort to adapt your forms and improve overall user experience and at the same
time conform to modern standard for look and feel of your web app. If you have
similar quick improvements please share below since I'm always on the lookout
for how to improve usability.</p>
<hr>Datasets beat Modelshttp://localhost/blog/datasets-beat-models/2021-06-24T12:00:00Z2021-06-24T12:00:00Z<p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/1624540361257.jpeg" alt="datasets beat models"></p>
<hr>
<p>I know it’s easy to get carried away by all the shiny, sophisticated ML models out there,
but we need to have a serious conversation about datasets. In most industrial applications
of ML it’s the datasets that limit performance not model complexity. </p>
<p>Lack of high quality training datasets is the biggest challenge for industrial machine
learning right now. That should not be surprising if you consider how much bigger the
supply of models is compared to datasets. A big portion of the ML algorithms development
happens thanks to open source and academic communities which gladly publish their work in
exchange for reputation points. In contrast, developing and curating datasets is often
less valued and not as glamorous. </p>
<p>My experience from working on data-driven solutions for industrial problems is that there
is no shortage of data and huge volumes are available in raw form; however converting them
into training and validation datasets is what usually blocks ML/AI progress. </p>
<p>I won't be controversial if I attribute 80% of a data scientist's time to what is collectively
termed preprocessing, cleaning or structuring the data. The result of all of those activities
is a dataset that hopefully accurately represents reality and can serve as ground truth for
model development and evaluation. Creating high quality datasets however takes effort, skills
and a right blend of domain knowledge and experience with data.</p>
<p>This is exactly what we are passionate about at unifai. We are approaching dataset development
as an engineering practice and we are creating high quality datasets to increase adoption of
machine learning in the industry. Our experience in data science, data engineering, software
development and heavy asset industry puts us in a unique position to deliver datasets with a
perfect mix of domain knowledge.</p>
<p>Unifai is not alone in betting on datasets, one of the biggest names in AI: Andrew Ng recently
started promoting a more data-centric view of AI instead of the model-centric view. In a recent
announcement together with landing.ai and deeplearning.ai he launched a competition that focuses
on improving data while keeping the model fixed to inspire new methods for improving data.</p>
<p>Get in touch with me if you share similar challenges or if you're sitting on piles of raw data
and your organization wants to get some quick wins with machine learning.</p>
<hr>
<p>This post was originally published on <a href="https://www.linkedin.com/pulse/datasets-beat-models-lukasz-mentel">LinkedIn</a>.</p>Zeolite pore size oddityhttp://localhost/blog/zeolite-pore-size-oddity/2017-10-31T12:00:00Z2017-10-31T12:00:00Z<p>One of the ways to classify zeolites is usually by the size of their pore
openings given in the peculiar unit of T atom count. T atom stands for
tetrachedrally coordinated atoms, usually the term lumps together silicon and aluminum in case of
zeolites or aluminum and phosphorus in case of aluminumphpsphates. The
structural units characterising pores are usually rings from which the tube-like channels are formed
(as shown below) and therefore the as described as <em>n</em> membered rings or <em>n</em>MR
in short. Where <em>n</em> can be as small as 4 and as large as 20.</p>
<p>[picture of 8MR, 10MR and 12MR with a corresponding tube]</p>
<p>By inspecting the figure below showing the distribution of ring sizes among the
known zeolite structures it is easy to see that 8, 10, and 12 MR structures
dominate over the remaining topologies. Which justifies the why this parameter
might serve as a good classification. It should also be taken into account that
for most of the interesting applications topologies with MR < 8 are not
intersting since molecules cannot enter the small channels and channels larger
than 14 would have very similar properties.</p>
<p>IUPAC classification into nano, micro and mesoporous structures in a <a href="https://doi.org/10.1351/pac199466081739">technical
report</a>.</p>
<p>It is customary to call 8, 10 and 12MR, <em>small</em>, <em>medium</em> and <em>large</em> pore
zeolites suggesting that this classification provides distinct groups of
similar characteristing being size. While intuitively and qualitatively correct,
applying it directly can lead to incorrect insights since this MR measure
provides limited structural information. A closer inspection of each class
revelas that in each class there is a distribution of sizes as evaluated by
different criteria actually associated with a more functional definition of
pore size.</p>
<p>apply clustering based on size largst cavity diameter (LCD) and some other
features related to functional size and compare them to the classification
based on MR and see which are misclassified.</p>Superimpose chemical structureshttp://localhost/blog/superimpose-structures/2017-09-28T12:00:00Z2017-09-28T12:00:00Z<p>I was struggling recently how to present multiple chemical structures that differed
slightly in their geometry but </p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
<span class="kn">from</span> <span class="nn">ase</span> <span class="kn">import</span> <span class="n">Atoms</span>
<span class="kn">import</span> <span class="nn">ase.io</span>
<span class="k">def</span> <span class="nf">read_structures</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">fpatt</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="sd">'''</span>
<span class="sd"> Read all the structures from `path` that match the `fpatt` pattern</span>
<span class="sd"> '''</span>
<span class="n">fpatt</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">fpatt</span><span class="p">)</span>
<span class="n">out</span> <span class="o">=</span> <span class="p">{</span><span class="s1">'II'</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">(),</span> <span class="s1">'III'</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">(),</span> <span class="s1">'IV'</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">()}</span>
<span class="k">for</span> <span class="n">fname</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">listdir</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">fpatt</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="n">fname</span><span class="p">)</span>
<span class="k">if</span> <span class="n">m</span><span class="p">:</span>
<span class="n">symbol</span><span class="p">,</span> <span class="n">oxs</span> <span class="o">=</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">out</span><span class="p">[</span><span class="n">oxs</span><span class="p">][</span><span class="n">symbol</span><span class="p">]</span> <span class="o">=</span> <span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">fname</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">verbose</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">'ERROR '</span><span class="p">,</span> <span class="n">fname</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">'Read: '</span><span class="p">,</span> <span class="s1">', '</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s1">'</span><span class="si">{}</span><span class="s1">: </span><span class="si">{}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">out</span><span class="o">.</span><span class="n">items</span><span class="p">()]))</span>
<span class="k">return</span> <span class="n">out</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">path</span> <span class="o">=</span> <span class="s1">'AlPO-5-H'</span>
<span class="n">patt</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'([A-Za-z]+)\(([IV]+)\)-AlPO-5-O2-H\.traj'</span>
<span class="n">halpo</span> <span class="o">=</span> <span class="n">read_structures</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">patt</span><span class="p">)</span>
<span class="n">atoms</span> <span class="o">=</span> <span class="n">Atoms</span><span class="p">()</span>
<span class="k">for</span> <span class="n">me</span><span class="p">,</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">halpo</span><span class="p">[</span><span class="s1">'IV'</span><span class="p">]</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="n">atoms</span> <span class="o">=</span> <span class="n">atoms</span> <span class="o">+</span> <span class="n">a</span>
<span class="n">transmittances</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.8</span><span class="p">]</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">atoms</span><span class="p">)</span>
<span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">'Me-AlPO-5-H.pov'</span><span class="p">,</span> <span class="n">atoms</span><span class="p">,</span>
<span class="n">rotation</span><span class="o">=</span><span class="s1">'-15y'</span><span class="p">,</span>
<span class="n">show_unit_cell</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="n">run_povray</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">display</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">pause</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">canvas_width</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span>
<span class="n">camera_type</span><span class="o">=</span><span class="s1">'perspective'</span><span class="p">,</span>
<span class="n">transmittances</span><span class="o">=</span><span class="n">transmittances</span><span class="p">)</span>
<span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">'Me-AlPO-5-H_0y.pov'</span><span class="p">,</span> <span class="n">atoms</span><span class="p">,</span>
<span class="n">rotation</span><span class="o">=</span><span class="s1">''</span><span class="p">,</span>
<span class="n">show_unit_cell</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="n">run_povray</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">display</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">pause</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">canvas_width</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span>
<span class="n">camera_type</span><span class="o">=</span><span class="s1">'perspective'</span><span class="p">,</span>
<span class="n">transmittances</span><span class="o">=</span><span class="n">transmittances</span><span class="p">)</span>
</code></pre></div>
<p>Rendering the scenes for multiple structures was a bit too slow for my laptop so I had to use
a supercomputer to speed things up.</p>
<p>This is a bit of a special case since the structures are taken from a periodic crystals
and therefore all have the same unit cell size. It simpifies things a lot since then all
the coordinates are given with respect to origin of the unit cell which is the same for
all the materials. For (non-periodic) molecules we would have to align the molecules first
since their coordinates can be defined with respect to different origins. One possible solution
is to minimize the <a href="https://en.wikipedia.org/wiki/Root-mean-square_deviation_of_atomic_positions">root mean square deviation</a> between the structures in order
to realign them which is usually done using the <a href="https://doi.org/10.1107/S0567739476001873">Kabsch</a> algorithm. For those curious
to see it in practice e is also a python available <a href="https://github.com/charnley/rmsd">rmsdpy</a>.</p>