<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dougfenstermacher.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dougfenstermacher.com/" rel="alternate" type="text/html" /><updated>2026-08-14T04:07:09+00:00</updated><id>https://dougfenstermacher.com/feed.xml</id><title type="html">Doug Fenstermacher</title><subtitle>Doug Fenstermacher&apos;s personal website</subtitle><author><name>Doug Fenstermacher &lt;douglas.fenstermacher@gmail.com&gt;</name></author><entry><title type="html">Epochron Multi-Stopwatch</title><link href="https://dougfenstermacher.com/project/epochron-stopwatch" rel="alternate" type="text/html" title="Epochron Multi-Stopwatch" /><published>2026-08-11T00:00:00+00:00</published><updated>2026-08-11T00:00:00+00:00</updated><id>https://dougfenstermacher.com/project/epochron-stopwatch</id><content type="html" xml:base="https://dougfenstermacher.com/project/epochron-stopwatch"><![CDATA[<p>A recurring theme for personal projects is stuff that’s distance running / track &amp; field projects due to my past as a runner.  When combined with my preference for grassroots projects, I tend to try to find gaps in what can be done technically and what is already being done.  In the distance running world, that gap seems to be wide, (for reasons I will leave to other posts).</p>

<p>This project  was originally self-dogfooding.  I went to a track meet to watch my old college team compete.  I tried to track splits during races on my phone, but had trouble focusing on the race while also keeping splits, particularly when multiple competitors were involved. This seemed like a tractable problem, so I built a first version of a multi-stopwatch over the course of a couple weeks during the evening.  It wasn’t perfect, but it made managing multiple stopwatches much easier than the other apps available at the time.</p>

<p>I kept idly coming up with feature my ideal stopwatch would have, but I realized I had made some fundamental mistakes in my haste to get a working version that would fundamentally prevent me from implementing them.  I eventually decided to start over to build a better version to resolve those issues so I could add more meaningful features.</p>

<h3 id="what-epochron-is" class="linked-heading">
    What Epochron Is<a href="#what-epochron-is">🔗</a>
</h3>

<p>Epochron runs as many stopwatches as you need, at once, in a browser.  Each one can be named, assigned to a group, and record splits and laps against distances.  A running timer can be copied to start tracking someone else mid-race, which is the situation that started this whole thing.  Sessions can be archived and searched later.</p>

<p>It installs as a <abbr title="Progressive Web App">PWA</abbr>, so it can live on a phone’s home screen and keeps working without a signal, which matters because cell service at many cross country/track meets is reliably terrible.  Everything stays on the device: so no data leaves the browser.  That’s partly a privacy stance and partly a practical one, since a tool that needs a server is a tool that fails in the field.</p>

<p>It’s built in Angular v22 and TypeScript, free, and the source is <a href="https://github.com/dpfens/stopwatch">on GitHub</a>.  It can be <a href="https://stopwatch.dougfenstermacher.com/">used</a> without sign-ups or downloads.</p>

<p><img src="/assets/img/epochron-group.png" alt="The main stopwatch view, running several timers at once" class="w-100" /></p>

<h3 id="stopwatch-structure" class="linked-heading">
    Stopwatch Structure<a href="#stopwatch-structure">🔗</a>
</h3>

<p>A stopwatch is fundamentally just a set of labelled timestamps, starting with a <code class="language-plaintext highlighter-rouge">start</code> timestamp, and ending with a <code class="language-plaintext highlighter-rouge">stop</code> timestamp, with <code class="language-plaintext highlighter-rouge">split</code> and <code class="language-plaintext highlighter-rouge">lap</code> timestamps in between.  This is fundamentally how this stopwatch is structured: as a</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">UnitValue</span> <span class="p">{</span>
    <span class="nl">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
    <span class="nl">unit</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">type</span> <span class="nx">OperationalStopWatchEventType</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">start</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">stop</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">resume</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">UserOperationalStopwatchEventType</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">user_start</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">user_stop</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">user_resume</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">PerformanceMonitoringStopWatchEventType</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">split</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">lap</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">interval</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">StopWatchEventType</span> <span class="o">=</span> <span class="nx">OperationalStopWatchEventType</span> <span class="o">|</span> <span class="nx">UserOperationalStopwatchEventType</span> <span class="o">|</span> <span class="nx">PerformanceMonitoringStopWatchEventType</span><span class="p">;</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">BaseStopwatchEvent</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">StopWatchEventType</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="na">metadata</span><span class="p">:</span> <span class="nx">CreationModificationDates</span><span class="p">;</span>
    <span class="nl">type</span><span class="p">:</span> <span class="nx">T</span><span class="p">;</span>
    <span class="nl">timestamp</span><span class="p">:</span> <span class="nb">Date</span><span class="p">;</span>
    <span class="nl">unit</span><span class="p">?:</span> <span class="nx">UnitValue</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">type</span> <span class="nx">StartEvent</span> <span class="o">=</span> <span class="nx">BaseStopwatchEvent</span><span class="o">&lt;</span><span class="dl">'</span><span class="s1">start</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">StopEvent</span> <span class="o">=</span> <span class="nx">BaseStopwatchEvent</span><span class="o">&lt;</span><span class="dl">'</span><span class="s1">stop</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">SplitEvent</span> <span class="o">=</span> <span class="nx">BaseStopwatchEvent</span><span class="o">&lt;</span><span class="dl">'</span><span class="s1">split</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">LapEvent</span> <span class="o">=</span> <span class="nx">BaseStopwatchEvent</span><span class="o">&lt;</span><span class="dl">'</span><span class="s1">lap</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">StopwatchState</span> <span class="p">{</span>
    <span class="nl">sequence</span><span class="p">:</span> <span class="nx">BaseStopwatchEvent</span><span class="p">[];</span>
    <span class="nl">lap</span><span class="p">:</span> <span class="nx">UnitValue</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By only storing timestamps, we can</p>
<ul>
  <li>Dynamically derive our stopwatch view at runtime</li>
  <li>Simple adding/removing splits and modifying splits/laps (if necessary), as the corresponding changes to the stopwatch are automatically derived.</li>
  <li>Add more stopwatch event types as needed, which is a core aspect of adding more features</li>
</ul>

<h3 id="groups" class="linked-heading">
    Groups<a href="#groups">🔗</a>
</h3>

<p>In the original version of the stopwatch web app, users could only select multiple stopwatches temporarily to either start/stop them all at once or to add a split/lap.  That functionality is preserved but also extends it to allow users to assign stopwatches to groups.  This lets users only view/use specific stopwatches at a given time.  More importantly, it allows users to  designate the relationship between specific stopwatches, which I break into <code class="language-plaintext highlighter-rouge">GroupTimingBehavior</code> and <code class="language-plaintext highlighter-rouge">GroupEvaluationBehavior</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export type GroupTimingBehavior = 
    | 'parallel'        // Stopwatches run simultaneously (e.g., team members working on the same task)
    | 'sequential'      // Stopwatches run in a defined order (e.g., relay race or assembly line)
    | 'independent'     // No timing constraints
    | 'synchronized'    // Start/stop together
    | 'overlapping';    // Partial temporal overlap (e.g., shift handoffs)

export type GroupEvaluationBehavior =
    | 'independent'     // No relation of evaluation
    | 'comparative'     // Ranked/compared against each other
    | 'cumulative'      // Summed for totals
    | 'threshold'       // Measured against targets/SLAs
    | 'proportional'    // Analyzed as percentages of whole
    | 'trending';       // Tracked for patterns over time

export interface GroupTraits {
    timing: GroupTimingBehavior;
    evaluation: GroupEvaluationBehavior[];
}
</code></pre></div></div>

<p>By default, a group is assigned <code class="language-plaintext highlighter-rouge">independent</code> timing behavior, and <code class="language-plaintext highlighter-rouge">independent</code> evaluation, meaning they are only in the group for convenience and will not be evaluated against each other.  Users can assign a different timing behavior, and assign one-or-more evaluation behaviors.</p>

<h4 id="timing-behavior" class="linked-heading">
    Timing Behavior<a href="#timing-behavior">🔗</a>
</h4>
<p>These represent how the user intends on using the stopwatches in a group.  For example, if they are tracking concurrent race between individuals (<code class="language-plaintext highlighter-rouge">individual</code> or <code class="language-plaintext highlighter-rouge">parallel</code>), or are they tracking the members of a relay (<code class="language-plaintext highlighter-rouge">sequential</code>).</p>

<ul>
  <li>Sequential:  Useful for when stopwatch need to run in a defined order, like in a relay race or in an assembly line.  This add a single start/stop button to the group, where when the start button starts the first stopwatch, and when the stop button is clicked, the next stopwatch is automatically started.</li>
  <li>Parallel:  Useful for when the stopwatch are for the same concurrent task.</li>
  <li>Synchronized:  Useful for when stopwatches must start and/or stop together. This adds a button to the UI to allow easy start/stop of all stopwatches in the group.</li>
</ul>

<p>It is worth noting that timing behavior can be changed at any point, so a user can change them based on their given momentary task.</p>

<h3 id="evaluation-behavior" class="linked-heading">
    Evaluation Behavior<a href="#evaluation-behavior">🔗</a>
</h3>
<p>These represent how the user intends on evaluating the stopwatches in a given group.  In order for these evaluation behaviors to be useful, the user must be setting laps, and/or adding distances to the splits they record.</p>

<ul>
  <li>Comparative: Stopwatches in the group are to be compared against one another.  This results in rankings being shown at the group-level, with relative times between them</li>
  <li>Cumululative: The aggregated elapsed stopwatch time of all the stopwatches is meaningful.  The group view will show the aggregated stopwatch time.</li>
  <li>Proportional: Shows stopwatch times at the group-level as percentages of the aggregate elapsed times.</li>
  <li>Trending:  The viewer will be evaluating the stopwatches based on their trends while running.  This will result in the group view identifying and displaying trends identified per-stopwatch while they are running.</li>
</ul>

<p>As with timing behaviors, evaluation behaviors can be changed at any point, so a user can change them based on their given momentary task.</p>

<h3 id="time-split-prediction" class="linked-heading">
    Time / Split Prediction<a href="#time-split-prediction">🔗</a>
</h3>

<p>During a race, coaches and enthusiasts are often projecting in their minds whether someone is on-pace or off-pace, or their projected time at given distances.  I updated the multi-stopwatch to do split prediction based on previous lap and splits.  It takes a rigorous approach.  There were two pieces necessary to provide this functionality automatically:</p>

<ul>
  <li>Time prediction: Can we make confident predictions?</li>
  <li>Split prediction:  If we can make confident prediction predictions, which distances/splits should we predict?</li>
</ul>

<p>Rather than coming up with a unified formula for predicting each, I implemented a variety of approaches for each problem.</p>

<h4 id="time-prediction" class="linked-heading">
    Time prediction<a href="#time-prediction">🔗</a>
</h4>

<p>All five approaches share the same shape.  They work on segments rather than cumulative splits, since the fifth 400 tells you more about the sixth than the total time through 2000m does.  Each fits some model of pace to those segments, projects it forward a segment at a time until it reaches the target distance, and accumulates the variance of each projected segment as it goes.  The output is never a bare number: it’s a point estimate paired with a confidence interval, which is what makes the first question answerable at all.  The same machinery runs in reverse, so each can also answer where a runner will be at a given time rather than when they’ll reach a given distance.</p>

<p>What separates them is the assumption each makes about how pace changes over a race, and how much data that assumption costs.</p>

<ul>
  <li>The Kalman filter assumes a true pace exists underneath the hand-timing noise, and also needs 2 splits.</li>
  <li>The regressions assume a linear trend and want 3.</li>
  <li>Generalized progression needs 4 splits before it will try to distinguish one pattern from another.</li>
</ul>

<p>Those minimums are the practical answer to whether we can predict confidently: rather than one global threshold, each approach stays quiet until it has enough to work with, and falls back to something simpler when its own assumptions break down mid-race.</p>

<ul>
  <li>Exponentially Weighted Moving Average (EWMA)
    <ul>
      <li>Assumption: assumes nothing beyond “recent is more relevant”</li>
      <li>Minimum splits: 2</li>
      <li><a href="https://otexts.com/fpp3/ses.html">Simple exponential smoothing</a> is applied to pace, which is a weighted average of segment paces with no trend term at all, where every prior split still counts but geometrically less.  It makes the fewest assumptions of the currrent approaches and will lag a runner who is steadily changing pace, which is exactly the trade for being the one approach that works on almost no data.</li>
    </ul>
  </li>
  <li><a href="https://www.cs.unc.edu/~welch/media/pdf/kalman_intro.pdf">Kalman Filter</a>
    <ul>
      <li>Assumption: assumes a true pace exists underneath the hand-timing noise</li>
      <li>Minimum splits: 2</li>
      <li>Models a latent “true pace” that evolves with process noise, observed with measurement noise.  Hand timing is noisy in a way the other approaches treat as signal, and separating the two is the whole point.  It can optionally carry a drift term to model systematic pace change.  Process and measurement noise are estimated from the splits themselves rather than known ahead of time, so the estimates are principled but not optimal in the strict sense.</li>
    </ul>
  </li>
  <li>Regressions:
Assume a linear trend.
Minimum splits: 3
    <ul>
      <li><a href="https://itl.nist.gov/div898/handbook/pmd/section1/pmd141.htm">Ordinary Least Squares (OLS) Linear Regression</a>:  Fits pace as <code class="language-plaintext highlighter-rouge">ax + b</code>, choosing the coefficients that minimize squared error across the observed segments, then integrates the fitted pace forward.  <code class="language-plaintext highlighter-rouge">x</code> is the segment number by default, but can instead be cumulative distance or cumulative time, which separates distance-related fatigue from time-related fatigue.  When no trend exists the slope goes to zero and the prediction collapses to the mean pace.</li>
      <li><a href="https://itl.nist.gov/div898/handbook/pmd/section1/pmd143.htm">Weighted Least Squares (WLS)</a> with Recency Decay:  Similar to linear regression, but weighs the error of more recent splits higher.  Weights decay exponentially with age down to a floor, so early splits lose influence without ever dropping out entirely.</li>
    </ul>
  </li>
  <li>Generalized Progression:  Changes in pace often follow a regular shape, so this fits several candidate shapes and keeps whichever explains the splits best.  The three below are the same linear fit under a <a href="https://www.itl.nist.gov/div898/handbook/eda/section3/eda336.htm">Box-Cox transform</a> (λ = 1, 0, and -1 respectively), which means additional shapes cost nothing but a new λ.  Candidates are scored by <a href="https://doi.org/10.1093/biomet/76.2.297">AICc</a>, the small-sample correction to Akaike’s Information Criterion, so one always wins; the criterion ranks the shapes against each other rather than testing whether any progression exists at all.
    <ul>
      <li>Minimum splits: 4</li>
      <li><a href="https://mathworld.wolfram.com/ArithmeticProgression.html">Arithmetic</a>:  Splits are increasing/decreasing by a precise number (ex. Doug’s 400m splits are dropping 5 seconds each)</li>
      <li><a href="https://mathworld.wolfram.com/GeometricProgression.html">Geometric</a>:  Splits are increasing/decreasing by a percentage (ex. Doug is slowing down by 5% every 400m)</li>
      <li><a href="https://mathworld.wolfram.com/HarmonicProgression.html">Harmonic</a>:  Speed is increasing/decreasing by a specific rate (ex. Doug’s speed is dropping by exactly 0.5 meters per second every 400m).  The name is exact rather than loose: a harmonic progression is one whose reciprocals form an arithmetic progression, and speed is the reciprocal of pace.</li>
    </ul>
  </li>
</ul>

<p>Since no single approach is right for every race, the predictions can also be combined.  An ensemble runs all of them and blends the results, weighting each by the tightness of its confidence interval, by its recent accuracy, or equally.  An adaptive selector instead scores the candidates against the last few segments and hands the race to whichever is currently performing best.</p>

<h4 id="split-prediction" class="linked-heading">
    Split prediction<a href="#split-prediction">🔗</a>
</h4>

<p>For split prediction, we account for the split distances that have already been recorded, since the application does not currently know what the target distance (if any) of the stopwatch is.</p>

<p>The approaches fall into two families: continuing the rhythm the user has already been recording, or working out which distances are meaningful for their sport.</p>

<h5 id="pattern-continuation" class="linked-heading">
    Pattern continuation<a href="#pattern-continuation">🔗</a>
</h5>

<p>These only look at the recorded splits and the lap, and make no assumptions about the sport.  Splits taken by hand are never exact, so each matches within a tolerance rather than on equality.</p>

<ul>
  <li>Fixed Interval:  Splits are being taken at a consistent distance (ex. 400/800/1200 predicts 1600).  Deltas are clustered rather than compared directly, and a majority of them have to fall in the same cluster before an interval is claimed.</li>
  <li>Delta Sequence:  Splits repeat on a cycle rather than a single interval (ex. 0/100/400/500/800 predicts 900, not 1200).  A cycle is accepted while most of its positions still match, so one missed or late split doesn’t invalidate it.</li>
  <li>GCD Grid:  Splits sit on a grid coarser than their pattern of use, which distinguishes every other grid position from every one (ex. 200/600/1000 predicts 1400, not 1200).  Distances are rounded before the divisor is taken, since a single stray meter would otherwise collapse the grid.</li>
  <li>Lap Relative:  Splits recur at the same position within a lap regardless of absolute distance (ex. always at the 200m mark of a 400m lap).  Positions are grouped circularly, so a split taken just before the line stays with the ones taken just after it.</li>
  <li>Ratio / Fractional:  Same as lap-relative, but in fractions of a lap, which matters when the lap doesn’t divide cleanly (ex. three-quarters of a 333.33m lap).  Each position snaps to the nearest fraction people actually use, and anything that lands near none of them is ignored.</li>
</ul>

<h5 id="milestone-inference" class="linked-heading">
    Milestone inference<a href="#milestone-inference">🔗</a>
</h5>

<p>These assume people stop the watch at round numbers, so the question becomes which set of round numbers applies.  Each sport gets a definition covering its base unit, conventional milestones, applicable distance range, expected pace range, and the lap distances that imply it.  Every match is scored rather than accepted or rejected, so a near miss lowers confidence instead of eliminating a system.</p>

<ul>
  <li>Lap-Based:  Lap distance implies the sport (ex. 25m is swimming, 400m is track, 2000m is rowing).</li>
  <li>Round Number Affinity:  Splits are scored against each system’s milestones, base unit, and distance range (ex. 1000/2000/3000 scores nearly perfectly for metric kilometers).</li>
  <li>Pace-Based:  Median pace narrows the field on its own (ex. 0.9 sec/m is swimming and cannot be cycling), and the median is used specifically so a single bad split doesn’t move it.</li>
  <li>Composite:  Weights the three inferences above, with lap highest and round number affinity lowest, and rewards agreement between them.</li>
  <li>Target Distance:  Generates milestones up to a known or inferred finish, and appends the finish itself as the final split.</li>
</ul>

<h3 id="try-it" class="linked-heading">
    Try It<a href="#try-it">🔗</a>
</h3>

<p>Epochron is live at <a href="https://stopwatch.dougfenstermacher.com/">stopwatch.dougfenstermacher.com</a> and the source is at <a href="https://github.com/dpfens/stopwatch">github.com/dpfens/stopwatch</a>, under the Unlicense.  It’s at v0.2.0 and still moving; if you time things for a living or a hobby and something here does not fit how you actually work, I want to hear about it.</p>]]></content><author><name>Doug Fenstermacher</name></author><category term="project" /><category term="typescript" /><category term="angular" /><category term="running" /><category term="math" /><summary type="html"><![CDATA[Most stopwatches assume one runner and one clock. Epochron is built for coaches and timers tracking multiple people at once. It supports group stopwatches for relays, heats, or ranked comparisons, and get live split predictions mid-race instead of doing the math yourself. It runs entirely in the browser, works offline (built for track meets with no signal), and keeps all data on your device. No account required.]]></summary></entry><entry><title type="html">XDoG: Computing Aesthetically Pleasing Lines</title><link href="https://dougfenstermacher.com/experiment/difference-of-gaussians" rel="alternate" type="text/html" title="XDoG: Computing Aesthetically Pleasing Lines" /><published>2026-04-13T00:00:00+00:00</published><updated>2026-04-13T00:00:00+00:00</updated><id>https://dougfenstermacher.com/experiment/difference-of-gaussians</id><content type="html" xml:base="https://dougfenstermacher.com/experiment/difference-of-gaussians"><![CDATA[<p>As I said in a previous post, I am learning to draw. Drawing by hand is going pretty well, but learning to draw on a computer is proving more difficult. I understand what layers are and what masks are, but I am still learning to plan out my drawing in terms of layers and masks.  I started with sketching, and still struggle with color and composition.</p>

<p>For that reason, I decided to take what I learned in my last post about hatching to see if I could use structural tensors to create more robust approach for turning images into decent sketches.</p>

<p>Below is an interactive demo of X-DoG/F-DoG (without extensions) for you to use.  It has WebGL and WebGPU support (as a learning opportunity) so it’s very fast, even on large images.  I have built some extensions, like multi-scale tensor/detail extraction, and color retention but they aren’t ready to be published yet.</p>

<p>If you want to read about the algorithm itself, you can read the original
<a href="https://users.cs.northwestern.edu/~sco590/winnemoeller-cag2012.pdf">XDoG: An eXtended difference-of-Gaussians compendium including advanced image stylization</a> paper.</p>

<p><strong>UPDATE</strong>: I have converted this to use a Web Worker (run on a separate thread), after realizing FDoG can be a bit slower for larger images.</p>

<head>
    <!-- Bootstrap 5.3 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
    
    <style>
        :root {
            --accent-color: black;
            --accent-dim: gray;
            --surface-1: rgba(30, 41, 59, 0.7);
            --surface-2: rgba(51, 65, 85, 0.5);
        }
        
        body {
            min-height: 100vh;
        }
        
        .font-mono {
            font-family: 'JetBrains Mono', monospace;
        }
        
        .accent-text {
            color: var(--accent-color);
        }
        
        .navbar {
            backdrop-filter: blur(12px);
            border-bottom: 1px solid rgba(255, 255, 255, 0.05);
        }
        
        .navbar-brand {
            font-weight: 600;
            letter-spacing: -0.02em;
        }
        
        .card {
            backdrop-filter: blur(12px);
            border: 1px solid rgba(255, 255, 255, 0.08);
            border-radius: 1rem;
        }
        
        .card-header {
            background: transparent;
            border-bottom: 1px solid rgba(255, 255, 255, 0.08);
            font-weight: 500;
        }
        
        /* Drop Zone */
        .drop-zone {
            border: 2px dashed rgba(110, 110, 110, 0.3);
            border-radius: 1rem;
            padding: 3rem;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        .drop-zone:hover,
        .drop-zone.drag-over {
            border-color: var(--accent-color);
        }
        
        .drop-zone-icon {
            font-size: 3rem;
            color: var(--accent-color);
            opacity: 0.6;
        }
        
        /* Image boxes */
        .image-container {
            position: relative;
            border-radius: 0.75rem;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 500px;
        }
        
        .image-container img,
        .image-container canvas {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }
        
        .image-label {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 0.5rem 1rem;
            background: black;
            font-size: 0.8rem;
            color: white;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        
        /* Controls */
        .form-label {
            font-size: 0.85rem;
            font-weight: 500;
            color: black;
            margin-bottom: 0.35rem;
        }
        
        .form-range {
            height: 0.5rem;
        }
        
        .form-range::-webkit-slider-thumb {
            background: black;
        }
        
        .param-value {
            font-family: 'JetBrains Mono', monospace;
            font-size: 0.8rem;
            color: var(--accent-color);
            min-width: 4rem;
            text-align: right;
        }
        
        /* Preset buttons */
        .preset-btn {
            font-size: 0.8rem;
            padding: 0.4rem 0.75rem;
            border: 1px solid black;
            background: transparent;
            color: black;
            transition: all 0.2s ease;
        }
        
        .preset-btn:hover {
            color: var(--accent-color);
            background: rgba(110, 231, 183, 0.1);
        }
        
        .preset-btn.active {
            border-color: var(--accent-color);
            color: var(--accent-color);
        }
        
        /* Mode toggle */
        .mode-toggle .btn-check:checked + .btn {
            background: rgba(110, 231, 183, 0.2);
            border-color: var(--accent-color);
            color: var(--accent-color);
        }
        
        .mode-toggle .btn {
            border-color: rgba(255, 255, 255, 0.15);
        }
        
        /* Accordion customization */
        .accordion-button {
            background: transparent;
            color: black
            font-weight: 500;
            padding: 0.75rem 1rem;
        }
        
        .accordion-button:not(.collapsed) {
            color: var(--accent-color);
        }
        
        .accordion-button::after {
            filter: invert(1);
        }
        
        /* Processing spinner */
        .processing-overlay {
            position: absolute;
            inset: 0;
            background: rgba(15, 23, 42, 0.9);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 1rem;
            z-index: 10;
            border-radius: 0.75rem;
        }
        
        .processing-overlay.d-none {
            display: none !important;
        }
        
        /* Scrollbar */
        ::-webkit-scrollbar {
            width: 8px;
        }
        
        ::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.05);
        }
        
        ::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.2);
            border-radius: 4px;
        }
        
        ::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.3);
        }
        
        /* Section titles */
        .section-title {
            font-size: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            color: #64748b;
            margin-bottom: 0.75rem;
            padding-bottom: 0.5rem;
            border-bottom: 1px solid rgba(255, 255, 255, 0.08);
        }
        
        /* FDoG params panel */
        .fdog-params {
            transition: all 0.3s ease;
        }
        
        .fdog-params.collapsed {
            max-height: 0;
            overflow: hidden;
            opacity: 0;
            margin: 0 !important;
            padding: 0 !important;
        }
        
        /* Info tooltip icon */
        .info-icon {
            color: #64748b;
            cursor: help;
            font-size: 0.85rem;
        }
        
        .info-icon:hover {
            color: var(--accent-color);
        }
    </style>
</head>
<div class="py-4">
    <div class="row g-4">
        <!-- Left Panel - Images -->
        <div class="col-12 col-md-8">
            <div class="card h-100">
                <div class="card-body">
                    <!-- Drop Zone -->
                    <div class="drop-zone mb-4" id="dropZone">
                        <div class="drop-zone-icon mb-2">
                            <i class="bi bi-cloud-arrow-up"></i>
                        </div>
                        <p class="mb-1 text-light">Drop an image here or click to upload</p>
                        <small class="text-secondary">Supports JPG, PNG, WebP</small>
                        <input type="file" id="fileInput" accept="image/*" hidden="" />
                    </div>

                    <!-- Image Tabs -->
                    <ul class="nav nav-tabs" id="imageTabs" role="tablist">
                        <li class="nav-item" role="presentation">
                            <button class="nav-link active" id="original-tab" data-bs-toggle="tab" data-bs-target="#original-pane" type="button" role="tab" aria-controls="original-pane" aria-selected="true">
                                Original
                            </button>
                        </li>
                        <li class="nav-item" role="presentation">
                            <button class="nav-link" id="result-tab" data-bs-toggle="tab" data-bs-target="#result-pane" type="button" role="tab" aria-controls="result-pane" aria-selected="false">
                                Result
                            </button>
                        </li>
                    </ul>

                    <div class="tab-content" id="imageTabsContent">
                        <div class="tab-pane fade show active" id="original-pane" role="tabpanel" aria-labelledby="original-tab" tabindex="0">
                            <div class="image-container image-container-lg">
                                <img id="originalImage" src="" alt="Original" class="d-none" />
                                <div class="text-secondary" id="originalPlaceholder">
                                    <i class="bi bi-image fs-1 opacity-25"></i>
                                </div>
                            </div>
                        </div>

                        <div class="tab-pane fade" id="result-pane" role="tabpanel" aria-labelledby="result-tab" tabindex="0">
                            <div class="image-container image-container-lg">
                                <canvas id="resultCanvas"></canvas>
                                <div class="processing-overlay d-none" id="processingOverlay">
                                    <div class="spinner-border text-light" role="status">
                                        <span class="visually-hidden">Processing...</span>
                                    </div>
                                    <span class="text-light">Processing...</span>
                                </div>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Action Buttons -->
                    <div class="d-flex gap-2 mt-3">
                        <button class="btn border border-2 border-black flex-grow-1" id="processBtn" disabled="">
                            <i class="bi bi-magic me-2"></i>Generate Line Drawing
                        </button>
                        <button class="btn border border-2 border-black" id="downloadBtn" disabled="" title="Download Result">
                            <i class="bi bi-download"></i>
                        </button>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- Right Panel - Controls -->
        <div class="col-12 col-md-4">
            <div class="card">
                <div class="card-body">
                    
                    <!-- Mode Selection -->
                    <div class="section-title">Processing Mode</div>
                    <div class="btn-group mode-toggle w-100 mb-4" role="group">
                        <input type="radio" class="btn-check" name="mode" id="modeXDoG" value="xdog" checked="" />
                        <label class="btn border border-2 border-black" for="modeXDoG">
                            <i class="bi bi-lightning me-1"></i> XDoG
                        </label>
                        <input type="radio" class="btn-check" name="mode" id="modeFDoG" value="fdog" />
                        <label class="btn border border-2 border-black ms-2" for="modeFDoG">
                            <i class="bi bi-bezier2 me-1"></i> FDoG
                        </label>
                    </div>
                    
                    <!-- XDoG Presets -->
                    <div id="xdogPresets">
                        <div class="section-title">Style Presets</div>
                        <div class="d-flex flex-wrap gap-2 mb-4">
                            <button class="btn preset-btn active" data-preset="threshold" data-mode="xdog">
                                ✒️ Line Art
                            </button>
                            <button class="btn preset-btn" data-preset="pencilShading" data-mode="xdog">
                                ✏️ Pencil
                            </button>
                            <button class="btn preset-btn" data-preset="pastel" data-mode="xdog">
                                🎨 Pastel
                            </button>
                            <button class="btn preset-btn" data-preset="charcoal" data-mode="xdog">
                                🖤 Charcoal
                            </button>
                            <button class="btn preset-btn" data-preset="woodcut" data-mode="xdog">
                                🪵 Woodcut
                            </button>
                        </div>
                    </div>
                    
                    <!-- FDoG Presets -->
                    <div id="fdogPresets" class="d-none">
                        <div class="section-title">FDoG Style Presets</div>
                        <div class="d-flex flex-wrap gap-2 mb-4">
                            <button class="btn preset-btn active" data-preset="standard" data-mode="fdog">
                                📐 Standard
                            </button>
                            <button class="btn preset-btn" data-preset="pastel" data-mode="fdog">
                                🎨 Pastel
                            </button>
                            <button class="btn preset-btn" data-preset="woodcut" data-mode="fdog">
                                🪵 Woodcut
                            </button>
                        </div>
                    </div>
                    
                    <!-- Core DoG Parameters -->
                    <div class="accordion" id="paramsAccordion">
                        <div class="accordion-item bg-transparent border-0">
                            <h2 class="accordion-header">
                                <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#coreParams">
                                    Core Parameters
                                </button>
                            </h2>
                            <div id="coreParams" class="accordion-collapse collapse show" data-bs-parent="#paramsAccordion">
                                <div class="accordion-body">
                                    <!-- Sigma -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                σ (Sigma) - Edge Scale
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Controls the scale of detected edges. Larger values detect coarser edges."></i>
                                            </label>
                                            <span class="param-value" id="sigmaValue">1.4</span>
                                        </div>
                                        <input type="range" class="form-range" id="sigma" min="0.3" max="8" step="0.1" value="1.4" />
                                    </div>
                                    
                                    <!-- K -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                k - Blur Ratio
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Ratio between the two Gaussian blurs. 1.6 is recommended."></i>
                                            </label>
                                            <span class="param-value" id="kValue">1.6</span>
                                        </div>
                                        <input type="range" class="form-range" id="k" min="1.1" max="3" step="0.1" value="1.6" />
                                    </div>
                                    
                                    <!-- P (sharpening strength) -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                p - Sharpening Strength
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Controls edge emphasis. ~20 for normal, ~100+ for woodcut style."></i>
                                            </label>
                                            <span class="param-value" id="pValue">20</span>
                                        </div>
                                        <input type="range" class="form-range" id="p" min="0" max="150" step="1" value="20" />
                                    </div>
                                    
                                    <!-- Epsilon -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                ε (Epsilon) - Threshold
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Values above this become white. Range 0-1 for normalized images."></i>
                                            </label>
                                            <span class="param-value" id="epsilonValue">0.78</span>
                                        </div>
                                        <input type="range" class="form-range" id="epsilon" min="0" max="1" step="0.01" value="0.78" />
                                    </div>
                                    
                                    <!-- Phi -->
                                    <div class="mb-0">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                φ (Phi) - Threshold Sharpness
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Controls transition sharpness. Low values for soft gradients, high for hard edges."></i>
                                            </label>
                                            <span class="param-value" id="phiValue">100</span>
                                        </div>
                                        <input type="range" class="form-range" id="phi" min="0.01" max="200" step="0.01" value="100" />
                                    </div>
                                </div>
                            </div>
                        </div>
                        
                        <!-- FDoG-Specific Parameters -->
                        <div class="accordion-item bg-transparent border-0 fdog-params collapsed" id="fdogParamsSection">
                            <h2 class="accordion-header">
                                <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#fdogParams">
                                    FDoG Flow Parameters
                                </button>
                            </h2>
                            <div id="fdogParams" class="accordion-collapse collapse" data-bs-parent="#paramsAccordion">
                                <div class="accordion-body">
                                    <!-- SigmaC -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                σc - Structure Tensor Smoothing
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Controls ETF smoothness. Small values capture fine edges, large values create smoother flow."></i>
                                            </label>
                                            <span class="param-value" id="sigmaCValue">2.28</span>
                                        </div>
                                        <input type="range" class="form-range" id="sigmaC" min="0.1" max="8" step="0.1" value="2.28" />
                                    </div>
                                    
                                    <!-- SigmaM -->
                                    <div class="mb-3">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                σm - Flow-Aligned Smoothing
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Controls line coherence. Larger values create longer, smoother lines."></i>
                                            </label>
                                            <span class="param-value" id="sigmaMValue">4.4</span>
                                        </div>
                                        <input type="range" class="form-range" id="sigmaM" min="0" max="25" step="0.1" value="4.4" />
                                    </div>
                                    
                                    <!-- SigmaA -->
                                    <div class="mb-0">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <label class="form-label mb-0">
                                                σa - Anti-Aliasing
                                                <i class="bi bi-info-circle info-icon ms-1" data-bs-toggle="tooltip" title="Post-processing smoothing along ETF. 0 disables, 0.5-2 typical."></i>
                                            </label>
                                            <span class="param-value" id="sigmaAValue">1.0</span>
                                        </div>
                                        <input type="range" class="form-range" id="sigmaA" min="0" max="10" step="0.1" value="1.0" />
                                    </div>
                                </div>
                            </div>
                        </div>
                        
                        <!-- Preprocessing -->
                        <div class="accordion-item bg-transparent border-0">
                            <h2 class="accordion-header">
                                <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#preprocessParams">
                                    Preprocessing
                                </button>
                            </h2>
                            <div id="preprocessParams" class="accordion-collapse collapse" data-bs-parent="#paramsAccordion">
                                <div class="accordion-body">
                                    <div class="mb-3">
                                        <label class="form-label">Preprocessing Mode</label>
                                        <select class="form-select form-select-sm" id="preprocessMode">
                                            <option value="none">None</option>
                                            <option value="light">Light (clean images)</option>
                                            <option value="standard" selected="">Standard (most photos)</option>
                                            <option value="heavy">Heavy (textured images)</option>
                                            <option value="nature">Nature (landscapes)</option>
                                            <option value="artistic">Artistic (painterly)</option>
                                        </select>
                                    </div>
                                    <small class="text-secondary">
                                        Preprocessing smooths textures while preserving edges, 
                                        reducing noise in the final result.
                                    </small>
                                </div>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Info -->
                    <div class="mt-4 p-3 rounded" style="background: rgba(110, 231, 183, 0.05); border: 1px solid rgba(110, 231, 183, 0.1);">
                        <h6 class="accent-text mb-2">
                            <i class="bi bi-lightbulb me-1"></i> Tips
                        </h6>
                        <small class="text-secondary d-block mb-2">
                            <strong class="text-light">XDoG</strong> uses isotropic Gaussian blur - fast and great for most images.
                        </small>
                        <small class="text-secondary d-block">
                            <strong class="text-light">FDoG</strong> uses flow-guided blur along edge tangents for smoother, 
                            more coherent lines. Best for portraits and subjects where line continuity matters.
                        </small>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

<script type="module">
    import { 
        STYLE_PRESETS,
        FDOG_STYLE_PRESETS
    } from '/assets/js/libs/homemade/xdog/index.js';
    const worker = new Worker(
        new URL('/assets/js/workers/xdog-base.js', import.meta.url), 
        { type: 'module' }
    );

    worker.onmessage = function(e) {
        const {data} = e.data;
        // Display result
        resultCanvas.width = data.width;
        resultCanvas.height = data.height;
        const ctx = resultCanvas.getContext('2d');
        ctx.putImageData(data, 0, 0);
        processBtn.disabled = false;
        processingOverlay.classList.add('d-none');
        resultTab.show();
    }

    worker.onerror = function(e) {
        console.error(e);
        console.error("Error message:", event.message);
        console.error("File name:", event.filename);
        console.error("Line number:", event.lineno);
    }

    // State
    let currentMode = 'xdog';
    let loadedImage = null;

    // DOM Elements
    const dropZone = document.getElementById('dropZone');
    const fileInput = document.getElementById('fileInput');
    const originalImage = document.getElementById('originalImage');
    const originalPlaceholder = document.getElementById('originalPlaceholder');
    const resultCanvas = document.getElementById('resultCanvas');
    const processBtn = document.getElementById('processBtn');
    const downloadBtn = document.getElementById('downloadBtn');
    const processingOverlay = document.getElementById('processingOverlay');
    const xdogPresets = document.getElementById('xdogPresets');
    const fdogPresets = document.getElementById('fdogPresets');
    const fdogParamsSection = document.getElementById('fdogParamsSection');
    const preprocessMode = document.getElementById('preprocessMode');

    // Core parameter sliders
    const sliders = {
        sigma: document.getElementById('sigma'),
        k: document.getElementById('k'),
        p: document.getElementById('p'),
        epsilon: document.getElementById('epsilon'),
        phi: document.getElementById('phi'),
    };

    // FDoG-specific sliders
    const fdogSliders = {
        sigmaC: document.getElementById('sigmaC'),
        sigmaM: document.getElementById('sigmaM'),
        sigmaA: document.getElementById('sigmaA'),
    };

    // Initialize Bootstrap tooltips
    const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
    const tooltipList = [...tooltipTriggerList].map(el => new bootstrap.Tooltip(el));

    const originalTab = bootstrap.Tab.getOrCreateInstance(document.getElementById('original-tab'));
    const resultTab   = bootstrap.Tab.getOrCreateInstance(document.getElementById('result-tab'));

    // Helper to wait for image load
    function waitForImage(img) {
        return new Promise((resolve, reject) => {
            if (img.complete && img.naturalWidth > 0) {
                resolve(img);
            } else {
                img.onload = () => resolve(img);
                img.onerror = reject;
            }
        });
    }

    // Update slider value displays
    function updateSliderDisplays() {
        document.getElementById('sigmaValue').textContent = parseFloat(sliders.sigma.value).toFixed(1);
        document.getElementById('kValue').textContent = parseFloat(sliders.k.value).toFixed(1);
        document.getElementById('pValue').textContent = sliders.p.value;
        document.getElementById('epsilonValue').textContent = parseFloat(sliders.epsilon.value).toFixed(2);
        document.getElementById('phiValue').textContent = parseFloat(sliders.phi.value).toFixed(1);
        
        document.getElementById('sigmaCValue').textContent = parseFloat(fdogSliders.sigmaC.value).toFixed(2);
        document.getElementById('sigmaMValue').textContent = parseFloat(fdogSliders.sigmaM.value).toFixed(1);
        document.getElementById('sigmaAValue').textContent = parseFloat(fdogSliders.sigmaA.value).toFixed(1);
    }

    // Apply XDoG preset
    function applyXDoGPreset(presetName) {
        const preset = STYLE_PRESETS[presetName];
        if (!preset) return;

        sliders.sigma.value = preset.sigma;
        sliders.k.value = preset.k;
        sliders.p.value = preset.p;
        sliders.epsilon.value = preset.epsilon;
        sliders.phi.value = preset.phi;

        updateSliderDisplays();
    }

    // Apply FDoG preset
    function applyFDoGPreset(presetName) {
        const preset = FDOG_STYLE_PRESETS[presetName];
        if (!preset) return;

        sliders.sigma.value = preset.sigma;
        sliders.k.value = preset.k;
        sliders.p.value = preset.p;
        sliders.epsilon.value = preset.epsilon;
        sliders.phi.value = preset.phi;
        
        fdogSliders.sigmaC.value = preset.sigmaC;
        fdogSliders.sigmaM.value = preset.sigmaM;
        fdogSliders.sigmaA.value = preset.sigmaA;

        updateSliderDisplays();
    }

    // Get current parameters from sliders
    function getParams() {
        const params = {
            sigma: parseFloat(sliders.sigma.value),
            k: parseFloat(sliders.k.value),
            p: parseFloat(sliders.p.value),
            epsilon: parseFloat(sliders.epsilon.value),
            phi: parseFloat(sliders.phi.value),
        };
        
        if (currentMode === 'fdog') {
            params.sigmaC = parseFloat(fdogSliders.sigmaC.value);
            params.sigmaM = parseFloat(fdogSliders.sigmaM.value);
            params.sigmaA = parseFloat(fdogSliders.sigmaA.value);
        }
        
        return params;
    }

    // Process image
    async function processImage() {
        if (!loadedImage) return;

        processBtn.disabled = true;
        processingOverlay.classList.remove('d-none');

        // Small delay to let UI update
        await new Promise(r => setTimeout(r, 50));

        try {
            const params = getParams();
            
            // Create canvas to get image data
            const tempCanvas = document.createElement('canvas');
            const tempCtx = tempCanvas.getContext('2d');
            tempCanvas.width = loadedImage.naturalWidth;
            tempCanvas.height = loadedImage.naturalHeight;
            tempCtx.drawImage(loadedImage, 0, 0);
            
            const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
            worker.postMessage({
                preprocess: preprocessMode.value,
                mode: currentMode,
                params: params,
                data: imageData
            });

            downloadBtn.disabled = false;
        } catch (error) {
            console.error('Processing error:', error);
            alert('Error processing image: ' + error.message);
        }
    }

    // Handle file selection
    function handleFile(file) {
        if (!file.type.startsWith('image/')) {
            alert('Please select an image file');
            return;
        }

        const reader = new FileReader();
        reader.onload = async (e) => {
            originalImage.src = e.target.result;
            await waitForImage(originalImage);
            originalImage.classList.remove('d-none');
            originalPlaceholder.classList.add('d-none');
            loadedImage = originalImage;
            processBtn.disabled = false;
            
            // Auto-process
            processImage();
        };
        reader.readAsDataURL(file);
    }

    // Toggle mode UI
    function updateModeUI() {
        if (currentMode === 'xdog') {
            xdogPresets.classList.remove('d-none');
            fdogPresets.classList.add('d-none');
            fdogParamsSection.classList.add('collapsed');
        } else {
            xdogPresets.classList.add('d-none');
            fdogPresets.classList.remove('d-none');
            fdogParamsSection.classList.remove('collapsed');
        }
    }

    // Event Listeners
    dropZone.addEventListener('click', () => fileInput.click());
    
    dropZone.addEventListener('dragover', (e) => {
        e.preventDefault();
        dropZone.classList.add('drag-over');
    });
    
    dropZone.addEventListener('dragleave', () => {
        dropZone.classList.remove('drag-over');
    });
    
    dropZone.addEventListener('drop', (e) => {
        e.preventDefault();
        dropZone.classList.remove('drag-over');
        const file = e.dataTransfer.files[0];
        if (file) handleFile(file);
    });

    fileInput.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (file) handleFile(file);
        originalTab.show();
    });

    // Mode toggle
    document.querySelectorAll('input[name="mode"]').forEach(radio => {
        radio.addEventListener('change', (e) => {
            currentMode = e.target.value;
            updateModeUI();
            
            // Apply default preset for the mode
            if (currentMode === 'xdog') {
                applyXDoGPreset('threshold');
                document.querySelectorAll('#xdogPresets .preset-btn').forEach(b => b.classList.remove('active'));
                document.querySelector('#xdogPresets .preset-btn[data-preset="threshold"]').classList.add('active');
            } else {
                applyFDoGPreset('standard');
                document.querySelectorAll('#fdogPresets .preset-btn').forEach(b => b.classList.remove('active'));
                document.querySelector('#fdogPresets .preset-btn[data-preset="standard"]').classList.add('active');
            }
            
            if (loadedImage) processImage();
        });
    });

    // XDoG preset buttons
    document.querySelectorAll('#xdogPresets .preset-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            document.querySelectorAll('#xdogPresets .preset-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            applyXDoGPreset(btn.dataset.preset);
            if (loadedImage) processImage();
        });
    });

    // FDoG preset buttons
    document.querySelectorAll('#fdogPresets .preset-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            document.querySelectorAll('#fdogPresets .preset-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            applyFDoGPreset(btn.dataset.preset);
            if (loadedImage) processImage();
        });
    });

    // Core sliders
    Object.values(sliders).forEach(slider => {
        slider.addEventListener('input', updateSliderDisplays);
        slider.addEventListener('change', () => {
            if (loadedImage) processImage();
        });
    });

    // FDoG sliders
    Object.values(fdogSliders).forEach(slider => {
        slider.addEventListener('input', updateSliderDisplays);
        slider.addEventListener('change', () => {
            if (loadedImage) processImage();
        });
    });

    // Preprocessing mode change
    preprocessMode.addEventListener('change', () => {
        if (loadedImage) processImage();
    });

    processBtn.addEventListener('click', processImage);

    downloadBtn.addEventListener('click', () => {
        const link = document.createElement('a');
        link.download = `line-drawing-${currentMode}.png`;
        link.href = resultCanvas.toDataURL('image/png');
        link.click();
    });

    // Initialize
    updateSliderDisplays();
    updateModeUI();
    applyXDoGPreset('threshold');
</script>]]></content><author><name>Doug Fenstermacher</name></author><category term="experiment" /><category term="data" /><category term="math" /><category term="webgpu" /><category term="webgl" /><category term="drawing" /><summary type="html"><![CDATA[A deeper dive into modern approaches for generating aesthetically pleasing lines based on difference-of-Gaussians (DoG) edge detections.]]></summary></entry><entry><title type="html">Streamline Hatching: A Programmer’s Attempt at Computational Drawing</title><link href="https://dougfenstermacher.com/experiment/streamline-hatching" rel="alternate" type="text/html" title="Streamline Hatching: A Programmer’s Attempt at Computational Drawing" /><published>2025-12-26T00:00:00+00:00</published><updated>2025-12-26T00:00:00+00:00</updated><id>https://dougfenstermacher.com/experiment/streamline-hatching</id><content type="html" xml:base="https://dougfenstermacher.com/experiment/streamline-hatching"><![CDATA[<p>I’ve always wanted to be able to draw. I like the idea of taking an image in my mind and making it so everyone else can see what I see. And I like the creative aspect of it.  Being able to draw whatever I want, however I want.  But I can’t draw. Not yet, at least.  I’m working on it though.  I try to doodle while I watch TV to build my confidence.  I try to sketch thing I’m bad at, like clothes, faces, people to get better at proportions.  But I struggle particularly with different styles of drawing.</p>

<p>This is a guide to one particular technique called <em>streamline hatching</em> where <em>hatching</em> is a pen &amp; paper approach for shading an image where strokes following the form of objects, density varies with tone, crosshatching building up in shadows, etc. More importantly, you’ll understand <em>why</em> each piece of the pipeline exists and what happens when you change it.</p>

<h2 id="what-we-re-building-toward" class="linked-heading">
    What We're Building Toward<a href="#what-we-re-building-toward">🔗</a>
</h2>

<p>Hatching is a drawing technique where artists use parallel lines to create tone and texture. The direction of the strokes typically follows the form of what’s being drawn, like curving around a sphere, running along a cylinder, radiating from a corner etc. The spacing between strokes controls darkness, so closer strokes appear darker which means a darker shade like a shadow, and further apart strokes for highlights.</p>

<p>Cross-hatching adds a second layer of strokes at an angle to the first, building up darker tones in shadow regions.</p>

<p>When hatching is done well, hatching reveals what shape something is as well as how dark it is.</p>

<p>My goal was to replicate this. Given an input image, I wanted to:</p>

<ol>
  <li>Understand the directional structure of the image.  where are the forms, and which way do they “flow”?</li>
  <li>Generate strokes that follow this structure</li>
  <li>Control density and placement so that dark areas get more strokes than light areas</li>
  <li>Optionally add cross-hatching in the darkest regions</li>
</ol>

<p>The output should look like something a human might have drawn.</p>

<h2 id="the-pipeline-at-a-glance" class="linked-heading">
    The Pipeline at a Glance<a href="#the-pipeline-at-a-glance">🔗</a>
</h2>

<p>Here’s the full pipeline:</p>

<ol>
  <li>Input Image</li>
  <li>Grayscale Conversion</li>
  <li>Structure Tensor Computation (at multiple scales)</li>
  <li>Orientation &amp; Coherence Fields</li>
  <li>Edge Tangent Flow (ETF) Refinement</li>
  <li>Direction Propagation</li>
  <li>Stroke Generation</li>
  <li>Rendering</li>
</ol>

<p>Here’s the working implementation. Try it with the preset shapes or upload your own image.  Experiment with the parameters and watch how the output changes. The rest of this post explains what’s happening under the hood.</p>

<p class="codepen" data-height="900" data-default-tab="result" data-slug-hash="emzmzdd" data-pen-title="Hatching - WebGPU" data-user="dfens" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/dfens/pen/emzmzdd">
  Hatching - WebGPU</a> by Douglas Fenstermacher (<a href="https://codepen.io/dfens">@dfens</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<h3 id="the-parameter-space" class="linked-heading">
    The Parameter Space<a href="#the-parameter-space">🔗</a>
</h3>

<p>The full system has many parameters.  Here they are, grouped by the aspect of the outcome they effect:</p>

<p><strong>Analysis parameters</strong> (affect how structure is perceived):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sigma</code> array: Scales for multi-scale analysis</li>
  <li><code class="language-plaintext highlighter-rouge">scaleCombination</code>: How to combine scales (‘eigenvalue’ or ‘gradient’)</li>
  <li><code class="language-plaintext highlighter-rouge">fineScaleBias</code>: Preference for fine vs. coarse detail</li>
  <li><code class="language-plaintext highlighter-rouge">minCoherenceThreshold</code>: Below this, orientation is unreliable</li>
</ul>

<p><strong>Flow field parameters</strong> (affect smoothness and propagation):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">etfIterations</code>: How many refinement passes</li>
  <li><code class="language-plaintext highlighter-rouge">etfKernelSize</code>: Size of the refinement neighborhood</li>
  <li><code class="language-plaintext highlighter-rouge">propagationIterations</code>: How far direction spreads into flat regions</li>
</ul>

<p><strong>Stroke parameters</strong> (affect individual strokes):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">strokeCount</code>: How many strokes to attempt</li>
  <li><code class="language-plaintext highlighter-rouge">minLength</code>, <code class="language-plaintext highlighter-rouge">maxLength</code>: Stroke length range</li>
  <li><code class="language-plaintext highlighter-rouge">strokeWidth</code>, <code class="language-plaintext highlighter-rouge">widthVariation</code>: Appearance</li>
  <li><code class="language-plaintext highlighter-rouge">strokeJitter</code>: Hand-drawn wobble</li>
</ul>

<p><strong>Placement parameters</strong> (affect overall coverage):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">toneInfluence</code>: Weight darkness vs. uniform coverage</li>
  <li><code class="language-plaintext highlighter-rouge">minSpacing</code>: Minimum distance between stroke seeds</li>
  <li><code class="language-plaintext highlighter-rouge">seedRandomness</code>: Jitter in seed grid</li>
</ul>

<p><strong>Tone parameters</strong> (affect light/dark handling):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">backgroundBrightnessThreshold</code>: What counts as “background”</li>
  <li><code class="language-plaintext highlighter-rouge">lowGradientThreshold</code>: What counts as “no structure”</li>
  <li><code class="language-plaintext highlighter-rouge">crossHatch</code>: Enable perpendicular strokes</li>
  <li><code class="language-plaintext highlighter-rouge">crossHatchThreshold</code>: Darkness level for cross-hatching</li>
</ul>

<h3 id="presets-as-starting-points" class="linked-heading">
    Presets as Starting Points<a href="#presets-as-starting-points">🔗</a>
</h3>

<p>The implementation includes several presets that configure these parameters for different effects:</p>

<p><strong>Sketch</strong>: Loose, gestural strokes</p>
<ul>
  <li>Fewer, longer strokes</li>
  <li>Higher width variation</li>
  <li>Lower opacity</li>
</ul>

<p><strong>Engraving</strong>: Dense, controlled lines</p>
<ul>
  <li>Many short strokes</li>
  <li>Low width variation</li>
  <li>High tone influence</li>
  <li>Cross-hatching enabled</li>
</ul>

<p><strong>Crosshatch</strong>: Emphasis on tonal building</p>
<ul>
  <li>Medium density</li>
  <li>Strong cross-hatching in shadows</li>
</ul>

<p><strong>Simple Shapes</strong>: For geometric forms</p>
<ul>
  <li>Single scale (coarse)</li>
  <li>High propagation to fill flat regions</li>
</ul>

<h3 id="computational-considerations" class="linked-heading">
    Computational Considerations<a href="#computational-considerations">🔗</a>
</h3>

<p>The implementation supports both CPU and WebGPU backends. The core algorithms are the same; only the tensor operations differ.</p>

<p>For CPU:</p>
<ul>
  <li>Structure tensor computation: <code class="language-plaintext highlighter-rouge">O(width × height × scales × kernel_size^2)</code></li>
  <li>ETF refinement: <code class="language-plaintext highlighter-rouge">O(width × height × iterations × kernel_size^2)</code></li>
  <li>Stroke generation: <code class="language-plaintext highlighter-rouge">O(stroke_count × max_length)</code></li>
</ul>

<p>For interactive use on typical images (500×500), CPU processing takes 1-3 seconds on a modern browser. WebGPU reduces this substantially for the tensor operations but has overhead for the stroke generation phase which remains CPU-bound.</p>

<h2 id="further-reading" class="linked-heading">
    Further Reading<a href="#further-reading">🔗</a>
</h2>

<p>For you want to look into the underlying techniques:</p>

<ul>
  <li><strong>Structure Tensor</strong>: Förstner, W., &amp; Gülch, E. (1987). “A fast operator for detection and precise location of distinct points, corners and centres of circular features.” Search for “structure tensor image processing” for accessible tutorials.</li>
  <li><strong>Edge Tangent Flow</strong>: Kang, H., Lee, S., &amp; Chui, C. K. (2007). “Coherent Line Drawing.” This paper introduced the ETF refinement technique.</li>
  <li><strong>Streamline Visualization</strong>: Search “streamline integration visualization” for the fluid dynamics perspective on field-following curves.</li>
  <li><strong>Non-Photorealistic Rendering</strong>: The field of NPR has extensive literature on computational illustration techniques. Gooch &amp; Gooch’s book “Non-Photorealistic Rendering” is a comprehensive introduction.</li>
</ul>

<p><em>The complete implementation, including an interactive demo, is available on <a href="https://codepen.io/dfens/full/emzmzdd">CodePen</a>. Try it with your own images and experiment with the parameters.  the best way to understand the system is to see how each setting affects the output.</em></p>]]></content><author><name>Doug Fenstermacher</name></author><category term="experiment" /><category term="data" /><category term="math" /><category term="webgpu" /><category term="drawing" /><category term="svg" /><summary type="html"><![CDATA[Sharing an attempt at computational hatching. Using structure tensors, edge tangent flow, and streamline integration to transform photographs into hand-drawn-looking illustrations.]]></summary></entry><entry><title type="html">Balancing LLM Prompt Analytics and User Privacy</title><link href="https://dougfenstermacher.com/blog/llm-prompt-analytics" rel="alternate" type="text/html" title="Balancing LLM Prompt Analytics and User Privacy" /><published>2024-09-22T00:00:00+00:00</published><updated>2024-09-22T00:00:00+00:00</updated><id>https://dougfenstermacher.com/blog/llm-prompt-analytics</id><content type="html" xml:base="https://dougfenstermacher.com/blog/llm-prompt-analytics"><![CDATA[<p>LLMs have been evolving rapidly.  It’s important to organizations that run these LLMs learn how their users use them, which can conflict with user privacy especially if they enter private/personal information into them.  This post explores an approach for giving actionable information on how people use conversational LLMs without violating their privacy.</p>

<h2 id="respecting-user-privacy" class="linked-heading">
    Respecting User Privacy<a href="#respecting-user-privacy">🔗</a>
</h2>

<p>While embeddings have become a popular and powerful tool for analyzing text data, including user prompts, they come with significant privacy implications that warrant careful consideration. Embeddings capture the semantic essence of text, often in ways that can inadvertently reveal more information than intended. Embeddings can encode personal details, writing styles, or specific knowledge that, when analyzed in aggregate, could lead to the identification of individuals or the disclosure of private information.</p>

<p>For instance, embeddings of prompts related to medical conditions, financial situations, or personal relationships could, even unintentionally, create a detailed profile of a user’s life circumstances. In corporate environments, embeddings might capture proprietary information or strategic plans embedded within prompts. The risk of such unintended disclosures is particularly concerning given the often personal or sensitive nature of interactions with AI assistants.</p>

<p>Moreover, the high-dimensional nature of embeddings means they can capture subtle patterns that users themselves might not be aware they’re revealing. This “hidden” information could be exploited through advanced analysis techniques, potentially compromising user privacy in ways that are difficult to anticipate or mitigate.</p>

<p>Given these concerns, it’s crucial to adopt an approach that explicitly respects the privacy of end-users while still providing valuable insights for system improvement and user experience enhancement. This is where the analysis of orthogonal components comes into play. By focusing on abstract, high-level characteristics of prompts rather than their specific content, we can gain useful analytical insights while maintaining a strong commitment to user privacy.</p>

<p>In this scenario, we can think of orthogonal components as dimensions of interaction we are curious about, which do not influence each other.  For example, the ambiguity of a prompt and the purpose of an LLM prompt can change independently.  One can write a prompt with the same purpose with varying degrees of ambiguity.  Changing the ambiguity of a prompt does not effect the purpose of the prompt, and vice versa.</p>

<p>The orthogonal component approach allows us to do a few things.  We can add more dimensions and values within each dimension without invalidating our existing data, and every new dimension/value can provide context across all the other dimensions.  And it provides a framework for analysis that is inherently more privacy-preserving, as it captures the nature of the interaction rather than the content itself.</p>

<p>In the following sections, I’ll explore how this privacy-respecting method can be implemented effectively, providing robust analytics capabilities without compromising the trust and confidentiality that users expect when interacting with LLMs.</p>

<h2 id="orthogonal-components" class="linked-heading">
    Orthogonal Components<a href="#orthogonal-components">🔗</a>
</h2>

<p>The orthogonal components of a prompt provide a multidimensional framework for analyzing user interactions with large language models (LLMs). By breaking down prompts into these distinct elements, we can gain valuable insights into user behavior and LLM performance without compromising individual privacy. These components encompass a wide range of factors, from the basic intent of the prompt to the level of creativity expected in the response.</p>

<ul>
  <li><strong>Intent</strong>: The core purpose or goal of the prompt.</li>
  <li><strong>Complexity</strong>: The level of difficulty or sophistication required in the response.</li>
  <li><strong>Ambiguity</strong>: The degree of clarity or vagueness in the prompt’s wording.</li>
  <li><strong>Scope</strong>: The breadth or narrowness of the topic or task at hand.</li>
  <li><strong>Specificity</strong>: The level of detail requested in the response.</li>
  <li><strong>Tone</strong>: The emotional or stylistic approach requested (e.g., formal, casual, humorous).</li>
  <li><strong>Domain(s)</strong>: The field or subject area the prompt relates to.</li>
  <li><strong>Cognitive level</strong>: The depth of thought or analysis required (e.g., recall, application, evaluation).</li>
  <li><strong>Persona</strong>: Any specific character or role the LLM is asked to assume.</li>
  <li><strong>Abstraction level</strong>: How concrete or abstract the prompt and expected response should be.</li>
  <li><strong>Creativity</strong>: The degree of originality or innovation expected in the response.</li>
  <li><strong>Subjectivity vs. Objectivity</strong>: The degree of personal opinion versus impartial analysis required.</li>
</ul>

<p>At the core of this framework is the intent, which identifies the fundamental purpose of the user’s query. This is complemented by the complexity and ambiguity components, which gauge the sophistication required in the response and the clarity of the prompt itself. The scope and specificity components work together to define the breadth of the topic and the level of detail requested, providing a clear picture of the user’s informational needs.</p>

<p>The tone and persona components offer insights into the stylistic and role-playing aspects of user interactions with the LLM. These elements can reveal patterns in how users engage with AI systems, whether they prefer formal interactions or more casual, character-driven exchanges. The domain component helps categorize prompts into specific fields or subject areas, allowing for a better understanding of the topics users are most interested in exploring.</p>

<p>Cognitive level and abstraction level components provide a deeper look into the type of thinking and conceptualization users expect from the LLM. These elements can help identify whether users are primarily seeking factual recall, complex analysis, or abstract reasoning. The creativity component adds another dimension, indicating the degree of originality users are looking for in AI-generated responses.</p>

<p>By analyzing the combinations of these orthogonal components across numerous user interactions, we can begin to create detailed personas of end-user conversations. These personas represent archetypal users based on their typical prompt patterns, without relying on any specific prompt content. For example, we might identify a “Detail-Oriented Analyst” persona who consistently uses prompts with high specificity, low ambiguity, and a preference for objective responses across various domains.</p>

<p>Another persona might be the “Creative Brainstormer,” characterized by prompts with high creativity, broad scope, and often involving persona-based interactions. By recognizing these patterns, product developers can gain valuable insights into how different user types engage with the LLM, informing decisions about feature development, interface design, and model fine-tuning.</p>

<p>These user personas can drive product development in several ways. For instance, if a significant portion of users fit the “Detail-Oriented Analyst” persona, developers might prioritize features that enhance the LLM’s ability to provide precise, well-structured responses. Conversely, for the “Creative Brainstormer” persona, the focus might be on expanding the model’s capacity for generating novel ideas and engaging in more dynamic, role-playing scenarios.</p>

<p>Importantly, this approach to analytics and persona creation maintains user privacy by focusing on aggregate patterns rather than private details of individual prompts. It allows organizations to make data-driven decisions about their LLM implementations while respecting user confidentiality. As AI technology continues to evolve, this balance between insightful analytics and robust privacy protection will be crucial in building and maintaining user trust in AI systems.</p>

<h3 id="conversations" class="linked-heading">
    Conversations<a href="#conversations">🔗</a>
</h3>

<p>In addition to prompt-level orthogonal components, we can also identify and analyze components at the conversation level. These conversation-level components provide a broader perspective on user interactions with the LLM, capturing patterns and characteristics that emerge over the course of multiple exchanges. Here are some key conversation-level orthogonal components:</p>

<ul>
  <li><strong>Conversation Length</strong>: The number of turns or exchanges in the conversation.</li>
  <li><strong>Topic Consistency</strong>: The degree to which the conversation stays focused on a single topic or ranges across multiple subjects.</li>
  <li><strong>Depth Progression</strong>: How the conversation evolves in terms of complexity and detail from start to finish.</li>
  <li><strong>Interaction Pattern</strong>: The rhythm of the conversation, such as rapid back-and-forth exchanges or longer, more deliberate responses.</li>
  <li><strong>Goal Orientation</strong>: Whether the conversation is task-oriented, exploratory, or social in nature.</li>
  <li><strong>Emotional Trajectory</strong>: How the tone or sentiment of the conversation changes over time.</li>
  <li><strong>Creativity Fluctuation</strong>: The variation in creative elements throughout the conversation.</li>
  <li><strong>Query Refinement</strong>: How user prompts become more or less specific or focused as the conversation progresses.</li>
  <li><strong>AI Reliance</strong>: The extent to which the user relies on the AI’s responses versus contributing their own knowledge.</li>
  <li><strong>Coherence</strong>: The logical flow and connectedness between different parts of the conversation.</li>
  <li><strong>Learning Curve</strong>: Evidence of the user adapting their interaction style or understanding of the AI’s capabilities over time.</li>
  <li><strong>Multi-modal Interaction</strong>: The use of different types of inputs or requests (e.g., text, code, data analysis) within a single conversation.</li>
</ul>

<p>These conversation-level components provide valuable insights into how users engage with the LLM over extended interactions. By analyzing these components, we can identify patterns in user behavior, preferences in conversation style, and the effectiveness of the LLM in sustaining meaningful dialogues.</p>

<p>For example, by examining the Depth Progression and Query Refinement components, we might observe that successful problem-solving conversations often start with broad, exploratory queries and gradually narrow down to specific, detailed exchanges. This insight could inform improvements in the LLM’s ability to guide users through complex problem-solving processes.</p>

<p>Similarly, analyzing the Emotional Trajectory and Interaction Pattern could reveal how different conversation rhythms correlate with user satisfaction or productive outcomes. This information could be used to train the LLM to adapt its response style to better match user preferences and optimize engagement.</p>

<p>Importantly, these conversation-level components maintain user privacy by focusing on abstract characteristics of the interaction rather than specific content. They allow for rich analysis of user engagement patterns without risking the exposure of sensitive information contained within individual prompts or responses.</p>

<p>By combining these conversation-level insights with the prompt-level orthogonal components discussed earlier, we can build a comprehensive, privacy-respecting framework for understanding and improving LLM interactions.</p>

<h2 id="implementation" class="linked-heading">
    Implementation<a href="#implementation">🔗</a>
</h2>

<p>Implementing a system to analyze prompts based on orthogonal components requires careful consideration of both technical and practical aspects. While the concept is promising, the execution needs to be efficient and cost-effective. Let’s explore a viable implementation strategy that balances accuracy, privacy, and resource utilization.</p>

<p>As noted, training a dedicated model to classify prompts based on the orthogonal components would indeed be a resource-intensive process. The extensive data labeling, cleaning, and refinement required for such an approach could make it prohibitively expensive and time-consuming for many organizations. This method, while potentially very accurate, may not be feasible for teams working with limited budgets or tight timelines.</p>

<p>The alternative approach of using the LLM itself to classify user prompts is more promising, especially when considering the capabilities of advanced models like those offered by OpenAI and Anthropic. The concern about doubling the token usage (and thus the cost) by submitting each prompt twice is valid. However, as suggested, OpenAI’s Structured Output responses provide an easy solution to this problem.</p>

<p>Custom schema responses allow us to define a specific output format that the LLM should follow. By carefully crafting this schema, we can instruct the LLM to not only provide the requested response to the user’s prompt but also to classify the prompt according to our orthogonal components. This approach effectively kills two birds with one stone, obtaining both the user’s desired output and our analytical data in a single API call.</p>

<p>To implement this, we would need to design a JSON schema that includes fields for each of the orthogonal components we want to track. For example, our schema might look something like this:</p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"response"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"components"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"intent"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"string"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"complexity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ambiguity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"specificity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tone"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"domain"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"string"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"cognitiveLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"persona"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"string"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"abstractionLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"creativity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"subjectivity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>With this schema in place, we would then modify our API calls to the LLM to request both the standard response and the prompt analysis. The LLM would process the user’s prompt, generate the appropriate response, and simultaneously classify the prompt according to our specified orthogonal components.</p>

<p>One challenge with this approach is ensuring that the LLM’s classification is consistent and accurate. To address this, we might need to provide clear guidelines or examples for each component as part of our system prompt. We could also implement a calibration process, where we periodically submit known prompts and compare the LLM’s classifications to predetermined benchmarks.</p>

<p>On the backend, we would need to set up a system to store and analyze the classification data. This could involve a database to record the orthogonal component values for each prompt, along with analytics tools to identify patterns and trends over time.  This could be stored efficiently in a set of normalized SQL tables. The key is storing only the component classifications without any of the original prompt text.  This sort of access could be controlled by providing external teams with permissioned <code class="language-plaintext highlighter-rouge">VIEWS</code> that exclude embeddings and original text.</p>

<p>To further enhance privacy, we could implement additional safeguards such as data aggregation and anonymization techniques. For instance, we might only analyze data in batches of a certain size, or use differential privacy methods to add controlled noise to the data, making it impossible to reverse-engineer individual prompts from the stored classifications.</p>

<p>As we accumulate data, we can begin to create user personas based on common patterns in the orthogonal components. This process might involve clustering algorithms to identify groups of users with similar prompt characteristics. These personas can then inform product development decisions, helping to tailor the LLM’s capabilities to the needs and preferences of different user types.</p>

<p>It’s important to note that while this implementation strategy is efficient in terms of API usage, it does rely on the accuracy and consistency of the LLM’s self-classification. Regular auditing and refinement of the classification process may be necessary to ensure the ongoing reliability of the data. Additionally, as LLM capabilities evolve, the classification schema and process may need to be updated to take advantage of new features or to address any emerging biases or inaccuracies.</p>

<h2 id="analysis" class="linked-heading">
    Analysis<a href="#analysis">🔗</a>
</h2>

<p>Analyzing the orthogonal components of user prompts over time can reveal fascinating trends and patterns in LLM usage. This analysis could provide valuable insights into user behavior, evolving needs, and the overall effectiveness of the LLM. Let’s explore some potential avenues for analysis and the trends they might uncover.</p>

<h3 id="sequence-analysis" class="linked-heading">
    Sequence Analysis<a href="#sequence-analysis">🔗</a>
</h3>
<p>By examining sequences of orthogonal component combinations, we could identify common user journeys or task flows. For instance, we might observe that users often start with low-complexity, high-ambiguity prompts in a particular domain, gradually increasing in specificity and complexity as they refine their queries. This could indicate a learning curve as users become more familiar with the LLM’s capabilities.</p>

<h3 id="temporal-patterns" class="linked-heading">
    Temporal Patterns<a href="#temporal-patterns">🔗</a>
</h3>
<p>Analyzing how component combinations change over time (e.g., daily, weekly, or seasonally) could reveal interesting usage patterns. We might find that creativity and abstraction levels peak during certain times of day, possibly correlating with when users are engaged in brainstorming or creative tasks. Conversely, we might see a trend towards high-specificity, low-ambiguity prompts during typical working hours, suggesting task-oriented usage.</p>

<h3 id="domain-clustering" class="linked-heading">
    Domain Clustering<a href="#domain-clustering">🔗</a>
</h3>
<p>By clustering prompts based on their domain component, we could identify which subject areas are most popular among users. This analysis might reveal unexpected connections between domains, such as users frequently transitioning between seemingly unrelated fields. Such insights could inform interdisciplinary applications of the LLM.</p>

<h3 id="cognitive-progression" class="linked-heading">
    Cognitive Progression<a href="#cognitive-progression">🔗</a>
</h3>
<p>Tracking the cognitive level component over time for individual users or user groups could show how interaction with the LLM impacts cognitive engagement. We might observe a trend towards higher cognitive levels as users become more adept at formulating complex queries, indicating that the LLM is effectively supporting intellectual growth.</p>

<h3 id="persona-evolution" class="linked-heading">
    Persona Evolution<a href="#persona-evolution">🔗</a>
</h3>
<p>By analyzing how the persona component changes across user sessions, we could gain insights into how users perceive and interact with AI. A trend towards more frequent use of specific personas might suggest that users are becoming more comfortable with role-playing scenarios or are finding particular personas especially helpful for certain tasks.</p>

<h3 id="ambiguity-and-specificity-correlation" class="linked-heading">
    Ambiguity and Specificity Correlation<a href="#ambiguity-and-specificity-correlation">🔗</a>
</h3>
<p>Examining the relationship between ambiguity and specificity over time could reveal how users learn to formulate more effective prompts. We might see a general trend towards lower ambiguity and higher specificity as users become more experienced, potentially indicating improved communication between humans and AI.</p>

<h3 id="creativity-cycles" class="linked-heading">
    Creativity Cycles<a href="#creativity-cycles">🔗</a>
</h3>
<p>Analyzing the creativity component across user bases could uncover cycles or triggers for creative use of the LLM. This might reveal specific combinations of other components that tend to precede or accompany high-creativity prompts, offering insights into the conditions that foster innovative thinking.</p>

<h3 id="tone-adaptation" class="linked-heading">
    Tone Adaptation<a href="#tone-adaptation">🔗</a>
</h3>
<p>Tracking changes in the tone component could show how users adapt their communication style with the LLM over time. We might observe users converging on particular tones that they find most effective, or adapting their tone based on the complexity or domain of their queries.</p>

<p>By analyzing these trends and patterns, organizations could gain a deeper understanding of how the user’s approach to using the LLM changes over time. This information could drive decisions about model updates, feature development, and user education initiatives. For example, if analysis reveals that users struggle to reduce ambiguity in certain domains, this could prompt the development of more targeted help features or refinements to the LLM’s ability to handle ambiguous queries in those areas.</p>

<h3 id="wisdom-of-the-crowd" class="linked-heading">
    Wisdom of the Crowd<a href="#wisdom-of-the-crowd">🔗</a>
</h3>

<p>We can extend our focus to compare trajectories across user conversations and between different users. This comparative analysis opens up new possibilities for clustering, similarity measurement, and pattern recognition on a broader scale.</p>

<h3 id="cross-conversation-analysis" class="linked-heading">
    Cross-Conversation Analysis<a href="#cross-conversation-analysis">🔗</a>
</h3>
<p>By examining the trajectories of orthogonal component combinations across multiple conversations for a single user, we can identify consistent patterns in their LLM interaction style. For instance, we might discover that a user typically begins conversations with high-ambiguity, low-specificity prompts, gradually increasing in complexity and specificity as the conversation progresses. This could indicate a preference for exploratory dialogue followed by focused inquiry.</p>

<h3 id="user-similarity-clustering" class="linked-heading">
    User Similarity Clustering<a href="#user-similarity-clustering">🔗</a>
</h3>
<p>Comparing these conversation trajectories between users allows us to cluster individuals with similar interaction patterns. We could employ techniques like Dynamic Time Warping (DTW) to measure the similarity between conversation sequences, accounting for variations in conversation length and pacing. This clustering could reveal distinct user archetypes, such as “Methodical Researchers” who consistently progress from broad to narrow queries, or “Creative Explorers” who maintain high levels of ambiguity and creativity throughout their interactions.</p>

<h3 id="divergence-analysis" class="linked-heading">
    Divergence Analysis<a href="#divergence-analysis">🔗</a>
</h3>
<p>By measuring how quickly user trajectories diverge from one another, we can gain insights into the diversity of user needs and interaction styles. Rapid divergence might indicate a user base with varied expertise levels or diverse use cases, while slower divergence could suggest more homogeneous user behavior. This information could be valuable for tailoring the LLM’s capabilities to support a wide range of interaction styles or for identifying underserved user segments.</p>

<h3 id="temporal-cohort-comparison" class="linked-heading">
    Temporal Cohort Comparison<a href="#temporal-cohort-comparison">🔗</a>
</h3>
<p>Grouping users into cohorts based on when they first started using the LLM allows us to compare trajectory evolution over time. We might find that newer users show more erratic patterns in their orthogonal component combinations, while long-term users demonstrate more stable and efficient trajectories. This could provide insights into the learning curve associated with LLM use and inform onboarding strategies.</p>

<h3 id="cross-domain-transfer" class="linked-heading">
    Cross-Domain Transfer<a href="#cross-domain-transfer">🔗</a>
</h3>
<p>Analyzing how users’ trajectories change when they switch between domains can reveal interesting patterns of knowledge transfer. We might observe that users who exhibit high complexity and low ambiguity in one domain quickly achieve similar levels in new domains, suggesting effective cross-domain learning. Conversely, consistent drops in complexity when switching domains could indicate areas where additional user support is needed.</p>

<h3 id="collaborative-pattern-analysis" class="linked-heading">
    Collaborative Pattern Analysis<a href="#collaborative-pattern-analysis">🔗</a>
</h3>
<p>For LLMs used in collaborative settings, we can analyze how the trajectories of multiple users interacting on the same project align or diverge. This could uncover effective collaboration patterns, such as complementary query styles that lead to more comprehensive project outcomes.</p>

<h3 id="anomaly-detection" class="linked-heading">
    Anomaly Detection<a href="#anomaly-detection">🔗</a>
</h3>
<p>By establishing baseline trajectories for different user types, we can implement anomaly detection to identify unusual interaction patterns. This could be useful for detecting potential misuse of the LLM, identifying users who might benefit from additional support, or discovering innovative use cases that deviate from the norm.</p>

<h3 id="predictive-modeling" class="linked-heading">
    Predictive Modeling<a href="#predictive-modeling">🔗</a>
</h3>
<p>Using historical trajectory data, we can build predictive models to anticipate future user needs based on the early stages of their conversation trajectories. This could enable proactive adjustments to the LLM’s behavior or the suggestion of relevant resources to support the user’s likely trajectory.</p>

<p>By leveraging these comparative analyses, teams/organizations can better understand their user base and how individuals interact with their LLM over time. This can contribute to live personalization strategies and inform feature development.</p>

<h2 id="next-steps" class="linked-heading">
    Next Steps<a href="#next-steps">🔗</a>
</h2>

<p>The field of LLM analytics is rapidly evolving, and the approach we’ve discussed using orthogonal components is just the beginning. As we look to the future, several intriguing possibilities emerge for advancing LLM analytics while maintaining user privacy. Here are some speculative next steps:</p>

<ol>
  <li><strong>Dynamic Component Analysis</strong>: Future systems might adaptively modify the orthogonal components based on observed patterns. This could involve automatically identifying new relevant components or refining existing ones to better capture emerging user behaviors and LLM capabilities.</li>
  <li><strong>Multi-Modal Analytics</strong>: As LLMs increasingly integrate with other AI systems (like image recognition or speech processing), analytics might expand to cover these multi-modal interactions. This could involve developing new orthogonal components specific to visual or auditory prompts.</li>
  <li><strong>Temporal Pattern Recognition</strong>: Advanced analytics might focus on identifying patterns in user behavior over time. This could involve tracking how individual users or user groups evolve in their interaction styles, potentially revealing learning curves or changing preferences in AI engagement.</li>
  <li><strong>Federated Learning for Analytics</strong>: To further enhance privacy, federated learning techniques could be applied to LLM analytics. This would allow multiple organizations to collaboratively improve their analytical models without sharing raw data, only aggregated insights.</li>
  <li><strong>Prompt Optimization Feedback Loops</strong>: Analytics data could be used to create systems that automatically suggest improvements to user prompts. This could help users more effectively engage with LLMs while also providing valuable data on prompt refinement patterns.</li>
  <li><strong>Emotional Intelligence Analytics</strong>: Future systems might attempt to gauge the emotional state or satisfaction level of users based on their prompts and interactions. This could involve developing new components related to user sentiment or engagement levels.</li>
  <li><strong>Cross-Platform Behavior Analysis</strong>: As LLMs become more ubiquitous, analytics might expand to track how users interact differently with various LLM implementations across different platforms or use cases.</li>
  <li><strong>Predictive Analytics for LLM Interactions</strong>: Advanced systems might use historical data to predict future user behaviors or needs, allowing for proactive improvements to LLM capabilities or user interfaces.</li>
</ol>

<p>These potential advancements in LLM analytics could deepen our understanding of how humans interact with AI systems, driving improvements in LLM technology while maintaining the trust of end-users. As with any technology that is not yet mature, ethical considerations and transparency are crucial.</p>]]></content><author><name>Doug Fenstermacher</name></author><category term="blog" /><category term="llm" /><category term="data" /><summary type="html"><![CDATA[Learn how to implement effective prompt analytics for large language models while safeguarding user privacy. Discover key strategies, best practices, and ethical considerations in this guide.]]></summary></entry><entry><title type="html">Neural Variational Document Models with PyTorch for Topic Extraction</title><link href="https://dougfenstermacher.com/experiment/nlp-neural-variational-document-model" rel="alternate" type="text/html" title="Neural Variational Document Models with PyTorch for Topic Extraction" /><published>2024-05-15T00:00:00+00:00</published><updated>2024-05-15T00:00:00+00:00</updated><id>https://dougfenstermacher.com/experiment/nlp-neural-variational-document-model</id><content type="html" xml:base="https://dougfenstermacher.com/experiment/nlp-neural-variational-document-model"><![CDATA[<p>Neural Variational Document Models (NVDMs) are a class of unsupervised learning models that leverage the power of deep learning and variational inference to uncover latent topics and semantic structures within a collection of documents. These models combine the expressiveness of neural networks with the probabilistic framework of latent variable models, enabling them to capture complex relationships and generate meaningful representations of text data.</p>

<p>At their core, NVDMs are auto-encoders, consisting of an encoder-decoder architecture where the encoder maps the input documents to a continuous latent space, and the decoder reconstructs the original documents from the latent representations. The encoder is typically a neural network, such as a Convolutional Neural Network (CNN) or a Recurrent Neural Network (RNN), which learns to compress the input documents into a lower-dimensional latent space. The decoder, on the other hand, is responsible for generating the words in the documents based on the latent representations.</p>

<p>The key innovation in NVDMs is the incorporation of variational inference, which allows the model to learn a probabilistic distribution over the latent space. By introducing a variational posterior distribution, NVDMs can capture the uncertainty associated with the latent representations and enable more robust and interpretable topic modeling. The objective of training an NVDM is to maximize the evidence lower bound (ELBO), which consists of a reconstruction term and a regularization term that encourages the variational posterior to be close to a prior distribution, typically a multivariate Gaussian.</p>

<p>NVDMs have found numerous applications in natural language processing tasks, such as topic modeling, document clustering, text generation, and sentiment analysis. By learning rich and interpretable representations of documents, NVDMs enable users to gain insights into the underlying themes and semantic structures within their text data. Moreover, the ability to generate new documents from the learned latent space opens up possibilities for content creation and data augmentation.</p>

<h2 id="fundamentals-of-neural-variational-document-models" class="linked-heading">
    Fundamentals of Neural Variational Document Models<a href="#fundamentals-of-neural-variational-document-models">🔗</a>
</h2>

<p>Neural Variational Document Models (NVDMs) are built upon three key concepts: latent variable models, variational inference, and the encoder-decoder architecture.</p>

<h3 id="latent-variable-models" class="linked-heading">
    Latent Variable Models<a href="#latent-variable-models">🔗</a>
</h3>

<p>Latent variable models are probabilistic models that introduce unobserved (latent) variables to capture the underlying structure and dependencies in the observed data. In the context of document modeling, latent variables can represent abstract concepts, such as topics or themes, that influence the generation of words in a document. By learning the latent variables, NVDMs can uncover the hidden semantic structure within a collection of documents.</p>

<p>The joint probability distribution of an NVDM can be expressed as:</p>

<p><code class="language-plaintext highlighter-rouge">p(x, z) = p(z) * p(x|z)</code></p>

<p>where x represents the observed document, z represents the latent variables, <code class="language-plaintext highlighter-rouge">p(z)</code> is the prior distribution over the latent variables, and <code class="language-plaintext highlighter-rouge">p(x|z)</code> is the conditional probability of the document given the latent variables.</p>

<p>Variational Inference:
Variational inference is a technique used to approximate intractable posterior distributions in probabilistic models. In NVDMs, the true posterior distribution <code class="language-plaintext highlighter-rouge">p(z|x)</code> is often computationally intractable due to the complexity of the model. Variational inference introduces a variational posterior distribution <code class="language-plaintext highlighter-rouge">q(z|x)</code>, which is a simpler and more tractable distribution that approximates the true posterior.</p>

<p>The goal of variational inference is to minimize the Kullback-Leibler (KL) divergence between the variational posterior and the true posterior:</p>

<p><code class="language-plaintext highlighter-rouge">KL(q(z|x) || p(z|x))</code></p>

<p>By minimizing the KL divergence, the variational posterior is encouraged to be close to the true posterior, enabling efficient inference and learning in NVDMs.</p>

<h3 id="encoder-decoder-architecture" class="linked-heading">
    Encoder-Decoder Architecture<a href="#encoder-decoder-architecture">🔗</a>
</h3>

<p>NVDMs employ an encoder-decoder architecture to learn the latent representations and generate documents. The encoder is a neural network that takes the input document x and maps it to a continuous latent space, producing the parameters of the variational posterior distribution <code class="language-plaintext highlighter-rouge">q(z|x)</code>. The encoder can be implemented using various neural network architectures, such as Convolutional Neural Networks (CNNs) or Recurrent Neural Networks (RNNs), depending on the nature of the text data.</p>

<p>The decoder, on the other hand, is responsible for generating the words in the document based on the latent representation z sampled from the variational posterior. The decoder is typically a neural network that models the conditional probability distribution <code class="language-plaintext highlighter-rouge">p(x|z)</code>. Common choices for the decoder include RNNs or feed-forward neural networks with a softmax output layer to generate the probability distribution over the vocabulary.</p>

<p>During training, the encoder and decoder are jointly optimized to maximize the evidence lower bound (ELBO), which consists of two terms:</p>

<p><code class="language-plaintext highlighter-rouge">ELBO = E[log p(x|z)] - KL(q(z|x) || p(z))</code></p>

<p>The first term is the expected log-likelihood of the document given the latent variables, encouraging the decoder to generate documents similar to the input. The second term is the KL divergence between the variational posterior and the prior distribution, acting as a regularizer to prevent overfitting and promote a smooth latent space.  By leveraging the power of latent variable models, variational inference, and the encoder-decoder architecture, NVDMs can learn meaningful and interpretable representations of documents, enabling various downstream tasks such as topic modeling, document clustering, and text generation.</p>

<h3 id="reparameterization-trick" class="linked-heading">
    Reparameterization trick<a href="#reparameterization-trick">🔗</a>
</h3>

<p>In the context of NVDMs, the goal is to learn a variational posterior distribution <code class="language-plaintext highlighter-rouge">q(z|x)</code> that approximates the true posterior distribution <code class="language-plaintext highlighter-rouge">p(z|x)</code> over the latent variables z given the input document x. However, directly sampling from the variational posterior during the forward pass of the model would block the gradients, as the sampling operation is not differentiable.</p>

<p>The reparameterization trick addresses this issue by separating the sampling process into two parts: a deterministic part and a stochastic part. Instead of directly sampling from the variational posterior <code class="language-plaintext highlighter-rouge">q(z|x)</code>, which is typically parameterized as a Gaussian distribution with mean <code class="language-plaintext highlighter-rouge">μ</code> and standard deviation <code class="language-plaintext highlighter-rouge">σ</code>, the trick introduces an auxiliary noise variable <code class="language-plaintext highlighter-rouge">ε</code> drawn from a standard Gaussian distribution <code class="language-plaintext highlighter-rouge">N(0, 1)</code>.</p>

<p>The reparameterization is as follows:</p>

<p><code class="language-plaintext highlighter-rouge">z = μ + σ * ε, where ε ~ N(0, 1)</code></p>

<p>By expressing the sampling process in this way, the gradients can flow through the deterministic parts (<code class="language-plaintext highlighter-rouge">μ</code> and <code class="language-plaintext highlighter-rouge">σ</code>) of the reparameterization, while the stochastic part (<code class="language-plaintext highlighter-rouge">ε</code>) remains independent of the model parameters.</p>

<p>By using the reparameterization trick, the NVDM can backpropagate gradients through the sampling process and optimize the variational parameters (<code class="language-plaintext highlighter-rouge">μ</code> and <code class="language-plaintext highlighter-rouge">σ</code>) using standard gradient descent methods. This allows the model to learn a meaningful latent representation of the input documents while effectively approximating the true posterior distribution.</p>

<p>The reparameterization trick has been widely adopted in variational inference and generative models, as it enables efficient training and inference in models with continuous latent variables. It has been instrumental in the success of VAEs and their variants, including NVDMs, in various domains such as computer vision, natural language processing, and recommendation systems.</p>

<h2 id="advantages-of-nvdms-over-traditional-topic-modeling-techniques" class="linked-heading">
    Advantages of NVDMs over Traditional Topic Modeling Techniques<a href="#advantages-of-nvdms-over-traditional-topic-modeling-techniques">🔗</a>
</h2>

<p>Neural Variational Document Models (NVDMs) offer several advantages over traditional topic modeling techniques, such as Latent Dirichlet Allocation (LDA) and Probabilistic Latent Semantic Analysis (PLSA). These advantages stem from the ability of NVDMs to leverage deep learning architectures and variational inference, enabling them to capture complex semantic relationships, handle large-scale datasets, and provide flexibility in model design. Let’s explore each of these advantages in more detail.</p>

<p>Capturing Complex Semantic Relationships:
Traditional topic modeling techniques, such as LDA and PLSA, rely on bag-of-words representations and assume a simple generative process for document generation. They often struggle to capture the complex semantic relationships and dependencies between words and topics. In contrast, NVDMs utilize deep learning architectures, such as Convolutional Neural Networks (CNNs) or Recurrent Neural Networks (RNNs), which can learn rich and expressive representations of text data.</p>

<p>By employing neural networks, NVDMs can automatically learn intricate patterns and dependencies in the text, capturing non-linear relationships between words and topics. The encoder in an NVDM can extract meaningful features and contextual information from the input documents, allowing the model to uncover more nuanced and semantically coherent topics. This ability to capture complex semantic relationships enables NVDMs to generate more interpretable and meaningful topic representations compared to traditional techniques.</p>

<p>Traditional topic modeling techniques often face scalability challenges when dealing with large-scale datasets. As the number of documents and vocabulary size increase, the computational complexity of these techniques grows exponentially, making them impractical for handling massive text corpora. NVDMs, on the other hand, can efficiently handle large-scale datasets thanks to their integration with deep learning frameworks like PyTorch.</p>

<p>NVDMs can leverage the parallel processing capabilities of modern hardware, such as GPUs, to speed up training and inference. The mini-batch training paradigm used in deep learning allows NVDMs to process documents in parallel, enabling efficient learning even on large datasets. Moreover, the stochastic nature of variational inference in NVDMs enables them to approximate the posterior distribution using subsamples of the data, further improving scalability.</p>

<p>PyTorch, with its optimized tensor operations and built-in support for GPU acceleration, provides a powerful platform for training NVDMs on large-scale datasets. The framework’s efficient memory management and ability to handle sparse computations make it well-suited for processing text data, which often exhibits high dimensionality and sparsity.</p>

<p>NVDMs offer a high degree of flexibility in model architecture, allowing researchers and practitioners to design models tailored to their specific needs and domains. The encoder-decoder architecture of NVDMs can be easily customized and extended to incorporate various neural network components and techniques.</p>

<p>For example, the encoder can be implemented using different neural network architectures, such as CNNs for capturing local patterns or RNNs for modeling sequential dependencies. The choice of architecture can be based on the characteristics of the text data and the desired level of complexity. Similarly, the decoder can be designed to generate words based on different probability distributions or incorporate additional information, such as document metadata or linguistic features.</p>

<p>PyTorch’s modular and flexible design makes it easy to experiment with different architectures and components in NVDMs. The framework provides a wide range of pre-built neural network modules, loss functions, and optimization algorithms, allowing users to quickly prototype and iterate on their models. The dynamic computational graph in PyTorch enables dynamic model construction and modification, facilitating the development of custom NVDM architectures.</p>

<p>Furthermore, NVDMs can be extended to incorporate additional features or constraints, such as prior knowledge, multi-modal information, or semi-supervised learning objectives. The flexibility in model architecture enables researchers to explore novel variations and adaptations of NVDMs, pushing the boundaries of topic modeling and uncovering new insights from text data.</p>

<h2 id="implementation" class="linked-heading">
    Implementation<a href="#implementation">🔗</a>
</h2>

<p>This implementation provides a solid foundation for training and utilizing an NVDM for topic modeling tasks. By leveraging PyTorch’s powerful features and the encoder-decoder architecture, the model can learn meaningful latent representations and generate interpretable topics from a collection of documents.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="nn">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>


<span class="k">class</span> <span class="nc">NVDM</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tokenizer</span><span class="p">,</span> <span class="n">embed_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">,</span> <span class="n">latent_dim</span><span class="p">,</span> <span class="n">dropout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">num_layers</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">uniqueness_weight</span><span class="o">=</span><span class="mf">0.0</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">(</span><span class="n">NVDM</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">tokenizer</span> <span class="o">=</span> <span class="n">tokenizer</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">vocab_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">.</span><span class="n">vocabulary</span><span class="p">())</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">embed_dim</span> <span class="o">=</span> <span class="n">embed_dim</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">hidden_dim</span> <span class="o">=</span> <span class="n">hidden_dim</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">latent_dim</span> <span class="o">=</span> <span class="n">latent_dim</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">uniqueness_weight</span> <span class="o">=</span> <span class="n">uniqueness_weight</span>
        <span class="c1"># Encoder MLP
</span>        <span class="n">encoder_layers</span> <span class="o">=</span> <span class="p">[</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">vocab_size</span><span class="p">,</span> <span class="n">embed_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">BatchNorm1d</span><span class="p">(</span><span class="n">embed_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">LeakyReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="n">dropout</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">embed_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">BatchNorm1d</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">LeakyReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="n">dropout</span><span class="p">)</span>
            <span class="p">]</span>
        
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_layers</span><span class="p">):</span>
            <span class="n">encoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">))</span>
            <span class="n">encoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">BatchNorm1d</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">))</span>
            <span class="n">encoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">LeakyReLU</span><span class="p">())</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="o">*</span><span class="n">encoder_layers</span>
        <span class="p">)</span>

        <span class="c1"># intermediate representation
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">mu</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">latent_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">log_var</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">latent_dim</span><span class="p">)</span>

        <span class="c1"># Decoder MLP
</span>        <span class="n">decoder_layers</span> <span class="o">=</span> <span class="p">[</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">latent_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">BatchNorm1d</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">LeakyReLU</span><span class="p">()</span>
        <span class="p">]</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_layers</span><span class="p">):</span>
            <span class="n">decoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">))</span>
            <span class="n">decoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">BatchNorm1d</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">))</span>
            <span class="n">decoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">LeakyReLU</span><span class="p">())</span>
            <span class="n">decoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="n">dropout</span><span class="p">))</span>

        <span class="n">decoder_layers</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">latent_dim</span><span class="p">))</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span><span class="o">*</span><span class="n">decoder_layers</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">dropout_layer</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Dropout</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="n">dropout</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">decoder_output</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">latent_dim</span><span class="p">,</span> <span class="n">embed_dim</span><span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">output</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">embed_dim</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">vocab_size</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">output</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">normal_</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.01</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">output</span><span class="p">.</span><span class="n">bias</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">fill_</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">encode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">mu</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">mu</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
        <span class="n">log_var</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">log_var</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">mu</span><span class="p">,</span> <span class="n">log_var</span>

    <span class="k">def</span> <span class="nf">reparameterize</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">mu</span><span class="p">,</span> <span class="n">log_var</span><span class="p">):</span>
        <span class="n">std</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="mf">0.5</span> <span class="o">*</span> <span class="n">log_var</span><span class="p">)</span>
        <span class="n">eps</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">randn_like</span><span class="p">(</span><span class="n">std</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">mu</span> <span class="o">+</span> <span class="n">eps</span> <span class="o">*</span> <span class="n">std</span>

    <span class="k">def</span> <span class="nf">decode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">z</span><span class="p">):</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">dropout_layer</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">decoder_output</span><span class="p">(</span><span class="n">z</span><span class="p">))</span>
        <span class="n">logits</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">output</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">logits</span>
    
    <span class="k">def</span> <span class="nf">topic_word_distribution</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">word_embeddings</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">output</span><span class="p">.</span><span class="n">weight</span>

        <span class="c1"># Compute the topic-word scores
</span>        <span class="n">topic_word_scores</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">decoder_output</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="n">t</span><span class="p">(),</span> <span class="n">word_embeddings</span><span class="p">.</span><span class="n">t</span><span class="p">())</span>
        <span class="n">topic_word_probabilities</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">topic_word_scores</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">topic_word_probabilities</span>
        
    <span class="k">def</span> <span class="nf">topic_words</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span>
        <span class="n">topic_word_distributions</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">topic_word_distribution</span><span class="p">()</span>

        <span class="c1"># Get the top k words for each topic
</span>        <span class="n">top_probs</span><span class="p">,</span> <span class="n">top_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">topk</span><span class="p">(</span><span class="n">topic_word_distributions</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">top_k</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="n">topic_words</span> <span class="o">=</span> <span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">tokenizer</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">indices</span><span class="p">.</span><span class="n">tolist</span><span class="p">())</span> <span class="k">for</span> <span class="n">indices</span> <span class="ow">in</span> <span class="n">top_indices</span><span class="p">]</span>
        <span class="n">topic_probabilities</span> <span class="o">=</span> <span class="p">[</span><span class="n">probs</span><span class="p">.</span><span class="n">tolist</span><span class="p">()</span> <span class="k">for</span> <span class="n">probs</span> <span class="ow">in</span> <span class="n">top_probs</span><span class="p">]</span>
        <span class="k">return</span> <span class="n">topic_words</span><span class="p">,</span> <span class="n">topic_probabilities</span>
    
    
    <span class="k">def</span> <span class="nf">topic_uniqueness_loss</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">batch_word_probabilities</span><span class="p">):</span>
        <span class="c1"># Normalize word probabilities to get document distributions
</span>        <span class="n">document_distributions</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">normalize</span><span class="p">(</span><span class="n">batch_word_probabilities</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Calculate the topic-word distribution matrix
</span>        <span class="n">topic_word_dist</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">topic_word_distribution</span><span class="p">().</span><span class="n">t</span><span class="p">()</span>

        <span class="c1"># Calculate the document-topic distribution matrix
</span>        <span class="n">document_topic_dist</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">document_distributions</span><span class="p">,</span> <span class="n">topic_word_dist</span><span class="p">)</span>

        <span class="c1"># Calculate the topic co-occurrence matrix
</span>        <span class="n">topic_cooccurrence_matrix</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">document_topic_dist</span><span class="p">.</span><span class="n">t</span><span class="p">(),</span> <span class="n">document_topic_dist</span><span class="p">)</span>

        <span class="c1"># Normalize the topic co-occurrence matrix
</span>        <span class="n">topic_cooccurrence_matrix</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">normalize</span><span class="p">(</span><span class="n">topic_cooccurrence_matrix</span><span class="p">,</span> <span class="n">p</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Calculate the average pairwise similarity between topics
</span>        <span class="n">num_topics</span> <span class="o">=</span> <span class="n">topic_cooccurrence_matrix</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
        <span class="n">topic_similarity</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">topic_cooccurrence_matrix</span><span class="p">)</span> <span class="o">-</span> <span class="n">num_topics</span>
        <span class="n">avg_topic_similarity</span> <span class="o">=</span> <span class="n">topic_similarity</span> <span class="o">/</span> <span class="p">(</span><span class="n">num_topics</span> <span class="o">*</span> <span class="p">(</span><span class="n">num_topics</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span>

        <span class="k">return</span> <span class="n">avg_topic_similarity</span>

    <span class="k">def</span> <span class="nf">kullback_liebler_loss</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">logits</span><span class="p">):</span>
        <span class="c1"># Normalize the document representations to get probability distributions
</span>        <span class="n">original_dist</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">reconstructed_dist</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        
        <span class="c1"># Calculate KL divergence loss
</span>        <span class="k">return</span> <span class="n">F</span><span class="p">.</span><span class="n">kl_div</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">reconstructed_dist</span><span class="p">),</span> <span class="n">original_dist</span><span class="p">,</span> <span class="n">reduction</span><span class="o">=</span><span class="s">'batchmean'</span><span class="p">)</span>
    

    <span class="k">def</span> <span class="nf">reconstruction_loss</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">logits</span><span class="p">):</span>
        <span class="c1"># Calculate reconstruction/log-likehihood loss
</span>        <span class="n">probs</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">log_softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">batch_size</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
        <span class="k">return</span> <span class="o">-</span><span class="n">torch</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">probs</span><span class="p">)</span> <span class="o">/</span> <span class="n">batch_size</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">documents</span><span class="p">):</span>
        <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">stack</span><span class="p">([</span><span class="bp">self</span><span class="p">.</span><span class="n">tokenizer</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">document</span><span class="p">)</span> <span class="k">for</span> <span class="n">document</span> <span class="ow">in</span> <span class="n">documents</span><span class="p">])</span>
        <span class="n">mu</span><span class="p">,</span> <span class="n">log_var</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">z</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">reparameterize</span><span class="p">(</span><span class="n">mu</span><span class="p">,</span> <span class="n">log_var</span><span class="p">)</span>
        <span class="n">logits</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
        <span class="n">recon_loss</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">reconstruction_loss</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">logits</span><span class="p">)</span>
        <span class="n">kl_loss</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">kullback_liebler_loss</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">logits</span><span class="p">)</span>

        <span class="n">topic_uniqueness_loss</span> <span class="o">=</span> <span class="mf">0.0</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">uniqueness_weight</span><span class="p">:</span>
            <span class="n">batch_word_probabilities</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
            <span class="n">uniqueness_loss</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">topic_uniqueness_loss</span><span class="p">(</span><span class="n">batch_word_probabilities</span><span class="p">)</span>
            <span class="c1">#print('uniqueness_loss: ', self.uniqueness_weight, ' * ', uniqueness_loss, flush=True)
</span>            <span class="n">topic_uniqueness_loss</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">uniqueness_weight</span> <span class="o">*</span> <span class="n">uniqueness_loss</span>
        
        <span class="k">return</span> <span class="n">recon_loss</span> <span class="o">+</span> <span class="n">kl_loss</span> <span class="o">+</span> <span class="n">topic_uniqueness_loss</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The constructor takes various parameters such as the tokenizer, embedding dimension, hidden dimension, latent dimension, dropout rate, and number of layers. Both the encoder and decoder are implemented as multi-layer perceptrons (MLPs) using PyTorch’s nn.Sequential module.  The encoder MLP maps the input document representation to a hidden representation and then to the parameters (mean and log-variance) of the variational posterior distribution.  The decoder MLP takes a sample from the variational posterior and reconstructs the original document representation.</p>

<p>The loss implementation includes three loss functions: reconstruction loss, KL divergence loss, and topic uniqueness loss.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">reconstruction_loss</code> method computes the negative log-likelihood of the reconstructed document representation given the original document representation.</li>
  <li><code class="language-plaintext highlighter-rouge">kullback_liebler_loss</code> method calculates the KL divergence between the original document distribution and the reconstructed document distribution.</li>
  <li><code class="language-plaintext highlighter-rouge">topic_uniqueness_loss</code> method computes the average pairwise similarity between topics based on the topic co-occurrence matrix, encouraging the model to learn distinct topics.</li>
</ul>

<p>In natural language processing (NLP) tasks, such as topic modeling with NVDMs, it is essential to represent text documents in a format that can be easily processed and understood by machine learning models. One common approach is to use the Bag-of-Words (BoW) representation.</p>

<p>The Bag-of-Words representation treats each document as a collection of words, disregarding the order and grammatical structure of the words in the document. It represents a document as a vector, where each element corresponds to a unique word in the vocabulary, and the value of the element indicates the presence or frequency of that word in the document.</p>

<p>Here are a few reasons why the Bag-of-Words representation is necessary:</p>

<ul>
  <li>Simplicity: BoW is a simple and intuitive way to represent text documents. It captures the essential information about the presence and frequency of words in a document, which is often sufficient for many NLP tasks, including topic modeling.
Fixed-length representation: BoW converts variable-length text documents into fixed-length numerical vectors. This is crucial for machine learning models, which typically require input data to have a consistent shape and size.</li>
  <li>Computationally efficient: BoW representation is computationally efficient to generate and process. It allows for fast training and inference of models, especially when dealing with large datasets.</li>
  <li>Compatibility with machine learning algorithms: Many machine learning algorithms, including NVDMs, are designed to work with numerical data. The BoW representation converts text documents into numerical vectors, making them compatible with these algorithms.</li>
</ul>

<p>However, it’s important to note that the BoW representation has some limitations. It ignores the order and context of words, which can lead to the loss of semantic information. More advanced techniques, such as word embeddings (e.g., Word2Vec, GloVe), can capture semantic relationships between words and provide more meaningful representations.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">from</span> <span class="nn">torch.utils.data</span> <span class="kn">import</span> <span class="n">Dataset</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span><span class="p">,</span> <span class="n">Counter</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">csv</span>


<span class="k">class</span> <span class="nc">CSVDataset</span><span class="p">(</span><span class="n">Dataset</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">text_field</span><span class="p">,</span> <span class="n">label_field</span><span class="p">,</span> <span class="n">load</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">min_df</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">max_df</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="s">'inf'</span><span class="p">),</span> <span class="n">min_tf</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">max_tf</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="s">'inf'</span><span class="p">)):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">path</span> <span class="o">=</span> <span class="n">path</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">text_field</span> <span class="o">=</span> <span class="n">text_field</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">label_field</span> <span class="o">=</span> <span class="n">label_field</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">min_tf</span> <span class="o">=</span> <span class="n">min_tf</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">max_tf</span> <span class="o">=</span> <span class="n">max_tf</span>
        <span class="k">if</span> <span class="n">load</span><span class="p">:</span>
            <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
                    <span class="bp">self</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">))</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">term_frequencies</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">total_tokens</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">document_frequencies</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">total_documents</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">process_corpus</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">text_field</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">min_df</span> <span class="o">=</span> <span class="n">min_df</span>
        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">min_df</span><span class="p">,</span> <span class="nb">float</span><span class="p">)</span> <span class="ow">and</span> <span class="mf">0.0</span> <span class="o">&lt;=</span> <span class="n">min_df</span> <span class="o">&lt;</span> <span class="mf">1.0</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">min_df</span> <span class="o">=</span> <span class="n">min_df</span> <span class="o">*</span> <span class="bp">self</span><span class="p">.</span><span class="n">total_documents</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">max_df</span> <span class="o">=</span> <span class="n">max_df</span>
        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">max_df</span><span class="p">,</span> <span class="nb">float</span><span class="p">)</span> <span class="ow">and</span> <span class="mf">0.0</span> <span class="o">&lt;=</span> <span class="n">max_df</span> <span class="o">&lt;</span> <span class="mf">1.0</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">max_df</span> <span class="o">=</span> <span class="n">max_df</span> <span class="o">*</span> <span class="bp">self</span><span class="p">.</span><span class="n">total_documents</span>

    <span class="k">def</span> <span class="nf">process_corpus</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">field</span><span class="p">):</span>
        <span class="n">term_frequencies</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">()</span>
        <span class="n">document_frequencies</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">()</span>
        <span class="n">total_documents</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">total_tokens</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
            <span class="n">total_tokens</span> <span class="o">=</span> <span class="mi">0</span>
            <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">):</span>
                <span class="n">doc</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="n">field</span><span class="p">]</span>
                <span class="n">tokens</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">split</span><span class="p">()</span>
                <span class="n">term_frequencies</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">tokens</span><span class="p">)</span>
                <span class="n">document_frequencies</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">tokens</span><span class="p">))</span>
                <span class="n">total_tokens</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tokens</span><span class="p">)</span>
                <span class="n">total_documents</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="k">return</span> <span class="n">term_frequencies</span><span class="p">,</span> <span class="n">total_tokens</span><span class="p">,</span> <span class="n">document_frequencies</span><span class="p">,</span> <span class="n">total_documents</span>
    
    <span class="k">def</span> <span class="nf">is_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">document_frequencies</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">min_df</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">document_frequencies</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">max_df</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">term_frequencies</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">min_tf</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">term_frequencies</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">max_tf</span>

    <span class="k">def</span> <span class="nf">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">:</span>
            <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">)</span>

        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">reader</span><span class="p">(</span><span class="n">input_file</span><span class="p">)):</span>
                <span class="k">pass</span>
        <span class="k">return</span> <span class="n">index</span> <span class="o">+</span> <span class="mi">1</span>

    <span class="k">def</span> <span class="nf">__getitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">idx</span><span class="p">):</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">:</span>
            <span class="n">text</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">idx</span><span class="p">][</span><span class="bp">self</span><span class="p">.</span><span class="n">text_field</span><span class="p">]</span>
            <span class="n">label</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">idx</span><span class="p">][</span><span class="bp">self</span><span class="p">.</span><span class="n">label_field</span><span class="p">]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">text</span> <span class="o">=</span> <span class="bp">None</span>
            <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
                <span class="k">for</span> <span class="n">file_idx</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">)):</span>
                    <span class="k">if</span> <span class="n">file_idx</span> <span class="o">==</span> <span class="n">idx</span><span class="p">:</span>
                        <span class="n">text</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">text_field</span><span class="p">]</span>
                        <span class="n">label</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">label_field</span><span class="p">]</span>
                        <span class="k">break</span>
        <span class="k">return</span> <span class="n">text</span><span class="p">,</span> <span class="n">label</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Advantages of the <code class="language-plaintext highlighter-rouge">CSVDataset</code> implementation:</p>

<ul>
  <li>Flexibility in data loading: The <code class="language-plaintext highlighter-rouge">CSVDataset</code> class allows for flexible loading of data from a CSV file. It supports loading the entire dataset into memory at once (<code class="language-plaintext highlighter-rouge">load=True</code>) or loading data on-the-fly as needed. This flexibility enables efficient memory usage, especially when dealing with large datasets.</li>
  <li>Customizable text and label fields: The <code class="language-plaintext highlighter-rouge">CSVDataset</code> class takes <code class="language-plaintext highlighter-rouge">text_field</code> and <code class="language-plaintext highlighter-rouge">label_field</code> parameters, allowing you to specify the column names in the CSV file that correspond to the text data and labels, respectively. This makes it easy to adapt the dataset to different CSV formats and column names.</li>
  <li>Preprocessing and filtering: The <code class="language-plaintext highlighter-rouge">CSVDataset</code> class provides functionality for preprocessing and filtering the text data based on term frequencies and document frequencies. The process_corpus method computes the term frequencies, document frequencies, and total token counts for the entire corpus. The is_valid method allows for filtering tokens based on the specified thresholds.</li>
</ul>

<p>Advantages of the <code class="language-plaintext highlighter-rouge">min_df</code>, <code class="language-plaintext highlighter-rouge">max_df</code>, <code class="language-plaintext highlighter-rouge">min_tf</code>, and <code class="language-plaintext highlighter-rouge">max_tf</code> parameters:</p>

<ul>
  <li>Filtering rare and common words: The <code class="language-plaintext highlighter-rouge">min_df</code> and <code class="language-plaintext highlighter-rouge">max_df</code> parameters allow for filtering words based on their document frequency. By setting <code class="language-plaintext highlighter-rouge">min_df</code>, you can exclude words that appear in very few documents, which are often less informative. Similarly, by setting <code class="language-plaintext highlighter-rouge">max_df</code>, you can exclude words that appear in a large portion of the documents, as they may be too common and not discriminative.</li>
  <li>Filtering low and high frequency words: The <code class="language-plaintext highlighter-rouge">min_tf</code> and <code class="language-plaintext highlighter-rouge">max_tf</code> parameters enable filtering words based on their term frequency. By setting <code class="language-plaintext highlighter-rouge">min_tf</code>, you can exclude words that occur very rarely in the corpus, which may be noise or less relevant. On the other hand, by setting <code class="language-plaintext highlighter-rouge">max_tf</code>, you can exclude words that occur too frequently and may not provide much discriminative power.</li>
  <li>Vocabulary control: By applying these filtering parameters, you can control the size and quality of the vocabulary used for the BoW representation. This helps in reducing the dimensionality of the feature space and focusing on the most informative words for the given task.</li>
</ul>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
</pre></td><td class="code"><pre><span class="k">class</span> <span class="nc">TokenizedDataset</span><span class="p">(</span><span class="n">CSVDataset</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__getitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">idx</span><span class="p">):</span>
        <span class="n">text</span><span class="p">,</span> <span class="n">label</span> <span class="o">=</span> <span class="nb">super</span><span class="p">().</span><span class="n">__getitem__</span><span class="p">(</span><span class="n">idx</span><span class="p">)</span>
        <span class="n">text</span> <span class="o">=</span> <span class="p">[</span><span class="n">token</span> <span class="k">for</span> <span class="n">token</span> <span class="ow">in</span> <span class="n">text</span><span class="p">.</span><span class="n">split</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">is_valid</span><span class="p">(</span><span class="n">token</span><span class="p">)]</span>
        <span class="k">return</span> <span class="s">' '</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">text</span><span class="p">),</span> <span class="n">label</span>


<span class="k">class</span> <span class="nc">BowTokenizer</span><span class="p">:</span>
    <span class="n">UNKNOWN_TOKEN</span> <span class="o">=</span> <span class="s">'&lt;UNK&gt;'</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">vocab_size</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">stopwords</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">vocab_size</span> <span class="o">=</span> <span class="n">vocab_size</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">word2idx</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">idx2word</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">vocab</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">stopwords</span> <span class="o">=</span> <span class="n">stopwords</span>
        
    <span class="k">def</span> <span class="nf">fit</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">):</span>
        <span class="c1"># Tokenize the texts and build the vocabulary
</span>        <span class="n">word_counts</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">text</span> <span class="ow">in</span> <span class="n">texts</span><span class="p">:</span>
            <span class="n">words</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">tokenize</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
            <span class="n">word_counts</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>

        <span class="c1"># Sort the words by frequency and select the top vocab_size words
</span>        <span class="n">vocab_size</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">vocab_size</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">vocab_size</span><span class="p">:</span>
            <span class="n">vocab_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word_counts</span><span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">vocab</span> <span class="o">=</span> <span class="p">[</span><span class="n">word</span> <span class="k">for</span> <span class="n">word</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">word_counts</span><span class="p">.</span><span class="n">most_common</span><span class="p">(</span><span class="n">vocab_size</span><span class="p">)]</span>
        
        <span class="c1"># Create word-to-index and index-to-word mappings
</span>        <span class="n">word2idx</span> <span class="o">=</span> <span class="p">{</span><span class="n">word</span><span class="p">:</span> <span class="n">idx</span> <span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">vocab</span><span class="p">)}</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">word2idx</span> <span class="o">=</span> <span class="n">word2idx</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">idx2word</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">word2idx</span><span class="p">.</span><span class="n">values</span><span class="p">(),</span> <span class="n">word2idx</span><span class="p">.</span><span class="n">keys</span><span class="p">()))</span>
    
    <span class="k">def</span> <span class="nf">vocabulary</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">vocab</span><span class="p">)</span>
        
    <span class="k">def</span> <span class="nf">tokenize</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">):</span>
        <span class="c1"># Tokenize the text into words
</span>        <span class="n">words</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">findall</span><span class="p">(</span><span class="sa">r</span><span class="s">'\b\w+\b'</span><span class="p">,</span> <span class="n">text</span><span class="p">.</span><span class="n">lower</span><span class="p">())</span>
        <span class="k">return</span> <span class="n">words</span>
    
    <span class="k">def</span> <span class="nf">encode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">):</span>
        <span class="c1"># Tokenize the text and convert words to indices
</span>        <span class="n">words</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">tokenize</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
        <span class="n">word_counts</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>

        <span class="n">vocab_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">vocab</span><span class="p">)</span>
        <span class="n">data</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">vocab_size</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="nb">float</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">word</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">word_counts</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">if</span> <span class="n">word</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">word2idx</span><span class="p">:</span>
                <span class="k">continue</span>
            <span class="n">word_index</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">word2idx</span><span class="p">[</span><span class="n">word</span><span class="p">]</span>
            <span class="n">data</span><span class="p">[</span><span class="n">word_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">count</span>
        <span class="k">return</span> <span class="n">data</span>
    
    <span class="k">def</span> <span class="nf">decode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_ids</span><span class="p">):</span>
        <span class="c1"># Convert token indices to
</span>        <span class="n">words</span> <span class="o">=</span> <span class="p">[</span><span class="bp">self</span><span class="p">.</span><span class="n">idx2word</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">UNKNOWN_TOKEN</span><span class="p">)</span> <span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="n">input_ids</span><span class="p">]</span>
        <span class="k">return</span> <span class="s">' '</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">words</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Advantages of the <code class="language-plaintext highlighter-rouge">BowTokenizer</code> implementation:</p>

<ul>
  <li>Vocabulary building: The <code class="language-plaintext highlighter-rouge">BowTokenizer</code> class provides functionality for building a vocabulary from the text data. It tokenizes the texts, counts the word frequencies, and selects the top <code class="language-plaintext highlighter-rouge">vocab_size</code> words to form the vocabulary. This allows for creating a compact and representative vocabulary for the BoW representation.</li>
  <li>Encoding and decoding: The <code class="language-plaintext highlighter-rouge">BowTokenizer</code> class offers methods for encoding text into BoW vectors and decoding BoW vectors back into text. The encode method tokenizes the text, maps the words to their corresponding indices in the vocabulary, and returns a vector representation. The decode method converts the BoW vector back into text by mapping the indices to their corresponding words.</li>
  <li>Handling unknown words: The <code class="language-plaintext highlighter-rouge">BowTokenizer</code> class handles unknown words (words not present in the vocabulary) by mapping them to a special <code class="language-plaintext highlighter-rouge">&lt;UNK&gt;</code> token. This ensures that the encoding process can handle out-of-vocabulary words gracefully.</li>
</ul>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">torch.optim</span> <span class="k">as</span> <span class="n">optim</span>
<span class="kn">from</span> <span class="nn">torch.utils.data</span> <span class="kn">import</span> <span class="n">DataLoader</span>
<span class="kn">import</span> <span class="nn">utils</span>
<span class="kn">from</span> <span class="nn">nvdm</span> <span class="kn">import</span> <span class="n">NVDM</span>

<span class="kn">import</span> <span class="nn">csv</span>


<span class="k">def</span> <span class="nf">train</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">dataloader</span><span class="p">,</span> <span class="n">optimizer</span><span class="p">,</span> <span class="n">device</span><span class="p">):</span>
    <span class="n">model</span><span class="p">.</span><span class="n">train</span><span class="p">()</span>
    <span class="n">total_loss</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">batch_index</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
    <span class="k">for</span> <span class="n">documents</span><span class="p">,</span> <span class="n">labels</span> <span class="ow">in</span> <span class="n">dataloader</span><span class="p">:</span>
        <span class="n">batch_index</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">optimizer</span><span class="p">.</span><span class="n">zero_grad</span><span class="p">()</span>
        <span class="n">loss</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">documents</span><span class="p">)</span>
        <span class="n">loss</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>

        <span class="c1"># Clip gradients
</span>        <span class="n">torch</span><span class="p">.</span><span class="n">nn</span><span class="p">.</span><span class="n">utils</span><span class="p">.</span><span class="n">clip_grad_norm_</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">parameters</span><span class="p">(),</span> <span class="n">max_norm</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>

        <span class="n">optimizer</span><span class="p">.</span><span class="n">step</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">batch_index</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Loss #</span><span class="si">{</span><span class="n">batch_index</span><span class="si">}</span><span class="s">: '</span><span class="p">,</span> <span class="n">loss</span><span class="p">.</span><span class="n">item</span><span class="p">(),</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">total_loss</span> <span class="o">+=</span> <span class="n">loss</span><span class="p">.</span><span class="n">item</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">total_loss</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">dataloader</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">argparse</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'NVDM Training'</span><span class="p">)</span>

    <span class="c1"># NVDM configuration parameters
</span>    <span class="n">nvdm_config</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">add_argument_group</span><span class="p">(</span><span class="s">'NVDM Configuration'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--vocab_size'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">10000</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of topics'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--topics'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of topics'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--alpha'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Alpha parameter'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--hidden_size'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Hidden size'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--embedding_size'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Embedding size'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--dropout'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Dropout rate'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--num_layers'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of layers'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--num_encoder_heads'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of attention heads in the encoding step'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--num_decoder_heads'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of attention heads in the decoding step'</span><span class="p">)</span>
    <span class="n">nvdm_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--temperature'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Temperature for topic mixture'</span><span class="p">)</span>

    <span class="c1"># data parametersargs.path
</span>    <span class="n">data_config</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">add_argument_group</span><span class="p">(</span><span class="s">'Preprocess Function Parameters'</span><span class="p">)</span>
    <span class="n">data_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--path'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'/samples/corpus.csv'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'CSV path to load data from'</span><span class="p">)</span>
    <span class="n">data_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--text'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'content'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'CSV field containing the text'</span><span class="p">)</span>
    <span class="n">data_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--label'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'content'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'CSV field containing text label'</span><span class="p">)</span>
    <span class="n">data_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--stream'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s">'store_true'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Stream data directly from the file instead of loading it into memory'</span><span class="p">)</span>

    <span class="c1"># preprocess parameters
</span>    <span class="n">preprocess_config</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">add_argument_group</span><span class="p">(</span><span class="s">'Preprocess Function Parameters'</span><span class="p">)</span>
    <span class="n">preprocess_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--mindf'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Minimum document frequency'</span><span class="p">)</span>
    <span class="n">preprocess_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--maxdf'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="s">'inf'</span><span class="p">),</span> <span class="n">help</span><span class="o">=</span><span class="s">'Maximum document frequency'</span><span class="p">)</span>
    <span class="n">preprocess_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--mintf'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Minimum term frequency'</span><span class="p">)</span>
    <span class="n">preprocess_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--maxtf'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="nb">float</span><span class="p">(</span><span class="s">'inf'</span><span class="p">),</span> <span class="n">help</span><span class="o">=</span><span class="s">'Maximum document frequency'</span><span class="p">)</span>

    <span class="c1"># Loss function parameters
</span>    <span class="n">loss_config</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">add_argument_group</span><span class="p">(</span><span class="s">'Loss Function Parameters'</span><span class="p">)</span>
    <span class="n">loss_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--reconstruction_weight'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Reconstruction loss weight'</span><span class="p">)</span>
    <span class="n">loss_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--kl_weight'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'KL divergence loss weight'</span><span class="p">)</span>
    <span class="n">loss_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--sparsity_weight'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Sparsity loss weight'</span><span class="p">)</span>
    <span class="n">loss_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--uniqueness_weight'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Uniqueness loss weight'</span><span class="p">)</span>

    <span class="c1"># Training parameters
</span>    <span class="n">training_config</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">add_argument_group</span><span class="p">(</span><span class="s">'Training Parameters'</span><span class="p">)</span>
    <span class="n">training_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--epochs'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Number of epochs'</span><span class="p">)</span>
    <span class="n">training_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--batch_size'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Batch size'</span><span class="p">)</span>
    <span class="n">training_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--learning_rate'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Learning rate'</span><span class="p">)</span>
    <span class="n">training_config</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--debug'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s">'store_true'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Enable debug mode'</span><span class="p">)</span>

    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
    
    <span class="c1"># Device setup
</span>    <span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">device</span><span class="p">(</span><span class="s">'cuda'</span> <span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s">'cpu'</span><span class="p">)</span>

    <span class="c1"># Define the dataset and data loader
</span>    <span class="n">load</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">stream</span><span class="p">:</span>
        <span class="n">load</span><span class="o">=</span><span class="bp">False</span>

    <span class="n">dataset</span> <span class="o">=</span> <span class="n">utils</span><span class="p">.</span><span class="n">TokenizedDataset</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="n">text_field</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">text</span><span class="p">,</span> <span class="n">label_field</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">label</span><span class="p">,</span> <span class="n">load</span><span class="o">=</span><span class="n">load</span><span class="p">,</span> <span class="n">min_df</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">mindf</span><span class="p">,</span> <span class="n">max_df</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">maxdf</span><span class="p">,</span> <span class="n">min_tf</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">mintf</span><span class="p">,</span> <span class="n">max_tf</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">maxtf</span><span class="p">)</span>
    <span class="n">dataloader</span> <span class="o">=</span> <span class="n">DataLoader</span><span class="p">(</span><span class="n">dataset</span><span class="p">,</span> <span class="n">batch_size</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">shuffle</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">drop_last</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">tokenizer</span> <span class="o">=</span> <span class="n">utils</span><span class="p">.</span><span class="n">BowTokenizer</span><span class="p">(</span><span class="n">vocab_size</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">vocab_size</span><span class="p">)</span>
     <span class="c1"># Load and preprocess the dataset
</span>    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
        <span class="n">documents</span> <span class="o">=</span> <span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="n">args</span><span class="p">.</span><span class="n">text</span><span class="p">]</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">)]</span>
        <span class="n">tokenizer</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">documents</span><span class="p">)</span>

    <span class="c1"># Assume we have a list 'documents' containing the input text documents
</span>    <span class="k">print</span><span class="p">(</span><span class="s">'preprocessing data'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="c1"># Create the NVDM model
</span>    <span class="n">model</span> <span class="o">=</span> <span class="n">NVDM</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">embedding_size</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">hidden_size</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">topics</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">dropout</span><span class="p">,</span> <span class="n">uniqueness_weight</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">uniqueness_weight</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>

    <span class="c1"># Optimizer
</span>    <span class="n">optimizer</span> <span class="o">=</span> <span class="n">optim</span><span class="p">.</span><span class="n">Adam</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">parameters</span><span class="p">(),</span> <span class="n">lr</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">learning_rate</span><span class="p">)</span>

    <span class="c1"># Training loop
</span>    <span class="k">print</span><span class="p">(</span><span class="s">'training model'</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">epoch</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">epochs</span><span class="p">):</span>
        <span class="n">train_loss</span> <span class="o">=</span> <span class="n">train</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">dataloader</span><span class="p">,</span> <span class="n">optimizer</span><span class="p">,</span> <span class="n">device</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Epoch [</span><span class="si">{</span><span class="n">epoch</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">args</span><span class="p">.</span><span class="n">epochs</span><span class="si">}</span><span class="s">], Loss: </span><span class="si">{</span><span class="n">train_loss</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

        <span class="n">topic_words</span><span class="p">,</span> <span class="n">topic_probabilities</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">topic_words</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="n">probabilities</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">topic_words</span><span class="p">,</span> <span class="n">topic_probabilities</span><span class="p">)):</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'</span><span class="se">\n</span><span class="s">Topic #</span><span class="si">{</span><span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="si">}</span><span class="s"> Words: '</span><span class="p">,</span> <span class="n">words</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span><span class="s">'Topic Probabilities:'</span><span class="p">,</span> <span class="n">probabilities</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This implementation uses <code class="language-plaintext highlighter-rouge">argparse</code> to allow developers to configure the architecture, computational complexity and supported vocbaulary from the terminal, aong with the file/dataset the NVDM is trained on.  The NVDM consistently produces high quality topics, even without topic coherence loss.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Topic #1 Words:  national_junior triple_jump wind_assist european_indoor_champion hurdles heptathlon organiser_calendar_doha_qat world_junior national_record_holder sprint_double
Topic Probabilities: [0.0005295022274367511, 0.000521067064255476, 0.0004984174738638103, 0.0004961825325153768, 0.0004958845092914999, 0.00047388693201355636, 0.0004721697769127786, 0.000470947299618274, 0.00046596929314546287, 0.00046446104533970356]

Topic #2 Words:  kilometre_reach tanui start_push_pace imane_merga kiprono pass_km_km athl_te splits kidane kamworor
Topic Probabilities: [0.0001887115795398131, 0.00016992191376630217, 0.00016913456784095615, 0.00016887785750441253, 0.00016846574726514518, 0.00016821929602883756, 0.00016521599900443107, 0.00016431287804152817, 0.0001635944499867037, 0.00016298888658639044]

Topic #3 Words:  fraser_pryce kaniskina maurice_greene bolt world_champion_ato farah greene gatlin korzeniowski tyson_gay
Topic Probabilities: [0.0002071216731565073, 0.00019888173846993595, 0.00019405571219976991, 0.00019289054034743458, 0.0001920316572068259, 0.00018928298959508538, 0.00018790591275319457, 0.00018012983491644263, 0.00017984140140470117, 0.000177944268216379]

Topic #4 Words:  fourth_overall nsw dobrynska ennis score combined_event effort decathlon kasyanov heptathlon
Topic Probabilities: [0.0001549244625493884, 0.00015470129437744617, 0.0001546467246953398, 0.00015251476725097746, 0.00015251018339768052, 0.00015153890126384795, 0.00014963878493290395, 0.00014948415628168732, 0.00014858909707982093, 0.00014747283421456814]

Topic #5 Words:  stage_km stage_km_stage km_stage pass_km_mark pass_kilometre se anti_dope_programme sato hold_steady tsegay
Topic Probabilities: [0.0002340153732802719, 0.00019602605607360601, 0.00019109177810605615, 0.00018873649241868407, 0.0001754782279022038, 0.00017319770995527506, 0.00017031136667355895, 0.00016833780682645738, 0.00016813860565889627, 0.00016755572869442403]

Topic #6 Words:  razorback_track_field tahoma_panose_font_font family_arial_narrow_panose arkansas_athletic_visit nixon color_windowtext_span_balloontextchar div_msonormal_margin_top font_font_family_itc stone_serif_std_medium tahoma_san_serif_msochpdefault
Topic Probabilities: [0.00020097965898457915, 0.00019553006859496236, 0.0001942943490575999, 0.00019251251069363207, 0.00019250520563218743, 0.0001921903167385608, 0.00019170019368175417, 0.0001908671110868454, 0.00019030227849725634, 0.0001891691208584234]

Topic #7 Words:  ncaas hellebaut yeah bergqvist pass_height xc harting anti_dope_programme low_stick swede
Topic Probabilities: [0.00020984900766052306, 0.00020819688506890088, 0.00020724012574646622, 0.00020646779739763588, 0.00020583244622685015, 0.00020467313879635185, 0.00020236226555425674, 0.0001983269612537697, 0.00019566438277252018, 0.00019545013492461294]

Topic #8 Words:  bend_knee plank set_rep abs knee posture push_harder main_protagonist dumbbell upper_body
Topic Probabilities: [0.00013974822650197893, 0.00013468462566379458, 0.00013362171011976898, 0.00013106761616654694, 0.00013051718997303396, 0.0001305115583818406, 0.00013051058340352029, 0.00013008633686695248, 0.00012852296640630811, 0.00012837933900300413]

Topic #9 Words:  datum_addition_shoe_weight stride_flexibility_forefoot_account usage_battery_mechanical_test lab_provide_objective_exclusive measure_sole_thickness_sit foot_road_foam_cushion review_shoe michigan_shoe_real_world pennsylvania_shoe_real_world tester_shoe
Topic Probabilities: [0.00029314172570593655, 0.00029293980333022773, 0.0002928385802078992, 0.000292605662252754, 0.00029258636641316116, 0.0002924941072706133, 0.00029232309316284955, 0.0002862800029106438, 0.00027762691024690866, 0.00026143176364712417]

Topic #10 Words:  sauce kasyanov round_foul wlodarczyk freimuth overnight_leader dressing round_throw pepper topping
Topic Probabilities: [0.00032631121575832367, 0.0003227636043448001, 0.0003218145575374365, 0.00032164782169274986, 0.00031468141241930425, 0.0003138505853712559, 0.0003078986192122102, 0.0003074088890571147, 0.0003049601800739765, 0.0002995755639858544]

Topic #11 Words:  bell_lap closing_lap jebet atapuerca foam_midsole stable_ride medial_post faith_kipyegon heath dibaba
Topic Probabilities: [0.00017135417147073895, 0.00016612840408924967, 0.000163451477419585, 0.00015913633978925645, 0.00015847643953748047, 0.00015797841479070485, 0.0001568600710015744, 0.00015681587683502585, 0.0001563854602864012, 0.00015422738215420395]

Topic #12 Words:  arron greene robles maurice_greene roble merritt wariner doucour powell kallur
Topic Probabilities: [0.0005106705939397216, 0.0005042121629230678, 0.0004950464935973287, 0.00046031526289880276, 0.0004537209460977465, 0.0004504402750171721, 0.00044670907664112747, 0.0004097057390026748, 0.0004081916413269937, 0.000406078586820513]

Topic #13 Words:  platform feature promote joint fitting argentina tech bra flexibility runners
Topic Probabilities: [0.00017971890338230878, 0.0001771343086147681, 0.0001728379720589146, 0.0001703346788417548, 0.000169746796018444, 0.00016817505820654333, 0.00016802331083454192, 0.00016549485735595226, 0.0001650949561735615, 0.00016503891674801707]

Topic #14 Words:  field_stay_tune complete_coverage_iu_track iu immediate_release_track_field federations clinic program nominee kerala indoor_track_field
Topic Probabilities: [0.00015527514915447682, 0.0001551182213006541, 0.00015354147762991488, 0.00014967596507631242, 0.0001477991754654795, 0.0001419319596607238, 0.00014136313984636217, 0.00014117403770796955, 0.00014114829536993057, 0.00014088221359997988]

Topic #15 Words:  km race_walk_challenge chihuahua km_loop ekiden km_km women_km stage_km km_stage world_marathon_champion
Topic Probabilities: [0.00045582986786030233, 0.00042629288509488106, 0.00041378289461135864, 0.00040664817788638175, 0.0003824761079158634, 0.000382121535949409, 0.000376749288989231, 0.00037533120485022664, 0.00037033201078884304, 0.00036467579775489867]

Topic #16 Words:  training_program tempo_pace hill_sprint key_workout training_schedule tempo_run build_mileage specific_workout energy_system speed_workout
Topic Probabilities: [0.00015735242050141096, 0.0001552267640363425, 0.00015487417113035917, 0.0001523850078228861, 0.00015200886991806328, 0.00015089314547367394, 0.00015057215932756662, 0.00015039686695672572, 0.00014915651991032064, 0.00014892316539771855]

Topic #17 Words:  road_foam_cushion_stride addition_shoe_weight_measure flexibility_forefoot_account_review sole_thickness_sit_foot shoe_real_world_usage provide_objective_exclusive_datum battery_mechanical_test_lab brightcove_id_video michigan_shoe_real_world review_shoe
Topic Probabilities: [0.0003328978782519698, 0.00033284176606684923, 0.00033272075233981013, 0.0003327052399981767, 0.0003280891396570951, 0.0003277743235230446, 0.0003274726332165301, 0.0003065402852371335, 0.00029554389766417444, 0.00029002776136621833]

Topic #18 Words:  true_freshman iu wartburg uw_oshkosh uw_la_crosse ncaa_meet ducks johns_hopkins lsu nyu
Topic Probabilities: [0.0005267398082651198, 0.0004757751594297588, 0.0004727425111923367, 0.00046986431698314846, 0.0004570367746055126, 0.0004565916024148464, 0.0004489163984544575, 0.0004444819933269173, 0.0004420706245582551, 0.0004363793705124408]

Topic #19 Words:  tzis score overnight_leader total_score chernova overnight_lead pts fajdek gotzis snug_fit
Topic Probabilities: [0.00024404078430961818, 0.0002303147193742916, 0.00022389950754586607, 0.00022108246048446745, 0.00021435876260511577, 0.0002082447026623413, 0.0002077820390695706, 0.00020295534341130406, 0.00020166372996754944, 0.00019990364671684802]

Topic #20 Words:  kipruto automatic_qualifying_spot kejelcha singletrack ayana koech kipyegon reach_kilometre kilometre_remain deba
Topic Probabilities: [0.0004376643046271056, 0.00041711798985488713, 0.0003987165109720081, 0.0003969542740378529, 0.0003965102368965745, 0.0003922139585483819, 0.0003744940913747996, 0.0003742391418199986, 0.00036667476524598897, 0.00035903151729144156]

Topic #21 Words:  div_msonormal_margin_margin msohyperlink_mso_style_priority style_priority_color_purple underline_visited_span_mso san_serif_link_span pt_font_family_calibri color_blue_text_decoration family_cambria_panose_font math_panose_font_font font_family_calibri_panose
Topic Probabilities: [0.0003982365597039461, 0.0003865780890919268, 0.00038599688559770584, 0.0003858261334244162, 0.00038576123188249767, 0.0003767758025787771, 0.0003719555970747024, 0.0003674782929010689, 0.0003656522312667221, 0.00036364945117384195]

Topic #22 Words:  training_program run_economy elite_runner program pennsylvania improve lactate consistent cross_training downhill
Topic Probabilities: [0.00014887095312587917, 0.00014796634786762297, 0.00014657336578238755, 0.00014462492254097015, 0.0001440627092961222, 0.00014293886488303542, 0.00014185230247676373, 0.0001412749697919935, 0.00014071576879359782, 0.0001404146896675229]

Topic #23 Words:  stage_km_stage fukuoka improved associated chiba km_stage live_japan increase muscle_fatigue function
Topic Probabilities: [0.0001788241497706622, 0.0001709579664748162, 0.00016188611334655434, 0.0001599854585947469, 0.00015961381723172963, 0.00015759470988996327, 0.00015517046267632395, 0.00015402464487124234, 0.00015372017514891922, 0.00015293341130018234]

Topic #24 Words:  grill pepper cook_minute garlic stir avocado sauce olive_oil syrup slice
Topic Probabilities: [0.00029838853515684605, 0.0002721975324675441, 0.00026485041598789394, 0.0002625574416015297, 0.0002616708807181567, 0.0002607014321256429, 0.00025924600777216256, 0.00025659558014012873, 0.00024157052394002676, 0.0002401871606707573]

Topic #25 Words:  eaton hardee follow_page trey_hardee brightcove_id claye sebrle ashton_eaton bayer attempt_clearance
Topic Probabilities: [0.00029643013840541244, 0.0002523841103538871, 0.00023353486903943121, 0.00023333514400292188, 0.00023249629884958267, 0.00023083601263351738, 0.00023046253772918135, 0.0002253647253382951, 0.00022452250414062291, 0.0002195353154093027]

Topic #26 Words:  bq vail pre_nats postseason cliff flagstaff deer west_regional hardrock dirt_road
Topic Probabilities: [0.0002408408181509003, 0.00023494136985391378, 0.00023437567870132625, 0.00022776515106670558, 0.00021635669691022485, 0.00021620663756038994, 0.00021123082842677832, 0.0002079781552311033, 0.0002075362717732787, 0.00020752135606016964]

Topic #27 Words:  ncaas span_amp_amp amp_amp_amp_lt gt_amp_amp_amp redactor_selection_marker_data amp_amp_lt_span verified_redactor_amp_amp marker_data_verified_redactor amp_amp_amp_gt marker_class_redactor_selection
Topic Probabilities: [0.0005251255352050066, 0.0005159128922969103, 0.0005129625787958503, 0.0005118255503475666, 0.0005064702709205449, 0.0004995728377252817, 0.0004968844004906714, 0.0004857392341364175, 0.00047573723713867366, 0.0004749214858748019]

Topic #28 Words:  allentown_shoe_real_world lab_test measure_sole_thickness_sit usage_battery_mechanical_test stride_flexibility_forefoot_account lab_provide_objective_exclusive datum_addition_shoe_weight foot_road_foam_cushion athl_te review_shoe
Topic Probabilities: [0.00015008448099251837, 0.00014843278040643781, 0.00014576577814295888, 0.00014571099018212408, 0.00014551033382304013, 0.00014550470223184675, 0.00014537919196300209, 0.00014508143067359924, 0.00014476204523816705, 0.0001446998940082267]

Topic #29 Words:  pace pace_slow hill_sprint mile_tempo_run run_faster lagat rupp training_cycle pacing hill
Topic Probabilities: [0.0004469976993277669, 0.000368804088793695, 0.00036803274997510016, 0.0003578069154173136, 0.0003511097456794232, 0.00034814863465726376, 0.0003470892843324691, 0.0003425931208766997, 0.0003401785215828568, 0.0003377698012627661]

Topic #30 Words:  closing_lap water_jump backstretch cherono houlihan berian ayana sowinski kipruto kilometre_reach
Topic Probabilities: [0.000206675409572199, 0.0001992114121094346, 0.00019332184456288815, 0.00019155119662173092, 0.00018596377049107105, 0.00018261026707477868, 0.00018221103528048843, 0.00018160931358579546, 0.00017999696137849241, 0.00017984103760682046]
</code></pre></div></div>

<h2 id="references" class="linked-heading">
    References<a href="#references">🔗</a>
</h2>

<ol>
  <li>“Neural Variational Inference for Text Processing” by Yishu Miao, Lei Yu, and Phil Blunsom (2016)</li>
  <li>“Discovering Discrete Latent Topics with Neural Variational Inference” by Yishu Miao, Edward Grefenstette, and Phil Blunsom (2017)</li>
  <li>“Topic Modeling with Wasserstein Autoencoders” by Feng Nan, Ran Ding, Ramesh Nallapati, and Bing Xiang (2019)</li>
  <li>“A Primer on Neural Network Models for Natural Language Processing” by Yoav Goldberg (2016)</li>
  <li>“Autoencoding Variational Bayes” by Diederik P. Kingma and Max Welling (2014)</li>
</ol>

<h3 id="github-projects" class="linked-heading">
    Github Projects<a href="#github-projects">🔗</a>
</h3>

<ol>
  <li><a href="https://github.com/pytorch/examples/tree/master/vae">PyTorch Examples: Text Generation with VAEs</a></li>
  <li><a href="https://github.com/ysmiao/nvdm">Neural Variational Document Model (NVDM) Implementation</a></li>
  <li><a href="https://github.com/vlukiyanov/pt-avitm">Variational Autoencoder for Topic Modeling</a></li>
  <li><a href="https://github.com/PetrochukM/PyTorch-NLP">PyTorch-NLP: Natural Language Processing with PyTorch</a></li>
</ol>]]></content><author><name>Doug Fenstermacher</name></author><category term="experiment" /><category term="python" /><category term="nlp" /><category term="data" /><category term="math" /><summary type="html"><![CDATA[Discover how Neural Variational Document Models, implemented using PyTorch, improve topic modeling and unsupervised learning in natural language processing. Learn about the architecture, training process, and applications of these latent variable models for text analysis and beyond.]]></summary></entry><entry><title type="html">Harnessing X-Means Clustering and CIE2000 for Visually Striking Dominant Color Extraction</title><link href="https://dougfenstermacher.com/project/xmeans-cie2000-dominant-color-extraction-visualization-tutorial" rel="alternate" type="text/html" title="Harnessing X-Means Clustering and CIE2000 for Visually Striking Dominant Color Extraction" /><published>2024-02-01T00:00:00+00:00</published><updated>2024-02-01T00:00:00+00:00</updated><id>https://dougfenstermacher.com/project/xmeans-cie2000-dominant-color-extraction-visualization-tutorial</id><content type="html" xml:base="https://dougfenstermacher.com/project/xmeans-cie2000-dominant-color-extraction-visualization-tutorial"><![CDATA[<style>
    .dominant-colors {
        display: flex;
        min-height: 5em;
        width: 100%;
        flex-wrap: wrap;
    }

    img#test {
        max-width: 50vw;
        min-height: 20em;
    }

    .color {
        display: flex;
        justify-content: space-between;
        font-family: Arial, Helvetica, sans-serif;
        flex-grow: 1;
        height: 7em;
        padding: 1em;
    }

    .color .color-value {
        font-size: 2.5em;
    }


    .color .color-proportion {
        font-size: 1em;
        text-align: right;
    }

    .color .color-proportion::after {
        content: '%';

    }
</style>

<p><img id="test" src="/assets/img/posts/example.jpg" /></p>
<div>
    <label>Min K</label>
    <input type="number" id="min-k" min="2" increment="1" value="6" />

    <label>Max K</label>
    <input type="number" id="max-k" min="2" increment="1" value="6" />

    <label>Max Iterations</label>
    <input type="number" id="max-iterations" min="5" increment="1" value="10" />
    <button id="submit">Extract</button>
</div>
<div class="dominant-colors" style="padding-top: 1em;"></div>
<div style="width: 100%;">
    <canvas id="bubble-chart" style="height: 40em;"></canvas>
</div>
<script src="/assets/js/posts/xmeans.js"></script>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script>
    document.body.addEventListener("dragenter", dragenter, false);
    document.body.addEventListener("dragover", dragover, false);
    document.body.addEventListener("drop", drop, false);

    var imgElement = document.querySelector('#test'),
        minKElement = document.querySelector('#min-k'),
        maxKElement = document.querySelector('#max-k'),
        maxIterationsElement = document.querySelector('#max-iterations'),
        bubbleChartElement = document.querySelector('#bubble-chart'),
        bubbleChart = new Chart(bubbleChartElement, {
            type: 'bubble',
            data: {
                datasets: []
            },
            options: {
                maintainAspectRatio: false,
                scales: {
                    x: {
                        title: {
                            display: true,
                            align: 'center',
                            text: 'Avg Inter-centroid distance'
                        }
                    },
                    y: {
                        title: {
                            display: true,
                            align: 'center',
                            text: 'Intra-cluster distance'
                        }
                    }
                }
            }
        });
        submitElement = document.querySelector('#submit');
                
    minKElement.addEventListener('change', function(e) {
        var minK = parseInt(minKElement.value),
            maxK = parseInt(maxKElement.value);
        if (minK > maxK) {
            maxKElement.value = minK;
        }
    });

    maxKElement.addEventListener('change', function(e) {
        var minK = parseInt(minKElement.value),
            maxK = parseInt(maxKElement.value),
            maxIterations = parseInt(maxIterationsElement.value);
        if (minK > maxK) {
            minKElement.value = maxK;
        }
        if (maxK > maxIterations) {
            maxIterationsElement.value = maxK;
        }
    });

    if (window.Worker) {
        const worker = new Worker('/assets/js/posts/color-extraction/worker.js');
        worker.onmessage = function(event) {
            const data = event.data;
            data.clusters.sort(function(a, b) {
                return b.points.length - a.points.length;
            });
            const dominantColorContainerElement = document.querySelector('.dominant-colors');
            renderColorPalette(dominantColorContainerElement, data);
            renderBubbleChart(bubbleChartElement, data);
            submitElement.textContent = 'Submit';
            submitElement.disabled = false;
        }

        submitElement.addEventListener('click', function() {
            submitElement.textContent = 'Processing';
            submitElement.disabled = true;
            var minKClusters = parseInt(minKElement.value),
                maxKClusters = parseInt(maxKElement.value),
                maxIterations = parseInt(maxIterationsElement.value),
                imgData = loadData(imgElement),
                processedImgData = [];
            for (var i = 0; i < imgData.data.length; i += 4) {
                var rgbColor = [imgData.data[i], imgData.data[i + 1], imgData.data[i + 2], imgData.data[i + 3]],
                    labColor = rgb2lab(rgbColor);
                processedImgData.push(labColor);
            }
            worker.postMessage({minK: minKClusters, maxK: maxKClusters, maxIterations: maxIterations, data: processedImgData});
        });
    }

    function loadData(imgElement) {
        const canvas = document.createElement("canvas"),
            ctx = canvas.getContext("2d");
        canvas.width = imgElement.width;
        canvas.height = imgElement.height;
        ctx.drawImage(imgElement, 0, 0);
        const imageData = ctx.getImageData(0, 0, imgElement.width, imgElement.height);
        return imageData;
    }

    function clusterPixels(element, minK, maxK, maxIterations) {
        var imgData = loadData(element),
            data = [];
        for (var i = 0; i < imgData.data.length; i += 4) {
            var rgbColor = [imgData.data[i], imgData.data[i + 1], imgData.data[i + 2], imgData.data[i + 3]],
                labColor = rgb2lab(rgbColor);
            data.push(labColor);
        }
        var kmeansClusterer = new XMeans(minK, maxK, deltaE2000, {maxIterations: maxIterations, splitThreshold: 0.05, distanceThreshold: 1}),
            results = kmeansClusterer.transform(data, {kDTree: false});
        results.daviesBouldinIndices = kmeansClusterer.daviesBouldinIndex(results);
        results.calinskiHarabaszIndex = kmeansClusterer.calinskiHarabaszIndex(data, results);
        results.clusterDistances = kmeansClusterer.meanDistanceBetweenCentroids(results.clusters);
        results.intraClusterDistances = kmeansClusterer.meanDistanceWithinClusters(results.clusters);
        return results
    }

    function extractDominantColors(element, minK, maxK, maxIterations) {
        var clusterData = clusterPixels(imgElement, minK, maxK, maxIterations);
        // sort colors by prominance
        clusterData.clusters.sort(function(a, b) {
            return b.points.length - a.points.length;
        });
        return clusterData;
    }


    function renderColorPalette(containerElement, results) {
        containerElement.innerHTML = '';

        var clusters = results.clusters,
            black = rgb2lab([0, 0, 0, 1]),
            white = rgb2lab([255, 255, 255, 1]);
        
        // order by dominance
        clusters.sort(function(a, b) {
            return b.points.length - a.points.length;
        });

        // total number of points
        var totalPoints = clusters.reduce(function(sum, cluster) {
            return sum + cluster.points.length;
        }, 0);
        for (var i = 0; i < clusters.length; i++) {
            var cluster = clusters[i],
                color = cluster.centroid,
                rgbColor = lab2rgb(color),
                hexColor = rgb2Hex(rgbColor),
                colorElement = document.createElement('div'),
                proportion = ((cluster.points.length / totalPoints) * 100).toFixed(2);
            colorElement.className = 'color';
            colorElement.style.background = hexColor;
            colorElement.style.color = deltaE2000(black, color) > deltaE2000(white, color) ? `black` : 'white';

            colorElement.innerHTML = `<div class="color-value">${hexColor}</div>
                <div class="color-proportion">${proportion}</div>`;
            containerElement.appendChild(colorElement);
        }
    }


    function renderBubbleChart(element, results) {
        var clusters = results.clusters,
            black = rgb2lab([0, 0, 0, 1]),
            white = rgb2lab([255, 255, 255, 1]);
        
        // order by dominance
        clusters.sort(function(a, b) {
            return b.points.length - a.points.length;
        });

        // total number of points
        var totalPoints = clusters.reduce(function(sum, cluster) {
            return sum + cluster.points.length;
        }, 0);

        var datasets = [];
        for (var i = 0; i < clusters.length; i++) {
            var cluster = clusters[i],
                color = cluster.centroid,
                rgbColor = lab2rgb(color),
                hexColor = rgb2Hex(rgbColor),
                proportion = ((cluster.points.length / totalPoints) * 100).toFixed(2),
                dataset = {
                    label: hexColor,
                    backgroundColor: hexColor,
                    data: [{
                        x: results.clusterDistances[i],
                        y: results.intraClusterDistances[i],
                        r: proportion * 3
                    }]
                };
            datasets.push(dataset);
        }
        var data = {
            datasets: datasets
        };
        bubbleChart.data.labels.pop();
        bubbleChart.data.datasets = datasets;
        bubbleChart.update();
    }

    function componentToHex(c) {
        var hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
    }

    function rgb2Hex(rgb) {
        var r = Math.round(rgb[0]),
            g = Math.round(rgb[1]),
            b = Math.round(rgb[2]);
        return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }


    function rgb2lab(rgba) {
        var r = rgba[0] / 255,
            g = rgba[1] / 255,
            b = rgba[2] / 255,
            alpha = rgba[3], // alpha, untouched
            x,
            y,
            z;

        r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
        g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
        b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;

        x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
        y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.0;
        z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;

        x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
        y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
        z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;

        return [116 * y - 16, 500 * (x - y), 200 * (y - z), alpha];
    }

    function lab2rgb(laba) {
        var y = (laba[0] + 16) / 116,
            x = laba[1] / 500 + y,
            z = y - laba[2] / 200,
            alpha = laba[3], // alpha, untouched
            r,
            g,
            b;

        x = 0.95047 * (x * x * x > 0.008856 ? x * x * x : (x - 16 / 116) / 7.787);
        y = 1.0 * (y * y * y > 0.008856 ? y * y * y : (y - 16 / 116) / 7.787);
        z = 1.08883 * (z * z * z > 0.008856 ? z * z * z : (z - 16 / 116) / 7.787);

        r = x * 3.2406 + y * -1.5372 + z * -0.4986;
        g = x * -0.9689 + y * 1.8758 + z * 0.0415;
        b = x * 0.0557 + y * -0.204 + z * 1.057;

        r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
        g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
        b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;

        return [
            Math.max(0, Math.min(1, r)) * 255,
            Math.max(0, Math.min(1, g)) * 255,
            Math.max(0, Math.min(1, b)) * 255,
            alpha
        ];
    }

    function dragenter(e) {
        e.stopPropagation();
        e.preventDefault();
    }

    function dragover(e) {
        e.stopPropagation();
        e.preventDefault();
        }

    function drop(e) {
        e.stopPropagation();
        e.preventDefault();

        const dt = e.dataTransfer;
        const files = dt.files;
        
        if (files.length) {
            var file = files[0];
            imgElement.src = URL.createObjectURL(file);
            imgElement.onload = () => {
            URL.revokeObjectURL(imgElement.src);
            };
        }
    }

    function deltaE2000(labA, labB, options) {
    /*
    * CIE2000 equation for quantifying perceptual distance between colors.
    * For a very high-level explanation, see https://sensing.konicaminolta.us/us/blog/identifying-color-differences-using-l-a-b-or-l-c-h-coordinates/
    * or for a more mathematical approach: https://zschuessler.github.io/DeltaE/learn/
    * Implementation based on equations from http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html 
    * 
    * Thresholds
    * 0: No perceivable difference between the colors.
    * 0-1: Very slight difference, likely imperceptible to most observers.
    * 1-2: Slight difference, noticeable to trained observers or under careful examination.
    * 2-3.5: Distinct difference, noticeable to most observers.
    * 3.5-5: Significant difference, readily apparent to all observers.
    * Above 5: Very large difference, colors are clearly distinct.
    * 
    * Weighting factors
    * kL (lightness weighting factor): Controls the influence of lightness differences on the overall Delta E 2000 value.
    * kC (chroma weighting factor): Controls the influence of chroma differences.
    * kH (hue weighting factor): Controls the influence of hue differences.
    * 
    * Higher values of a given factor emphasize that aspect of the color.  Each ranges from 0 to 2, and default to 1.
    */

    const l1 = labA[0],
        a1 = labA[1],
        b1 = labA[2],
        l2 = labB[0],
        a2 = labB[1],
        b2 = labB[2];

    // missing utility functions added to Math Object
    Math.rad2deg = function(rad) {
        return 360 * rad / (2 * Math.PI);
    };
    Math.deg2rad = function(deg) {
        return (2 * Math.PI * deg) / 360;
    };
    
    const avgL = (l1 + l2) / 2;
    const c1 = Math.sqrt(Math.pow(a1, 2) + Math.pow(b1, 2));
    const c2 = Math.sqrt(Math.pow(a2, 2) + Math.pow(b2, 2));
    const avgC = (c1 + c2) / 2;
    const g = (1 - Math.sqrt(Math.pow(avgC, 7) / (Math.pow(avgC, 7) + Math.pow(25, 7)))) / 2;

    const a1p = a1 * (1 + g);
    const a2p = a2 * (1 + g);

    const c1p = Math.sqrt(Math.pow(a1p, 2) + Math.pow(b1, 2));
    const c2p = Math.sqrt(Math.pow(a2p, 2) + Math.pow(b2, 2));

    const avgCp = (c1p + c2p) / 2;

    let h1p = Math.rad2deg(Math.atan2(b1, a1p));
    if (h1p < 0) {
        h1p = h1p + 360;
    }

    let h2p = Math.rad2deg(Math.atan2(b2, a2p));
    if (h2p < 0) {
        h2p = h2p + 360;
    }

    const avghp = Math.abs(h1p - h2p) > 180 ? (h1p + h2p + 360) / 2 : (h1p + h2p) / 2;

    const t = 1 - 0.17 * Math.cos(Math.deg2rad(avghp - 30)) + 0.24 * Math.cos(Math.deg2rad(2 * avghp)) + 0.32 * Math.cos(Math.deg2rad(3 * avghp + 6)) - 0.2 * Math.cos(Math.deg2rad(4 * avghp - 63));

    let deltahp = h2p - h1p;
    if (Math.abs(deltahp) > 180) {
        if (h2p <= h1p) {
            deltahp += 360;
        } else {
            deltahp -= 360;
        }
    }

    const deltalp = l2 - l1;
    const deltacp = c2p - c1p;

    deltahp = 2 * Math.sqrt(c1p * c2p) * Math.sin(Math.deg2rad(deltahp) / 2);

    const sl = 1 + ((0.015 * Math.pow(avgL - 50, 2)) / Math.sqrt(20 + Math.pow(avgL - 50, 2)));
    const sc = 1 + 0.045 * avgCp;
    const sh = 1 + 0.015 * avgCp * t;

    const deltaro = 30 * Math.exp(-(Math.pow((avghp - 275) / 25, 2)));
    const rc = 2 * Math.sqrt(Math.pow(avgCp, 7) / (Math.pow(avgCp, 7) + Math.pow(25, 7)));
    const rt = -rc * Math.sin(2 * Math.deg2rad(deltaro));

    options = options || {};

    /*
     * kl (lightness) weighting factor ( 0.0 <= kC <= 2.0)
     * Increasing kL amplifies the importance of lightness differences.
     * This can be useful for applications where subtle lightness variations are crucial, like in textile or paint industries.
     * Conversely, lowering kL downplays lightness changes, which might be beneficial for web design where screen brightness can affect perceived lightness. 
     * */
    const kl = Math.min(
            Math.max(
                options.kl || 1.0,
                0.0),
            2.0
        );
    /*
     * kC (chroma) weighting factor ( 0.0 <= kC <= 2.0)
     * Adjusting kC modifies the influence of chroma (color saturation) in the calculation.
     * Raising kC highlights chroma differences, making vibrant colors stand out more.
     * Lowering it reduces the impact of chroma variations, potentially minimizing the appearance of color shifts due to factors like lighting or viewing angle. 
     * */
    const kc = Math.min(
            Math.max(
                options.kl || 1.0,
                0.0),
            2.0
        );
    /*
     * kH (hue) weighting factor ( 0.0 <= kC <= 2.0)
     * Modifying kH alters the emphasis on hue (color tint) differences.Modifying kH alters the emphasis on hue (color tint) differences.
     * This is rarely used in practical applications due to the complexity of hue calculations and its potentially limited impact on perceived color changes in most contexts.
     * */
    const kh = Math.min(
            Math.max(
                options.kl || 1.0,
                0.0),
            2.0
        );

    const deltaE = Math.sqrt(Math.pow(deltalp / (kl * sl), 2) + Math.pow(deltacp / (kc * sc), 2) + Math.pow(deltahp / (kh * sh), 2) + rt * (deltacp / (kc * sc)) * (deltahp / (kh * sh)));
    return deltaE;
}

/**
 * Called the Just Noticeable difference threshold.
 * Represents the smallest color difference that a typical observer can perceive under specific viewing conditions.
 * For Delta E 2000, a JND value of around 1 is generally accepted. This means a Delta E 2000 difference of 1 or
 * less is often considered imperceptible to most people.
 */
deltaE2000.JND = 1.0;
</script>

<p>In the world of image processing and computer vision, extracting dominant colors from an image has become an increasingly important task. Whether you’re a data-driven designer looking to create visually appealing color palettes or a machine learning enthusiast exploring the intricacies of unsupervised learning, understanding how to accurately identify and quantify the most prominent colors in an image is a valuable skill. In this blog post, we’ll dive into the fascinating realm of dominant color extraction using the powerful combination of X-Means clustering and the CIE2000 color distance metric.</p>

<p>The X-Means clustering algorithm, an extension of the well-known K-Means algorithm, has gained significant attention in recent years due to its ability to automatically determine the optimal number of clusters in a dataset. By leveraging the principles of unsupervised machine learning, X-Means allows us to group similar colors together without requiring prior knowledge of the number of dominant colors present in an image. This adaptability makes X-Means a valuable tool for color quantization and palette generation tasks.</p>

<p>However, the effectiveness of any color clustering algorithm heavily relies on the choice of color distance metric. Traditional color spaces, such as RGB or HSV, often fail to accurately represent the perceptual differences between colors as perceived by the human eye. Developed by the International Commission on Illumination (CIE), CIE2000 takes into account the intricacies of human color perception, ensuring that the clustering process yields results that are visually meaningful and intuitive. By combining the power of X-Means clustering with the perceptual accuracy of CIE2000, we can unlock new possibilities in dominant color extraction, and image quantization.</p>

<h2 id="understanding-x-means-clustering" class="linked-heading">
    Understanding X-Means Clustering<a href="#understanding-x-means-clustering">🔗</a>
</h2>

<p>To understand X-Means clustering we need to start with K-Means clustering.  K-Means is a widely used unsupervised learning algorithm for clustering data points. It partitions a given dataset into a specified number of clusters (K) based on the similarity between data points. The algorithm follows these steps:</p>

<ol>
  <li>Initialization: Randomly select K data points as initial cluster centroids.</li>
  <li>Assignment: Assign each data point to the nearest centroid based on a distance metric (e.g., Euclidean distance).</li>
  <li>Update: Recalculate the centroids of each cluster by taking the mean of all data points assigned to that cluster.</li>
  <li>Iteration: Repeat steps 2 and 3 until the centroids no longer change significantly or a maximum number of iterations is reached.</li>
</ol>

<p>The goal of K-Means is to minimize the sum of squared distances between each data point and its assigned centroid, resulting in compact and well-separated clusters.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nx">KMeans</span> <span class="p">{</span>
    <span class="cm">/**
     * A configurable implementation of the K-means clustering algorithm
     * 
     * 
     * @param {int} minK 
     * @param {Function} distanceMetric 
     * @param {object} options
     */</span>

    <span class="kd">constructor</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">distanceMetric</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">minK</span> <span class="o">=</span> <span class="nx">minK</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span> <span class="o">=</span> <span class="nx">distanceMetric</span><span class="p">;</span>

        <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">distanceThreshold</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">distanceThreshold</span> <span class="o">||</span> <span class="mf">0.0</span><span class="p">;</span> <span class="c1">// minimum threshold of centroid changes to continue iterations</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">maxIterations</span> <span class="o">||</span> <span class="nb">Number</span><span class="p">.</span><span class="nx">MAX_SAFE_INTEGER</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">meanFunc</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">meanFunc</span> <span class="o">||</span> <span class="k">this</span><span class="p">.</span><span class="nx">_arithmeticMean</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">||</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">validateDataset</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset must be array</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset must have at least </span><span class="dl">'</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> data points</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">]))</span> <span class="p">{</span>
                <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
                <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset points must be an array</span><span class="dl">'</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset points must be a non-empty array</span><span class="dl">'</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">transform</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
        <span class="cm">/**
         * Executes the k-means clustering algorithm against a dataset
         * with the given optional optimization parameters.  Parameters are
         * 
         * - validate (bool):  Indicates if the dataset should be validated
         * - kDTree (bool):  Indicates if a KD-tree should be used to find nearest centroids
         * 
         * @param {Array} dataset
         * @param {object} options
         */</span>
        <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
        <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">||</span> <span class="k">this</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">validate</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">validateDataset</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kd">let</span> <span class="nx">iterations</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="kd">let</span> <span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">;</span>

        <span class="c1">// Initialize centroids randomly</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">useNaiveSharding</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroidsNaiveSharding</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Run the main k-means algorithm</span>
        <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">))</span> <span class="p">{</span>
            <span class="c1">// Save old centroids for convergence test.</span>
            <span class="nx">oldCentroids</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">centroids</span><span class="p">];</span>
            <span class="nx">iterations</span><span class="o">++</span><span class="p">;</span>

            <span class="c1">// Assign labels to each datapoint based on centroids</span>
            <span class="nx">labels</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">);</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_recalculateCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kd">const</span> <span class="nx">clusters</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">clusters</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">labels</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="p">{</span>
            <span class="na">clusters</span><span class="p">:</span> <span class="nx">clusters</span><span class="p">,</span>
            <span class="na">centroids</span><span class="p">:</span> <span class="nx">centroids</span><span class="p">,</span>
            <span class="na">iterations</span><span class="p">:</span> <span class="nx">iterations</span><span class="p">,</span>
            <span class="na">converged</span><span class="p">:</span> <span class="nx">iterations</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span><span class="p">,</span>
        <span class="p">};</span>
        <span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">kDTree</span> <span class="o">=</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// prep data structure:</span>
        <span class="kd">const</span> <span class="nx">labels</span> <span class="o">=</span> <span class="p">{},</span>
            <span class="nx">centroidIndices</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">c</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">c</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">labels</span><span class="p">[</span><span class="nx">c</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
                <span class="na">points</span><span class="p">:</span> <span class="p">[],</span>
                <span class="na">centroid</span><span class="p">:</span> <span class="nx">centroids</span><span class="p">[</span><span class="nx">c</span><span class="p">],</span>
            <span class="p">};</span>
            <span class="nx">centroidIndices</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">c</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="kd">var</span> <span class="nx">tree</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">kDTree</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">centroidDimensions</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nx">i</span>
            <span class="p">});</span>
            <span class="nx">tree</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">KDTree</span><span class="p">(</span><span class="nx">centroids</span><span class="p">,</span> <span class="nx">centroidIndices</span><span class="p">,</span> <span class="nx">centroidDimensions</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="c1">// For each element in the dataset, choose the closest centroid. </span>
        <span class="c1">// Make that centroid the element's label.</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataSet</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nx">dataSet</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="kd">let</span> <span class="nx">closestCentroid</span><span class="p">,</span> <span class="nx">closestCentroidIndex</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">kDTree</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">var</span> <span class="nx">closestCentroids</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nx">nearestNeighbor</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
                <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">closestCentroids</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
                    <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">closestCentroid</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="kd">let</span> <span class="nx">prevDistance</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kd">let</span> <span class="nx">centroid</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
                    <span class="k">if</span> <span class="p">(</span><span class="nx">j</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">centroid</span><span class="p">;</span>
                        <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">j</span><span class="p">;</span>
                        <span class="nx">prevDistance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">closestCentroid</span><span class="p">);</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="c1">// get distance:</span>
                        <span class="kd">const</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">);</span>
                        <span class="k">if</span> <span class="p">(</span><span class="nx">distance</span> <span class="o">&lt;</span> <span class="nx">prevDistance</span><span class="p">)</span> <span class="p">{</span>
                            <span class="nx">prevDistance</span> <span class="o">=</span> <span class="nx">distance</span><span class="p">;</span>
                            <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">centroid</span><span class="p">;</span>
                            <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">j</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="c1">// add point to centroid labels:</span>
            <span class="nx">labels</span><span class="p">[</span><span class="nx">closestCentroidIndex</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">labels</span><span class="p">;</span>
    <span class="p">}</span>


    <span class="nx">_compareDatasets</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">a</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">b</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span> <span class="p">{</span>
                <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// selects random points as centroids from the dataset</span>
        <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">centroidsIndex</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="kd">let</span> <span class="nx">index</span><span class="p">;</span>
        <span class="k">while</span> <span class="p">(</span><span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">index</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_randomBetween</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">numSamples</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">index</span><span class="p">)</span> <span class="o">===</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">index</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">centroids</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">centroid</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">centroidsIndex</span><span class="p">[</span><span class="nx">i</span><span class="p">]]];</span>
            <span class="nx">centroids</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">centroid</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">centroids</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_getRandomCentroidsNaiveSharding</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// implementation of a variation of naive sharding centroid initialization method</span>
        <span class="c1">// (not using sums or sorting, just dividing into k shards and calc mean)</span>
        <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">step</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">numSamples</span> <span class="o">/</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">centroids</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="nx">step</span> <span class="o">*</span> <span class="nx">i</span><span class="p">;</span>
            <span class="kd">let</span> <span class="nx">end</span> <span class="o">=</span> <span class="nx">step</span> <span class="o">*</span> <span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">===</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">end</span> <span class="o">=</span> <span class="nx">numSamples</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="nx">centroids</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_calcMeanCentroid</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">end</span><span class="p">));</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">centroids</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_calcMeanCentroid</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">end</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">features</span> <span class="o">=</span> <span class="nx">dataSet</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">end</span> <span class="o">-</span> <span class="nx">start</span><span class="p">;</span>
        <span class="kd">let</span> <span class="nx">mean</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">features</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">mean</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">start</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">end</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">features</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">mean</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">mean</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+</span> <span class="nx">dataSet</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="nx">j</span><span class="p">]</span> <span class="o">/</span> <span class="nx">n</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">mean</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">iterations</span> <span class="o">&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">oldCentroids</span> <span class="o">||</span> <span class="o">!</span><span class="nx">oldCentroids</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">oldCentroids</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">let</span> <span class="nx">maxMovement</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">oldCentroids</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
                <span class="nx">maxMovement</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">maxMovement</span><span class="p">,</span> <span class="nx">distance</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="k">if</span> <span class="p">(</span><span class="nx">maxMovement</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">threshold</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kd">let</span> <span class="nx">haveChanged</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_compareDatasets</span><span class="p">(</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">oldCentroids</span><span class="p">[</span><span class="nx">i</span><span class="p">]))</span> <span class="p">{</span>
                    <span class="nx">haveChanged</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="nx">haveChanged</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_recalculateCentroids</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">labels</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Each centroid is the arithmetic mean of the points that</span>
        <span class="c1">// have that centroid's label.  If no points have</span>
        <span class="c1">// a centroid's label, you should randomly re-initialize it.</span>
        <span class="kd">let</span> <span class="nx">newCentroid</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">newCentroidList</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">k</span> <span class="k">in</span> <span class="nx">labels</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">centroidGroup</span> <span class="o">=</span> <span class="nx">labels</span><span class="p">[</span><span class="nx">k</span><span class="p">];</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">centroidGroup</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// find mean:</span>
                <span class="nx">newCentroid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">meanFunc</span><span class="p">(</span><span class="nx">centroidGroup</span><span class="p">.</span><span class="nx">points</span><span class="p">);</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="c1">// get new random centroid</span>
                <span class="nx">newCentroid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
            <span class="p">}</span>
            <span class="nx">newCentroidList</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">newCentroid</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">newCentroidList</span><span class="p">;</span>
    <span class="p">}</span>


    <span class="nx">_arithmeticMean</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">totalPoints</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">means</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">means</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">point</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">point</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">val</span> <span class="o">=</span> <span class="nx">point</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
                <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+</span> <span class="nx">val</span> <span class="o">/</span> <span class="nx">totalPoints</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">means</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_randomBetween</span><span class="p">(</span><span class="nx">min</span><span class="p">,</span> <span class="nx">max</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span>
            <span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="p">(</span><span class="nx">max</span> <span class="o">-</span> <span class="nx">min</span><span class="p">)</span> <span class="o">+</span> <span class="nx">min</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Despite its simplicity and effectiveness, <a href="https://ieeexplore.ieee.org/document/1056489">K-Means</a> has some limitations. One major drawback is that it requires the number of clusters (K) to be specified in advance. Determining the optimal value of K can be challenging, especially when dealing with complex datasets. If the chosen <code class="language-plaintext highlighter-rouge">K</code> does not align with the natural structure of the data, the resulting clusters may not be meaningful or representative.
Moreover, K-Means is sensitive to the initial placement of centroids and may converge to suboptimal solutions. It can also struggle with clusters of different sizes, densities, or non-spherical shapes.</p>

<p>To address these limitations, an adaptive approach that can automatically determine the optimal number of clusters is desirable.</p>

<p>X-Means is an extension of the K-Means algorithm that aims to overcome the limitation of requiring a pre-specified number of clusters. It automatically determines the optimal number of clusters based on the structure of the data.
The key idea behind X-Means is to start with a small number of clusters and iteratively split them into smaller clusters if doing so improves the clustering quality. The algorithm follows these steps:</p>

<ol>
  <li>Initialization: Start with a small number of clusters (e.g., K=1) and apply K-Means to the dataset.</li>
  <li>Splitting: For each existing cluster, consider splitting it into two subclusters. Apply K-Means to each potential split and calculate a model selection criterion to assess the quality of the split.</li>
  <li>Comparison: Compare the model selection criterion scores of the original cluster and the split subclusters. If the split results in a better score, accept the split and update the cluster assignments.</li>
  <li>Iteration: Repeat steps 2 and 3 until no further splits improve the BIC score or a maximum number of iterations is reached.</li>
</ol>

<p>By iteratively splitting clusters and evaluating the resulting models, X-Means can automatically determine the optimal number of clusters that best fit the data. This adaptive approach eliminates the need for manual tuning of the K parameter and allows the algorithm to discover the underlying structure of the dataset more effectively.</p>

<p>X-Means combines the simplicity and efficiency of K-Means with the flexibility of automatically determining the appropriate number of clusters. It provides a more robust and data-driven approach to clustering, making it a valuable tool in various domains such as customer segmentation, anomaly detection, and pattern recognition.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nx">XMeans</span> <span class="kd">extends</span> <span class="nx">KMeans</span> <span class="p">{</span>
    <span class="cm">/**
     * A configurable implementation of the X-means clustering algorithm.
     * The X-means clustering creates between minK and maxK clusters by
     * splitting a cluster into 2 clusters.  The algorithm splits the
     * cluster with contributes most towards the entropy of the model.
     * 
     * When minK === maxK, behaves exactly like KMeans clustering.
     * 
     * @param {int} minK minimum
     * @param {int} maxK max number of clusters to create
     * @param {Function} distanceMetric 
     * @param {object} options
     */</span>

    <span class="kd">constructor</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">maxK</span><span class="p">,</span> <span class="nx">distanceMetric</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">distanceMetric</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">minK</span> <span class="o">&gt;</span> <span class="nx">maxK</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">maxK must be greater than, or equal to minK</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">maxK</span> <span class="o">=</span> <span class="nx">maxK</span><span class="p">;</span>

        <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">splitThreshold</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">splitThreshold</span> <span class="o">||</span> <span class="mf">0.0</span><span class="p">;</span> <span class="c1">// threshold for splitting a cluster</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">bicThreshold</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">bicThreshold</span> <span class="o">||</span> <span class="mf">0.0</span><span class="p">;</span> <span class="c1">// threshold for stopping the clustering</span>
    <span class="p">}</span>

    <span class="nx">transform</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
        <span class="cm">/**
         * Executes the k-means clustering algorithm against a dataset
         * with the given optional optimization parameters.  Parameters are
         * 
         * - validate (bool):  Indicates if the dataset should be validated
         * - kDTree (bool):  Indicates if a KD-tree should be used to find nearest centroids
         * 
         * @param {Array} dataset
         * @param {object} options
         */</span>
        <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
        <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">||</span> <span class="kc">false</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">validate</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">validateDataset</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kd">let</span> <span class="nx">iterations</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="kd">let</span> <span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">;</span>

        <span class="c1">// Initialize centroids randomly</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">useNaiveSharding</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroidsNaiveSharding</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="nx">labels</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">);</span>
        <span class="kd">var</span> <span class="nx">oldBIC</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">.</span><span class="nx">MAX_SAFE_INTEGER</span><span class="p">,</span>
            <span class="nx">currentBIC</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_calculateBIC</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nx">i</span><span class="p">;</span>
            <span class="p">})),</span>
            <span class="nx">currentK</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">;</span>
        <span class="c1">// Run the main X-means algorithm</span>
        <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">,</span> <span class="nx">oldBIC</span><span class="p">,</span> <span class="nx">currentBIC</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="dl">'</span><span class="s1">currentK: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">currentK</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">, maxK: </span><span class="dl">'</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxK</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">, Iteration #</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">iterations</span><span class="p">);</span>
            <span class="c1">// Save old centroids for convergence test.</span>
            <span class="nx">oldCentroids</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">centroids</span><span class="p">];</span>
            <span class="nx">iterations</span><span class="o">++</span><span class="p">;</span>

            <span class="c1">// Assign labels to each datapoint based on centroids</span>
            <span class="nx">labels</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">);</span>
            <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_recalculateCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">);</span>

            <span class="k">if</span> <span class="p">(</span><span class="nx">currentK</span> <span class="o">===</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxK</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="c1">// X-Means calculations</span>

            <span class="c1">// Calculate BIC for each cluster to determine the one with highest BIC</span>
            <span class="kd">const</span> <span class="nx">clusterBICs</span> <span class="o">=</span> <span class="p">[];</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">clusterBIC</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_calculateBIC</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
                <span class="nx">clusterBICs</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
                    <span class="na">clusterIndex</span><span class="p">:</span> <span class="nx">i</span><span class="p">,</span>
                    <span class="na">BIC</span><span class="p">:</span> <span class="nx">clusterBIC</span>
                <span class="p">});</span>
            <span class="p">}</span>
            <span class="nx">clusterBICs</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nx">b</span><span class="p">.</span><span class="nx">BIC</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">BIC</span><span class="p">;</span>
            <span class="p">});</span>
            <span class="kd">const</span> <span class="nx">highestBICClusterIndex</span> <span class="o">=</span> <span class="nx">clusterBICs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clusterIndex</span><span class="p">;</span>

            <span class="c1">// Split the cluster with highest BIC</span>
            <span class="kd">const</span> <span class="nx">splitClusters</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_splitCluster</span><span class="p">(</span><span class="nx">labels</span><span class="p">[</span><span class="nx">highestBICClusterIndex</span><span class="p">],</span> <span class="nx">labels</span><span class="p">);</span>

            <span class="c1">// Calculate BIC for split model</span>
            <span class="kd">let</span> <span class="nx">splitLabels</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">labels</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">splitCentroids</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">splitClusters</span><span class="p">.</span><span class="nx">centroids</span><span class="p">);</span> <span class="c1">// add clusters from split</span>
            <span class="nx">splitCentroids</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">highestBICClusterIndex</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="c1">// remove split cluster</span>
            <span class="nx">splitLabels</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">splitCentroids</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">);</span> <span class="c1">// assign labels based on new centroids</span>
            <span class="kd">const</span> <span class="nx">splitBIC</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_calculateBIC</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">splitLabels</span><span class="p">,</span> <span class="nx">splitCentroids</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nx">i</span><span class="p">;</span>
            <span class="p">}));</span>
            <span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="dl">'</span><span class="s1">current BIC:</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">currentBIC</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">, potential BIC: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">splitBIC</span><span class="p">);</span>

            <span class="c1">// Replace clusters and centroids if BIC is improved</span>
            <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_isImproved</span><span class="p">(</span><span class="nx">currentBIC</span><span class="p">,</span> <span class="nx">splitBIC</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">splitThreshold</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">centroids</span> <span class="o">=</span> <span class="nx">splitCentroids</span><span class="p">;</span>
                <span class="nx">labels</span> <span class="o">=</span> <span class="nx">splitLabels</span><span class="p">;</span>
                <span class="nx">currentK</span><span class="o">++</span><span class="p">;</span>
                <span class="nx">oldBIC</span> <span class="o">=</span> <span class="nx">currentBIC</span><span class="p">;</span>
                <span class="nx">currentBIC</span> <span class="o">=</span> <span class="nx">splitBIC</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="kd">const</span> <span class="nx">clusters</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">cluster</span> <span class="k">in</span> <span class="nx">labels</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">clusters</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">labels</span><span class="p">[</span><span class="nx">cluster</span><span class="p">]);</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="p">{</span>
            <span class="na">clusters</span><span class="p">:</span> <span class="nx">clusters</span><span class="p">,</span>
            <span class="na">centroids</span><span class="p">:</span> <span class="nx">centroids</span><span class="p">,</span>
            <span class="na">iterations</span><span class="p">:</span> <span class="nx">iterations</span><span class="p">,</span>
            <span class="na">converged</span><span class="p">:</span> <span class="nx">iterations</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span><span class="p">,</span>
        <span class="p">};</span>
        <span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_isImproved</span><span class="p">(</span><span class="nx">oldValue</span><span class="p">,</span> <span class="nx">newValue</span><span class="p">,</span> <span class="nx">threshold</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">threshold</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">threshold</span> <span class="o">&lt;</span> <span class="mf">1.0</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">threshold</span> <span class="o">=</span> <span class="nx">threshold</span> <span class="o">*</span> <span class="nx">oldValue</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">oldValue</span> <span class="o">-</span> <span class="nx">newValue</span> <span class="o">&gt;</span> <span class="nx">threshold</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">,</span> <span class="nx">oldBIC</span><span class="p">,</span> <span class="nx">bic</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_isImproved</span><span class="p">(</span><span class="nx">oldBIC</span><span class="p">,</span> <span class="nx">bic</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">bicThreshold</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="k">super</span><span class="p">.</span><span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="nx">_splitCluster</span><span class="p">(</span><span class="nx">cluster</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// Apply k-means with k=2 to the points in the cluster</span>
        <span class="kd">const</span> <span class="nx">kmeans</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">KMeans</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">kmeans</span><span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">cluster</span><span class="p">.</span><span class="nx">points</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="nx">_calculateBIC</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">k</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">let</span> <span class="nx">withinClusterVariance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">centroidIndex</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="kd">const</span> <span class="nx">clusterCentroid</span> <span class="o">=</span> <span class="nx">labels</span><span class="p">[</span><span class="nx">centroidIndex</span><span class="p">].</span><span class="nx">centroid</span><span class="p">;</span>
            <span class="kd">const</span> <span class="nx">clusterPoints</span> <span class="o">=</span> <span class="nx">labels</span><span class="p">[</span><span class="nx">centroidIndex</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>

            <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">clusterPoints</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">squaredDistance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">clusterPoints</span><span class="p">[</span><span class="nx">j</span><span class="p">],</span> <span class="nx">clusterCentroid</span><span class="p">);</span>
                <span class="nx">withinClusterVariance</span> <span class="o">+=</span> <span class="nx">squaredDistance</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">BIC</span> <span class="o">=</span> <span class="nx">n</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">withinClusterVariance</span> <span class="o">/</span> <span class="nx">n</span><span class="p">)</span> <span class="o">+</span> <span class="nx">k</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">n</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">BIC</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">_arithmeticMean</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">totalPoints</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">means</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">means</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">point</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">point</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">val</span> <span class="o">=</span> <span class="nx">point</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
                <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+</span> <span class="nx">val</span> <span class="o">/</span> <span class="nx">totalPoints</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">means</span><span class="p">;</span>
    <span class="p">}</span>

<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This X-Means implementation uses the Bayesian Information Criterion as it’s model selection criterion for splitting clusters.  The Bayesian Information Criterion (BIC) is a statistical measure used to assess the quality of a clustering model. Intuitively, the BIC balances the goodness of fit of the model with the complexity of the model. It takes into account how well the model fits the data (i.e., how close the data points are to their assigned cluster centroids) and penalizes the model for having a large number of clusters. The BIC encourages finding a clustering solution that captures the underlying structure of the data while avoiding overfitting.</p>

<p>A lower BIC value indicates a better clustering model. As the number of clusters increases, the model becomes more complex, and the BIC penalizes this complexity to prevent overfitting. The BIC helps strike a balance between the model’s ability to capture the data’s structure and its simplicity. The BIC is used in this  X-Means implementation to determine whether splitting a cluster into two subclusters improves the overall clustering quality. If the BIC improves significantly after splitting a cluster, it suggests that the split captures meaningful substructures in the data. The algorithm continues splitting clusters until the improvement in BIC falls below a specified threshold, indicating that further splitting does not substantially enhance the clustering quality.</p>

<p>Deciding on an appropriate BIC threshold for the X-Means algorithm can be challenging, as it depends on the specific characteristics of your dataset and the desired granularity of the resulting clusters.  One common approach is to use the <code class="language-plaintext highlighter-rouge">elbow method</code> to run the XMeans algorithm with different BIC thresholds and plot the resulting BIC values against the number of clusters. You can then look for an “elbow” point in the plot, where the rate of improvement in BIC starts to diminish. The BIC threshold corresponding to this elbow point can be a good choice, as it indicates a balance between model complexity and goodness of fit.</p>

<p>Here’s a high-level overview of how to use this <code class="language-plaintext highlighter-rouge">XMeans</code> implementation:</p>

<ol>
  <li>Create an instance of the XMeans class
    <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">minK</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
    <span class="nx">maxK</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
    <span class="nx">distanceMetric</span> <span class="o">=</span> <span class="nx">euclideanDistanceFunc</span><span class="p">,</span>
    <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
      <span class="na">splitThreshold</span><span class="p">:</span> <span class="mf">0.05</span><span class="p">,</span>
      <span class="na">bicThreshold</span><span class="p">:</span> <span class="mf">0.05</span>
    <span class="p">}</span>
<span class="kd">var</span> <span class="nx">clf</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMeans</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">maxK</span><span class="p">,</span> <span class="nx">distanceMetric</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
</code></pre></div>    </div>
    <p>The <code class="language-plaintext highlighter-rouge">splitThreshold</code> and <code class="language-plaintext highlighter-rouge">bicThreshold</code> parameters play important roles in the XMeans algorithm:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">splitThreshold</code>: This parameter determines the threshold for splitting a cluster. If the difference between the current BIC  and the potential BIC after splitting a cluster exceeds this threshold, the cluster is split into two. A higher value makes the algorithm more reluctant to split clusters, while a lower value encourages more splits.</li>
      <li><code class="language-plaintext highlighter-rouge">bicThreshold</code>: This parameter determines the threshold for stopping the clustering process. If the improvement in BIC between iterations falls below this threshold, the algorithm stops further splitting of clusters. A higher value leads to earlier termination of the clustering process, while a lower value allows for more iterations and potentially more clusters.</li>
    </ul>

    <p>If the <code class="language-plaintext highlighter-rouge">splitThreshold</code> or <code class="language-plaintext highlighter-rouge">bicThreshold</code> is greater than <code class="language-plaintext highlighter-rouge">1</code>, the implementation will consider the value as an absolute value and only split a cluster, or end the execution if the BIC is below the specified value. If the specified threshold is between <code class="language-plaintext highlighter-rouge">0</code> and <code class="language-plaintext highlighter-rouge">1</code> in this implementation, the <code class="language-plaintext highlighter-rouge">_isImproved</code> method will interpret the given <code class="language-plaintext highlighter-rouge">*threshold</code> value as a proportion, and split the cluster or end the execution if the BIC has improved by more than the given proportion since the previous iteration.</p>
  </li>
  <li>Call the <code class="language-plaintext highlighter-rouge">transform</code> method on the <code class="language-plaintext highlighter-rouge">XMeans</code> instance, passing in the <code class="language-plaintext highlighter-rouge">data</code> to be clustered:
    <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
        <span class="na">kDTree</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
        <span class="na">validate</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">},</span>
    <span class="nx">results</span> <span class="o">=</span> <span class="nx">clf</span><span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
</code></pre></div>    </div>
    <p>The <code class="language-plaintext highlighter-rouge">XMeans.transform</code> method accepts the following parameters in the <code class="language-plaintext highlighter-rouge">options</code> object:</p>
    <ul>
      <li><strong>KDTree</strong>: Indicates the algorithm is to use a KDTree for identifying th4e nearest centroid to each point in the <code class="language-plaintext highlighter-rouge">data</code>.  Default is <code class="language-plaintext highlighter-rouge">false</code>.</li>
      <li><strong>validate</strong>: Indicates the implementation is verify the <code class="language-plaintext highlighter-rouge">data</code> is a valid dataset: An array of arrays of numbers.</li>
    </ul>
  </li>
</ol>

<h2 id="the-cie2000-color-distance-metric" class="linked-heading">
    The CIE2000 Color Distance Metric<a href="#the-cie2000-color-distance-metric">🔗</a>
</h2>

<p>RGB (Red, Green, Blue) and HSV (Hue, Saturation, Value) are commonly used color spaces in computer vision and image processing. However, they have inherent limitations when it comes to representing perceptual differences between colors.</p>

<p>In the RGB color space, the Euclidean distance between two colors does not accurately reflect the perceived difference by the human visual system. For example, the RGB distance between a dark blue and a light blue may be smaller than the distance between a red and an orange, even though the latter pair appears more similar to the human eye.</p>

<p>Similarly, the HSV color space, while better at separating color information from brightness, still has limitations in representing perceptual differences. The hue component in HSV is represented as an angle, which can lead to discontinuities and inconsistencies when measuring color distances.</p>

<p>To address the limitations of traditional color spaces, the International Commission on Illumination (CIE) developed the CIE2000 color distance metric. CIE2000 is designed to provide a more accurate representation of perceptual color differences, taking into account the complexities of human color perception.</p>

<p>The CIE2000 metric is based on the CIELAB color space, which is a perceptually uniform color space. In CIELAB, colors are represented using three components: <code class="language-plaintext highlighter-rouge">L</code> for lightness, <code class="language-plaintext highlighter-rouge">a</code> for green-red, and <code class="language-plaintext highlighter-rouge">b</code> for blue-yellow. The CIE2000 formula incorporates additional corrections and weighting factors to account for the non-uniformity of color perception across different regions of the color space.</p>

<p>The development of CIE2000 involved extensive research and empirical studies to understand and quantify human color perception. The CIE conducted experiments with human observers to gather data on perceived color differences, which was then used to refine the metric’s parameters and equations.</p>

<p>Using the CIE2000 color distance metric offers several advantages for color comparison and clustering tasks:</p>

<ol>
  <li>Perceptual Accuracy: CIE2000 provides a more perceptually uniform measure of color difference, aligning closely with human color perception. This means that colors that appear similar to the human eye will have smaller CIE2000 distances, while colors that appear more distinct will have larger distances.</li>
  <li>Improved Clustering Results: When used in conjunction with clustering algorithms like K-Means or X-Means, CIE2000 can lead to more meaningful and visually coherent color clusters. By capturing perceptual differences accurately, CIE2000 helps group colors that are perceptually similar, resulting in clusters that align better with human perception.</li>
  <li>Color Consistency: CIE2000 ensures consistent color comparison across different regions of the color space. It takes into account the non-uniformity of color perception, applying appropriate corrections and weighting factors to maintain perceptual uniformity.</li>
  <li>Robustness to Illumination Changes: CIE2000 is designed to be more robust to changes in illumination compared to other color distance metrics. It incorporates corrections for the effect of illumination on color appearance, making it suitable for scenarios where lighting conditions may vary.</li>
</ol>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
</pre></td><td class="code"><pre><span class="kd">function</span> <span class="nx">deltaE2000</span><span class="p">(</span><span class="nx">labA</span><span class="p">,</span> <span class="nx">labB</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/*
    * CIE2000 equation for quantifying perceptual distance between colors.
    * For a very high-level explanation, see https://sensing.konicaminolta.us/us/blog/identifying-color-differences-using-l-a-b-or-l-c-h-coordinates/
    * or for a more mathematical approach: https://zschuessler.github.io/DeltaE/learn/
    * Implementation based on equations from http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html 
    * 
    * Thresholds
    * 0: No perceivable difference between the colors.
    * 0-1: Very slight difference, likely imperceptible to most observers.
    * 1-2: Slight difference, noticeable to trained observers or under careful examination.
    * 2-3.5: Distinct difference, noticeable to most observers.
    * 3.5-5: Significant difference, readily apparent to all observers.
    * Above 5: Very large difference, colors are clearly distinct.

    * Limits
    * 0: Completely identical
    * 100:  Completely different
    * 
    * Weighting factors
    * kL (lightness weighting factor): Controls the influence of lightness differences on the overall Delta E 2000 value.
    * kC (chroma weighting factor): Controls the influence of chroma differences.
    * kH (hue weighting factor): Controls the influence of hue differences.
    * 
    * Higher values of a given factor emphasize that aspect of the color.  Each ranges from 0 to 2, and default to 1.
    */</span>

    <span class="kd">const</span> <span class="nx">l1</span> <span class="o">=</span> <span class="nx">labA</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
        <span class="nx">a1</span> <span class="o">=</span> <span class="nx">labA</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="nx">b1</span> <span class="o">=</span> <span class="nx">labA</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span>
        <span class="nx">l2</span> <span class="o">=</span> <span class="nx">labB</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
        <span class="nx">a2</span> <span class="o">=</span> <span class="nx">labB</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="nx">b2</span> <span class="o">=</span> <span class="nx">labB</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>

    <span class="c1">// missing utility functions added to Math Object</span>
    <span class="nb">Math</span><span class="p">.</span><span class="nx">rad2deg</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">rad</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">360</span> <span class="o">*</span> <span class="nx">rad</span> <span class="o">/</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">PI</span><span class="p">);</span>
    <span class="p">};</span>
    <span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">deg</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">PI</span> <span class="o">*</span> <span class="nx">deg</span><span class="p">)</span> <span class="o">/</span> <span class="mi">360</span><span class="p">;</span>
    <span class="p">};</span>
    
    <span class="kd">const</span> <span class="nx">avgL</span> <span class="o">=</span> <span class="p">(</span><span class="nx">l1</span> <span class="o">+</span> <span class="nx">l2</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">c1</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">a1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">b1</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">c2</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">a2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">b2</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">avgC</span> <span class="o">=</span> <span class="p">(</span><span class="nx">c1</span> <span class="o">+</span> <span class="nx">c2</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">g</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgC</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgC</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="mi">7</span><span class="p">))))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">a1p</span> <span class="o">=</span> <span class="nx">a1</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="nx">g</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">a2p</span> <span class="o">=</span> <span class="nx">a2</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="nx">g</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">c1p</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">a1p</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">b1</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">c2p</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">a2p</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">b2</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>

    <span class="kd">const</span> <span class="nx">avgCp</span> <span class="o">=</span> <span class="p">(</span><span class="nx">c1p</span> <span class="o">+</span> <span class="nx">c2p</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>

    <span class="kd">let</span> <span class="nx">h1p</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">rad2deg</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">atan2</span><span class="p">(</span><span class="nx">b1</span><span class="p">,</span> <span class="nx">a1p</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">h1p</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">h1p</span> <span class="o">=</span> <span class="nx">h1p</span> <span class="o">+</span> <span class="mi">360</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">let</span> <span class="nx">h2p</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">rad2deg</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">atan2</span><span class="p">(</span><span class="nx">b2</span><span class="p">,</span> <span class="nx">a2p</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">h2p</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">h2p</span> <span class="o">=</span> <span class="nx">h2p</span> <span class="o">+</span> <span class="mi">360</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">avghp</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">h1p</span> <span class="o">-</span> <span class="nx">h2p</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">180</span> <span class="p">?</span> <span class="p">(</span><span class="nx">h1p</span> <span class="o">+</span> <span class="nx">h2p</span> <span class="o">+</span> <span class="mi">360</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span> <span class="p">:</span> <span class="p">(</span><span class="nx">h1p</span> <span class="o">+</span> <span class="nx">h2p</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="mf">0.17</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">cos</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="nx">avghp</span> <span class="o">-</span> <span class="mi">30</span><span class="p">))</span> <span class="o">+</span> <span class="mf">0.24</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">cos</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nx">avghp</span><span class="p">))</span> <span class="o">+</span> <span class="mf">0.32</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">cos</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="mi">3</span> <span class="o">*</span> <span class="nx">avghp</span> <span class="o">+</span> <span class="mi">6</span><span class="p">))</span> <span class="o">-</span> <span class="mf">0.2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">cos</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="mi">4</span> <span class="o">*</span> <span class="nx">avghp</span> <span class="o">-</span> <span class="mi">63</span><span class="p">));</span>

    <span class="kd">let</span> <span class="nx">deltahp</span> <span class="o">=</span> <span class="nx">h2p</span> <span class="o">-</span> <span class="nx">h1p</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">deltahp</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">180</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">h2p</span> <span class="o">&lt;=</span> <span class="nx">h1p</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">deltahp</span> <span class="o">+=</span> <span class="mi">360</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nx">deltahp</span> <span class="o">-=</span> <span class="mi">360</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">deltalp</span> <span class="o">=</span> <span class="nx">l2</span> <span class="o">-</span> <span class="nx">l1</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">deltacp</span> <span class="o">=</span> <span class="nx">c2p</span> <span class="o">-</span> <span class="nx">c1p</span><span class="p">;</span>

    <span class="nx">deltahp</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nx">c1p</span> <span class="o">*</span> <span class="nx">c2p</span><span class="p">)</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sin</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="nx">deltahp</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">sl</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">+</span> <span class="p">((</span><span class="mf">0.015</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgL</span> <span class="o">-</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="o">/</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="mi">20</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgL</span> <span class="o">-</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">2</span><span class="p">)));</span>
    <span class="kd">const</span> <span class="nx">sc</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">+</span> <span class="mf">0.045</span> <span class="o">*</span> <span class="nx">avgCp</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">sh</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">+</span> <span class="mf">0.015</span> <span class="o">*</span> <span class="nx">avgCp</span> <span class="o">*</span> <span class="nx">t</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">deltaro</span> <span class="o">=</span> <span class="mi">30</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">exp</span><span class="p">(</span><span class="o">-</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">avghp</span> <span class="o">-</span> <span class="mi">275</span><span class="p">)</span> <span class="o">/</span> <span class="mi">25</span><span class="p">,</span> <span class="mi">2</span><span class="p">)));</span>
    <span class="kd">const</span> <span class="nx">rc</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgCp</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">avgCp</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="mi">7</span><span class="p">)));</span>
    <span class="kd">const</span> <span class="nx">rt</span> <span class="o">=</span> <span class="o">-</span><span class="nx">rc</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sin</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">deg2rad</span><span class="p">(</span><span class="nx">deltaro</span><span class="p">));</span>

    <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>

    <span class="cm">/*
        * kl (lightness) weighting factor ( 0.0 &lt;= kC &lt;= 2.0)
        * Increasing kL amplifies the importance of lightness differences.
        * This can be useful for applications where subtle lightness variations are crucial, like in textile or paint industries.
        * Conversely, lowering kL downplays lightness changes, which might be beneficial for web design where screen brightness can affect perceived lightness. 
        * */</span>
    <span class="kd">const</span> <span class="nx">kl</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span>
            <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span>
                <span class="nx">options</span><span class="p">.</span><span class="nx">kl</span> <span class="o">||</span> <span class="mf">1.0</span><span class="p">,</span>
                <span class="mf">0.0</span><span class="p">),</span>
            <span class="mf">2.0</span>
        <span class="p">);</span>
    <span class="cm">/*
        * kC (chroma) weighting factor ( 0.0 &lt;= kC &lt;= 2.0)
        * Adjusting kC modifies the influence of chroma (color saturation) in the calculation.
        * Raising kC highlights chroma differences, making vibrant colors stand out more.
        * Lowering it reduces the impact of chroma variations, potentially minimizing the appearance of color shifts due to factors like lighting or viewing angle. 
        * */</span>
    <span class="kd">const</span> <span class="nx">kc</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span>
            <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span>
                <span class="nx">options</span><span class="p">.</span><span class="nx">kC</span> <span class="o">||</span> <span class="mf">1.0</span><span class="p">,</span>
                <span class="mf">0.0</span><span class="p">),</span>
            <span class="mf">2.0</span>
        <span class="p">);</span>
    <span class="cm">/*
        * kH (hue) weighting factor ( 0.0 &lt;= kC &lt;= 2.0)
        * Modifying kH alters the emphasis on hue (color tint) differences.Modifying kH alters the emphasis on hue (color tint) differences.
        * This is rarely used in practical applications due to the complexity of hue calculations and its potentially limited impact on perceived color changes in most contexts.
        * */</span>
    <span class="kd">const</span> <span class="nx">kh</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span>
            <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span>
                <span class="nx">options</span><span class="p">.</span><span class="nx">kH</span> <span class="o">||</span> <span class="mf">1.0</span><span class="p">,</span>
                <span class="mf">0.0</span><span class="p">),</span>
            <span class="mf">2.0</span>
        <span class="p">);</span>

    <span class="kd">const</span> <span class="nx">deltaE</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">deltalp</span> <span class="o">/</span> <span class="p">(</span><span class="nx">kl</span> <span class="o">*</span> <span class="nx">sl</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">deltacp</span> <span class="o">/</span> <span class="p">(</span><span class="nx">kc</span> <span class="o">*</span> <span class="nx">sc</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">deltahp</span> <span class="o">/</span> <span class="p">(</span><span class="nx">kh</span> <span class="o">*</span> <span class="nx">sh</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="nx">rt</span> <span class="o">*</span> <span class="p">(</span><span class="nx">deltacp</span> <span class="o">/</span> <span class="p">(</span><span class="nx">kc</span> <span class="o">*</span> <span class="nx">sc</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="nx">deltahp</span> <span class="o">/</span> <span class="p">(</span><span class="nx">kh</span> <span class="o">*</span> <span class="nx">sh</span><span class="p">)));</span>
    <span class="k">return</span> <span class="nx">deltaE</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">MIN</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">MAX</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>

<span class="cm">/**
    * Just Noticeable Difference threshold.
    * Represents the smallest color difference that a typical observer can perceive under specific viewing conditions.
    * For Delta E 2000, a JND value of around 1 is generally accepted. This means a Delta E 2000 difference of 1 or
    * less is often considered imperceptible to most people.
    */</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">JND</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">;</span>
<span class="cm">/**
    * Barely Noticeable Difference threshold.
    * The color difference is barely perceptible. Only experienced observers under favorable viewing conditions may be able to
    * detect the difference.
    */</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">BND</span> <span class="o">=</span> <span class="mf">2.0</span><span class="p">;</span>
<span class="cm">/**
    * Noticeable Difference threshold.
    * The color difference is noticeable but still considered acceptable in many applications. It represents a good color match.
    */</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">ND</span> <span class="o">=</span> <span class="mf">3.5</span><span class="p">;</span>
<span class="cm">/**
    * Apparent Difference threshold.
    * The color difference is becoming more apparent and may be unacceptable in certain applications requiring precise color matching.
    */</span>
<span class="nx">deltaE2000</span><span class="p">.</span><span class="nx">AD</span> <span class="o">=</span> <span class="mf">5.0</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>In this code, the <code class="language-plaintext highlighter-rouge">deltaE2000</code> function takes two <code class="language-plaintext highlighter-rouge">LAB</code> colors as input and computes the CIE2000 color distance between them. The implementation follows the steps outlined in the CIE2000 formula, including the calculation of various intermediate values and corrections.</p>

<h3 id="cie2000-tuning" class="linked-heading">
    CIE2000 Tuning<a href="#cie2000-tuning">🔗</a>
</h3>

<p>The CIE2000 color distance metric provides a way to measure the perceptual difference between two colors, taking into account various aspects of color perception. One of the powerful features of CIE2000 is the ability to customize the perceptual distance calculation by adjusting the weight given to different color components. This is achieved through the use of three optional parameters: <code class="language-plaintext highlighter-rouge">kL</code>, <code class="language-plaintext highlighter-rouge">kC</code>, and <code class="language-plaintext highlighter-rouge">kH</code>, which are all <code class="language-plaintext highlighter-rouge">1</code> by default.</p>

<ul>
  <li>kL (Lightness Weight): The <code class="language-plaintext highlighter-rouge">kL</code> parameter controls the weight given to the lightness component (L*) in the perceptual distance calculation.
    <ul>
      <li>Increasing the value of <code class="language-plaintext highlighter-rouge">kL</code> will make the perceptual distance more sensitive to lightness differences, while decreasing it will reduce the impact of lightness variations.  Adjusting <code class="language-plaintext highlighter-rouge">kL</code> can be useful in scenarios where lightness differences are more or less important than other color aspects.</li>
      <li>Increase Example: When comparing colors in a low-light environment or dealing with dark colors, increasing kL can help emphasize lightness differences and improve the perceptual distance measurement.</li>
      <li>Decrease Example: In scenarios where the colors are primarily distinguished by their chroma and hue, such as comparing colors in a well-lit environment, decreasing kL can be appropriate.</li>
    </ul>
  </li>
  <li>kC (Chroma Weight): The <code class="language-plaintext highlighter-rouge">kC</code> parameter determines the weight given to the chroma component (C*) in the perceptual distance calculation.
    <ul>
      <li>Increasing <code class="language-plaintext highlighter-rouge">kC</code> will make the perceptual distance more sensitive to chroma (saturation) differences, while decreasing it will reduce the impact of chroma variations.  Modifying <code class="language-plaintext highlighter-rouge">kC</code> can be beneficial when dealing with colors where chroma differences are of particular importance or when you want to emphasize or de-emphasize the role of saturation in the distance calculation.</li>
      <li>Increase Example: If you are comparing colors in a context where vividness and saturation are key factors, such as in graphic design or art, increasing kC can be beneficial.</li>
      <li>Decrease Example: If you are working with pastel or desaturated colors, where chroma variations are subtle, decreasing kC can be appropriate.</li>
    </ul>
  </li>
  <li>kH (Hue Weight): The <code class="language-plaintext highlighter-rouge">kH</code> parameter controls the weight given to the hue component (H*) in the perceptual distance calculation.
    <ul>
      <li>Increasing <code class="language-plaintext highlighter-rouge">kH</code> will make the perceptual distance more sensitive to hue differences, while decreasing it will reduce the impact of hue variations.  Adjusting <code class="language-plaintext highlighter-rouge">kH</code> can be useful when working with colors where hue differences are more or less significant than other color aspects.</li>
      <li>Increase Example: If you are working with a color dataset where colors are primarily distinguished by their hue, such as in color categorization or classification tasks, increasing kH can be beneficial.</li>
      <li>Decrease Example:  If you are comparing colors in a context where hue shifts are common or expected, such as in color correction or white balance adjustments, decreasing kH can be appropriate.</li>
    </ul>
  </li>
</ul>

<p>In a color search application for fashion, where users are looking for similar shades of a particular color, increasing <code class="language-plaintext highlighter-rouge">kC</code> and <code class="language-plaintext highlighter-rouge">kH</code> while slightly decreasing <code class="language-plaintext highlighter-rouge">kL</code> can help prioritize chroma and hue similarities over lightness variations.</p>

<p>When analyzing color differences in a printing process, where maintaining consistent lightness is crucial, increasing <code class="language-plaintext highlighter-rouge">kL</code> while keeping <code class="language-plaintext highlighter-rouge">kC</code> and <code class="language-plaintext highlighter-rouge">kH</code> at their default values can help ensure that lightness variations are given more weight in the distance calculation.</p>

<p>In a color palette generator tool, where creating visually appealing and harmonious color schemes is the goal, increasing kH while slightly decreasing <code class="language-plaintext highlighter-rouge">kL</code> and <code class="language-plaintext highlighter-rouge">kC</code> can help prioritize hue relationships and create palettes with balanced color combinations.</p>

<p>The interpretation of the <code class="language-plaintext highlighter-rouge">deltaE2000</code> distance values can vary depending on the context and application, but some thresholds for general guidelines are included for gauging the perceptual difference from the distance metric which may prove useful in post-processing and analysis of dominant colors.</p>

<h2 id="dominant-color-extraction-with-x-means-and-cie2000" class="linked-heading">
    Dominant Color Extraction with X-Means and CIE2000<a href="#dominant-color-extraction-with-x-means-and-cie2000">🔗</a>
</h2>

<p>Extracting the dominant colors from an image takes the following steps:</p>

<ol>
  <li>Image Loading: Begin by loading the image into your program or website.</li>
  <li>Color Space Conversion: Convert the image from its original color space (e.g., RGB) to the CIELAB color space. The CIELAB color space is perceptually uniform and is the basis for the CIE2000 color distance metric. This conversion ensures that the color distances calculated in later steps are perceptually meaningful.</li>
  <li>Pixel Extraction: Extract the pixel values from the image. Each pixel should be represented as a three-dimensional vector containing the color of the pixel in the CIELAB color space.</li>
  <li>Preprocessing the Image Data for Clustering:
    <ul>
      <li>Pixel Sampling: Depending on the size of the image and computational resources available, we may sample a subset of pixels for clustering. This can be done by randomly selecting a fixed number of pixels or by using a systematic sampling approach.</li>
    </ul>
  </li>
  <li>Execute the X-Means Algorithm.  Initialize the X-Means algorithm with the preprocessed pixel data. Specify the initial number of clusters (e.g., K=1) and set the CIE2000 (<code class="language-plaintext highlighter-rouge">deltaE2000</code>) color distance metric as the distance function.</li>
  <li>Dominant Color Representation: Once the X-Means clustering is complete, the cluster centroids represent the dominant colors in the image. These centroids are typically stored as CIELAB color values and can be converted back to the original color space (e.g., RGB) for visualization or further processing.</li>
  <li>Post-processing and Analysis: This can include merging similar colors, filtering out less significant colors based on their cluster sizes, or sorting the colors based on their prominence in the image.</li>
</ol>

<h3 id="color-space-conversion" class="linked-heading">
    Color Space Conversion<a href="#color-space-conversion">🔗</a>
</h3>

<p>First we need to convert the pixels in our image from RGB to the LAB color space, as the <code class="language-plaintext highlighter-rouge">deltaE2000</code> function expects the colors to be in the CIELAB color space:</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">rgb2lab</span><span class="p">(</span><span class="nx">rgba</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">r</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
        <span class="nx">g</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
        <span class="nx">b</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
        <span class="nx">alpha</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="c1">// alpha, untouched</span>
        <span class="nx">x</span><span class="p">,</span>
        <span class="nx">y</span><span class="p">,</span>
        <span class="nx">z</span><span class="p">;</span>

    <span class="nx">r</span> <span class="o">=</span> <span class="nx">r</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">r</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">r</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>
    <span class="nx">g</span> <span class="o">=</span> <span class="nx">g</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">g</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">g</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>
    <span class="nx">b</span> <span class="o">=</span> <span class="nx">b</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">b</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">b</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>

    <span class="nx">x</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.4124</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.3576</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.1805</span><span class="p">)</span> <span class="o">/</span> <span class="mf">0.95047</span><span class="p">;</span>
    <span class="nx">y</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.2126</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.7152</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.0722</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.0</span><span class="p">;</span>
    <span class="nx">z</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.0193</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.1192</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.9505</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.08883</span><span class="p">;</span>

    <span class="nx">x</span> <span class="o">=</span> <span class="nx">x</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">x</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>
    <span class="nx">y</span> <span class="o">=</span> <span class="nx">y</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">y</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">y</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>
    <span class="nx">z</span> <span class="o">=</span> <span class="nx">z</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">z</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">z</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>

    <span class="k">return</span> <span class="p">[</span><span class="mi">116</span> <span class="o">*</span> <span class="nx">y</span> <span class="o">-</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">500</span> <span class="o">*</span> <span class="p">(</span><span class="nx">x</span> <span class="o">-</span> <span class="nx">y</span><span class="p">),</span> <span class="mi">200</span> <span class="o">*</span> <span class="p">(</span><span class="nx">y</span> <span class="o">-</span> <span class="nx">z</span><span class="p">),</span> <span class="nx">alpha</span><span class="p">];</span>
<span class="p">}</span></code></pre></figure>

<h3 id="clustering-with-x-means" class="linked-heading">
    Clustering with X-Means<a href="#clustering-with-x-means">🔗</a>
</h3>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">clusterPixels</span><span class="p">(</span><span class="nx">imgElement</span><span class="p">,</span> <span class="nx">minK</span><span class="p">,</span> <span class="nx">maxK</span><span class="p">,</span> <span class="nx">maxIterations</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">imgData</span> <span class="o">=</span> <span class="nx">loadData</span><span class="p">(</span><span class="nx">imgElement</span><span class="p">),</span>
        <span class="nx">data</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">rgbColor</span> <span class="o">=</span> <span class="p">[</span><span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">],</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">],</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">3</span><span class="p">]],</span>
            <span class="nx">labColor</span> <span class="o">=</span> <span class="nx">rgb2lab</span><span class="p">(</span><span class="nx">rgbColor</span><span class="p">);</span>
        <span class="nx">data</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">labColor</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="kd">var</span> <span class="nx">clusterer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMeans</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">maxK</span><span class="p">,</span> <span class="nx">euclidean</span><span class="p">,</span> <span class="p">{</span><span class="na">maxIterations</span><span class="p">:</span> <span class="nx">maxIterations</span><span class="p">});</span>
    <span class="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">clusterer</span><span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="p">{</span><span class="na">kDTree</span><span class="p">:</span> <span class="kc">false</span><span class="p">});</span>
    <span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">loadData</span><span class="p">(</span><span class="nx">imgElement</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">canvas</span><span class="dl">"</span><span class="p">),</span>
        <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">imgElement</span><span class="p">.</span><span class="nx">width</span><span class="p">;</span>
    <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">imgElement</span><span class="p">.</span><span class="nx">height</span><span class="p">;</span>
    <span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">imgElement</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">imageData</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">getImageData</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">imgElement</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">imgElement</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">imageData</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h3 id="post-processing-and-analysis" class="linked-heading">
    Post-processing and Analysis<a href="#post-processing-and-analysis">🔗</a>
</h3>

<p>Post-processing allows you to refine the extracted dominant colors and create visually cohesive color palettes.  By analyzing the identified colors, we can prioritize dominant colors, merge similar shades, remove outlying colors with a low proportion of pixels, or adjust the color representation to suit specific design or application requirements.  This refinement process ensures that the resulting color palettes are aesthetically pleasing and representative of the image’s overall color scheme.</p>

<h4 id="evaluate-quality-of-clusters" class="linked-heading">
    Evaluate Quality of Clusters<a href="#evaluate-quality-of-clusters">🔗</a>
</h4>
<p>Clustering evaluation helps assess the quality of the color clusters in terms of compactness and separation. Compactness refers to how tightly the colors within a cluster are grouped together, while separation refers to how well-distinguished the clusters are from each other. Evaluation metrics such as silhouette score or Davies-Bouldin index can quantify these aspects. A high silhouette score indicates that colors within a cluster are more similar to each other than to colors in other clusters, suggesting well-defined and distinct color groups. By evaluating cluster quality, you can determine if the XMeans algorithm is effectively partitioning the color space and creating cohesive color clusters.</p>

<p>Clustering evaluation can also guide the selection of appropriate parameters for the XMeans algorithm, such as the BIC threshold or the range of the number of clusters (<code class="language-plaintext highlighter-rouge">minK</code> and <code class="language-plaintext highlighter-rouge">maxK</code>). By evaluating the clustering results for different parameter settings, you can identify the combination that yields the best color clusters. For example, you can compare the average silhouette score or other evaluation metrics for different BIC thresholds and choose the threshold that produces the most perceptually meaningful and well-separated color clusters.</p>

<h5 id="centroid-distance-and-intracluster-distance" class="linked-heading">
    Centroid distance and Intracluster distance<a href="#centroid-distance-and-intracluster-distance">🔗</a>
</h5>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">meanDistanceBetweenCentroids</span><span class="p">(</span><span class="nx">clusters</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
        <span class="nx">centroids</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">cluster</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">cluster</span><span class="p">.</span><span class="nx">centroid</span><span class="p">;</span> <span class="p">});</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">output</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">centroid</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">output</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">meanDistanceWithinClusters</span><span class="p">(</span><span class="nx">clusters</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">output</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">centroid</span><span class="p">,</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">output</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h5 id="silhouette-coefficient" class="linked-heading">
    Silhouette Coefficient<a href="#silhouette-coefficient">🔗</a>
</h5>

<p>The <a href="https://en.wikipedia.org/wiki/Silhouette_(clustering)">Silhouette Coefficient</a> is an intuitive measure of how well each data point fits into its assigned cluster compared to other clusters. It quantifies the notion of “belonging” by considering the similarity of a data point to its own cluster and its dissimilarity to the nearest neighboring cluster. A high Silhouette Coefficient indicates that a data point is well-matched to its assigned cluster and poorly matched to neighboring clusters, suggesting a good clustering result. Conversely, a low or negative Silhouette Coefficient suggests that a data point may be on the border between clusters or even assigned to the wrong cluster. By computing the average Silhouette Coefficient across all data points, you can assess the overall quality and consistency of the clustering solution. A higher average Silhouette Coefficient indicates better-defined and more separated clusters, while a lower value suggests overlapping or less distinct clusters. Interpreting the Silhouette Coefficient provides valuable insights into the effectiveness of the clustering algorithm and helps in determining the optimal number of clusters for a given dataset.</p>

<p>A value close to 1 indicates that the color data point is well-matched to its assigned cluster and poorly matched to neighboring clusters, suggesting a good clustering result.  A value close to 0 suggests that the color data point is on the border between two clusters.  A negative value indicates that the color data point may have been assigned to the wrong cluster, as it is closer to points in a neighboring cluster.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
</pre></td><td class="code"><pre><span class="cm">/**
* Calculates the Silhouette coefficient for all points in a given clustering result.
*
* The Silhouette coefficient is a widely used metric for evaluating the quality of clustering.
* It measures how well data points are assigned to their clusters by considering both the intra-cluster
* distance (distance between points within the same cluster) and the inter-cluster distance
* (distance between points in different clusters).
*
* @param {Array&lt;Array&lt;number&gt;&gt;} dataset - The dataset used for clustering.
* @param {Array&lt;object&gt;} clusters - Array of cluster labels for each data point.
*
* @returns {Array} The Silhouette coefficient  for each cluster.
*/</span>
<span class="nx">silhouetteCoefficient</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">clusters</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">numClusters</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">silhouetteScores</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">numSamples</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numSamples</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">point</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
        <span class="kd">let</span> <span class="nx">clusterIndex</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">k</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">k</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">k</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">point</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">clusterIndex</span> <span class="o">=</span> <span class="nx">k</span><span class="p">;</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="kd">const</span> <span class="nx">clusterPoints</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">clusterIndex</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">withinClusterDist</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">point</span><span class="p">,</span> <span class="nx">clusterPoints</span><span class="p">);</span>

        <span class="kd">let</span> <span class="nx">nearestClusterDist</span> <span class="o">=</span> <span class="kc">Infinity</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">k</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">k</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">k</span> <span class="o">!==</span> <span class="nx">clusterIndex</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">otherClusterPoints</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">k</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>
                <span class="kd">const</span> <span class="nx">distToCluster</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">point</span><span class="p">,</span> <span class="nx">otherClusterPoints</span><span class="p">);</span>
                <span class="nx">nearestClusterDist</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span><span class="nx">nearestClusterDist</span><span class="p">,</span> <span class="nx">distToCluster</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="kd">const</span> <span class="nx">silhouetteScore</span> <span class="o">=</span> <span class="p">(</span><span class="nx">nearestClusterDist</span> <span class="o">-</span> <span class="nx">withinClusterDist</span><span class="p">)</span> <span class="o">/</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">withinClusterDist</span><span class="p">,</span> <span class="nx">nearestClusterDist</span><span class="p">);</span>
        <span class="nx">silhouetteScores</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">silhouetteScore</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">silhouetteScores</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">point</span><span class="p">,</span> <span class="nx">points</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{},</span>
        <span class="nx">numPoints</span> <span class="o">=</span> <span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">let</span> <span class="nx">totalDist</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numPoints</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">otherPoint</span> <span class="o">=</span> <span class="nx">points</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
        <span class="kd">const</span> <span class="nx">dist</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">point</span><span class="p">,</span> <span class="nx">otherPoint</span><span class="p">);</span>
        <span class="nx">totalDist</span> <span class="o">+=</span> <span class="nx">dist</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">totalDist</span> <span class="o">/</span> <span class="nx">numPoints</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h5 id="davies-bouldin-index" class="linked-heading">
    Davies-Bouldin Index<a href="#davies-bouldin-index">🔗</a>
</h5>

<p>The <a href="https://en.wikipedia.org/wiki/Davies%E2%80%93Bouldin_index">Davies-Bouldin Index</a> is an intuitive measure of the compactness and separation of clusters in a clustering solution. It quantifies the idea that good clusters should be both compact (low intra-cluster distances) and well-separated from other clusters (high inter-cluster distances). The Davies-Bouldin Index calculates the average similarity between each cluster and its most similar cluster, taking into account the size of the clusters and the distances between their centroids.</p>

<p>A lower Davies-Bouldin Index indicates better clustering, as it suggests that the clusters are more compact and have larger distances between them. Conversely, a higher Davies-Bouldin Index suggests that the clusters are less compact and have smaller distances between them, indicating a less optimal clustering solution.</p>

<p>By comparing the Davies-Bouldin Index across different clustering algorithms or parameter settings, you can determine which approach yields the most compact and well-separated clusters. The Davies-Bouldin Index is particularly useful when the true number of clusters is unknown, as it can help in selecting the optimal number of clusters that minimizes the index value. Interpreting the Davies-Bouldin Index provides insights into the quality of the clustering solution and aids in comparing and selecting the best clustering approach for a given dataset.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
</pre></td><td class="code"><pre><span class="cm">/**
* The Davies-Bouldin Index (DBI) is a cluster validity measure used to assess the quality of clustering results.
* It considers both the within-cluster scatter (compactness) and the between-cluster separation of clusters.
* Lower DBI values indicate better clustering, with a theoretical minimum of 0 representing perfectly separated clusters.
* 
* Lower scores indicate better clustering.
* A score of 0 indicates perfectly separated clusters.
* Higher scores indicate more overlap between clusters.
* 
* Limitations:
* DBI can be sensitive to the size and shape of clusters.
* It may not be suitable for high-dimensional data.
* 
* @param {*} dataset 
* @param {*} labels 
* @param {*} centroids 
* @returns {Array} The Davies-Bouldin Index scores for each cluster (a floating-point number).
*/</span>
<span class="nx">daviesBouldinIndex</span><span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">numClusters</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">dbiScores</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">numClusters</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">clusterPoints</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">withinClusterDist</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">clusterPoints</span><span class="p">);</span>

        <span class="kd">let</span> <span class="nx">maxScore</span> <span class="o">=</span> <span class="o">-</span><span class="kc">Infinity</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">!==</span> <span class="nx">j</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">otherClusterPoints</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">j</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>
                <span class="c1">// Calculate the distance between the centroids of the current cluster and the other cluster</span>
                <span class="kd">const</span> <span class="nx">betweenClusterDist</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">j</span><span class="p">]);</span>
                <span class="c1">// Calculate the average distance from the centroid of the other cluster to the points within that cluster</span>
                <span class="kd">const</span> <span class="nx">otherWithinClusterDist</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_avgDistanceToPoints</span><span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">j</span><span class="p">],</span> <span class="nx">otherClusterPoints</span><span class="p">);</span>
                <span class="c1">// Calculate the score using the formula: (withinClusterDist + otherWithinClusterDist) / betweenClusterDist</span>
                <span class="kd">const</span> <span class="nx">score</span> <span class="o">=</span> <span class="p">(</span><span class="nx">withinClusterDist</span> <span class="o">+</span> <span class="nx">otherWithinClusterDist</span><span class="p">)</span> <span class="o">/</span> <span class="nx">betweenClusterDist</span><span class="p">;</span>
                <span class="c1">// Update the maximum score if the current score is higher</span>
                <span class="nx">maxScore</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">maxScore</span><span class="p">,</span> <span class="nx">score</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="nx">dbiScores</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">maxScore</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">dbiScores</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h5 id="the-calinski-harabasz-index" class="linked-heading">
    The Calinski Harabasz Index<a href="#the-calinski-harabasz-index">🔗</a>
</h5>

<p>The <a href="https://en.wikipedia.org/wiki/Calinski%E2%80%93Harabasz_index">Calinski-Harabasz Index</a>, also known as the Variance Ratio Criterion, is an intuitive measure of the quality of a clustering solution that balances the within-cluster compactness and between-cluster separation. It quantifies the idea that a good clustering solution should have clusters that are both internally homogeneous (low within-cluster variance) and well-separated from each other (high between-cluster variance). The Calinski-Harabasz Index is calculated as the ratio of the between-cluster dispersion to the within-cluster dispersion, taking into account the number of clusters and the total number of data points.</p>

<p>A higher Calinski-Harabasz Index indicates better clustering, as it suggests that the clusters are well-separated and have low internal variability. Conversely, a lower Calinski-Harabasz Index suggests that the clusters are less separated and have higher internal variability, indicating a less optimal clustering solution.</p>

<p>By comparing the Calinski-Harabasz Index across different clustering algorithms or parameter settings, you can determine which approach yields the most well-defined and separated clusters. The Calinski-Harabasz Index is particularly useful when the true number of clusters is unknown, as it can help in selecting the optimal number of clusters that maximizes the index value. Interpreting the Calinski-Harabasz Index provides insights into the quality of the clustering solution and aids in comparing and selecting the best clustering approach for a given dataset.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
</pre></td><td class="code"><pre><span class="cm">/**
* The Calinski Harabasz Index (CH Index), also known as the variance ratio criterion,
* is an internal cluster validity index used to assess the quality of clustering results.
* It measures the ratio of between-cluster dispersion to within-cluster dispersion. In simpler terms,
* it checks how well-separated the clusters are and how compact they are within themselves.
* 
* Interpretation:
* Higher CH Index scores indicate better clustering results.
* There's no fixed upper bound, but generally, higher scores suggest better separation and compactness of clusters.
* Scores closer to 0 indicate poorly separated or scattered clusters.
* 
* Advantages:
* Simple to calculate and interpret.
* Suitable for comparing clustering results for different numbers of clusters (k).
* Sensitive to both cluster separation and within-cluster compactness.
* 
* Disadvantages:
* Can be sensitive to the choice of distance metric.
* May not be reliable for high-dimensional data or complex cluster shapes.
* 
* Calinski, T., &amp; Harabasz, J. (1974). A dendrite method for cluster analysis. Communications in Statistics-theory and Methods, 3(1), 1-27.
* 
* @param {Array} dataset 
* @param {object} results
* @returns {Number}
*/</span>
<span class="nx">calinskiHarabaszIndex</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">results</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Get the number of clusters and data points</span>
    <span class="kd">const</span> <span class="nx">numClusters</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>

    <span class="c1">// Calculate the overall centroid of the dataset</span>
    <span class="kd">const</span> <span class="nx">overallCentroid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_calculateCentroid</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>

    <span class="c1">// Calculate the between-cluster sum of squares (SSB)</span>
    <span class="kd">let</span> <span class="nx">ssb</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">clusterSize</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">clusterCentroid</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
        <span class="kd">const</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">clusterCentroid</span><span class="p">,</span> <span class="nx">overallCentroid</span><span class="p">);</span>
        <span class="nx">ssb</span> <span class="o">+=</span> <span class="nx">clusterSize</span> <span class="o">*</span> <span class="nx">distance</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Calculate the within-cluster sum of squares (SSW)</span>
    <span class="kd">let</span> <span class="nx">ssw</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numClusters</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">clusterPoints</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">;</span>
        <span class="kd">const</span> <span class="nx">clusterCentroid</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">clusterPoints</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">point</span> <span class="o">=</span> <span class="nx">clusterPoints</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
            <span class="kd">const</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">point</span><span class="p">,</span> <span class="nx">clusterCentroid</span><span class="p">);</span>
            <span class="nx">ssw</span> <span class="o">+=</span> <span class="nx">distance</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// Calculate the Calinski-Harabasz Index</span>
    <span class="kd">const</span> <span class="nx">chIndex</span> <span class="o">=</span> <span class="p">(</span><span class="nx">ssb</span> <span class="o">/</span> <span class="p">(</span><span class="nx">numClusters</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">ssw</span> <span class="o">/</span> <span class="p">(</span><span class="nx">numSamples</span> <span class="o">-</span> <span class="nx">numClusters</span><span class="p">));</span>

    <span class="k">return</span> <span class="nx">chIndex</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">_calculateCentroid</span><span class="p">(</span><span class="nx">points</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">numFeatures</span> <span class="o">=</span> <span class="nx">points</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">centroid</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">numFeatures</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numSamples</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">numFeatures</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">centroid</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">points</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="nx">j</span><span class="p">];</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">numFeatures</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">centroid</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">/=</span> <span class="nx">numSamples</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">centroid</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h4 id="color-evaluation" class="linked-heading">
    Color Evaluation<a href="#color-evaluation">🔗</a>
</h4>
<p>Centroid/color evaluation enables developers to programmatically assess and refine the clustering results.</p>

<h5 id="sorting-by-prominence" class="linked-heading">
    Sorting by Prominence<a href="#sorting-by-prominence">🔗</a>
</h5>
<p>By sorting the clusters based on the number of points (pixels) belonging to each cluster, you can identify the most dominant colors in the image. The clusters with a larger number of points represent colors that appear more frequently and have a significant presence in the image. Prioritizing these dominant colors ensures that the resulting color palette captures the main color characteristics of the image. This is particularly useful when you want to extract a limited number of representative colors or generate a color scheme that reflects the overall color distribution of the image.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">clusters</span> <span class="o">=</span> <span class="nx">clusterData</span><span class="p">.</span><span class="nx">clusters</span><span class="p">;</span>
<span class="c1">// sort dominant colors based on on number of pixels assigned to its cluster</span>
<span class="nx">clusters</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">b</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="p">});</span></code></pre></figure>

<h5 id="remove-similar-colors" class="linked-heading">
    Remove similar colors<a href="#remove-similar-colors">🔗</a>
</h5>
<p>Removing similar colors helps eliminate redundancy in the color palette and ensures a more diverse representation of colors. After applying XMeans clustering with the CIE2000 distance metric, you may find that some clusters have centroids that are perceptually very similar. These similar colors may not add significant value to the color palette and can be considered redundant. By comparing the centroids of different clusters using the CIE2000 metric and removing clusters that fall below a certain distance threshold, you can merge similar colors and obtain a more concise and varied color palette. This approach helps avoid having multiple clusters representing essentially the same color, making the resulting color palette more efficient and visually distinct.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">removeSimilarClusters</span><span class="p">(</span><span class="nx">clusters</span><span class="p">,</span> <span class="nx">threshold</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">cluster</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">cluster</span><span class="p">.</span><span class="nx">centroid</span><span class="p">,</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">centroid</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">distance</span> <span class="o">&lt;</span> <span class="nx">threshold</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// if current cluster is more dominant, keep it</span>
                <span class="k">return</span> <span class="nx">cluster</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">}</span></code></pre></figure>

<p>The <code class="language-plaintext highlighter-rouge">removeSimilarColors</code> method removes clusters/colors which have centroids within a given distance <code class="language-plaintext highlighter-rouge">threshold</code> of each other, leaving the dominant cluster (determined based on number of members of the cluster).  In the context of clustering pixels with <code class="language-plaintext highlighter-rouge">deltaE2000</code>, the threshold values on the <code class="language-plaintext highlighter-rouge">deltaE2000</code> function can be useful as starting points.</p>

<h5 id="color-dominance-index" class="linked-heading">
    Color Dominance Index<a href="#color-dominance-index">🔗</a>
</h5>

<p>The color Dominance Index (CDI) measures the perceptual dominance of a color in a composition based on its area, saturation, and lightness.  It assigns weights to each color based on these factors and calculates an overall dominance score.  Higher CDI values indicate a more dominant and visually prominent color in the composition.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">colorDominanceIndex</span><span class="p">(</span><span class="nx">clusters</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">totalArea</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">sum</span><span class="p">,</span> <span class="nx">cluster</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">sum</span> <span class="o">+</span> <span class="nx">cluster</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">dominanceScores</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">cluster</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">L</span><span class="p">,</span> <span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">]</span> <span class="o">=</span> <span class="nx">cluster</span><span class="p">.</span><span class="nx">centroid</span><span class="p">,</span>
        <span class="nx">area</span> <span class="o">=</span> <span class="nx">clusters</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>

    <span class="c1">// Calculate the color dominance score</span>
    <span class="kd">const</span> <span class="nx">dominanceScore</span> <span class="o">=</span> <span class="p">(</span><span class="nx">area</span> <span class="o">/</span> <span class="nx">totalArea</span><span class="p">)</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nx">a</span> <span class="o">*</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="nx">b</span><span class="p">)</span> <span class="o">/</span> <span class="mi">100</span><span class="p">,</span> <span class="mf">0.7</span><span class="p">)</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">L</span> <span class="o">/</span> <span class="mi">100</span><span class="p">,</span> <span class="mf">0.3</span><span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span>
      <span class="na">color</span><span class="p">:</span> <span class="p">[</span><span class="nx">L</span><span class="p">,</span> <span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">],</span>
      <span class="nx">dominanceScore</span><span class="p">,</span>
    <span class="p">};</span>
  <span class="p">});</span>

  <span class="c1">// Sort colors by their dominance scores in descending order</span>
  <span class="nx">dominanceScores</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">b</span><span class="p">.</span><span class="nx">dominanceScore</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">dominanceScore</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="nx">dominanceScores</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h2 id="results-and-visualization" class="linked-heading">
    Results and Visualization<a href="#results-and-visualization">🔗</a>
</h2>

<p>The next step is to present the clusters and evaluation in a meaningful and visually appealing way. One common approach is to create a color palette that showcases the dominant colors alongside the original image.</p>

<p>To create a color palette, you can display the extracted dominant colors as individual color swatches or rectangles, arranged in a grid or a horizontal/vertical layout. Each color swatch should be labeled with its corresponding CIELAB or RGB color values, allowing for precise color identification and reproduction.  By presenting the extracted dominant colors in a well-organized color palette, viewers can easily understand and appreciate the key colors that contribute to the overall visual composition of the image.</p>

<iframe src="https://assets.pinterest.com/ext/embed.html?id=68117013105312368" height="618" width="345" frameborder="0" scrolling="no"></iframe>
<iframe src="https://assets.pinterest.com/ext/embed.html?id=1407443628285285" height="445" width="345" frameborder="0" scrolling="no"></iframe>
<iframe src="https://assets.pinterest.com/ext/embed.html?id=274297433574411106" height="618" width="345" frameborder="0" scrolling="no"></iframe>

<p>In addition to the color swatches, it’s helpful to include the percentage or proportion of each dominant color in the image. This information provides insights into the relative prominence of each color and can be displayed as a percentage value or a visual representation, such as a pie chart or bar graph.  Bubble graphs can be very effective for visualizing the inter-centroid distance and intra-cluster distance along with the proportion of each dominant color.</p>

<p><img src="/assets/img/posts/bubble-chart.png" alt="An example bubble chart for evaluating the quality of color clusters extracted from an image" class="mw-100" /></p>

<p>The distances between the bubbles represent the average inter-centroid distances between clusters, with larger distances indicating well-separated and distinct clusters in the perceptual color space, while smaller distances suggest closer proximity and potential overlap or similarity between clusters. The size of each bubble represents the number of data points within each cluster, with smaller bubbles indicating fewer data points and larger bubbles indicating more data points. The average distance to the centroid within each cluster, represented by labels or annotations, provides information about the compactness of each cluster, with smaller distances suggesting tightly grouped and homogeneous data points, and larger distances indicating more spread out and variable data within the cluster.</p>

<p>A dashboard to display the generated color palette with a bubble chart showing the cluster/centroid relationships provide users with a comprehensive overview of the clustering results. Users can quickly grasp the overall color scheme and appreciate the aesthetic qualities of each palette. Alongside the color palette, a bubble chart offers a powerful tool for evaluating the clustering structure and characteristics. Each bubble represents a color cluster, with its size indicating the number of pixels within the cluster and its position reflecting the cluster’s centroid in the perceptual color space. The distances between the bubbles represent the average inter-centroid distances, allowing users to assess the separation and distinctiveness of the color clusters. Additionally, the average distances to the centroids within each cluster are displayed, providing insights into the compactness and homogeneity of the colors within each cluster. By interacting with the bubble chart, users can explore the relationships between color clusters, identify potential outliers or anomalies, and make informed decisions about the quality and suitability of the generated color palette. A simple dashboard’s intuitive and visually engaging presentation of color palettes and clustering evaluation empowers users to analyze, compare, and tune the parameters to the XMeans clustering algorithm.</p>

<p><img src="/assets/img/posts/color-clustering-dashboard.png" alt="An example K-means clustering dashboard for evaluating dominant color extraction" class="mw-100" /></p>

<h2 id="applications-and-use-cases" class="linked-heading">
    Applications and Use Cases<a href="#applications-and-use-cases">🔗</a>
</h2>

<ul>
  <li><strong>Color palette extraction</strong>: By applying X-means clustering with the <code class="language-plaintext highlighter-rouge">deltaE2000</code> metric to an image, you can effectively extract a representative color palette. This is useful for graphic designers, artists, or anyone working with color schemes, as it helps identify the dominant colors in an image while considering human color perception.</li>
  <li><strong>Image compression</strong>: Clustering pixels using X-means with <code class="language-plaintext highlighter-rouge">deltaE2000</code> can be used for image compression. By reducing the number of colors in an image to a smaller set of representative colors, you can decrease the image file size while maintaining perceptual quality. This is particularly useful for web graphics or applications where bandwidth is limited.</li>
  <li><strong>Color-based image retrieval</strong>: X-means clustering with <code class="language-plaintext highlighter-rouge">deltaE2000</code> can be used to build a color-based image retrieval system. By clustering images based on their dominant colors, you can quickly search and retrieve visually similar images from a large database. This is useful for applications like visual search engines, image recommendation systems, or content-based image retrieval.</li>
  <li><strong>Color harmony analysis</strong>: By applying X-means clustering with <code class="language-plaintext highlighter-rouge">deltaE2000</code> to a collection of appeal images, you can analyze color harmony patterns and discover common color combinations that are visually appealing. This information can be valuable for designers, marketers, or researchers studying color psychology and aesthetics.</li>
  <li><strong>Color-based anomaly detection</strong>: In industrial or scientific applications, X-means clustering with <code class="language-plaintext highlighter-rouge">deltaE2000</code> can be used to detect color-based anomalies or defects in images. By comparing the color distribution of a test image to a reference image or a cluster model, you can identify regions or pixels that deviate significantly from the expected color range, indicating potential defects or abnormalities.</li>
  <li><strong>Theme-based user interfaces</strong>: : Use X-means clustering with <code class="language-plaintext highlighter-rouge">deltaE2000</code> to dynamically theme user interfaces that adapt to the color scheme of user-provided images. By extracting dominant colors from an image and applying them to UI elements, you can create personalized interfaces that match the user’s aesthetic preferences or the content they are interacting with.</li>
</ul>

<h2 id="conclusion" class="linked-heading">
    Conclusion<a href="#conclusion">🔗</a>
</h2>

<p>The benefits of using X-Means clustering with CIE2000 are clear. This technique not only automatically determines the optimal number of color clusters but also ensures that the resulting color palettes are perceptually meaningful and closely aligned with human color perception. The ability to extract dominant colors accurately opens up a wide range of possibilities in various domains, from data-driven design and image compression to object recognition and data visualization.</p>

<p>To encourage further exploration and experimentation, I’ve provided code implementations for each key component of the dominant color extraction process. These implementations serve as a starting point for readers to dive deeper into the world of color clustering and image processing. We invite you to adapt, modify, and enhance these code snippets to suit your specific needs and explore the limitless potential of this technique.</p>

<p>The significance of perceptually accurate color clustering becomes evident that this approach is not merely an academic exercise but a powerful tool with real-world implications. In a world increasingly driven by visual data, the ability to extract meaningful color information from images is crucial. By embracing techniques like X-Means clustering and CIE2000 color distance, we can create visually stunning designs based on human perception.</p>

<h2 id="references-and-further-reading" class="linked-heading">
    References and Further Reading<a href="#references-and-further-reading">🔗</a>
</h2>

<ul>
  <li><a href="https://www.cs.cmu.edu/~dpelleg/download/xmeans.pdf">X-means extending K-means with Efficient Estimation of the Number of Clusters</a></li>
  <li><a href="https://hajim.rochester.edu/ece/sites/gsharma/ciede2000/ciede2000noteCRNA.pdf">The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations</a></li>
  <li><a href="https://cie.co.at/publications/colorimetry-part-6-ciede2000-colour-difference-formula">CIE</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Color_difference">Color difference</a></li>
  <li><a href="https://www.researchgate.net/publication/229511830_The_development_of_the_CIE_2000_colour-difference_formula_CIEDE2000">The development of the CIE 2000 colour‐difference formula: CIEDE2000</a></li>
</ul>]]></content><author><name>Doug Fenstermacher</name></author><category term="project" /><category term="data" /><category term="math" /><category term="javascript" /><summary type="html"><![CDATA[Discover how to harness the power of the X-Means clustering algorithm and CIE2000 color distance metric to accurately extract dominant colors from images. This advanced technique combines unsupervised machine learning with human color perception principles to generate visually appealing and representative color palettes. Perfect for data-driven design, image analysis, and computer vision applications. Dive into the world of color science and elevate your image processing skills with this comprehensive guide.]]></summary></entry><entry><title type="html">Boosting Web Performance: Implementing K-Means Clustering with WebAssembly and Emscripten</title><link href="https://dougfenstermacher.com/experiment/webassembly-emscripten-quantization" rel="alternate" type="text/html" title="Boosting Web Performance: Implementing K-Means Clustering with WebAssembly and Emscripten" /><published>2023-12-27T00:00:00+00:00</published><updated>2023-12-27T00:00:00+00:00</updated><id>https://dougfenstermacher.com/experiment/webassembly-emscripten-quantization</id><content type="html" xml:base="https://dougfenstermacher.com/experiment/webassembly-emscripten-quantization"><![CDATA[<p>In the realm of web development, performance optimization is a constant pursuit. As we push the boundaries of what’s possible in the browser, we often find ourselves seeking innovative solutions to enhance speed and efficiency. Enter WebAssembly and Emscripten—two powerful tools that are revolutionizing the way we approach web performance. We’ll explore how WebAssembly and Emscripten can be leveraged to implement the K-Means clustering algorithm with exceptional performance. By comparing traditional JavaScript implementations with highly optimized, vectorized C++ versions compiled to WebAssembly, we’ll uncover the potential of these cutting-edge technologies.</p>

<h2 id="why-webassembly" class="linked-heading">
    Why WebAssembly?<a href="#why-webassembly">🔗</a>
</h2>

<p>JavaScript, the long-standing backbone of web development, has been the go-to language for creating interactive and dynamic web experiences. However, when it comes to performance-critical tasks, JavaScript has certain limitations that can hinder its efficiency. One of the major performance bottlenecks in JavaScript is its garbage collection mechanism. JavaScript relies on automatic memory management, where the JavaScript engine periodically scans the memory to identify and remove unused objects. While this frees developers from manual memory management, it comes at a performance cost. The garbage collection process can introduce pauses and slowdowns, especially when dealing with large heaps or complex object graphs. This can be particularly problematic for memory-intensive tasks or applications that require consistent and predictable performance.</p>

<p>Another area where JavaScript may suffer from performance loss is in its Just-In-Time (JIT) compilation. JavaScript engines, such as V8 in Chrome or SpiderMonkey in Firefox, employ JIT compilers to optimize JavaScript code during runtime. The JIT compiler analyzes the code and attempts to optimize it based on runtime information and heuristics. However, the effectiveness of JIT compilation can vary depending on the complexity and predictability of the code. In certain scenarios, the JIT compiler may struggle to make optimal decisions, leading to suboptimal performance. Additionally, the JIT compilation process itself introduces overhead, as the compiler needs to analyze and optimize the code on the fly. This overhead can be significant for computationally intensive tasks or code with complex control flow.</p>

<p>In contrast, WebAssembly offers a compelling alternative for performance-critical tasks. WebAssembly is a low-level, binary instruction format that is designed to be executed at near-native speeds. It bypasses the limitations of JavaScript’s garbage collection by providing manual memory management capabilities. Developers have fine-grained control over memory allocation and deallocation, allowing for more efficient memory usage and avoiding the performance penalties associated with garbage collection. Moreover, WebAssembly is ahead-of-time (AOT) compiled, meaning that the code is compiled to machine code before execution. This eliminates the need for JIT compilation and its associated overhead, resulting in faster startup times and more predictable performance.</p>

<p>A crucial aspect of delivering a seamless user experience is ensuring that your website responds to user interactions within 200 milliseconds. This 200ms interactivity rule is a well-established guideline that sets the benchmark for perceived responsiveness. However, JavaScript, the backbone of modern web applications, faces a significant challenge in meeting this threshold due to its single-threaded nature. When the browser is executing computationally intensive tasks written in JavaScript, it becomes unresponsive to user input, leading to a frustrating experience.</p>

<p>By leveraging the power of low-level languages like C++, developers can optimize performance-critical code and compile it to WebAssembly using tools like Emscripten. The resulting WebAssembly modules can be seamlessly integrated into JavaScript applications, offloading heavy computations to a separate thread. This frees up the main JavaScript thread to handle user interactions promptly, ensuring that the 200ms interactivity rule is met. Moreover, WebAssembly’s near-native performance characteristics enable developers to build highly interactive and responsive web applications without compromising on functionality or user experience. As the demand for rich, immersive web experiences continues to grow, the combination of JavaScript and WebAssembly promises to revolutionize the way we build and interact with websites, making the 200ms interactivity threshold a reality for even the most complex applications.</p>

<h2 id="understanding-webassembly-and-emscripten" class="linked-heading">
    Understanding WebAssembly and Emscripten<a href="#understanding-webassembly-and-emscripten">🔗</a>
</h2>

<p>WebAssembly (WASM), is a low-level, binary instruction format designed to be executed in web browsers. It provides a way to run code written in languages like C, C++, and Rust on the web at near-native speeds, offering performance that is comparable to native applications.</p>

<p>WebAssembly is designed to complement JavaScript, not replace it. It is particularly useful for performance-critical tasks, such as complex algorithms, game engines, virtual machines, and multimedia processing. By offloading computationally intensive tasks to WebAssembly, web applications can achieve significant performance improvements while keeping the flexibility and interactivity of JavaScript.</p>

<p>When it comes to compiling C++ code to WebAssembly, Emscripten is the go-to tool for web developers. Emscripten is an open-source compiler toolchain that allows us to compile C and C++ code into WebAssembly and leverage the performance benefits of WASM in web applications.</p>

<p>Emscripten acts as a bridge between the C++ ecosystem and the web. It provides a way to take existing C++ codebases and bring them to the browser without the need for extensive modifications. This is particularly useful when we have performance-critical code written in C++ that we want to integrate into our web applications.</p>

<p>By using Emscripten, developers can leverage the performance optimizations and low-level control of C++ while still targeting the web platform, as well as take advantage of the vast ecosystem of C++ libraries and frameworks in web projects.</p>

<h2 id="k-means-clustering" class="linked-heading">
    K-Means Clustering<a href="#k-means-clustering">🔗</a>
</h2>

<p>K-Means clustering is a popular unsupervised machine learning algorithm that aims to partition a dataset into K clusters based on similarity. In the context of image processing, K-Means clustering finds valuable applications in image quantization and dominant color extraction. Let’s explore how K-Means clustering works and why it is a good candidate for acceleration using WebAssembly.
At its core, the K-Means clustering algorithm follows a simple yet effective approach:</p>

<ol>
  <li>Initialization: The algorithm randomly selects <code class="language-plaintext highlighter-rouge">K</code> data points from the dataset as the initial cluster centroids.</li>
  <li>Assignment: Each data point in the dataset is assigned to the nearest centroid based on a distance metric, typically Euclidean distance.</li>
  <li>Update: The centroids of each cluster are recalculated by taking the mean of all the data points assigned to that cluster.</li>
  <li>Iteration: Steps 2 and 3 are repeated until convergence, i.e., when the centroids no longer change significantly or a maximum number of iterations is reached.</li>
</ol>

<p>When applied to image quantization, K-Means clustering can effectively reduce the number of colors in an image while preserving its overall visual appearance. Each pixel in the image is treated as a data point, and the algorithm clusters similar colors together. The resulting cluster centroids represent the dominant colors in the image. By replacing each pixel’s color with its corresponding cluster centroid, we obtain a quantized version of the image with a reduced color palette.</p>

<p>The K-Means clustering algorithm is computationally intensive, especially when dealing with large datasets or high-resolution images. This is where WebAssembly comes into play. WebAssembly is designed to provide near-native performance in web browsers, making it an ideal candidate for accelerating performance-critical tasks like K-Means clustering.</p>

<p>By implementing the K-Means clustering algorithm in a language like C++ and compiling it to WebAssembly using tools like Emscripten, we can leverage the low-level performance optimizations offered by WebAssembly. The compiled WebAssembly module can be efficiently executed in the browser, taking advantage of the browser’s optimized execution environment and hardware acceleration.</p>

<p>WebAssembly’s linear memory model and manual memory management capabilities allow for efficient data manipulation and reduced overhead compared to JavaScript’s garbage-collected memory model. This is particularly beneficial for the K-Means clustering algorithm, which involves iterative computations and frequent memory access.</p>

<p>Furthermore, WebAssembly’s support for Single Instruction, Multiple Data (SIMD) instructions enables parallelization of certain operations, such as distance calculations and centroid updates. By utilizing SIMD instructions, the K-Means clustering algorithm can achieve significant performance gains, especially when processing large datasets or high-resolution images.</p>

<p>Here is our implementation of the K-Means clustering algorithm (excluding the KDTree implementation for brevity’s sake):</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nx">KMeans</span> <span class="p">{</span>
  <span class="cm">/**
   * A configurable implementation of the K-means clustering algorithm
   * 
   * @param {int} minK The number of clusters to be found in the data
   * @param {Function} distanceMetric 
   * @param {object} options
   */</span>

  <span class="kd">constructor</span><span class="p">(</span><span class="nx">minK</span><span class="p">,</span> <span class="nx">distanceMetric</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">minK</span> <span class="o">=</span> <span class="nx">minK</span><span class="p">;</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span> <span class="o">=</span> <span class="nx">distanceMetric</span><span class="p">;</span>

      <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">maxIterations</span> <span class="o">||</span> <span class="nb">Number</span><span class="p">.</span><span class="nx">MAX_SAFE_INTEGER</span><span class="p">;</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">meanFunc</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">meanFunc</span> <span class="o">||</span> <span class="k">this</span><span class="p">.</span><span class="nx">_arithmeticMean</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">validateDataset</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset must be array</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="k">if</span> <span class="p">(</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset must have at least </span><span class="dl">'</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> data points</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">]))</span> <span class="p">{</span>
              <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset points must be an array</span><span class="dl">'</span><span class="p">);</span>
          <span class="p">}</span>

          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
              <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">dataset points must be a non-empty array</span><span class="dl">'</span><span class="p">);</span>
          <span class="p">}</span>
      <span class="p">}</span>

      <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">transform</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
      <span class="cm">/**
       * Executes the k-means clustering algorithm against a dataset
       * with the given optional optimization parameters.  Parameters are
       * 
       * - validate (bool):  Indicates if the dataset should be validated
       * - kDTree (bool):  Indicates if a KD-tree should be used to find nearest centroids
       * 
       * @param {Array} dataset
       * @param {object} options
       */</span>
      <span class="kd">var</span> <span class="nx">options</span> <span class="o">=</span> <span class="nx">options</span> <span class="o">||</span> <span class="p">{};</span>
      <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span> <span class="o">||</span> <span class="kc">false</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">validate</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">this</span><span class="p">.</span><span class="nx">validateDataset</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="kd">let</span> <span class="nx">iterations</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="kd">let</span> <span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">labels</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">;</span>

      <span class="c1">// Initialize centroids randomly</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">useNaiveSharding</span> <span class="o">||</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroidsNaiveSharding</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="c1">// Run the main k-means algorithm</span>
      <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">))</span> <span class="p">{</span>
          <span class="c1">// Save old centroids for convergence test.</span>
          <span class="nx">oldCentroids</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">centroids</span><span class="p">];</span>
          <span class="nx">iterations</span><span class="o">++</span><span class="p">;</span>

          <span class="c1">// Assign labels to each datapoint based on centroids</span>
          <span class="nx">labels</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">kDTree</span><span class="p">);</span>
          <span class="nx">centroids</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_recalculateCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">labels</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="kd">const</span> <span class="nx">clusters</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">clusters</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">labels</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
      <span class="p">}</span>
      <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="p">{</span>
          <span class="na">clusters</span><span class="p">:</span> <span class="nx">clusters</span><span class="p">,</span>
          <span class="na">centroids</span><span class="p">:</span> <span class="nx">centroids</span><span class="p">,</span>
          <span class="na">iterations</span><span class="p">:</span> <span class="nx">iterations</span><span class="p">,</span>
          <span class="na">converged</span><span class="p">:</span> <span class="nx">iterations</span> <span class="o">&lt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span><span class="p">,</span>
      <span class="p">};</span>
      <span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_getLabels</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">kDTree</span> <span class="o">=</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// prep data structure:</span>
      <span class="kd">const</span> <span class="nx">labels</span> <span class="o">=</span> <span class="p">{},</span>
          <span class="nx">centroidIndices</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">c</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">c</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">labels</span><span class="p">[</span><span class="nx">c</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
              <span class="na">points</span><span class="p">:</span> <span class="p">[],</span>
              <span class="na">centroid</span><span class="p">:</span> <span class="nx">centroids</span><span class="p">[</span><span class="nx">c</span><span class="p">],</span>
          <span class="p">};</span>
          <span class="nx">centroidIndices</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">c</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="kd">var</span> <span class="nx">tree</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">kDTree</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">var</span> <span class="nx">centroidDimensions</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="nx">i</span><span class="p">});</span>
          <span class="nx">tree</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">KDTree</span><span class="p">(</span><span class="nx">centroids</span><span class="p">,</span> <span class="nx">centroidIndices</span><span class="p">,</span> <span class="nx">centroidDimensions</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="c1">// For each element in the dataset, choose the closest centroid. </span>
      <span class="c1">// Make that centroid the element's label.</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataSet</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nx">dataSet</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
          <span class="kd">let</span> <span class="nx">closestCentroid</span><span class="p">,</span> <span class="nx">closestCentroidIndex</span>
          <span class="k">if</span> <span class="p">(</span><span class="nx">kDTree</span><span class="p">)</span> <span class="p">{</span>
              <span class="kd">var</span> <span class="nx">closestCentroids</span> <span class="o">=</span> <span class="nx">tree</span><span class="p">.</span><span class="nx">nearestNeighbor</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
                  <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">closestCentroids</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
                  <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">closestCentroid</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
          <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
              <span class="kd">let</span> <span class="nx">prevDistance</span><span class="p">;</span>
              <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                  <span class="kd">let</span> <span class="nx">centroid</span> <span class="o">=</span> <span class="nx">centroids</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
                  <span class="k">if</span> <span class="p">(</span><span class="nx">j</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                      <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">centroid</span><span class="p">;</span>
                      <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">j</span><span class="p">;</span>
                      <span class="nx">prevDistance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">closestCentroid</span><span class="p">);</span>
                  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                      <span class="c1">// get distance:</span>
                      <span class="kd">const</span> <span class="nx">distance</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">distanceMetric</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">centroid</span><span class="p">);</span>
                      <span class="k">if</span> <span class="p">(</span><span class="nx">distance</span> <span class="o">&lt;</span> <span class="nx">prevDistance</span><span class="p">)</span> <span class="p">{</span>
                          <span class="nx">prevDistance</span> <span class="o">=</span> <span class="nx">distance</span><span class="p">;</span>
                          <span class="nx">closestCentroid</span> <span class="o">=</span> <span class="nx">centroid</span><span class="p">;</span>
                          <span class="nx">closestCentroidIndex</span> <span class="o">=</span> <span class="nx">j</span><span class="p">;</span>
                      <span class="p">}</span>
                  <span class="p">}</span>
              <span class="p">}</span>
          <span class="p">}</span>
          <span class="c1">// add point to centroid labels:</span>
          <span class="nx">labels</span><span class="p">[</span><span class="nx">closestCentroidIndex</span><span class="p">].</span><span class="nx">points</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">labels</span><span class="p">;</span>
  <span class="p">}</span>


  <span class="nx">_compareDatasets</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">if</span> <span class="p">(</span><span class="nx">a</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">b</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span> <span class="p">{</span>
              <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// selects random points as centroids from the dataset</span>
      <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">centroidsIndex</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="kd">let</span> <span class="nx">index</span><span class="p">;</span>
      <span class="k">while</span> <span class="p">(</span><span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">index</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_randomBetween</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">numSamples</span><span class="p">);</span>
          <span class="k">if</span> <span class="p">(</span><span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">index</span><span class="p">)</span> <span class="o">===</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
              <span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">index</span><span class="p">);</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="kd">const</span> <span class="nx">centroids</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroidsIndex</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">centroid</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">dataset</span><span class="p">[</span><span class="nx">centroidsIndex</span><span class="p">[</span><span class="nx">i</span><span class="p">]]];</span>
          <span class="nx">centroids</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">centroid</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">centroids</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_getRandomCentroidsNaiveSharding</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// implementation of a variation of naive sharding centroid initialization method</span>
      <span class="c1">// (not using sums or sorting, just dividing into k shards and calc mean)</span>
      <span class="kd">const</span> <span class="nx">numSamples</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">step</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">numSamples</span> <span class="o">/</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">centroids</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="nx">step</span> <span class="o">*</span> <span class="nx">i</span><span class="p">;</span>
          <span class="kd">let</span> <span class="nx">end</span> <span class="o">=</span> <span class="nx">step</span> <span class="o">*</span> <span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
          <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">===</span> <span class="k">this</span><span class="p">.</span><span class="nx">minK</span><span class="p">)</span> <span class="p">{</span>
              <span class="nx">end</span> <span class="o">=</span> <span class="nx">numSamples</span><span class="p">;</span>
          <span class="p">}</span>
          <span class="nx">centroids</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_calcMeanCentroid</span><span class="p">(</span><span class="nx">dataset</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">end</span><span class="p">));</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">centroids</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_calcMeanCentroid</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">end</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">features</span> <span class="o">=</span> <span class="nx">dataSet</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">end</span> <span class="o">-</span> <span class="nx">start</span><span class="p">;</span>
      <span class="kd">let</span> <span class="nx">mean</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">features</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">mean</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">start</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">end</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">features</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
              <span class="nx">mean</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">mean</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+</span> <span class="nx">dataSet</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="nx">j</span><span class="p">]</span> <span class="o">/</span> <span class="nx">n</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">mean</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_shouldStop</span><span class="p">(</span><span class="nx">oldCentroids</span><span class="p">,</span> <span class="nx">centroids</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">iterations</span> <span class="o">&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">maxIterations</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">oldCentroids</span> <span class="o">||</span> <span class="o">!</span><span class="nx">oldCentroids</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="kd">let</span> <span class="nx">sameCount</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">centroids</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">_compareDatasets</span><span class="p">(</span><span class="nx">centroids</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">oldCentroids</span><span class="p">[</span><span class="nx">i</span><span class="p">]))</span> <span class="p">{</span>
              <span class="nx">sameCount</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">sameCount</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">_recalculateCentroids</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="nx">labels</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Each centroid is the arithmetic mean of the points that</span>
      <span class="c1">// have that centroid's label. Important: If a centroid is empty (no points have</span>
      <span class="c1">// that centroid's label) we should randomly re-initialize it.</span>
      <span class="kd">let</span> <span class="nx">newCentroid</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">newCentroidList</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">k</span> <span class="k">in</span> <span class="nx">labels</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">centroidGroup</span> <span class="o">=</span> <span class="nx">labels</span><span class="p">[</span><span class="nx">k</span><span class="p">];</span>
          <span class="k">if</span> <span class="p">(</span><span class="nx">centroidGroup</span><span class="p">.</span><span class="nx">points</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
              <span class="c1">// find mean:</span>
              <span class="nx">newCentroid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">meanFunc</span><span class="p">(</span><span class="nx">centroidGroup</span><span class="p">.</span><span class="nx">points</span><span class="p">);</span>
          <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
              <span class="c1">// get new random centroid</span>
              <span class="nx">newCentroid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRandomCentroids</span><span class="p">(</span><span class="nx">dataSet</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
          <span class="p">}</span>
          <span class="nx">newCentroidList</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">newCentroid</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">newCentroidList</span><span class="p">;</span>
  <span class="p">}</span>


  <span class="nx">_arithmeticMean</span><span class="p">(</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">totalPoints</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">means</span> <span class="o">=</span> <span class="p">[];</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">means</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">dataset</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">point</span> <span class="o">=</span> <span class="nx">dataset</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
          <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">point</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
              <span class="kd">const</span> <span class="nx">val</span> <span class="o">=</span> <span class="nx">point</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
              <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">means</span><span class="p">[</span><span class="nx">j</span><span class="p">]</span> <span class="o">+</span> <span class="nx">val</span> <span class="o">/</span> <span class="nx">totalPoints</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">means</span><span class="p">;</span>
  <span class="p">}</span>
<span class="nx">Discuss</span> <span class="nx">the</span> <span class="nx">limitations</span> <span class="nx">and</span> <span class="nx">performance</span> <span class="nx">drawbacks</span> <span class="k">of</span> <span class="nx">the</span> <span class="nx">JavaScript</span> <span class="nx">implementation</span>
  <span class="nx">_randomBetween</span><span class="p">(</span><span class="nx">min</span><span class="p">,</span> <span class="nx">max</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span>
          <span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="p">(</span><span class="nx">max</span> <span class="o">-</span> <span class="nx">min</span><span class="p">)</span> <span class="o">+</span> <span class="nx">min</span>
      <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This Javascript implementation of k-means clustering has several drawbacks by being implementation in Javascript:</p>

<ol>
  <li>Computational Intensity
    <ul>
      <li>The K-Means algorithm involves iterative computations, including distance calculations and centroid updates, which can be computationally intensive, especially for large datasets (like image data) or high-dimensional data.</li>
      <li>The browser is single-threaded for Javascript, so computationally intensive tasks block user-interaction on the site while the computation is in-progress.</li>
      <li>JavaScript, being a dynamically-typed, interpreted language is not as suited for such computationally intensive tasks.</li>
      <li>The lack of native support for parallelization in JavaScript can limit the ability to use multi-core processors for faster computation.</li>
    </ul>
  </li>
  <li>Memory Management
    <ul>
      <li>JavaScript relies on automatic memory management through garbage collection, which can introduce overhead and performance penalties.</li>
      <li>The K-Means algorithm often requires frequent memory allocation and deallocation for storing data points, centroids, and intermediate results.</li>
      <li>The garbage collector may introduce pauses and slowdowns, especially when dealing with large datasets or complex data structures.</li>
    </ul>
  </li>
  <li>Lack of Typed Arrays
    <ul>
      <li>JavaScript does not have native support for typed arrays, which are essential for efficient storage and manipulation of numerical data.</li>
      <li>The provided implementation uses regular JavaScript arrays, which are dynamically-typed and can incur additional memory overhead and slower access times compared to typed arrays.</li>
      <li>Without typed arrays, the algorithm may not be able to take full advantage of hardware-level optimizations for numerical computations.</li>
    </ul>
  </li>
  <li>Limited Vectorization
    <ul>
      <li>JavaScript does not have built-in support for vectorization or SIMD (Single Instruction, Multiple Data) operations.</li>
      <li>Vectorization allows for parallel processing of multiple data points simultaneously, which can significantly speed up computations.</li>
      <li>The absence of vectorization support in JavaScript means that the algorithm cannot leverage the full potential of modern CPUs for faster execution.</li>
    </ul>
  </li>
</ol>

<h2 id="webassembly-and-c-implementation-of-k-means-clustering" class="linked-heading">
    WebAssembly and C++ Implementation of K-Means Clustering<a href="#webassembly-and-c-implementation-of-k-means-clustering">🔗</a>
</h2>

<p>Now let’s create our C++ implementation of K-Means clustering for compilation to WebAssembly.  Several key aspects contribute to C++’s potential for more performant code execution:</p>

<ol>
  <li>Static Typing:
    <ul>
      <li>C++ is a statically-typed language, meaning that variable types are explicitly declared and checked at compile-time.</li>
      <li>Static typing allows for better code optimization by the compiler, as it knows the exact data types being used.</li>
      <li>JavaScript, on the other hand, is dynamically-typed, which means that variable types can change during runtime, making it harder for the JavaScript engine to optimize the code.</li>
    </ul>
  </li>
  <li>Memory Management:
    <ul>
      <li>C++ provides low-level control over memory management through manual memory allocation and deallocation using pointers.</li>
      <li>Developers have fine-grained control over memory layout and can optimize memory usage for specific requirements.</li>
      <li>JavaScript relies on automatic memory management through garbage collection, which can introduce overhead and potential performance issues.</li>
    </ul>
  </li>
  <li>Compiler Optimizations:
    <ul>
      <li>C++ compilers, such as Clang or GCC, are highly sophisticated and can perform advanced optimizations during the compilation process</li>
      <li>These optimizations include dead code elimination, loop unrolling, and inline expansion, among others.</li>
      <li>JavaScript engines, like V8 or SpiderMonkey, also perform optimizations, but they are limited by the dynamic nature of the language and the need for just-in-time (JIT) compilation.</li>
    </ul>
  </li>
  <li>Direct Hardware Access:
    <ul>
      <li>C++ allows for direct access to hardware resources, such as CPU instructions and memory addresses.</li>
      <li>This low-level control enables developers to write highly optimized code that can take full advantage of the underlying hardware.</li>
      <li>JavaScript, being a high-level language, does not provide direct access to hardware and relies on the browser or runtime environment for hardware interaction.</li>
    </ul>
  </li>
  <li>Ahead-of-Time Compilation:
    <ul>
      <li>C++ code is compiled ahead-of-time (AOT) into machine code before execution.</li>
      <li>AOT compilation allows for extensive code analysis and optimization, resulting in faster execution times.</li>
      <li>JavaScript code is typically interpreted or compiled just-in-time (JIT) during runtime, which can introduce some performance overhead.</li>
    </ul>
  </li>
  <li>Lack of Runtime Overhead:
    <ul>
      <li>C++ has minimal runtime overhead because it compiles directly to machine code.</li>
      <li>There is no need for an interpreter or virtual machine, which can introduce additional layers of abstraction and performance overhead.</li>
      <li>JavaScript, being an interpreted language, relies on a JavaScript engine to execute the code, which can add some runtime overhead.</li>
    </ul>
  </li>
  <li>Manual Optimization Techniques:
    <ul>
      <li>C++ allows developers to manually optimize code using techniques like loop unrolling, cache optimization, and SIMD (Single Instruction, Multiple Data) operations.</li>
      <li>JavaScript engines apply some optimizations automatically, but the level of manual optimization is limited compared to C++.</li>
    </ul>
  </li>
</ol>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
</pre></td><td class="code"><pre><span class="cp">#ifndef KMEANS_H
#define KMEANS_H
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;tuple&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;algorithm&gt;</span><span class="cp">
</span>

<span class="k">namespace</span> <span class="n">clustering</span> <span class="p">{</span>

    <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
    <span class="k">class</span> <span class="nc">KMeans</span> <span class="p">{</span>
    <span class="nl">private:</span>
        <span class="kt">long</span> <span class="kt">int</span> <span class="n">m_k</span><span class="p">;</span>
        <span class="kt">long</span> <span class="kt">int</span> <span class="n">m_max_iterations</span><span class="p">;</span>
        <span class="kt">double</span> <span class="n">m_tolerance</span><span class="p">;</span>
        <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="n">m_distance</span><span class="p">)(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">);</span>

        <span class="kt">void</span> <span class="n">initialize_random_centroids</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">centroids</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">sample_count</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">centroid_count</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dimensions</span><span class="p">);</span>
                <span class="n">T</span> <span class="n">random_seed</span> <span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="p">(</span><span class="n">sample_count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">)</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">random_seed</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="kt">void</span> <span class="n">initialize_kpp_centroids</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">centroids</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">sample_count</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">centroid_count</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dimensions</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">matrix</span><span class="p">(</span><span class="n">sample_count</span><span class="p">);</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">matrix</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">sample_count</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="cp">#pragma omp parallel for private(i) shared(data, matrix)
</span>            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">double</span> <span class="n">distance</span><span class="p">;</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">==</span> <span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">distance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="n">distance</span> <span class="o">=</span> <span class="n">m_distance</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">),</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">));</span>
                    <span class="p">}</span>
                    <span class="n">matrix</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">)</span> <span class="o">=</span> <span class="n">distance</span><span class="p">;</span>
                    <span class="n">matrix</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">distance</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="c1">//set first seed</span>
            <span class="n">T</span> <span class="n">random_seed</span> <span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="p">(</span><span class="n">sample_count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
            <span class="cp">#pragma omp parallel for private(i) shared(centroids, data)
</span>            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">random_seed</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">last_centroid</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">current_centroid</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

            <span class="k">while</span> <span class="p">(</span><span class="n">current_centroid</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">)</span> <span class="p">{</span>

                <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">distances</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">potential_point</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="kt">double</span> <span class="n">current_min_distance</span> <span class="o">=</span> <span class="mi">99999</span><span class="p">;</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">current_centroid</span><span class="p">;</span> <span class="o">++</span><span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                        <span class="kt">double</span> <span class="n">potential_distance</span> <span class="o">=</span> <span class="n">m_distance</span><span class="p">(</span><span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">k</span><span class="p">),</span> <span class="n">potential_point</span><span class="p">);</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">potential_distance</span> <span class="o">&lt;</span> <span class="n">current_min_distance</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">current_min_distance</span> <span class="o">=</span> <span class="n">potential_distance</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                    <span class="n">distances</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">current_min_distance</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">size_t</span> <span class="n">last_centroid_index</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">distance</span><span class="p">(</span><span class="n">distances</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">std</span><span class="o">::</span><span class="n">max_element</span><span class="p">(</span><span class="n">distances</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">distances</span><span class="p">.</span><span class="n">end</span><span class="p">()));</span>
                <span class="n">last_centroid</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">last_centroid_index</span><span class="p">);</span>
                <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">current_centroid</span><span class="p">)</span> <span class="o">=</span> <span class="n">last_centroid</span><span class="p">;</span>
                <span class="o">++</span><span class="n">current_centroid</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="kt">double</span> <span class="n">update_centroids</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">centroids</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">clusters</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">centroid_count</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">sample_count</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="kt">size_t</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">size</span><span class="p">();</span>

            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">sums</span><span class="p">(</span><span class="n">centroid_count</span><span class="p">);</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">size_t</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">counts</span><span class="p">(</span><span class="n">centroid_count</span><span class="p">);</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">new_centroids</span><span class="p">(</span><span class="n">centroid_count</span><span class="p">);</span>

            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">new_centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dimensions</span><span class="p">);</span>
                <span class="n">sums</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dimensions</span><span class="p">);</span>
                <span class="n">counts</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">size_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dimensions</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="cp">#pragma omp parallel for private(i) shared(data, centroids)
</span>            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">sample</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">cluster</span> <span class="o">=</span> <span class="n">clusters</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="kt">size_t</span> <span class="n">count</span> <span class="o">=</span> <span class="n">counts</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">cluster</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
                    <span class="n">counts</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">cluster</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
                    <span class="n">sums</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">cluster</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">+=</span> <span class="n">sample</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="cp">#pragma omp parallel for private(i) shared(centroids)
</span>            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">T</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">sums</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="kt">size_t</span> <span class="n">count</span> <span class="o">=</span> <span class="n">counts</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="n">new_centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">)</span> <span class="o">=</span> <span class="n">sum</span> <span class="o">/</span> <span class="p">(</span><span class="n">T</span><span class="p">)</span><span class="n">count</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="kt">double</span> <span class="n">changes</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">m_distance</span><span class="p">(</span><span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">),</span> <span class="n">new_centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
                <span class="n">changes</span> <span class="o">+=</span> <span class="n">distance</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">)</span> <span class="o">=</span> <span class="n">new_centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">).</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="k">return</span> <span class="n">changes</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">long</span> <span class="kt">int</span> <span class="n">update_clusters</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">centroids</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">clusters</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">centroid_count</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">sample_count</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

            <span class="kt">long</span> <span class="kt">int</span> <span class="n">assignment_changes</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

            <span class="cp">#pragma omp parallel for private(i) shared(data)
</span>            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">sample_count</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">sample</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">closest_centroid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="kt">double</span> <span class="n">closest_centroid_distance</span> <span class="o">=</span> <span class="mi">9999999</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">centroid_count</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">centroid</span> <span class="o">=</span> <span class="n">centroids</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">j</span><span class="p">);</span>
                    <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_distance</span><span class="p">(</span><span class="n">sample</span><span class="p">,</span> <span class="n">centroid</span><span class="p">);</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">distance</span> <span class="o">&lt;</span> <span class="n">closest_centroid_distance</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">closest_centroid_distance</span> <span class="o">=</span> <span class="n">distance</span><span class="p">;</span>
                        <span class="n">closest_centroid</span> <span class="o">=</span> <span class="n">j</span><span class="p">;</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">clusters</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">!=</span> <span class="n">closest_centroid</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">clusters</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">closest_centroid</span><span class="p">;</span>
                    <span class="cp">#pragma omp critical
</span>                    <span class="o">++</span><span class="n">assignment_changes</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">assignment_changes</span><span class="p">;</span>
        <span class="p">}</span>

    <span class="nl">public:</span>
        <span class="n">KMeans</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">,</span> <span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">,</span> <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="n">distance_func</span><span class="p">)(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">m_k</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span>
            <span class="n">m_max_iterations</span> <span class="o">=</span> <span class="n">max_iterations</span><span class="p">;</span>
            <span class="n">m_tolerance</span> <span class="o">=</span> <span class="n">tolerance</span><span class="p">;</span>
            <span class="n">m_distance</span> <span class="o">=</span> <span class="n">distance_func</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">void</span> <span class="n">setK</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_k</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">long</span> <span class="kt">int</span> <span class="n">getK</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_k</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">void</span> <span class="n">setMaxIterations</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">maxIterations</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_max_iterations</span> <span class="o">=</span> <span class="n">maxIterations</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">long</span> <span class="kt">int</span> <span class="n">getMaxIterations</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_max_iterations</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">void</span> <span class="n">setTolerance</span><span class="p">(</span><span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_tolerance</span> <span class="o">=</span> <span class="n">tolerance</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">double</span> <span class="n">getTolerance</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_tolerance</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">predict</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>

            <span class="kt">size_t</span> <span class="n">sample_size</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="n">clusters</span><span class="p">(</span><span class="n">sample_size</span><span class="p">);</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">centroids</span><span class="p">(</span><span class="n">m_k</span><span class="p">);</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">current_iteration</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="kt">double</span> <span class="n">centroid_changes</span> <span class="o">=</span> <span class="n">m_tolerance</span><span class="p">;</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">assignment_changes</span> <span class="o">=</span> <span class="n">m_tolerance</span><span class="p">;</span>

            <span class="n">initialize_kpp_centroids</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">centroids</span><span class="p">);</span>
            <span class="k">while</span> <span class="p">(</span><span class="n">current_iteration</span> <span class="o">&lt;</span> <span class="n">m_max_iterations</span> <span class="o">&amp;&amp;</span> <span class="n">centroid_changes</span> <span class="o">&gt;=</span> <span class="n">m_tolerance</span><span class="p">)</span> <span class="p">{</span>
                <span class="o">++</span><span class="n">current_iteration</span><span class="p">;</span>
                <span class="n">assignment_changes</span> <span class="o">=</span> <span class="n">update_clusters</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
                <span class="n">centroid_changes</span> <span class="o">=</span> <span class="n">update_centroids</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">&gt;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">output</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">tie</span><span class="p">(</span><span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">output</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">};</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>When using Emscripten to compile C++ code to WebAssembly for use in JavaScript applications, it’s often necessary to create a C++ wrapper class to expose the desired functionality to JavaScript. This wrapper class acts as an interface between the C++ code and the JavaScript environment. By defining a clear and intuitive API in the wrapper class, developers can encapsulate the complexity of the underlying C++ implementation and provide a seamless integration with JavaScript. The wrapper class handles the conversion of data types between C++ and JavaScript, manages memory allocation and deallocation, and defines the public methods and properties that JavaScript code can access.Without a well-designed wrapper class, exposing C++ functionality directly to JavaScript can be cumbersome and error-prone, leading to issues such as memory leaks, unexpected behavior, and difficulties in maintaining and extending the codebase. By investing time in creating a robust and efficient C++ wrapper class, developers can ensure that their WebAssembly modules are easy to use, performant, and maintainable, ultimately enhancing the overall quality and reliability of their JavaScript applications.</p>

<p>Let’s create our own K-Means wrapper class in C++ and corresponding C++ distance functions.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
</pre></td><td class="code"><pre><span class="cp">#ifndef WASM_KMEANS_H
#define WASM_KMEANS_H
</span>
<span class="cp">#include</span> <span class="cpf">&lt;emscripten/bind.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;emscripten/val.h&gt;</span><span class="cp">
#include</span> <span class="cpf">"../kmeans.cpp"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">"utility.hpp"</span><span class="cp">
</span>
<span class="k">namespace</span> <span class="n">wasm</span> <span class="p">{</span>

    <span class="k">namespace</span> <span class="n">cluster</span> <span class="p">{</span>

        <span class="k">struct</span> <span class="nc">KResult</span> <span class="p">{</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">centroids</span><span class="p">;</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">clusters</span><span class="p">;</span>
        <span class="p">};</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="kt">double</span> <span class="n">ssd</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">point1</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">point2</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Sum of Squared Difference (SSD)</span>
            <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
            <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">dimension1</span> <span class="o">=</span> <span class="n">point1</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">dimension2</span> <span class="o">=</span> <span class="n">point2</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">dimension1</span> <span class="o">!=</span> <span class="n">dimension2</span><span class="p">){</span>
                <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimension1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
                <span class="n">distance</span> <span class="o">+=</span> <span class="n">pow</span><span class="p">(</span><span class="n">point2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">point1</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="mi">2</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="kt">double</span> <span class="n">euclidean</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">point1</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">point2</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Euclidean Distance</span>
            <span class="k">return</span> <span class="n">sqrt</span><span class="p">(</span><span class="n">ssd</span><span class="p">(</span><span class="n">point1</span><span class="p">,</span> <span class="n">point2</span><span class="p">));</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="k">class</span> <span class="nc">KMeans</span> <span class="p">{</span>
            <span class="k">static</span> <span class="k">const</span> <span class="kr">inline</span> <span class="n">std</span><span class="o">::</span><span class="n">unordered_map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="p">)(</span><span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">distance_funcs</span> <span class="o">=</span> <span class="p">{</span>
                <span class="p">{</span> <span class="s">"euclidean"</span><span class="p">,</span> <span class="n">euclidean</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">}</span>
            <span class="p">};</span>

            <span class="nl">public:</span>
                <span class="n">KMeans</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">,</span> <span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">distanceFunc</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">distance_funcs</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">distanceFunc</span><span class="p">)</span> <span class="o">==</span> <span class="n">distance_funcs</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
                        <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">invalid_argument</span><span class="p">(</span><span class="n">distanceFunc</span> <span class="o">+</span> <span class="s">" is not a valid distance metric"</span><span class="p">);</span>
                    <span class="p">}</span>
                    <span class="n">m_distance_func</span> <span class="o">=</span> <span class="n">distanceFunc</span><span class="p">;</span>
                    <span class="n">m_instance</span> <span class="o">=</span> <span class="k">new</span> <span class="n">clustering</span><span class="o">::</span><span class="n">KMeansContiguous</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="n">tolerance</span><span class="p">,</span> <span class="n">dimensions</span><span class="p">,</span> <span class="n">distance_funcs</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">distanceFunc</span><span class="p">));</span>
                <span class="p">}</span>

                <span class="n">KResult</span> <span class="n">predict</span><span class="p">(</span><span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsData</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// convert TypedArray to a T* pointer</span>
                    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">jsDataLength</span><span class="o">=</span> <span class="n">jsData</span><span class="p">[</span><span class="s">"length"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">();</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">jsData</span><span class="p">[</span><span class="s">"buffer"</span><span class="p">];</span> 
                    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">byte_data</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">convertJSArrayToNumberVector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">jsData</span><span class="p">);</span>
                    <span class="n">T</span><span class="o">*</span> <span class="n">data</span> <span class="o">=</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">T</span><span class="o">*&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">byte_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span> 

                    <span class="k">auto</span> <span class="n">results</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">predict</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">jsDataLength</span><span class="p">);</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>

                    <span class="c1">// convert data to Javascript</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getDimensions</span><span class="p">();</span>

                    <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsCentroids</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">array</span><span class="p">();</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getK</span><span class="p">();</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">k</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">jsCentroids</span><span class="p">.</span><span class="n">call</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"push"</span><span class="p">,</span> <span class="n">wasm</span><span class="o">::</span><span class="n">utility</span><span class="o">::</span><span class="n">contiguousVecToArray</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">dimensions</span><span class="p">],</span> <span class="n">dimensions</span><span class="p">));</span>
                    <span class="p">}</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">centroids</span><span class="p">);</span>

                    <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="n">clusters</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsClusters</span> <span class="o">=</span> <span class="n">wasm</span><span class="o">::</span><span class="n">utility</span><span class="o">::</span><span class="n">contiguousVecToArray</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">clusters</span><span class="p">,</span> <span class="n">jsDataLength</span> <span class="o">/</span> <span class="n">dimensions</span><span class="p">);</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">clusters</span><span class="p">);</span>
                    <span class="k">return</span> <span class="n">KResult</span><span class="p">{</span> <span class="n">jsCentroids</span><span class="p">,</span> <span class="n">jsClusters</span><span class="p">};</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setK</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setK</span><span class="p">(</span><span class="n">k</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">long</span> <span class="kt">int</span> <span class="n">getK</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getK</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setMaxIterations</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">maxIterations</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setMaxIterations</span><span class="p">(</span><span class="n">maxIterations</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">long</span> <span class="kt">int</span> <span class="n">getMaxIterations</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getMaxIterations</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setTolerance</span><span class="p">(</span><span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setTolerance</span><span class="p">(</span><span class="n">tolerance</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">double</span> <span class="n">getTolerance</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getTolerance</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">getDistanceFunc</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_distance_func</span><span class="p">;</span>
                <span class="p">}</span>

            <span class="nl">private:</span>
                <span class="n">clustering</span><span class="o">::</span><span class="n">KMeansContiguous</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">*</span> <span class="n">m_instance</span><span class="p">;</span>
                <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">m_distance_func</span><span class="p">;</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>To enhance the interoperability between JavaScript and the underlying C++ <code class="language-plaintext highlighter-rouge">KMeans</code> class, the wrapper class provides a comprehensive set of getters and setters for all properties. This allows developers to seamlessly read and modify the configuration of the <code class="language-plaintext highlighter-rouge">KMeans</code> instance at runtime, adapting it to the specific requirements of their application. Moreover, the wrapper class offers a text-based mapping of distance functions to their corresponding C++ implementations, enabling developers to easily select and switch between different distance metrics without diving into the complexities of the C++ codebase. By exposing a clean and intuitive interface, the wrapper class empowers developers to fine-tune and optimize the behavior of the K-Means algorithm, while abstracting away the low-level details of the C++ implementation. This approach not only enhances the flexibility and maintainability of the JavaScript application but also facilitates a more efficient and productive development workflow, ultimately leading to better performance and a smoother user experience.</p>

<p>Next we will need to create our utility functions to translate data between Javascript data structures and C++ data structures and vice versa.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
</pre></td><td class="code"><pre><span class="cp">#ifndef WASM_UTILITY_H
#define WASM_UTILITY_H
</span>
<span class="cp">#include</span> <span class="cpf">&lt;emscripten/val.h&gt;</span><span class="cp">
</span>
<span class="k">namespace</span> <span class="n">wasm</span> <span class="p">{</span>

    <span class="k">namespace</span> <span class="n">utility</span> <span class="p">{</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">contiguousVecToArray</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dataLength</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">arr</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">array</span><span class="p">();</span>
            <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dataLength</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">arr</span><span class="p">.</span><span class="n">call</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"push"</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]));</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">arr</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">arrayToVec</span><span class="p">(</span><span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">array</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">array</span><span class="p">.</span><span class="n">isArray</span><span class="p">())</span> <span class="p">{</span>
                <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="s">"Input is not a valid array"</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">length</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="s">"length"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">();</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">length</span><span class="p">);</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">length</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">();</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;&gt;</span> <span class="n">array2DToVec</span><span class="p">(</span><span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">array</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">array</span><span class="p">.</span><span class="n">isArray</span><span class="p">())</span> <span class="p">{</span>
                <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="s">"Input is not a valid array"</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="c1">// Dimensions</span>
            <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">arrayLength</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="s">"length"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">();</span>
            <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">columnCount</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">"length"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">();</span> 
            <span class="kt">bool</span> <span class="n">hasBuffer</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">hasOwnProperty</span><span class="p">(</span><span class="s">"buffer"</span><span class="p">);</span>

            <span class="c1">// Pre-allocate result vector</span>
            <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;&gt;</span> <span class="n">result</span><span class="p">(</span><span class="n">arrayLength</span><span class="p">);</span>

            <span class="c1">// Optimize assuming contiguous inner arrays:</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">arrayLength</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">innerArray</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>

                <span class="c1">// Get a direct pointer (if possible)</span>
                <span class="n">T</span><span class="o">*</span> <span class="n">rowData</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">hasBuffer</span><span class="p">)</span> <span class="p">{</span> 
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">innerArray</span><span class="p">[</span><span class="s">"buffer"</span><span class="p">];</span> 
                    <span class="n">rowData</span> <span class="o">=</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">T</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">uintptr_t</span><span class="o">&gt;</span><span class="p">());</span>
                <span class="p">}</span>

                <span class="k">if</span> <span class="p">(</span><span class="n">rowData</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// Super-efficient copy from contiguous data</span>
                    <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">assign</span><span class="p">(</span><span class="n">rowData</span><span class="p">,</span> <span class="n">rowData</span> <span class="o">+</span> <span class="n">columnCount</span><span class="p">);</span> 
                <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                    <span class="c1">// Fallback to the original method if no contiguous buffer</span>
                    <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">reserve</span><span class="p">(</span><span class="n">columnCount</span><span class="p">);</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">col</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">col</span> <span class="o">&lt;</span> <span class="n">columnCount</span><span class="p">;</span> <span class="o">++</span><span class="n">col</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">push_back</span><span class="p">(</span><span class="n">innerArray</span><span class="p">[</span><span class="n">col</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">());</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">vecToArray</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;&amp;</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">arr</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">array</span><span class="p">();</span>
            <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">value</span> <span class="o">:</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">arr</span><span class="p">.</span><span class="n">call</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"push"</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">value</span><span class="p">));</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">arr</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">vecToTypedArray</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;&amp;</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">dataSize</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">T</span><span class="p">);</span>

            <span class="c1">// Allocate an ArrayBuffer</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">arrayBuffer</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">global</span><span class="p">(</span><span class="s">"ArrayBuffer"</span><span class="p">).</span><span class="n">new_</span><span class="p">(</span><span class="n">dataSize</span><span class="p">);</span>

            <span class="c1">// Access the ArrayBuffer's data directly</span>
            <span class="kt">void</span><span class="o">*</span> <span class="n">arrayBufferMemory</span> <span class="o">=</span> <span class="n">arrayBuffer</span><span class="p">[</span><span class="s">"data"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">*&gt;</span><span class="p">();</span>

            <span class="c1">// Copy the data into the ArrayBuffer's memory</span>
            <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">arrayBufferMemory</span><span class="p">,</span> <span class="n">data</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">dataSize</span><span class="p">);</span>

            <span class="c1">// Create a Float32Array view on the ArrayBuffer</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">float32Array</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">global</span><span class="p">(</span><span class="s">"Float32Array"</span><span class="p">).</span><span class="n">new_</span><span class="p">(</span><span class="n">arrayBuffer</span><span class="p">);</span>

            <span class="k">return</span> <span class="n">float32Array</span><span class="p">;</span>
        <span class="p">}</span>

    <span class="p">}</span>
<span class="p">}</span>

<span class="cp">#endif </span><span class="cm">/* WASM_UTILITY_H */</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>The utility functions provided in the code snippet serve a crucial role in facilitating seamless data exchange between C++ and JavaScript in a WebAssembly (WASM) context. These functions are essential for businesses and developers looking to leverage the performance benefits of C++ while maintaining the flexibility and interactivity of JavaScript in their web applications.</p>

<p>Overall, these utility functions provide a bridge between C++ and JavaScript in our C++ wrapper class, to keep the focus of the wrapper class on the high-level translation of data between C++ and Javascript. They allow for efficient data transfer, seamless integration, and optimized performance, ultimately leading to improved developer experiences and faster development cycles.</p>

<p>Lastly, we need to create our WASM bindings for Emscripten to create interfaces between our C++ and Javascript.  Creating WASM bindings with Emscripten allows developers to make client-side use of C++ libraries, and apply C++ skills to web development.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
</pre></td><td class="code"><pre><span class="cp">#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;tuple&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;emscripten/val.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;emscripten/bind.h&gt;</span><span class="cp">
#include</span> <span class="cpf">"kmeans.cpp"</span><span class="cp">
#include</span> <span class="cpf">"utilities.cpp"</span><span class="cp">
</span>
<span class="k">using</span> <span class="k">namespace</span> <span class="n">emscripten</span><span class="p">;</span>
<span class="k">using</span> <span class="k">namespace</span> <span class="n">clustering</span><span class="p">;</span>


<span class="n">EMSCRIPTEN_BINDINGS</span><span class="p">(</span><span class="n">highp</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">register_vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"VectorInt"</span><span class="p">);</span>
    <span class="n">register_vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"VectorLongInt"</span><span class="p">);</span>
    <span class="n">register_vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"VectorDouble"</span><span class="p">);</span>
    <span class="n">register_vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;&gt;</span><span class="p">(</span><span class="s">"VectorMatrixDouble"</span><span class="p">);</span>
    <span class="n">register_vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"VectorString"</span><span class="p">);</span>

    <span class="n">function</span><span class="p">(</span><span class="s">"JsArrayToVectorDouble"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">emscripten</span><span class="o">::</span><span class="n">vecFromJSArray</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">);</span>
    <span class="n">function</span><span class="p">(</span><span class="s">"JsArrayToVectorInt"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">emscripten</span><span class="o">::</span><span class="n">vecFromJSArray</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">);</span>
    <span class="n">function</span><span class="p">(</span><span class="s">"JsArrayToVectorString"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">emscripten</span><span class="o">::</span><span class="n">vecFromJSArray</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span><span class="p">);</span>

    <span class="n">value_object</span><span class="o">&lt;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KResult</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"KResult"</span><span class="p">)</span>
        <span class="p">.</span><span class="n">field</span><span class="p">(</span><span class="s">"centroids"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KResult</span><span class="o">::</span><span class="n">centroids</span><span class="p">)</span>
        <span class="p">.</span><span class="n">field</span><span class="p">(</span><span class="s">"clusters"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KResult</span><span class="o">::</span><span class="n">clusters</span><span class="p">);</span>

    <span class="n">class_</span><span class="o">&lt;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;&gt;</span><span class="p">(</span><span class="s">"KMeans"</span><span class="p">)</span>
        <span class="p">.</span><span class="n">constructor</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">double</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span><span class="p">()</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"setK"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">setK</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"getK"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">getK</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"setMaxIterations"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">setMaxIterations</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"getMaxIterations"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">getMaxIterations</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"setTolerance"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">setTolerance</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"getTolerance"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">getTolerance</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"getDistance"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">getDistanceFunc</span><span class="p">)</span>
        <span class="p">.</span><span class="n">function</span><span class="p">(</span><span class="s">"predict"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wasm</span><span class="o">::</span><span class="n">cluster</span><span class="o">::</span><span class="n">KMeans</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">predict</span><span class="p">);</span>

    <span class="n">function</span><span class="p">(</span><span class="s">"sad"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">distance</span><span class="o">::</span><span class="n">sad</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">);</span>
    <span class="n">function</span><span class="p">(</span><span class="s">"euclidean"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">distance</span><span class="o">::</span><span class="n">euclidean</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Now we need to execute the <code class="language-plaintext highlighter-rouge">em++</code> command to compile our C++ into WebAssembly:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell">em++ <span class="nt">-std</span><span class="o">=</span>c++17 <span class="se">\</span>
    <span class="nt">-lembind</span> <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">WASM</span><span class="o">=</span>1 <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">MODULARIZE</span><span class="o">=</span>1 <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">EXPORT_NAME</span><span class="o">=</span><span class="s2">"'KMEANS'"</span> <span class="se">\</span>
    <span class="nt">-sALLOW_MEMORY_GROWTH</span> <span class="se">\</span>
    <span class="nt">-O2</span> <span class="nt">-gsource-map</span> <span class="se">\</span>
    <span class="nt">--profiling</span> <span class="se">\</span>
    <span class="nt">--profiling-funcs</span> <span class="se">\</span>
    <span class="nt">--tracing</span> <span class="se">\</span>
    <span class="nt">-sNO_DISABLE_EXCEPTION_CATCHING</span> <span class="se">\</span>
    <span class="nt">-sASSERTIONS</span> <span class="se">\</span>
    <span class="nt">-o</span> ./bin/kmeans.js <span class="se">\</span>
    ./bindings.cpp</code></pre></figure>

<p>Emscripten offers developers a wide range of <a href="https://emscripten.org/docs/tools_reference/emcc.html">command-line flags</a> allowing fine-grained control over the compilation process, enabling optimization, debugging, and customization of the generated WASM module.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">-lembind</code>: This flag links the Embind library, which is a C++ binding library for Emscripten. Embind simplifies the process of exposing C++ functions and classes to JavaScript, making it easier to create bindings between the two languages.</li>
  <li><code class="language-plaintext highlighter-rouge">-s WASM=1</code>: This flag specifies that the output should be a WebAssembly module. It instructs Emscripten to compile the C++ code to WASM instead of the default asm.js format.</li>
  <li><code class="language-plaintext highlighter-rouge">-s MODULARIZE=1</code>: This flag modularizes the generated JavaScript code. It wraps the Emscripten runtime and the generated WASM module in a function, allowing for better encapsulation and avoiding global namespace pollution.</li>
  <li><code class="language-plaintext highlighter-rouge">-s EXPORT_NAME="'KMEANS'"</code>: This flag sets the name of the exported JavaScript function that will be used to initialize the WASM module. In this case, the exported function will be named “KMEANS”.</li>
  <li><code class="language-plaintext highlighter-rouge">-sALLOW_MEMORY_GROWTH</code>: This flag allows the WASM module’s memory to grow dynamically at runtime. By default, WASM modules have a fixed memory size, but enabling this flag allows the module to request more memory as needed.</li>
  <li><code class="language-plaintext highlighter-rouge">-O2</code>: This flag sets the optimization level to 2, which enables a balance between fast compilation and good performance. It applies various optimization techniques to the generated code, resulting in smaller and faster WASM modules.</li>
  <li><code class="language-plaintext highlighter-rouge">-gsource-map</code>: This flag generates a source map file alongside the compiled WASM module. Source maps provide a mapping between the generated WASM code and the original C++ source code, enabling better debugging and profiling experiences.</li>
  <li><code class="language-plaintext highlighter-rouge">--profiling</code>: This flag enables profiling support in the generated WASM module. It adds necessary instrumentation to collect profiling data, allowing developers to analyze and optimize the performance of their WASM code.</li>
  <li><code class="language-plaintext highlighter-rouge">--profiling-funcs</code>: This flag enables function-level profiling. It generates additional profiling information for individual functions, providing more granular performance insights</li>
  <li><code class="language-plaintext highlighter-rouge">--tracing</code>: This flag enables tracing support in the generated WASM module. It allows developers to trace the execution of their WASM code, which can be useful for debugging and understanding the program’s flow.</li>
  <li><code class="language-plaintext highlighter-rouge">-sNO_DISABLE_EXCEPTION_CATCHING</code>: This flag disables the automatic disabling of exception catching in the generated WASM module. By default, Emscripten disables exception catching for performance reasons, but this flag allows exceptions to be caught and handled normally.</li>
  <li><code class="language-plaintext highlighter-rouge">-sASSERTIONS</code>: This flag enables assertions in the generated WASM module. Assertions are used for runtime checks and can help catch bugs and logical errors during development.</li>
  <li><code class="language-plaintext highlighter-rouge">-o ./bin/kmeans.js</code>: This flag specifies the output file name and location for the generated JavaScript file that accompanies the WASM module. In this case, the output file will be named “kmeans.js” and will be placed in the “./bin” directory.</li>
  <li><code class="language-plaintext highlighter-rouge">./bindings.cpp</code>: This is the input file containing the C++ code to be compiled to WASM. It likely includes the bindings code that exposes the necessary functions and classes to JavaScript.</li>
</ol>

<p><strong>NOTE</strong>: The  <code class="language-plaintext highlighter-rouge">-gsource-map</code>, <code class="language-plaintext highlighter-rouge">--profiling</code>, <code class="language-plaintext highlighter-rouge">--profiling-funcs</code>, <code class="language-plaintext highlighter-rouge">--tracing</code>, and <code class="language-plaintext highlighter-rouge">-sASSERTIONS</code> flags are including for performance profiling and would not be included in a production to increase performance.</p>

<h3 id="benchmarking-logic" class="linked-heading">
    Benchmarking Logic<a href="#benchmarking-logic">🔗</a>
</h3>

<p>Benchmarking logic is a critical aspect of comparing the performance of our Javascript <code class="language-plaintext highlighter-rouge">KMeans</code> algorithm implementation and our Emscripten implementation. By carefully designing and executing benchmarks, we can objectively measure and analyze the execution time, resource usage, and scalability of both implementations under various scenarios.  The test will be to execute K-Means clustering on 667,000 pixels on 4 dimensions (l,a,b,alpha) and assign these pixels to 6 centroids.</p>

<p>The following code will be the basis of our benchmarks:</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
</pre></td><td class="code"><pre><span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;img</span> <span class="na">id=</span><span class="s">"uploaded-image"</span> <span class="na">src=</span><span class="s">"..."</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
    <span class="nt">&lt;script&gt;</span>
        <span class="kd">var</span> <span class="nx">imgElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#uploaded-image</span><span class="dl">"</span><span class="p">),</span>
            <span class="nx">totalTests</span> <span class="o">=</span> <span class="mi">1000</span><span class="p">,</span>
            <span class="nx">maxIterations</span> <span class="o">=</span> <span class="mi">50</span><span class="p">,</span>
            <span class="nx">kClusters</span> <span class="o">=</span> <span class="mi">10</span><span class="p">,</span>
            <span class="nx">rawImgData</span> <span class="o">=</span> <span class="nx">loadData</span><span class="p">(</span><span class="nx">element</span><span class="p">),</span>
            <span class="nx">preparedImgData</span> <span class="o">=</span> <span class="nx">prepareData</span><span class="p">(</span><span class="nx">rawImgData</span><span class="p">),</span>
            <span class="nx">kmeansClf</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">KMeans</span><span class="p">(</span><span class="nx">kClusters</span><span class="p">,</span> <span class="nx">maxIterations</span><span class="p">,</span> <span class="mf">0.00001</span><span class="p">,</span> <span class="dl">"</span><span class="s2">euclidean</span><span class="dl">"</span><span class="p">),</span>
            <span class="nx">results</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">totalTests</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">setTimeout</span><span class="p">(</span><span class="k">async</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="c1">// timeout included to avoid browser lock-ups, while ensuring consistent measurements</span>
                <span class="kd">var</span> <span class="nx">start</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(),</span>
                    <span class="nx">results</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">kmeansClf</span><span class="p">.</span><span class="nx">predict</span><span class="p">(</span><span class="nx">preparedImgData</span><span class="p">),</span>
                    <span class="nx">duration</span> <span class="o">=</span> <span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">())</span> <span class="o">-</span> <span class="nx">start</span><span class="p">;</span>
                <span class="nx">results</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">duration</span><span class="p">);</span>
            <span class="p">},</span> <span class="mi">0</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="c1">// compile results</span>
        <span class="kd">var</span> <span class="nx">mean</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span><span class="p">)</span> <span class="o">/</span> <span class="nx">totalTests</span><span class="p">,</span>
            <span class="nx">stdDeviation</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">sqrt</span><span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">x</span> <span class="o">=&gt;</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">x</span> <span class="o">-</span> <span class="nx">mean</span><span class="p">,</span> <span class="mi">2</span><span class="p">)).</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span><span class="p">)</span> <span class="o">/</span> <span class="nx">totalTests</span><span class="p">);</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">mean</span><span class="p">,</span> <span class="nx">stdDeviation</span><span class="p">);</span>
        <span class="cm">/**
         * Loads data from image
         */</span>
        <span class="kd">function</span> <span class="nx">loadData</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">canvas</span><span class="dl">"</span><span class="p">),</span>
                <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">);</span>
            <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">width</span><span class="p">;</span>
            <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">height</span><span class="p">;</span>
            <span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">element</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">imageData</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">getImageData</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">element</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">element</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
            <span class="k">return</span> <span class="nx">imageData</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="cm">/*
         * Converts image RGB colors to LAB colors
         * */</span>
        <span class="kd">function</span> <span class="nx">prepareData</span><span class="p">(</span><span class="nx">imgData</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">output</span> <span class="o">=</span> <span class="p">[];</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">var</span> <span class="nx">rgbColor</span> <span class="o">=</span> <span class="p">[</span><span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">],</span> <span class="nx">imgData</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">2</span><span class="p">],</span> <span class="mf">1.0</span><span class="p">],</span>
                <span class="nx">labColor</span> <span class="o">=</span> <span class="nx">rgb2lab</span><span class="p">(</span><span class="nx">rgbColor</span><span class="p">);</span>
                <span class="nx">output</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">labColor</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="nx">output</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="cm">/**
         * converts an RGBA color array to an LAB color array
         */</span>
        <span class="kd">function</span> <span class="nx">rgb2lab</span><span class="p">(</span><span class="nx">rgba</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">r</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
                <span class="nx">g</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
                <span class="nx">b</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">/</span> <span class="mi">255</span><span class="p">,</span>
                <span class="nx">alpha</span> <span class="o">=</span> <span class="nx">rgba</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="c1">// alpha, untouched</span>
                <span class="nx">x</span><span class="p">,</span>
                <span class="nx">y</span><span class="p">,</span>
                <span class="nx">z</span><span class="p">;</span>
            <span class="nx">r</span> <span class="o">=</span> <span class="nx">r</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">r</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">r</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>
            <span class="nx">g</span> <span class="o">=</span> <span class="nx">g</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">g</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">g</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>
            <span class="nx">b</span> <span class="o">=</span> <span class="nx">b</span> <span class="o">&gt;</span> <span class="mf">0.04045</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">((</span><span class="nx">b</span> <span class="o">+</span> <span class="mf">0.055</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.055</span><span class="p">,</span> <span class="mf">2.4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">b</span> <span class="o">/</span> <span class="mf">12.92</span><span class="p">;</span>

            <span class="nx">x</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.4124</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.3576</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.1805</span><span class="p">)</span> <span class="o">/</span> <span class="mf">0.95047</span><span class="p">;</span>
            <span class="nx">y</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.2126</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.7152</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.0722</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.0</span><span class="p">;</span>
            <span class="nx">z</span> <span class="o">=</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span> <span class="mf">0.0193</span> <span class="o">+</span> <span class="nx">g</span> <span class="o">*</span> <span class="mf">0.1192</span> <span class="o">+</span> <span class="nx">b</span> <span class="o">*</span> <span class="mf">0.9505</span><span class="p">)</span> <span class="o">/</span> <span class="mf">1.08883</span><span class="p">;</span>

            <span class="nx">x</span> <span class="o">=</span> <span class="nx">x</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">x</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>
            <span class="nx">y</span> <span class="o">=</span> <span class="nx">y</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">y</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">y</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>
            <span class="nx">z</span> <span class="o">=</span> <span class="nx">z</span> <span class="o">&gt;</span> <span class="mf">0.008856</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">pow</span><span class="p">(</span><span class="nx">z</span><span class="p">,</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="p">:</span> <span class="mf">7.787</span> <span class="o">*</span> <span class="nx">z</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">/</span> <span class="mi">116</span><span class="p">;</span>

            <span class="k">return</span> <span class="k">new</span> <span class="nb">Float32Array</span><span class="p">([</span><span class="mi">116</span> <span class="o">*</span> <span class="nx">y</span> <span class="o">-</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">500</span> <span class="o">*</span> <span class="p">(</span><span class="nx">x</span> <span class="o">-</span> <span class="nx">y</span><span class="p">),</span> <span class="mi">200</span> <span class="o">*</span> <span class="p">(</span><span class="nx">y</span> <span class="o">-</span> <span class="nx">z</span><span class="p">),</span> <span class="nx">alpha</span><span class="p">]);</span>
        <span class="p">}</span>
    <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Let’s benchmark the  with the Javascript implementation of the K-Means algorithm.</p>

<table>
  <tbody>
    <tr>
      <td>Type</td>
      <td>Average (ms)</td>
      <td>Std Dev (ms)</td>
      <td>Factor</td>
    </tr>
    <tr>
      <td>JS</td>
      <td>17,892</td>
      <td>203</td>
      <td>1</td>
    </tr>
    <tr>
      <td>WASM</td>
      <td>61,054</td>
      <td>482</td>
      <td>3.412</td>
    </tr>
  </tbody>
</table>

<p>That is very underwhelming, especially considering how much work we have put into our WebAssembly implementation.  This C++ K-Means implementation over 3x slower our Javascript implementation.</p>

<p>An important thing to keep in mind is that we are compiling C++ code to WebAssembly, not simply running C++ logic.  So we need to implement the C++ to run optimally in WebAssembly.</p>

<ol>
  <li>The code extensively uses vector methods such as <code class="language-plaintext highlighter-rouge">at()</code> and <code class="language-plaintext highlighter-rouge">size()</code> to access and manipulate data. While these methods provide a convenient and safe way to interact with vectors in C++, they can introduce performance overhead when compiled to WebAssembly.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">at()</code>: The <code class="language-plaintext highlighter-rouge">at()</code> method is used to access elements in a vector, providing bounds checking to ensure that the accessed index within the vector’s range.  However, when compiled to WebAssembly, the <code class="language-plaintext highlighter-rouge">at()</code> method is translated to JavaScript code that performs additional checks and indirections.  These checks and indirections add overhead to each element access, slowing down the overall performance of the code.  In this implementation, everytime we access an item in an array (which is very often), we add another bottleneck.</li>
      <li><code class="language-plaintext highlighter-rouge">size()</code>: The <code class="language-plaintext highlighter-rouge">size()</code> method is used to retrieve the number of elements in a vector.  In C++, the <code class="language-plaintext highlighter-rouge">size()</code> method typically has constant-time complexity, as the size of the vector is stored internally. However, when compiled to WebAssembly, the <code class="language-plaintext highlighter-rouge">size()</code> method may be translated to JavaScript code that calculates the size dynamically, introducing additional overhead.</li>
      <li><strong>Solution</strong>:  To mitigate the performance bottlenecks caused by vector methods, it’s recommended to use direct array access whenever possible. Direct array access involves accessing elements using the square bracket notation (<code class="language-plaintext highlighter-rouge">[]</code>) just as in Javascript instead of the <code class="language-plaintext highlighter-rouge">at()</code> method, and storing the size of the vector in a separate variable instead of calling <code class="language-plaintext highlighter-rouge">size()</code> repeatedly.</li>
    </ul>
  </li>
</ol>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="c1">// Instead of using at() method</span>
<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">};</span>
<span class="kt">int</span> <span class="n">element</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>

<span class="c1">// Use direct array access</span>
<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">};</span>
<span class="kt">int</span> <span class="n">element</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span></code></pre></figure>

<ol>
  <li>Unnecessary Copies:
    <ul>
      <li>Minimize unnecessary copying of data between vectors or other data structures.</li>
      <li><strong>Solution</strong>: Pass vectors by reference or <code class="language-plaintext highlighter-rouge">const</code> reference whenever possible to avoid the overhead of copying.</li>
    </ul>
  </li>
  <li>The code extensively uses nested <code class="language-plaintext highlighter-rouge">std::vector&lt;T&gt;</code> to represent and manipulate data. While nested <code class="language-plaintext highlighter-rouge">vectors</code> provide a convenient way to organize and access multi-dimensional data in C++, they can introduce performance challenges when compiled to WebAssembly.
    <ul>
      <li>Vectors of vectors do not guarantee contiguous memory layout.  Each inner vector is allocated separately, resulting in fragmented memory. Accessing elements in nested <code class="language-plaintext highlighter-rouge">vectors</code> requires multiple levels of indirection, as each inner <code class="language-plaintext highlighter-rouge">vector</code> needs to be accessed separately.  The non-contiguous memory layout can lead to poor cache locality and increased memory access latency.</li>
      <li><strong>Solution</strong>:  Use contiguous memory layouts whenever possible. Contiguous memory layouts involve storing multi-dimensional data in a single, flattened array, where elements are laid out sequentially in memory.</li>
    </ul>
  </li>
</ol>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="c1">// Instead of using vectors of vectors</span>
<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">},</span> <span class="p">{</span><span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">},</span> <span class="p">{</span><span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">}};</span>
<span class="kt">int</span> <span class="n">element</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="mi">2</span><span class="p">];</span>

<span class="c1">// Use contiguous memory layout</span>
<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">};</span>
<span class="kt">int</span> <span class="n">rows</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">cols</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">element</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="mi">1</span> <span class="o">*</span> <span class="n">cols</span> <span class="o">+</span> <span class="mi">2</span><span class="p">];</span></code></pre></figure>

<p>Of these changes, the change from nested <code class="language-plaintext highlighter-rouge">vectors</code> to contiguous memory layouts will be the most time-consuming change, but will also have a significant increase in performance.</p>

<p><strong>NOTE</strong>:  I am including these issues to demonstrate that many known optimizations from C/C++ also apply when compiling to WebAssembly via Emscripten (namely contiguous memory layouts), whereas as avoiding unnecessary methods calls to avoid Javascript-WebAssembly translations is a less obvious performance optimization when first starting writing C/C++ for Emscripten.</p>

<h3 id="vectorization-of-c-implementation" class="linked-heading">
    Vectorization of C++ Implementation<a href="#vectorization-of-c-implementation">🔗</a>
</h3>

<p>I have gone ahead and re-implemented the <code class="language-plaintext highlighter-rouge">KMeans</code> class as <code class="language-plaintext highlighter-rouge">KMeansContiguous</code>.  This implementation includes all of our optimizations we just listed:</p>

<ul>
  <li>It uses direct memory access</li>
  <li>Passes data by reference between functions</li>
  <li>Uses contiguous memory for working with raw data, distance matrices, and clustering assignments.</li>
</ul>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
</pre></td><td class="code"><pre><span class="cp">#ifndef KMEANS_H
#define KMEANS_H
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;tuple&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;algorithm&gt;</span><span class="cp">
</span>

<span class="k">namespace</span> <span class="n">clustering</span> <span class="p">{</span>

    <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
    <span class="k">class</span> <span class="nc">KMeansContiguous</span> <span class="p">{</span>
        <span class="nl">private:</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">m_k</span><span class="p">;</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">m_max_iterations</span><span class="p">;</span>
            <span class="kt">double</span> <span class="n">m_tolerance</span><span class="p">;</span>
            <span class="kt">long</span> <span class="kt">int</span> <span class="n">m_dimensions</span><span class="p">;</span>
            <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="n">m_distance</span><span class="p">)(</span><span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span><span class="p">);</span>

            <span class="kt">void</span> <span class="n">initialize_random_centroids</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dataLength</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span><span class="p">)</span> <span class="p">{</span> 
                <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="kt">size_t</span> <span class="n">dataPoints</span> <span class="o">=</span> <span class="n">dataLength</span> <span class="o">/</span> <span class="n">m_dimensions</span><span class="p">;</span>

                <span class="n">srand</span><span class="p">(</span><span class="n">time</span><span class="p">(</span><span class="nb">NULL</span><span class="p">));</span> 
                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// initialize centroid to a random point from the provided data</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">random_seed</span> <span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="p">(</span><span class="n">dataPoints</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">random_seed</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">];</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="kt">void</span> <span class="n">initialize_kpp_centroids</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dataLength</span><span class="p">,</span> <span class="n">T</span> <span class="o">*</span><span class="n">centroids</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">size_t</span> <span class="n">dataPoints</span> <span class="o">=</span> <span class="n">dataLength</span> <span class="o">/</span> <span class="n">m_dimensions</span><span class="p">;</span>
                <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

                <span class="kt">double</span> <span class="o">**</span> <span class="n">centroidIndices</span> <span class="o">=</span> <span class="p">(</span><span class="kt">double</span> <span class="o">**</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">double</span> <span class="o">*</span><span class="p">)</span> <span class="o">*</span> <span class="n">m_k</span><span class="p">);</span>
                <span class="n">srand</span><span class="p">(</span><span class="n">time</span><span class="p">(</span><span class="nb">NULL</span><span class="p">));</span> 

                <span class="c1">//set first seed</span>
                <span class="kt">size_t</span> <span class="n">random_seed</span> <span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="p">(</span><span class="n">dataPoints</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
                <span class="n">centroidIndices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">[</span><span class="n">random_seed</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">];</span>
                <span class="cp">#pragma omp parallel for private(i) shared(centroids, data)
</span>                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">centroids</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">random_seed</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">i</span><span class="p">];</span>
                <span class="p">}</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">current_centroid</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

                <span class="k">while</span> <span class="p">(</span><span class="n">current_centroid</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">double</span> <span class="n">maxMinDistance</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">numeric_limits</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">min</span><span class="p">();</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">maxCentroidIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">dataPoints</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="c1">// check if already selected as a centroid</span>
                        <span class="kt">bool</span> <span class="n">isSelected</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
                        <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">current_centroid</span><span class="p">;</span> <span class="o">++</span><span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                            <span class="k">if</span> <span class="p">(</span><span class="o">&amp;</span><span class="n">data</span><span class="p">[</span><span class="n">j</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">]</span> <span class="o">==</span> <span class="n">centroidIndices</span><span class="p">[</span><span class="n">k</span><span class="p">])</span> <span class="p">{</span>
                                <span class="n">isSelected</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
                                <span class="k">break</span><span class="p">;</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">isSelected</span><span class="p">)</span> <span class="p">{</span>
                            <span class="k">continue</span><span class="p">;</span>
                        <span class="p">}</span>
                        <span class="cm">/* end Check if already selected */</span>
                        
                        <span class="kt">double</span> <span class="n">currentMinDistance</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">numeric_limits</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">max</span><span class="p">();</span>
                        <span class="k">for</span> <span class="p">(</span><span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">current_centroid</span><span class="p">;</span> <span class="o">++</span><span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                            <span class="kt">double</span> <span class="n">potentialDistance</span> <span class="o">=</span> <span class="n">m_distance</span><span class="p">(</span><span class="o">&amp;</span><span class="n">centroids</span><span class="p">[</span><span class="n">k</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">[</span><span class="n">j</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">],</span> <span class="n">m_dimensions</span><span class="p">);</span>
                            <span class="k">if</span> <span class="p">(</span><span class="n">potentialDistance</span> <span class="o">&lt;</span> <span class="n">currentMinDistance</span><span class="p">)</span> <span class="p">{</span>
                                <span class="n">currentMinDistance</span> <span class="o">=</span> <span class="n">potentialDistance</span><span class="p">;</span>
                            <span class="p">}</span>
                        <span class="p">}</span>

                        <span class="k">if</span> <span class="p">(</span><span class="n">currentMinDistance</span> <span class="o">&gt;</span> <span class="n">maxMinDistance</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">maxMinDistance</span> <span class="o">=</span> <span class="n">currentMinDistance</span><span class="p">;</span>
                            <span class="n">maxCentroidIndex</span> <span class="o">=</span> <span class="n">j</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span>

                    <span class="n">centroids</span><span class="p">[</span><span class="n">current_centroid</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">maxCentroidIndex</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">];</span>
                    <span class="n">centroidIndices</span><span class="p">[</span><span class="n">current_centroid</span><span class="p">]</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">[</span><span class="n">maxCentroidIndex</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">];</span>
                    <span class="o">++</span><span class="n">current_centroid</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="kt">double</span> <span class="n">update_centroids</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dataLength</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="n">clusters</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">size_t</span> <span class="n">dataPoints</span> <span class="o">=</span> <span class="n">dataLength</span> <span class="o">/</span> <span class="n">m_dimensions</span><span class="p">;</span>

                <span class="kt">long</span> <span class="kt">double</span> <span class="o">*</span> <span class="n">sums</span> <span class="o">=</span> <span class="p">(</span><span class="kt">long</span> <span class="kt">double</span> <span class="o">*</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">long</span> <span class="kt">double</span><span class="p">)</span> <span class="o">*</span> <span class="n">m_k</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">);</span>
                <span class="kt">size_t</span> <span class="o">*</span> <span class="n">counts</span> <span class="o">=</span> <span class="p">(</span><span class="kt">size_t</span> <span class="o">*</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">size_t</span><span class="p">)</span> <span class="o">*</span> <span class="n">m_k</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">);</span>
                <span class="n">T</span> <span class="o">*</span> <span class="n">new_centroids</span> <span class="o">=</span> <span class="p">(</span><span class="n">T</span> <span class="o">*</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="o">*</span> <span class="n">m_k</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">);</span>

                <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">cluster</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">cluster</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">;</span> <span class="o">++</span><span class="n">cluster</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">dimension</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">dimension</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">dimension</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">sums</span><span class="p">[</span><span class="n">cluster</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">dimension</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
                    <span class="p">}</span>
                <span class="p">}</span>

                <span class="cp">#pragma omp parallel for private(i) shared(data, centroids)
</span>                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">dataPoints</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="o">++</span><span class="n">counts</span><span class="p">[</span><span class="n">clusters</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">i</span><span class="p">];</span>
                        <span class="n">sums</span><span class="p">[</span><span class="n">clusters</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">i</span><span class="p">]</span> <span class="o">+=</span> <span class="n">data</span><span class="p">[</span><span class="n">j</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">i</span><span class="p">];</span>
                    <span class="p">}</span>
                <span class="p">}</span>

                <span class="c1">// find euclidean mean of points assigned to each centroid</span>
                <span class="c1">// to determine the  new centroid location</span>
                <span class="cp">#pragma omp parallel for private(i) shared(centroids)
</span>                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// check if has any assignments</span>
                    <span class="kt">bool</span> <span class="n">hasAssignments</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">counts</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">])</span> <span class="p">{</span>
                            <span class="n">hasAssignments</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
                            <span class="k">break</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                    <span class="c1">// if centroid has assignments, move centroid to  euclidean mean of points assigned to it</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">hasAssignments</span><span class="p">)</span> <span class="p">{</span>
                        <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                            <span class="kt">long</span> <span class="kt">double</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">sums</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">];</span>
                            <span class="kt">size_t</span> <span class="n">count</span> <span class="o">=</span> <span class="n">counts</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">];</span>
                            <span class="c1">//printf("Sum: %Lf, Count: %f\n", sum, T(count));</span>
                            <span class="n">new_centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">sum</span> <span class="o">/</span> <span class="p">(</span><span class="n">T</span><span class="p">)</span><span class="n">count</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="c1">// if no assignments, assign centroid to a random point</span>
                        <span class="kt">size_t</span> <span class="n">random_seed</span> <span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="p">(</span><span class="n">dataPoints</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
                        <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">new_centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">random_seed</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">];</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>

                <span class="kt">double</span> <span class="n">changes</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">m_distance</span><span class="p">(</span><span class="o">&amp;</span><span class="n">centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">new_centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">],</span> <span class="n">m_dimensions</span><span class="p">);</span>
                    <span class="n">changes</span> <span class="o">+=</span> <span class="n">distance</span><span class="p">;</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">new_centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">+</span> <span class="n">j</span><span class="p">];</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="k">return</span> <span class="n">changes</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">long</span> <span class="kt">int</span> <span class="n">update_clusters</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dataLength</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="n">clusters</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">size_t</span> <span class="n">dataPoints</span> <span class="o">=</span> <span class="n">dataLength</span> <span class="o">/</span> <span class="n">m_dimensions</span><span class="p">;</span>
                <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">assignment_changes</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

                <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">closest_centroid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="kt">double</span> <span class="n">closest_centroid_distance</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">numeric_limits</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">max</span><span class="p">();</span>
                <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dataPoints</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">T</span><span class="o">*</span> <span class="n">sample</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">data</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">];</span>
                    <span class="n">closest_centroid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                    <span class="n">closest_centroid_distance</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">numeric_limits</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;::</span><span class="n">max</span><span class="p">();</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">m_k</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">T</span><span class="o">*</span> <span class="n">centroid</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">centroids</span><span class="p">[</span><span class="n">j</span> <span class="o">*</span> <span class="n">m_dimensions</span><span class="p">];</span>
                        <span class="n">distance</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_distance</span><span class="p">(</span><span class="n">sample</span><span class="p">,</span> <span class="n">centroid</span><span class="p">,</span> <span class="n">m_dimensions</span><span class="p">);</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">distance</span> <span class="o">&lt;</span> <span class="n">closest_centroid_distance</span><span class="p">)</span> <span class="p">{</span>
                            <span class="n">closest_centroid_distance</span> <span class="o">=</span> <span class="n">distance</span><span class="p">;</span>
                            <span class="n">closest_centroid</span> <span class="o">=</span> <span class="n">j</span><span class="p">;</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">clusters</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">!=</span> <span class="n">closest_centroid</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">clusters</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">closest_centroid</span><span class="p">;</span>
                        <span class="o">++</span><span class="n">assignment_changes</span><span class="p">;</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="k">return</span> <span class="n">assignment_changes</span><span class="p">;</span>
            <span class="p">}</span>

        <span class="nl">public:</span>
            <span class="n">KMeansContiguous</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">,</span> <span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">,</span> <span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">,</span> <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="n">distance_func</span><span class="p">)(</span><span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">m_k</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span>
                <span class="n">m_max_iterations</span> <span class="o">=</span> <span class="n">max_iterations</span><span class="p">;</span>
                <span class="n">m_tolerance</span> <span class="o">=</span> <span class="n">tolerance</span><span class="p">;</span>
                <span class="n">m_dimensions</span> <span class="o">=</span> <span class="n">dimensions</span><span class="p">;</span>
                <span class="n">m_distance</span> <span class="o">=</span> <span class="n">distance_func</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">void</span> <span class="n">setK</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_k</span> <span class="o">=</span> <span class="n">k</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">long</span> <span class="kt">int</span> <span class="n">getK</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_k</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">void</span> <span class="n">setDimensions</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_dimensions</span> <span class="o">=</span> <span class="n">dimensions</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">long</span> <span class="kt">int</span> <span class="n">getDimensions</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_dimensions</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">void</span> <span class="n">setMaxIterations</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">maxIterations</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_max_iterations</span> <span class="o">=</span> <span class="n">maxIterations</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">long</span> <span class="kt">int</span> <span class="n">getMaxIterations</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_max_iterations</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">void</span> <span class="n">setTolerance</span><span class="p">(</span><span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_tolerance</span> <span class="o">=</span> <span class="n">tolerance</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">double</span> <span class="n">getTolerance</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_tolerance</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">T</span> <span class="o">*</span> <span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="o">&gt;</span> <span class="n">predict</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">length</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">pointCount</span> <span class="o">=</span> <span class="n">length</span> <span class="o">/</span> <span class="n">m_dimensions</span><span class="p">;</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="n">clusters</span> <span class="o">=</span> <span class="p">(</span><span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">long</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span> <span class="n">pointCount</span><span class="p">);</span>
                <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span> <span class="o">=</span> <span class="p">(</span><span class="n">T</span> <span class="o">*</span><span class="p">)</span> <span class="n">malloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="o">*</span> <span class="n">m_dimensions</span> <span class="o">*</span> <span class="n">m_k</span><span class="p">);</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">current_iteration</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
                <span class="kt">double</span> <span class="n">centroid_changes</span> <span class="o">=</span> <span class="n">m_tolerance</span><span class="p">;</span>
                <span class="kt">long</span> <span class="kt">int</span> <span class="n">assignment_changes</span> <span class="o">=</span> <span class="n">m_tolerance</span><span class="p">;</span>

                <span class="n">initialize_kpp_centroids</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">length</span><span class="p">,</span> <span class="n">centroids</span><span class="p">);</span>
                <span class="k">while</span> <span class="p">(</span><span class="n">current_iteration</span> <span class="o">&lt;</span> <span class="n">m_max_iterations</span> <span class="o">&amp;&amp;</span> <span class="n">centroid_changes</span> <span class="o">&gt;=</span> <span class="n">m_tolerance</span><span class="p">)</span> <span class="p">{</span>
                    <span class="o">++</span><span class="n">current_iteration</span><span class="p">;</span>
                    <span class="n">assignment_changes</span> <span class="o">=</span> <span class="n">update_clusters</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">length</span><span class="p">,</span> <span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
                    <span class="c1">//emscripten_console_log(("Assignment changes: " + std::to_string(assignment_changes)).c_str());</span>
                    <span class="n">centroid_changes</span> <span class="o">=</span> <span class="n">update_centroids</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">length</span><span class="p">,</span> <span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
                <span class="p">}</span>
                <span class="c1">//printf("Current iteration: %zu, centroid changes: %f\n", current_iteration, centroid_changes);</span>
                <span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">T</span> <span class="o">*</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="o">*&gt;</span> <span class="n">output</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">tie</span><span class="p">(</span><span class="n">centroids</span><span class="p">,</span> <span class="n">clusters</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">output</span><span class="p">;</span>
            <span class="p">}</span>
    <span class="p">};</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>We will need to modify our wrapper C++ class to use the correct <code class="language-plaintext highlighter-rouge">KMeans</code> implementation, as well as change our distance functions to use contiguous memory and direct array access:</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="cp">#ifndef WASM_KMEANS_H
#define WASM_KMEANS_H
</span>
<span class="cp">#include</span> <span class="cpf">&lt;emscripten/bind.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;emscripten/val.h&gt;</span><span class="cp">
#include</span> <span class="cpf">"../kmeans.cpp"</span><span class="cp">
#include</span> <span class="cpf">"../distance.hpp"</span><span class="cp">
#include</span> <span class="cpf">&lt;wasm_simd128.h&gt;</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">"utility.hpp"</span><span class="cp">
</span>
<span class="k">namespace</span> <span class="n">wasm</span> <span class="p">{</span>

    <span class="k">namespace</span> <span class="n">cluster</span> <span class="p">{</span>

        <span class="k">struct</span> <span class="nc">KResult</span> <span class="p">{</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">centroids</span><span class="p">;</span>
            <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">clusters</span><span class="p">;</span>
        <span class="p">};</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="kt">double</span> <span class="n">ssd</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">point1</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">point2</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Sum of Squared Difference (SSD)</span>
            <span class="kt">double</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
                <span class="n">distance</span> <span class="o">+=</span> <span class="n">pow</span><span class="p">(</span><span class="n">point2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">point1</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="mi">2</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="kt">double</span> <span class="n">euclidean</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">point1</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">point2</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Euclidean Distance</span>
            <span class="k">return</span> <span class="n">sqrt</span><span class="p">(</span><span class="n">ssd</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">point1</span><span class="p">,</span> <span class="n">point2</span><span class="p">,</span> <span class="n">dimensions</span><span class="p">));</span>
        <span class="p">}</span>

        <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
        <span class="k">class</span> <span class="nc">KMeans</span> <span class="p">{</span>
            <span class="k">static</span> <span class="k">const</span> <span class="kr">inline</span> <span class="n">std</span><span class="o">::</span><span class="n">unordered_map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">double</span> <span class="p">(</span><span class="o">*</span> <span class="p">)(</span><span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span><span class="p">)</span><span class="o">&gt;</span> <span class="n">distance_funcs</span> <span class="o">=</span> <span class="p">{</span>
                <span class="p">{</span> <span class="s">"euclidean"</span><span class="p">,</span> <span class="n">euclidean</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">}</span>
            <span class="p">};</span>

            <span class="nl">public:</span>
                <span class="n">KMeans</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">,</span> <span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">distanceFunc</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">distance_funcs</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">distanceFunc</span><span class="p">)</span> <span class="o">==</span> <span class="n">distance_funcs</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
                        <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">invalid_argument</span><span class="p">(</span><span class="n">distanceFunc</span> <span class="o">+</span> <span class="s">" is not a valid distance metric"</span><span class="p">);</span>
                    <span class="p">}</span>
                    <span class="n">m_distance_func</span> <span class="o">=</span> <span class="n">distanceFunc</span><span class="p">;</span>
                    <span class="n">m_instance</span> <span class="o">=</span> <span class="k">new</span> <span class="n">clustering</span><span class="o">::</span><span class="n">KMeansContiguous</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">max_iterations</span><span class="p">,</span> <span class="n">tolerance</span><span class="p">,</span> <span class="n">dimensions</span><span class="p">,</span> <span class="n">distance_funcs</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="n">distanceFunc</span><span class="p">));</span>
                <span class="p">}</span>

                <span class="n">KResult</span> <span class="n">predict</span><span class="p">(</span><span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsData</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// convert TypedArray to a T* pointer</span>
                    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">jsDataLength</span><span class="o">=</span> <span class="n">jsData</span><span class="p">[</span><span class="s">"length"</span><span class="p">].</span><span class="n">as</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">();</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">jsData</span><span class="p">[</span><span class="s">"buffer"</span><span class="p">];</span> 
                    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">byte_data</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">convertJSArrayToNumberVector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">jsData</span><span class="p">);</span>
                    <span class="n">T</span><span class="o">*</span> <span class="n">data</span> <span class="o">=</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">T</span><span class="o">*&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">byte_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span> 

                    <span class="k">auto</span> <span class="n">results</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">predict</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">jsDataLength</span><span class="p">);</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>

                    <span class="c1">// convert data to Javascript</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span> <span class="o">=</span> <span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getDimensions</span><span class="p">();</span>

                    <span class="n">T</span><span class="o">*</span> <span class="n">centroids</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsCentroids</span> <span class="o">=</span> <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span><span class="o">::</span><span class="n">array</span><span class="p">();</span>
                    <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getK</span><span class="p">();</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">k</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">jsCentroids</span><span class="p">.</span><span class="n">call</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"push"</span><span class="p">,</span> <span class="n">wasm</span><span class="o">::</span><span class="n">utility</span><span class="o">::</span><span class="n">contiguousVecToArray</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">centroids</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">dimensions</span><span class="p">],</span> <span class="n">dimensions</span><span class="p">));</span>
                    <span class="p">}</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">centroids</span><span class="p">);</span>

                    <span class="kt">long</span> <span class="kt">int</span> <span class="o">*</span> <span class="n">clusters</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
                    <span class="n">emscripten</span><span class="o">::</span><span class="n">val</span> <span class="n">jsClusters</span> <span class="o">=</span> <span class="n">wasm</span><span class="o">::</span><span class="n">utility</span><span class="o">::</span><span class="n">contiguousVecToArray</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">clusters</span><span class="p">,</span> <span class="n">jsDataLength</span> <span class="o">/</span> <span class="n">dimensions</span><span class="p">);</span>
                    <span class="n">free</span><span class="p">(</span><span class="n">clusters</span><span class="p">);</span>
                    <span class="k">return</span> <span class="n">KResult</span><span class="p">{</span> <span class="n">jsCentroids</span><span class="p">,</span> <span class="n">jsClusters</span><span class="p">};</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setK</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setK</span><span class="p">(</span><span class="n">k</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">long</span> <span class="kt">int</span> <span class="n">getK</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getK</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setMaxIterations</span><span class="p">(</span><span class="k">const</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">maxIterations</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setMaxIterations</span><span class="p">(</span><span class="n">maxIterations</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">long</span> <span class="kt">int</span> <span class="n">getMaxIterations</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getMaxIterations</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="kt">void</span> <span class="n">setTolerance</span><span class="p">(</span><span class="k">const</span> <span class="kt">double</span> <span class="n">tolerance</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">setTolerance</span><span class="p">(</span><span class="n">tolerance</span><span class="p">);</span>
                <span class="p">}</span>

                <span class="kt">double</span> <span class="n">getTolerance</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_instance</span><span class="o">-&gt;</span><span class="n">getTolerance</span><span class="p">();</span>
                <span class="p">}</span>

                <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">getDistanceFunc</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">m_distance_func</span><span class="p">;</span>
                <span class="p">}</span>

            <span class="nl">private:</span>
                <span class="n">clustering</span><span class="o">::</span><span class="n">KMeansContiguous</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">*</span> <span class="n">m_instance</span><span class="p">;</span>
                <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">m_distance_func</span><span class="p">;</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Our new Emscripten implementation expects data ato be provided one contiguous memory array.  To provide a good developer experience to front-end developers, we will include a Javascript wrapper class in our WebAssembly library to our Emscripten <code class="language-plaintext highlighter-rouge">KMeans</code> class allowing developers to continue passing data in as 2-dimensional data while having the wrapper class handle the transformation of that data into a single typed array and passing it to the Emscripten <code class="language-plaintext highlighter-rouge">KMeans</code> implementation in the expected format.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">class</span> <span class="nx">KMeans</span> <span class="p">{</span>
    <span class="kd">constructor</span><span class="p">(</span><span class="nx">k</span><span class="p">,</span> <span class="nx">iterations</span><span class="p">,</span> <span class="nx">tolerance</span><span class="p">,</span> <span class="nx">distance</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">k</span> <span class="o">=</span> <span class="nx">k</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">iterations</span> <span class="o">=</span> <span class="nx">iterations</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">tolerance</span> <span class="o">=</span> <span class="nx">tolerance</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">distance</span> <span class="o">=</span> <span class="nx">distance</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">async</span> <span class="nx">predict</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">dimensions</span> <span class="o">=</span> <span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">,</span>
            <span class="nx">transformedData</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="kd">constructor</span><span class="p">.</span><span class="nx">flatten</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
            <span class="nx">module</span> <span class="o">=</span> <span class="k">await</span> <span class="k">new</span> <span class="nx">KMEANS</span><span class="p">();</span>
        <span class="kd">var</span> <span class="nx">clf</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">module</span><span class="p">.</span><span class="nx">KMeans</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">k</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">iterations</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">tolerance</span><span class="p">,</span> <span class="nx">dimensions</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">distance</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">clf</span><span class="p">.</span><span class="nx">predict</span><span class="p">(</span><span class="nx">transformedData</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="kd">static</span> <span class="nx">flatten</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">dimensions</span> <span class="o">=</span> <span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span><span class="p">,</span>
            <span class="nx">transformedData</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Float32Array</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span> <span class="o">*</span> <span class="nx">dimensions</span><span class="p">);</span>
        <span class="c1">// flatten data for classification</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">var</span> <span class="nx">dataEntry</span> <span class="o">=</span> <span class="nx">data</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">j</span> <span class="o">&lt;</span> <span class="nx">dimensions</span><span class="p">;</span> <span class="nx">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">transformedData</span><span class="p">[</span><span class="nx">i</span> <span class="o">*</span> <span class="nx">dimensions</span> <span class="o">+</span> <span class="nx">j</span><span class="p">]</span> <span class="o">=</span> <span class="nx">dataEntry</span><span class="p">[</span><span class="nx">j</span><span class="p">];</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nx">transformedData</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>To keep our Emscripten module simple to use, we will want to include this wrapper in the Emscripten Javacript wrapper for our WebAssembly library using the <code class="language-plaintext highlighter-rouge">--extern-post-js &lt;file-path&gt;</code> flag:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="nt">--extern-post-js</span> ./kmeans-contiguous-wrapper.js</code></pre></figure>

<p>This <code class="language-plaintext highlighter-rouge">--extern-post-js</code> flag adds the contents of a specified Javascript file to the end of the Emscripten-generated Javascript file.  The custom Javsacript can be used to add wrapper logic around the generated WASM functions/classes, or add library metadata.</p>

<p>Now let’s run our benchmarks again:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Average (ms)</th>
      <th>Std Dev (ms)</th>
      <th>Factor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>JS</td>
      <td>17,892</td>
      <td>203</td>
      <td>1</td>
    </tr>
    <tr>
      <td>WASM</td>
      <td>6,569</td>
      <td>349</td>
      <td>0.367</td>
    </tr>
  </tbody>
</table>

<p>We now have close to a 2.75x increase in performance compared to our Javascript implementation while keeping the front-end developers experience pretty simple.  But, we can further improve performance by adding SIMD instructions.</p>

<h3 id="simd-vectorization" class="linked-heading">
    SIMD Vectorization<a href="#simd-vectorization">🔗</a>
</h3>

<p>SIMD is a parallel processing technique that allows a single instruction to perform the same operation on multiple data elements simultaneously. It enables the processor to exploit data-level parallelism and execute multiple calculations in a single clock cycle.</p>

<p>In the context of WebAssembly, SIMD instructions are supported through the SIMD128 extension. This extension introduces a set of 128-bit wide SIMD instructions that can operate on 32-bit floating-point numbers, providing a significant boost in performance for suitable workloads.</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><span class="c1">// Without SIMD</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">dataPoint</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">centroid</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="n">distance</span> <span class="o">+=</span> <span class="n">diff</span> <span class="o">*</span> <span class="n">diff</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// With SIMD (using SIMD128 extension)</span>
<span class="cp">#include</span> <span class="cpf">&lt;wasm_simd128.h&gt;</span><span class="cp">
</span>
<span class="n">v128_t</span> <span class="n">simdDistance</span> <span class="o">=</span> <span class="n">wasm_f32x4_splat</span><span class="p">(</span><span class="mf">0.0</span><span class="n">f</span><span class="p">);</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">v128_t</span> <span class="n">simdDataPoint</span> <span class="o">=</span> <span class="n">wasm_v128_load</span><span class="p">(</span><span class="o">&amp;</span><span class="n">dataPoint</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
    <span class="n">v128_t</span> <span class="n">simdCentroid</span> <span class="o">=</span> <span class="n">wasm_v128_load</span><span class="p">(</span><span class="o">&amp;</span><span class="n">centroid</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
    <span class="n">v128_t</span> <span class="n">simdDiff</span> <span class="o">=</span> <span class="n">wasm_f32x4_sub</span><span class="p">(</span><span class="n">simdDataPoint</span><span class="p">,</span> <span class="n">simdCentroid</span><span class="p">);</span>
    <span class="n">simdDistance</span> <span class="o">=</span> <span class="n">wasm_f32x4_add</span><span class="p">(</span><span class="n">simdDistance</span><span class="p">,</span> <span class="n">wasm_f32x4_mul</span><span class="p">(</span><span class="n">simdDiff</span><span class="p">,</span> <span class="n">simdDiff</span><span class="p">));</span>
<span class="p">}</span>
<span class="kt">float</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">wasm_f32x4_extract_lane</span><span class="p">(</span><span class="n">simdDistance</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span>
                 <span class="n">wasm_f32x4_extract_lane</span><span class="p">(</span><span class="n">simdDistance</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span>
                 <span class="n">wasm_f32x4_extract_lane</span><span class="p">(</span><span class="n">simdDistance</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span>
                 <span class="n">wasm_f32x4_extract_lane</span><span class="p">(</span><span class="n">simdDistance</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span></code></pre></figure>

<p>In this example, the SIMD version calculates distances for four elements at a time using SIMD128 instructions. The <code class="language-plaintext highlighter-rouge">wasm_v128_load</code> function loads four floating-point values into a SIMD register, and the subsequent SIMD operations (<code class="language-plaintext highlighter-rouge">wasm_f32x4_sub</code>, <code class="language-plaintext highlighter-rouge">wasm_f32x4_mul</code>, and <code class="language-plaintext highlighter-rouge">wasm_f32x4_add</code>) perform the necessary calculations in parallel. Finally, the individual elements are extracted from the SIMD register and summed up to obtain the final distance.  This example also demonstrates the <a href="https://en.wikipedia.org/wiki/Loop_unrolling">Loop unrolling</a> optimization technique.</p>

<p>When optimizing with SIMD instructions, it’s essential to consider factors such as data layout, memory alignment, and the use of appropriate SIMD operations. By carefully designing the code to take advantage of SIMD parallelism, we can greatly enhance the performance of WebAssembly modules.</p>

<p>To fully leverage SIMD instructions, it’s important to ensure that the data layout is compatible with SIMD operations.  Instead of using vectors of vectors, which can lead to non-contiguous memory access, contiguous memory layout is necessary, such as a flattened array.  Aligning data to 16-byte boundaries can further optimize memory access patterns and enable more efficient SIMD operations.</p>

<p>Here is our optimized distance function definition:</p>

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
</pre></td><td class="code"><pre><span class="cp">#include</span> <span class="cpf">&lt;wasm_simd128.h&gt;</span><span class="cp">
</span>
<span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="kt">double</span> <span class="nf">ssd</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">point1</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">point2</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Sum of Squared Difference (SSD)</span>
    <span class="n">T</span> <span class="n">distance</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>

    <span class="cp">#ifdef __wasm_simd128__
</span>        <span class="n">v128_t</span> <span class="n">sum</span> <span class="o">=</span> <span class="n">wasm_f64x2_splat</span><span class="p">(</span><span class="mf">0.0</span><span class="n">f</span><span class="p">);</span>

        <span class="c1">// Calculate SSD in blocks for better performance</span>
        <span class="k">const</span> <span class="kt">int</span> <span class="n">SIMD_BLOCK_SIZE</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>

        <span class="kt">long</span> <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="n">dimensions</span> <span class="o">-</span> <span class="n">SIMD_BLOCK_SIZE</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="n">SIMD_BLOCK_SIZE</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">v128_t</span> <span class="n">vec1</span> <span class="o">=</span> <span class="n">wasm_f64x2_make</span><span class="p">(</span><span class="n">point1</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">point1</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]);</span>
            <span class="n">v128_t</span> <span class="n">vec2</span> <span class="o">=</span> <span class="n">wasm_f64x2_make</span><span class="p">(</span><span class="n">point2</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">point2</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]);</span>

            <span class="n">v128_t</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">wasm_f64x2_sub</span><span class="p">(</span><span class="n">vec1</span><span class="p">,</span> <span class="n">vec2</span><span class="p">);</span>
            <span class="n">v128_t</span> <span class="n">sqr_diff</span> <span class="o">=</span> <span class="n">wasm_f64x2_mul</span><span class="p">(</span><span class="n">diff</span><span class="p">,</span> <span class="n">diff</span><span class="p">);</span>
            <span class="n">sum</span> <span class="o">=</span> <span class="n">wasm_f64x2_add</span><span class="p">(</span><span class="n">sum</span><span class="p">,</span> <span class="n">sqr_diff</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Handle any remaining elements </span>
        <span class="k">for</span> <span class="p">(;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">printf</span><span class="p">(</span><span class="s">"remaining</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
            <span class="n">T</span> <span class="n">diff</span> <span class="o">=</span> <span class="n">point2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">point1</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
            <span class="n">distance</span> <span class="o">+=</span> <span class="n">diff</span> <span class="o">*</span> <span class="n">diff</span><span class="p">;</span> <span class="c1">// Avoid unnecessary SIMD conversion</span>
        <span class="p">}</span>

        <span class="c1">// Extract the sum from the SIMD vector</span>
        <span class="n">distance</span> <span class="o">+=</span> <span class="n">wasm_f64x2_extract_lane</span><span class="p">(</span><span class="n">sum</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">wasm_f64x2_extract_lane</span><span class="p">(</span><span class="n">sum</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="cp">#else
</span>        <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">dimensions</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">){</span>
            <span class="n">distance</span> <span class="o">+=</span> <span class="n">pow</span><span class="p">(</span><span class="n">point2</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">point1</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="mi">2</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="cp">#endif
</span>    <span class="k">return</span> <span class="n">distance</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="kt">double</span> <span class="n">euclidean</span><span class="p">(</span><span class="n">T</span><span class="o">*</span> <span class="n">point1</span><span class="p">,</span> <span class="n">T</span><span class="o">*</span> <span class="n">point2</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">int</span> <span class="n">dimensions</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Euclidean Distance</span>
    <span class="k">return</span> <span class="n">sqrt</span><span class="p">(</span><span class="n">ssd</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">point1</span><span class="p">,</span> <span class="n">point2</span><span class="p">,</span> <span class="n">dimensions</span><span class="p">));</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>To compile it with SIMD support we will need to add an extra flag <code class="language-plaintext highlighter-rouge">-msimd128</code>:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell">em++ <span class="nt">-std</span><span class="o">=</span>c++17 <span class="se">\</span>
    <span class="nt">-lembind</span> <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">WASM</span><span class="o">=</span>1 <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">MODULARIZE</span><span class="o">=</span>1 <span class="se">\</span>
    <span class="nt">-s</span> <span class="nv">EXPORT_NAME</span><span class="o">=</span><span class="s2">"'KMEANS'"</span> <span class="se">\</span>
    <span class="nt">-msimd128</span> <span class="se">\</span>
    <span class="nt">-sALLOW_MEMORY_GROWTH</span> <span class="se">\</span>
    <span class="nt">-O2</span> <span class="nt">-gsource-map</span> <span class="se">\</span>
    <span class="nt">--profiling</span> <span class="se">\</span>
    <span class="nt">--profiling-funcs</span> <span class="se">\</span>
    <span class="nt">--tracing</span> <span class="se">\</span>
    <span class="nt">-sNO_DISABLE_EXCEPTION_CATCHING</span> <span class="se">\</span>
    <span class="nt">-sASSERTIONS</span> <span class="se">\</span>
    <span class="nt">-o</span> ./bin/kmeans.js <span class="se">\</span>
    ./bindings.cpp</code></pre></figure>

<p>Now let’s run our benchmarks again:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Average (ms)</th>
      <th>Std Dev (ms)</th>
      <th>Factor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>JS</td>
      <td>17,892</td>
      <td>203</td>
      <td>1</td>
    </tr>
    <tr>
      <td>WASM</td>
      <td>6,812</td>
      <td>680</td>
      <td>0.380</td>
    </tr>
  </tbody>
</table>

<p>Based on these, adding SIMD to our implementation slowed down execution a bit, likely because we only have 4 dimensions in our data.  That said, our implementation could be further optimized:</p>

<ul>
  <li>Change the data type from <code class="language-plaintext highlighter-rouge">double</code> to <code class="language-plaintext highlighter-rouge">float</code> to reduce the amount of memory usage, and increase memory bandwidth and computation speed.</li>
  <li>Add SIMD usage in the <code class="language-plaintext highlighter-rouge">update_centroids</code>, <code class="language-plaintext highlighter-rouge">update_clusters</code>, <code class="language-plaintext highlighter-rouge">initialize_kpp_centroids</code>, and <code class="language-plaintext highlighter-rouge">initialize_random_centroids</code> methods to process multiple elements at once.</li>
</ul>

<h2 id="benchmarking-and-performance-analysis" class="linked-heading">
    Benchmarking and Performance Analysis<a href="#benchmarking-and-performance-analysis">🔗</a>
</h2>

<p>Our benchmarking criteria required the K-means implementations to converge upon the exact weights to test the full performance of the implementations.  In business application we would not likely require such a precise convergence of centroids, and would loosen the tolerance to get results faster.</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Average (ms)</th>
      <th>Std Dev (ms)</th>
      <th>Factor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>JS</td>
      <td>17,892</td>
      <td>203</td>
      <td>1</td>
    </tr>
    <tr>
      <td>WASM (naive)</td>
      <td>61,054</td>
      <td>482</td>
      <td>3.412</td>
    </tr>
    <tr>
      <td>WASM (contiguous)</td>
      <td>6,569</td>
      <td>349</td>
      <td>0.367</td>
    </tr>
    <tr>
      <td>WASM (contiguous + SIMD)</td>
      <td>6,812</td>
      <td>680</td>
      <td>0.380</td>
    </tr>
  </tbody>
</table>

<p>Based on these benchmarks, the key to optimizing performance of WebAssembly via EmScripten is</p>

<ol>
  <li>Contiguous memory layout</li>
  <li>Direct memory access</li>
  <li>Pass values by reference when possible</li>
  <li>SIMD vectorization</li>
</ol>

<p>By optimizing the K-Means clustering algorithm with WebAssembly, we have demonstrated its potential for enabling real-time interactivity. Our C++ implementation processed over 670,000 data points nearly 50 times in under 7 seconds, a nearly 2.75x speed improvement compared to Javacript methods, all within the browser. This optimization opens up possibilities for advanced features and functionalities in web applications, such as sophisticated data analysis techniques, real-time data streaming, and interactive visualizations. The resulting applications can provide users with richer insights and enhanced decision-making capabilities. Moreover, these optimizations improve scalability and cost-efficiency by allowing client-side resources to handle a higher volume of computational tasks. This enables organizations to serve more users and process more data with existing resources, leading to cost savings and a better return on investment.</p>

<h2 id="conclusion" class="linked-heading">
    Conclusion<a href="#conclusion">🔗</a>
</h2>

<p>WebAssembly modules can seamlessly interact with JavaScript code, allowing for easy integration into existing web applications.  Emscripten provides bindings and APIs to facilitate communication between WebAssembly and JavaScript, enabling data exchange and function calls.  This interoperability allows us to leverage the strengths of both languages—the performance of WebAssembly for clustering algorithms and the flexibility of JavaScript for user interactions and UI.</p>

<p>Offloading computationally intensive tasks like clustering algorithms to the client-side using WebAssembly can reduce server load and costs.  Scaling becomes more manageable as the computational burden is distributed among users’ devices rather than concentrated on the server.  This scalability advantage is particularly significant for applications with a large user base or those dealing with massive datasets.</p>

<p>Whether you’re building interactive data visualizations, real-time analytics tools, or complex simulations, exploring WebAssembly can unlock new possibilities for your web projects. With the growing support and ecosystem around WebAssembly, now is the perfect time to dive in and start harnessing its potential. So, if you’re looking to push the boundaries of web performance and deliver exceptional user experiences, consider incorporating WebAssembly into your development toolkit. The future of high-performance web applications is here, and WebAssembly is leading the charge.</p>

<h2 id="references" class="linked-heading">
    References<a href="#references">🔗</a>
</h2>

<ul>
  <li><a href="https://webassembly.org/">WebAssembly.org</a></li>
  <li><a href="https://emscripten.org/docs/">Emscripten</a></li>
  <li><a href="https://developer.mozilla.org/en-US/docs/WebAssembly">MDN</a></li>
  <li><a href="https://www.w3.org/TR/wasm-core-2/">W3C Spec</a></li>
</ul>]]></content><author><name>Doug Fenstermacher</name></author><category term="experiment" /><category term="C++" /><category term="math" /><category term="javascript" /><category term="data" /><summary type="html"><![CDATA[Exploring complexities of optimizing web performance by implementing K-Means clustering algorithms using WebAssembly and Emscripten.]]></summary></entry><entry><title type="html">2 Trustworthy Alternatives to Improving Performance Lists in Track &amp;amp; Field</title><link href="https://dougfenstermacher.com/experiment/trustworthy-alternatives-improving-performance-lists-track-field" rel="alternate" type="text/html" title="2 Trustworthy Alternatives to Improving Performance Lists in Track &amp;amp; Field" /><published>2021-07-12T00:00:00+00:00</published><updated>2021-07-12T00:00:00+00:00</updated><id>https://dougfenstermacher.com/experiment/trustworthy-alternatives-improving-performance-lists-track-field</id><content type="html" xml:base="https://dougfenstermacher.com/experiment/trustworthy-alternatives-improving-performance-lists-track-field"><![CDATA[<h2 id="compounding-white-lies" class="linked-heading">
    Compounding White Lies<a href="#compounding-white-lies">🔗</a>
</h2>

<p>While I was watching an IAAF track &amp; field meet, I began thinking about when I used to compete in track &amp; field and the performance lists that we used.  In prestigous sanctioned meets like NCAA and IAAF meets, meet directors ensure that the competitions are fair by validating the submitted athletes and their seed performances.  These performances are then used to partition the submitted athletes into optimal heats/flights.  This makes for good competition for athletes and good show for spectators.  But only a very small, elite group of athletes will ever compete in such an environment where fair opportunity is so aggressively pursued and strictly ensured.  For the vast majority of track &amp; field athletes the system of seeding and heat placement is easily manipulated by coaches to the advantage of their athletes and to the detriment of the event participants.</p>

<p>The high school and college meets I have competed in required coaches to register athletes for events with the meet host/director including a corresponding performance.   As in professional meets, these hosts/directors would then use these submitted performances to decide how to split the athletes into heats and flights.  Unlike the NCAA or IAAF, most track &amp; field programs do not the budget, staffing, or even the information to check whether the submitted performances for their meet is accurate or even realistic for each athlete.  Most coaches know these performances are not validated and knowingly inflate the submitted performances of their athletes beyond what they have performed or are capable of performing,  to ensure their own athletes are grouped into heats/flights with superior athletes.  While their athletes are less likely to win their heat or even place highly, the likelihood of the athlete setting a new personal record (PR) increases because the athlete will want to stay with the other athletes to avoid the embarrassment of coming in last place or prove they deserve to be there.</p>

<p>While we could make the argument that if all the coaches are inflating their seeds, then the playing field has been effectively leveled, this is not the case.</p>

<ul>
  <li>Seed inflation puts honest coaches and athletes at a disadvantage, and also reduces the quality of competition for spectators, which are equally important to the sport.  These spectators expect to come and watch competitive races with athletes on equal footing which cannot happen when flights/heats are full of athletes who do not deserve to be in it.  This practice results in races with faster runners in slower heats where they will have less competition merely because their coach submitted an accurate seed time, and slower runners placed in faster heats where they may reduce the competitiveness and entertainment value of the heat.</li>
  <li>Seed inflation provides incentives for seed performances to be further inflated, resulting in  poorly regulated feedback loop.  For coaches to give their athletes an opportunity to be in the heat their athletes deserve, they must also inflate their submitted seeds, or inflate them beyond those of the other coaches.  Those coaches then inflate their seeds further to give their athletes better opportunities.  The only regulator of this feedback look is the meet host/director’s willingness to recognize/allow the submission of the seeds, which can vary widely based on the host/director, the coach, and the athlete in question.  This perpetual inflation of seeds makes performance lists reflect the seeds of competitors, which results in heat/flight assignments being made using inaccurate information, and ends with a perpetual decrease in the competitiveness of heats/flights due to the wrong heat/flight assignments being made.</li>
</ul>

<p>Both of these scenarios result in sub-optimal races which directly diminish both the opportunities for athletes and the entertainment value for spectators.</p>

<p>Now that we have effectively covered what the problem is, what causes the problem, why the problem matters, and why the problem will persist/grow we can move onto covering how this problem might be solved:</p>

<ol>
*  Automatic validation of meet submissions and flagging suspicious performance submissions
*  Formalized coach/meet accountability
*  Largely removing the concept of performance submissions in meet registration
</ol>

<h2 id="a-step-towards-accountability" class="linked-heading">
    A step towards accountability<a href="#a-step-towards-accountability">🔗</a>
</h2>

<p>Given the small budget that many track &amp; field programs have, the number of track &amp; field meets each year, and the sheer number of athletes, a centralized service for validating athlete performances would be most effective, unbiased, and transparent.  A validation service would need to have a record of all athletes and their previous performances to validate submitted seed times for any existing entities.  Existing services like <a href="https://milesplit.com">Milesplit</a>, <a href="https://www.tfrrs.org/">TFRRS</a>, or <a href="https://www.iaaf.org">IAAF</a> who already have stored decades of performances are the most equipped to provide a scale-able validation services and, most importantly, have some degree of trust within the track &amp; field community.  The validation services could be integrated into existing meet management software so that meet directors do not have to change their workflows.</p>

<p>These race result organizations could write API endpoints to their race results, which could sold be a subscription-based product to meet management software companies to use validate performances when seeds are submitted.  The service would provide an passive income stream to race result organizations from the meet management companies, while improving the quality of meet management software, providing a service to the track &amp; field community.  The resulting data produced from track &amp; field meets could then be loaded into the race result services and improve the quality of their validation services, resulting in a feedback loop, further reinforcing the quality, validity, and trust-worthiness of the service for athletic community.  This would produce a feedback loop with the validation service organizations, meet management software companies, and the track &amp; field community.</p>

<h2 id="accountability-through-formalized-trust" class="linked-heading">
    Accountability through formalized trust<a href="#accountability-through-formalized-trust">🔗</a>
</h2>

<p>In our daily lives we get many opportunities to establish trust and distrust between other people through repeated interactions.  If they live up their word and are honest then our trust in them increases, and if they do not and/or are dishonest, trust decreases. 
 These same interactions occur between meet directors and coaches.  If a coach continuously sends accurate seed performances, trust in the meet director increases. If they continuously inflate their performances, trust decreases.</p>

<p>But what about when a meet director receives seed performances from a new team or a new coach?  The meet director has never worked with them, and has no relationship on which to evaluate trust.  The meet director may not be able to interact with the coach/team enough to determine the degree of trust which they should have for them.  Well, what do you do when you hear from someone whom you don’t know and need to know how much to trust them?  You find out if the people you know trust them.  Meet directors can do the same thing, using their existing network of relationships.  They can talk to coaches they trust and find out about if they trust this coach.  And if those coaches do not know if the coach in question is trustworthy they can reach out to the coaches they have worked with, and so on.  This allows coaches to get some idea of the trustworthiness of new coaches/teams without having to take the time to establish a relationship/reputation directly.  This concept of trusting people based on the trust someone else has for them is known as transitive trust.</p>

<p>In 2003, Stanford researchers published <a href="https://nlp.stanford.edu/pubs/eigentrust.pdf">The EigenTrust Algorithm for Reputation Management in P2P Networks</a>, which serves to manage reputation of large numbers of interconnected people in large networks.  The algorithm was built for usage in a peer-to-peer network, meaning that trust is based on the trustworthiness/quality of the assets exchanged between two peers in the network.  In our case, these assets, would be seed times.  The EigenTrust algorithm implements this idea of transitive trust by by having each member of the network provide a list of members of the network they trust.  These members are given high trust scores from their peer which listed them as trustworthy, forming the local trustworthy community around each peer in the network.  The algorithm then calculates a value of trust between all members of the network based on those pre-trusted members and the concept of trust transitivity.</p>

<p>A trust network would be made up of coaches and meet directors. Meets are almost always organized by other coaches, meaning that coaches attend meets held/hosted by other coaches.  This makes the host/meet director a consumer of seed performances from the attending coaches, and the coaches registering for the meet producers of seed performances.  This makes the track &amp; field coaching community both the consumers and producers of seed performances, effectively creating a system which, as each coach submits more and more seed performances to other coaches, will converge upon accurate trust scores for all coaches.  As in the case of autovalidation, this results in a feedback loop by rewarding honest behaviors and punishing dishonest behavior.</p>

<p>But, EigenTrust has a couple of key issues which stem from the high trust that is initially given to pre-trusted peers:</p>

<ul>
  <li>Trustworthiness can change over time, so a pre-trusted peer is not prerequisite for trustworthiness into the indefinite future.  Trust is dynamic and can change over time, so a trustworthy peer at one point may not be trustworthy later.  EigenTrust does not account for such changes over time, potentially jeopardizing the reliability of the network</li>
  <li>Some peers in the network may be trustworthy and provide honest performance seeds but still be given low trust.  Members of the network may overlook honest contributes of reliable members.  While they may be able to recover in the form of other peers identifying them as trustworthy, the lack of recorded trust by the forgetful peer may prevent them from achieving a high overall level of trust in the network</li>
  <li>A dishonest performance seed from a highly trustworthy peer may result in a collapse of trust in the network.  In such a case, the member’s trustworthiness could collapse and result in a significant drop in trust of their honest peers and potentially across the entire network due to the effects of trust transitivity.</li>
</ul>

<p>These flaws resulted in the development of a many other systems for automated trust management, such as <a href="https://www.sciencedirect.com/science/article/pii/S1319157815000440">HonestPeer</a>  which manages for changing trust and the corruption of previously trusted entites, or the <a href="https://pdfs.semanticscholar.org/4a6f/6dc3ddda1b301a776d74883ba9bfc686ef2b.pdf">Trust Network Analysis with Subjective Logic (TNASL)</a> approach for managing multiple aspects of trustworthiness between entities.</p>

<p>It should also be noted that the EigenTrust, HonestPeer, and TNASL systems can be applied to other aspects of athletics by altering who the members of the network are, and what the assets being exchanged are.  For example, in a competition context the members of a network could be other athletes, and the assets they provide to other athletes is their performance in a shared competition at a athletics meet.  An athlete’s performance can be judged by the other participants in their shared competition based on the legitimacy of their performance, such as fairness, lack of doping, etc . which will be reflected in other athlete’s trust of them.  But as with performance seeds, we must consider that behavior can change over time, in that athletes currently distrusted may exhibit trustworthy behavior at a later point and that currently trustworthy peers may be come untrustworthy.  In this example, HonestPeer would account for changes in behavior in the form of updated trustworthiness scores, which would be important in a cheating/doping context. We can create similar applications for other aspects as well, such as sportsmanship, etc.</p>

<h2 id="automated-performance-lists" class="linked-heading">
    Automated performance lists<a href="#automated-performance-lists">🔗</a>
</h2>

<p>This solution is the most radical solution, but I believe the technology to implement it exists and has a relatively low barrier to entry for organizations with a large database of race results.  The performance list has been necessary since the inception of track &amp; field races.  However, the technology exists now to determine what an athlete is capable of based on their past performances. 
 Typically athletes do not make unpredictable significant leaps in their performances, but make small incremental improvements.  In fact, as athletes become better their performances tend to improve by smaller and smaller increments.  This makes estimating (with reasonable accuracy) what the expected performance of an athlete should be possible.</p>

<p>Let’s say that we are a meet director for a large college meet and we received entries for 87 male competitors in the pole vault.  We had access to this season’s performances for these athletes we could average their performances to get their average performance this season.  Using the averages for each athlete we would have an unbiased view of their capabilities to use to create the most competitive flights.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
</pre></td><td class="code"><pre><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">time</span>
    <span class="kn">import</span> <span class="nn">csv</span>
    <span class="kn">import</span> <span class="nn">datetime</span>
    <span class="n">file_name</span> <span class="o">=</span> <span class="s">'performance.csv'</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file_name</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
        <span class="n">data</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">))</span>
    <span class="n">athlete_lookup</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
        <span class="n">entity_id</span> <span class="o">=</span> <span class="n">performance</span><span class="p">[</span><span class="s">'entity_id'</span><span class="p">]</span>

        <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">entity_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">entity_id</span><span class="o">=</span><span class="n">entity_id</span><span class="p">,</span> <span class="n">performances</span><span class="o">=</span><span class="p">[])</span>
        <span class="k">if</span> <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">][</span><span class="s">'performances'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">performance</span><span class="p">)</span>

    <span class="n">athletes</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">athlete</span><span class="p">,</span> <span class="n">performance_data</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">dated_performances</span> <span class="o">=</span> <span class="p">[(</span><span class="n">performance</span><span class="p">[</span><span class="s">'date'</span><span class="p">],</span> <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">])</span> <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">performance_data</span><span class="p">[</span><span class="s">'performances'</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
        <span class="n">raw_performances</span> <span class="o">=</span> <span class="p">[</span><span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]</span> <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">performance_data</span><span class="p">[</span><span class="s">'performances'</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
        <span class="n">performance_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">raw_performances</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">performance_count</span><span class="p">:</span>
            <span class="n">average</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">raw_performances</span><span class="p">)</span> <span class="o">/</span> <span class="n">performance_count</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">average</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">performance_data</span><span class="p">[</span><span class="s">'average'</span><span class="p">]</span> <span class="o">=</span> <span class="n">average</span>
        <span class="n">athletes</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">performance_data</span><span class="p">)</span>
    <span class="n">heat_size</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="n">athlete_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">athletes</span><span class="p">)</span>
    <span class="n">heat_count</span> <span class="o">=</span> <span class="n">athlete_count</span> <span class="o">/</span> <span class="n">heat_size</span>
    <span class="k">if</span> <span class="n">athlete_count</span> <span class="o">%</span> <span class="n">heat_size</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">heat_count</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">heats</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">athletes</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">athletes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">athlete</span><span class="p">:</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'average'</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">heat_count</span><span class="p">):</span>
        <span class="k">print</span> <span class="n">i</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">,</span> <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span>
        <span class="n">heat</span> <span class="o">=</span> <span class="n">athletes</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">:(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span><span class="p">]</span>
        <span class="n">heats</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">heat</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">heat_index</span><span class="p">,</span> <span class="n">heat</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heats</span><span class="p">):</span>
        <span class="k">print</span> <span class="s">'</span><span class="se">\n</span><span class="s">Heat #%s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">heat_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">athlete_index</span><span class="p">,</span> <span class="n">athlete</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heat</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
            <span class="k">print</span> <span class="s">'Seed #%s: %s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">athlete_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'average'</span><span class="p">])</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This solution works.  But athletes usually improve throughout the season, or they get injured and their performances get worse.  Either way, the more recent performances of an athlete are more indicative of their future performance, so we need a measure which gives higher weight to more recent performances.  In this case we will use an <a href="https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">exponential moving average (EMA)</a></p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
</pre></td><td class="code"><pre><span class="k">def</span> <span class="nf">exponential_moving_average</span><span class="p">(</span><span class="n">iterable</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="n">iterable_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">iterable</span><span class="p">)</span>
    <span class="n">alpha</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'alpha'</span><span class="p">,</span> <span class="mf">2.0</span> <span class="o">/</span> <span class="p">(</span><span class="n">iterable_size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
    <span class="n">numerator</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">denominator</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">base_factor</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">alpha</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">iterable_size</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">i</span><span class="p">:</span>
            <span class="n">numerator</span> <span class="o">=</span> <span class="n">iterable</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
            <span class="n">denominator</span> <span class="o">=</span> <span class="mf">1.0</span>
            <span class="k">continue</span>
        <span class="n">factor</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nb">pow</span><span class="p">(</span><span class="n">base_factor</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
        <span class="n">numerator</span> <span class="o">+=</span> <span class="n">factor</span> <span class="o">*</span> <span class="n">iterable</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="n">denominator</span> <span class="o">+=</span> <span class="n">factor</span>
    <span class="k">return</span> <span class="n">numerator</span> <span class="o">/</span> <span class="n">denominator</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">time</span>
    <span class="kn">import</span> <span class="nn">csv</span>
    <span class="kn">import</span> <span class="nn">datetime</span>
    <span class="n">file_name</span> <span class="o">=</span> <span class="s">'performance.csv'</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file_name</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
        <span class="n">data</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">))</span>
    <span class="n">athlete_lookup</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
        <span class="n">entity_id</span> <span class="o">=</span> <span class="n">performance</span><span class="p">[</span><span class="s">'entity_id'</span><span class="p">]</span>
        <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">entity_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">entity_id</span><span class="o">=</span><span class="n">entity_id</span><span class="p">,</span> <span class="n">performances</span><span class="o">=</span><span class="p">[])</span>
        <span class="k">if</span> <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">][</span><span class="s">'performances'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">performance</span><span class="p">)</span>

    <span class="n">athletes</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">athlete</span><span class="p">,</span> <span class="n">performance_data</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">raw_performances</span> <span class="o">=</span> <span class="p">[</span><span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]</span> <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">performance_data</span><span class="p">[</span><span class="s">'performances'</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
        <span class="n">performance_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">raw_performances</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">performance_count</span><span class="p">:</span>
            <span class="n">ema</span> <span class="o">=</span> <span class="n">exponential_moving_average</span><span class="p">(</span><span class="n">raw_performances</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">ema</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">performance_data</span><span class="p">[</span><span class="s">'ema'</span><span class="p">]</span> <span class="o">=</span> <span class="n">average</span>
        <span class="n">athletes</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">performance_data</span><span class="p">)</span>
    <span class="n">heat_size</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="n">athlete_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">athletes</span><span class="p">)</span>
    <span class="n">heat_count</span> <span class="o">=</span> <span class="n">athlete_count</span> <span class="o">/</span> <span class="n">heat_size</span>
    <span class="k">if</span> <span class="n">athlete_count</span> <span class="o">%</span> <span class="n">heat_size</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">heat_count</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">heats</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">athletes</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">athletes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">athlete</span><span class="p">:</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'ema'</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">heat_count</span><span class="p">):</span>
        <span class="k">print</span> <span class="n">i</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">,</span> <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span>
        <span class="n">heat</span> <span class="o">=</span> <span class="n">athletes</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">:(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span><span class="p">]</span>
        <span class="n">heats</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">heat</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">heat_index</span><span class="p">,</span> <span class="n">heat</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heats</span><span class="p">):</span>
        <span class="k">print</span> <span class="s">'</span><span class="se">\n</span><span class="s">Heat #%s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">heat_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">athlete_index</span><span class="p">,</span> <span class="n">athlete</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heat</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
            <span class="k">print</span> <span class="s">'Seed #%s: %s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">athlete_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'ema'</span><span class="p">])</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>This is much better.  This approach gives the most recent performance of an athlete twice weight as their second most recent performance, and 4 times as much weight as their third most recent performance and so on.  The weight that is given to each performance is configurable using the <code>alpha</code> keyword argument in <code>exponential_moving_average</code>.  But we have one more issue:  What if they only started pole vaulting very recently?
 What if their second most recent performance was months ago at the beginning of the season?  The exponential moving average would still give it a fairly high weight.  An even better measure of the athlete’s ability would be an exponential moving average that would take into account the time differences between performances</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">time_series_exponential_moving_average</span><span class="p">(</span><span class="n">iterable</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="n">iterable_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">iterable</span><span class="p">)</span>
    <span class="n">alpha</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'alpha'</span><span class="p">,</span> <span class="mf">2.0</span> <span class="o">/</span> <span class="p">(</span><span class="n">iterable_size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
    <span class="n">numerator</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">denominator</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">base_factor</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">alpha</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">iterable_size</span><span class="p">):</span>
        <span class="n">date</span><span class="p">,</span> <span class="n">value</span> <span class="o">=</span> <span class="n">iterable</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">i</span><span class="p">:</span>
            <span class="n">numerator</span> <span class="o">=</span> <span class="n">value</span>
            <span class="n">denominator</span> <span class="o">=</span> <span class="mf">1.0</span>
            <span class="n">previous_date</span> <span class="o">=</span> <span class="n">date</span>
            <span class="n">time_difference</span> <span class="o">=</span> <span class="mf">0.0</span>
            <span class="k">continue</span>
        <span class="n">time_difference</span> <span class="o">=</span> <span class="n">time_difference</span> <span class="o">+</span> <span class="p">(</span><span class="n">previous_date</span> <span class="o">-</span> <span class="n">date</span><span class="p">).</span><span class="n">days</span>  <span class="o">+</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'normalization'</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">time_difference</span><span class="p">:</span>
            <span class="n">time_difference</span> <span class="o">=</span> <span class="mf">1.0</span>
        <span class="n">factor</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nb">pow</span><span class="p">(</span><span class="n">base_factor</span><span class="p">,</span> <span class="n">time_difference</span><span class="p">)</span>
        <span class="n">numerator</span> <span class="o">+=</span> <span class="n">factor</span> <span class="o">*</span> <span class="n">value</span>
        <span class="n">denominator</span> <span class="o">+=</span> <span class="n">factor</span>
        <span class="n">previous_date</span> <span class="o">=</span> <span class="n">date</span>
    <span class="k">return</span> <span class="n">numerator</span> <span class="o">/</span> <span class="n">denominator</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">csv</span>
    <span class="kn">import</span> <span class="nn">datetime</span>
    <span class="n">file_name</span> <span class="o">=</span> <span class="s">'performance.csv'</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file_name</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">input_file</span><span class="p">:</span>
        <span class="n">data</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">input_file</span><span class="p">))</span>
    <span class="n">athlete_lookup</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
        <span class="n">entity_id</span> <span class="o">=</span> <span class="n">performance</span><span class="p">[</span><span class="s">'entity_id'</span><span class="p">]</span>
        <span class="n">performance</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">performance</span><span class="p">[</span><span class="s">'date'</span><span class="p">],</span> <span class="s">'%Y-%m-%d'</span><span class="p">)</span>

        <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">entity_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">entity_id</span><span class="o">=</span><span class="n">entity_id</span><span class="p">,</span> <span class="n">performances</span><span class="o">=</span><span class="p">[])</span>
        <span class="k">if</span> <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">]:</span>
            <span class="n">athlete_lookup</span><span class="p">[</span><span class="n">entity_id</span><span class="p">][</span><span class="s">'performances'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">performance</span><span class="p">)</span>

    <span class="n">athletes</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">athlete</span><span class="p">,</span> <span class="n">performance_data</span> <span class="ow">in</span> <span class="n">athlete_lookup</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">dated_performances</span> <span class="o">=</span> <span class="p">[(</span><span class="n">performance</span><span class="p">[</span><span class="s">'date'</span><span class="p">],</span> <span class="n">performance</span><span class="p">[</span><span class="s">'value'</span><span class="p">])</span> <span class="k">for</span> <span class="n">performance</span> <span class="ow">in</span> <span class="n">performance_data</span><span class="p">[</span><span class="s">'performances'</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
        <span class="n">performance_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">dated_performances</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">performance_count</span><span class="p">:</span>
            <span class="n">ts_ema</span> <span class="o">=</span> <span class="n">time_series_exponential_moving_average</span><span class="p">(</span><span class="n">dated_performances</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">tsema</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">performance_data</span><span class="p">[</span><span class="s">'tsema'</span><span class="p">]</span> <span class="o">=</span> <span class="n">ts_ema</span>
        <span class="n">athletes</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">performance_data</span><span class="p">)</span>
    <span class="n">heat_size</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="n">athlete_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">athletes</span><span class="p">)</span>
    <span class="n">heat_count</span> <span class="o">=</span> <span class="n">athlete_count</span> <span class="o">/</span> <span class="n">heat_size</span>
    <span class="k">if</span> <span class="n">athlete_count</span> <span class="o">%</span> <span class="n">heat_size</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">heat_count</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">heats</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">athletes</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">athletes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">athlete</span><span class="p">:</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'ts_ema'</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">heat_count</span><span class="p">):</span>
        <span class="k">print</span> <span class="n">i</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">,</span> <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span>
        <span class="n">heat</span> <span class="o">=</span> <span class="n">athletes</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">heat_size</span><span class="p">:(</span><span class="n">i</span><span class="o">*</span><span class="n">heat_size</span><span class="p">)</span> <span class="o">+</span> <span class="n">heat_size</span><span class="p">]</span>
        <span class="n">heats</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">heat</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">heat_index</span><span class="p">,</span> <span class="n">heat</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heats</span><span class="p">):</span>
        <span class="k">print</span> <span class="s">'</span><span class="se">\n</span><span class="s">Heat #%s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">heat_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">athlete_index</span><span class="p">,</span> <span class="n">athlete</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">heat</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
            <span class="k">print</span> <span class="s">'Seed #%s: %s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">athlete_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">athlete</span><span class="p">[</span><span class="s">'ts_ema'</span><span class="p">])</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Now we have a more optimal solution to our problem of generating an accurate measure of athlete capability at a given time.  All of these implementation of averages performance run in linear time which should be scale-able to hundreds of thousands of athletes.  We could create a more detailed measure using exponential moving standard deviation, but I believe we have adequately shown that we can use moving averages to get a fairly accurate measure for seeding heats/flights.  This concept can be expanded upon by taking into account the specific attributes of a given competitions, such as topography, terrain, and the weather forecast for the given competition.</p>

<p>This solution would eliminate the practice of seed submission entirely, and could potentially result in an unbiased, transparent, and audit-able method for creating performance lists and heat/flight assignments.  But, it does come with a different set of potential issues.</p>

<ul>
  <li>If such a measure were used, it would still not address how to seed athletes that are new to the sport.  Meet directors would still need to rely on seed times from coaches to determine how these newcomers should be seeded in their heats/flights.</li>
  <li>Implementation would require skilled mathematicians/statisticians, and software developers to implement the automated feedback system for determining how to change the prediction calculations based on the results of each competition.  As the system is in-place over time, the predictions would likely improve, and could yield information regarding athletes, competitions, and courses.</li>
  <li>This system would cost money to calculate/predict the seed times for each competition.</li>
</ul>

<h2 id="holding-a-new-system-accountable" class="linked-heading">
    Holding a new system accountable<a href="#holding-a-new-system-accountable">🔗</a>
</h2>

<p>While the methods we discussed hold promise, we still need the capability of quantitatively evaluating the precision/accuracy of our performance lists.  Since the performance list is intended to estimate the performances of the respective athletes in the competition, we can use the actual performances from the competitions to evaluate the accuracy/precision of the generated performance lists.</p>

<p>If the automated trust management approach is used, the trust of the submitter of a performance can be updated according to the accuracy of the submitted performance.  For example, If Jane Doe were to submit the seed time for Jill Doe in the 1600m at 4:02.00, but she only ran 4:24 (my PR), then the trust for Jane Doe would decrease according to the magnitude of the inaccuracy of the seed.</p>

<p>If using the auto-generated approach, the model/equation used to estimate the performance for that individual would be updated.  In the case of distance running, specifically the Riegel model, the exponent coefficient could be adjusted based on the magnitude of the inaccuracy of the current coefficient used for Jill.</p>

<h2 id="conclusion" class="linked-heading">
    Conclusion<a href="#conclusion">🔗</a>
</h2>
<p>This was a thought experiment that I came up with while watching the IAAF meet on the internet. 
 I understand that inflating seed performances is far from the biggest problem in track &amp; field, as I have been following the news on WADA and the Russian doping stories.  Based on my reading during the meet and that evening, I think that the above approaches have applications in other sports, such as swimming and diving, as well as applications to other community-based problems (ex. doping, ) in sports.</p>]]></content><author><name>Doug Fenstermacher</name></author><category term="experiment" /><category term="data" /><category term="running" /><category term="forecasting" /><category term="math" /><summary type="html"><![CDATA[Improving meet performance lists in track & field using the previous performances of the competitors]]></summary></entry><entry><title type="html">FitnessJS</title><link href="https://dougfenstermacher.com/project/fitnessjs" rel="alternate" type="text/html" title="FitnessJS" /><published>2021-06-21T00:00:00+00:00</published><updated>2021-06-21T00:00:00+00:00</updated><id>https://dougfenstermacher.com/project/fitnessjs</id><content type="html" xml:base="https://dougfenstermacher.com/project/fitnessjs"><![CDATA[<h2 id="origin" class="linked-heading">
    Origin<a href="#origin">🔗</a>
</h2>

<p>FitnessJS started as a simple command-line utility to estimate running performances in college based on previous performances.  I wanted to know what my competitors could run for a 5k based on their latest personal record in the 8k.  Originally, it was a simple C program that simply distance running predictions using only 2 models.   In fact, this was the original code:</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
</pre></td><td class="code"><pre><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="c1">;</span><span class="cp">
#include</span> <span class="cpf">&lt;math.h&gt;</span><span class="c1">;</span><span class="cp">
</span>
<span class="cp">#define MILEMETERS 1609.34
</span>
<span class="cm">/*
	Riegel Running Model
	t1 = time
	d1 = old distance
	d2 = new distance
	d1 &amp; d2 must be in the same unit
	return time in same unit as t1
*/</span>
<span class="kt">double</span> <span class="nf">riegel</span><span class="p">(</span><span class="kt">double</span> <span class="n">t1</span><span class="p">,</span> <span class="kt">double</span> <span class="n">d1</span><span class="p">,</span> <span class="kt">double</span> <span class="n">d2</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="n">t1</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">d1</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">d2</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">t1</span> <span class="o">*</span> <span class="n">pow</span><span class="p">(</span> <span class="p">(</span><span class="n">d2</span><span class="o">/</span><span class="n">d1</span><span class="p">),</span> <span class="mi">1</span><span class="p">.</span><span class="mo">06</span> <span class="p">);</span>
<span class="p">}</span>

<span class="cm">/*
	Cameron Running Model
	t1 = time in seconds
	d1 = distance in miles
	d2 = distance in miles
*/</span>
<span class="kt">double</span> <span class="nf">cameron</span><span class="p">(</span><span class="kt">double</span> <span class="n">t1</span><span class="p">,</span> <span class="kt">double</span> <span class="n">d1</span><span class="p">,</span> <span class="kt">double</span> <span class="n">d2</span><span class="p">)</span> <span class="p">{</span>
	<span class="kt">double</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">13</span><span class="p">.</span><span class="mi">49681</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="mi">8865</span><span class="o">*</span><span class="n">d1</span> <span class="o">+</span> <span class="mi">2</span><span class="p">.</span><span class="mi">438936</span><span class="o">/</span><span class="n">pow</span><span class="p">(</span><span class="n">d1</span><span class="p">,</span><span class="mi">0</span><span class="p">.</span><span class="mi">7905</span><span class="p">);</span>
	<span class="kt">double</span> <span class="n">b</span> <span class="o">=</span> <span class="mi">13</span><span class="p">.</span><span class="mi">49681</span> <span class="o">-</span> <span class="mi">0</span><span class="p">.</span><span class="mo">04</span><span class="mi">8865</span><span class="o">*</span><span class="n">d2</span> <span class="o">+</span> <span class="mi">2</span><span class="p">.</span><span class="mi">438936</span><span class="o">/</span><span class="n">pow</span><span class="p">(</span><span class="n">d2</span><span class="p">,</span><span class="mi">0</span><span class="p">.</span><span class="mi">7905</span><span class="p">);</span>
	<span class="k">return</span> <span class="p">(</span><span class="n">t1</span><span class="o">/</span><span class="n">d1</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">a</span><span class="o">/</span><span class="n">b</span><span class="p">)</span> <span class="o">*</span> <span class="n">d2</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
	<span class="kt">double</span> <span class="n">t1</span><span class="p">,</span> <span class="n">d1</span><span class="p">,</span> <span class="n">d2</span><span class="p">,</span> <span class="n">riegelResult</span><span class="p">,</span> <span class="n">cameronResult</span><span class="p">;</span>

	<span class="k">if</span><span class="p">(</span><span class="n">argc</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">printf</span><span class="p">(</span><span class="s">"This function takes three arguments:</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
		<span class="n">printf</span><span class="p">(</span><span class="s">"time1 (seconds), distance1 (meters), distance2(meters)"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="n">sscanf</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span><span class="s">"%lf"</span><span class="p">,</span><span class="o">&amp;</span><span class="n">t1</span><span class="p">);</span>
	<span class="n">sscanf</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span><span class="s">"%lf"</span><span class="p">,</span><span class="o">&amp;</span><span class="n">d1</span><span class="p">);</span>
	<span class="n">sscanf</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span><span class="s">"%lf"</span><span class="p">,</span><span class="o">&amp;</span><span class="n">d2</span><span class="p">);</span>
	<span class="n">riegelResult</span> <span class="o">=</span> <span class="n">riegel</span><span class="p">(</span><span class="n">t1</span><span class="p">,</span> <span class="n">d1</span><span class="p">,</span> <span class="n">d2</span><span class="p">);</span>
    <span class="n">cameronResult</span> <span class="o">=</span> <span class="n">cameron</span><span class="p">(</span><span class="n">t1</span><span class="p">,</span> <span class="n">d1</span> <span class="o">/</span> <span class="n">MILEMETERS</span><span class="p">,</span> <span class="n">d2</span> <span class="o">/</span> <span class="n">MILEMETERS</span><span class="p">);</span>
	<span class="n">printf</span><span class="p">(</span><span class="s">"Riegel: %f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">riegelResult</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Cameron: %f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">cameronResult</span><span class="p">);</span>
	<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>As I progressed through college I was introduced to more broad health and fitness indicators with applications outside of track and field.  With over 30 calculations at the time, I decided to rewrite my simple program so that I would not have to remember all of the formulas, input units and output units we had in my classes and so I could build more interesting programs with these formulas and models.
At the time, NodeJS had just been released and Javascript was starting to become a language for both back-end and front-end programming (and I thought I might be able save money on hosting if I wrote front-end applications), so I chose Javascript as the language for version 1.0 of my FitnessJS library.</p>

<p>As time has passed, the FitnessJS project has grown both in purpose and in implementation. 
 This small project has expanded to include a sibling project for large-scale Python data analysis called <a href="">PyFit</a>.  The goal for FitnessJS (and PyFit) are to be the premiere open source libraries for creating fitness-based applications.</p>

<h2 id="cardiovascular" class="linked-heading">
    Cardiovascular<a href="#cardiovascular">🔗</a>
</h2>

<p>The first and largest namespace in the library is the cardiovascular namespace.  The namespace contains estimators and formulas for determining heart rate, VO<sub>2 Max</sub>, and residual volumes, energy expenditure   The cardiovascular namespace contains 3 subspaces:</p>

<ul>
  <li>Cardiac - Heart rate from age, age from heart rate, mean arterial pressure (MAP)</li>
  <li>Energy - Basal Metabolic Rate (BMR), Resting Metabolic Rate (RMR), Total Energy Expenditure (TEE)</li>
  <li>Respiration - Residual volume, VO<sub>2 Reserve</sub>, VO<sub>2 Max</sub></li>
</ul>

<h3 id="cardiac" class="linked-heading">
    Cardiac<a href="#cardiac">🔗</a>
</h3>
<p>The Cardiac namespace contains 12 classes which can be used to estimate maximum heart rate rate from age, and to estimate age from maximum heart rate.  These class all use the same interface:</p>

<ul>
  <li>predict - predicts maximum heart rate</li>
  <li>age - predicts age in years from maximum heart rate</li>
</ul>

<p>Other methods in the namespace are the following functions:</p>

<ul>
  <li>mean_arterial_pressure - used to calculate overall blood flow, and therefore nutrient delivery to organs</li>
  <li>Karvonen - Used to calculate exercise heart rate for a given training intensity (as a percentage)</li>
  <li>Zoladz - calculate heart rate zones based on maximum heart rate</li>
</ul>

<h3 id="energy" class="linked-heading">
    Energy<a href="#energy">🔗</a>
</h3>

<ul>
  <li>Basal Metabolic Rate (BMR) - kilocalories needed to keep critical processes going while at rest.  BMR is usually measured under strict conditions: in a darkened room after waking up from 8 hours of sleep in the testing facility, and fasting for 12 hours.</li>
  <li>Resting Metabolic Rate (RMR) - kilocalories needed to keep critical processes going while at rest</li>
  <li>Resting Daily Energy Expenditure</li>
  <li>Total Energy Expenditure (TEE) - kilocalories needed to perform vital processes, and carry out physical functions</li>
  <li>Terrain - calculating energy expenditure of a traveler across a terrain at a given walking speed</li>
</ul>

<h3 id="respiration" class="linked-heading">
    Respiration<a href="#respiration">🔗</a>
</h3>
<p>Calculations for predicting residual volume, and VO<sub>2</sub>.  Residual volume can be used</p>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>Within in the Cardio subnamespace, developers can predict the maximum heart for demographics based on age.  This can be used to identify the exertion of exercisers based on the percentage of their maximum heart rate they are exerting.  The maximum heart rate can then be used to calculate training zones for exercises regimens.
The energy subnamespace provides methods for calculating the amount of energy than people need to sustain their body, both for critical systems and for sustaining levels of physical activity.  The amount of energy can be used in nutrition systems to estimate if individuals caloric intake meets the essential needs of their bodies.  Adequate caloric intake is especially important for young athletes  for adequate recovery and long-term physical growth, and competitive athletes for increasing recovery times and sustaining a high level of training.</p>

<h2 id="composition" class="linked-heading">
    Composition<a href="#composition">🔗</a>
</h2>

<p>Provides methods for predicting body composition and and formulas for evaluating results of body composition tests.</p>

<ul>
  <li>Density - Provides methods for evaluating skinfold measures</li>
  <li>Fat - Provides methods for calculating body fat percentages</li>
  <li>Hydration - Estimate daily water intake in Liters per day</li>
  <li>Ideal - estimate ideal body weight, waist circumference</li>
  <li>Index - estimate body composition indices</li>
  <li>Mass - estimate fat-free mass</li>
  <li>Stature - estimate stature</li>
  <li>Surface Area - estimate surface area of an individual</li>
</ul>

<p>Search trends on Google for weight loss and exercise are strongly correlated together, indicating a link between these two aspects.  This correlation indicated a need for a fitness library to address body weight and body composition calculations</p>

<script type="text/javascript" src="https://ssl.gstatic.com/trends_nrtr/1435_RC10/embed_loader.js"></script>
<script type="text/javascript"> trends.embed.renderExploreWidget("TIMESERIES", {"comparisonItem":[{"keyword":"/m/023s6n","geo":"US","time":"today 12-m"},{"keyword":"/m/019w6h","geo":"US","time":"today 12-m"}],"category":44,"property":""}, {"exploreQuery":"cat=44&geo=US&q=%2Fm%2F023s6n,%2Fm%2F019w6h&date=today 12-m,today 12-m","guestPath":"https://trends.google.com:443/trends/embed/"}); </script>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>The most applicable namespaces are fat, hydration, ideal, and mass.  While the others have clinical significance, they do not tend to be as commonly useful.</p>

<p>When starting training for an individual, or when starting to analyze a population, the data is not the most detailed or informative.  Commonly, you request the basic information from the individual/users before you start collecting more detailed information.  The composition namespace helps developers to make general inferences about individuals, so they can identify the most effective for learning more about them.  For example, let’s say I registered for an account on a fitness site where I gave the following information:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Name</td>
      <td>Doug Fenstermacher</td>
    </tr>
    <tr>
      <td>Date of Birth</td>
      <td>March 17th, 1990</td>
    </tr>
    <tr>
      <td>Height</td>
      <td>5’10”</td>
    </tr>
    <tr>
      <td>Weight</td>
      <td>145lb</td>
    </tr>
    <tr>
      <td>5k personal record (running)</td>
      <td>15:23</td>
    </tr>
  </tbody>
</table>

<p>The developer for that website can infer immediately that I am a young adult male of average height, and of relatively low weight compared to the rest of the U.S. population, and that at some point, I could run a fairly fast 5k.  Now, let’s say they are using the composition namespace in the FitnessJS library:</p>

<ul>
  <li>Based on the <code class="language-plaintext highlighter-rouge">adultBMI</code>` method, I am roughly 1.4% body fat</li>
  <li>Based on the <code class="language-plaintext highlighter-rouge">Ideal</code>` class, my naive ideal weight is 156lb, making me 16lb under ideal weight (which would not be uncommon for a runner with a 15:23 personal record)</li>
  <li>Based on the <code class="language-plaintext highlighter-rouge">Ideal</code>` class, my naive ideal waistline is 32” inches</li>
  <li>Based on the <code class="language-plaintext highlighter-rouge">SurfaceArea</code>` class, my surface area is 1.47m<sup>2</sup></li>
  <li>I need to drink 2 Liters of water per day</li>
  <li>My stride length is estimated to be 73 centimeters</li>
</ul>

<p>The developer could take next steps by prompting me to provide information regarding my current activity level, my current water intake, my waistline, and what my time was for my last race.  This way, the developer could learn if my water intake is adequate (from my current water intake), how far off my running performances are from my past peak running ability (based on my latest race results), and determine if the calculated BMI and body fat percentage is an accurate reflection of my current body composition.  Then the developer will have a fairly comprehensive view of the latest user, and be able provide recommendations based on an accurate view of the user.</p>

<h2 id="models" class="linked-heading">
    Models<a href="#models">🔗</a>
</h2>

<p>The models namespace contains measures and predictors that were built using statistical analysis.  Currently the models are all under the aerobic subnamespace.  These models are the following:</p>

<ul>
  <li>Riegel</li>
  <li>Cameron</li>
  <li>Vickers-Vertosick</li>
</ul>

<h3 id="riegel" class="linked-heading">
    Riegel<a href="#riegel">🔗</a>
</h3>
<p>The Riegel model has been expanded since my first implementation.  The current implementation allows users to optionally use a more specific factor for predicting performances based on gender, age group, and sport based on Peter Riegel’s publication <a href="http://www.runscore.com/coursemeasurement/Articles/ARHE.pdf">Athletics Records and Human Endurance</a> published in <a href="">American Scientist</a> in 1981.   Riegel’s model is accurate for performances between 3.5 minutes and 230 minutes.</p>

<h3 id="cameron" class="linked-heading">
    Cameron<a href="#cameron">🔗</a>
</h3>
<p>The Cameron model hasn’t changes since the first implementation.  The model performs well for races between 400m and 50 miles.  Dave Cameron came up with this model by taking some of the top times in the world for distances between 400m and 50 miles and using non-linear regression to fit a function.  You can read his email exchange provided by<a href="http://www.cs.uml.edu/~phoffman/cammod.html">UMass Lowell</a></p>

<h3 id="vickers-vertosick" class="linked-heading">
    Vickers-Vertosick<a href="#vickers-vertosick">🔗</a>
</h3>
<p>A new, experimental addition to the library.  This model focuses on recreational runners and uses average weekly mileage and previous race time to predict performances in the marathon.</p>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>The Riegel and Cameron model were the original models I used in college to predict race performances.  These models continue to be the most common models used to predict athletic performances.  I used the Riegel and Cameron performance models for predicting my own performances in events and for predicting those of my teammates and competitors.  Regardless of whether either of us had competed in the event before these models enabled me to compare how I might fare against competitors, or my teammates.
Other uses of these formulas may be to predict how far a person ran if only the duration of their run is known.  If comparing a large number of athletes across distances, the models can be used to normalize endurance performances so that the runners can be seeded/ranked.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">performances</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Doug Funny</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">distance</span><span class="p">:</span> <span class="mi">800</span><span class="p">,</span> <span class="c1">// meters</span>
        <span class="na">time</span><span class="p">:</span> <span class="mf">121.2</span> <span class="c1">// seconds (2:01.2)</span>
    <span class="p">},</span>
    <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Skeeter Valentine</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">distance</span><span class="p">:</span> <span class="mi">3200</span><span class="p">,</span> <span class="c1">// meters</span>
        <span class="na">time</span><span class="p">:</span> <span class="mf">528.3</span> <span class="c1">// seconds (8:48.3)</span>
    <span class="p">}</span>
<span class="p">],</span>
<span class="nx">comparisonDistance</span> <span class="o">=</span> <span class="mi">1600</span><span class="p">;</span> <span class="c1">// meters</span>

<span class="k">for</span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">performances</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">performance</span> <span class="o">=</span> <span class="nx">performances</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span>
        <span class="nx">model</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fit</span><span class="p">.</span><span class="nx">model</span><span class="p">.</span><span class="nx">aerobic</span><span class="p">.</span><span class="nx">Riegel</span><span class="p">(</span><span class="nx">performance</span><span class="p">.</span><span class="nx">distance</span><span class="p">,</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">time</span><span class="p">);</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">predicted time for </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> in the </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">comparisonDistance</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">m:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">model</span><span class="p">.</span><span class="nx">time</span><span class="p">(</span><span class="nx">comparisonDistance</span><span class="p">));</span>
<span class="p">}</span>
<span class="c1">// predicted time for Doug Funny in the 1600m: 252.69370042788785</span>
<span class="c1">// predicted time for Skeeter Valentine in the 1600m: 253.38961711976856</span></code></pre></figure>

<p>According these calculations, Skeeter will lose to Doug in a 1600m race, but it will be a close race.</p>

<h2 id="sport" class="linked-heading">
    Sport<a href="#sport">🔗</a>
</h2>

<p>Sport is for sport-specific calculations.  Currently, the namespace includes a single running namespace, which contains 4 namespaces within it:</p>

<ul>
  <li>Adjustment - adjusting performances based on temperature</li>
  <li>Jack Daniels - uses Jack Daniel’s formulas for calculating VO<sub>2 Max</sub>, velocity, pacing zones for training, and more</li>
  <li>Pace - Calculating pace and speed of a run</li>
</ul>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>The running module has calculations that are uniquely useful for coaches and running enthusiasts.  The jackdaniels namespace contains functions for calculating paces based on training zones for workouts, and for calculating the percent of VO2 a person is working at based on how long they have been running.
Performances can also be adjusted for temperature changes.  Although, the temperature can only be adjusted at 5 degree intervals.</p>

<h2 id="strength" class="linked-heading">
    Strength<a href="#strength">🔗</a>
</h2>

<p>The strength namespace focuses on weightlifting exercises and vertical jump measurement.  This namespace has functions for comparing weightlifting performances across demographics, predicting 1 repetition maximums, and for estimating the amount of power in a vertical jump.  The most commonly used functionality is the RM calculations.  While running is my sport of choice, across the world weight training is currentyl the most popular method of exercise, making stable RM estimator calculations all the more essential to creating low-risk training programs</p>

<script type="text/javascript" src="https://ssl.gstatic.com/trends_nrtr/1435_RC10/embed_loader.js"></script>
<script type="text/javascript"> trends.embed.renderExploreWidget("TIMESERIES", {"comparisonItem":[{"keyword":"/m/0c4f_","geo":"","time":"today 12-m"},{"keyword":"/m/06h7j","geo":"","time":"today 12-m"},{"keyword":"/m/06z6r","geo":"","time":"today 12-m"},{"keyword":"/m/01sgl","geo":"","time":"today 12-m"}],"category":44,"property":""}, {"exploreQuery":"cat=44&date=today%2012-m,today%2012-m,today%2012-m,today%2012-m&geo=,,,&q=%2Fm%2F0c4f_,%2Fm%2F06h7j,%2Fm%2F06z6r,%2Fm%2F01sgl","guestPath":"https://trends.google.com:443/trends/embed/"}); </script>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>Comparing weightlifting performances is a trivial task when one can control how the session will be performed, such as the activity, weight lifted, or the number of repetitions.  But what about comparing independent performances that have already occurred?
The strength module provided classes for comparing athletes of varying weight, weight lifted, and number of repetitions.  These methods were developed by weightlifters, for other weightlifters.</p>

<p>The strength namespace provides many models for predicting weight for 1 repetition maximums (1-RM), or n-RM.  These are useful for determining the weights beginner lifters should use for lower repetition sets while reducing the risk of adding too little weight, or more importantly, too much weight.  These models can also be used to calculate the weight lifted for a number of repetitions based on a lifter’s 1-RM.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">performance</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Doug Fenstermacher</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">exercise</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Bicep curls</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">repetitions</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
    <span class="na">weight</span><span class="p">:</span> <span class="mi">60</span> <span class="c1">// lb</span>
<span class="p">},</span>
<span class="nx">model</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fit</span><span class="p">.</span><span class="nx">strength</span><span class="p">.</span><span class="nx">Baechle</span><span class="p">(</span><span class="nx">performance</span><span class="p">.</span><span class="nx">repetitions</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">1-RM: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">model</span><span class="p">.</span><span class="nx">predict</span><span class="p">(</span><span class="nx">performance</span><span class="p">.</span><span class="nx">weight</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">lb</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// 1-RM: 73.86lb</span></code></pre></figure>

<h2 id="anthropometry-latest-addition" class="linked-heading">
    Anthropometry (latest addition)<a href="#anthropometry-latest-addition">🔗</a>
</h2>

<p>A new addition to the FitnessJS library, anthropometry is the measurement of the human body.  This namespace is loosely affiliated with the exercise physiology.  The intent of this namespace is to be used to supplement the calculations from other measures, or to estimate the total height of an individual which then can used in other calculations.  Fun fact: Anthropometry is commonly used in fashion design when determine the range of sizes for clothing and the proportions between parts of clothing.</p>

<ul>
  <li>Segment Height - estimated from total height</li>
  <li>Total Height - estimated from segment height</li>
</ul>

<h3 id="applications" class="linked-heading">
    Applications<a href="#applications">🔗</a>
</h3>
<p>Anthropometry allows developers to derive information about the dimensions of a person’s body from other information about their body.  Currently, the anthropometry namespace contains functions regarding height, meaning that it can be used to determine the height of a person based on the height of other parts of their body.  For example, if a user were to provide information that their hips were 0.9434meters from the ground, the height_from_height_hips function could be used to determine that the user is roughly 1.78 meters tall.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">hipHeight</span> <span class="o">=</span> <span class="mf">0.9434</span><span class="p">;</span> <span class="c1">// meters</span>

<span class="kd">var</span> <span class="nx">totalHeight</span> <span class="o">=</span> <span class="nx">Fit</span><span class="p">.</span><span class="nx">anthropometry</span><span class="p">.</span><span class="nx">height_from_height_hips</span><span class="p">(</span><span class="nx">hipHeight</span><span class="p">);</span> <span class="c1">// 1.78 meters tall</span></code></pre></figure>]]></content><author><name>Doug Fenstermacher</name></author><category term="project" /><category term="data" /><category term="math" /><category term="forecasting" /><summary type="html"><![CDATA[A Typescript library for physical fitness calculations]]></summary></entry><entry><title type="html">Data Simplification &amp;amp; Summarization</title><link href="https://dougfenstermacher.com/blog/simplification-summarization" rel="alternate" type="text/html" title="Data Simplification &amp;amp; Summarization" /><published>2021-05-18T00:00:00+00:00</published><updated>2021-05-18T00:00:00+00:00</updated><id>https://dougfenstermacher.com/blog/simplification-summarization</id><content type="html" xml:base="https://dougfenstermacher.com/blog/simplification-summarization"><![CDATA[<h2 id="why" class="linked-heading">
    Why?<a href="#why">🔗</a>
</h2>

<p>Why would we want to simplify data?  We hear all about large technology companies that use “big data” to predict things previously thought to be impossible, such as training a computer to drive a car. 
 So why would we want to take data and simplify it?  Wouldn’t that mean we are losing information that we should use?
Before we get to that we should probably answer some basic questions first. Like, what is a polyline?  <em>A polyline is a continuous line made up of many straight line segments</em>.  SVG even defines an element called a <a href="https://www.w3.org/TR/SVG11/shapes.html#PolylineElement">polyline</a> which is exactly the same as the polylines I am talking about here.  A polyline can represent real-world data such as trajectories, topological lines, and even graphs.   <em>Simplifying a polyline means reducing the number of points in a polyline but still keeping the general shape of the original line.</em></p>

<p>Polylines often want to be simplified for a few different reasons:</p>

<ul>
    <li><b>Reduce cost of data storage.</b>  Storing many polylines with many points, reducing the number of points in each polyline can save lots of space on the machines hosting the database.  In a cloud-based scenario, this could result in a monthly saving of thousands of dollars by no longer needing to pay for storage space.</li>
    <li><b>Speed up computation of other processes.</b>- Some algorithms that run on polylines can take significantly longer when the polyline contains more points.  By reducing the the number of points, developers can execute these algorithms in less time, and using less memory.</li>
    <li><b>Denoising.</b>  In some cases, polylines may contain noise, or points that are not useful in their application.  For example, in a person's GPS trajectory, many points may be recorded when the person has stopped at a location.  Since only 2 points would be required to indicate when the person arrived and left the location, the other points could be considered noise and not worth storing/processing.</li>
</ul>

<p>A graph is data structure used to represent networks such as social networks, road networks, supply chains, documents, etc.  These networks can be very large and thus difficult for a human to interpret.  Graph summarization focuses on facilitating <q cite="https://arxiv.org/abs/1612.04883">the identification of structure and meaning in data</q> so that humans can interpret the structure of a graph.</p>

<h2 id="how" class="linked-heading">
    How?<a href="#how">🔗</a>
</h2>

<p>Polyline simplification has many different algorithms.  In fact, there were already many different algorithms in existence by 1990.  To this day, the most popular popular algorithms for polyline simplification are <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Ramer-Douglas-Puecker</a>, and <a href="https://www.semanticscholar.org/paper/The-Douglas-Peucker-Algorithm-for-Line-through-Visvalingam-Whyatt/571e05f74711277f769f8194c2c60d29c70ce749">Visvalingham-Whyatt</a>.  These approaches can be applied in any number of dimensions using any calculable distance metric.</p>

<p>Ramer-Douglas-Puecker measures how different a point is from it’s neighbors by measuring the perpendicular distances from the point, to a line between it’s two neighbors.  If the point is within a user-specific distance of the line, the point is removed.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">perpendicular_distance</span><span class="p">(</span><span class="n">point</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
    <span class="s">"""
    perpendicular distance between a point and a line segment

    Arguments:
        point (tuple|list): The point
        a (tuple|list): The start point of the line segment
        b (tuple|list): The end point of the line segment

    Returns:
        float: perpendicular distance
    """</span>
    <span class="n">point</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">point</span><span class="p">)</span>
    <span class="n">a</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="n">b</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
    <span class="n">ba</span> <span class="o">=</span> <span class="n">b</span> <span class="o">-</span> <span class="n">a</span>
    <span class="n">numerator</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">cross</span><span class="p">(</span><span class="n">ba</span><span class="p">,</span> <span class="n">b</span><span class="o">-</span><span class="n">point</span><span class="p">))</span>
    <span class="n">denominator</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">ba</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">numerator</span><span class="o">/</span><span class="n">denominator</span>


<span class="k">def</span> <span class="nf">ramer_douglas_peucker</span><span class="p">(</span><span class="n">points</span><span class="p">,</span> <span class="n">epsilon</span><span class="p">):</span>
    <span class="s">"""
    Algorithm that decimates a curve composed of line segments to a similar curve with fewer points

    Arguments:
        points(list): list of sequential points in the polyline
        epsilon (int|float): The maximum distance from the existing line to be considered an essential point

    Returns:
        list:  The simplified polyline
    """</span>
    <span class="n">dmax</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">points</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">d</span> <span class="o">=</span> <span class="n">perpendicular_distance</span><span class="p">(</span><span class="n">points</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">points</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">points</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">d</span> <span class="o">&gt;</span> <span class="n">dmax</span><span class="p">:</span>
            <span class="n">index</span> <span class="o">=</span> <span class="n">i</span>
            <span class="n">dmax</span> <span class="o">=</span> <span class="n">d</span>

    <span class="k">if</span> <span class="n">dmax</span> <span class="o">&gt;</span> <span class="n">epsilon</span><span class="p">:</span>
        <span class="n">results1</span> <span class="o">=</span> <span class="n">ramer_douglas_peucker</span><span class="p">(</span><span class="n">points</span><span class="p">[:</span><span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">],</span> <span class="n">epsilon</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="n">results2</span> <span class="o">=</span> <span class="n">ramer_douglas_peucker</span><span class="p">(</span><span class="n">points</span><span class="p">[</span><span class="n">index</span><span class="p">:],</span> <span class="n">epsilon</span><span class="p">)</span>
        <span class="n">results</span> <span class="o">=</span> <span class="n">results1</span> <span class="o">+</span> <span class="n">results2</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">results</span> <span class="o">=</span> <span class="p">[</span><span class="n">points</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">points</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
    <span class="k">return</span> <span class="n">results</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">points</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">),(</span><span class="mi">1</span><span class="p">,</span><span class="mf">0.1</span><span class="p">),(</span><span class="mi">2</span><span class="p">,</span><span class="o">-</span><span class="mf">0.1</span><span class="p">),(</span><span class="mi">3</span><span class="p">,</span><span class="mi">5</span><span class="p">),(</span><span class="mi">4</span><span class="p">,</span><span class="mi">6</span><span class="p">),(</span><span class="mi">5</span><span class="p">,</span><span class="mi">7</span><span class="p">),(</span><span class="mi">6</span><span class="p">,</span><span class="mf">8.1</span><span class="p">),(</span><span class="mi">7</span><span class="p">,</span><span class="mi">9</span><span class="p">),(</span><span class="mi">8</span><span class="p">,</span><span class="mi">9</span><span class="p">),(</span><span class="mi">9</span><span class="p">,</span><span class="mi">9</span><span class="p">)]</span>
    <span class="n">correct</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.1</span><span class="p">),</span> <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mf">8.1</span><span class="p">),</span> <span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">)]</span>
    <span class="n">epsilon</span> <span class="o">=</span> <span class="mf">1.0</span>
    <span class="n">new_points</span> <span class="o">=</span> <span class="n">ramer_douglas_peucker</span><span class="p">(</span><span class="n">points</span><span class="p">,</span> <span class="n">epsilon</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">new_points</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Visvalingham-Whyatt has a fairly intuitive approach.  This approach finds the area of triangles made up between each point and its two neighbors.  The point with the smallest area triangle is removed, then the triangles of it’s neighbors are re-calculated, and the point with the smallest area triangle is removed.  This process is performed repeatedly until a user-specified number of minimum points remain in the polyline, or until only the start and end point of the polyline remain.  Unlike Ramer-Douglas-Pueker this approach accounts for both the distance between the point and it’s neighbors and the distance between the two neighbors themselves.  This ensures  that when two neighbors of a point are near to each other, the point is more likely to be removed, and less likely to be removed when they are far from each other. It also runs in O(n) time.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
</pre></td><td class="code"><pre><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">math</span>


<span class="k">def</span> <span class="nf">triangle_area</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">):</span>
    <span class="n">a</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="n">b</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
    <span class="n">c</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>

    <span class="n">ab</span> <span class="o">=</span> <span class="n">a</span> <span class="o">-</span> <span class="n">b</span>
    <span class="n">ab_dist</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">ab</span><span class="p">)</span>

    <span class="n">cb</span> <span class="o">=</span> <span class="n">c</span> <span class="o">-</span> <span class="n">b</span>
    <span class="n">cb_dist</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">cb</span><span class="p">)</span>
    <span class="n">fraction</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">ab</span><span class="p">,</span> <span class="n">cb</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">ab_dist</span> <span class="o">*</span> <span class="n">cb_dist</span><span class="p">)</span>
    <span class="n">theta</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="n">acos</span><span class="p">(</span><span class="n">fraction</span><span class="p">)</span>
    <span class="k">return</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">ab_dist</span> <span class="o">*</span> <span class="n">cb_dist</span> <span class="o">*</span> <span class="n">math</span><span class="p">.</span><span class="n">sin</span><span class="p">(</span><span class="n">theta</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">visvalingham_whyatt</span><span class="p">(</span><span class="n">points</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="s">"""
    Visvalingham-Whyatt algorithm for polyline simplification

    Runs in  linear O(n) time

    Parameters:
        points(list): list of sequential points in the polyline

    Keyword Arguments:
        min_points(int):  Minimum number of points in polyline, defaults to 2
        inplace (bool):  Indicates if the input polyline should remove points from the input list, defaults to False

    Returns:
        list: A list of min_points of the simplified polyline
    """</span>
    <span class="n">point_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">points</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'inplace'</span><span class="p">,</span> <span class="bp">False</span><span class="p">):</span>
        <span class="n">new_points</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">points</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">new_points</span> <span class="o">=</span> <span class="n">points</span>
    <span class="n">areas</span> <span class="o">=</span> <span class="p">[</span><span class="nb">float</span><span class="p">(</span><span class="s">'inf'</span><span class="p">)]</span>
    <span class="n">point_indexes</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="n">point_count</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">point_count</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">area</span> <span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">points</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">],</span> <span class="n">points</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">points</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span>
        <span class="n">areas</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">area</span><span class="p">)</span>

    <span class="n">min_points</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'min_points'</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">while</span> <span class="nb">len</span><span class="p">(</span><span class="n">new_points</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">min_points</span><span class="p">:</span>
        <span class="n">smallest_effective_index</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">point_indexes</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">areas</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
        <span class="n">new_points</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="n">smallest_effective_index</span><span class="p">)</span>
        <span class="n">areas</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="n">smallest_effective_index</span><span class="p">)</span>
        <span class="n">point_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">new_points</span><span class="p">)</span>
        <span class="n">point_indexes</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="n">point_count</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span>
        <span class="c1"># recompute area for point after previous_smallest_effective_index
</span>        <span class="k">if</span> <span class="n">smallest_effective_index</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">areas</span><span class="p">[</span><span class="n">smallest_effective_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span> <span class="o">-</span> <span class="mi">2</span><span class="p">],</span> <span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">],</span> <span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span><span class="p">])</span>
        <span class="c1"># recompute area for point before previous smallest_effective_index
</span>        <span class="k">if</span> <span class="n">smallest_effective_index</span> <span class="o">&lt;</span> <span class="n">point_count</span> <span class="o">-</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">areas</span><span class="p">[</span><span class="n">smallest_effective_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">triangle_area</span><span class="p">(</span><span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">],</span> <span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span><span class="p">],</span> <span class="n">new_points</span><span class="p">[</span><span class="n">smallest_effective_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">new_points</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">points</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">),(</span><span class="mi">1</span><span class="p">,</span><span class="mf">0.1</span><span class="p">),(</span><span class="mi">2</span><span class="p">,</span><span class="o">-</span><span class="mf">0.1</span><span class="p">),(</span><span class="mi">3</span><span class="p">,</span><span class="mi">5</span><span class="p">),(</span><span class="mi">4</span><span class="p">,</span><span class="mi">6</span><span class="p">),(</span><span class="mi">5</span><span class="p">,</span><span class="mi">7</span><span class="p">),(</span><span class="mi">6</span><span class="p">,</span><span class="mf">8.1</span><span class="p">),(</span><span class="mi">7</span><span class="p">,</span><span class="mi">9</span><span class="p">),(</span><span class="mi">8</span><span class="p">,</span><span class="mi">9</span><span class="p">),(</span><span class="mi">9</span><span class="p">,</span><span class="mi">9</span><span class="p">)]</span>

    <span class="n">point_count</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">points</span><span class="p">)</span>
    <span class="n">new_points</span> <span class="o">=</span> <span class="n">visvalingham_whyatt</span><span class="p">(</span><span class="n">points</span><span class="p">,</span> <span class="n">min_points</span><span class="o">=</span><span class="n">point_count</span> <span class="o">-</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">points</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">new_points</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Graph summarization has relatively few algorithms.  Graphs can contain many nodes, each of which can have one or more edge to any of the other nodes in the graph.   This greatly increases the amount of computation needed to process the graph, and the computational efficiency needed to reasonably process large graphs only became available in the last 20 years.  However, not all algorithms can be used on all types of graphs (static, attributed, temporal, etc.), and many algorithms have different summarization objectives (query time, pattern recognition, visualization, etc).</p>

<p>One approach to graph summarization is removing edges from a graph such that the overall structure of the original graph is preserved.  Dedensification is a process that does this by replacing edges around high-degree nodes and with a compressor node that shares edges between the original sources and targets of the original edges.  For example, let’s say we have 10 low-degree nodes with edges to 10 high-degree nodes represented by 100 edges.  Dedensification would replace those 100 edges with a 10 edges connecting to a compressor node, and 10 edges connecting the compressor node to the original high-degree node.  This reduces the number of edges between those nodes by 80%, speeds up algorithm performance on the updated graph and make it easier for viewers to process the number of nodes in the updated graph.</p>

<p>The <a href="http://pages.cs.wisc.edu/~jignesh/publ/summarization.pdf">SNAP and KSNAP algorithms</a> make graphs easier to interpret by creating a summary graph of supernodes that represent sets of nodes with the same user-specified node attributes and have similar edge connectivity with other supernodes in the graph.  The SNAP algorithm creates a summary graph of supernodes that represent nodes that share node attribute values and share edges of the same type with the same other supernodes.  That way, viewers of the summary graph can assume that the supernodes in the summary graph represent the exact node attributes and edge types from the original graph.</p>

<p>In the case of large graphs, the compressed graph may contain many compressed-nodes that represent a small number of nodes, making the compressed graph still hard to interpret by a human.  KSNAP resolves this by allowing the user to specify the number of supernodes in the produced summary graph.  This allows users to get summaries with varying levels of detail.  For example, high-level summaries may only have a handful of nodes, while a detailed summary may have many nodes.  To achieve a summary graph with fewer nodes the KSNAP algorithm loosens the SNAP constraint of “all nodes in a supernode must have the node attributes and the same edge types with other supernodes” to “all nodes in a supernode must have the same node attributes and <em>similar connectivity</em> with other supernodes in the graph”.  This way, the summary graph can still be a fairly accurate summary of the graph structure, and still produce an accurate summary.</p>

<p>To make these graph algorithms more generally available, I wrote the <code class="language-plaintext highlighter-rouge">summarization</code> module to the networkx Python library which is available in networkx 2.6.  Currently the <code class="language-plaintext highlighter-rouge">dedensify</code> function and <code class="language-plaintext highlighter-rouge">snap_aggregation</code> is included, with the <code class="language-plaintext highlighter-rouge">KSNAP</code> algorithms in <a href="https://github.com/dpfens/networkx/tree/ksnap">testing</a>.</p>

<h2 id="applications-conclusion" class="linked-heading">
    Applications &amp; Conclusion<a href="#applications-conclusion">🔗</a>
</h2>

<p>Polyline simplication can be applied to time series of data that is represented by a coordinate system.  This means that it could be applied to GPS trajectories, user behavior vectors, financial market positions, and more.  Graph summarization can also be applied to any graph where each node has meaningful attributes and the relationships need to be understood by a human.  To apply graph summarization to attributes of continuous data, you will need to convert the continuous attributes to ordinal categories, or implement <a href="https://ieeexplore.ieee.org/document/5447830">Discovery-driven Graph Summarization</a> to create those ordinal categories automatically.</p>

<p>These approaches to removing noise from the data can be very useful for efficiently storing and intensive computing, as well as for human consumption.  I would highly recommend using these approaches in industry and personal projects when applicable.</p>]]></content><author><name>Doug Fenstermacher</name></author><category term="blog" /><category term="data" /><category term="graph-theory" /><category term="math" /><category term="python" /><summary type="html"><![CDATA[Simplifying polylines and trajectories using the Ramer-Douglas-Puecker algorithm, and summarizing graphs/networks to speed up computations using grouping-based and compression-based algorithms.]]></summary></entry></feed>