Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 125 additions & 147 deletions microcalibration-dashboard/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion microcalibration-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
"eslint": "^9",
"eslint-config-next": "15.3.4",
"tailwindcss": "^4",
"typescript": "^5"
"typescript": "5.9.3"
}
}
36,974 changes: 36,867 additions & 107 deletions microcalibration-dashboard/public/calibration_log.csv

Large diffs are not rendered by default.

525 changes: 405 additions & 120 deletions microcalibration-dashboard/src/app/page.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
'use client';

import { useMemo, useState } from 'react';
import { CalibrationDataPoint, ValidationDataPoint } from '@/types/calibration';
import { globMatch } from '@/utils/globMatch';

interface CalibrationVsSimComparisonProps {
calibrationData: CalibrationDataPoint[];
validationData: ValidationDataPoint[];
}

function formatNumber(n: number): string {
if (!isFinite(n)) return 'Inf';
if (Math.abs(n) >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
return n.toFixed(2);
}

interface JoinedRow {
target_name: string;
xw_estimate: number;
sim_value: number;
target_value: number;
xw_error: number;
sim_error: number;
gap: number;
}

export default function CalibrationVsSimComparison({ calibrationData, validationData }: CalibrationVsSimComparisonProps) {
const [filterVar, setFilterVar] = useState('');

const joined = useMemo(() => {
// Get final epoch calibration data
const maxEpoch = Math.max(...calibrationData.map(d => d.epoch));
const finalEpochData = calibrationData.filter(d => d.epoch === maxEpoch);

const calByName = new Map<string, CalibrationDataPoint>();
for (const d of finalEpochData) {
calByName.set(d.target_name, d);
}

const rows: JoinedRow[] = [];
for (const v of validationData) {
const cal = calByName.get(v.target_name);
if (!cal) continue;

const xw_error = cal.estimate - cal.target;
const sim_error = v.sim_value - v.target_value;
rows.push({
target_name: v.target_name,
xw_estimate: cal.estimate,
sim_value: v.sim_value,
target_value: v.target_value,
xw_error,
sim_error,
gap: sim_error - xw_error,
});
}

return rows;
}, [calibrationData, validationData]);

const filtered = useMemo(() => {
if (!filterVar) return joined;
return joined.filter(r => globMatch(filterVar, r.target_name));
}, [joined, filterVar]);

const regressions = useMemo(() => {
const regressed: JoinedRow[] = [];
for (const r of joined) {
if (r.target_value === 0) continue;
const xwRelErr = Math.abs(r.xw_error) / Math.abs(r.target_value);
const simRelErr = Math.abs(r.sim_error) / Math.abs(r.target_value);
if (simRelErr > xwRelErr + 0.10) {
regressed.push(r);
}
}
return regressed;
}, [joined]);

const worstRegressions = useMemo(() => {
return [...regressions]
.sort((a, b) => {
const aRel = Math.abs(a.target_value) > 0 ? Math.abs(a.gap) / Math.abs(a.target_value) : 0;
const bRel = Math.abs(b.target_value) > 0 ? Math.abs(b.gap) / Math.abs(b.target_value) : 0;
return bRel - aRel;
})
.slice(0, 5);
}, [regressions]);

if (joined.length === 0) {
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Calibration vs Simulation</h2>
<p className="text-gray-500">No matching target_names found between calibration and validation data. Ensure both CSVs use the same target_name format.</p>
</div>
);
}

const meanAbsGap = filtered.reduce((s, r) => s + Math.abs(r.gap), 0) / filtered.length;
const nWorse = filtered.filter(r => Math.abs(r.sim_error) > Math.abs(r.xw_error)).length;
const nRegressed10pct = regressions.length;

const bannerColor = nRegressed10pct === 0
? 'bg-green-50 border-green-200 text-green-800'
: nRegressed10pct < 5
? 'bg-yellow-50 border-yellow-200 text-yellow-800'
: 'bg-red-50 border-red-200 text-red-800';

const bannerLabel = nRegressed10pct === 0
? 'No significant regressions'
: nRegressed10pct < 5
? `${nRegressed10pct} target(s) regressed >10pp`
: `${nRegressed10pct} targets regressed >10pp`;

return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Calibration vs Simulation Comparison</h2>

{/* Regression severity banner */}
<div className={`rounded-lg border p-3 mb-4 ${bannerColor}`}>
<div className="font-medium text-sm">{bannerLabel}</div>
{worstRegressions.length > 0 && (
<ul className="mt-2 text-xs space-y-1">
{worstRegressions.map((r, i) => {
const relGap = Math.abs(r.target_value) > 0
? (Math.abs(r.gap) / Math.abs(r.target_value) * 100).toFixed(1)
: '?';
return (
<li key={i} className="font-mono">
{r.target_name}: gap {formatNumber(r.gap)} ({relGap}% of target)
</li>
);
})}
</ul>
)}
</div>

<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-blue-600">{joined.length}</div>
<div className="text-xs text-gray-500">Matched Targets</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-amber-600">{formatNumber(meanAbsGap)}</div>
<div className="text-xs text-gray-500">Mean |Gap|</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className={`text-2xl font-bold ${nWorse > joined.length / 2 ? 'text-red-600' : 'text-green-600'}`}>
{nWorse}/{filtered.length}
</div>
<div className="text-xs text-gray-500">Sim Worse than X*w</div>
</div>
</div>

<input
type="text"
placeholder="Search... (* = wildcard)"
value={filterVar}
onChange={e => setFilterVar(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm mb-3 w-full max-w-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>

<p className="text-xs text-gray-500 mb-2">{filtered.length} rows</p>

<div className="overflow-x-auto max-h-[500px] overflow-y-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Target Name</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Target</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">X*w Est.</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Sim Value</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">X*w Error</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Sim Error</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Gap</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{filtered.slice(0, 500).map((r, i) => (
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-3 py-2 text-sm font-mono text-gray-900 max-w-xs truncate" title={r.target_name}>
{r.target_name}
</td>
<td className="px-3 py-2 text-sm text-gray-700 text-right">{formatNumber(r.target_value)}</td>
<td className="px-3 py-2 text-sm text-gray-700 text-right">{formatNumber(r.xw_estimate)}</td>
<td className="px-3 py-2 text-sm text-gray-700 text-right">{formatNumber(r.sim_value)}</td>
<td className="px-3 py-2 text-sm text-right">
<span className={Math.abs(r.xw_error) / Math.abs(r.target_value) < 0.05 ? 'text-green-600' : 'text-orange-600'}>
{formatNumber(r.xw_error)}
</span>
</td>
<td className="px-3 py-2 text-sm text-right">
<span className={Math.abs(r.sim_error) / Math.abs(r.target_value) < 0.05 ? 'text-green-600' : 'text-orange-600'}>
{formatNumber(r.sim_error)}
</span>
</td>
<td className="px-3 py-2 text-sm text-right">
<span className={r.gap > 0 ? 'text-red-600' : 'text-green-600'}>
{formatNumber(r.gap)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { CalibrationDataPoint } from '@/types/calibration';
import { useState, useMemo } from 'react';
import { compareTargetNames } from '@/utils/targetOrdering';
import { globMatch } from '@/utils/globMatch';

interface ComparisonDataTableProps {
firstData: CalibrationDataPoint[];
Expand Down Expand Up @@ -78,8 +79,8 @@ export default function ComparisonDataTable({
}));

// Filter by search term
return rows.filter(row =>
row.targetName.toLowerCase().includes(filter.toLowerCase())
return rows.filter(row =>
globMatch(filter, row.targetName)
);
}, [firstData, secondData, epochFilter, filter]);

Expand Down Expand Up @@ -347,7 +348,7 @@ export default function ComparisonDataTable({
</select>
<input
type="text"
placeholder="Search target names..."
placeholder="Search... (* = wildcard)"
value={filter}
onChange={(e) => {
setFilter(e.target.value);
Expand Down
5 changes: 3 additions & 2 deletions microcalibration-dashboard/src/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { CalibrationDataPoint } from '@/types/calibration';
import { useState, useMemo } from 'react';
import { compareTargetNames } from '@/utils/targetOrdering';
import { globMatch } from '@/utils/globMatch';

interface DataTableProps {
data: CalibrationDataPoint[];
Expand All @@ -25,7 +26,7 @@ export default function DataTable({ data }: DataTableProps) {
const tableFilteredData = useMemo(() => {
return data.filter(item =>
item.epoch === epochFilter &&
(item.target_name.toLowerCase().includes(filter.toLowerCase()))
globMatch(filter, item.target_name)
);
}, [data, filter, epochFilter]);

Expand Down Expand Up @@ -136,7 +137,7 @@ export default function DataTable({ data }: DataTableProps) {
</select>
<input
type="text"
placeholder="Search target names..."
placeholder="Search... (* = wildcard)"
value={filter}
onChange={(e) => {
setFilter(e.target.value);
Expand Down
Loading
Loading