<?xml version="1.0" encoding="UTF-8"?>        <rss version="2.0"
             xmlns:atom="http://www.w3.org/2005/Atom"
             xmlns:dc="http://purl.org/dc/elements/1.1/"
             xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
             xmlns:admin="http://webns.net/mvcb/"
             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
             xmlns:content="http://purl.org/rss/1.0/modules/content/">
        <channel>
            <title>
									Methods - Welcome, please register to post topics or comment!				            </title>
            <link>https://cyclesresearchinstitute.org/community/methods/</link>
            <description>Harmonics and Cycles Forum for scientific discussion and the pursuit and sharing of knowledge on all things harmonics and cycles. Please register and confirm your email if you wish to comment or post topics.</description>
            <language>en-US</language>
            <lastBuildDate>Wed, 02 Sep 2026 07:55:00 +0000</lastBuildDate>
            <generator>wpForo</generator>
            <ttl>60</ttl>
							                    <item>
                        <title>Graph Digitizer</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/graph-digitizer/</link>
                        <pubDate>Sun, 30 Aug 2026 07:52:18 +0000</pubDate>
                        <description><![CDATA[Quite often I find graphs published in articles but I can not find the data that the graph is based on. It can be very frustrating. So I made this app to allow the graphic to be dropped into...]]></description>
                        <content:encoded><![CDATA[<p>Quite often I find graphs published in articles but I can not find the data that the graph is based on. It can be very frustrating. So I made this app to allow the graphic to be dropped into it and after a few clicks on the scales and such it gives you a CSV file of the data in the graph. Here are the steps to follow:</p>
<ol>
<li>Copy the code below into your browser (or save and click to load).</li>
<li>Load the image by browsing to it.</li>
<li>Calibrate the x axis (usually time) by clicking each of an early and late date/time and specifying the value at that point.</li>
<li>If the y axis is on a log scale click that button.</li>
<li>Calibrate the y axis by clicking each of a low vale and a high value on the scale and typing in those values. </li>
<li>Give the series a name.</li>
<li>Select the colour with the cross hairs. Can be delicate for thin lines but it shows what you selected.  If you miss you can try again.</li>
<li>Give it the lower left and upper right corners that the graph stays inside. Make sure that there are no other things of that colour in that area or they will get digitized too.</li>
<li>Press extract curve.</li>
<li>Copy the resulting CSV file which has the co-ordinates of all the x,y points along the graph.</li>
<li>Go paste it somewhere useful.</li>
</ol>
<p>Thanks Claude for your help.</p>
<pre contenteditable="false">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;title&gt;Graph Digitizer&lt;/title&gt;
&lt;style&gt;
  :root{
    --bg: #14181c;
    --panel: #1c2126;
    --panel-2: #232a30;
    --line: #313a41;
    --text: #e7ecf0;
    --muted: #8b98a3;
    --accent: #5fd3c4;
    --accent-dim: #3a7d74;
    --warn: #e8a33d;
    --danger: #e0665f;
    --mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
    --sans: -apple-system, "Segoe UI", ui-sans-serif, system-ui, sans-serif;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:var(--sans);}
  body{padding:20px;}
  h1{font-size:15px;letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0 0 2px;font-weight:600;}
  .sub{color:var(--muted);font-size:12.5px;margin:0 0 18px;line-height:1.5;}
  .layout{display:grid;grid-template-columns:300px 1fr 320px;gap:16px;align-items:start;}
  @media (max-width:1100px){.layout{grid-template-columns:1fr;}}
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px;}
  .panel + .panel{margin-top:14px;}
  .step{border-left:2px solid var(--line);padding:2px 0 14px 14px;position:relative;}
  .step:last-child{padding-bottom:2px;}
  .step::before{content:attr(data-n);position:absolute;left:-11px;top:0;width:20px;height:20px;border-radius:50%;background:var(--panel-2);border:1px solid var(--line);color:var(--muted);font-family:var(--mono);font-size:10.5px;display:flex;align-items:center;justify-content:center;}
  .step.active::before{border-color:var(--accent);color:var(--accent);background:#132523;}
  .step.done::before{content:"?";border-color:var(--accent-dim);color:var(--accent);}
  .step h3{margin:0 0 6px;font-size:12.5px;color:var(--text);font-weight:600;}
  .step p.hint{margin:0 0 8px;font-size:11.5px;color:var(--muted);line-height:1.5;}
  button{font-family:var(--sans);font-size:12.5px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);padding:7px 11px;border-radius:6px;cursor:pointer;}
  button:hover{border-color:var(--accent-dim);}
  button:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
  button.primary{background:var(--accent);color:#0c1214;border-color:var(--accent);font-weight:600;}
  button.primary:hover{background:#7ee0d3;}
  button:disabled{opacity:.35;cursor:not-allowed;}
  button.small{padding:4px 8px;font-size:11px;}
  input,input{font-family:var(--mono);font-size:12.5px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:6px 8px;width:100%;}
  input:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
  label{font-size:11px;color:var(--muted);display:block;margin-bottom:3px;}
  .row{display:flex;gap:8px;margin-bottom:8px;}
  .row &gt; div{flex:1;}
  .canvas-wrap{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px;position:relative;overflow:auto;max-height:78vh;}
  .canvas-inner{position:relative;display:inline-block;}
  canvas{display:block;max-width:100%;cursor:crosshair;border-radius:2px;}
  .readout{position:absolute;pointer-events:none;font-family:var(--mono);font-size:10.5px;background:rgba(12,18,20,.88);color:var(--accent);border:1px solid var(--accent-dim);padding:3px 6px;border-radius:4px;white-space:nowrap;transform:translate(10px,-28px);}
  .drop{border:1.5px dashed var(--line);border-radius:8px;padding:28px 10px;text-align:center;color:var(--muted);font-size:12px;cursor:pointer;}
  .drop:hover{border-color:var(--accent-dim);color:var(--text);}
  .series-item{border:1px solid var(--line);border-radius:6px;padding:8px;margin-bottom:8px;background:var(--panel-2);}
  .series-item.active{border-color:var(--accent);}
  .swatch{width:13px;height:13px;border-radius:3px;display:inline-block;vertical-align:-2px;border:1px solid rgba(255,255,255,.2);margin-right:6px;}
  .series-name{font-size:12.5px;font-weight:600;}
  .series-meta{font-size:10.5px;color:var(--muted);font-family:var(--mono);margin-top:3px;}
  .tabs{display:flex;gap:4px;margin-bottom:10px;}
  .tabs button{flex:1;}
  .tabs button.active{background:var(--panel-2);border-color:var(--accent);color:var(--accent);}
  table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:11px;}
  th,td{text-align:left;padding:3px 6px;border-bottom:1px solid var(--line);}
  th{color:var(--muted);font-weight:500;position:sticky;top:0;background:var(--panel);}
  .tablewrap{max-height:220px;overflow:auto;border:1px solid var(--line);border-radius:6px;margin-top:6px;}
  textarea{width:100%;height:140px;font-family:var(--mono);font-size:11px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:6px;padding:8px;resize:vertical;}
  .flexgap{display:flex;gap:8px;margin-top:8px;}
  .flexgap button{flex:1;}
  .empty{color:var(--muted);font-size:12px;text-align:center;padding:20px 6px;}
  .checkrow{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--muted);margin-bottom:8px;}
  .checkrow input{width:auto;}
  .legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
  .toolbar-note{font-size:10.5px;color:var(--warn);margin-top:6px;line-height:1.4;}
  ::-webkit-scrollbar{width:9px;height:9px;}
  ::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px;}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Graph Digitizer&lt;/h1&gt;
&lt;p class="sub"&gt;Load a chart image, calibrate its axes against two known reference points on each, sample the curve's color, then extract a dense table of coordinate pairs. Everything runs locally in this page.&lt;/p&gt;

&lt;div class="layout"&gt;

  &lt;!-- LEFT: workflow --&gt;
  &lt;div&gt;
    &lt;div class="panel"&gt;
      &lt;div class="step" id="step-load" data-n="1"&gt;
        &lt;h3&gt;Load image&lt;/h3&gt;
        &lt;p class="hint"&gt;Drop a chart image, or browse for one.&lt;/p&gt;
        &lt;div class="drop" id="dropZone"&gt;Drop image here, or click to choose a file&lt;/div&gt;
        &lt;input type="file" id="fileInput" accept="image/*" style="display:none;"&gt;
      &lt;/div&gt;

      &lt;div class="step" id="step-x" data-n="2"&gt;
        &lt;h3&gt;Calibrate X axis&lt;/h3&gt;
        &lt;p class="hint"&gt;Click two points on the X axis whose values you know (e.g. two gridline ticks), entering the value after each click.&lt;/p&gt;
        &lt;div class="checkrow"&gt;&lt;input type="checkbox" id="xLog"&gt;&lt;label for="xLog" style="margin:0;"&gt;Log scale on X&lt;/label&gt;&lt;/div&gt;
        &lt;button id="btnCalibX" disabled&gt;Set X reference points&lt;/button&gt;
        &lt;div id="xStatus" class="series-meta" style="margin-top:6px;"&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="step" id="step-y" data-n="3"&gt;
        &lt;h3&gt;Calibrate Y axis&lt;/h3&gt;
        &lt;p class="hint"&gt;Same idea, on the Y axis.&lt;/p&gt;
        &lt;div class="checkrow"&gt;&lt;input type="checkbox" id="yLog"&gt;&lt;label for="yLog" style="margin:0;"&gt;Log scale on Y&lt;/label&gt;&lt;/div&gt;
        &lt;button id="btnCalibY" disabled&gt;Set Y reference points&lt;/button&gt;
        &lt;div id="yStatus" class="series-meta" style="margin-top:6px;"&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="step" id="step-series" data-n="4"&gt;
        &lt;h3&gt;Trace a curve&lt;/h3&gt;
        &lt;p class="hint"&gt;Name the series, then click directly on the curve in the image to sample its color. Adjust tolerance if it misses spots or catches noise.&lt;/p&gt;
        &lt;div class="row"&gt;
          &lt;div&gt;&lt;label&gt;Series name&lt;/label&gt;&lt;input type="text" id="seriesName" placeholder="e.g. Exxon curve"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;button id="btnPickColor" disabled&gt;Pick color from curve&lt;/button&gt;
        &lt;div id="colorPreview" class="series-meta" style="margin-top:6px;"&gt;&lt;/div&gt;
        &lt;div style="margin-top:10px;"&gt;
          &lt;label&gt;Color match tolerance: &lt;span id="tolVal"&gt;45&lt;/span&gt;&lt;/label&gt;
          &lt;input type="range" id="tolerance" min="2" max="160" value="45" style="width:100%;"&gt;
        &lt;/div&gt;
        &lt;div style="margin-top:10px;"&gt;
          &lt;button id="btnSetRegion" disabled style="width:100%;"&gt;Restrict to plot area (optional)&lt;/button&gt;
          &lt;div id="regionStatus" class="series-meta" style="margin-top:4px;"&gt;Not set — will scan the whole image. Set this if legend text, axis labels, or anything else shares the curve's color.&lt;/div&gt;
          &lt;button id="btnClearRegion" class="small" style="margin-top:6px;"&gt;Clear region (use whole image)&lt;/button&gt;
        &lt;/div&gt;
        &lt;button id="btnExtract" class="primary" disabled style="margin-top:10px;width:100%;"&gt;Extract curve&lt;/button&gt;
        &lt;p class="toolbar-note" id="extractNote"&gt;&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="panel"&gt;
      &lt;h3 style="margin:0 0 8px;font-size:12.5px;"&gt;Series extracted&lt;/h3&gt;
      &lt;div id="seriesList"&gt;&lt;div class="empty"&gt;None yet&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- CENTER: canvas --&gt;
  &lt;div class="canvas-wrap"&gt;
    &lt;div class="canvas-inner" id="canvasInner"&gt;
      &lt;canvas id="canvas" width="600" height="400"&gt;&lt;/canvas&gt;
      &lt;div class="readout" id="readout" style="display:none;"&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- RIGHT: results --&gt;
  &lt;div class="panel"&gt;
    &lt;h3 style="margin:0 0 10px;font-size:12.5px;"&gt;Output&lt;/h3&gt;
    &lt;div class="row"&gt;
      &lt;div&gt;
        &lt;label&gt;X axis label&lt;/label&gt;
        &lt;input type="text" id="xLabel" value="X" placeholder="e.g. Millions of years ago"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="checkrow" style="margin-top:2px;"&gt;
      &lt;input type="checkbox" id="resampleOn"&gt;
      &lt;label for="resampleOn" style="margin:0;"&gt;Resample to even X steps&lt;/label&gt;
    &lt;/div&gt;
    &lt;div class="row" id="resampleRow" style="display:none;"&gt;
      &lt;div&gt;&lt;label&gt;Number of points&lt;/label&gt;&lt;input type="number" id="resampleN" value="200" min="2" max="5000"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="tabs"&gt;
      &lt;button id="tabTable" class="active"&gt;Table&lt;/button&gt;
      &lt;button id="tabCSV"&gt;CSV text&lt;/button&gt;
    &lt;/div&gt;
    &lt;div id="viewTable"&gt;&lt;div class="tablewrap"&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;x&lt;/th&gt;&lt;th id="thY"&gt;y&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody id="tableBody"&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div id="viewCSV" style="display:none;"&gt;&lt;textarea id="csvText" readonly placeholder="Extract a curve to see data here."&gt;&lt;/textarea&gt;&lt;/div&gt;
    &lt;div class="flexgap"&gt;
      &lt;button id="btnCopy"&gt;Copy&lt;/button&gt;
      &lt;button id="btnDownload" class="primary"&gt;Download CSV&lt;/button&gt;
    &lt;/div&gt;
    &lt;label style="margin-top:12px;"&gt;Combine series into one file&lt;/label&gt;
    &lt;div class="flexgap" style="margin-top:0;"&gt;
      &lt;button id="btnDownloadAll"&gt;Download all (shared X grid)&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;

&lt;script&gt;
(() =&gt; {
  const $ = id =&gt; document.getElementById(id);
  const canvas = $('canvas'), ctx = canvas.getContext('2d', {willReadFrequently:true});
  const readout = $('readout');
  const canvasInner = $('canvasInner');

  let img = null, imgData = null;
  let calib = { x: [], y: [], xLog:false, yLog:false };
  let mode = null; // 'calibX' | 'calibY' | 'pickColor' | 'setRegion'
  let pendingCalibAxis = null;
  let series = []; // {name, color:, tolerance, points:}
  let activeSeries = null;
  let region = null; // {x0,x1,y0,y1} in pixel coords, or null = whole image
  let regionClickBuffer = [];

  // ---------- image loading ----------
  const dropZone = $('dropZone'), fileInput = $('fileInput');
  dropZone.addEventListener('click', () =&gt; fileInput.click());
  dropZone.addEventListener('dragover', e =&gt; { e.preventDefault(); dropZone.style.borderColor = 'var(--accent)'; });
  dropZone.addEventListener('dragleave', () =&gt; dropZone.style.borderColor = 'var(--line)');
  dropZone.addEventListener('drop', e =&gt; {
    e.preventDefault(); dropZone.style.borderColor = 'var(--line)';
    if (e.dataTransfer.files.length) loadFile(e.dataTransfer.files);
  });
  fileInput.addEventListener('change', e =&gt; { if (e.target.files.length) loadFile(e.target.files); });

  function loadFile(file) {
    const reader = new FileReader();
    reader.onload = ev =&gt; {
      const im = new Image();
      im.onload = () =&gt; {
        const MAXW = 1800;
        let w = im.naturalWidth, h = im.naturalHeight;
        if (w &gt; MAXW) { h = Math.round(h * MAXW / w); w = MAXW; }
        canvas.width = w; canvas.height = h;
        ctx.drawImage(im, 0, 0, w, h);
        img = im;
        imgData = ctx.getImageData(0, 0, w, h);
        dropZone.textContent = file.name + ' — loaded (' + w + '×' + h + 'px). Drop another to replace.';
        $('btnCalibX').disabled = false;
        $('btnCalibY').disabled = false;
        updateStepStates();
      };
      im.src = ev.target.result;
    };
    reader.readAsDataURL(file);
  }

  // ---------- coordinate helpers ----------
  function canvasCoords(evt) {
    const r = canvas.getBoundingClientRect();
    const scaleX = canvas.width / r.width, scaleY = canvas.height / r.height;
    return {
      px: (evt.clientX - r.left) * scaleX,
      py: (evt.clientY - r.top) * scaleY
    };
  }

  function mapAxis(pVal, p1, p2, log) {
    // linear/log interpolation given two calibration points {px, val}
    if (log) {
      const l1 = Math.log10(p1.val), l2 = Math.log10(p2.val);
      const l = l1 + (pVal - p1.px) * (l2 - l1) / (p2.px - p1.px);
      return Math.pow(10, l);
    }
    return p1.val + (pVal - p1.px) * (p2.val - p1.val) / (p2.px - p1.px);
  }

  function pixelToData(px, py) {
    if (calib.x.length &lt; 2 || calib.y.length &lt; 2) return null;
    const dataX = mapAxis(px, calib.x, calib.x, xLogOn());
    const dataY = mapAxis(py, calib.y, calib.y, yLogOn());
    return { dataX, dataY };
  }
  function xLogOn(){ return $('xLog').checked; }
  function yLogOn(){ return $('yLog').checked; }

  // ---------- mouse readout ----------
  canvas.addEventListener('mousemove', evt =&gt; {
    if (!img) return;
    const { px, py } = canvasCoords(evt);
    const r = canvas.getBoundingClientRect();
    readout.style.left = (evt.clientX - r.left) + 'px';
    readout.style.top = (evt.clientY - r.top) + 'px';
    readout.style.display = 'block';
    let text = 'px ' + Math.round(px) + ', ' + Math.round(py);
    if (calib.x.length === 2 &amp;&amp; calib.y.length === 2) {
      const d = pixelToData(px, py);
      if (d) text += '  ?  x=' + fmt(d.dataX) + ', y=' + fmt(d.dataY);
    }
    readout.textContent = text;
  });
  canvas.addEventListener('mouseleave', () =&gt; readout.style.display = 'none');

  function fmt(n){ return Math.abs(n) &gt;= 1000 ? n.toFixed(0) : n.toPrecision(4).replace(/\.?0+$/,''); }

  // ---------- calibration flow ----------
  $('btnCalibX').addEventListener('click', () =&gt; startCalib('x'));
  $('btnCalibY').addEventListener('click', () =&gt; startCalib('y'));

  function startCalib(axis) {
    calib = [];
    mode = 'calib' + axis.toUpperCase();
    pendingCalibAxis = axis;
    statusFor(axis).textContent = 'Click point 1 on the ' + axis.toUpperCase() + ' axis…';
    setButtonsBusy(true);
  }

  function statusFor(axis){ return axis === 'x' ? $('xStatus') : $('yStatus'); }

  canvas.addEventListener('click', evt =&gt; {
    if (!img) return;
    const { px, py } = canvasCoords(evt);

    if (mode === 'calibX' || mode === 'calibY') {
      const axis = pendingCalibAxis;
      askValue(axis === 'x' ? px : py, pt =&gt; {
        calib.push({ px: axis === 'x' ? px : py, val: pt });
        drawMarker(px, py, '#e8a33d');
        if (calib.length === 1) {
          statusFor(axis).textContent = 'Point 1 set. Click point 2 on the ' + axis.toUpperCase() + ' axis…';
        } else {
          statusFor(axis).textContent = axis.toUpperCase() + ' calibrated: ' +
            calib.val + ' ? ' + calib.val;
          mode = null; setButtonsBusy(false);
          updateStepStates();
        }
      });
      return;
    }

    if (mode === 'setRegion') {
      regionClickBuffer.push({ px, py });
      drawMarker(px, py, '#5fd3c4');
      if (regionClickBuffer.length === 1) {
        $('regionStatus').textContent = 'Top-left set. Click the bottom-right corner of the plot area…';
      } else {
        const  = regionClickBuffer;
        region = {
          x0: Math.min(a.px, b.px), x1: Math.max(a.px, b.px),
          y0: Math.min(a.py, b.py), y1: Math.max(a.py, b.py)
        };
        $('regionStatus').textContent = 'Region set: x ' + Math.round(region.x0) + '–' + Math.round(region.x1) +
          ', y ' + Math.round(region.y0) + '–' + Math.round(region.y1) + '. Extraction will ignore everything outside this box.';
        mode = null;
      }
      return;
    }

    if (mode === 'pickColor') {
      const idx = (Math.floor(py) * canvas.width + Math.floor(px)) * 4;
      const rC = imgData.data, g = imgData.data, b = imgData.data;
      activeSeries.color = ;
      $('colorPreview').innerHTML = '&lt;span class="swatch" style="background:rgb(' + rC + ',' + g + ',' + b + ')"&gt;&lt;/span&gt;sampled rgb(' + rC + ', ' + g + ', ' + b + ')';
      mode = null;
      $('btnExtract').disabled = false;
      redrawBase();
    }
  });

  // inline value-entry (no blocking dialogs — sandboxed iframes can block those)
  function askValue(pixelPos, onDone) {
    const panel = document.createElement('div');
    panel.style.cssText = 'margin-top:6px;display:flex;gap:6px;';
    panel.innerHTML = '&lt;input type="text" id="__valInput" placeholder="value at this point" style="flex:1;"&gt;&lt;button id="__valOk" class="small primary"&gt;Set&lt;/button&gt;';
    const host = mode === 'calibX' ? $('xStatus') : $('yStatus');
    host.after(panel);
    const input = panel.querySelector('#__valInput');
    input.focus();
    function commit() {
      const v = parseFloat(input.value);
      panel.remove();
      if (!isNaN(v)) onDone(v);
    }
    panel.querySelector('#__valOk').addEventListener('click', commit);
    input.addEventListener('keydown', e =&gt; { if (e.key === 'Enter') commit(); });
  }

  function setButtonsBusy(busy) {
    $('btnCalibX').disabled = busy;
    $('btnCalibY').disabled = busy;
  }

  function drawMarker(px, py, color) {
    ctx.save();
    ctx.strokeStyle = color; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI*2); ctx.stroke();
    ctx.restore();
  }

  function redrawBase() {
    if (!img) return;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  }

  // ---------- series ----------
  $('btnPickColor').addEventListener('click', () =&gt; {
    const name = $('seriesName').value.trim() || ('Series ' + (series.length + 1));
    activeSeries = { name, color: null, tolerance: parseInt($('tolerance').value,10), points: [] };
    mode = 'pickColor';
    $('colorPreview').textContent = 'Click on the curve in the image…';
  });

  $('btnSetRegion').addEventListener('click', () =&gt; {
    regionClickBuffer = [];
    mode = 'setRegion';
    $('regionStatus').textContent = 'Click the top-left corner of the plot area…';
  });

  $('btnClearRegion').addEventListener('click', () =&gt; {
    region = null;
    regionClickBuffer = [];
    mode = null;
    $('regionStatus').textContent = 'Not set — will scan the whole image. Set this if legend text, axis labels, or anything else shares the curve\'s color.';
  });

  $('tolerance').addEventListener('input', e =&gt; { $('tolVal').textContent = e.target.value; if (activeSeries) activeSeries.tolerance = parseInt(e.target.value,10); });

  $('btnExtract').addEventListener('click', () =&gt; {
    if (!activeSeries || !activeSeries.color || calib.x.length &lt; 2 || calib.y.length &lt; 2) return;
    const  = activeSeries.color;
    const baseTol = activeSeries.tolerance;
    const w = canvas.width, h = canvas.height;
    const data = imgData.data;
    // hard-clamp to the pixel range you calibrated — those ARE the graph's real limits,
    // so we never scan (or report data from) outside them. A region box can only narrow this further.
    const calibXMin = Math.min(calib.x.px, calib.x.px), calibXMax = Math.max(calib.x.px, calib.x.px);
    const calibYMin = Math.min(calib.y.px, calib.y.px), calibYMax = Math.max(calib.y.px, calib.y.px);
    let xStart = Math.round(calibXMin), xEnd = Math.round(calibXMax);
    let yStart = Math.round(calibYMin), yEnd = Math.round(calibYMax);
    if (region) {
      xStart = Math.max(xStart, Math.floor(region.x0)); xEnd = Math.min(xEnd, Math.ceil(region.x1));
      yStart = Math.max(yStart, Math.floor(region.y0)); yEnd = Math.min(yEnd, Math.ceil(region.y1));
    }
    xStart = Math.max(0, xStart); xEnd = Math.min(w - 1, xEnd);
    yStart = Math.max(0, yStart); yEnd = Math.min(h - 1, yEnd);

    function matchesAt(x, tol) {
      const out = [];
      for (let y = yStart; y &lt;= yEnd; y++) {
        const idx = (y * w + x) * 4;
        const dr = data-cr, dg = data-cg, db = data-cb;
        if (Math.sqrt(dr*dr+dg*dg+db*db) &lt;= tol) out.push(y);
      }
      return out;
    }

    function runsOf(matches) {
      const runs = [];
      if (!matches.length) return runs;
      let runStart = matches, prev = matches;
      for (let i = 1; i &lt;= matches.length; i++) {
        const y = matches;
        if (y === undefined || y - prev &gt; 2) {
          runs.push({ centroid: (runStart + prev) / 2, size: prev - runStart + 1 });
          if (y !== undefined) runStart = y;
        }
        prev = y;
      }
      return runs;
    }

    const pts = [];
    let lastPy = null;
    let stretchedColumns = 0, deadColumns = [];
    const TOL_STEPS = ; // multipliers tried in order until a match is found

    for (let x = xStart; x &lt;= xEnd; x++) {
      let matches = [], usedTol = baseTol;
      for (const mult of TOL_STEPS) {
        usedTol = baseTol * mult;
        matches = matchesAt(x, usedTol);
        if (matches.length) { if (mult &gt; 1) stretchedColumns++; break; }
      }
      if (!matches.length) { deadColumns.push(x); continue; }

      const runs = runsOf(matches);
      let chosen;
      if (lastPy === null) {
        chosen = runs.reduce((a, b) =&gt; (b.size &gt; a.size ? b : a), runs);
      } else {
        chosen = runs.reduce((a, b) =&gt;
          Math.abs(b.centroid - lastPy) &lt; Math.abs(a.centroid - lastPy) ? b : a, runs);
      }
      lastPy = chosen.centroid;
      const d = pixelToData(x, chosen.centroid);
      pts.push({ px: x, dataX: d.dataX, dataY: d.dataY });
    }
    activeSeries.points = pts.sort((a,b) =&gt; a.dataX - b.dataX);
    series.push(activeSeries);

    // auto-fill short gaps (a handful of pixels — typically where another curve/gridline
    // crosses this one and briefly steals the match) by interpolating; leave long gaps alone
    const FILL_MAX_PX = 8;
    const ptsByPx = activeSeries.points.slice().sort((a,b) =&gt; a.px - b.px);
    const filled = [];
    let filledGapCount = 0;
    for (let i = 0; i &lt; ptsByPx.length; i++) {
      filled.push(ptsByPx);
      if (i &lt; ptsByPx.length - 1) {
        const gap = ptsByPx.px - ptsByPx.px;
        if (gap &gt; 1 &amp;&amp; gap &lt;= FILL_MAX_PX + 1) {
          filledGapCount++;
          for (let px = ptsByPx.px + 1; px &lt; ptsByPx.px; px++) {
            const t = (px - ptsByPx.px) / gap;
            filled.push({
              px,
              dataX: ptsByPx.dataX + t * (ptsByPx.dataX - ptsByPx.dataX),
              dataY: ptsByPx.dataY + t * (ptsByPx.dataY - ptsByPx.dataY),
              interpolated: true
            });
          }
        }
      }
    }
    activeSeries.points = filled.sort((a,b) =&gt; a.dataX - b.dataX);

    // report exactly what was and wasn't covered, in data units, so gaps are diagnosable
    const scannedCols = xEnd - xStart + 1;
    const fullRangeD = pixelToData(xStart, yStart), fullRangeD2 = pixelToData(xEnd, yStart);
    let note = pts.length + ' of ' + scannedCols + ' scanned columns matched directly (' + stretchedColumns + ' needed relaxed tolerance), plus ' +
      filledGapCount + ' short gap(s) auto-filled by interpolation (likely line crossings).';
    if (pts.length) {
      const xs = pts.map(p =&gt; p.dataX);
      note += ' Data spans x=' + fmt(Math.min(...xs)) + ' to x=' + fmt(Math.max(...xs)) +
        '. Scan was clamped to your calibrated range, x=' + fmt(Math.min(fullRangeD.dataX, fullRangeD2.dataX)) + ' to x=' + fmt(Math.max(fullRangeD.dataX, fullRangeD2.dataX)) + '.';
    }
    // only report gaps too wide to have been auto-filled above
    const longDeadColumns = [];
    {
      let gs = null, gp = null;
      const flushIfLong = () =&gt; { if (gs !== null &amp;&amp; (gp - gs) &gt; FILL_MAX_PX) longDeadColumns.push(); };
      for (const c of deadColumns) {
        if (gs === null) { gs = c; gp = c; }
        else if (c - gp &lt;= 1) { gp = c; }
        else { flushIfLong(); gs = c; gp = c; }
      }
      flushIfLong();
    }
    if (longDeadColumns.length) {
      const gapText = longDeadColumns.slice(0, 6).map(() =&gt; {
        const da = pixelToData(a, yStart).dataX, db = pixelToData(b, yStart).dataX;
        return fmt(Math.min(da,db)) + '–' + fmt(Math.max(da,db));
      }).join(', ');
      note += ' No match (even relaxed) over a stretch wider than a crossing, in: ' + gapText +
        (longDeadColumns.length &gt; 6 ? ', +' + (longDeadColumns.length-6) + ' more' : '') +
        ' — likely the line is genuinely a different color there, or absent from the source.';
    }
    $('extractNote').textContent = note;
    renderSeriesList();
    renderOutput();
    activeSeries = null;
    $('btnExtract').disabled = true;
    $('seriesName').value = '';
    $('colorPreview').textContent = '';
  });

  function renderSeriesList() {
    const host = $('seriesList');
    if (!series.length) { host.innerHTML = '&lt;div class="empty"&gt;None yet&lt;/div&gt;'; return; }
    host.innerHTML = '';
    series.forEach((s, i) =&gt; {
      const div = document.createElement('div');
      div.className = 'series-item';
      div.innerHTML = '&lt;span class="swatch" style="background:rgb(' + s.color.join(',') + ')"&gt;&lt;/span&gt;' +
        '&lt;span class="series-name"&gt;' + s.name + '&lt;/span&gt;' +
        '&lt;div class="series-meta"&gt;' + s.points.length + ' points · tolerance ' + s.tolerance + '&lt;/div&gt;' +
        '&lt;div class="flexgap"&gt;&lt;button class="small" data-act="view"&gt;View&lt;/button&gt;&lt;button class="small" data-act="remove"&gt;Remove&lt;/button&gt;&lt;/div&gt;';
      div.querySelector('').addEventListener('click', () =&gt; { renderOutput(i); });
      div.querySelector('').addEventListener('click', () =&gt; { series.splice(i,1); renderSeriesList(); renderOutput(); });
      host.appendChild(div);
    });
  }

  // ---------- output ----------
  $('tabTable').addEventListener('click', () =&gt; switchTab('table'));
  $('tabCSV').addEventListener('click', () =&gt; switchTab('csv'));
  function switchTab(which) {
    $('tabTable').classList.toggle('active', which==='table');
    $('tabCSV').classList.toggle('active', which==='csv');
    $('viewTable').style.display = which==='table' ? '' : 'none';
    $('viewCSV').style.display = which==='csv' ? '' : 'none';
  }
  $('resampleOn').addEventListener('change', e =&gt; { $('resampleRow').style.display = e.target.checked ? '' : 'none'; renderOutput(currentViewIdx); });
  $('resampleN').addEventListener('change', () =&gt; renderOutput(currentViewIdx));
  $('xLabel').addEventListener('input', () =&gt; renderOutput(currentViewIdx));

  let currentViewIdx = 0;
  function renderOutput(idx) {
    if (idx !== undefined) currentViewIdx = idx;
    if (!series.length) {
      $('tableBody').innerHTML = '';
      $('csvText').value = '';
      $('thY').textContent = 'y';
      return;
    }
    const s = series;
    $('thY').textContent = s.name;
    let pts = s.points;
    if ($('resampleOn').checked) pts = resample(pts, parseInt($('resampleN').value,10) || 200);

    const xLabel = $('xLabel').value || 'x';
    let csv = xLabel + ',' + s.name + '\n';
    let rows = '';
    pts.forEach(p =&gt; {
      csv += p.dataX.toPrecision(6) + ',' + p.dataY.toPrecision(6) + '\n';
      rows += '&lt;tr&gt;&lt;td&gt;' + fmt(p.dataX) + '&lt;/td&gt;&lt;td&gt;' + fmt(p.dataY) + '&lt;/td&gt;&lt;/tr&gt;';
    });
    $('tableBody').innerHTML = rows;
    $('csvText').value = csv;
  }

  function resample(pts, n) {
    if (pts.length &lt; 2) return pts;
    const xs = pts.map(p =&gt; p.dataX);
    const xMin = Math.min(...xs), xMax = Math.max(...xs);
    const out = [];
    for (let i = 0; i &lt; n; i++) {
      const xt = xMin + (xMax - xMin) * i / (n - 1);
      out.push({ dataX: xt, dataY: interpAt(pts, xt) });
    }
    return out;
  }
  function interpAt(pts, xt) {
    for (let i = 0; i &lt; pts.length - 1; i++) {
      const a = pts, b = pts;
      if ((xt &gt;= a.dataX &amp;&amp; xt &lt;= b.dataX) || (xt &lt;= a.dataX &amp;&amp; xt &gt;= b.dataX)) {
        const t = (xt - a.dataX) / (b.dataX - a.dataX || 1);
        return a.dataY + t * (b.dataY - a.dataY);
      }
    }
    return pts.dataY;
  }

  $('btnCopy').addEventListener('click', () =&gt; {
    $('csvText').select();
    navigator.clipboard.writeText($('csvText').value).catch(()=&gt;{ document.execCommand('copy'); });
  });
  $('btnDownload').addEventListener('click', () =&gt; {
    if (!series.length) return;
    const s = series;
    downloadText($('csvText').value, (s.name || 'series').replace(/\s+/g,'_') + '.csv');
  });
  $('btnDownloadAll').addEventListener('click', () =&gt; {
    if (!series.length) return;
    const useResample = $('resampleOn').checked;
    const n = parseInt($('resampleN').value,10) || 200;
    const allX = series.flatMap(s =&gt; s.points.map(p =&gt; p.dataX));
    const xMin = Math.min(...allX), xMax = Math.max(...allX);
    const count = useResample ? n : 200;
    const xLabel = $('xLabel').value || 'x';
    let header = xLabel + ',' + series.map(s =&gt; s.name).join(',');
    let csv = header + '\n';
    for (let i = 0; i &lt; count; i++) {
      const xt = xMin + (xMax - xMin) * i / (count - 1);
      const row = ;
      series.forEach(s =&gt; row.push(interpAt(s.points, xt).toPrecision(6)));
      csv += row.join(',') + '\n';
    }
    downloadText(csv, 'combined_series.csv');
  });

  function downloadText(text, filename) {
    const blob = new Blob(, {type:'text/csv'});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    URL.revokeObjectURL(a.href);
  }

  function updateStepStates() {
    $('step-load').classList.toggle('done', !!img);
    $('step-x').classList.toggle('active', !!img &amp;&amp; calib.x.length &lt; 2);
    $('step-x').classList.toggle('done', calib.x.length === 2);
    $('step-y').classList.toggle('active', calib.x.length === 2 &amp;&amp; calib.y.length &lt; 2);
    $('step-y').classList.toggle('done', calib.y.length === 2);
    $('step-series').classList.toggle('active', calib.x.length === 2 &amp;&amp; calib.y.length === 2);
    $('btnPickColor').disabled = !(calib.x.length === 2 &amp;&amp; calib.y.length === 2);
    $('btnSetRegion').disabled = !(calib.x.length === 2 &amp;&amp; calib.y.length === 2);
  }
})();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>RayTomes</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/graph-digitizer/</guid>
                    </item>
				                    <item>
                        <title>Kotov&#039;s Method App</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/kotovs-method-app/</link>
                        <pubDate>Sun, 30 Aug 2026 07:26:06 +0000</pubDate>
                        <description><![CDATA[The following is an app for calculating Kotov&#039; Method for communality of a set of values. You just load it into a browser and then past or type in the list of values to the window, select th...]]></description>
                        <content:encoded><![CDATA[<p>The following is an app for calculating Kotov' Method for communality of a set of values. You just load it into a browser and then past or type in the list of values to the window, select the range over which you want the spectrum and press compute spectrum. It is preloaded with the Planets distance from the Sun for you to see the sort of results that you can expect.</p>
<p>In my research this is a method that I have used very extensively with great results. When I ran this on the radius of black holes recently (there is much better data now than when I wrote on it in my book)  it showed that exactly the same two big peaks occurred in black holes as in the solar system. Quite clearly the same waves pervade the entire Universe (as black holes are at the centre of most or all galaxies).</p>
<p>I cannot recommend this technique too highly. Thank you Kotov who sadly died late last year. He also discovered the 160 minute cycle in the Sun, which wikipedia erroneously says is not real. He showed that the 160 minute cycle is present in many classes of objects - binaries, asteroid rotations, etc as well as the planets.</p>
<pre contenteditable="false">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;title&gt;Kotov's Method v2 — Black Hole Event Horizon Radii&lt;/title&gt;
&lt;style&gt;
  body{background:#fff; color:#1c1c18; margin:0; padding:24px;}
  .kwc-wrap{--paper:transparent; --ink:#1c1c18; --rule:#ddd; --accent:#2f6f66; --accent-soft:rgba(47,111,102,0.12); --muted:#666;
    font-family: Georgia, 'Iowan Old Style', 'Palatino Linotype', serif; color:var(--ink); max-width:1100px; margin:0 auto;}
  .kwc-wrap .mono{font-family:'IBM Plex Mono','SFMono-Regular',Menlo,Consolas,monospace;}
  .kwc-wrap h1{margin:0 0 4px 0; font-size:1.3rem; font-weight:600;}
  .kwc-wrap &gt; p{margin:0 0 16px 0; color:var(--muted); font-size:.88rem; max-width:70ch;}
  .kwc-layout{display:grid; grid-template-columns: 300px 1fr; gap:20px;}
  @media (max-width:820px){ .kwc-layout{grid-template-columns:1fr;} }
  .kwc-panel{padding-right:16px; border-right:1px solid var(--rule);}
  @media (max-width:820px){ .kwc-panel{border-right:none; border-bottom:1px solid var(--rule); padding-bottom:16px;} }
  .kwc-field{margin-bottom:14px;}
  .kwc-field label{display:block; font-size:.72rem; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:5px;}
  .kwc-wrap textarea, .kwc-wrap input, .kwc-wrap input{
    width:100%; font-family:'IBM Plex Mono','SFMono-Regular',Menlo,Consolas,monospace;
    font-size:.78rem; padding:6px 8px; border:1px solid var(--rule); border-radius:3px;
    background:#fafafa; color:var(--ink); box-sizing:border-box;
  }
  .kwc-wrap textarea{height:280px; resize:vertical; line-height:1.4;}
  .kwc-row{display:flex; gap:8px;}
  .kwc-row .kwc-field{flex:1;}
  .kwc-wrap fieldset{border:1px solid var(--rule); border-radius:4px; padding:8px 10px; margin:0 0 14px 0;}
  .kwc-wrap legend{font-size:.72rem; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); padding:0 5px;}
  .kwc-radiorow{display:flex; gap:12px; align-items:center; font-size:.82rem;}
  .kwc-radiorow label{display:flex; align-items:center; gap:5px; color:var(--ink); text-transform:none; letter-spacing:0;}
  .kwc-wrap button{font-family:inherit; font-size:.85rem; padding:8px 14px; border-radius:3px; border:1px solid var(--ink); cursor:pointer; background:#fff;}
  .kwc-wrap button:hover{background:#f0f0f0;}
  .kwc-btnrow{display:flex; gap:8px; flex-wrap:wrap; margin-top:4px;}
  .kwc-plotwrap{border:1px solid var(--rule); padding:16px; border-radius:4px;}
  .kwc-plottitle{font-size:.88rem; margin:0 0 10px 0;}
  #kwc-plot{width:100%; height:300px; display:block;}
  .kwc-axislabel{text-align:center; font-size:.72rem; color:var(--muted); margin-top:2px;}
  .kwc-wrap table{width:100%; border-collapse:collapse; font-size:.82rem;}
  .kwc-wrap th,.kwc-wrap td{text-align:left; padding:5px 8px; border-bottom:1px solid var(--rule);}
  .kwc-wrap th{color:var(--muted); font-weight:600; text-transform:uppercase; font-size:.68rem;}
  .kwc-score-bar{display:inline-block; height:6px; background:var(--accent); border-radius:2px; vertical-align:middle; margin-right:6px;}
  .kwc-peaks{margin-top:18px;}
  .kwc-peaks h2{font-size:.88rem; margin:0 0 8px 0;}
  .kwc-note{font-size:.72rem; color:var(--muted); margin-top:8px; line-height:1.5;}
  .kwc-peakrow{cursor:pointer;}
  .kwc-peakrow:hover{background:#f5f5f5;}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="kwc-wrap"&gt;
&lt;h1&gt;Kotov's Method v2&lt;/h1&gt;
&lt;p&gt;Click any row in the peak table below to highlight that point on the plot and see the per-point fit breakdown.&lt;/p&gt;

&lt;div class="kwc-layout"&gt;
  &lt;div class="kwc-panel"&gt;
    &lt;div class="kwc-field"&gt;
      &lt;label for="kwc-values"&gt;Input values — one per line: value, optional ", weight"&lt;/label&gt;
      &lt;textarea id="kwc-values" class="mono"&gt;0.387, Mercury
0.723, Venus
1.000, Earth
1.524, Mars
5.203, Jupiter
9.537, Saturn
19.191, Uranus
30.069, Neptune
39.482, Pluto (dwarf planet)&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;div class="kwc-row"&gt;
      &lt;div class="kwc-field"&gt;&lt;label for="kwc-qmin"&gt;Range min&lt;/label&gt;&lt;input type="number" id="kwc-qmin" value="0.1" step="any"&gt;&lt;/div&gt;
      &lt;div class="kwc-field"&gt;&lt;label for="kwc-qmax"&gt;Range max&lt;/label&gt;&lt;input type="number" id="kwc-qmax" value="150" step="any"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="kwc-field"&gt;
      &lt;label for="kwc-stepsize"&gt;Coarse step size (% per step, log scale)&lt;/label&gt;
      &lt;input type="number" id="kwc-stepsize" value="0.1" step="any"&gt;
      &lt;div id="kwc-stepcount" class="mono" style="font-size:.68rem; color:var(--muted); margin-top:4px;"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;fieldset&gt;
      &lt;legend&gt;Error exponent&lt;/legend&gt;
      &lt;div class="kwc-radiorow"&gt;
        &lt;label&gt;&lt;input type="radio" name="kwc-power" value="1" checked&gt; 1&lt;/label&gt;
        &lt;label&gt;&lt;input type="radio" name="kwc-power" value="2"&gt; 2&lt;/label&gt;
        &lt;label&gt;&lt;input type="radio" name="kwc-power" value="3"&gt; 3&lt;/label&gt;
      &lt;/div&gt;
    &lt;/fieldset&gt;
    &lt;div class="kwc-btnrow"&gt;
      &lt;button id="kwc-run"&gt;Compute spectrum&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div&gt;
    &lt;div class="kwc-plotwrap"&gt;
      &lt;p class="kwc-plottitle" id="kwc-plottitle"&gt;Commensurability spectrum&lt;/p&gt;
      &lt;canvas id="kwc-plot"&gt;&lt;/canvas&gt;
      &lt;p class="kwc-axislabel"&gt;Trial quantum value (log scale)&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class="kwc-peaks"&gt;
      &lt;h2&gt;Strongest peaks (refined) — click a row to highlight on the plot&lt;/h2&gt;
      &lt;div class="kwc-radiorow" style="margin-bottom:8px;"&gt;
        &lt;label&gt;&lt;input type="radio" name="kwc-sortmode" value="strength"&gt; Strongest first&lt;/label&gt;
        &lt;label&gt;&lt;input type="radio" name="kwc-sortmode" value="numeric" checked&gt; Numerical order&lt;/label&gt;
      &lt;/div&gt;
      &lt;table&gt;
        &lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;Trial value&lt;/th&gt;&lt;th&gt;Relative strength&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
        &lt;tbody id="kwc-peaktable"&gt;&lt;/tbody&gt;
      &lt;/table&gt;
    &lt;/div&gt;
    &lt;div class="kwc-peaks" id="kwc-breakdown-section" style="display:none;"&gt;
      &lt;h2 id="kwc-breakdown-title"&gt;Fit breakdown&lt;/h2&gt;
      &lt;table&gt;
        &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Input value&lt;/th&gt;&lt;th&gt;Ratio&lt;/th&gt;&lt;th&gt;Nearest integer&lt;/th&gt;&lt;th&gt;Deviation&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
        &lt;tbody id="kwc-breakdowntable"&gt;&lt;/tbody&gt;
      &lt;/table&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
(function(){
let lastPlotQs=[], lastPlotScores=[], lastPeaks=[], highlightQ=null, lastVals=[], lastBaseline=null;

function parseValues(){
  const lines = document.getElementById('kwc-values').value.split(/\n/).map(s=&gt;s.trim()).filter(s=&gt;s.length&gt;0);
  const pairs = [];
  for(const line of lines){
    const parts = line.split(',').map(s=&gt;s.trim());
    const v = parseFloat(parts);
    if(isNaN(v)) continue;
    const label = parts.length&gt;1 ? parts.slice(1).join(', ') : null;
    pairs.push({v,w:1,label});
  }
  return pairs;
}

function scoreAt(q, vals, power){
  let err=0;
  for(const {v,w} of vals){
    const larger = q&gt;=v;
    const r = larger? q/v : v/q;
    const delta = Math.abs(r-Math.round(r));
    err += w*Math.pow(delta,power);
  }
  return 1/Math.max(err,1e-9);
}

function computeRandomBaseline(vals, qmin, qmax, power, nTrials){
  const logMin=Math.log(qmin), logMax=Math.log(qmax);
  const scores=[];
  for(let i=0;i&lt;nTrials;i++){
    const q = Math.exp(logMin + Math.random()*(logMax-logMin));
    scores.push(scoreAt(q, vals, power));
  }
  const mean = scores.reduce((a,b)=&gt;a+b,0)/scores.length;
  const variance = scores.reduce((a,b)=&gt;a+(b-mean)*(b-mean),0)/scores.length;
  const sd = Math.sqrt(variance);
  return {mean, sd};
}

function drawPlot(qs, scores, baseline){
  const canvas = document.getElementById('kwc-plot');
  const rect = canvas.getBoundingClientRect();
  const dpr = window.devicePixelRatio||1;
  canvas.width = rect.width*dpr; canvas.height = rect.height*dpr;
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr,dpr);
  const W=rect.width, H=rect.height;
  const padL=48, padR=10, padT=8, padB=22;
  const plotW=W-padL-padR, plotH=H-padT-padB;
  const logMin=Math.log10(qs), logMax=Math.log10(qs);
  const twoSD = baseline ? baseline.mean+2*baseline.sd : 0;
  const lowest=Math.min(...scores), highest=Math.max(Math.max(...scores), twoSD);
  const span=highest-lowest;
  const yMax=highest+span*0.08, yMin=Math.max(0, lowest-span*0.1);
  function xPix(q){ return padL + (Math.log10(q)-logMin)/(logMax-logMin)*plotW; }
  function yPix(s){ return padT + plotH - ((s-yMin)/(yMax-yMin))*plotH; }

  ctx.strokeStyle = 'rgba(128,128,128,0.25)';
  ctx.fillStyle = '#888';
  ctx.font='10px monospace'; ctx.textAlign='center';
  let decade=Math.floor(logMin);
  while(decade&lt;=Math.ceil(logMax)){
    for(const m of ){
      const val=m*Math.pow(10,decade);
      if(val&gt;=qs &amp;&amp; val&lt;=qs){
        const x=xPix(val);
        ctx.beginPath(); ctx.moveTo(x,padT); ctx.lineTo(x,padT+plotH); ctx.stroke();
        ctx.fillText(val&gt;=1?val.toFixed(0):val.toString(), x, padT+plotH+14);
      }
    }
    decade++;
  }

  if(baseline){
    ctx.textAlign='left'; ctx.font='9px monospace';
    ctx.strokeStyle = '#888'; ctx.lineWidth=1; ctx.setLineDash();
    let y = yPix(baseline.mean);
    ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+plotW,y); ctx.stroke();
    ctx.fillStyle='#888'; ctx.fillText('expected', padL+plotW-50, y-3);

    ctx.strokeStyle = '#b08a2f';
    y = yPix(twoSD);
    ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+plotW,y); ctx.stroke();
    ctx.fillStyle='#b08a2f'; ctx.fillText('2 s.d.', padL+plotW-40, y-3);
    ctx.setLineDash([]);
  }

  ctx.strokeStyle = '#1c1c18';
  ctx.lineWidth=1; ctx.beginPath();
  for(let i=0;i&lt;qs.length;i++){
    const x=xPix(qs), y=yPix(scores);
    if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
  }
  ctx.stroke();
  ctx.strokeRect(padL,padT,plotW,plotH);

  if(highlightQ!==null &amp;&amp; highlightQ&gt;=qs &amp;&amp; highlightQ&lt;=qs){
    const x = xPix(highlightQ);
    ctx.strokeStyle = '#c0392b';
    ctx.lineWidth = 1.5;
    ctx.setLineDash();
    ctx.beginPath(); ctx.moveTo(x,padT); ctx.lineTo(x,padT+plotH); ctx.stroke();
    ctx.setLineDash([]);
  }
}

function renderPeakTable(peaks){
  lastPeaks=peaks;
  const sortMode = document.querySelector('input:checked').value;
  const ordered = peaks.slice().sort((a,b)=&gt; sortMode==='numeric'? a.q-b.q : b.score-a.score);
  const tbody=document.getElementById('kwc-peaktable');
  tbody.innerHTML='';
  const maxScore = peaks.length? Math.max(...peaks.map(p=&gt;p.score)):1;
  ordered.forEach((p,idx)=&gt;{
    const tr=document.createElement('tr');
    tr.className='kwc-peakrow';
    const barW=Math.round((p.score/maxScore)*70);
    tr.innerHTML = `&lt;td&gt;${idx+1}&lt;/td&gt;&lt;td class="mono"&gt;${p.q.toPrecision(6)}&lt;/td&gt;
      &lt;td&gt;&lt;span class="kwc-score-bar" style="width:${barW}px"&gt;&lt;/span&gt;&lt;span class="mono"&gt;${p.score.toFixed(3)}&lt;/span&gt;&lt;/td&gt;`;
    tr.addEventListener('click', ()=&gt;{
      highlightQ=p.q; drawPlot(lastPlotQs,lastPlotScores,lastBaseline);
      showBreakdown(p.q);
    });
    tbody.appendChild(tr);
  });
}

document.querySelectorAll('input').forEach(el=&gt;{
  el.addEventListener('change', ()=&gt;{ if(lastPeaks.length) renderPeakTable(lastPeaks); });
});

function showBreakdown(q){
  const tbody = document.getElementById('kwc-breakdowntable');
  tbody.innerHTML='';
  const rows = lastVals.map((item,i)=&gt;{
    const larger = q&gt;=item.v;
    const r = larger? q/item.v : item.v/q;
    const nearest = Math.round(r);
    const dev = Math.abs(r-nearest);
    return {label: item.label || `${item.v}`, ratio:r, nearest, dev};
  });
  rows.sort((a,b)=&gt;a.dev-b.dev);
  for(const row of rows){
    const tr=document.createElement('tr');
    tr.innerHTML = `&lt;td&gt;${row.label}&lt;/td&gt;&lt;td class="mono"&gt;${row.ratio.toFixed(5)}&lt;/td&gt;&lt;td class="mono"&gt;${row.nearest}&lt;/td&gt;&lt;td class="mono"&gt;${row.dev.toFixed(5)}&lt;/td&gt;`;
    tbody.appendChild(tr);
  }
  document.getElementById('kwc-breakdown-title').textContent = `Fit breakdown for trial value ${q.toPrecision(6)}`;
  document.getElementById('kwc-breakdown-section').style.display='';
}

function run(){
  const vals = parseValues();
  if(vals.length===0){ return; }
  lastVals = vals;
  highlightQ=null;
  document.getElementById('kwc-breakdown-section').style.display='none';
  const qmin = parseFloat(document.getElementById('kwc-qmin').value);
  const qmax = parseFloat(document.getElementById('kwc-qmax').value);
  const stepPct = Math.max(0.001, parseFloat(document.getElementById('kwc-stepsize').value)||0.1);
  const power = parseFloat(document.querySelector('input:checked').value);
  const stepRatio = 1+stepPct/100;
  let n = Math.ceil(Math.log(qmax/qmin)/Math.log(stepRatio))+1;
  const capped = n&gt;200000;
  if(capped) n=200000;
  document.getElementById('kwc-stepcount').textContent = n.toLocaleString()+' coarse trial values'+(capped?' (capped)':'');

  const logMin=Math.log(qmin), logMax=Math.log(qmax);
  const coarseQs=[], coarseScores=[];
  for(let i=0;i&lt;n;i++){
    const q = Math.exp(logMin + (logMax-logMin)*i/(n-1));
    coarseQs.push(q); coarseScores.push(scoreAt(q, vals, power));
  }

  const candIdx=[];
  for(let i=1;i&lt;coarseScores.length-1;i++){
    if(coarseScores&gt;coarseScores &amp;&amp; coarseScores&gt;=coarseScores) candIdx.push(i);
  }
  candIdx.sort((a,b)=&gt;coarseScores-coarseScores);
  const picked=[];
  for(const i of candIdx){
    const q=coarseQs;
    if(picked.every(p=&gt; q &gt; p*(1+1/100) || q &lt; p/(1+1/100))) picked.push(q);
    if(picked.length&gt;=15) break;
  }

  const finePoints = [];
  const refinedPeaks = [];
  for(const q0 of picked){
    let best={q:q0, score:scoreAt(q0,vals,power)};
    const windowFactor = 1 + stepPct/100 * 3;
    const nFine = 400;
    const flogMin = Math.log(q0/windowFactor), flogMax = Math.log(q0*windowFactor);
    for(let j=0;j&lt;nFine;j++){
      const q = Math.exp(flogMin + (flogMax-flogMin)*j/(nFine-1));
      const s = scoreAt(q, vals, power);
      finePoints.push();
      if(s&gt;best.score) best={q,score:s};
    }
    refinedPeaks.push(best);
  }

  const merged = coarseQs.map((q,i)=&gt;[q,coarseScores]).concat(finePoints);
  merged.sort((a,b)=&gt;a-b);
  const plotQs = merged.map(p=&gt;p);
  const plotScores = merged.map(p=&gt;p);
  lastPlotQs=plotQs; lastPlotScores=plotScores;

  const uniqPeaks=[];
  refinedPeaks.sort((a,b)=&gt;b.score-a.score);
  for(const p of refinedPeaks){ if(uniqPeaks.every(u=&gt;p.q&gt;u.q*1.01||p.q&lt;u.q/1.01)) uniqPeaks.push(p); }

  lastBaseline = computeRandomBaseline(vals, qmin, qmax, power, 5000);

  drawPlot(plotQs, plotScores, lastBaseline);
  renderPeakTable(uniqPeaks);
  document.getElementById('kwc-plottitle').textContent = `Commensurability spectrum (adaptively refined) — p=${power}, range `;
}

document.getElementById('kwc-run').addEventListener('click', run);
window.addEventListener('resize', ()=&gt;{ if(lastPlotQs.length) drawPlot(lastPlotQs,lastPlotScores,lastBaseline); });
run();
})();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>RayTomes</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/kotovs-method-app/</guid>
                    </item>
				                    <item>
                        <title>How Accurately Can We Measure Cycle Periods?</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/how-accurately-can-we-measure-cycle-periods/</link>
                        <pubDate>Sat, 25 Jul 2026 04:44:39 +0000</pubDate>
                        <description><![CDATA[How Accurately Can We Measure Cycle Periods?
When a piece of cycles research reports a period like &quot;26.65 million years&quot; or &quot;3.41 years,&quot; there&#039;s an obvious follow-up question that too ofte...]]></description>
                        <content:encoded><![CDATA[<h1>How Accurately Can We Measure Cycle Periods?</h1>
<p>When a piece of cycles research reports a period like "26.65 million years" or "3.41 years," there's an obvious follow-up question that too often goes unanswered: <em>how precisely is that actually known?</em> A number without a stated precision invites two opposite mistakes — dismissing a real match because it "isn't exact," or accepting a false match because it "looks close." Both are avoidable with a simple rule.</p>
<h2>The rule</h2>
<p><strong>Accuracy = cycle period ÷ (10 × number of full cycles present in the data)</strong></p>
<p>The "number of full cycles present" is just the total length of the data divided by the candidate period. So a longer data span, or a shorter candidate period, both mean more repeats of the cycle are on record — and the more repeats you have, the tighter the accuracy the rule assigns.</p>
<h2>A necessary condition: the method has to allow a fractional number of cycles</h2>
<p>The rule only holds for spectral methods that let a trial period fit a <strong>non-integer</strong> number of cycles into the data window — techniques in the least-squares spectral analysis family, which search continuously across frequency rather than only at fixed grid points. </p>
<p><span style="font-size: 12pt">CATS software does do fractional numbers of cycles in the data, down to 0.01 variations which is more than adequate for getting within 0.1 cycles.</span></p>
<p>An ordinary DFT (or the FFT that computes it efficiently) doesn't have this freedom: it's restricted to a fixed harmonic grid, which effectively forces a whole number of cycles to fit exactly into the data span. That constraint alone costs a factor of about ten in the achievable precision. So the rule above assumes a fitting method that isn't hamstrung this way — with a plain FFT, the same data yields roughly ten times the uncertainty.</p>
<h2>Why it works</h2>
<p>The intuition is straightforward: each time a cycle completes another full repeat within the data, that's another independent confirmation of exactly where its peaks and troughs fall. Fitting the centre of a known, clean waveform shape against many repeats pins down its period far more tightly than a single repeat could, because the errors in locating each individual peak partly cancel out across the whole set rather than adding up.</p>
<p>The factor of 10 in the rule isn't a guess — it was established empirically, by testing the method on monthly commodity price data containing cycles of known, exactly-verifiable length: 12, 6, 4, 3, and 2.4 months, all arising directly from the calendar seasons. Because the true periods in that test case were already certain, the actual precision the method achieved could be measured directly against them, and a consistent ×10 improvement over the naive resolution limit held up.</p>
<p>Applied to the recent factor-analysis results from 281 years of spliced European commodity price data, the rule reproduces the exact stated precision for each cycle found:</p>
<table>
<thead>
<tr>
<th>Cycle (years)</th>
<th>Cycles present (281 ÷ period)</th>
<th>Rule's stated accuracy</th>
</tr>
</thead>
<tbody>
<tr>
<td>2.953</td>
<td>95.2</td>
<td>±0.003</td>
</tr>
<tr>
<td>3.411</td>
<td>82.4</td>
<td>±0.004</td>
</tr>
<tr>
<td>7.23</td>
<td>38.9</td>
<td>±0.02</td>
</tr>
<tr>
<td>10.96</td>
<td>25.6</td>
<td>±0.04</td>
</tr>
</tbody>
</table>
<p>Notice the pattern: shorter cycles, with many more repeats fitting into the same data span, get pinned down far more tightly than longer cycles with only a few repeats — exactly what the rule predicts.</p>
<h2>Why the distinction matters in practice</h2>
<p>Applying the rule honestly changes which apparent "matches" between a found cycle and a known reference cycle actually hold up.</p>
<p>A found period of 10.96 years, with a rule-derived accuracy of ±0.04, is a genuine match to the roughly 11-year sunspot cycle — the gap between them is well inside the stated uncertainty.</p>
<p>But a found period of 3.411 years, with a much tighter accuracy of ±0.004 owing to its many repeats in the data, turns out <strong>not</strong> to be a genuine match to the well-known 3.389-year (40-month) stock market cycle — the gap between them, though small in absolute terms, is more than five times the stated precision.</p>
<p>This is the central, sometimes uncomfortable lesson: a number that "looks close" is not the same as a number that matches within its properly calculated precision.</p>
<hr />
<h2>Appendix: how this relates to the uncertainty principle</h2>
<p>The rule above answers a different question from the one usually raised when "uncertainty" comes up in cycles work, and it's worth being precise about the difference.</p>
<p>There are really two separate questions:</p>
<ol>
<li><strong>How close together can two real periods be before we can no longer tell them apart at all?</strong> This is a hard limit set purely by how much data you have.</li>
<li><strong>Given that a single, clean cycle really is there, how precisely can we pin down its exact value?</strong> This is a much softer limit, set by the quality of the fit — and it's what the rule above answers.</li>
</ol>
<p>The first question is where a version of Heisenberg's uncertainty principle comes in. It can be rearranged from its usual energy-time form (ΔE·Δt ≥ a fixed constant) into a statement about cycles: (time uncertainty) × (period uncertainty) is bounded, where the time uncertainty is simply the length of the data. This is the classical time-bandwidth product (the Gabor limit) that falls out of the same Fourier mathematics behind the quantum relation — not because it's literally quantum mechanics, but because both are consequences of the same fact about Fourier-conjugate pairs. Converted to a period uncertainty, it becomes the standard Rayleigh frequency-resolution criterion from spectroscopy and signal processing: period² ÷ data-span — the naive baseline that the rule above improves on by roughly a factor of 10.</p>
<p>The reason the two questions have different answers is the same reason a telescope's diffraction limit sets how close two stars can appear before blurring into one, while the <em>position</em> of a single isolated star can still be measured to a small fraction of that same diffraction width, given a clean enough signal. The Fourier/Rayleigh limit sets the <em>width</em> you can't beat without more data; the ×10 rule is the empirically-measured <em>centroiding gain</em> available within that width when a single clean cycle is being fitted rather than two cycles being separated. Both trace back to the same underlying relation — one from the physics-textbook side, one rediscovered independently from the data-analysis side.</p>
<p>A side note on terminology: the general family of fractional-cycle-fitting methods described above is usually called <strong>Least-Squares Spectral Analysis (LSSA)</strong>, or the <strong>Vaníček method</strong> after its 1969 originator. The discussion here assumes evenly-spaced (regular) data throughout. For unevenly-spaced or gapped data, a related variant called the <strong>Date-Compensated Discrete Fourier Transform (DCDFT)</strong> — along with the closely related <strong>Lomb-Scargle periodogram</strong> — extends the same fractional-cycle fitting to irregular sampling, which is common in astronomy.</p>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>RayTomes</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/how-accurately-can-we-measure-cycle-periods/</guid>
                    </item>
				                    <item>
                        <title>Spectrum analyzer</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/spectrum-analyzer/</link>
                        <pubDate>Sat, 18 Jul 2026 05:20:09 +0000</pubDate>
                        <description><![CDATA[Spectrum analyzer
ArticleA spectrum analyser is an instrument that measures the magnitude or power of an input signal across a range of frequencies. It displays frequency on one axis and sig...]]></description>
                        <content:encoded><![CDATA[<h2>Spectrum analyzer</h2>
<h3>Article</h3><p>A spectrum analyser is an instrument that measures the magnitude or power of an input signal across a range of frequencies. It displays frequency on one axis and signal level on the other.</p>
<p>Analysers may sweep a tuned filter across the frequency range, use a bank of filters, or calculate spectra digitally with a fast Fourier transform. Real-time instruments can capture rapidly changing or intermittent signals.</p>
<p>Measurements include carrier frequency, harmonics, modulation products, bandwidth, noise, distortion, interference, and spurious emissions. Resolution bandwidth and video bandwidth affect the detail and smoothness of the display.</p>
<p>Spectrum analysers are widely used in radio, telecommunications, audio, electronics, electromagnetic compatibility testing, radar, satellite systems, and scientific instrumentation.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Spectrum_analyzer" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Spectrum_analyzer</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/spectrum-analyzer/</guid>
                    </item>
				                    <item>
                        <title>Spectroscopy</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/spectroscopy/</link>
                        <pubDate>Sat, 18 Jul 2026 05:20:08 +0000</pubDate>
                        <description><![CDATA[Spectroscopy
ArticleSpectroscopy is the study and measurement of interactions between radiation and matter as a function of wavelength, frequency, energy, or another spectral variable. It or...]]></description>
                        <content:encoded><![CDATA[<h2>Spectroscopy</h2>
<h3>Article</h3><p>Spectroscopy is the study and measurement of interactions between radiation and matter as a function of wavelength, frequency, energy, or another spectral variable. It originally referred mainly to visible light but now covers many forms of radiation and particles.</p>
<p>Spectra may be produced through absorption, emission, scattering, fluorescence, diffraction, or resonance. Characteristic features arise from transitions between atomic, molecular, electronic, vibrational, rotational, spin, or nuclear energy states.</p>
<p>Spectroscopy can identify substances and measure composition, structure, concentration, temperature, pressure, motion, fields, and reaction dynamics. Different methods are selected according to the energy scale and material being investigated.</p>
<p>It is a central method in chemistry, physics, astronomy, biology, medicine, environmental science, remote sensing, archaeology, and materials research.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Spectroscopy" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Spectroscopy</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/spectroscopy/</guid>
                    </item>
				                    <item>
                        <title>Spectrometer</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/spectrometer/</link>
                        <pubDate>Sat, 18 Jul 2026 05:18:23 +0000</pubDate>
                        <description><![CDATA[Spectrometer
ArticleA spectrometer is an instrument that separates and measures components of a physical spectrum. Most commonly it records the intensity of electromagnetic radiation as a fu...]]></description>
                        <content:encoded><![CDATA[<h2>Spectrometer</h2>
<h3>Article</h3><p>A spectrometer is an instrument that separates and measures components of a physical spectrum. Most commonly it records the intensity of electromagnetic radiation as a function of wavelength, frequency, or energy.</p>
<p>Optical spectrometers may use prisms, diffraction gratings, interferometers, filters, or detectors to separate radiation. Other instruments measure mass-to-charge ratio, particle energy, magnetic resonance, or acoustic frequency.</p>
<p>Important characteristics include spectral range, resolution, sensitivity, calibration, signal-to-noise ratio, and acquisition speed. The instrument&#039;s design depends on the type of radiation and the measurement required.</p>
<p>Spectrometers are used in laboratories, observatories, spacecraft, environmental monitoring, medicine, manufacturing, chemistry, physics, and materials analysis.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Spectrometer" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Spectrometer</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/spectrometer/</guid>
                    </item>
				                    <item>
                        <title>Spectrogram</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/spectrogram/</link>
                        <pubDate>Sat, 18 Jul 2026 05:18:22 +0000</pubDate>
                        <description><![CDATA[Spectrogram
ArticleA spectrogram is a visual representation of how the frequency content of a signal changes over time. Time is normally shown on the horizontal axis, frequency on the vertic...]]></description>
                        <content:encoded><![CDATA[<h2>Spectrogram</h2>
<h3>Article</h3><p>A spectrogram is a visual representation of how the frequency content of a signal changes over time. Time is normally shown on the horizontal axis, frequency on the vertical axis, and colour or brightness represents amplitude or power.</p>
<p>Spectrograms are commonly calculated by dividing a signal into overlapping time windows and applying a Fourier transform to each segment. This procedure is known as the short-time Fourier transform.</p>
<p>The choice of window length creates a trade-off between time and frequency resolution. Short windows locate rapid events more accurately, while long windows distinguish nearby frequencies more clearly.</p>
<p>Spectrograms are widely used for speech, music, animal calls, machinery, radar, sonar, seismology, astronomy, and biomedical signals.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Spectrogram" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Spectrogram</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/spectrogram/</guid>
                    </item>
				                    <item>
                        <title>Emission spectrum</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/emission-spectrum/</link>
                        <pubDate>Sat, 18 Jul 2026 05:18:20 +0000</pubDate>
                        <description><![CDATA[Emission spectrum
ArticleAn emission spectrum is the distribution of electromagnetic radiation emitted by a source across wavelength or frequency. It may contain discrete lines, broad bands,...]]></description>
                        <content:encoded><![CDATA[<h2>Emission spectrum</h2>
<h3>Article</h3><p>An emission spectrum is the distribution of electromagnetic radiation emitted by a source across wavelength or frequency. It may contain discrete lines, broad bands, a continuous background, or combinations of these forms.</p>
<p>Hot dense materials can produce nearly continuous thermal spectra, while low-density gases often emit characteristic spectral lines. Molecules may generate bands composed of many closely spaced rotational and vibrational transitions.</p>
<p>Emission spectra arise when excited atoms, ions, molecules, or solids release energy as radiation. The spectrum depends on the source&#039;s composition, temperature, density, pressure, and excitation mechanism.</p>
<p>Emission spectroscopy is used to identify substances and study flames, plasmas, lamps, stars, nebulae, planetary atmospheres, chemical reactions, and industrial processes.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Emission_spectrum" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Emission_spectrum</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/emission-spectrum/</guid>
                    </item>
				                    <item>
                        <title>Emission line</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/emission-line/</link>
                        <pubDate>Sat, 18 Jul 2026 05:18:19 +0000</pubDate>
                        <description><![CDATA[Emission line
ArticleAn emission line is a narrow feature in a spectrum produced when atoms, ions, molecules, or other systems release radiation at particular wavelengths. It commonly occurs...]]></description>
                        <content:encoded><![CDATA[<h2>Emission line</h2>
<h3>Article</h3><p>An emission line is a narrow feature in a spectrum produced when atoms, ions, molecules, or other systems release radiation at particular wavelengths. It commonly occurs when an excited state loses energy and transitions to a lower state.</p>
<p>The emitted photon has an energy equal to the difference between the two states. Because allowed energy levels are characteristic of each species, emission lines can be used as identifying fingerprints.</p>
<p>The line&#039;s intensity, width, shape, position, and splitting can reveal abundance, temperature, density, pressure, motion, magnetic fields, and interactions with surrounding matter.</p>
<p>Emission lines are observed in laboratory discharges, flames, lasers, planetary atmospheres, nebulae, stars, galaxies, and many other radiating systems.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Emission_line" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Emission_line</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/emission-line/</guid>
                    </item>
				                    <item>
                        <title>Electromagnetic spectroscopy</title>
                        <link>https://cyclesresearchinstitute.org/community/methods/electromagnetic-spectroscopy/</link>
                        <pubDate>Sat, 18 Jul 2026 05:18:17 +0000</pubDate>
                        <description><![CDATA[Electromagnetic spectroscopy
ArticleElectromagnetic spectroscopy studies the interaction of electromagnetic radiation with matter across regions ranging from radio waves to gamma rays. Diffe...]]></description>
                        <content:encoded><![CDATA[<h2>Electromagnetic spectroscopy</h2>
<h3>Article</h3><p>Electromagnetic spectroscopy studies the interaction of electromagnetic radiation with matter across regions ranging from radio waves to gamma rays. Different frequencies probe different forms of physical structure and energy transitions.</p>
<p>Radio and microwave spectroscopy can examine rotations, spins, and magnetic environments. Infrared spectroscopy commonly measures molecular vibrations, while visible and ultraviolet spectroscopy often probes electronic transitions.</p>
<p>X-ray and gamma-ray methods investigate inner electrons, nuclei, high-energy processes, and material composition. Spectra may be produced by absorption, emission, scattering, fluorescence, or resonance.</p>
<p>These methods are used to identify substances and determine composition, structure, temperature, motion, fields, and reaction conditions in chemistry, physics, astronomy, biology, medicine, and materials science.</p>
<hr><h3>Source details and credits</h3><ul><li><strong>Source / publisher:</strong> Wikipedia</li><li><strong>URL type:</strong> WWW</li><li><strong>Credits:</strong> Wikipedia</li><li><strong>URL:</strong> <a href="https://en.wikipedia.org/wiki/Electromagnetic_spectroscopy" rel="nofollow noopener" target="_blank">https://en.wikipedia.org/wiki/Electromagnetic_spectroscopy</a></li></ul>]]></content:encoded>
						                            <category domain="https://cyclesresearchinstitute.org/community/methods/">Methods</category>                        <dc:creator>CRI</dc:creator>
                        <guid isPermaLink="true">https://cyclesresearchinstitute.org/community/methods/electromagnetic-spectroscopy/</guid>
                    </item>
							        </channel>
        </rss>
		