<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Niladri Adhikary</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>http://blog.trintler.me/favicon.png</icon>
  <id>http://blog.trintler.me/</id>
  <link href="http://blog.trintler.me/" rel="alternate"/>
  <link href="http://blog.trintler.me/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Niladri Adhikary</rights>
  <subtitle>weblog of trintlermint</subtitle>
  <title>trintler's weblog</title>
  <updated>2026-04-28T11:53:16.841Z</updated>
  <entry>
    <author>
      <name>Niladri Adhikary</name>
    </author>
    <category term="Essay" scheme="http://blog.trintler.me/categories/Essay/"/>
    <category term="identity" scheme="http://blog.trintler.me/tags/identity/"/>
    <category term="security" scheme="http://blog.trintler.me/tags/security/"/>
    <category term="privacy" scheme="http://blog.trintler.me/tags/privacy/"/>
    <category term="verification" scheme="http://blog.trintler.me/tags/verification/"/>
    <category term="self-sovereign identity" scheme="http://blog.trintler.me/tags/self-sovereign-identity/"/>
    <content>
      <![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><h1 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h1><p>The rapid proliferation of artificially generated content has fundamentally altered the digital landscape. Recent research demonstrates that large language models can pass the Turing test<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Jones, C. R., & Bergen, B. K. (2025). _https://arxiv.org/abs/2503.23674_ Large language models pass the Turing test</a>. _arXiv_.">[1]</span></a></sup>, that LLM-generated content is now widespread across society<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Liang, W., Zhang, Y., Codreanu, M., Wang, J., Cao, H., & Zou, J. (2025). _arxiv.org/abs/2502.09747_ The widespread adoption of large language model-assisted writing across society. _arXiv_.">[2]</span></a></sup>, and that foundational barriers such as CAPTCHAs can be reliably solved by artificial intelligence<sup id="fnref:3"><a href="#fn:3" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Plesner, A., Vontobel, T., & Wattenhofer, R. (2024). Breaking reCAPTCHAv2. In _2024 IEEE 48th Annual COMPSAC_ (pp. 1047–1056). IEEE. doi:10.1109/compsac61105.2024.00142">[3]</span></a></sup>. In response, many platforms have turned to government-issued identity verification as a definitive solution. Although government-issued identity verification addresses the genuine challenge of distinguishing humans from automated systems, its widespread implementation commoditises personal identity data and exposes users to inconsistent privacy protections. Privacy-preserving alternatives, particularly open-source methodologies that allow individuals to identify themselves, should therefore be adopted as the primary verification mechanism. This essay first examines the rationale behind identity verification, then analyses the privacy risks inherent in current government-issued approaches, and argues that decentralised alternatives offer a superior balance between platform security and individual privacy.</p><span id="more"></span><h1 id="Current-Systems"><a href="#Current-Systems" class="headerlink" title="Current Systems"></a>Current Systems</h1><p>Conventional behavioural verification methods are no longer adequate to distinguish human users from automated systems, because the same AI capabilities that drive the verification problem also defeat its existing solutions. When Jones and Bergen (2025)<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Jones, C. R., & Bergen, B. K. (2025). _https://arxiv.org/abs/2503.23674_ Large language models pass the Turing test</a>. _arXiv_.">[1]</span></a></sup> demonstrate that large language models pass the Turing test, and Plesner et al. (2024)<sup id="fnref:3"><a href="#fn:3" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Plesner, A., Vontobel, T., & Wattenhofer, R. (2024). Breaking reCAPTCHAv2. In _2024 IEEE 48th Annual COMPSAC_ (pp. 1047–1056). IEEE. doi:10.1109/compsac61105.2024.00142">[3]</span></a></sup> show that AI systems solve reCAPTCHA challenges with high accuracy, any verification method relying purely on behavioural cues becomes increasingly untenable. This obsolescence, compounded by the widespread adoption of LLM-generated content<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Liang, W., Zhang, Y., Codreanu, M., Wang, J., Cao, H., & Zou, J. (2025). _arxiv.org/abs/2502.09747_ The widespread adoption of large language model-assisted writing across society. _arXiv_.">[2]</span></a></sup>, creates a verification vacuum that platforms understandably seek to fill.</p><h1 id="Risks-Behind-the-System"><a href="#Risks-Behind-the-System" class="headerlink" title="Risks Behind the System"></a>Risks Behind the System</h1><p>However, government-issued verification is not a proportionate response to this vacuum; rather, it introduces substantial privacy risks through third-party data handling and the commoditisation of personal identity. The inadequacy of passwords and security questions<sup id="fnref:4"><a href="#fn:4" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Berozashvili, T. (2024). Securing digital identities in the era of remote identity verification. doi:10.13140/RG.2.2.11839.11688">[4]</span></a></sup> has driven platforms toward biometric data and government-issued credentials, yet this escalation reveals a perverse dynamic: the mechanism designed to signal trustworthiness simultaneously normalises the surrender of state-issued documents to commercial entities. Verification badges structurally reward credential exposure, as Xiao et al. (2023)<sup id="fnref:5"><a href="#fn:5" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Xiao, M., Wang, M., Kulshrestha, A., & Mayer, J. (2023). _arxiv.org/abs/2304.14939_ Account verification on social media: User perceptions and paid enrollment. _arXiv_.">[5]</span></a></sup> demonstrate through Twitter and LinkedIn, where users voluntarily trade personal data for perceived credibility; although that evidence is drawn from two platforms, the underlying incentive plausibly recurs wherever verified status confers a comparable advantage. Moreover, government-issued digital credentials operate within complex privacy landscapes where data may traverse multiple jurisdictions with varying regulatory standards<sup id="fnref:6"><a href="#fn:6" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Flanagan, H. (2023). _openid.net/Government-issued-Digital-Credentials-and-the-Privacy-Landscape-Final_ Government-issued digital credentials and the privacy landscape. OpenID Foundation.">[6]</span></a></sup>. Centralised verification thus incentivises data aggregation, because the verifying authority accumulates a dataset whose commercial value frequently exceeds its original verification purpose, producing unequal access and gaps in accountability across jurisdictions<sup id="fnref:7"><a href="#fn:7" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="McGrath, K. (2016). Identity verification and societal challenges: Explaining the gap between service provision and development outcomes. _MIS Quarterly_, 40(2), 485–500. JSTOR">[7]</span></a></sup>. Consequently, when identity verification becomes a prerequisite for full platform participation, identity is transformed from an inherent right into a commoditised asset.</p><h1 id="Present-Alternatives"><a href="#Present-Alternatives" class="headerlink" title="Present Alternatives"></a>Present Alternatives</h1><p>Although privacy-preserving verification technologies remain at relatively early stages of deployment, their architectural principles demonstrate that security and user privacy need not be mutually exclusive objectives. Satybaldy et al. (2022)<sup id="fnref:8"><a href="#fn:8" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Satybaldy, A., Subedi, A., & Nowostawski, M. (2022). A framework for online document verification using self-sovereign identity technology. _Sensors_, 22(21), 8408. doi: 10.3390/s2221840">[8]</span></a></sup> propose a Self-Sovereign Identity framework that enables document verification without centralised storage of sensitive credentials, allowing individuals to maintain control over their personal data while proving their legitimacy. Building on this principle, Muth et al. (2023)<sup id="fnref:9"><a href="#fn:9" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="Muth, R., Galal, T., Heiss, J., & Tschorsch, F. (2023). Towards smart contract-based verification of anonymous credentials. In _Financial Cryptography and Data Security: FC 2022 International Workshops_ (pp. 481–498). Springer.">[9]</span></a></sup> research smart contract-based verification of anonymous credentials, enabling platforms to confirm user authenticity through cryptographic proofs rather than direct access to government identification. The architectural distinction is significant, as under SSI, the verifying platform never possesses the credential itself but only a cryptographic proof of its validity, which eliminates the centralised data aggregation problem identified in the preceding analysis. Critics may contend that SSI’s limited adoption renders government-issued verification the only scalable option today; although that concession is genuine in the short term, it does not justify entrenching centralised verification, since each new deployment that moves toward decentralised systems reduces the future cost of migration away from commoditised identity infrastructure. These alternatives demonstrate that platforms could adopt privacy-preserving approaches that address the legitimate need for human verification while structurally preventing the privacy compromises that government-issued identification demands.</p><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Government-issued identity verification, while addressing a genuine challenge posed by AI-generated content and automated systems, should not become the default mechanism for online platforms. The evidence presented demonstrates that the need for human verification is well established, given the capabilities of modern language models and the obsolescence of traditional barriers. However, current implementations create unacceptable privacy trade-offs by commoditising personal identity data and exposing users to inconsistent jurisdictional protections. Decentralised technologies such as Self-Sovereign Identity and anonymous credential verification offer viable pathways that reconcile platform security with individual privacy. Thus, the distinction is ultimately architectural. Systems that never possess a user’s credentials cannot commoditise them, regardless of jurisdictional pressures.</p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Jones, C. R., &amp; Bergen, B. K. (2025). <em>https://arxiv.org/abs/2503.23674</em> Large language models pass the Turing test</a>. <em>arXiv</em>.<a href="#fnref:1" rev="footnote"> ↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Liang, W., Zhang, Y., Codreanu, M., Wang, J., Cao, H., &amp; Zou, J. (2025). <em>arxiv.org/abs/2502.09747</em> The widespread adoption of large language model-assisted writing across society. <em>arXiv</em>.<a href="#fnref:2" rev="footnote"> ↩</a></span></li><li id="fn:3"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">3.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Plesner, A., Vontobel, T., &amp; Wattenhofer, R. (2024). Breaking reCAPTCHAv2. In <em>2024 IEEE 48th Annual COMPSAC</em> (pp. 1047–1056). IEEE. doi:10.1109/compsac61105.2024.00142<a href="#fnref:3" rev="footnote"> ↩</a></span></li><li id="fn:4"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">4.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Berozashvili, T. (2024). Securing digital identities in the era of remote identity verification. doi:10.13140/RG.2.2.11839.11688<a href="#fnref:4" rev="footnote"> ↩</a></span></li><li id="fn:5"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">5.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Xiao, M., Wang, M., Kulshrestha, A., &amp; Mayer, J. (2023). <em>arxiv.org/abs/2304.14939</em> Account verification on social media: User perceptions and paid enrollment. <em>arXiv</em>.<a href="#fnref:5" rev="footnote"> ↩</a></span></li><li id="fn:6"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">6.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Flanagan, H. (2023). <em>openid.net/Government-issued-Digital-Credentials-and-the-Privacy-Landscape-Final</em> Government-issued digital credentials and the privacy landscape. OpenID Foundation.<a href="#fnref:6" rev="footnote"> ↩</a></span></li><li id="fn:7"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">7.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">McGrath, K. (2016). Identity verification and societal challenges: Explaining the gap between service provision and development outcomes. <em>MIS Quarterly</em>, 40(2), 485–500. JSTOR<a href="#fnref:7" rev="footnote"> ↩</a></span></li><li id="fn:8"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">8.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Satybaldy, A., Subedi, A., &amp; Nowostawski, M. (2022). A framework for online document verification using self-sovereign identity technology. <em>Sensors</em>, 22(21), 8408. doi: 10.3390/s2221840<a href="#fnref:8" rev="footnote"> ↩</a></span></li><li id="fn:9"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">9.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">Muth, R., Galal, T., Heiss, J., &amp; Tschorsch, F. (2023). Towards smart contract-based verification of anonymous credentials. In <em>Financial Cryptography and Data Security: FC 2022 International Workshops</em> (pp. 481–498). Springer.<a href="#fnref:9" rev="footnote"> ↩</a></span></li></ol></div></div>]]>
    </content>
    <id>http://blog.trintler.me/2026/04/28/Verifying-Humans-Without-Surrendering-Identity/</id>
    <link href="http://blog.trintler.me/2026/04/28/Verifying-Humans-Without-Surrendering-Identity/"/>
    <published>2026-04-27T22:00:00.000Z</published>
    <summary>Although government-issued identity verification addresses the genuine challenge of distinguishing humans from automated systems, its widespread implementation commoditises personal identity data and exposes users to inconsistent privacy protections.</summary>
    <title>Verifying Humans Without Surrendering Identity: A Case Against Government-Issued Online Verification</title>
    <updated>2026-04-28T11:53:16.841Z</updated>
  </entry>
  <entry>
    <author>
      <name>Niladri Adhikary</name>
    </author>
    <category term="Projects" scheme="http://blog.trintler.me/categories/Projects/"/>
    <category term="oss" scheme="http://blog.trintler.me/tags/oss/"/>
    <category term="js" scheme="http://blog.trintler.me/tags/js/"/>
    <category term="tts" scheme="http://blog.trintler.me/tags/tts/"/>
    <category term="npm" scheme="http://blog.trintler.me/tags/npm/"/>
    <category term="hexo" scheme="http://blog.trintler.me/tags/hexo/"/>
    <content>
      <![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><h2 id="Motivation"><a href="#Motivation" class="headerlink" title="Motivation"></a>Motivation</h2><p>I wanted to use a Text to speech to read these blog posts aloud, the only condition being that, it would have low-latency and locally compute all tokens. The reasoning behind this? because of the fact that I do not own a server! They are extremely expensive.</p><blockquote><p>A very wise person once said, when <em>resources</em> for modern solutions become infeasibly expensive to you, rely on old solutions. Who said that? I did, but that part does not matter.</p></blockquote><p>Thus, <strong>SAM</strong> came to mind: the Software Automatic Mouth<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="SAM (Software Automatic Mouth) was created by Mark Barton of Don't Ask Software in 1982, originally for the Commodore 64. It was one of the first commercially available speech synthesisers for home computers.">[1]</span></a></sup> is a speech synthesiser originally written in 1982 by Mark Barton of Don’t Ask Software for the Commodore 64. Christian Schiffler (@discordier) ported SAM to JavaScript as <strong>SamJs</strong><sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="SamJs v0.3.0 by Christian Schiffler (discordier). A JavaScript port of SAM that preserves the original phoneme engine. Source: [github.com/discordier/sam](https://github.com/discordier/sam).">[2]</span></a></sup>, preserving the original’s distinctive robotic phoneme engine in a <em>looooong</em> ca. 3000-line browser-compatible library. SAM does not stream audio from a server! It runs entirely in the browser, synthesising speech from raw text via the Web Audio API<sup id="fnref:3"><a href="#fn:3" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="The Web Audio API provides the `AudioContext`, `AudioBuffer`, and `AudioBufferSourceNode` interfaces used for PCM playback. SAM outputs audio at 22050 Hz, which is passed directly to `AudioContext.createBuffer()`.">[3]</span></a></sup>. Therefore there exist no latency beyond local computation. Helpful considering the new state of my internet after moving in! (It is <em>awful</em>)</p><p>The product is <strong>hexo-sam-reader</strong>, which is a Hexo plugin that adds a SAM-powered TTS reader widget to any blog post, made for Hexo. This document traces the full development from a hardcoded prototype to a published npm package. In the future I plan to have a smoother sounding voice, but I would like to talk about SAM first!</p><span id="more"></span><h2 id="Scope-of-This-Document"><a href="#Scope-of-This-Document" class="headerlink" title="Scope of This Document"></a>Scope of This Document</h2><p>This post covers the evolution of hexo-sam-reader across two distinct phases: the <code>sam-v1</code> prototype (a <em>looong</em> Hexo helper script) and the published <code>hexo-sam-reader</code> package (a npm plugin for any hexo user to use). It examines the text processing pipeline, the playback engine, the configuration refactor that removed all hardcoding, and the documentation process.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="I-The-sam-v1-Prototype"><a href="#I-The-sam-v1-Prototype" class="headerlink" title="I: The sam-v1 Prototype"></a>I: The sam-v1 Prototype</h1><h2 id="The-Helper"><a href="#The-Helper" class="headerlink" title="The Helper"></a>The Helper</h2><p>The project began as a single file: <code>sam-reader.js</code>, ca. 600 lines, dropped directly into the Hexo blog’s <code>scripts/</code> directory. It registered itself as a Hexo <code>EJS</code> helper using <code>hexo.extend.helper.register()</code> and returned an HTML string containing the widget markup, inline CSS, and the entire client-side JavaScript.</p><p>The registration was:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">register</span>(<span class="string">&quot;sam_reader&quot;</span>, <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> page = <span class="variable language_">this</span>.<span class="property">page</span>;</span><br><span class="line">  <span class="keyword">if</span> (!page || !page.<span class="property">sam</span>) <span class="keyword">return</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> safeTitle = (page.<span class="property">title</span> || <span class="string">&quot;&quot;</span>)</span><br><span class="line">    .<span class="title function_">replace</span>(<span class="regexp">/&quot;/g</span>, <span class="string">&quot;&amp;quot;&quot;</span>)</span><br><span class="line">    .<span class="title function_">replace</span>(<span class="regexp">/&lt;/g</span>, <span class="string">&quot;&amp;lt;&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string">&lt;div class=&quot;meta-widget sam-reader-widget&quot; id=&quot;sam-reader&quot; data-post-title=&quot;<span class="subst">$&#123;safeTitle&#125;</span>&quot;&gt;</span></span><br><span class="line"><span class="string">  &lt;!-- 500+ lines of HTML, CSS, and JS --&gt;</span></span><br><span class="line"><span class="string">&lt;/div&gt;`</span>;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>This is the Hexo helper pattern: a function that receives the current page context via <code>this.page</code> and returns raw HTML. The conditional <code>if (!page.sam) return &#39;&#39;</code> ensures the widget only renders on posts with <code>sam: true</code> in their front matter.</p><h2 id="Hardcoded-Values"><a href="#Hardcoded-Values" class="headerlink" title="Hardcoded Values"></a>Hardcoded Values</h2><p>Every configurable value was baked into the source. The colour scheme was embedded in CSS literal strings:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.sam-reader-widget</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: <span class="number">#000</span>;</span><br><span class="line">  <span class="attribute">border</span>: <span class="number">1px</span> dashed <span class="number">#924a41</span>;</span><br><span class="line">  <span class="attribute">font-family</span>: DOS, SimHei, Monaco, Menlo, Consolas, <span class="string">&quot;Courier New&quot;</span>, monospace;</span><br><span class="line">&#125;</span><br><span class="line"><span class="selector-class">.sam-reader-widget</span> <span class="selector-class">.sam-title</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#c08179</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="selector-class">.sam-reader-widget</span> <span class="selector-class">.sam-controls</span> <span class="selector-tag">button</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: <span class="number">#352b42</span>;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#c08179</span>;</span><br><span class="line">  <span class="attribute">border</span>: <span class="number">1px</span> dashed <span class="number">#924a41</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The voice parameters were fixed in the HTML markup:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">  &gt;</span>Speed <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;range&quot;</span> <span class="attr">id</span>=<span class="string">&quot;sam-speed&quot;</span> <span class="attr">min</span>=<span class="string">&quot;20&quot;</span> <span class="attr">max</span>=<span class="string">&quot;200&quot;</span> <span class="attr">value</span>=<span class="string">&quot;72&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span><span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">  &gt;</span>Pitch <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;range&quot;</span> <span class="attr">id</span>=<span class="string">&quot;sam-pitch&quot;</span> <span class="attr">min</span>=<span class="string">&quot;0&quot;</span> <span class="attr">max</span>=<span class="string">&quot;255&quot;</span> <span class="attr">value</span>=<span class="string">&quot;64&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span><span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">  &gt;</span>Mouth <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;range&quot;</span> <span class="attr">id</span>=<span class="string">&quot;sam-mouth&quot;</span> <span class="attr">min</span>=<span class="string">&quot;0&quot;</span> <span class="attr">max</span>=<span class="string">&quot;255&quot;</span> <span class="attr">value</span>=<span class="string">&quot;128&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span><span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">  &gt;</span>Throat <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;range&quot;</span> <span class="attr">id</span>=<span class="string">&quot;sam-throat&quot;</span> <span class="attr">min</span>=<span class="string">&quot;0&quot;</span> <span class="attr">max</span>=<span class="string">&quot;255&quot;</span> <span class="attr">value</span>=<span class="string">&quot;128&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span><span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br></pre></td></tr></table></figure><p>The abbreviation list was hardcoded inside the <code>cleanForSam()</code> function as a sequence of individual regex replacements:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bNL\b/g</span>, <span class="string">&quot;Netherlands&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bKIT\b/g</span>, <span class="string">&quot;K I T&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bTUD\b/g</span>, <span class="string">&quot;T U D&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bICPC\b/g</span>, <span class="string">&quot;I C P C&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bTAPC\b/g</span>, <span class="string">&quot;T A P C&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bBAPC\b/g</span>, <span class="string">&quot;B A P C&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bNWERC\b/g</span>, <span class="string">&quot;N W E R C&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bSSH\b/g</span>, <span class="string">&quot;S S H&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bJOSS\b/g</span>, <span class="string">&quot;J O S S&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bCLI\b/g</span>, <span class="string">&quot;C L I&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bAPI\b/g</span>, <span class="string">&quot;A P I&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bLaTeX\b/gi</span>, <span class="string">&quot;lay-tech&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bCMake\b/gi</span>, <span class="string">&quot;see-make&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\be\.g\./g</span>, <span class="string">&quot;for example&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\bi\.e\./g</span>, <span class="string">&quot;that is&quot;</span>);</span><br></pre></td></tr></table></figure><p>Eighteen abbreviations, each a separate <code>text.replace()</code> call. Adding a new one meant editing the helper source. The content selector was hardcoded as <code>.mypage</code>. The SAM library path was hardcoded as <code>/js/sam.js</code>. The pause duration was <code>var PAUSE_MS = 400</code>. The chunk length limit was <code>200</code>.</p><p>This worked for my blog specifically. However, could it could work for anyone else’s? No.</p><h2 id="How-the-Prototype-Functioned"><a href="#How-the-Prototype-Functioned" class="headerlink" title="How the Prototype Functioned"></a>How the Prototype Functioned</h2><p>Despite the hardcoding, the prototype established the complete processing pipeline that survived into the published package:</p><ol><li><strong>Conditional rendering</strong>: Check <code>page.sam</code> in front matter.</li><li><strong>Widget injection</strong>: Return inline HTML with controls (Play, Pause, Stop), a progress bar, voice setting sliders, and a status indicator.</li><li><strong>Async SAM loading</strong>: Dynamically create a <code>&lt;script&gt;</code> tag pointing to <code>sam.js</code>, initialise on load.</li><li><strong>Text extraction</strong>: Clone the post’s DOM subtree, remove non-readable elements, walk the tree to produce segments.</li><li><strong>Text cleaning</strong>: Apply regex transformations to produce SAM-compatible ASCII text.</li><li><strong>Chunking</strong>: Split cleaned text into chunks of at most 200 characters, respecting sentence and comma boundaries.</li><li><strong>Playback</strong>: Feed each chunk to <code>SamJs</code>, convert to a Web Audio buffer, play sequentially with pauses between sections.</li></ol><p>The prototype was functional. The question was whether it could become distributable.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Phase-II-Package-Initialisation"><a href="#Phase-II-Package-Initialisation" class="headerlink" title="Phase II: Package Initialisation"></a>Phase II: Package Initialisation</h1><h2 id="The-npm-Structure"><a href="#The-npm-Structure" class="headerlink" title="The npm Structure"></a>The npm Structure</h2><p>At 10:51 on 2026-04-02, the first commit (<code>0e7d294</code>) created the package structure:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">hexo-sam-reader/</span><br><span class="line">├── package.json</span><br><span class="line">├── index.js</span><br><span class="line">├── lib/</span><br><span class="line">│   ├── generator.js</span><br><span class="line">│   └── helper.js</span><br><span class="line">└── assets/</span><br><span class="line">    └── sam.js</span><br></pre></td></tr></table></figure><p>The <code>package.json</code> declared the package with standard npm metadata:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hexo-sam-reader&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;SAM (Software Automatic Mouth) text-to-speech reader widget for Hexo blog posts&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;index.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;files&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;lib/&quot;</span><span class="punctuation">,</span> <span class="string">&quot;assets/&quot;</span><span class="punctuation">,</span> <span class="string">&quot;index.js&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;keywords&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;hexo&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;hexo-plugin&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;sam&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;tts&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;text-to-speech&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;voice&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;reader&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;accessibility&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;author&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Niladri Adhikary (trintlermint)&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;license&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MIT&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;node&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&gt;=14&quot;</span> <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>The <code>files</code> array is critical for npm distribution: it declares which files are included when the package is installed. Without it, npm includes everything, potentially shipping development artifacts. With it, the installed package contains only <code>index.js</code>, the <code>lib/</code> directory, and the <code>assets/</code> directory (which holds the bundled <code>sam.js</code> library).</p><h2 id="Splitting-the-Monolith"><a href="#Splitting-the-Monolith" class="headerlink" title="Splitting the Monolith"></a>Splitting the Monolith</h2><p>The prototype’s single file was decomposed into three modules with distinct responsibilities.</p><h3 id="index-js-Registration-and-Configuration"><a href="#index-js-Registration-and-Configuration" class="headerlink" title="index.js: Registration and Configuration"></a>index.js: Registration and Configuration</h3><p>The entry point handles Hexo plugin registration. Hexo automatically loads <code>index.js</code> from any package in <code>node_modules/</code> whose name starts with <code>hexo-</code>:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* global hexo */</span></span><br><span class="line"><span class="meta">&quot;use strict&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&quot;path&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> defaultStyle = &#123;</span><br><span class="line">  <span class="attr">background</span>: <span class="string">&quot;#000&quot;</span>,</span><br><span class="line">  <span class="attr">border_color</span>: <span class="string">&quot;#924a41&quot;</span>,</span><br><span class="line">  <span class="attr">text_color</span>: <span class="string">&quot;#c08179&quot;</span>,</span><br><span class="line">  <span class="attr">button_bg</span>: <span class="string">&quot;#352b42&quot;</span>,</span><br><span class="line">  <span class="attr">button_hover_bg</span>: <span class="string">&quot;#924a41&quot;</span>,</span><br><span class="line">  <span class="attr">button_active_bg</span>: <span class="string">&quot;#493aa5&quot;</span>,</span><br><span class="line">  <span class="attr">button_active_border</span>: <span class="string">&quot;#867ade&quot;</span>,</span><br><span class="line">  <span class="attr">progress_bg</span>: <span class="string">&quot;#252525&quot;</span>,</span><br><span class="line">  <span class="attr">progress_bar</span>: <span class="string">&quot;#867ade&quot;</span>,</span><br><span class="line">  <span class="attr">progress_border</span>: <span class="string">&quot;#3a3a3a&quot;</span>,</span><br><span class="line">  <span class="attr">status_color</span>: <span class="string">&quot;#bbb&quot;</span>,</span><br><span class="line">  <span class="attr">config_accent</span>: <span class="string">&quot;#867ade&quot;</span>,</span><br><span class="line">  <span class="attr">font_family</span>: <span class="string">&quot;DOS, SimHei, Monaco, Menlo, Consolas, &#x27;Courier New&#x27;, monospace&quot;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">hexo.<span class="property">config</span>.<span class="property">sam_reader</span> = <span class="title class_">Object</span>.<span class="title function_">assign</span>(</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">front_matter_key</span>: <span class="string">&quot;sam&quot;</span>,</span><br><span class="line">    <span class="attr">content_selector</span>: <span class="string">&quot;.mypage&quot;</span>,</span><br><span class="line">    <span class="attr">asset_path</span>: <span class="string">&quot;/js/hexo-sam-reader&quot;</span>,</span><br><span class="line">    <span class="attr">speed</span>: <span class="number">72</span>,</span><br><span class="line">    <span class="attr">pitch</span>: <span class="number">64</span>,</span><br><span class="line">    <span class="attr">mouth</span>: <span class="number">128</span>,</span><br><span class="line">    <span class="attr">throat</span>: <span class="number">128</span>,</span><br><span class="line">    <span class="attr">pause_ms</span>: <span class="number">400</span>,</span><br><span class="line">    <span class="attr">chunk_max_length</span>: <span class="number">200</span>,</span><br><span class="line">    <span class="attr">abbreviations</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">skip_selectors</span>: <span class="string">&quot;&quot;</span>,</span><br><span class="line">    <span class="attr">style</span>: &#123;&#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  hexo.<span class="property">config</span>.<span class="property">sam_reader</span>,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">hexo.<span class="property">config</span>.<span class="property">sam_reader</span>.<span class="property">style</span> = <span class="title class_">Object</span>.<span class="title function_">assign</span>(</span><br><span class="line">  &#123;&#125;,</span><br><span class="line">  defaultStyle,</span><br><span class="line">  hexo.<span class="property">config</span>.<span class="property">sam_reader</span>.<span class="property">style</span> || &#123;&#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">register</span>(<span class="string">&quot;sam_reader&quot;</span>, <span class="built_in">require</span>(<span class="string">&quot;./lib/helper&quot;</span>)(hexo));</span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">generator</span>.<span class="title function_">register</span>(</span><br><span class="line">  <span class="string">&quot;sam_reader_assets&quot;</span>,</span><br><span class="line">  <span class="built_in">require</span>(<span class="string">&quot;./lib/generator&quot;</span>)(hexo),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>Two <code>Object.assign()</code> calls perform the configuration merge. The first merges the user’s <code>sam_reader</code> block from <code>_config.yml</code> over the plugin defaults. The second merges the user’s <code>style</code> sub-object over the default colour scheme. The merge order — defaults first, user config second — ensures user values override defaults while preserving any defaults the user did not specify.</p><h3 id="lib-generator-js-Virtual-Asset-Serving"><a href="#lib-generator-js-Virtual-Asset-Serving" class="headerlink" title="lib&#x2F;generator.js: Virtual Asset Serving"></a>lib&#x2F;generator.js: Virtual Asset Serving</h3><p>The generator module solves a distribution problem. In the prototype, <code>sam.js</code> had to be manually placed in the blog’s <code>source/js/</code> directory. The generator eliminates this by serving the bundled <code>sam.js</code> as a virtual Hexo asset:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&quot;use strict&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&quot;path&quot;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&quot;fs&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="keyword">function</span> (<span class="params">hexo</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> config = hexo.<span class="property">config</span>.<span class="property">sam_reader</span>;</span><br><span class="line">    <span class="keyword">const</span> assetPath = config.<span class="property">asset_path</span>.<span class="title function_">replace</span>(<span class="regexp">/^\//</span>, <span class="string">&quot;&quot;</span>).<span class="title function_">replace</span>(<span class="regexp">/\/$/</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> samFile = path.<span class="title function_">join</span>(__dirname, <span class="string">&quot;..&quot;</span>, <span class="string">&quot;assets&quot;</span>, <span class="string">&quot;sam.js&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">path</span>: assetPath + <span class="string">&quot;/sam.js&quot;</span>,</span><br><span class="line">      <span class="attr">data</span>: <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> fs.<span class="title function_">createReadStream</span>(samFile);</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>This is Hexo’s generator API: return an object with a <code>path</code> (the URL path) and a <code>data</code> function (the content). The <code>data</code> function returns a readable stream rather than loading the entire 3,276-line file into memory. When Hexo builds the site, the generator creates a virtual file at <code>/js/hexo-sam-reader/sam.js</code> (by default) without the user copying anything.</p><h3 id="lib-helper-js-The-Widget-Renderer"><a href="#lib-helper-js-The-Widget-Renderer" class="headerlink" title="lib&#x2F;helper.js: The Widget Renderer"></a>lib&#x2F;helper.js: The Widget Renderer</h3><p>The helper module contains the widget HTML, CSS, and client-side JavaScript. It was extracted from the prototype with one structural change: instead of registering itself, it exports a factory function that receives the <code>hexo</code> instance and returns the helper function:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&quot;use strict&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="keyword">function</span> (<span class="params">hexo</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">var</span> page = <span class="variable language_">this</span>.<span class="property">page</span>;</span><br><span class="line">    <span class="keyword">var</span> config = hexo.<span class="property">config</span>.<span class="property">sam_reader</span>;</span><br><span class="line">    <span class="keyword">var</span> key = config.<span class="property">front_matter_key</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!page || !page[key]) <span class="keyword">return</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ... widget rendering</span></span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>The closure over <code>hexo</code> gives the helper access to <code>hexo.config.sam_reader</code> without global state. The front matter key is now configurable: instead of checking <code>page.sam</code>, it checks <code>page[config.front_matter_key]</code>, allowing users to use any key they prefer.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Phase-III-The-Configuration-Refactor"><a href="#Phase-III-The-Configuration-Refactor" class="headerlink" title="Phase III: The Configuration Refactor"></a>Phase III: The Configuration Refactor</h1><h2 id="The-Architectural-Shift"><a href="#The-Architectural-Shift" class="headerlink" title="The Architectural Shift"></a>The Architectural Shift</h2><p>Commit <code>cfc40c2</code> (“feat: remove hardcoding from the library, push to _config.yml instead”) was the most significant change in the project’s history. Executed fifty minutes after the initial commit, it transformed hexo-sam-reader from a personal script into a distributable plugin.</p><p>The core insight was that every value a user might want to change should live in <code>_config.yml</code>, not in source code. This applied to thirteen categories of configuration:</p><table><thead><tr><th>Category</th><th>Prototype</th><th>Package</th></tr></thead><tbody><tr><td>Front matter key</td><td>Hardcoded <code>sam</code></td><td><code>config.front_matter_key</code></td></tr><tr><td>Content selector</td><td>Hardcoded <code>.mypage</code></td><td><code>config.content_selector</code></td></tr><tr><td>Asset path</td><td>Hardcoded <code>/js/sam.js</code></td><td><code>config.asset_path</code></td></tr><tr><td>Voice speed</td><td>Hardcoded <code>72</code></td><td><code>config.speed</code></td></tr><tr><td>Voice pitch</td><td>Hardcoded <code>64</code></td><td><code>config.pitch</code></td></tr><tr><td>Voice mouth</td><td>Hardcoded <code>128</code></td><td><code>config.mouth</code></td></tr><tr><td>Voice throat</td><td>Hardcoded <code>128</code></td><td><code>config.throat</code></td></tr><tr><td>Pause duration</td><td>Hardcoded <code>400</code></td><td><code>config.pause_ms</code></td></tr><tr><td>Chunk max length</td><td>Hardcoded <code>200</code></td><td><code>config.chunk_max_length</code></td></tr><tr><td>Abbreviations</td><td>18 hardcoded regexes</td><td><code>config.abbreviations</code></td></tr><tr><td>Skip selectors</td><td>Hardcoded list</td><td><code>config.skip_selectors</code></td></tr><tr><td>Widget colours</td><td>13 hardcoded hex values</td><td><code>config.style.*</code></td></tr><tr><td>Font family</td><td>Hardcoded string</td><td><code>config.style.font_family</code></td></tr></tbody></table><h2 id="Abbreviation-Handling-Case-Sensitivity"><a href="#Abbreviation-Handling-Case-Sensitivity" class="headerlink" title="Abbreviation Handling: Case Sensitivity"></a>Abbreviation Handling: Case Sensitivity</h2><p>The most nuanced part of the refactor was the abbreviation system. In the prototype, each abbreviation was a separate <code>text.replace()</code> call with manually chosen flags (<code>/g</code> for case-sensitive, <code>/gi</code> for case-insensitive). The package needed a general mechanism.</p><p>The solution uses the casing of the abbreviation key to determine match behaviour:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> keys = <span class="title class_">Object</span>.<span class="title function_">keys</span>(<span class="variable constant_">ABBREVIATIONS</span>);</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> a = <span class="number">0</span>; a &lt; keys.<span class="property">length</span>; a++) &#123;</span><br><span class="line">  <span class="keyword">var</span> k = keys[a];</span><br><span class="line">  <span class="keyword">var</span> flags = k === k.<span class="title function_">toUpperCase</span>() ? <span class="string">&quot;g&quot;</span> : <span class="string">&quot;gi&quot;</span>;</span><br><span class="line">  text = text.<span class="title function_">replace</span>(</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">RegExp</span>(</span><br><span class="line">      <span class="string">&quot;\\b&quot;</span> + k.<span class="title function_">replace</span>(<span class="regexp">/[.*+?^\/\\|()[\]&#123;&#125;]/g</span>, <span class="string">&quot;\\$&amp;&quot;</span>) + <span class="string">&quot;\\b&quot;</span>,</span><br><span class="line">      flags,</span><br><span class="line">    ),</span><br><span class="line">    <span class="variable constant_">ABBREVIATIONS</span>[k],</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>If the key is entirely uppercase (<code>SSH</code>, <code>CLI</code>, <code>API</code>), the regex uses the <code>g</code> flag only — case-sensitive matching. If the key contains any lowercase character (<code>LaTeX</code>, <code>CMake</code>, <code>libssh</code>), the regex uses <code>gi</code> — case-insensitive matching. The rationale: an all-caps acronym like <code>SSH</code> should not match <code>ssh</code> in a URL path or code snippet where the casing is meaningful, but a mixed-case term like <code>LaTeX</code> should match regardless of how the author capitalised it.</p><p>The key is also escaped with a regex-safe replacement before being compiled into a <code>RegExp</code> constructor, preventing injection of regex metacharacters through the configuration.</p><h2 id="Configuration-in-Practice"><a href="#Configuration-in-Practice" class="headerlink" title="Configuration in Practice"></a>Configuration in Practice</h2><p>On my blog, the <code>_config.yml</code> block declares thirty abbreviations, thirteen style properties, and five voice parameters:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">sam_reader:</span></span><br><span class="line">  <span class="attr">front_matter_key:</span> <span class="string">sam</span></span><br><span class="line">  <span class="attr">content_selector:</span> <span class="string">&quot;.mypage&quot;</span></span><br><span class="line">  <span class="attr">speed:</span> <span class="number">72</span></span><br><span class="line">  <span class="attr">pitch:</span> <span class="number">64</span></span><br><span class="line">  <span class="attr">mouth:</span> <span class="number">128</span></span><br><span class="line">  <span class="attr">throat:</span> <span class="number">128</span></span><br><span class="line">  <span class="attr">pause_ms:</span> <span class="number">400</span></span><br><span class="line">  <span class="attr">chunk_max_length:</span> <span class="number">200</span></span><br><span class="line">  <span class="attr">abbreviations:</span></span><br><span class="line">    <span class="attr">NL:</span> <span class="string">&quot;Netherlands&quot;</span></span><br><span class="line">    <span class="attr">DE:</span> <span class="string">&quot;Germany&quot;</span></span><br><span class="line">    <span class="attr">KIT:</span> <span class="string">&quot;K I T&quot;</span></span><br><span class="line">    <span class="attr">ICPC:</span> <span class="string">&quot;I C P C&quot;</span></span><br><span class="line">    <span class="attr">TAPC:</span> <span class="string">&quot;T A P C&quot;</span></span><br><span class="line">    <span class="attr">SSH:</span> <span class="string">&quot;S S H&quot;</span></span><br><span class="line">    <span class="attr">CLI:</span> <span class="string">&quot;C L I&quot;</span></span><br><span class="line">    <span class="attr">API:</span> <span class="string">&quot;A P I&quot;</span></span><br><span class="line">    <span class="attr">HTML:</span> <span class="string">&quot;H T M L&quot;</span></span><br><span class="line">    <span class="attr">CSS:</span> <span class="string">&quot;C S S&quot;</span></span><br><span class="line">    <span class="attr">FTXUI:</span> <span class="string">&quot;F T X U I&quot;</span></span><br><span class="line">    <span class="attr">LaTeX:</span> <span class="string">&quot;lay-tech&quot;</span></span><br><span class="line">    <span class="attr">CMake:</span> <span class="string">&quot;see-make&quot;</span></span><br><span class="line">    <span class="attr">libssh:</span> <span class="string">&quot;lib S S H&quot;</span></span><br><span class="line">    <span class="attr">&quot;e.g.&quot;:</span> <span class="string">&quot;for example&quot;</span></span><br><span class="line">    <span class="attr">&quot;i.e.&quot;:</span> <span class="string">&quot;that is&quot;</span></span><br><span class="line">  <span class="attr">style:</span></span><br><span class="line">    <span class="attr">background:</span> <span class="string">&quot;#000&quot;</span></span><br><span class="line">    <span class="attr">border_color:</span> <span class="string">&quot;#924a41&quot;</span></span><br><span class="line">    <span class="attr">text_color:</span> <span class="string">&quot;#c08179&quot;</span></span><br><span class="line">    <span class="attr">button_bg:</span> <span class="string">&quot;#352b42&quot;</span></span><br><span class="line">    <span class="attr">progress_bar:</span> <span class="string">&quot;#867ade&quot;</span></span><br><span class="line">    <span class="attr">config_accent:</span> <span class="string">&quot;#867ade&quot;</span></span><br><span class="line">    <span class="attr">font_family:</span> <span class="string">&quot;DOS, SimHei, Monaco, Menlo, Consolas, &#x27;Courier New&#x27;, monospace&quot;</span></span><br></pre></td></tr></table></figure><p>A user with a different theme would only need to change the selectors and colours. A user writing in a domain with different acronyms would only need to change the abbreviations. The voice parameters remain adjustable via sliders at runtime; the <code>_config.yml</code> values set the initial positions.</p><h2 id="The-Trade-Off-Batteries-Not-Included"><a href="#The-Trade-Off-Batteries-Not-Included" class="headerlink" title="The Trade-Off: Batteries Not Included"></a>The Trade-Off: Batteries Not Included</h2><p>The prototype shipped with eighteen hardcoded abbreviations: every acronym I used in my posts. The published package ships with an empty abbreviation map. This was a deliberate choice. Distributing my personal abbreviation list as defaults would cause incorrect speech for users who do not write about ICPC or use FTXUI. An empty default forces users to declare their own terms, which is the correct behaviour for a general-purpose plugin.</p><p>The same logic applies to the content selector. The prototype hardcoded <code>.mypage</code>, which is specific to the theme I use. The package defaults to <code>.mypage</code> but documents the override for other themes (e.g., <code>.e-content</code> for the default Landscape theme). The default is a suggestion, not an assumption.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Phase-IV-The-Text-Processing-Pipeline"><a href="#Phase-IV-The-Text-Processing-Pipeline" class="headerlink" title="Phase IV: The Text Processing Pipeline"></a>Phase IV: The Text Processing Pipeline</h1><h2 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h2><p>The text processing pipeline converts a rendered HTML blog post into an array of SAM-compatible text chunks. It operates in four stages:</p><ol><li><strong>Extraction</strong> (<code>getPostSegments()</code>): DOM walking to produce raw text segments with pause markers.</li><li><strong>Cleaning</strong> (<code>cleanForSam()</code>): Regex transformations to produce ASCII text SAM can pronounce.</li><li><strong>Table reading</strong> (<code>readTable()</code>): Structured extraction of tabular data with column headers.</li><li><strong>Chunking</strong> (<code>buildChunks()</code>): Splitting cleaned text into chunks within SAM’s character limit.</li></ol><h2 id="Text-Extraction-Walking-the-DOM"><a href="#Text-Extraction-Walking-the-DOM" class="headerlink" title="Text Extraction: Walking the DOM"></a>Text Extraction: Walking the DOM</h2><p>The extraction function clones the post’s content container, removes non-readable elements, and walks the DOM tree to produce an array of segments. Each segment is either a string (speakable text) or <code>null</code> (a pause marker):</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">getPostSegments</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> segments = [];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// read the post title first</span></span><br><span class="line">  <span class="keyword">var</span> postTitle = widget.<span class="title function_">getAttribute</span>(<span class="string">&quot;data-post-title&quot;</span>) || <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">if</span> (postTitle) &#123;</span><br><span class="line">    segments.<span class="title function_">push</span>(<span class="title function_">cleanForSam</span>(postTitle) + <span class="string">&quot;.&quot;</span>);</span><br><span class="line">    segments.<span class="title function_">push</span>(<span class="literal">null</span>); <span class="comment">// pause after title</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> el = <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="variable constant_">CONTENT_SEL</span>);</span><br><span class="line">  <span class="keyword">if</span> (!el) <span class="keyword">return</span> segments;</span><br><span class="line">  <span class="keyword">var</span> clone = el.<span class="title function_">cloneNode</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// remove non-readable elements</span></span><br><span class="line">  <span class="keyword">var</span> baseSel =</span><br><span class="line">    <span class="string">&quot;pre, script, style, .highlight, img, svg, &quot;</span> +</span><br><span class="line">    <span class="string">&quot;.sam-reader-widget, .alert, figure, .article-footer-copyright, &quot;</span> +</span><br><span class="line">    <span class="string">&quot;noscript, iframe, video, audio, canvas, .gist&quot;</span>;</span><br><span class="line">  <span class="keyword">var</span> skipSel = <span class="variable constant_">EXTRA_SKIP</span> ? baseSel + <span class="string">&quot;, &quot;</span> + <span class="variable constant_">EXTRA_SKIP</span> : baseSel;</span><br><span class="line">  <span class="keyword">var</span> remove = clone.<span class="title function_">querySelectorAll</span>(skipSel);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; remove.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    remove[i].<span class="property">parentNode</span>.<span class="title function_">removeChild</span>(remove[i]);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">walk</span>(<span class="params">node</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (node.<span class="property">nodeType</span> === <span class="number">3</span>) &#123;</span><br><span class="line">      <span class="comment">// text node</span></span><br><span class="line">      <span class="keyword">var</span> t = node.<span class="property">textContent</span>;</span><br><span class="line">      <span class="keyword">if</span> (t &amp;&amp; t.<span class="title function_">trim</span>()) segments.<span class="title function_">push</span>(<span class="title function_">cleanForSam</span>(t));</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (node.<span class="property">nodeType</span> !== <span class="number">1</span>) <span class="keyword">return</span>;</span><br><span class="line">    <span class="keyword">var</span> tag = node.<span class="property">tagName</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (tag === <span class="string">&quot;TABLE&quot;</span>) &#123;</span><br><span class="line">      <span class="keyword">var</span> tableText = <span class="title function_">readTable</span>(node);</span><br><span class="line">      <span class="keyword">if</span> (tableText) segments.<span class="title function_">push</span>(<span class="title function_">cleanForSam</span>(tableText));</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="regexp">/^H[1-6]$/</span>.<span class="title function_">test</span>(tag)) &#123;</span><br><span class="line">      segments.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line">      <span class="keyword">var</span> hText = node.<span class="property">textContent</span>.<span class="title function_">trim</span>();</span><br><span class="line">      <span class="keyword">if</span> (hText) segments.<span class="title function_">push</span>(<span class="title function_">cleanForSam</span>(hText) + <span class="string">&quot;.&quot;</span>);</span><br><span class="line">      segments.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> children = node.<span class="property">childNodes</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> c = <span class="number">0</span>; c &lt; children.<span class="property">length</span>; c++) &#123;</span><br><span class="line">      <span class="title function_">walk</span>(children[c]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="regexp">/^(P|DIV|BLOCKQUOTE|LI|SECTION|ARTICLE|HR)$/</span>.<span class="title function_">test</span>(tag)) &#123;</span><br><span class="line">      segments.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">walk</span>(clone);</span><br><span class="line">  <span class="keyword">return</span> segments;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Several design decisions are embedded here. The clone operation prevents the extraction from modifying the visible page. Code blocks (<code>pre</code>, <code>.highlight</code>) are excluded because SAM cannot meaningfully pronounce source code. Headings receive pause markers on both sides, creating audible section breaks. Block-level elements receive trailing pauses, producing natural breathing points in the speech.</p><p>The skip selector list is extensible via <code>EXTRA_SKIP</code> (from <code>config.skip_selectors</code>), allowing users to exclude custom widgets, advertisement banners, or other non-content elements without modifying the plugin source.</p><h2 id="Text-Cleaning-The-Regex-Pipeline"><a href="#Text-Cleaning-The-Regex-Pipeline" class="headerlink" title="Text Cleaning: The Regex Pipeline"></a>Text Cleaning: The Regex Pipeline</h2><p>The <code>cleanForSam()</code> function applies over forty regex transformations in a specific order. The ordering matters: operator replacements must occur before angle bracket stripping, and abbreviation replacements must occur before non-ASCII removal.</p><p><strong>Stage 1: Remove unparseable content.</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// URLs</span></span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/https?:\/\/[^\s]+/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line"><span class="comment">// protocol-like patterns (van://-, ---://-, etc.)</span></span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[a-zA-Z0-9_-]*:\/\/[^\s]*/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line"><span class="comment">// email addresses</span></span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[\w.-]+@[\w.-]+/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line"><span class="comment">// arrow functions and code constructs</span></span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\([^)]*=&gt;\s*\&#123;[^&#125;]*\&#125;[^)]*\)/g</span>, <span class="string">&quot;&quot;</span>);</span><br></pre></td></tr></table></figure><p><strong>Stage 2: Replace operators with spoken equivalents.</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/&gt;=/g</span>, <span class="string">&quot; greater than or equal to &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/&lt;=/g</span>, <span class="string">&quot; less than or equal to &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/!=/g</span>, <span class="string">&quot; not equal to &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/===/g</span>, <span class="string">&quot; strictly equals &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/==/g</span>, <span class="string">&quot; equals &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/(?&lt;![&lt;&gt;])&gt;(?![&lt;&gt;])/g</span>, <span class="string">&quot; greater than &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/(?&lt;![&lt;&gt;])&lt;(?![&lt;&gt;])/g</span>, <span class="string">&quot; less than &quot;</span>);</span><br></pre></td></tr></table></figure><p>The lookahead and lookbehind assertions on <code>&gt;</code> and <code>&lt;</code> prevent false positives inside HTML tags. Without them, a stray <code>&lt;div&gt;</code> remnant would become “less than div greater than”.</p><p><strong>Stage 3: Handle slashes contextually.</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/ \/ /g</span>, <span class="string">&quot; OR &quot;</span>); <span class="comment">// &quot; / &quot; → &quot;OR&quot;</span></span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\//g</span>, <span class="string">&quot; slash &quot;</span>); <span class="comment">// &quot;/&quot; → &quot;slash&quot;</span></span><br></pre></td></tr></table></figure><p>A spaced slash (<code>/</code>) typically indicates alternatives (“Linux &#x2F; macOS”) and reads naturally as “or”. An unspaced slash (“TCP&#x2F;IP”) is a literal separator and reads as “slash”.</p><p><strong>Stage 4: Strip code artifacts and apply abbreviations.</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[&#123;&#125;()\[\]&lt;&gt;]/g</span>, <span class="string">&quot; &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[*_~#]+/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/\[\^\w+\]/g</span>, <span class="string">&quot;&quot;</span>); <span class="comment">// footnote markers like <sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="SAM (Software Automatic Mouth) was created by Mark Barton of Don't Ask Software in 1982, originally for the Commodore 64. It was one of the first commercially available speech synthesisers for home computers.">[1]</span></a></sup></span></span><br></pre></td></tr></table></figure><p>After stripping, the configured abbreviations are applied using the case-sensitive logic described in Phase III.</p><p><strong>Stage 5: Remove non-ASCII characters.</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[\x80-\xFF]/g</span>, <span class="string">&quot; &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[\u0100-\uFFFF]/g</span>, <span class="string">&quot; &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[€£¥©®™°±×÷=+|\\^~&amp;]/g</span>, <span class="string">&quot; &quot;</span>);</span><br><span class="line">text = text.<span class="title function_">replace</span>(<span class="regexp">/[^\x20-\x7E]/g</span>, <span class="string">&quot; &quot;</span>);</span><br></pre></td></tr></table></figure><p>SAM was designed for 7-bit ASCII English. Any character outside the printable ASCII range (0x20–0x7E) would produce either garbage phonemes or a synthesis error. The pipeline aggressively strips everything SAM cannot handle.</p><h2 id="Table-Reading-Row-Wise-with-Headers"><a href="#Table-Reading-Row-Wise-with-Headers" class="headerlink" title="Table Reading: Row-Wise with Headers"></a>Table Reading: Row-Wise with Headers</h2><p>Tables require special handling. Naive text extraction of a table produces an incoherent sequence of cell values without context. The <code>readTable()</code> function reads tables row-wise, prepending each cell’s value with its column header:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">readTable</span>(<span class="params">table</span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> headers = [];</span><br><span class="line">  <span class="keyword">var</span> ths = table.<span class="title function_">querySelectorAll</span>(<span class="string">&quot;thead th, thead td, tr:first-child th&quot;</span>);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; ths.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    headers.<span class="title function_">push</span>(ths[i].<span class="property">textContent</span>.<span class="title function_">trim</span>());</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">var</span> rows = table.<span class="title function_">querySelectorAll</span>(<span class="string">&quot;tbody tr, tr&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> text = <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> r = <span class="number">0</span>; r &lt; rows.<span class="property">length</span>; r++) &#123;</span><br><span class="line">    <span class="keyword">var</span> cells = rows[r].<span class="title function_">querySelectorAll</span>(<span class="string">&quot;td&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (cells.<span class="property">length</span> === <span class="number">0</span>) <span class="keyword">continue</span>; <span class="comment">// skip header row</span></span><br><span class="line">    <span class="keyword">var</span> rowParts = [];</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> c = <span class="number">0</span>; c &lt; cells.<span class="property">length</span>; c++) &#123;</span><br><span class="line">      <span class="keyword">var</span> label = c &lt; headers.<span class="property">length</span> ? headers[c] : <span class="string">&quot;&quot;</span>;</span><br><span class="line">      <span class="keyword">var</span> val = cells[c].<span class="property">textContent</span>.<span class="title function_">trim</span>();</span><br><span class="line">      <span class="keyword">if</span> (label &amp;&amp; val) rowParts.<span class="title function_">push</span>(label + <span class="string">&quot; &quot;</span> + val);</span><br><span class="line">      <span class="keyword">else</span> <span class="keyword">if</span> (val) rowParts.<span class="title function_">push</span>(val);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (rowParts.<span class="property">length</span> &gt; <span class="number">0</span>) text += rowParts.<span class="title function_">join</span>(<span class="string">&quot;, &quot;</span>) + <span class="string">&quot;. &quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> text;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Given this table:</p><table><thead><tr><th>Language</th><th>Year</th></tr></thead><tbody><tr><td>C</td><td>1972</td></tr><tr><td>Python</td><td>1991</td></tr></tbody></table><p>The function produces: “Language C, Year 1972. Language Python, Year 1991.” This is intelligible when spoken aloud, unlike the flat extraction “C 1972 Python 1991” which loses all structure.</p><h2 id="Chunking-Sentence-and-Comma-Boundaries"><a href="#Chunking-Sentence-and-Comma-Boundaries" class="headerlink" title="Chunking: Sentence and Comma Boundaries"></a>Chunking: Sentence and Comma Boundaries</h2><p>SAM has practical limits on input length. The <code>buildChunks()</code> function splits text into chunks of at most <code>CHUNK_MAX</code> characters (default 200), respecting natural language boundaries:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">buildChunks</span>(<span class="params">segments, maxLen</span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> result = [];</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> s = <span class="number">0</span>; s &lt; segments.<span class="property">length</span>; s++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (segments[s] === <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (result.<span class="property">length</span> === <span class="number">0</span> || result[result.<span class="property">length</span> - <span class="number">1</span>] !== <span class="literal">null</span>) &#123;</span><br><span class="line">        result.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> text = segments[s];</span><br><span class="line">    <span class="keyword">if</span> (!text) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// first pass: split on sentence boundaries</span></span><br><span class="line">    <span class="keyword">var</span> parts = text.<span class="title function_">split</span>(<span class="regexp">/(?&lt;=[.!?])\s+/</span>);</span><br><span class="line">    <span class="keyword">var</span> current = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; parts.<span class="property">length</span>; i++) &#123;</span><br><span class="line">      <span class="keyword">var</span> p = parts[i].<span class="title function_">trim</span>();</span><br><span class="line">      <span class="keyword">if</span> (!p) <span class="keyword">continue</span>;</span><br><span class="line">      <span class="keyword">if</span> (current.<span class="property">length</span> + p.<span class="property">length</span> + <span class="number">1</span> &gt; maxLen &amp;&amp; current.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        result.<span class="title function_">push</span>(current.<span class="title function_">trim</span>());</span><br><span class="line">        current = p;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        current += (current ? <span class="string">&quot; &quot;</span> : <span class="string">&quot;&quot;</span>) + p;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (current.<span class="title function_">trim</span>()) result.<span class="title function_">push</span>(current.<span class="title function_">trim</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// second pass: split remaining long chunks on commas</span></span><br><span class="line">  <span class="keyword">var</span> final = [];</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> j = <span class="number">0</span>; j &lt; result.<span class="property">length</span>; j++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (result[j] === <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (final.<span class="property">length</span> === <span class="number">0</span> || final[final.<span class="property">length</span> - <span class="number">1</span>] !== <span class="literal">null</span>) &#123;</span><br><span class="line">        final.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (result[j].<span class="property">length</span> &lt;= maxLen) &#123;</span><br><span class="line">      final.<span class="title function_">push</span>(result[j]);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">var</span> subparts = result[j].<span class="title function_">split</span>(<span class="regexp">/,\s*/</span>);</span><br><span class="line">      <span class="keyword">var</span> sub = <span class="string">&quot;&quot;</span>;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">var</span> k = <span class="number">0</span>; k &lt; subparts.<span class="property">length</span>; k++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (sub.<span class="property">length</span> + subparts[k].<span class="property">length</span> + <span class="number">2</span> &gt; maxLen &amp;&amp; sub.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">          final.<span class="title function_">push</span>(sub.<span class="title function_">trim</span>());</span><br><span class="line">          sub = subparts[k];</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          sub += (sub ? <span class="string">&quot;, &quot;</span> : <span class="string">&quot;&quot;</span>) + subparts[k];</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (sub.<span class="title function_">trim</span>()) final.<span class="title function_">push</span>(sub.<span class="title function_">trim</span>());</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// strip leading/trailing pauses</span></span><br><span class="line">  <span class="keyword">while</span> (final.<span class="property">length</span> &gt; <span class="number">0</span> &amp;&amp; final[<span class="number">0</span>] === <span class="literal">null</span>) final.<span class="title function_">shift</span>();</span><br><span class="line">  <span class="keyword">while</span> (final.<span class="property">length</span> &gt; <span class="number">0</span> &amp;&amp; final[final.<span class="property">length</span> - <span class="number">1</span>] === <span class="literal">null</span>) final.<span class="title function_">pop</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> final;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The two-pass approach is deliberate. The first pass splits on sentence endings (<code>.</code>, <code>!</code>, <code>?</code> followed by whitespace, using a lookbehind assertion). If a single sentence still exceeds the character limit, the second pass splits on commas. This produces chunks that align with natural speech prosody: SAM pauses between chunks, and those pauses coincide with where a human would pause.</p><p>Consecutive <code>null</code> pause markers are compressed to a single pause, preventing excessive silence from nested block elements.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Phase-V-The-Playback-Engine"><a href="#Phase-V-The-Playback-Engine" class="headerlink" title="Phase V: The Playback Engine"></a>Phase V: The Playback Engine</h1><h2 id="Web-Audio-API-Integration"><a href="#Web-Audio-API-Integration" class="headerlink" title="Web Audio API Integration"></a>Web Audio API Integration</h2><p>The playback engine converts text chunks into audible speech using the Web Audio API. For each chunk, it instantiates a new <code>SamJs</code> object with the current slider values, generates a Float32Array of audio samples, and plays them through an <code>AudioBufferSourceNode</code>:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">playChunk</span>(<span class="params">index</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (stopped || index &gt;= chunks.<span class="property">length</span>) &#123;</span><br><span class="line">    progressBar.<span class="property">style</span>.<span class="property">width</span> = stopped ? <span class="string">&quot;0%&quot;</span> : <span class="string">&quot;100%&quot;</span>;</span><br><span class="line">    statusEl.<span class="property">textContent</span> = stopped ? <span class="string">&quot;Stopped&quot;</span> : <span class="string">&quot;Done!&quot;</span>;</span><br><span class="line">    <span class="title function_">setButtons</span>(<span class="string">&quot;ready&quot;</span>);</span><br><span class="line">    playing = <span class="literal">false</span>;</span><br><span class="line">    paused = <span class="literal">false</span>;</span><br><span class="line">    stopped = <span class="literal">false</span>;</span><br><span class="line">    currentChunk = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (paused) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  currentChunk = index;</span><br><span class="line">  <span class="title function_">updateProgress</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// null = silent pause</span></span><br><span class="line">  <span class="keyword">if</span> (chunks[index] === <span class="literal">null</span>) &#123;</span><br><span class="line">    pauseTimer = <span class="built_in">setTimeout</span>(<span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">      pauseTimer = <span class="literal">null</span>;</span><br><span class="line">      <span class="keyword">if</span> (!stopped &amp;&amp; !paused) <span class="title function_">playChunk</span>(index + <span class="number">1</span>);</span><br><span class="line">    &#125;, <span class="variable constant_">PAUSE_MS</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!audioCtx)</span><br><span class="line">    audioCtx = <span class="title function_">new</span> (<span class="variable language_">window</span>.<span class="property">AudioContext</span> || <span class="variable language_">window</span>.<span class="property">webkitAudioContext</span>)();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> sam = <span class="keyword">new</span> <span class="title class_">SamJs</span>(&#123;</span><br><span class="line">    <span class="attr">speed</span>: <span class="built_in">parseInt</span>(speedInput.<span class="property">value</span>, <span class="number">10</span>),</span><br><span class="line">    <span class="attr">pitch</span>: <span class="built_in">parseInt</span>(pitchInput.<span class="property">value</span>, <span class="number">10</span>),</span><br><span class="line">    <span class="attr">mouth</span>: <span class="built_in">parseInt</span>(mouthInput.<span class="property">value</span>, <span class="number">10</span>),</span><br><span class="line">    <span class="attr">throat</span>: <span class="built_in">parseInt</span>(throatInput.<span class="property">value</span>, <span class="number">10</span>),</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> buf32;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    buf32 = sam.<span class="title function_">buf32</span>(chunks[index]);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="title function_">playChunk</span>(index + <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (!buf32 || buf32.<span class="property">length</span> === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="title function_">playChunk</span>(index + <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> audioBuffer = audioCtx.<span class="title function_">createBuffer</span>(<span class="number">1</span>, buf32.<span class="property">length</span>, <span class="number">22050</span>);</span><br><span class="line">  audioBuffer.<span class="title function_">getChannelData</span>(<span class="number">0</span>).<span class="title function_">set</span>(buf32);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> source = audioCtx.<span class="title function_">createBufferSource</span>();</span><br><span class="line">  source.<span class="property">buffer</span> = audioBuffer;</span><br><span class="line">  source.<span class="title function_">connect</span>(audioCtx.<span class="property">destination</span>);</span><br><span class="line">  currentSource = source;</span><br><span class="line"></span><br><span class="line">  source.<span class="property">onended</span> = <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    currentSource = <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (!stopped &amp;&amp; !paused) &#123;</span><br><span class="line">      <span class="title function_">playChunk</span>(index + <span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">  source.<span class="title function_">start</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The function is recursive: each chunk’s <code>onended</code> callback invokes <code>playChunk(index + 1)</code>. This creates a sequential playback chain without blocking the main thread. The <code>AudioContext</code> is created lazily on first play, complying with browser autoplay policies that require audio contexts to be initialised from user gesture handlers.</p><p>The sample rate of 22050 Hz matches SAM’s native output frequency. The <code>buf32()</code> method returns a Float32Array of PCM samples that map directly into a single-channel <code>AudioBuffer</code>.</p><h2 id="The-State-Machine"><a href="#The-State-Machine" class="headerlink" title="The State Machine"></a>The State Machine</h2><p>Playback state is managed through four boolean flags (<code>playing</code>, <code>paused</code>, <code>stopped</code>) and two resource references (<code>currentSource</code>, <code>pauseTimer</code>):</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">setButtons</span>(<span class="params">state</span>) &#123;</span><br><span class="line">  btnPlay.<span class="property">disabled</span> = state === <span class="string">&quot;playing&quot;</span>;</span><br><span class="line">  btnPause.<span class="property">disabled</span> = state !== <span class="string">&quot;playing&quot;</span>;</span><br><span class="line">  btnStop.<span class="property">disabled</span> = state === <span class="string">&quot;ready&quot;</span>;</span><br><span class="line">  btnPlay.<span class="property">classList</span>.<span class="title function_">toggle</span>(<span class="string">&quot;active&quot;</span>, state === <span class="string">&quot;playing&quot;</span>);</span><br><span class="line">  btnPause.<span class="property">classList</span>.<span class="title function_">toggle</span>(<span class="string">&quot;active&quot;</span>, state === <span class="string">&quot;paused&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">stopAudio</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (pauseTimer) &#123;</span><br><span class="line">    <span class="built_in">clearTimeout</span>(pauseTimer);</span><br><span class="line">    pauseTimer = <span class="literal">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (currentSource) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      currentSource.<span class="title function_">stop</span>();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line">    currentSource = <span class="literal">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The state transitions are:</p><ul><li><strong>Ready → Playing</strong>: User clicks Play. Segments are extracted, cleaned, chunked. <code>playChunk(0)</code> starts the chain.</li><li><strong>Playing → Paused</strong>: User clicks Pause. <code>stopAudio()</code> halts the current chunk. <code>currentChunk</code> is preserved. Clicking Play resumes from the same index.</li><li><strong>Playing → Stopped</strong>: User clicks Stop. <code>stopped = true</code> causes <code>playChunk()</code> to exit on its next invocation. Progress resets to zero.</li><li><strong>Paused → Playing</strong>: User clicks Play. <code>paused = false</code>, then <code>playChunk(currentChunk)</code> resumes.</li><li><strong>Playing → Done</strong>: The chunk index exceeds <code>chunks.length</code>. Progress bar fills to 100%.</li></ul><p>Voice settings (Speed, Pitch, Mouth, Throat) are read from the sliders at the moment each chunk is synthesised, not when playback starts. This means adjusting a slider mid-playback takes effect on the next chunk. To hear the change immediately, the user can pause and resume.</p><h2 id="Progress-Tracking"><a href="#Progress-Tracking" class="headerlink" title="Progress Tracking"></a>Progress Tracking</h2><p>Progress is tracked by counting speakable chunks (non-null entries) rather than all entries:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">countSpeakable</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> n = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; chunks.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (chunks[i] !== <span class="literal">null</span>) n++;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> n;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateProgress</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> total = <span class="title function_">countSpeakable</span>();</span><br><span class="line">  <span class="keyword">var</span> done = <span class="title function_">countSpoken</span>();</span><br><span class="line">  <span class="keyword">var</span> pct = total &gt; <span class="number">0</span> ? (done / total) * <span class="number">100</span> : <span class="number">0</span>;</span><br><span class="line">  progressBar.<span class="property">style</span>.<span class="property">width</span> = pct + <span class="string">&quot;%&quot;</span>;</span><br><span class="line">  statusEl.<span class="property">textContent</span> = done + <span class="string">&quot; / &quot;</span> + total;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This prevents pauses from inflating the total count and making the progress bar advance non-uniformly. A post with many headings (and therefore many pause markers) reports the same total as a post of equivalent text length without headings.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Phase-VI-Documentation-and-Publication"><a href="#Phase-VI-Documentation-and-Publication" class="headerlink" title="Phase VI: Documentation and Publication"></a>Phase VI: Documentation and Publication</h1><h2 id="The-README"><a href="#The-README" class="headerlink" title="The README"></a>The README</h2><p>Commit <code>d6ed17b</code> (1:14 PM) added a 224-line README covering installation, usage, configuration, architecture, and credits. The documentation was written with a specific goal: a user should be able to install and configure the plugin without reading the source code.</p><p>The “Inside out” section described the three-component architecture:</p><ol><li><strong>Generator</strong> (<code>lib/generator.js</code>) serves the bundled <code>sam.js</code> library as a virtual Hexo asset.</li><li><strong>Helper</strong> (<code>lib/helper.js</code>) renders the widget HTML&#x2F;CSS&#x2F;JS when <code>&lt;%- sam_reader() %&gt;</code> is called in a template.</li><li><strong>Client-side</strong>, the widget extracts text, cleans it, chunks it, and plays it via the Web Audio API.</li></ol><p>The README included a configuration block with every option documented, a table of text cleaning transformations, examples of abbreviation configuration with the case-sensitivity rules explained, and a styling example showing how to create a green-themed widget with only five overrides.</p><h2 id="Version-History"><a href="#Version-History" class="headerlink" title="Version History"></a>Version History</h2><p>The project went through three versions in a single day:</p><ul><li><strong>v1.0.0</strong> (10:51 AM): Initial commit. Functional but hardcoded.</li><li><strong>v1.0.1</strong> (11:44 AM): Configuration refactor. All hardcoding removed. Abbreviation system generalised. Fifty minutes of development.</li><li><strong>v1.0.2</strong> (7:25 PM): Documentation complete. README published. pnpm support added. Attribution comments added. Live demo link included.</li></ul><p>The version numbering follows semver: the jump from 1.0.0 to 1.0.1 marked a backwards-compatible feature addition (configuration support). The jump to 1.0.2 marked documentation and metadata patches.</p><h2 id="Commit-History"><a href="#Commit-History" class="headerlink" title="Commit History"></a>Commit History</h2><p>The full ten-commit history tells the development story concisely:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">0e7d294  init: desire to publish package</span><br><span class="line">3b5d5c2  chore: fix typos and inconcise comments</span><br><span class="line">cfc40c2  feat: remove hardcoding from the library, push to _config.yml instead</span><br><span class="line">ec40955  version: update to v1.0.1</span><br><span class="line">d6ed17b  docs: publish README documentation describing process, with asset sam.png</span><br><span class="line">f0dba3f  docs: fix readme toc link</span><br><span class="line">f795c36  update JS functions to add author and credit</span><br><span class="line">47d93d6  docs: add pnpm, improve usage guidelines (css wrapper)</span><br><span class="line">f52e824  Merge branch &#x27;main&#x27;</span><br><span class="line">3651782  update to v1.0.2</span><br></pre></td></tr></table></figure><p>The pattern is typical of a rapid development sprint: initial implementation, immediate typo cleanup, the substantial feature commit, a version bump, documentation in bulk, then small fixes and a final version bump.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Architecture-Summary"><a href="#Architecture-Summary" class="headerlink" title="Architecture Summary"></a>Architecture Summary</h1><p>The complete data flow from Hexo build to audible speech:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">hexo generate</span><br><span class="line">    │</span><br><span class="line">    ├── index.js</span><br><span class="line">    │   ├── Merge _config.yml with defaults (Object.assign)</span><br><span class="line">    │   ├── Register helper: sam_reader()</span><br><span class="line">    │   └── Register generator: sam_reader_assets</span><br><span class="line">    │</span><br><span class="line">    ├── lib/generator.js</span><br><span class="line">    │   └── Serve assets/sam.js at /js/hexo-sam-reader/sam.js</span><br><span class="line">    │</span><br><span class="line">    └── lib/helper.js (called per post where sam: true)</span><br><span class="line">        └── Returns HTML string:</span><br><span class="line">            ├── Widget markup (controls, progress bar, sliders)</span><br><span class="line">            ├── Inline CSS (colours from config.style)</span><br><span class="line">            └── Inline IIFE JavaScript:</span><br><span class="line">                │</span><br><span class="line">                ├── Load sam.js dynamically</span><br><span class="line">                ├── On click Play:</span><br><span class="line">                │   ├── getPostSegments()</span><br><span class="line">                │   │   ├── Clone content DOM</span><br><span class="line">                │   │   ├── Remove skip elements</span><br><span class="line">                │   │   ├── Walk tree → segments[]</span><br><span class="line">                │   │   │   ├── Text nodes → cleanForSam(text)</span><br><span class="line">                │   │   │   ├── Tables → readTable() → cleanForSam()</span><br><span class="line">                │   │   │   ├── Headings → null, text, null</span><br><span class="line">                │   │   │   └── Block elements → trailing null</span><br><span class="line">                │   │   └── Return [string | null, ...]</span><br><span class="line">                │   ├── buildChunks(segments, 200)</span><br><span class="line">                │   │   ├── Split by sentences (.!?)</span><br><span class="line">                │   │   ├── Split by commas if still too long</span><br><span class="line">                │   │   └── Compress consecutive nulls</span><br><span class="line">                │   └── playChunk(0)</span><br><span class="line">                │       ├── null → setTimeout(PAUSE_MS) → next</span><br><span class="line">                │       └── string → SamJs.buf32() → AudioBuffer → play → onended → next</span><br><span class="line">                │</span><br><span class="line">                ├── On click Pause: stopAudio(), preserve index</span><br><span class="line">                └── On click Stop: reset all state</span><br></pre></td></tr></table></figure><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>hexo-sam-reader was built in a single day. The prototype existed before that day, but the transformation from a personal script to a distributable package — the modularisation, the configuration framework, the virtual asset serving, the documentation — was completed in ten commits across eight and a half hours.</p><p>The technical constraints shaped the design. SAM operates on 7-bit ASCII, so the text pipeline strips everything else. SAM has practical input length limits, so the chunker splits on sentence and comma boundaries. SAM runs synchronously in the browser, so the playback engine chains chunks through <code>onended</code> callbacks to avoid blocking the main thread. The Web Audio API requires user gesture initialisation, so the <code>AudioContext</code> is created lazily on first play.</p><p>The configuration refactor was the pivotal decision. A plugin with hardcoded colours and abbreviations is a personal script. A plugin that reads its configuration from <code>_config.yml</code>, merges user overrides with sensible defaults, and lets users specify their own abbreviation maps and colour schemes is a package that other people can use.</p><p>The source is available at <a href="https://github.com/trintlermint/hexo-sam-reader">github.com&#x2F;trintlermint&#x2F;hexo-sam-reader</a>. Install with <code>npm install hexo-sam-reader</code>, add <code>sam: true</code> to a post, and call <code>&lt;%- sam_reader() %&gt;</code> in your theme’s sidebar partial.</p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">SAM (Software Automatic Mouth) was created by Mark Barton of Don't Ask Software in 1982, originally for the Commodore 64. It was one of the first commercially available speech synthesisers for home computers.<a href="#fnref:1" rev="footnote"> ↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">SamJs v0.3.0 by Christian Schiffler (discordier). A JavaScript port of SAM that preserves the original phoneme engine. Source: <a href="https://github.com/discordier/sam">github.com/discordier/sam</a>.<a href="#fnref:2" rev="footnote"> ↩</a></span></li><li id="fn:3"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">3.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">The Web Audio API provides the <code>AudioContext</code>, <code>AudioBuffer</code>, and <code>AudioBufferSourceNode</code> interfaces used for PCM playback. SAM outputs audio at 22050 Hz, which is passed directly to <code>AudioContext.createBuffer()</code>.<a href="#fnref:3" rev="footnote"> ↩</a></span></li></ol></div></div>]]>
    </content>
    <id>http://blog.trintler.me/2026/04/02/hexo-sam-reader-Development/</id>
    <link href="http://blog.trintler.me/2026/04/02/hexo-sam-reader-Development/"/>
    <published>2026-04-02T18:00:00.000Z</published>
    <summary>
      <![CDATA[<b>hexo-sam-reader</b> is a Hexo plugin that adds a SAM (Software Automatic Mouth) text-to-speech reader widget to blog posts. This document traces the full development of configuring the SAM TTS for my blog posts, and publishing on npm.]]>
    </summary>
    <title>hexo-sam-reader: Building a SAM Text-to-Speech Plugin for Hexo</title>
    <updated>2026-04-05T06:39:16.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Niladri Adhikary</name>
    </author>
    <category term="Projects" scheme="http://blog.trintler.me/categories/Projects/"/>
    <category term="oss" scheme="http://blog.trintler.me/tags/oss/"/>
    <category term="c++" scheme="http://blog.trintler.me/tags/c/"/>
    <category term="tui" scheme="http://blog.trintler.me/tags/tui/"/>
    <category term="ftxui" scheme="http://blog.trintler.me/tags/ftxui/"/>
    <category term="ssh" scheme="http://blog.trintler.me/tags/ssh/"/>
    <category term="yaml" scheme="http://blog.trintler.me/tags/yaml/"/>
    <category term="cmake" scheme="http://blog.trintler.me/tags/cmake/"/>
    <category term="packaging" scheme="http://blog.trintler.me/tags/packaging/"/>
    <content>
      <![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><h2 id="Motivation"><a href="#Motivation" class="headerlink" title="Motivation"></a>Motivation</h2><p>As a university student with examinations around the corner, one grows tired of the same Flashcards and Quizlet decks.For this reason, I wanted something that I could use freely as a Terminal User Interface that paired well with the rest of my dotfiles and respective setup. At a later point, this turned to some rivalry based on points scored on <em>Who knows the most Haskell?</em> (I do!)</p><p>After finding and contributing to FTXUI<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="FTXUI: Functional Terminal (X) User Interface. See [ArthurSonzogni/FTXUI on GitHub](https://github.com/ArthurSonzogni/FTXUI), version 6.1.9 used.">[2]</span></a></sup> on Github, I decided to develop <strong>Certamen</strong> <em>(latin: contest)</em><sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="See the [Wikipedia article on Certamen](https://en.wikipedia.org/wiki/Certamen), Latin for contest or quiz competition.">[1]</span></a></sup>.</p><span id="more"></span><h2 id="Scope-of-This-Document"><a href="#Scope-of-This-Document" class="headerlink" title="Scope of This Document"></a>Scope of This Document</h2><p>This post traces the full chronological development of Certamen across 105 commits in roughly 5 weeks of development which sprouts off of the initial CLI prototype made back in September 2025 to the current state of the project as of 3rd of April, 2026. This is designed to cover and explain my personal architectural choices, the TUI migration, the SSH server implementation, packaging for multiple platforms, and the refactoring passes that followed.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="I-The-CLI-Prototype-“Quizzer”"><a href="#I-The-CLI-Prototype-“Quizzer”" class="headerlink" title="I: The CLI Prototype, “Quizzer”"></a>I: The CLI Prototype, “Quizzer”</h1><h2 id="The-Initial-Version"><a href="#The-Initial-Version" class="headerlink" title="The Initial Version"></a>The Initial Version</h2><p>The project began back in 29th September 2025 under the name <strong>Quizzer</strong> as a single-file CLI application. With a <code>main.cpp</code> worth ca. 300 lines; it used <code>yaml-cpp</code><sup id="fnref:3"><a href="#fn:3" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="yaml-cpp: A YAML parser and emitter for C++. See [jbeder/yaml-cpp](https://github.com/jbeder/yaml-cpp) on GitHub.">[3]</span></a></sup> for quiz serialisation from a top-down mapping sequence with the standard <code>std::cin</code>&#x2F;<code>std::cout</code> user interaction.</p><p>The original data structure worked like so:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Question</span></span><br><span class="line">&#123;</span><br><span class="line">    std::string question;</span><br><span class="line">    std::vector&lt;std::string&gt; choices;</span><br><span class="line">    <span class="type">int</span> answer; <span class="comment">// &#x27;answer&#x27; is an index onto &#x27;choices&#x27;</span></span><br><span class="line">    std::optional&lt;std::string&gt; code;</span><br><span class="line">    std::optional&lt;std::string&gt; explain;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Quiz files at this stage were <em>flat</em> YAML sequences with <code>question</code>, <code>choices</code> and <code>answer</code> as mandatory fields:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">question:</span> <span class="string">What</span> <span class="string">is</span> <span class="number">2</span><span class="string">+2?</span></span><br><span class="line">  <span class="attr">choices:</span> [<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>]</span><br><span class="line">  <span class="attr">answer:</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p>The <strong>main menu</strong> was rendered trivially:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">static</span> <span class="type">int</span> <span class="title">menu_choice</span><span class="params">(<span class="type">bool</span> randomise)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;\nQuizzer (Randomise: &quot;</span> &lt;&lt; (randomise ? <span class="string">&quot;ON&quot;</span> : <span class="string">&quot;OFF&quot;</span>) &lt;&lt; <span class="string">&quot;)\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;1. Take Quiz\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;2. Add Question\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;3. Remove Question\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;4. Change Answer\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;5. List Questions\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;6. Save and Exit\n&quot;</span></span><br><span class="line">                 <span class="string">&quot;7. Toggle Randomise\n&quot;</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">read_int_in_range</span>(<span class="number">1</span>, <span class="number">7</span>, <span class="string">&quot;Choose (1-7): &quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>All input was <em>blocking</em>, validated through helper functions such as <code>read_int_in_range</code>, <code>read_line</code>, and <code>read_yes_no</code> to reduce duplications of code. The build system was a straightforward CMakeLists.txt linking <code>yaml-cpp</code>:</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">cmake_minimum_required</span>(VERSION <span class="number">3.12</span>)</span><br><span class="line"><span class="keyword">project</span>(quizzer LANGUAGES CXX)</span><br><span class="line"><span class="keyword">set</span>(CMAKE_CXX_STANDARD <span class="number">17</span>)</span><br><span class="line"><span class="keyword">find_package</span>(yaml-cpp REQUIRED)</span><br><span class="line"><span class="keyword">add_executable</span>(quizzer main.cpp)</span><br><span class="line"><span class="keyword">target_link_libraries</span>(quizzer PRIVATE yaml-cpp)</span><br></pre></td></tr></table></figure><p>While functional, this is not the end product I wanted for this passion project; I decided it would be more of use to others and myself if there were more highlights, options, and a more entertaining interface in comparison to the traditional <em>black and white</em> CLI. Thus, one decides to write a <em>Terminal</em> UI for this, not <em>Graphics</em>!</p><h2 id="Pre-Fixes"><a href="#Pre-Fixes" class="headerlink" title="Pre-Fixes"></a>Pre-Fixes</h2><p>The idea of making it a TUI was 5 months after the Quizzer app was built in September. Due to this, a few critical bugs were addressed and a few features were implemented by 2026-03-06 to ease the transition:</p><ul><li><strong>Ctrl+D hang</strong>: The program would enter an infinite loop on <code>EOF</code> because <code>std::cin</code> stream state was not being checked.</li><li><strong>Score display</strong>: The quiz result formatting was corrected, alongside which many optimisations were made.</li><li><strong>Diff before quit</strong>: A feature was added to show unsaved changes before exiting, which now developed into a full system to depict differences between <code>original</code> and <code>modified</code> states!</li><li><strong>Explanations and code in list view</strong>: <code>List Questions</code> was debugged to properly print code blocks and explanations inline, while earlier it had issues with sequencing and showing them.</li></ul><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="II-The-TUI-Migration"><a href="#II-The-TUI-Migration" class="headerlink" title="II: The TUI Migration"></a>II: The TUI Migration</h1><h2 id="Decomposition"><a href="#Decomposition" class="headerlink" title="Decomposition"></a>Decomposition</h2><p>On 2026-03-06, the centralised <code>main.cpp</code> was split into a header&#x2F;source structure under <code>src/</code>. This was the first step towards <em>at least some</em> modularity:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">src/</span><br><span class="line">  main.cpp          # screen routing</span><br><span class="line">  app.hpp           # AppState struct, screen enum</span><br><span class="line">  model.hpp/.cpp    # higher level data structures</span><br><span class="line">  syntax.hpp/.cpp</span><br><span class="line">  screens/</span><br><span class="line">    menu.cpp/.hpp</span><br><span class="line">    quiz.cpp/.hpp</span><br><span class="line">    quiz_result.cpp/.hpp</span><br><span class="line">    add_question.cpp/.hpp</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>Every screen reads&#x2F;writes to a single mutable state object; that being <strong>AppState</strong>. The reasoning behind the use of a flat state struct in place of a class hierarchy (OOP) was mainly due to the fact that FTXUI components are <em>closures</em> that capture <strong>references</strong>, and a single owning struct simplifies lifetime management.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum class</span> <span class="title class_">AppScreen</span></span><br><span class="line">&#123;</span><br><span class="line">    MENU,</span><br><span class="line">    QUIZ,</span><br><span class="line">    QUIZ_RESULT,</span><br><span class="line">    ADD_QUESTION,</span><br><span class="line">    <span class="comment">// ... rest of the main menu options</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AppState</span></span><br><span class="line">&#123;</span><br><span class="line">    std::vector&lt;Question&gt; questions;</span><br><span class="line">    <span class="type">bool</span> randomise = <span class="literal">false</span>;</span><br><span class="line">    std::string status_message;</span><br><span class="line">    AppScreen current_screen = AppScreen::MENU;</span><br><span class="line">    <span class="comment">// ... rest fields for screen-specific state</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Screen “routing” is implemented through a <code>Container::Tab</code> indexed by the enumeration:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> tab = Container::<span class="built_in">Tab</span>(std::<span class="built_in">move</span>(screens), &amp;screen_index);</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> main_renderer = <span class="built_in">Renderer</span>(tab, [&amp;] &#123;</span><br><span class="line">    screen_index = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(state.current_screen);</span><br><span class="line">    <span class="keyword">return</span> tab-&gt;<span class="built_in">Render</span>() | <span class="built_in">size</span>(WIDTH, LESS_THAN, <span class="number">120</span>) | center;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>This is a <strong>state machine</strong>! My favourite! where <code>AppScreen</code> is the state and user input events are the <em>transitions</em>. Each screen is a self-contained FTXUI component produced by a factory function e.g. <code>make_menu_screen(AppState&amp;)</code>, and screen transitions are performed by <em>mutating</em> <code>state.current_screen</code>.</p><h2 id="FTXUI-The-Terminal-UI-Framework"><a href="#FTXUI-The-Terminal-UI-Framework" class="headerlink" title="FTXUI: The Terminal UI Framework"></a>FTXUI: The Terminal UI Framework</h2><p><strong>FTXUI</strong><sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="FTXUI: Functional Terminal (X) User Interface. See [ArthurSonzogni/FTXUI on GitHub](https://github.com/ArthurSonzogni/FTXUI), version 6.1.9 used.">[2]</span></a></sup> is the rendering engine used for this project. It is useful in many ways:</p><ol><li><strong>Declarative rendering</strong>: the <code>Render()</code> lambda returns an element tree each frame; meaning that FTXUI handles diff-ing against the terminal (emulator).</li><li><strong>Component model</strong>: <code>CatchEvent</code> wrappers allow composing input handling without subclassing or further abstraction.</li><li><strong>Platform compatibility</strong>: Linux, and Homebrew (macOS&#x2F;Windows).</li></ol><blockquote><p>I did not want to move this to more popular alternatives such as <code>BubbleTea</code> or <code>LipGloss</code>, etc. since (1) I did not want to switch to a different language and (2) I wanted my code to be (less) indexed by LLMs.</p></blockquote><p>The CMake integration changed from a single-target build to a multi-library link, FTXUI facilitates Make processes by allowing <code>FetchContent</code>:</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">include</span>(FetchContent)</span><br><span class="line">FetchContent_Declare(ftxui</span><br><span class="line">  GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI</span><br><span class="line">  GIT_TAG v6.<span class="number">1.9</span></span><br><span class="line">)</span><br><span class="line">FetchContent_MakeAvailable(ftxui)</span><br><span class="line"></span><br><span class="line"><span class="keyword">find_package</span>(yaml-cpp REQUIRED)</span><br><span class="line"><span class="keyword">find_package</span>(libssh REQUIRED) <span class="comment"># ssh to come</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">file</span>(GLOB_RECURSE SOURCES src/*.cpp)</span><br><span class="line"><span class="keyword">add_executable</span>(certamen <span class="variable">$&#123;SOURCES&#125;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">target_link_libraries</span>(certamen</span><br><span class="line">  PRIVATE ftxui::screen ftxui::dom ftxui::component</span><br><span class="line">  PRIVATE yaml-cpp</span><br><span class="line">  PRIVATE ssh                 <span class="comment"># ssh to come</span></span><br><span class="line">  PRIVATE util</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>FTXUI provides three libraries: <code>ftxui::screen</code>, <code>ftxui::dom</code> for the element tree (layout, borders, colours), and <code>ftxui::component</code> for widgets and event handling.</p><h2 id="Screen-Implementation"><a href="#Screen-Implementation" class="headerlink" title="Screen Implementation"></a>Screen Implementation</h2><p>Each <strong>screen</strong> follows the same <em>pattern</em>. Here is the quiz screen as an example, which demonstrates the rendering of questions with <em>code blocks</em>, <em>choice selection</em>, and <em>answer feedback</em> after which a <code>Renderer</code> is returned to spit it out:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">ftxui::Component <span class="title">make_quiz_screen</span><span class="params">(AppState&amp; state)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> focusable = <span class="built_in">Renderer</span>([](<span class="type">bool</span>) &#123; <span class="keyword">return</span> <span class="built_in">text</span>(<span class="string">&quot;&quot;</span>); &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> component = <span class="built_in">CatchEvent</span>(focusable, [&amp;](Event event) &#123;</span><br><span class="line">        <span class="comment">// for example if I want to navigate using arrowkeys</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">nav_up_down</span>(event, state.quiz_selected, num_choices)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        <span class="comment">// or if I want to use numbers, 0 -&gt; 9.</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">nav_numeric</span>(event, state.quiz_selected, num_choices)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (event == Event::Return)</span><br><span class="line">        &#123;</span><br><span class="line">            state.quiz_answered = <span class="literal">true</span>;</span><br><span class="line">            state.quiz_was_correct = (state.quiz_selected == q.answer);</span><br><span class="line">            <span class="keyword">if</span> (state.quiz_was_correct) ++state.quiz_score;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Renderer</span>(component, [&amp;] &#123;</span><br><span class="line">        <span class="comment">// build element *tree* i.e. header, progress gauge, question, choices, explaination</span></span><br><span class="line">        Elements body;</span><br><span class="line">        body.<span class="built_in">push_back</span>(<span class="built_in">gauge</span>(progress) | <span class="built_in">color</span>(Color::Cyan));</span><br><span class="line">        body.<span class="built_in">push_back</span>(<span class="built_in">paragraph</span>(<span class="string">&quot; &quot;</span> + q.question) | bold);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (q.code &amp;&amp; !q.code-&gt;<span class="built_in">empty</span>())</span><br><span class="line">            body.<span class="built_in">push_back</span>(<span class="built_in">render_code_block</span>(*q.code, q.language));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; num_choices; ++i)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="comment">// colour based on selection state and correctness (so if I chose wrong, _RED_; else _GREEN_)</span></span><br><span class="line">            <span class="keyword">auto</span> choice_el = <span class="built_in">hbox</span>(&#123;</span><br><span class="line">                <span class="built_in">text</span>(marker), <span class="built_in">text</span>(std::<span class="built_in">to_string</span>(i + <span class="number">1</span>) + <span class="string">&quot;. &quot;</span>), <span class="built_in">text</span>(q.choices[i]),</span><br><span class="line">            &#125;);</span><br><span class="line">            <span class="keyword">if</span> (state.quiz_answered &amp;&amp; is_correct)</span><br><span class="line">                choice_el = choice_el | <span class="built_in">color</span>(Color::Green) | bold;</span><br><span class="line">            <span class="comment">// ...</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">vbox</span>(std::<span class="built_in">move</span>(body)) | borderRounded;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The <code>Renderer</code> wraps a <code>CatchEvent</code> that processes input and mutates <code>AppState</code>. The rendering lambda reads state each frame to produce the element tree (as shown in Line 21 above). FTXUI diffentiates this against the previous frame and emits only the necessary terminal <em>escape sequences</em>.<br>Now, in what I said above, I used a lot of <strong>jargon</strong>; what really happens is:<br>FTXUI sees <strong>“Hey! this has changed!”</strong> and it goes <strong>“Let’s update it and show the user what the program resolved to!”</strong><br>That is <em>all</em>.<br>Regardless, this separation of input handling from rendering is what permits the TUI to remain responsive, due to the continous “diffing”<sup id="fnref:5"><a href="#fn:5" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="FTXUI uses a declarative rendering model: the component returns an `Element` tree each frame, and the framework computes the minimal set of terminal escape sequences to update the display. This is conceptually similar to React's virtual DOM diffing.">[5]</span></a></sup>.</p><h2 id="Syntax-Highlighting-Engine"><a href="#Syntax-Highlighting-Engine" class="headerlink" title="Syntax Highlighting Engine"></a>Syntax Highlighting Engine</h2><p>A bespoke highlighter was implemented in <code>syntax.cpp</code> to render code blocks within quiz questions themselves, this way the user has an easier time parsing the code with their eyes. It supports four language families (Haskell, C-family, Python, Rust) with keyword colouring, string literals, numeric literals, comments (line&#x2F;block), and operators.</p><p>The design is a hand-written lexer that tokenises each line:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum class</span> <span class="title class_">Lang</span> &#123; Haskell, CFam, Python, Rust, Unknown &#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">const</span> std::unordered_set&lt;std::string&gt; haskell_kw = &#123; <span class="comment">// for example, for haskell:</span></span><br><span class="line">    <span class="string">&quot;module&quot;</span>,<span class="string">&quot;where&quot;</span>,<span class="string">&quot;let&quot;</span>,<span class="string">&quot;in&quot;</span>,<span class="string">&quot;if&quot;</span>,<span class="string">&quot;then&quot;</span>,<span class="string">&quot;else&quot;</span>,<span class="string">&quot;case&quot;</span>,<span class="string">&quot;of&quot;</span>,<span class="string">&quot;do&quot;</span>,</span><br><span class="line">    <span class="comment">// etc...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">static</span> Element <span class="title">highlight_line</span><span class="params">(<span class="type">const</span> std::string&amp; line, Lang lang,</span></span></span><br><span class="line"><span class="params"><span class="function">                              <span class="type">const</span> std::unordered_set&lt;std::string&gt;&amp; kw)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Elements parts;</span><br><span class="line">    <span class="comment">// detect comments, strings, numbers, keywords, operators</span></span><br><span class="line">    <span class="comment">// emit coloured text spans for any given token</span></span><br><span class="line">    <span class="keyword">while</span> (i &lt; len)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="comment">/* line comment */</span>)  <span class="built_in">flush_text</span>(line.<span class="built_in">substr</span>(i), Color::GrayDark);</span><br><span class="line">        <span class="keyword">if</span> (<span class="comment">/* string literal */</span>) <span class="built_in">flush_text</span>(s, Color::Yellow);</span><br><span class="line">        <span class="keyword">if</span> (<span class="comment">/* number */</span>)         <span class="built_in">flush_text</span>(num, Color::Magenta);</span><br><span class="line">        <span class="keyword">if</span> (kw.<span class="built_in">count</span>(word))       <span class="built_in">flush_text</span>(word, Color::Cyan);</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">hbox</span>(std::<span class="built_in">move</span>(parts));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>As you must be already familiar by now, <code>Color::name</code> signifies a base set colour which most Terminal Emulators support the rendering of! &#x3D;)</p></blockquote><p>The code block is then rendered with a language label and border:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">ftxui::Element <span class="title">render_code_block</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> std::string&amp; code,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> std::optional&lt;std::string&gt;&amp; language)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Lang lang = <span class="built_in">detect_lang</span>(language);</span><br><span class="line">    <span class="type">const</span> <span class="keyword">auto</span>&amp; kw = <span class="built_in">keywords_for</span>(lang);</span><br><span class="line"></span><br><span class="line">    Elements lines;</span><br><span class="line">    <span class="function">std::istringstream <span class="title">stream</span><span class="params">(code)</span></span>;</span><br><span class="line">    std::string line;</span><br><span class="line">    <span class="keyword">while</span> (std::<span class="built_in">getline</span>(stream, line))</span><br><span class="line">        lines.<span class="built_in">push_back</span>(<span class="built_in">hbox</span>(&#123; <span class="built_in">text</span>(<span class="string">&quot;  &quot;</span>), <span class="built_in">highlight_line</span>(line, lang, kw) &#125;));</span><br><span class="line"></span><br><span class="line">    std::string label = <span class="string">&quot; Code&quot;</span>;</span><br><span class="line">    <span class="keyword">if</span> (language &amp;&amp; !language-&gt;<span class="built_in">empty</span>())</span><br><span class="line">        label += <span class="string">&quot; [&quot;</span> + *language + <span class="string">&quot;]&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">vbox</span>(&#123;</span><br><span class="line">        <span class="built_in">text</span>(label) | bold | <span class="built_in">color</span>(Color::Cyan),</span><br><span class="line">        <span class="built_in">vbox</span>(std::<span class="built_in">move</span>(lines)),</span><br><span class="line">    &#125;) | borderRounded | <span class="built_in">color</span>(Color::GrayLight);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This is <strong>NOT</strong> a production-grade syntax highlighter. It does <strong>not</strong> handle multi-line strings, heredocs, or nested block comments spanning lines. It handles the common cases sufficient for quiz code snippets, and I did not want to add a linter dependency!</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="III-Name-Change"><a href="#III-Name-Change" class="headerlink" title="III: Name Change"></a>III: Name Change</h1><h2 id="Quizzer-to-Certamen"><a href="#Quizzer-to-Certamen" class="headerlink" title="Quizzer to Certamen"></a>Quizzer to Certamen</h2><p>On 2026-03-26, the project was faithfully renamed from <strong>Quizzer</strong> to <strong>Certamen</strong>. The word is Latin for <em>contest</em> or <em>quiz contest</em>, found via Wikipedia<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="See the [Wikipedia article on Certamen](https://en.wikipedia.org/wiki/Certamen), Latin for contest or quiz competition.">[1]</span></a></sup>.</p><p>This was executed across two commits (<code>09ce38b</code>, <code>d4eb31c</code>). One wonders, what is one of the easiest ways to replace this string accross all these files? With <code>sed</code>:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find . -<span class="built_in">type</span> f -<span class="built_in">exec</span> sed -i <span class="string">&#x27;s/quizzer/certamen/g&#x27;</span> &#123;&#125; +</span><br></pre></td></tr></table></figure><p>To explain this, <code>find</code> initially finds all files from the <code>./</code> directory i.e. current working directory.<br>After which, <code>-exec sed -i &#39;s/quizzer/certamen/g&#39; {} +</code> executes <code>sed</code>, which edits each file in place with the aforementioned <strong>regex</strong>. <code>{}</code> signifies the current file being processed kept in memory, and <code>+</code> groups multiple files together in <code>find</code>.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="IV-The-New-Quiz-Wrapper"><a href="#IV-The-New-Quiz-Wrapper" class="headerlink" title="IV: The New Quiz Wrapper"></a>IV: The New Quiz Wrapper</h1><h2 id="YAML-Schema-Evolution"><a href="#YAML-Schema-Evolution" class="headerlink" title="YAML Schema Evolution"></a>YAML Schema Evolution</h2><p>The original Quizzer used a flat YAML sequence as shown earlier, however, when the TUI was built, the schema was <em>wrapped</em> with some metadata. That being <code>name</code> and <code>author</code>; to accomodate for this, previous <code>question</code> were nested inside <code>questions</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Certamen</span> <span class="string">DEMO</span></span><br><span class="line"><span class="attr">author:</span> <span class="string">trintlermint</span></span><br><span class="line"><span class="attr">questions:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">question:</span> <span class="string">Which</span> <span class="string">Haskell</span> <span class="string">functions</span> <span class="string">can</span> <span class="string">calculate</span> <span class="string">the</span> <span class="string">Euclidean</span> <span class="string">norm?</span></span><br><span class="line">    <span class="attr">code:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      nrm1 :: Double -&gt; Double -&gt; Double</span></span><br><span class="line"><span class="string">      nrm1 a b = sqrt (a^2 + b^2)</span></span><br><span class="line"><span class="string"></span>    <span class="attr">explain:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      Both definitions compute sqrt(a^2 + b^2).</span></span><br><span class="line"><span class="string"></span>    <span class="attr">language:</span> <span class="string">haskell</span></span><br><span class="line">    <span class="attr">choices:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Only</span> <span class="string">nrm1</span> <span class="string">is</span> <span class="string">correct.</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Only</span> <span class="string">nrm2</span> <span class="string">is</span> <span class="string">correct.</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Both</span> <span class="string">are</span> <span class="string">correct.</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Neither</span> <span class="string">is</span> <span class="string">correct.</span></span><br><span class="line">    <span class="attr">answer:</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><p>The <code>language</code> field was added alongside the syntax highlighter. The <code>Question</code> struct gained it as well:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Question</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ... rest</span></span><br><span class="line">    std::optional&lt;std::string&gt; language;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>The <code>QuizFile</code> struct holds the new top-level metadata:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">QuizFile</span></span><br><span class="line">&#123;</span><br><span class="line">    std::string name;</span><br><span class="line">    std::string author;</span><br><span class="line">    std::vector&lt;Question&gt; questions;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Serialisation uses <code>yaml-cpp</code>‘s emitter API with <code>YAML::Literal</code> for multi-line code and explanation fields shown with how <code>|</code> is used earlier along with this new emitter:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">save_quiz</span><span class="params">(<span class="type">const</span> QuizFile&amp; quiz, <span class="type">const</span> std::string&amp; filename)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    YAML::Emitter out;</span><br><span class="line">    out &lt;&lt; YAML::BeginMap;</span><br><span class="line">    out &lt;&lt; YAML::Key &lt;&lt; <span class="string">&quot;name&quot;</span>   &lt;&lt; YAML::Value &lt;&lt; quiz.name;</span><br><span class="line">    out &lt;&lt; YAML::Key &lt;&lt; <span class="string">&quot;author&quot;</span> &lt;&lt; YAML::Value &lt;&lt; quiz.author;</span><br><span class="line">    out &lt;&lt; YAML::Key &lt;&lt; <span class="string">&quot;questions&quot;</span> &lt;&lt; YAML::Value;</span><br><span class="line">    <span class="comment">// emit_questions handles the sequence</span></span><br><span class="line">    out &lt;&lt; YAML::EndMap;</span><br><span class="line"></span><br><span class="line">    <span class="function">std::ofstream <span class="title">file_out</span><span class="params">(filename)</span></span>;</span><br><span class="line">    file_out &lt;&lt; out.<span class="built_in">c_str</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="V-Server-Shell"><a href="#V-Server-Shell" class="headerlink" title="V: Server Shell"></a>V: Server Shell</h1><h2 id="Architecture"><a href="#Architecture" class="headerlink" title="Architecture"></a>Architecture</h2><p>The <strong>Server Shell (SSH)</strong> server mode under option <code>certamen serve</code> was the most architecturally ambitious feature. The design forks a child process per client, connected via a PTY (pseudo-terminal), so that the full TUI renders identically over SSH as it does locally.</p><p>The current flow behind-the-scenes:</p><ol><li><strong><code>serve_main</code></strong> binds an SSH socket using <code>libssh</code>, this is the listener for connections to the server.</li><li>connection &#x3D;&gt; <code>fork()</code> creates a handler for the client.</li><li><strong><code>handle_client</code></strong> performs a SSH key exchange, authenticates the user, channel negotiation, and shell requests.</li><li><strong><code>forkpty</code></strong> creates a <em>PTY pair</em> and forks <em>again</em>; the child then runs <code>certamen --session</code> (itself) via <code>execvp</code> without asking client.</li><li><strong><code>bridge_io</code></strong> shuttles data between the SSH channel and the PTY master using <code>poll()</code>.</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The core I/O bridge goes as SSH channel and PTY master bidirectionally</span></span><br><span class="line"><span class="function"><span class="type">static</span> <span class="type">void</span> <span class="title">bridge_io</span><span class="params">(ssh_channel channel, <span class="type">int</span> master_fd, ssh_session session)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">char</span> buf[<span class="number">8192</span>];</span><br><span class="line">    <span class="type">int</span> ssh_fd = <span class="built_in">ssh_get_fd</span>(session);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> flags = <span class="built_in">fcntl</span>(master_fd, F_GETFL, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (flags &gt;= <span class="number">0</span>) <span class="built_in">fcntl</span>(master_fd, F_SETFL, flags | O_NONBLOCK);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (<span class="built_in">ssh_channel_is_open</span>(channel) &amp;&amp; !<span class="built_in">ssh_channel_is_eof</span>(channel))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">struct</span> <span class="title class_">pollfd</span> fds[<span class="number">2</span>];</span><br><span class="line">        fds[<span class="number">0</span>].fd = ssh_fd;    fds[<span class="number">0</span>].events = POLLIN;</span><br><span class="line">        fds[<span class="number">1</span>].fd = master_fd; fds[<span class="number">1</span>].events = POLLIN;</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> ret = <span class="built_in">poll</span>(fds, <span class="number">2</span>, <span class="number">200</span>);</span><br><span class="line">        <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123; <span class="keyword">if</span> (errno == EINTR) <span class="keyword">continue</span>; <span class="keyword">break</span>; &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// SSH to PTY</span></span><br><span class="line">        <span class="keyword">if</span> (fds[<span class="number">0</span>].revents &amp; (POLLIN | POLLHUP | POLLERR))</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">ssh_execute_message_callbacks</span>(session);</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="type">int</span> n = <span class="built_in">ssh_channel_read_nonblocking</span>(channel, buf, <span class="built_in">sizeof</span>(buf), <span class="number">0</span>);</span><br><span class="line">                <span class="keyword">if</span> (n &gt; <span class="number">0</span>) <span class="built_in">write</span>(master_fd, buf, n);</span><br><span class="line">                <span class="keyword">else</span> <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// PTY to SSH</span></span><br><span class="line">        <span class="keyword">if</span> (fds[<span class="number">1</span>].revents &amp; POLLIN)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="type">ssize_t</span> n = <span class="built_in">read</span>(master_fd, buf, <span class="built_in">sizeof</span>(buf));</span><br><span class="line">                <span class="keyword">if</span> (n &gt; <span class="number">0</span>) <span class="built_in">ssh_channel_write</span>(channel, buf, n);</span><br><span class="line">                <span class="keyword">else</span> <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="The-Session-Mode"><a href="#The-Session-Mode" class="headerlink" title="The Session Mode"></a>The Session Mode</h2><p>When the server forks a child, it executes <code>certamen --session --metrics /tmp/certamen_metrics_XXXXXX &lt;quiz files&gt;</code>. The <code>--session</code> flag triggers <code>session_main</code> that presents a stripped-down experience, which asks:</p><ol><li><strong>Name</strong>: The client enters their display name.</li><li><strong>Picker</strong>: If multiple files are loaded, a menu to select one only.</li><li><strong>Quiz</strong>: You’re in! The standard quiz screen (mid-game).</li><li><strong>Result</strong>: Score display, then return to the picker OR disconnect.</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">session_main</span><span class="params">(<span class="type">const</span> std::vector&lt;std::string&gt;&amp; quiz_files,</span></span></span><br><span class="line"><span class="params"><span class="function">                 <span class="type">const</span> std::string&amp; metrics_file)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> screen = ScreenInteractive::<span class="built_in">Fullscreen</span>();</span><br><span class="line"></span><br><span class="line">    std::string player_name;</span><br><span class="line">    <span class="built_in">run_name_prompt</span>(player_name, screen);</span><br><span class="line">    <span class="keyword">if</span> (player_name.<span class="built_in">empty</span>()) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (quiz_files.<span class="built_in">size</span>() == <span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">auto</span> [score, total] = <span class="built_in">run_quiz</span>(quiz_files[<span class="number">0</span>], screen);</span><br><span class="line">        <span class="built_in">write_metrics</span>(metrics_file, player_name, quiz_files[<span class="number">0</span>], score, total);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">true</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">int</span> chosen = <span class="built_in">run_quiz_picker</span>(quiz_files, player_name, screen);</span><br><span class="line">        <span class="keyword">if</span> (chosen &lt; <span class="number">0</span>) <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">auto</span> [score, total] = <span class="built_in">run_quiz</span>(quiz_files[chosen], screen);</span><br><span class="line">        <span class="built_in">write_metrics</span>(metrics_file, player_name, quiz_files[chosen], score, total);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The metrics are written to a temporary file in <code>tmp/</code> as illustrated earlier. Then read by the parent server process after the child exits, logged to stdout of the actual server, and then cleaned up:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[2026-03-26 20:18:45] METRICS [vanilla]: player=vanilla</span><br><span class="line">[2026-03-26 20:18:45] METRICS [vanilla]: quiz=algebra.yaml</span><br><span class="line">[2026-03-26 20:18:45] METRICS [vanilla]: score=8/10</span><br></pre></td></tr></table></figure><h2 id="Terminal-Resize-Handling"><a href="#Terminal-Resize-Handling" class="headerlink" title="Terminal Resize Handling"></a>Terminal Resize Handling</h2><p>Window resize events from the SSH client are sent through a message callback that updates the PTY dimensions:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">static</span> <span class="type">int</span> <span class="title">message_callback</span><span class="params">(ssh_session, ssh_message msg, <span class="type">void</span>* userdata)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> master_fd = *<span class="built_in">static_cast</span>&lt;<span class="type">int</span>*&gt;(userdata);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">ssh_message_type</span>(msg) == SSH_REQUEST_CHANNEL &amp;&amp;</span><br><span class="line">        <span class="built_in">ssh_message_subtype</span>(msg) == SSH_CHANNEL_REQUEST_WINDOW_CHANGE)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">int</span> cols = <span class="built_in">ssh_message_channel_request_pty_width</span>(msg);</span><br><span class="line">        <span class="type">int</span> rows = <span class="built_in">ssh_message_channel_request_pty_height</span>(msg);</span><br><span class="line">        <span class="keyword">struct</span> <span class="title class_">winsize</span> ws&#123;&#125;;</span><br><span class="line">        ws.ws_col = <span class="built_in">static_cast</span>&lt;<span class="type">unsigned</span> <span class="type">short</span>&gt;(cols);</span><br><span class="line">        ws.ws_row = <span class="built_in">static_cast</span>&lt;<span class="type">unsigned</span> <span class="type">short</span>&gt;(rows);</span><br><span class="line">        <span class="built_in">ioctl</span>(master_fd, TIOCSWINSZ, &amp;ws);</span><br><span class="line">        <span class="built_in">ssh_message_channel_request_reply_success</span>(msg);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>This way, lets say if someone has multiple windows open, if they delete said windows, Certamen comes back! &#x3D;D<br><img src="/2026/03/28/Certamen-the-TUI-Quizzing-App/multiple-windows.png" alt="Multiple Windows"></p></blockquote><h2 id="Platform-Concerns"><a href="#Platform-Concerns" class="headerlink" title="Platform Concerns"></a>Platform Concerns</h2><p>The SSH server uses <code>forkpty()</code><sup id="fnref:6"><a href="#fn:6" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="The `forkpty` approach was inspired by how `ttyd` and similar web-terminal tools expose TUI applications over HTTP/WebSocket by bridging PTY I/O.">[6]</span></a></sup> which is <strong>POSIX</strong>-specific. The header inclusion is branched:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">if</span> defined(__linux__)</span></span><br><span class="line">  <span class="meta">#<span class="keyword">include</span> <span class="string">&lt;pty.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">elif</span> defined(__APPLE__)</span></span><br><span class="line">  <span class="meta">#<span class="keyword">include</span> <span class="string">&lt;util.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line">  <span class="comment">// windows: no pty.h/openpty path in this implementation yet</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><p>This is the primary reason Windows builds are marked as <code>continue-on-error: true</code> in the CI pipeline. The local TUI mode works on Windows; the server does not however.</p><h2 id="Authentication"><a href="#Authentication" class="headerlink" title="Authentication"></a>Authentication</h2><p>The server supports two authentication modes:</p><ul><li><strong>Open</strong> <em>(default)</em>: Any SSH client connects without a password. The SSH username becomes the player name.</li><li><strong>Password</strong>: Pass <code>--password &lt;pw&gt;</code> and clients must authenticate, shown below:</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Open</span></span><br><span class="line">certamen serve quiz.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># Password</span></span><br><span class="line">certamen serve --password quiznight --port 3000 quiz.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># Client</span></span><br><span class="line">ssh -p 3000 alice@192.168.1.10</span><br></pre></td></tr></table></figure><p>The host <strong>RSA key</strong> is auto-generated on first run using <strong><code>ssh_pki_generate</code></strong> and stored as <code>certamen_host_rsa</code> with permissions <code>0600</code>, that is, sudo (root) can read&#x2F;write, other users cannot however.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="VI-Multi-File-Support"><a href="#VI-Multi-File-Support" class="headerlink" title="VI: Multi-File Support"></a>VI: Multi-File Support</h1><h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Initially, Certamen loaded a <strong>single</strong> YAML file. For a quiz night with multiple topics (say, algebra, history, and most importantly: <em>Haskell</em>), one would need to merge them manually or run separate instances. On 2026-03-28, multi-file support was implemented.</p><h2 id="Data-Architecture"><a href="#Data-Architecture" class="headerlink" title="Data Architecture"></a>Data Architecture</h2><p>Each loaded file is tracked by a <code>LoadedFile</code> struct:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">LoadedFile</span></span><br><span class="line">&#123;</span><br><span class="line">    std::string filename;</span><br><span class="line">    std::string name;</span><br><span class="line">    std::string author;</span><br><span class="line">    std::vector&lt;Question&gt; saved_questions;</span><br><span class="line">    std::string saved_name;</span><br><span class="line">    std::string saved_author;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Questions carry a <code>source_file</code> index (<code>-1</code> for unassigned). The <code>AppState</code> then holds a flat <code>std::vector&lt;Question&gt;</code> with all questions from all files chosen, along with <code>std::vector&lt;LoadedFile&gt;</code> for per-file metadata. This allows <code>source_file</code> to serve as an index to enable <strong>per-file</strong> saving and editing for each <em>screen</em> to use.</p><h2 id="Screen-Routing-with-File-Picker"><a href="#Screen-Routing-with-File-Picker" class="headerlink" title="Screen Routing with File Picker"></a>Screen Routing with File Picker</h2><p>When multiple files are loaded, editing operations (add, remove, etc.) must target a specific file. The <code>route_to</code> method intercepts navigation:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">AppScreen <span class="title">route_to</span><span class="params">(AppScreen dest)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (loaded_files.<span class="built_in">size</span>() &gt; <span class="number">1</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        pick_file_cursor = <span class="number">0</span>;</span><br><span class="line">        pick_file_then = dest;</span><br><span class="line">        <span class="keyword">return</span> AppScreen::PICK_FILE;</span><br><span class="line">    &#125;</span><br><span class="line">    target_file = loaded_files.<span class="built_in">empty</span>() ? <span class="number">-1</span> : <span class="number">0</span>;</span><br><span class="line">    <span class="built_in">build_target_indices</span>();</span><br><span class="line">    <span class="keyword">return</span> dest;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>If only one file is loaded it implies that routing proceeds directly. Else, the user is first sent to a file picker screen, then forwarded to the intended destination. The <code>pick_file_then</code> field stores the <em>deferred</em> target.</p><h2 id="Quiz-Setup-Screen"><a href="#Quiz-Setup-Screen" class="headerlink" title="Quiz Setup Screen"></a>Quiz Setup Screen</h2><p>When taking a quiz with multiple files, the user selects which files to include and in what order in <strong>local mode</strong>. The <code>QUIZ_SETUP</code> screen has two parts:</p><ul><li><strong>State 0</strong>: Toggle inclusion of each file (checkbox-style).</li><li><strong>State 1</strong>: Reorder the included files.</li></ul><p>The selected questions are then concatenated and fed to <code>start_quiz_from()</code>.</p><blockquote><p>One trivially sees that since <code>Randomise</code> already juggles around <strong>ALL</strong> loaded questions and choices, if <code>Randomise</code> is enabled, there is <strong>no</strong> <em>file picker</em>. Perhaps this is unintuitive, and the order of files should still be present? Do let me know!</p></blockquote><h2 id="Unsaved-Change-Tracking"><a href="#Unsaved-Change-Tracking" class="headerlink" title="Unsaved Change Tracking"></a>Unsaved Change Tracking</h2><p>The diff system compares the current in-memory state against the last-saved snapshot per file:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">has_unsaved_changes</span><span class="params">()</span> <span class="type">const</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(loaded_files.<span class="built_in">size</span>()); ++i)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">const</span> <span class="keyword">auto</span>&amp; lf = loaded_files[i];</span><br><span class="line">        <span class="keyword">if</span> (i == <span class="number">0</span> &amp;&amp; (quiz_name != lf.saved_name || quiz_author != lf.saved_author))</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        std::vector&lt;Question&gt; current;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">const</span> <span class="keyword">auto</span>&amp; q : questions)</span><br><span class="line">            <span class="keyword">if</span> (q.source_file == i)</span><br><span class="line">                current.<span class="built_in">push_back</span>(q);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (current != lf.saved_questions)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">const</span> <span class="keyword">auto</span>&amp; q : questions)</span><br><span class="line">        <span class="keyword">if</span> (q.source_file &lt; <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>We do not use the original file due to the fact that the original file may already have been edited mid-session or outside session. As the program has no way to know if this is the case, it saves snapshots instead.</p></blockquote><p>Diff lines are generated with prefix markers <code>[+]</code> <em>for additions</em>, <code>[-]</code> <em>for deletions</em>, <code>[~]</code> <em>for changes in answer&#x2F;choices</em>, and <code>[0]</code> <em>for no changes</em>, then rendered with colour coding via the shared <code>diff.hpp</code> utility:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> ftxui::Elements <span class="title">render_diff_lines</span><span class="params">(<span class="type">const</span> std::vector&lt;std::string&gt;&amp; diff_lines)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Elements entries;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">const</span> <span class="keyword">auto</span>&amp; line : diff_lines)</span><br><span class="line">    &#123;</span><br><span class="line">        Color line_color = Color::Default;</span><br><span class="line">        <span class="keyword">if</span> (line.<span class="built_in">size</span>() &gt; <span class="number">1</span> &amp;&amp; line[<span class="number">0</span>] == <span class="string">&#x27;[&#x27;</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (line[<span class="number">1</span>] == <span class="string">&#x27;+&#x27;</span>) line_color = Color::Green;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (line[<span class="number">1</span>] == <span class="string">&#x27;-&#x27;</span>) line_color = Color::RedLight;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (line[<span class="number">1</span>] == <span class="string">&#x27;~&#x27;</span>) line_color = Color::Yellow;</span><br><span class="line">        &#125;</span><br><span class="line">        entries.<span class="built_in">push_back</span>(<span class="built_in">text</span>(<span class="string">&quot;  &quot;</span> + line) | <span class="built_in">color</span>(line_color));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> entries;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The menu screen displays a <code>modified</code> indicator in yellow when unsaved changes are present.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="VII-CI-CD-and-Packaging"><a href="#VII-CI-CD-and-Packaging" class="headerlink" title="VII: CI&#x2F;CD and Packaging"></a>VII: CI&#x2F;CD and Packaging</h1><h2 id="GitHub-Actions"><a href="#GitHub-Actions" class="headerlink" title="GitHub Actions"></a>GitHub Actions</h2><p>On 2026-03-27, a multi-platform CI&#x2F;CD pipeline was established using GitHub Actions. The workflow triggers on tag pushes (<code>git tag v* &amp;&amp; git push origin v*</code>) and builds on three platforms:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build-linux:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">sudo</span> <span class="string">apt-get</span> <span class="string">install</span> <span class="string">cmake</span> <span class="string">ninja-build</span> <span class="string">libyaml-cpp-dev</span> <span class="string">libssh-dev</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">cmake</span> <span class="string">--preset</span> <span class="string">release</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">cmake</span> <span class="string">--build</span> <span class="string">--preset</span> <span class="string">release</span> <span class="string">--config</span> <span class="string">Release</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          mkdir certamen-linux-x64</span></span><br><span class="line"><span class="string">          cp build/bin/certamen certamen-linux-x64/</span></span><br><span class="line"><span class="string">          tar -czf certamen-linux-x64.tar.gz certamen-linux-x64/</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">  <span class="attr">build-macos:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">macos-latest</span></span><br><span class="line">    <span class="comment"># similar, using brew for dependencies</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">build-windows:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">windows-latest</span></span><br><span class="line">    <span class="attr">continue-on-error:</span> <span class="literal">true</span> <span class="comment"># SSH server incompatible so far</span></span><br><span class="line">    <span class="comment"># uses vcpkg for yaml-cpp and libssh</span></span><br></pre></td></tr></table></figure><p>The release job depends only on Linux and macOS; Windows is optional because the <code>forkpty</code>-based SSH server cannot compile there as mentioned <a href="#platform-concerns">previously</a>. Releases are created automatically via <code>softprops/action-gh-release</code>.</p><p>Few _hot_fixes followed the initial CI setup:</p><ul><li><strong><code>26bd229</code></strong>: Platform-specific headers in <code>serve.cpp</code> were branched (<code>#if defined(__linux__)</code> &#x2F; <code>#elif defined(__APPLE__)</code>) to fix macOS builds as was shown earlier in <a href="#platform-concerns">Platform Concers</a></li><li><strong><code>cd197e6</code></strong>: CMake presets were added for consistent cross-platform configuration.</li></ul><h2 id="Nix-Packaging"><a href="#Nix-Packaging" class="headerlink" title="Nix Packaging"></a>Nix Packaging</h2><p>A Nix flake was contributed by <a href="https://github.com/valyntyler">@valyntyler</a> on 2026-03-28, providing reproducible builds:</p><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># flake.nix</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">inputs</span> <span class="operator">=</span> &#123;</span><br><span class="line">    <span class="attr">nixpkgs.url</span> <span class="operator">=</span> <span class="string">&quot;github:nixos/nixpkgs?ref=nixos-unstable&quot;</span>;</span><br><span class="line">    <span class="attr">flake-parts.url</span> <span class="operator">=</span> <span class="string">&quot;github:hercules-ci/flake-parts&quot;</span>;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="attr">outputs</span> <span class="operator">=</span> inputs @ &#123;flake-parts, ...&#125;:</span><br><span class="line">    flake-parts.lib.mkFlake &#123;<span class="keyword">inherit</span> inputs;&#125; &#123;</span><br><span class="line">      <span class="attr">systems</span> <span class="operator">=</span> [<span class="string">&quot;x86_64-linux&quot;</span> <span class="string">&quot;aarch64-linux&quot;</span> <span class="string">&quot;aarch64-darwin&quot;</span> <span class="string">&quot;x86_64-darwin&quot;</span>];</span><br><span class="line">      <span class="attr">perSystem</span> <span class="operator">=</span> &#123;pkgs, ...&#125;: <span class="keyword">rec</span> &#123;</span><br><span class="line">        <span class="attr">devShells.default</span> <span class="operator">=</span> pkgs.callPackage <span class="symbol">./nix/shell.nix</span> &#123;&#125;;</span><br><span class="line">        <span class="attr">packages.certamen</span> <span class="operator">=</span> pkgs.callPackage <span class="symbol">./nix/package.nix</span> &#123;&#125;;</span><br><span class="line">        <span class="attr">packages.default</span> <span class="operator">=</span> packages.certamen;</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The package derivation:</p><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># nix/package.nix</span></span><br><span class="line">&#123; cmake, stdenv, ftxui, libssh, yaml-cpp &#125;:</span><br><span class="line"><span class="keyword">let</span> <span class="keyword">inherit</span> (stdenv) mkDerivation;</span><br><span class="line"><span class="keyword">in</span> mkDerivation &#123;</span><br><span class="line">  <span class="attr">pname</span> <span class="operator">=</span> <span class="string">&quot;certamen&quot;</span>;</span><br><span class="line">  <span class="attr">version</span> <span class="operator">=</span> <span class="string">&quot;1.0.3&quot;</span>;</span><br><span class="line">  <span class="attr">src</span> <span class="operator">=</span> <span class="symbol">../.</span>;</span><br><span class="line">  <span class="attr">buildInputs</span> <span class="operator">=</span> [ cmake ftxui libssh yaml-cpp ]; <span class="comment"># deps</span></span><br><span class="line">  <span class="attr">configurePhase</span> <span class="operator">=</span> <span class="string">&#x27;&#x27;cmake -B build -DCMAKE_BUILD_TYPE=Release&#x27;&#x27;</span>;</span><br><span class="line">  <span class="attr">buildPhase</span> <span class="operator">=</span> <span class="string">&#x27;&#x27;cmake --build build&#x27;&#x27;</span>;</span><br><span class="line">  <span class="attr">installPhase</span> <span class="operator">=</span> <span class="string">&#x27;&#x27;</span></span><br><span class="line"><span class="string">    mkdir -p $out/bin</span></span><br><span class="line"><span class="string">    cp ./build/bin/certamen $out/bin/certamen</span></span><br><span class="line"><span class="string">    chmod +x $out/bin/certamen</span></span><br><span class="line"><span class="string">  &#x27;&#x27;</span>; <span class="comment"># execution</span></span><br><span class="line">  <span class="attr">meta.mainProgram</span> <span class="operator">=</span> <span class="string">&quot;certamen&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This enables <strong>imperative</strong> installation trivially via:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nix profile add github:trintlermint/certamen#certamen</span><br></pre></td></tr></table></figure><p>Several CMake-related fixes followed to ensure the Nix build found <code>ftxui</code> as a system package rather than fetching it via <code>FetchContent</code>. The CMakeLists.txt was updated to attempt <code>find_package(ftxui QUIET)</code> first:</p><figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">find_package</span>(ftxui QUIET)</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">NOT</span> ftxui_FOUND)</span><br><span class="line">  <span class="keyword">include</span>(FetchContent)</span><br><span class="line">  FetchContent_Declare(ftxui</span><br><span class="line">    GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI</span><br><span class="line">    GIT_TAG v6.<span class="number">1.9</span></span><br><span class="line">  )</span><br><span class="line">  FetchContent_MakeAvailable(ftxui)</span><br><span class="line"><span class="keyword">endif</span>()</span><br></pre></td></tr></table></figure><blockquote><p>This is done due to the fact that many consumers of <code>NixPkgs</code> do not allow web <code>Fetch</code> access to programs such as <code>CMake</code> due to vulnerability concerns.</p></blockquote><h2 id="AUR-Packaging"><a href="#AUR-Packaging" class="headerlink" title="AUR Packaging"></a>AUR Packaging</h2><p>Certamen was published to the Arch Linux User Repository, installable via:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S certamen</span><br></pre></td></tr></table></figure><p>Or through any other AUR helper&#x2F;wrapper.</p><blockquote><p>This was natural considering <em>I use Arch Linux, btw</em>!</p></blockquote><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="VIII-Manual-and-QOL-Features"><a href="#VIII-Manual-and-QOL-Features" class="headerlink" title="VIII: Manual and QOL Features"></a>VIII: Manual and QOL Features</h1><h2 id="In-Game-Manual"><a href="#In-Game-Manual" class="headerlink" title="In-Game Manual"></a>In-Game Manual</h2><p>On 2026-03-31, a built-in manual screen was implemented. Accessible via option 10 on the menu or by pressing <code>0</code>, it provides a reference for all features, keybindings, and quiz format documentation without leaving the TUI. Which is seemingly rather helpful for new users! Less README!</p><h2 id="Codeberg-Migration"><a href="#Codeberg-Migration" class="headerlink" title="Codeberg Migration"></a>Codeberg Migration</h2><p>The repository has also been mirrored to Codeberg due to concerns about licensing, code ownership, and platform ethics. The README is now embossed with dual badges:<br><img src="https://img.shields.io/badge/Codeberg-certamen-blue?logo=codeberg" alt="Codeberg"><img src="https://img.shields.io/badge/GitHub-certamen-181717?logo=github" alt="GitHub"></p><h2 id="Stance-on-AI"><a href="#Stance-on-AI" class="headerlink" title="Stance on AI"></a>Stance on AI</h2><p><code>CONTRIBUTING.md</code> was updated with the project’s stance on AI-assisted contributions, as bearing the <a href="https://brainmade.org/">brainmade.org</a> mark, this project accepts <strong>NO</strong> purely <em>“vibe-coded”</em> contributions.<br>As shown by this <a href="https://github.com/trintlermint/certamen/pull/13"><em>“vibe-coded”</em> pull request</a> rejection.</p><p><img src="https://brainmade.org/white-logo.svg" alt="Brainmade Org Certamen"></p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="IX-Code-Quality-and-Refactoring"><a href="#IX-Code-Quality-and-Refactoring" class="headerlink" title="IX: Code Quality and Refactoring"></a>IX: Code Quality and Refactoring</h1><h2 id="Shared-Utility-Headers"><a href="#Shared-Utility-Headers" class="headerlink" title="Shared Utility Headers"></a>Shared Utility Headers</h2><p>After the feature set stabilised, duplicated code patterns were extracted into shared headers:</p><h3 id="Navigation-nav-hpp"><a href="#Navigation-nav-hpp" class="headerlink" title="Navigation (nav.hpp)"></a>Navigation (<code>nav.hpp</code>)</h3><p>Six <code>screen</code> files contained near-identical <code>j</code>&#x2F;<code>k</code>&#x2F;arrow-binds&#x2F;<code>1-9</code> navigation logic. This was pushed into two inline functions:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">bool</span> <span class="title">nav_up_down</span><span class="params">(<span class="type">const</span> ftxui::Event&amp; event, <span class="type">int</span>&amp; selected, <span class="type">int</span> count)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (event == ftxui::Event::ArrowUp || event == ftxui::Event::<span class="built_in">Character</span>(<span class="string">&#x27;k&#x27;</span>))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (selected &gt; <span class="number">0</span>) selected--;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (event == ftxui::Event::ArrowDown || event == ftxui::Event::<span class="built_in">Character</span>(<span class="string">&#x27;j&#x27;</span>))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (selected &lt; count - <span class="number">1</span>) selected++;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">bool</span> <span class="title">nav_numeric</span><span class="params">(<span class="type">const</span> ftxui::Event&amp; event, <span class="type">int</span>&amp; selected, <span class="type">int</span> count)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!event.<span class="built_in">is_character</span>()) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="type">char</span> ch = event.<span class="built_in">character</span>()[<span class="number">0</span>];</span><br><span class="line">    <span class="type">int</span> num = ch - <span class="string">&#x27;1&#x27;</span>;</span><br><span class="line">    <span class="keyword">if</span> (num &gt;= <span class="number">0</span> &amp;&amp; num &lt; count)</span><br><span class="line">    &#123;</span><br><span class="line">        selected = num;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This replaced approximately 100 lines of duplicated code across <code>quiz.cpp</code>, <code>change_answer.cpp</code>, <code>remove_question.cpp</code>, <code>edit_choice.cpp</code>, and <code>list_questions.cpp</code>.</p><h3 id="Diff-Rendering-diff-hpp"><a href="#Diff-Rendering-diff-hpp" class="headerlink" title="Diff Rendering (diff.hpp)"></a>Diff Rendering (<code>diff.hpp</code>)</h3><p>The diff colouring logic was duplicated between <code>save_confirm.cpp</code> and <code>quit_confirm.cpp</code>. The shared <code>render_diff_lines</code> function shown <a href="#diff-rendering-diffhpp">earlier</a> eliminated this.</p><h2 id="AppState-Helper"><a href="#AppState-Helper" class="headerlink" title="AppState Helper"></a>AppState Helper</h2><p>The pattern <code>state.current_screen = AppScreen::MENU; state.status_message.clear();</code> appeared in 15 locations. It was also lovingly compressed:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">return_to_menu</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    current_screen = AppScreen::MENU;</span><br><span class="line">    status_message.<span class="built_in">clear</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>Along with this, several other dead code and typos were fixed which appeared throughout the development process</p></blockquote><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Reflections"><a href="#Reflections" class="headerlink" title="Reflections"></a>Reflections</h1><h2 id="The-Flat-State-Decision"><a href="#The-Flat-State-Decision" class="headerlink" title="The Flat State Decision"></a>The Flat State Decision</h2><p>The decision to use a single <code>AppState</code> struct with <strong>40+</strong> fields rather than per-screen state objects or a more structured hierarchy was thoughtful. In FTXUI, components are typically closures capturing references. A single struct means that any given screen can read any state it needs without ownership <em>loop-de-loops</em>! The tradeoff is that <code>AppState</code> is large and its fields are loosely grouped by comments rather than enforced by <strong>rigor</strong> in types.</p><p>For a project of this scale (ca. 4200 lines across 30 source files), this is acceptable. A larger project would benefit from per-screen state structs composed within <code>AppState</code>, or <strong>message-passing</strong> architecture. The current design was chosen because it permitted rapid iteration during the intensive short term development sprint due to my current examinations.</p><h2 id="The-PTY-Fork-Design"><a href="#The-PTY-Fork-Design" class="headerlink" title="The PTY Fork Design"></a>The PTY Fork Design</h2><p>The SSH server’s design of forking the entire binary with <code>--session</code> and bridging via PTY is rather unconventional, however effective. The alternative would be to run the TUI rendering in-process and translate FTXUI’s output to SSH channel writes. This would require either:</p><ol><li>A custom <code>Screen</code> implementation that writes to an SSH channel instead of stdout, or</li><li>Intercepting FTXUI’s terminal output at the file descriptor level.</li></ol><p>Both approaches are brittle and couple the server tightly to FTXUI’s internals, making it difficult for someone to contribute openly, or for a <em>system administrator</em> to understand. The <code>forkpty</code> + <code>execvp</code> approach treats the TUI as a <strong>“black box”</strong>:</p><blockquote><p>Does it work locally? then it must it over SSH. (hmmm . . . .)</p></blockquote><p>The cost is a process per client, which is negligible for the expected concurrency (I dont imagine having a gigantic quiz night with 200 people, yet).</p><blockquote><p>Specifically, there is absolutely <strong>no way</strong> I am convincing 200 people to play quizzes with me <strong>over the terminal</strong>.</p></blockquote><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Certamen began as a 300-line CLI tool for quizzing myself on Haskell functions on my initial quartile 1 exams, and within a few weeks after months, it grew into a pile of 4200-line TUI application with syntax highlighting, multi-file quiz sessions, SSH multiplayer, cross-platform CI&#x2F;CD, and packaging for AUR and Nix.</p><p>The codebase is full of <em>spiky</em> edges. There are <strong>NO</strong> automated tests which makes <strong>Quality Assessment</strong> a <em>nightmare</em>; the state struct is more <em>centralised</em> than I enjoy myself; the Windows build is perpetually broken. These are understandable costs for a project whose primary purpose is serving as a personal Free and Open Source utility.</p><p><strong>Certamen</strong> means <em>contest</em>. The real contest, as it seems to turn out, was <strong>finishing</strong> this project before the next exam season!</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">See the <a href="https://en.wikipedia.org/wiki/Certamen">Wikipedia article on Certamen</a>, Latin for contest or quiz competition.<a href="#fnref:1" rev="footnote"> ↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">FTXUI: Functional Terminal (X) User Interface. See <a href="https://github.com/ArthurSonzogni/FTXUI">ArthurSonzogni/FTXUI on GitHub</a>, version 6.1.9 used.<a href="#fnref:2" rev="footnote"> ↩</a></span></li><li id="fn:3"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">3.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">yaml-cpp: A YAML parser and emitter for C++. See <a href="https://github.com/jbeder/yaml-cpp">jbeder/yaml-cpp</a> on GitHub.<a href="#fnref:3" rev="footnote"> ↩</a></span></li><li id="fn:4"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">4.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">libssh: The SSH library. See <a href="https://www.libssh.org/">libssh.org</a>.<a href="#fnref:4" rev="footnote"> ↩</a></span></li><li id="fn:5"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">5.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">FTXUI uses a declarative rendering model: the component returns an <code>Element</code> tree each frame, and the framework computes the minimal set of terminal escape sequences to update the display. This is conceptually similar to React's virtual DOM diffing.<a href="#fnref:5" rev="footnote"> ↩</a></span></li><li id="fn:6"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">6.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">The <code>forkpty</code> approach was inspired by how <code>ttyd</code> and similar web-terminal tools expose TUI applications over HTTP/WebSocket by bridging PTY I/O.<a href="#fnref:6" rev="footnote"> ↩</a></span></li></ol></div></div>]]>
    </content>
    <id>http://blog.trintler.me/2026/03/28/Certamen-the-TUI-Quizzing-App/</id>
    <link href="http://blog.trintler.me/2026/03/28/Certamen-the-TUI-Quizzing-App/"/>
    <published>2026-03-28T13:20:00.000Z</published>
    <summary>
      <![CDATA[<b>Certamen</b> is a Terminal User Interface quiz game engine written in C++, built with FTXUI, yaml-cpp and libssh. This document traces the entire development from CLI alpha called "Quizzer" to a TUI application with SSH multi-player, multi-file quiz sessions, syntax highlighting, and multi-platform CI/CD. Available freely on <a href="https://github.com/trintlermint/certamen">Github</a> and <a href="https://codeberg.org/trintlermint/certamen">Codeberg</a>.]]>
    </summary>
    <title>Certamen: Building a TUI Quiz Game Engine in C++</title>
    <updated>2026-04-05T06:35:24.596Z</updated>
  </entry>
  <entry>
    <author>
      <name>Niladri Adhikary</name>
    </author>
    <category term="Review" scheme="http://blog.trintler.me/categories/Review/"/>
    <category term="joss" scheme="http://blog.trintler.me/tags/joss/"/>
    <category term="review" scheme="http://blog.trintler.me/tags/review/"/>
    <category term="oss" scheme="http://blog.trintler.me/tags/oss/"/>
    <category term="open-sci" scheme="http://blog.trintler.me/tags/open-sci/"/>
    <category term="numerical-methods" scheme="http://blog.trintler.me/tags/numerical-methods/"/>
    <category term="python" scheme="http://blog.trintler.me/tags/python/"/>
    <category term="math" scheme="http://blog.trintler.me/tags/math/"/>
    <content>
      <![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><h1 id="SOLVE-NIVP"><a href="#SOLVE-NIVP" class="headerlink" title="SOLVE_NIVP"></a>SOLVE_NIVP</h1><p>I had the pleasure of peer-reviewing the research software package <strong>solve_nivp</strong>, published in the <a href="https://joss.theoj.org/">Journal of Open Source Software (JOSS)</a>, <a href="https://doi.org/10.21105/joss.09775"><strong>DOI: 10.21105/joss.09775</strong></a>, authored by <strong>David Riley</strong> and <strong>Ioannis Stefanou</strong>.</p><p><img src="https://joss.theoj.org/papers/10.21105/joss.09775/status.svg" alt="JOSS DOI badge"></p><p>I was invited as co-reviewer alongside <strong><a href="https://github.com/ductho-le">Ductho Le</a></strong> and <strong><a href="https://github.com/wwhenxuan">WhenXuan</a></strong> by <em>Editor</em> <strong><a href="https://danielskatz.org/">Daniel S. Katz</a></strong>. This post is <strong>not a review log</strong>, it is an explanation of what solve_nivp actually does, since the problem it solves is genuinely interesting and under-discussed.</p><span id="more"></span><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="What-is-Wrong-with-Non-Smooth-Systems"><a href="#What-is-Wrong-with-Non-Smooth-Systems" class="headerlink" title="What is Wrong with Non-Smooth Systems?"></a>What is Wrong with Non-Smooth Systems?</h1><h2 id="Smooth-ODEs"><a href="#Smooth-ODEs" class="headerlink" title="Smooth ODEs"></a>Smooth ODEs</h2><p>A classical ordinary differential equation <strong>(ODE)</strong> looks like this:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="10.433ex" height="2.301ex" role="img" focusable="false" viewBox="0 -767 4611.2 1017"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(313.8,-2) translate(-250 0)"><path data-c="2D9" d="M190 609Q190 637 208 653T252 669Q275 667 292 652T309 609Q309 579 292 564T250 549Q225 549 208 564T190 609Z"></path></g></g></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(1905.6,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(2455.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(2844.6,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(3416.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(3861.2,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(4222.2,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></p><p>where <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="6.841ex" height="1.636ex" role="img" focusable="false" viewBox="0 -683 3023.8 723"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z"></path></g><g data-mml-node="msup" transform="translate(1794.6,0)"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="211D" d="M17 665Q17 672 28 683H221Q415 681 439 677Q461 673 481 667T516 654T544 639T566 623T584 607T597 592T607 578T614 565T618 554L621 548Q626 530 626 497Q626 447 613 419Q578 348 473 326L455 321Q462 310 473 292T517 226T578 141T637 72T686 35Q705 30 705 16Q705 7 693 -1H510Q503 6 404 159L306 310H268V183Q270 67 271 59Q274 42 291 38Q295 37 319 35Q344 35 353 28Q362 17 353 3L346 -1H28Q16 5 16 16Q16 35 55 35Q96 38 101 52Q106 60 106 341T101 632Q95 645 55 648Q17 648 17 665ZM241 35Q238 42 237 45T235 78T233 163T233 337V621L237 635L244 648H133Q136 641 137 638T139 603T141 517T141 341Q141 131 140 89T134 37Q133 36 133 35H241ZM457 496Q457 540 449 570T425 615T400 634T377 643Q374 643 339 648Q300 648 281 635Q271 628 270 610T268 481V346H284Q327 346 375 352Q421 364 439 392T457 496ZM492 537T492 496T488 427T478 389T469 371T464 361Q464 360 465 360Q469 360 497 370Q593 400 593 495Q593 592 477 630L457 637L461 626Q474 611 488 561Q492 537 492 496ZM464 243Q411 317 410 317Q404 317 401 315Q384 315 370 312H346L526 35H619L606 50Q553 109 464 243Z"></path></g></g><g data-mml-node="mi" transform="translate(755,363) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g></g></g></svg></mjx-container> is the state and <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.244ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 550 910"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g></g></g></svg></mjx-container> is a <em>smooth</em> function. “Smooth” here means <em>differentiable</em>, <em>well-behaved</em>, and (crucially) friendly to solvers like Runge-Kutta, DOPRI, or SciPy’s <code>solve_ivp</code><sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[solve_ivp package](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html)">[1]</span></a></sup>. One takes small <em>steps</em>, estimates the derivative, continues by moving forward, repeat. It works beautifully when <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.244ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 550 910"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g></g></g></svg></mjx-container> “behaves.”</p><p>The issue is that a large class of real systems does not behave in the “ideal” proposed way; leaving them seemingly ignored.</p><h2 id="Impacts-Switching-Constraints-in-Nonsmooth-Systems"><a href="#Impacts-Switching-Constraints-in-Nonsmooth-Systems" class="headerlink" title="Impacts, Switching, Constraints in Nonsmooth Systems"></a>Impacts, Switching, Constraints in Nonsmooth Systems</h2><p>Consider, for example, a ball bouncing on a floor. Between bounces, the dynamics are perfectly smooth:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="6.897ex" height="2.188ex" role="img" focusable="false" viewBox="0 -762 3048.6 967"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D45E" d="M33 157Q33 258 109 349T280 441Q340 441 372 389Q373 390 377 395T388 406T404 418Q438 442 450 442Q454 442 457 439T460 434Q460 425 391 149Q320 -135 320 -139Q320 -147 365 -148H390Q396 -156 396 -157T393 -175Q389 -188 383 -194H370Q339 -192 262 -192Q234 -192 211 -192T174 -192T157 -193Q143 -193 143 -185Q143 -182 145 -170Q149 -154 152 -151T172 -148Q220 -148 230 -141Q238 -136 258 -53T279 32Q279 33 272 29Q224 -10 172 -10Q117 -10 75 30T33 157ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(316.8,-7) translate(-250 0)"><path data-c="A8" d="M95 612Q95 633 112 651T153 669T193 652T210 612Q210 588 194 571T152 554L127 560Q95 577 95 612ZM289 611Q289 634 304 649T335 668Q336 668 340 668T346 669Q369 669 386 652T404 612T387 572T346 554Q323 554 306 570T289 611Z"></path></g></g></g><g data-mml-node="mo" transform="translate(737.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mo" transform="translate(1793.6,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mi" transform="translate(2571.6,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g></g></g></svg></mjx-container></p><p>where <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="1.041ex" height="1.439ex" role="img" focusable="false" viewBox="0 -442 460 636"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45E" d="M33 157Q33 258 109 349T280 441Q340 441 372 389Q373 390 377 395T388 406T404 418Q438 442 450 442Q454 442 457 439T460 434Q460 425 391 149Q320 -135 320 -139Q320 -147 365 -148H390Q396 -156 396 -157T393 -175Q389 -188 383 -194H370Q339 -192 262 -192Q234 -192 211 -192T174 -192T157 -193Q143 -193 143 -185Q143 -182 145 -170Q149 -154 152 -151T172 -148Q220 -148 230 -141Q238 -136 258 -53T279 32Q279 33 272 29Q224 -10 172 -10Q117 -10 75 30T33 157ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g></g></svg></mjx-container> is height and <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.079ex" height="1.464ex" role="img" focusable="false" viewBox="0 -442 477 647"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g></g></g></svg></mjx-container> is gravitational acceleration. But the moment <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="1.041ex" height="1.439ex" role="img" focusable="false" viewBox="0 -442 460 636"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45E" d="M33 157Q33 258 109 349T280 441Q340 441 372 389Q373 390 377 395T388 406T404 418Q438 442 450 442Q454 442 457 439T460 434Q460 425 391 149Q320 -135 320 -139Q320 -147 365 -148H390Q396 -156 396 -157T393 -175Q389 -188 383 -194H370Q339 -192 262 -192Q234 -192 211 -192T174 -192T157 -193Q143 -193 143 -185Q143 -182 145 -170Q149 -154 152 -151T172 -148Q220 -148 230 -141Q238 -136 258 -53T279 32Q279 33 272 29Q224 -10 172 -10Q117 -10 75 30T33 157ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g></g></g></svg></mjx-container> hits zero, the velocity flips:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="12.525ex" height="2.053ex" role="img" focusable="false" viewBox="0 -825.2 5536.3 907.2"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="mo" transform="translate(518,413) scale(0.707)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g></g><g data-mml-node="mo" transform="translate(1395.9,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mo" transform="translate(2451.7,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mi" transform="translate(3229.7,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mo" transform="translate(3917.9,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="msup" transform="translate(4418.1,0)"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="mo" transform="translate(518,413) scale(0.707)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g></g></g></g></svg></mjx-container></p><p>where <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.53ex" height="1.779ex" role="img" focusable="false" viewBox="0 -775.2 1118.1 786.2"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="mo" transform="translate(518,363) scale(0.707)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g></g></g></g></svg></mjx-container> is the velocity just before impact, <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="2.53ex" height="1.779ex" role="img" focusable="false" viewBox="0 -775.2 1118.1 786.2"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="mo" transform="translate(518,363) scale(0.707)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g></g></g></g></svg></mjx-container> is just after, and <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.347ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3689.2 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mo" transform="translate(743.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z"></path></g><g data-mml-node="mo" transform="translate(1688.6,0)"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(1966.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(2466.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mn" transform="translate(2911.2,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mo" transform="translate(3411.2,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z"></path></g></g></g></svg></mjx-container> is the coefficient of restitution where (1 =&gt; perfectly elastic and 0 =&gt; dead drop). This is the <strong>Newton impact law</strong>; and it is not differentiable. The velocity field has a discontinuity at the <em>“constraint”</em> surface <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.439ex;" xmlns="http://www.w3.org/2000/svg" width="5.189ex" height="1.946ex" role="img" focusable="false" viewBox="0 -666 2293.6 860"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45E" d="M33 157Q33 258 109 349T280 441Q340 441 372 389Q373 390 377 395T388 406T404 418Q438 442 450 442Q454 442 457 439T460 434Q460 425 391 149Q320 -135 320 -139Q320 -147 365 -148H390Q396 -156 396 -157T393 -175Q389 -188 383 -194H370Q339 -192 262 -192Q234 -192 211 -192T174 -192T157 -193Q143 -193 143 -185Q143 -182 145 -170Q149 -154 152 -151T172 -148Q220 -148 230 -141Q238 -136 258 -53T279 32Q279 33 272 29Q224 -10 172 -10Q117 -10 75 30T33 157ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"></path></g><g data-mml-node="mo" transform="translate(737.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(1793.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container>.</p><div class="tikz-container tikz-svg" data-tikz-mode="svg"><div class="tikz-title">Bouncing Ball — Restitution</div><div class="tikz-body"><script type="text/tikz">\begin{tikzpicture}[scale=0.9, every node/.style={font=\footnotesize}]  \draw[thick, red!40!white] (-0.5,0) -- (8,0);  \fill[pattern=north east lines, pattern color=red!30!white] (-0.5,-0.3) rectangle (8,0);  \draw[thick, red!60!white, dashed]    (0,0) .. controls (0.5,3) and (1.5,3) .. (2,0)           .. controls (2.3,2) and (3.2,2) .. (3.5,0)           .. controls (3.7,1.2) and (4.3,1.2) .. (4.5,0)           .. controls (4.6,0.5) and (4.9,0.5) .. (5,0);  \shade[ball color=red!50!white] (0,0) circle (0.15);  \shade[ball color=red!50!white] (1,2.25) circle (0.15);  \shade[ball color=red!50!white] (2,0) circle (0.15);  \shade[ball color=red!50!white] (2.75,1.5) circle (0.15);  \shade[ball color=red!50!white] (3.5,0) circle (0.15);  \shade[ball color=red!50!white] (4,0.9) circle (0.15);  \shade[ball color=red!50!white] (4.75,0.375) circle (0.1);  \shade[ball color=red!50!white] (5,0) circle (0.15);  \draw[->, thick, white] (1,2.25) -- (1,1.25) node[midway, right] {$mg$};  \node[blue!60!white, right] at (5.5,2.5) {$v^+ = -e \cdot v^-$};  \node[white, right] at (5.5,1.8) {$e{=}1$: perfect elastic};  \node[white, right] at (5.5,1.2) {$e{=}0$: dead stop};  \draw[->, white] (-0.3,0) -- (-0.3,3.5) node[above] {$q$};  \draw[->, white] (-0.5,-0.5) -- (8,-0.5) node[right] {$t$};  \node[red!40!white, below] at (4,-0.5) {$q = 0$ (floor)};\end{tikzpicture}</script></div></div><div class="tikz-container tikz-ascii" data-tikz-mode="ascii" data-tikzsim="{&quot;type&quot;:&quot;bounce&quot;,&quot;height&quot;:450,&quot;color&quot;:[192,129,121],&quot;params&quot;:{&quot;gravity&quot;:0.12,&quot;restitution&quot;:0.7,&quot;ball_char&quot;:&quot;O&quot;,&quot;floor_char&quot;:&quot;=&quot;,&quot;max_trace&quot;:80}}"><div class="tikz-title">Bouncing Ball — Newton Impact Law</div><div class="tikz-body tikz-sim-placeholder" style="min-height:450px"><span class="tikz-sim-loading">initializing...</span></div></div><p>Other examples:</p><ul><li>A relay circuit where a diode switches on at a threshold voltage</li><li>A sliding-mode controller that changes sign when the error crosses past zero</li><li>A dry-friction joint where stick and slip have entirely different dynamics</li><li>Financial models with hard constraints (prices must be non-negative, quantities cannot be fractional)</li></ul><p>All of these belong to the family of <strong>nonsmooth dynamical systems</strong>. Specifically, solve_nivp targets systems expressible as <strong>differential inclusions</strong>:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="10.632ex" height="2.301ex" role="img" focusable="false" viewBox="0 -767 4699.2 1017"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mover"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(313.8,-2) translate(-250 0)"><path data-c="2D9" d="M190 609Q190 637 208 653T252 669Q275 667 292 652T309 609Q309 579 292 564T250 549Q225 549 208 564T190 609Z"></path></g></g></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z"></path></g><g data-mml-node="mi" transform="translate(1794.6,0)"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(2543.6,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(2932.6,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(3504.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(3949.2,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(4310.2,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></p><p>where <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.695ex" height="1.538ex" role="img" focusable="false" viewBox="0 -680 749 680"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g></g></g></svg></mjx-container> is a <strong>set-valued map</strong>, at <em>smooth points</em>, <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="15.71ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 6943.9 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(749,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(1138,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(1710,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(2154.7,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(2515.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(3182.4,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(4238.2,0)"><g data-mml-node="mi"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(550,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(939,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(1511,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(1955.7,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mo" transform="translate(2316.7,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></g></svg></mjx-container> is a singleton and you recover the classical <strong>ODE</strong>. At <em>discontinuities</em> however, <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="1.695ex" height="1.538ex" role="img" focusable="false" viewBox="0 -680 749 680"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g></g></g></svg></mjx-container> returns a set of feasible velocities (the <strong>Filippov convex hull</strong> at a switching surface, or a single value constrained by an inequality).</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Why-Classical-Solvers-Fail"><a href="#Why-Classical-Solvers-Fail" class="headerlink" title="Why Classical Solvers Fail"></a>Why Classical Solvers Fail</h1><h2 id="Stiffness-at-a-given-Discontinuity"><a href="#Stiffness-at-a-given-Discontinuity" class="headerlink" title="Stiffness at a given Discontinuity"></a>Stiffness at a given Discontinuity</h2><p>If you take a smooth solver (Runge-Kutta, Adams-Bashforth) and <em>naively</em> apply it to a nonsmooth system, two unfortunate results occur:</p><ol><li><p><strong>Event detection is now brittle!</strong> The solver doesn’t know when an impact occurs, this leads it to overshoot the constraint surface, finds itself on the <em>wrong side</em>, and then attempts to integrate backwards toward the event. This requires <strong>rootfinding</strong> at every step near a contact surface and gets <em>extremely</em> <strong>expensive</strong>.</p></li><li><p><strong>Regularisation makes everything stiff.</strong> One of the most prevalent workarounds is to replace the hard constraint with a smooth approximation, which is a steep <em>sigmoid</em> instead of a step (This can be thought of as <em>taxing the use</em> of an illegal item, instead of <em>banning</em> it.) This introduces a large <strong>Lipschitz constant</strong>, which forces explicit solvers to use tiny timesteps or blow up in terms of memory, time, etc. You end up taking thousands of steps to resolve what is fundamentally a single instance.</p></li></ol><p>It is worse for a Filippov system (switching at a surface): classical solvers exhibit <strong>chattering</strong> near the sliding surface, flipping between modes at each step, which leads to never having proper convergence.</p><div class="tikz-container tikz-svg" data-tikz-mode="svg"><div class="tikz-title">Solver Chattering at a Switching Surface</div><div class="tikz-body"><script type="text/tikz">\begin{tikzpicture}[scale=0.9, every node/.style={font=\footnotesize}]  \draw[thick, red!40!white, dashed] (-0.5,0) -- (8,0) node[right] {surface};  \draw[thick, red!60!white]    (0,2) -- (1,1) -- (1.5,0.8)    -- (2,0.3) -- (2.3,-0.2) -- (2.5,0.15) -- (2.7,-0.12)    -- (2.9,0.08) -- (3.1,-0.06) -- (3.3,0.03) -- (3.5,-0.02)    -- (3.7,0.01) -- (4,0) -- (7,0);  \shade[ball color=red!50!white] (0,2) circle (0.1);  \shade[ball color=red!50!white] (7,0) circle (0.1);  \draw[blue!60!white] (2.2,-0.6) -- (2.2,-0.8) -- (3.8,-0.8) -- (3.8,-0.6);  \node[blue!60!white, below] at (3,-0.9) {chattering};  \node[white, above] at (5.5,0.2) {sliding mode};  \draw[->, white] (-0.3,-1.5) -- (-0.3,2.5) node[above] {$x(t)$};  \draw[->, white] (-0.5,-1.5) -- (8,-1.5) node[right] {$t$};\end{tikzpicture}</script></div></div><div class="tikz-container tikz-ascii" data-tikz-mode="ascii" data-tikzsim="{&quot;type&quot;:&quot;chatter&quot;,&quot;height&quot;:450,&quot;color&quot;:[220,140,120],&quot;params&quot;:{&quot;surface_y&quot;:0.5,&quot;amplitude&quot;:0.3,&quot;frequency&quot;:2,&quot;decay&quot;:0.98}}"><div class="tikz-title">Solver Chattering at Switching Surface</div><div class="tikz-body tikz-sim-placeholder" style="min-height:450px"><span class="tikz-sim-loading">initializing...</span></div></div><h2 id="Interesting-Approach-Encode-the-Nonsmoothness"><a href="#Interesting-Approach-Encode-the-Nonsmoothness" class="headerlink" title="Interesting Approach: Encode the Nonsmoothness"></a>Interesting Approach: Encode the Nonsmoothness</h2><p>The cleaner solution, which solve_nivp implements, is to build the nonsmooth rules directly into the time-stepping scheme from the initial, rather than trying to smooth them away or detect them as events opposed to the ideal system.</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="The-Time-Stepping-Scheme"><a href="#The-Time-Stepping-Scheme" class="headerlink" title="The Time-Stepping Scheme"></a>The Time-Stepping Scheme</h1><h2 id="Implicit-Euler-as-the-Foundation"><a href="#Implicit-Euler-as-the-Foundation" class="headerlink" title="Implicit Euler as the Foundation"></a>Implicit Euler as the Foundation</h2><p>The <strong>implicit Euler</strong> method updates state via:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="27.526ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 12166.4 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2260.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="msub" transform="translate(3316.5,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mi" transform="translate(605,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mo" transform="translate(4618,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(5618.2,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(6194.2,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(6638.9,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(7188.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(7577.9,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(9560.8,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msub" transform="translate(10005.5,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="TeXAtom" transform="translate(394,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(11777.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></p><p>Unlike its explicit counterpart <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="23.437ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 10359.1 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2260.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="msub" transform="translate(3316.5,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mi" transform="translate(605,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mo" transform="translate(4618,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(5618.2,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(6194.2,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(6638.9,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(7188.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(7577.9,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mi" transform="translate(605,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mo" transform="translate(8657.1,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msub" transform="translate(9101.8,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mo" transform="translate(9970.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container>, this requires solving a (possibly nonlinear) system at each step. However the stability desired is derived fundamentally from it’s unconditional stableness for <em>stiff</em> problems.</p><h2 id="Augmenting"><a href="#Augmenting" class="headerlink" title="Augmenting"></a>Augmenting</h2><p>Now, suppose the system has a contact constraint: state <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.471ex;" xmlns="http://www.w3.org/2000/svg" width="4.486ex" height="1.471ex" role="img" focusable="false" viewBox="0 -442 1982.9 650"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></g></g></svg></mjx-container> must satisfy <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="11.474ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 5071.5 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(477,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(866,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2848.9,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(3515.7,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(4571.5,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container> (e.g., position above floor), and there exists a contact force <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.027ex;" xmlns="http://www.w3.org/2000/svg" width="1.319ex" height="1.597ex" role="img" focusable="false" viewBox="0 -694 583 706"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g></g></g></svg></mjx-container> that only acts when the constraint is active.</p><p>The complementarity condition encodes this:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="37.906ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 16754.3 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g><g data-mml-node="mo" transform="translate(860.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(1916.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(2416.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mstyle" transform="translate(2694.6,0)"><g data-mml-node="mspace"></g></g><g data-mml-node="mi" transform="translate(3861.2,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(4338.2,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(4727.2,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(6710.2,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(7376.9,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(8432.7,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(8932.7,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mstyle" transform="translate(9210.7,0)"><g data-mml-node="mspace"></g></g><g data-mml-node="mi" transform="translate(10377.4,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g><g data-mml-node="mo" transform="translate(11182.6,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="mi" transform="translate(11682.8,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(12159.8,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(12548.8,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(14531.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(15198.6,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(16254.3,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></p><p>The last conditions says that either the constraint is inactive i.e. <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="5.228ex" height="1.971ex" role="img" focusable="false" viewBox="0 -666 2310.6 871"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(754.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(1810.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container> and the force is zero, or the constraint is active i.e. <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="5.228ex" height="1.971ex" role="img" focusable="false" viewBox="0 -666 2310.6 871"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(754.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(1810.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container> and the force is whatever is needed to prevent penetration of system. These 3 conditions altogether form a <strong>Linear Complementarity Problem (LCP)</strong> when the dynamics are linear, or a <strong>Nonlinear Complementarity Problem (NCP)</strong> otherwise.</p><div class="tikz-container tikz-svg" data-tikz-mode="svg"><div class="tikz-title">Complementarity Condition</div><div class="tikz-body"><script type="text/tikz">\begin{tikzpicture}[scale=1.1]  \node[white, above, font=\footnotesize] at (1.2,3.8) {$q \geq 0$};  \draw[thick, red!40!white] (0,0) -- (2.4,0);  \fill[pattern=north east lines, pattern color=red!30!white] (0,-0.25) rectangle (2.4,0);  \fill[blue!50!white] (1.2,2.5) circle (0.3);  \draw[blue!30!white] (1.2,2.5) circle (0.3);  \draw[->, thick, white] (1.2,2.2) -- (1.2,1) node[midway, right, font=\footnotesize] {$g$};  \draw[dashed, blue!30!white] (0,0) -- (0,3.5);  \draw[dashed, blue!30!white] (2.4,0) -- (2.4,3.5);  \node[white, below, font=\footnotesize] at (1.2,-0.4) {position};  \node[white, above, font=\footnotesize] at (4.2,3.8) {$\lambda \geq 0$};  \draw[thick, red!40!white] (3,0) -- (5.4,0);  \fill[red!60!white] (3.8,0) rectangle (4.6,2);  \fill[red!40!white] (3.8,2) rectangle (4.6,2.5);  \fill[red!25!white] (3.8,2.5) rectangle (4.6,2.8);  \draw[dashed, blue!30!white] (3,0) -- (3,3.5);  \draw[dashed, blue!30!white] (5.4,0) -- (5.4,3.5);  \node[white, below, font=\footnotesize] at (4.2,-0.4) {force};  \node[white, above, font=\footnotesize] at (7.5,3.8) {$q \cdot \lambda = 0$};  \draw[dashed, blue!30!white] (6,0) -- (6,3.5);  \draw[dashed, blue!30!white] (9,0) -- (9,3.5);  \node[blue!60!white, font=\footnotesize] at (7.5,2.8) {if $q > 0$};  \node[white, font=\footnotesize] at (7.5,2.2) {then $\lambda = 0$};  \node[red!60!white, font=\footnotesize] at (7.5,1.4) {if $\lambda > 0$};  \node[white, font=\footnotesize] at (7.5,0.8) {then $q = 0$};  \node[white, below, font=\footnotesize] at (7.5,-0.4) {mutual excl.};  \node[blue!60!white, font=\normalsize] at (2.7,1.5) {$\perp$};\end{tikzpicture}</script></div></div><div class="tikz-container tikz-ascii" data-tikz-mode="ascii" data-tikzsim="{&quot;type&quot;:&quot;complementarity&quot;,&quot;height&quot;:450,&quot;color&quot;:[134,122,222],&quot;params&quot;:{&quot;gravity&quot;:0.08,&quot;restitution&quot;:0.65,&quot;speed&quot;:0.5}}"><div class="tikz-title">Complementarity Condition — Position vs Contact Force</div><div class="tikz-body tikz-sim-placeholder" style="min-height:450px"><span class="tikz-sim-loading">initializing...</span></div></div><p>The augmented implicit step at each time <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.357ex;" xmlns="http://www.w3.org/2000/svg" width="1.964ex" height="1.773ex" role="img" focusable="false" viewBox="0 -626 868.3 783.8"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="mi" transform="translate(394,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g></g></g></svg></mjx-container> is to find any given <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="8.572ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 3788.6 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(389,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2371.9,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(2816.6,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g><g data-mml-node="mo" transform="translate(3399.6,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container> satisfying</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="43.259ex" height="2.583ex" role="img" focusable="false" viewBox="0 -891.7 19120.3 1141.7"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2260.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="msub" transform="translate(3316.5,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mi" transform="translate(605,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mo" transform="translate(4618,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(5618.2,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(6194.2,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(6638.9,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z"></path></g><g data-mml-node="mo" transform="translate(7188.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(7577.9,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(9560.8,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msub" transform="translate(10005.5,0)"><g data-mml-node="mi"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z"></path></g><g data-mml-node="TeXAtom" transform="translate(394,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(11777.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(12388.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(13388.9,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z"></path></g><g data-mml-node="mo" transform="translate(13964.9,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mi" transform="translate(14409.5,0)"><path data-c="1D43A" d="M50 252Q50 367 117 473T286 641T490 704Q580 704 633 653Q642 643 648 636T656 626L657 623Q660 623 684 649Q691 655 699 663T715 679T725 690L740 705H746Q760 705 760 698Q760 694 728 561Q692 422 692 421Q690 416 687 415T669 413H653Q647 419 647 422Q647 423 648 429T650 449T651 481Q651 552 619 605T510 659Q492 659 471 656T418 643T357 615T294 567T236 496T189 394T158 260Q156 242 156 221Q156 173 170 136T206 79T256 45T308 28T353 24Q407 24 452 47T514 106Q517 114 529 161T541 214Q541 222 528 224T468 227H431Q425 233 425 235T427 254Q431 267 437 273H454Q494 271 594 271Q634 271 659 271T695 272T707 272Q721 272 721 263Q721 261 719 249Q714 230 709 228Q706 227 694 227Q674 227 653 224Q646 221 643 215T629 164Q620 131 614 108Q589 6 586 3Q584 1 581 1Q571 1 553 21T530 52Q530 53 528 52T522 47Q448 -22 322 -22Q201 -22 126 55T50 252Z"></path></g><g data-mml-node="mo" transform="translate(15195.5,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(15584.5,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="msup" transform="translate(17567.5,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mi" transform="translate(422,413) scale(0.707)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g></g><g data-mml-node="mi" transform="translate(18537.3,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g></g></g></svg></mjx-container><br><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="19.958ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 8821.6 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(777.8,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mi" transform="translate(1833.6,0)"><path data-c="1D706" d="M166 673Q166 685 183 694H202Q292 691 316 644Q322 629 373 486T474 207T524 67Q531 47 537 34T546 15T551 6T555 2T556 -2T550 -11H482Q457 3 450 18T399 152L354 277L340 262Q327 246 293 207T236 141Q211 112 174 69Q123 9 111 -1T83 -12Q47 -12 47 20Q47 37 61 52T199 187Q229 216 266 252T321 306L338 322Q338 323 288 462T234 612Q214 657 183 657Q166 657 166 673Z"></path></g><g data-mml-node="mo" transform="translate(2694.3,0)"><path data-c="22A5" d="M369 652Q369 653 370 655T372 658T375 662T379 665T384 667T391 668Q402 666 409 653V40H708Q723 32 723 20T708 0H71Q70 0 67 2T59 9T55 20T59 31T66 38T71 40H369V652Z"></path></g><g data-mml-node="mi" transform="translate(3750.1,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(4227.1,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(4616.1,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(6599.1,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(7265.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(8321.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></p><p>where <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="7.759ex" height="2.059ex" role="img" focusable="false" viewBox="0 -705 3429.6 910"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D43A" d="M50 252Q50 367 117 473T286 641T490 704Q580 704 633 653Q642 643 648 636T656 626L657 623Q660 623 684 649Q691 655 699 663T715 679T725 690L740 705H746Q760 705 760 698Q760 694 728 561Q692 422 692 421Q690 416 687 415T669 413H653Q647 419 647 422Q647 423 648 429T650 449T651 481Q651 552 619 605T510 659Q492 659 471 656T418 643T357 615T294 567T236 496T189 394T158 260Q156 242 156 221Q156 173 170 136T206 79T256 45T308 28T353 24Q407 24 452 47T514 106Q517 114 529 161T541 214Q541 222 528 224T468 227H431Q425 233 425 235T427 254Q431 267 437 273H454Q494 271 594 271Q634 271 659 271T695 272T707 272Q721 272 721 263Q721 261 719 249Q714 230 709 228Q706 227 694 227Q674 227 653 224Q646 221 643 215T629 164Q620 131 614 108Q589 6 586 3Q584 1 581 1Q571 1 553 21T530 52Q530 53 528 52T522 47Q448 -22 322 -22Q201 -22 126 55T50 252Z"></path></g><g data-mml-node="mo" transform="translate(1063.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(2119.6,0)"><path data-c="2207" d="M46 676Q46 679 51 683H781Q786 679 786 676Q786 674 617 326T444 -26Q439 -33 416 -33T388 -26Q385 -22 216 326T46 676ZM697 596Q697 597 445 597T193 596Q195 591 319 336T445 80L697 596Z"></path></g><g data-mml-node="mi" transform="translate(2952.6,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g></g></g></svg></mjx-container> is the constraint Jacobian. This is a <strong>Mixed Complementarity Problem (MCP)</strong>, and this is what solve_nivp solves at each step instead of tradition.</p><h2 id="Handling-Impacts"><a href="#Handling-Impacts" class="headerlink" title="Handling Impacts"></a>Handling Impacts</h2><p>For our previous bouncing ball from <a href="#impacts-switching-constraints-in-nonsmooth-systems">earlier</a>, the impact law is applied exactly at the transition step. When the solver detects that the constraint surface is crossed (i.e. <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="11.474ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 5071.5 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z"></path></g><g data-mml-node="mo" transform="translate(477,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msub" transform="translate(866,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="TeXAtom" transform="translate(605,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2848.9,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(3515.7,0)"><path data-c="3C" d="M694 -11T694 -19T688 -33T678 -40Q671 -40 524 29T234 166L90 235Q83 240 83 250Q83 261 91 266Q664 540 678 540Q681 540 687 534T694 519T687 505Q686 504 417 376L151 250L417 124Q686 -4 687 -5Q694 -11 694 -19Z"></path></g><g data-mml-node="mn" transform="translate(4571.5,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container> in the unconstrained prediction), it applies the <strong>restitution law</strong>:</p><p><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="27.387ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 12105.2 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="TeXAtom" transform="translate(518,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mi"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(600,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1378,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g><g data-mml-node="mo" transform="translate(2173.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mo" transform="translate(3229.5,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mi" transform="translate(4007.5,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"></path></g><g data-mml-node="mo" transform="translate(4695.7,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="msub" transform="translate(5195.9,0)"><g data-mml-node="mi"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path></g><g data-mml-node="mi" transform="translate(518,-150) scale(0.707)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></g></g><g data-mml-node="mstyle" transform="translate(6188.2,0)"><g data-mml-node="mspace"></g></g><g data-mml-node="mtext" transform="translate(7188.2,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(389,0)"></path><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(889,0)"></path><path data-c="20" d="" transform="translate(1278,0)"></path><path data-c="69" d="M69 609Q69 637 87 653T131 669Q154 667 171 652T188 609Q188 579 171 564T129 549Q104 549 87 564T69 609ZM247 0Q232 3 143 3Q132 3 106 3T56 1L34 0H26V46H42Q70 46 91 49Q100 53 102 60T104 102V205V293Q104 345 102 359T88 378Q74 385 41 385H30V408Q30 431 32 431L42 432Q52 433 70 434T106 436Q123 437 142 438T171 441T182 442H185V62Q190 52 197 50T232 46H255V0H247Z" transform="translate(1528,0)"></path><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z" transform="translate(1806,0)"></path><path data-c="70" d="M36 -148H50Q89 -148 97 -134V-126Q97 -119 97 -107T97 -77T98 -38T98 6T98 55T98 106Q98 140 98 177T98 243T98 296T97 335T97 351Q94 370 83 376T38 385H20V408Q20 431 22 431L32 432Q42 433 61 434T98 436Q115 437 135 438T165 441T176 442H179V416L180 390L188 397Q247 441 326 441Q407 441 464 377T522 216Q522 115 457 52T310 -11Q242 -11 190 33L182 40V-45V-101Q182 -128 184 -134T195 -145Q216 -148 244 -148H260V-194H252L228 -193Q205 -192 178 -192T140 -191Q37 -191 28 -194H20V-148H36ZM424 218Q424 292 390 347T305 402Q234 402 182 337V98Q222 26 294 26Q345 26 384 80T424 218Z" transform="translate(2639,0)"></path><path data-c="61" d="M137 305T115 305T78 320T63 359Q63 394 97 421T218 448Q291 448 336 416T396 340Q401 326 401 309T402 194V124Q402 76 407 58T428 40Q443 40 448 56T453 109V145H493V106Q492 66 490 59Q481 29 455 12T400 -6T353 12T329 54V58L327 55Q325 52 322 49T314 40T302 29T287 17T269 6T247 -2T221 -8T190 -11Q130 -11 82 20T34 107Q34 128 41 147T68 188T116 225T194 253T304 268H318V290Q318 324 312 340Q290 411 215 411Q197 411 181 410T156 406T148 403Q170 388 170 359Q170 334 154 320ZM126 106Q126 75 150 51T209 26Q247 26 276 49T315 109Q317 116 318 175Q318 233 317 233Q309 233 296 232T251 223T193 203T147 166T126 106Z" transform="translate(3195,0)"></path><path data-c="63" d="M370 305T349 305T313 320T297 358Q297 381 312 396Q317 401 317 402T307 404Q281 408 258 408Q209 408 178 376Q131 329 131 219Q131 137 162 90Q203 29 272 29Q313 29 338 55T374 117Q376 125 379 127T395 129H409Q415 123 415 120Q415 116 411 104T395 71T366 33T318 2T249 -11Q163 -11 99 53T34 214Q34 318 99 383T250 448T370 421T404 357Q404 334 387 320Z" transform="translate(3695,0)"></path><path data-c="74" d="M27 422Q80 426 109 478T141 600V615H181V431H316V385H181V241Q182 116 182 100T189 68Q203 29 238 29Q282 29 292 100Q293 108 293 146V181H333V146V134Q333 57 291 17Q264 -10 221 -10Q187 -10 162 2T124 33T105 68T98 100Q97 107 97 248V385H18V422H27Z" transform="translate(4139,0)"></path><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z" transform="translate(4528,0)"></path></g></g></g></svg></mjx-container></p><p>and then continues from the new post-impact state. There is surprisingly no <em>extra-smoothing</em> present here! =D</p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="What-solve-nivp-Provides"><a href="#What-solve-nivp-Provides" class="headerlink" title="What solve_nivp Provides"></a>What solve_nivp Provides</h1><h2 id="The-API"><a href="#The-API" class="headerlink" title="The API"></a>The API</h2><p>The library’s entry point is <code>solve_ivp_ns</code>. One specifies a given constraint type via a <code>projection</code> argument (e.g. <code>'unilateral'</code> for a one-sided contact, <code>'identity'</code> for unconstrained), and choose a nonlinear solver (<code>'VI'</code> for variational-inequality fixed-point, or <code>'semismooth_newton'</code>). Our lovely bouncing ball now looks like this:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="keyword">import</span> solve_nivp</span><br><span class="line"></span><br><span class="line">t, y, h, fk, info = solve_nivp.solve_ivp_ns(</span><br><span class="line">    fun=rhs,                          <span class="comment"># right-hand side f(t, x)</span></span><br><span class="line">    t_span=(<span class="number">0.0</span>, <span class="number">5.0</span>),</span><br><span class="line">    y0=np.array([<span class="number">0.0</span>, <span class="number">1.0</span>]),          <span class="comment"># [velocity, height]</span></span><br><span class="line">    method=<span class="string">'trapezoidal'</span>,</span><br><span class="line">    projection=<span class="string">'unilateral'</span>,          <span class="comment"># encodes q &gt;= 0 contact constraint</span></span><br><span class="line">    solver=<span class="string">'VI'</span>,</span><br><span class="line">    adaptive=<span class="literal">True</span>,</span><br><span class="line">    atol=<span class="number">1e-6</span>,</span><br><span class="line">    rtol=<span class="number">1e-3</span>,</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>The function returns a 5-tuple: <code>(t, y, h, fk, info)</code>; that being: time array, state trajectory, step sizes, constraint forces, and a solver info dictionary respectively. The solver is then setting up and solving the complementarity system with the given arguments at each step.</p><h2 id="Which-Systems"><a href="#Which-Systems" class="headerlink" title="Which Systems?"></a>Which Systems?</h2><p>solve_nivp covers three main categories including the ones mentioned earlier to this:</p><ul><li><strong>Unilateral constraints with impacts</strong>, the bouncing ball, rigid-body contact, granular media</li><li><strong>Switching systems (Filippov)</strong>, sliding-mode controllers, relay circuits, piecewise-linear dynamics</li><li><strong>DAEs with inequality constraints</strong>, mechanical systems with joints that can separate or lock</li></ul><p>For each category there are worked examples in the repository<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[solve_nivp](https://github.com/ERC-INJECT/solve_nivp) available on GitHub">[2]</span></a></sup>.</p><div class="tikz-container tikz-svg" data-tikz-mode="svg"><div class="tikz-title">Relay Switching — Filippov Dynamics</div><div class="tikz-body"><script type="text/tikz">\begin{tikzpicture}[scale=0.9, every node/.style={font=\footnotesize}]  \draw[thick, red!40!white, dashed] (-0.5,0) -- (7,0) node[right] {$\theta$};  \draw[thick, red!60!white]    (0,0) -- (0.39,0.57) -- (0.79,1.06) -- (1.18,1.38)    -- (1.57,1.5) -- (1.96,1.38) -- (2.36,1.06) -- (2.75,0.57)    -- (3.14,0) -- (3.53,-0.57) -- (3.93,-1.06) -- (4.32,-1.38)    -- (4.71,-1.5) -- (5.1,-1.38) -- (5.50,-1.06) -- (5.89,-0.57)    -- (6.28,0);  \node[red!60!white, above right] at (1.2,1.5) {input};  \draw[very thick, blue!60!white]    (0,3.5) -- (3.14,3.5) -- (3.14,2) -- (6.28,2) -- (6.28,3.5);  \node[blue!60!white, right] at (6.5,3.5) {$+1$};  \node[blue!60!white, right] at (6.5,2) {$-1$};  \node[blue!60!white, above] at (3,3.7) {output};  \draw[->, white] (-0.3,-2) -- (-0.3,4.2);  \draw[->, white] (-0.5,-2) -- (7,-2) node[right] {$t$};  \node[white, right] at (7.5,3) {Filippov:};  \node[white, right] at (7.5,2.2) {at $\theta$, velocity};  \node[white, right] at (7.5,1.4) {$\in [-1, +1]$};  \node[white, right] at (7.5,0.6) {(set-valued)};\end{tikzpicture}</script></div></div><div class="tikz-container tikz-ascii" data-tikz-mode="ascii" data-tikzsim="{&quot;type&quot;:&quot;relay&quot;,&quot;height&quot;:550,&quot;color&quot;:[192,129,121],&quot;params&quot;:{&quot;threshold&quot;:0,&quot;frequency&quot;:0.3,&quot;on_char&quot;:&quot;+&quot;,&quot;off_char&quot;:&quot;-&quot;}}"><div class="tikz-title">Relay Circuit — Filippov Switching</div><div class="tikz-body tikz-sim-placeholder" style="min-height:550px"><span class="tikz-sim-loading">initializing...</span></div></div><h2 id="Documentation-and-Tests"><a href="#Documentation-and-Tests" class="headerlink" title="Documentation and Tests"></a>Documentation and Tests</h2><p>The solve_nivp package is clearly shipped with the following on their github releases:</p><ul><li>A full Sphinx documentation site with mathematical background</li><li>Jupyter notebook tutorials for each system class</li><li>A test suite covering the main integrator cases and edge conditions (zero-velocity impact, constraint grazing, etc.</li></ul><div class="page-sep" data-chars="000://0" aria-hidden="true"></div><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Nonsmooth systems are not <em>edge cases</em> in engineering, they are everywhere and they must be responded to and considered for. Any system with contact, switching, or hard constraints is <strong>nonsmooth</strong>, and the classical ODE toolkit cannot handle them out-of-the-box. solve_nivp brings the complementarity approach to python with a clean API and proper documentation.</p><p>What stands out most to me is that most numerical software in this space is written as Fortran-wrapped Matlab, roughly readable by no one (including me!). This was rather refreshing python.</p><p>Thank you to <strong>David Riley</strong> and <strong>Ioannis Stefanou</strong> for the work on a genuinely useful package, and to <strong><a href="https://danielskatz.org/">Daniel S. Katz</a></strong> for editing the submission and inviting me.</p><p>The published article: <strong><a href="https://doi.org/10.21105/joss.09775">DOI: 10.21105/joss.09775</a></strong></p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html">solve_ivp package</a><a href="#fnref:1" rev="footnote"> ↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://github.com/ERC-INJECT/solve_nivp">solve_nivp</a> available on GitHub<a href="#fnref:2" rev="footnote"> ↩</a></span></li></ol></div></div>]]>
    </content>
    <id>http://blog.trintler.me/2026/03/24/JOSS-Reviewed-solve-nivp/</id>
    <link href="http://blog.trintler.me/2026/03/24/JOSS-Reviewed-solve-nivp/"/>
    <published>2026-03-24T18:29:20.000Z</published>
    <summary>
      <![CDATA[<b>solve_nivp</b> is a Python library for time-stepping nonsmooth ODE and DAE systems. This post explains what that means, why classical solvers fail at discontinuities, and how the library encodes impact laws and conditions directly into an implicit integrator, reviewed for the <a href="https://doi.org/10.21105/joss.09775">Journal of Open Source Software</a>.]]>
    </summary>
    <title>JOSS: Reviewed solve_nivp</title>
    <updated>2026-05-17T15:00:11.995Z</updated>
  </entry>
  <entry>
    <author>
      <name>Niladri Adhikary</name>
    </author>
    <category term="Events" scheme="http://blog.trintler.me/categories/Events/"/>
    <category term="icpc" scheme="http://blog.trintler.me/tags/icpc/"/>
    <category term="nwerc" scheme="http://blog.trintler.me/tags/nwerc/"/>
    <category term="bapc" scheme="http://blog.trintler.me/tags/bapc/"/>
    <category term="tapc" scheme="http://blog.trintler.me/tags/tapc/"/>
    <category term="competitive programming" scheme="http://blog.trintler.me/tags/competitive-programming/"/>
    <content>
      <![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><h1 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h1><h2 id="ICPC"><a href="#ICPC" class="headerlink" title="ICPC"></a>ICPC</h2><p>The International Collegiate Programming Contest is an incredibly renowned competitive programming competition with various stages, in this document I walk through my experience this 2026 ICPC in my three regional contests.</p><div class="page-sep" data-chars="van://-" aria-hidden="true"></div><h2 id="Team-Found"><a href="#Team-Found" class="headerlink" title="Team Found"></a>Team Found</h2><p><strong>Valyn</strong>, <strong>Anton</strong> and <strong>I</strong> formed the sudden team <strong>“(() &#x3D;&gt; {})();”</strong> – which indeed is meant to represent the no-ops lambda function in javascript or “arrow-function” – thought of by our one and only Valyn on the first day (the initial orchestrator.)</p><p><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/team_encoded.png" alt="TEAM"></p><span id="more"></span><h1 id="Contests"><a href="#Contests" class="headerlink" title="Contests"></a>Contests</h1><h2 id="TAPC-UTwente-Enschede-NL"><a href="#TAPC-UTwente-Enschede-NL" class="headerlink" title="TAPC, UTwente, Enschede, NL"></a>TAPC, UTwente, Enschede, NL</h2><p>Initially, we participated in the <strong><a href="https://tapc.ia.utwente.nl">Twents Algorithms Programming Contest (TAPC)</a></strong> at my university: <em><a href="https://utwente.nl">University of Twente</a></em> where we had a lot of fun, with a lax competition unexpectant of the continutation of this.<br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/tapc-encode.png" alt="TAPC"></p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h2 id="BAPC-TUD-Delft-NL"><a href="#BAPC-TUD-Delft-NL" class="headerlink" title="BAPC, TUD, Delft, NL"></a>BAPC, TUD, Delft, NL</h2><h3 id="Intro-1"><a href="#Intro-1" class="headerlink" title="Intro"></a>Intro</h3><p>Subsequently, the next month as time passed by almost immediately, we qualified for the <strong><a href="https://2025.bapc.eu/">Benelux Algorithms Programming Contest (BAPC)</strong></a> being held at <em><a href="https://tudelft.nl">Technical University Delft</em></a>. This was our first ever competition which was at such a scale and our nerves were racked to the computers (though there was only one.) Without a coach, this all was extremely difficult; however we persisted, formed a more unified Team Reference Document (TCR) which was made in the times when my proficiency with LaTeX wasn’t present (seen by the un-printed {} in the team name<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="![Silly escape characters.](no-arrowfunc.png)">[1]</span></a></sup>), and nor was my knowledge in this field worth much (also, I bought a <strong>HUGE</strong> QWERTY-US keyboard shown below to practice with (I usually use QWERTZ, but everyone else uses QWERTY-US.)). As you can imagine, our priorities were more present in complaining about our short stay in Rijswijk instead of Delft to cut costs (there was a beautiful arabic restaurant, though) (we almost slept with a 40-year-old in the same dorm as-well &#x3D;)).</p><p>The aforementioned “HUGE” Keyboard:<br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/big_keyboard_encoded.png" alt="The QWERTY-US kb of doom (why did i filter this?)"></p><h3 id="Result"><a href="#Result" class="headerlink" title="Result"></a>Result</h3><p>After the contest, our resolutions were moderate, wishing we could have performed better. However, we all saw we did quite a lot so far and were satisfied with the performance amidst the competiton with good solutions and program code.</p><div class="page-sep" data-chars="---://a" aria-hidden="true"></div><h2 id="NWERC-KIT-Karlsruhe-Germany"><a href="#NWERC-KIT-Karlsruhe-Germany" class="headerlink" title="NWERC, KIT, Karlsruhe, Germany"></a>NWERC, KIT, Karlsruhe, Germany</h2><h3 id="Intro-2"><a href="#Intro-2" class="headerlink" title="Intro"></a>Intro</h3><p>To plan for future competitions far ahead, we decided to string along a Year 2 senior, <strong>Adrian</strong> in our University to be our coach for the duration of the semester. Although, as it were looking like there was nothing more till next year: We get accepted to <strong><a href="https://2025.nwerc.eu">Northwestern Europe Regional Contest (NWERC)</a>!</strong> This was exciting, finally we get to make another chance.</p><h3 id="Concerns"><a href="#Concerns" class="headerlink" title="Concerns"></a>Concerns</h3><p>There were a few admistrative, and logistical costs to take us in the Dutch university to <em><a href="https://kit.edu">Karlsruhe Institute of Technology (KIT)</a></em>; incuring MIN ca. €250 &#x2F; person, and for this reason we have started works of our own study association described further. Leaving this aside, the trip to germany was fun, tiring and exhausting, sure, but fun.I think that was one of the most enjoyable FlixBus rides I have ever had since we were more focused on flipping a Swapfiets bicycle upside down (contemporary Dutch culture which might be illegal and hence filtered (see below)), speaking till 4:&#x2F;&#x2F; am when there was work to do the next day and coding our beautiful team website <strong><a href="https://arrowfunc.now">arrowfunc.now</a></strong> made by Valyn on that very day. We then register at the new accomodation, sleep in, and get ready for the next day, where we mostly just take EVERY goodie IMAGINABLE from the sponsors. I think there is a very really stress-release behind this. I recommend everyone to take as many goodies as they can from an event, and goodness did we score, I believe the valuation of the goods were approximately €150. I think that is apt reimbursement. Don’t believe me? Check the picture<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="![The forbidden stash.](things-gotten.jpeg)">[2]</span></a></sup>. Those little jetbrains pins alone on the bottom right cost ~€4 each (see under reference) (and these were what I got, my teammates got similar returns.)</p><p>The aforementioned upside down bike,<br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/swapfiets.png" alt="Swapfiet bike scandal"></p><h3 id="Result-1"><a href="#Result-1" class="headerlink" title="Result"></a>Result</h3><p>After thinking of who to gift these goodies to (families and friends) it quickly took time to do the real contest, we improved the TCR which helped, it was fun, exciting, and most of all tough. We hadn’t solved as many problems as we’d hoped due to ill health. However, we still put in the effort, Anton did an extremely impressive job, we all came up with interesting solutions and above all it was a lovely experience – and I would always do it again. I now do competitive programming problems for fun, and in my spare time, and I recommend you too! to me, it feels like playing chess. I love this stuff. The train back to Enschede while being in Dortmund Station at midnight sucked, but that’s how life is sometimes.</p><p>This is us in the hall:<br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/in_hall.png" alt="NWERC Hall photo, KIT, Karlsruhe"></p><div class="page-sep" data-chars="---://-" aria-hidden="true"></div><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>I am incredibly grateful to my <em>team, coach and university</em>, since without them I wouldnt have done nearly as much, and overall this was one of the most thrilling experiences I have had, along with now fostering the ideas of creating my own study assocation for the betterment of the competitive programming in my university and financial&#x2F;mental compensation, and for the creation of the social groups for it.</p><p>I have also in addition am grateful to have visited the beautiful cities Delft and Karlsruhe from these events, and the people I have met here.</p><p>A small landscape gallery:<br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/karlsruhe-ferris.jpeg" alt="Ferris wheel, Christmas market, Karlsruhe"><br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/christmas-encode.png" alt="Christmas Trees"><br><img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/embelic.png" alt="Embelic"></p><div class="page-sep" data-chars="000://0" aria-hidden="true"></div><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><img src="no-arrowfunc.png" alt="Silly escape characters."><a href="#fnref:1" rev="footnote"> ↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><img src="things-gotten.jpeg" alt="The forbidden stash."><a href="#fnref:2" rev="footnote"> ↩</a></span></li><li id="fn:3"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">3.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;">View the <a href="https://www.jetbrainsmerchandise.com/accessories.html?ba_product_type=56">Disatrous prices of Jetbrains Pins</a>, the price as of 11/2025:<img src="/2026/03/24/2025-2026-ICPC-Regional-Performance/pin_price.png" class="" title="&quot;Price at the time&quot;"><a href="#fnref:3" rev="footnote"> ↩</a></span></li></ol></div></div>]]>
    </content>
    <id>http://blog.trintler.me/2026/03/24/2025-2026-ICPC-Regional-Performance/</id>
    <link href="http://blog.trintler.me/2026/03/24/2025-2026-ICPC-Regional-Performance/"/>
    <published>2026-03-24T01:37:22.000Z</published>
    <summary>Having participated in the International Collegiate Programming Contest (ICPC) Regionals, I have gained a deep newfound passion for &quot;Competitive Programming.&quot;</summary>
    <title>2025/2026 ICPC Regional Performance</title>
    <updated>2026-04-28T11:35:01.399Z</updated>
  </entry>
</feed>
