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.
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).
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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kotov's Method v2 — Black Hole Event Horizon Radii</title>
<style>
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 > 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[type=text], .kwc-wrap input[type=number]{
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;}
</style>
</head>
<body>
<div class="kwc-wrap">
<h1>Kotov's Method v2</h1>
<p>Click any row in the peak table below to highlight that point on the plot and see the per-point fit breakdown.</p>
<div class="kwc-layout">
<div class="kwc-panel">
<div class="kwc-field">
<label for="kwc-values">Input values — one per line: value, optional ", weight"</label>
<textarea id="kwc-values" class="mono">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)</textarea>
</div>
<div class="kwc-row">
<div class="kwc-field"><label for="kwc-qmin">Range min</label><input type="number" id="kwc-qmin" value="0.1" step="any"></div>
<div class="kwc-field"><label for="kwc-qmax">Range max</label><input type="number" id="kwc-qmax" value="150" step="any"></div>
</div>
<div class="kwc-field">
<label for="kwc-stepsize">Coarse step size (% per step, log scale)</label>
<input type="number" id="kwc-stepsize" value="0.1" step="any">
<div id="kwc-stepcount" class="mono" style="font-size:.68rem; color:var(--muted); margin-top:4px;"></div>
</div>
<fieldset>
<legend>Error exponent</legend>
<div class="kwc-radiorow">
<label><input type="radio" name="kwc-power" value="1" checked> 1</label>
<label><input type="radio" name="kwc-power" value="2"> 2</label>
<label><input type="radio" name="kwc-power" value="3"> 3</label>
</div>
</fieldset>
<div class="kwc-btnrow">
<button id="kwc-run">Compute spectrum</button>
</div>
</div>
<div>
<div class="kwc-plotwrap">
<p class="kwc-plottitle" id="kwc-plottitle">Commensurability spectrum</p>
<canvas id="kwc-plot"></canvas>
<p class="kwc-axislabel">Trial quantum value (log scale)</p>
</div>
<div class="kwc-peaks">
<h2>Strongest peaks (refined) — click a row to highlight on the plot</h2>
<div class="kwc-radiorow" style="margin-bottom:8px;">
<label><input type="radio" name="kwc-sortmode" value="strength"> Strongest first</label>
<label><input type="radio" name="kwc-sortmode" value="numeric" checked> Numerical order</label>
</div>
<table>
<thead><tr><th>#</th><th>Trial value</th><th>Relative strength</th></tr></thead>
<tbody id="kwc-peaktable"></tbody>
</table>
</div>
<div class="kwc-peaks" id="kwc-breakdown-section" style="display:none;">
<h2 id="kwc-breakdown-title">Fit breakdown</h2>
<table>
<thead><tr><th>Input value</th><th>Ratio</th><th>Nearest integer</th><th>Deviation</th></tr></thead>
<tbody id="kwc-breakdowntable"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
(function(){
let lastPlotQs=[], lastPlotScores=[], lastPeaks=[], highlightQ=null, lastVals=[], lastBaseline=null;
function parseValues(){
const lines = document.getElementById('kwc-values').value.split(/\n/).map(s=>s.trim()).filter(s=>s.length>0);
const pairs = [];
for(const line of lines){
const parts = line.split(',').map(s=>s.trim());
const v = parseFloat(parts[0]);
if(isNaN(v)) continue;
const label = parts.length>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>=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<nTrials;i++){
const q = Math.exp(logMin + Math.random()*(logMax-logMin));
scores.push(scoreAt(q, vals, power));
}
const mean = scores.reduce((a,b)=>a+b,0)/scores.length;
const variance = scores.reduce((a,b)=>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[0]), logMax=Math.log10(qs[qs.length-1]);
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<=Math.ceil(logMax)){
for(const m of [1,2,5]){
const val=m*Math.pow(10,decade);
if(val>=qs[0] && val<=qs[qs.length-1]){
const x=xPix(val);
ctx.beginPath(); ctx.moveTo(x,padT); ctx.lineTo(x,padT+plotH); ctx.stroke();
ctx.fillText(val>=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([2,2]);
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<qs.length;i++){
const x=xPix(qs[i]), y=yPix(scores[i]);
if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.stroke();
ctx.strokeRect(padL,padT,plotW,plotH);
if(highlightQ!==null && highlightQ>=qs[0] && highlightQ<=qs[qs.length-1]){
const x = xPix(highlightQ);
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 1.5;
ctx.setLineDash([4,3]);
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[name=kwc-sortmode]:checked').value;
const ordered = peaks.slice().sort((a,b)=> 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=>p.score)):1;
ordered.forEach((p,idx)=>{
const tr=document.createElement('tr');
tr.className='kwc-peakrow';
const barW=Math.round((p.score/maxScore)*70);
tr.innerHTML = `<td>${idx+1}</td><td class="mono">${p.q.toPrecision(6)}</td>
<td><span class="kwc-score-bar" style="width:${barW}px"></span><span class="mono">${p.score.toFixed(3)}</span></td>`;
tr.addEventListener('click', ()=>{
highlightQ=p.q; drawPlot(lastPlotQs,lastPlotScores,lastBaseline);
showBreakdown(p.q);
});
tbody.appendChild(tr);
});
}
document.querySelectorAll('input[name=kwc-sortmode]').forEach(el=>{
el.addEventListener('change', ()=>{ if(lastPeaks.length) renderPeakTable(lastPeaks); });
});
function showBreakdown(q){
const tbody = document.getElementById('kwc-breakdowntable');
tbody.innerHTML='';
const rows = lastVals.map((item,i)=>{
const larger = q>=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)=>a.dev-b.dev);
for(const row of rows){
const tr=document.createElement('tr');
tr.innerHTML = `<td>${row.label}</td><td class="mono">${row.ratio.toFixed(5)}</td><td class="mono">${row.nearest}</td><td class="mono">${row.dev.toFixed(5)}</td>`;
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[name=kwc-power]:checked').value);
const stepRatio = 1+stepPct/100;
let n = Math.ceil(Math.log(qmax/qmin)/Math.log(stepRatio))+1;
const capped = n>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<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<coarseScores.length-1;i++){
if(coarseScores[i]>coarseScores[i-1] && coarseScores[i]>=coarseScores[i+1]) candIdx.push(i);
}
candIdx.sort((a,b)=>coarseScores[b]-coarseScores[a]);
const picked=[];
for(const i of candIdx){
const q=coarseQs[i];
if(picked.every(p=> q > p*(1+1/100) || q < p/(1+1/100))) picked.push(q);
if(picked.length>=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<nFine;j++){
const q = Math.exp(flogMin + (flogMax-flogMin)*j/(nFine-1));
const s = scoreAt(q, vals, power);
finePoints.push([q,s]);
if(s>best.score) best={q,score:s};
}
refinedPeaks.push(best);
}
const merged = coarseQs.map((q,i)=>[q,coarseScores[i]]).concat(finePoints);
merged.sort((a,b)=>a[0]-b[0]);
const plotQs = merged.map(p=>p[0]);
const plotScores = merged.map(p=>p[1]);
lastPlotQs=plotQs; lastPlotScores=plotScores;
const uniqPeaks=[];
refinedPeaks.sort((a,b)=>b.score-a.score);
for(const p of refinedPeaks){ if(uniqPeaks.every(u=>p.q>u.q*1.01||p.q<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 [${qmin}, ${qmax}]`;
}
document.getElementById('kwc-run').addEventListener('click', run);
window.addEventListener('resize', ()=>{ if(lastPlotQs.length) drawPlot(lastPlotQs,lastPlotScores,lastBaseline); });
run();
})();
</script>
</body>
</html>
