Explorar o código

Add option to include cluster scale in dist measurements

Kirk Trombley %!s(int64=3) %!d(string=hai) anos
pai
achega
857ecb1f0d
Modificáronse 3 ficheiros con 53 adicións e 13 borrados
  1. 1 1
      nearest.css
  2. 5 0
      nearest.html
  3. 47 12
      nearest.js

+ 1 - 1
nearest.css

@@ -39,7 +39,7 @@ body {
     align-items: center;
 }
 
-.dropdown > select {
+.dropdown > select, input {
     margin-top: 8px;
     width: 80%;
 }

+ 5 - 0
nearest.html

@@ -54,6 +54,11 @@
                     Warning: Cluster means work best with mean-focused metrics
                 </div>
 
+                <div class="container control hide">
+                    <label for="scale-by-cluster-size">Cluster Scale in Distance:</label>
+                    <input type="checkbox" checked oninput="onScaleByClusterChanged()" id="scale-by-cluster-size">
+                </div>
+
                 <div class="hideable_control hideable_control--hidden">
                     <label for="include-x">Include inertia:</label>
                     <input type="checkbox" checked oninput="onCustomControlsChanged()" id="include-x">

+ 47 - 12
nearest.js

@@ -2,6 +2,7 @@
 const getColorInputNode = () => document.getElementById("color-input");
 const getMetricDropdownNode = () => document.getElementById("metric");
 const getMeanArgumentDropdownNode = () => document.getElementById("image-summary");
+const getClusterScaleToggleNode = () => document.getElementById("scale-by-cluster-size");
 const getClusterMeanWarning = () => document.getElementById("cluster-mean-warning");
 const getIncludeXToggleNode = () => document.getElementById("include-x");
 const getNormQYToggleNode = () => document.getElementById("norm-q-y");
@@ -101,7 +102,8 @@ const readColorInput = () => {
 // State
 const state = {
   metric: null,
-  meanArgument: 0, // TODO
+  meanArgument: null,
+  includeScaleInDist: null,
   includeX: null,
   normQY: null,
   closeCoeff: null,
@@ -114,31 +116,41 @@ const state = {
 // Metrics
 const summarySelectors = [
   // true mean
-  stats => stats.trueMean,
+  stats => [stats.trueMean, 1],
   // largest cluster
-  stats => stats.kMeans[stats.largestCluster],
+  stats => [stats.kMeans[stats.largestCluster], stats.kWeights[stats.largestCluster]],
   // smallest cluster
-  stats => stats.kMeans[stats.smallestCluster],
+  stats => [stats.kMeans[stats.smallestCluster], stats.kWeights[stats.smallestCluster]],
   // best fit cluster
-  (stats, q) => stats.kMeans[argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
+  (stats, q) => {
+    const best = argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
+    return [stats.kMeans[best], stats.kWeights[best]];
+  },
   // worst fit cluster
-  (stats, q) => stats.kMeans[argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
+  (stats, q) => {
+    const worst = argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
+    return [stats.kMeans[worst], stats.kWeights[worst]];
+  },
 ];
 
 const selectedSummary = (stats, q) => summarySelectors[state.meanArgument](stats, q);
 
 const metrics = [
   // RMS
-  (stats, q) => stats.varFromZero - 2 * vectorDot(selectedSummary(stats, q).vector, q.vector),
+  (stats, q) => stats.varFromZero - 2 * vectorDot(selectedSummary(stats, q)[0].vector, q.vector),
   // mean angle
-  (stats, q) => -vectorDot(selectedSummary(stats, q).unit, q.unit),
+  (stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
   // mean dist
-  (stats, q) => vectorSqDist(selectedSummary(stats, q).vector, q.vector),
+  (stats, q) => { 
+    // TODO I know there's some way to avoid recalculation here but I'm just too lazy right now
+    const [data, scale] = selectedSummary(stats, q);
+    return vectorSqDist(data.vector, q.vector) / (state.includeScaleInDist ? scale : 1);
+  },
   // hue angle
-  (stats, q) => angleDiff(selectedSummary(stats, q).hue, q.hue),
+  (stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
   // custom
   (stats, q) => (state.includeX ? stats.varFromZero : 0) - state.closeCoeff * vectorDot(
-    selectedSummary(stats, q)[state.normQY ? "unit" : "vector"], 
+    selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"], 
     state.normQY ? q.unit : q.vector,
   ),
 ];
@@ -216,7 +228,7 @@ const mathDefinitions = {
 const metricText = [
   muArg => String.raw`${mathArgBest("min", "P")}\left[I\left(P\right) - 2\vec{q}\cdot \vec{\mu}\left(${muArg}\right)\right]`,
   muArg => String.raw`${mathArgBest("max", "P")}\left[\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[\left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[ ${state.includeScaleInDist ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : ""} \left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
   muArg => String.raw`${mathArgBest("min", "P")} \left[\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
 ].map(s => muArg => TeXZilla.toMathML(s(muArg)));
 
@@ -428,6 +440,26 @@ const checkClusterMeanWarning = () => {
   }
 }
 
+const checkScaleByClusterToggle = () => {
+  const toggle = getClusterScaleToggleNode()?.parentNode;
+  const unhidden = toggle.getAttribute("class").replaceAll("hide", "");
+  if (state.meanArgument !== 0 && state.metric === 2) {
+    toggle.setAttribute("class", unhidden);
+  } else {
+    toggle.setAttribute("class", unhidden + " hide");
+  }
+}
+
+const onScaleByClusterChanged = skipScore => {
+  state.includeScaleInDist = getClusterScaleToggleNode()?.checked ?? true;
+
+  updateObjective();
+
+  if (!skipScore) {
+    rescore();
+  }
+}
+
 const onMeanArgumentChanged = skipScore => {
   const meanArgument = getMeanArgumentDropdownNode()?.selectedIndex ?? 0;
   if (meanArgument === state.meanArgument) {
@@ -435,6 +467,7 @@ const onMeanArgumentChanged = skipScore => {
   }
   state.meanArgument = meanArgument;
   checkClusterMeanWarning();
+  checkScaleByClusterToggle();
   updateObjective();
   if (!skipScore) {
     rescore();
@@ -448,6 +481,7 @@ const onMetricChanged = skipScore => {
   }
   state.metric = metric;
   checkClusterMeanWarning();
+  checkScaleByClusterToggle();
   if (state.metric === 4) { // Custom
     showCustomControls();
     onCustomControlsChanged(skipScore); // triggers rescore
@@ -499,6 +533,7 @@ const onPageLoad = () => {
   onColorChanged(true);
   onMetricChanged(true);
   onMeanArgumentChanged(true);
+  onScaleByClusterChanged(true);
   onLimitChanged(true);
   // then do a rescore directly, which will do nothing unless old data was loaded
   rescore();