2026-07-09 21:07:34 -04:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useMemo, useState } from 'react';
|
|
|
|
|
import ToolShell from '@/components/toolkit/ToolShell';
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
type Mode = 'vector' | 'raster' | 'irradiance' | 'pulse';
|
|
|
|
|
|
|
|
|
|
function num(v: string, d = 0): number {
|
|
|
|
|
const n = Number(v);
|
|
|
|
|
return Number.isFinite(n) ? n : d;
|
|
|
|
|
}
|
|
|
|
|
function clamp(v: number, lo: number, hi: number) {
|
|
|
|
|
return Math.max(lo, Math.min(hi, v));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 17:22:41 -04:00
|
|
|
/** Circular spot-area ratio; pi/4 cancels. */
|
|
|
|
|
function areaFactorFromDiameter(spotSrc: number, spotDst: number) {
|
|
|
|
|
if (spotSrc <= 0 || spotDst <= 0) return 1;
|
|
|
|
|
const r = spotDst / spotSrc;
|
2026-07-09 21:07:34 -04:00
|
|
|
return r * r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function Page() {
|
|
|
|
|
// MODE
|
|
|
|
|
const [mode, setMode] = useState<Mode>('vector');
|
|
|
|
|
|
|
|
|
|
// SOURCE machine/lens
|
|
|
|
|
const [wSrc, setWSrc] = useState('100'); // rated W
|
|
|
|
|
const [pSrc, setPSrc] = useState('50'); // %
|
|
|
|
|
const [vSrc, setVSrc] = useState('300'); // mm/s
|
|
|
|
|
const [hSrc, setHSrc] = useState('0.1'); // mm (raster line spacing)
|
|
|
|
|
const [fSrc, setFSrc] = useState('30'); // kHz
|
|
|
|
|
const [tauSrc, setTauSrc] = useState('100'); // ns pulse width
|
2026-07-10 17:22:41 -04:00
|
|
|
const [fieldSrc, setFieldSrc] = useState('60'); // µm, 1/e² spot diameter
|
2026-07-09 21:07:34 -04:00
|
|
|
|
|
|
|
|
// DEST machine/lens
|
|
|
|
|
const [wDst, setWDst] = useState('50'); // rated W
|
|
|
|
|
const [vDst, setVDst] = useState('300'); // mm/s
|
|
|
|
|
const [hDst, setHDst] = useState('0.1'); // mm
|
|
|
|
|
const [fDst, setFDst] = useState('30'); // kHz
|
|
|
|
|
const [tauDst, setTauDst] = useState('100'); // ns
|
2026-07-10 17:22:41 -04:00
|
|
|
const [fieldDst, setFieldDst] = useState('40'); // µm
|
2026-07-09 21:07:34 -04:00
|
|
|
|
|
|
|
|
// Prefer adjusting speed/freq instead of exceeding 100% power
|
|
|
|
|
const [preferSpeedAdjust, setPreferSpeedAdjust] = useState(true);
|
|
|
|
|
|
|
|
|
|
const result = useMemo(() => {
|
|
|
|
|
const W1 = Math.max(num(wSrc, 1), 0.1);
|
|
|
|
|
const W2 = Math.max(num(wDst, 1), 0.1);
|
|
|
|
|
const p1 = clamp(num(pSrc, 0), 0, 100) / 100; // 0..1
|
|
|
|
|
const v1 = Math.max(num(vSrc, 0), 0.0001);
|
|
|
|
|
const v2 = Math.max(num(vDst, 0), 0.0001);
|
|
|
|
|
const h1 = Math.max(num(hSrc, 0), 0.000001);
|
|
|
|
|
const h2 = Math.max(num(hDst, 0), 0.000001);
|
|
|
|
|
const f1k = Math.max(num(fSrc, 0), 0.1);
|
|
|
|
|
const f2k = Math.max(num(fDst, 0), 0.1);
|
|
|
|
|
const tau2_ns = Math.max(num(tauDst, 0), 0.1);
|
2026-07-10 17:22:41 -04:00
|
|
|
const aFac = areaFactorFromDiameter(num(fieldSrc, 0), num(fieldDst, 0));
|
2026-07-09 21:07:34 -04:00
|
|
|
|
|
|
|
|
// Effective average power (W) after frequency efficiency
|
2026-07-10 17:22:41 -04:00
|
|
|
const P1eff = W1 * p1;
|
2026-07-09 21:07:34 -04:00
|
|
|
|
|
|
|
|
let p2Frac = p1; // destination power fraction (0..1)
|
|
|
|
|
let suggestedSpeed: number | undefined;
|
|
|
|
|
let suggestedFreq_kHz: number | undefined;
|
|
|
|
|
|
|
|
|
|
// Helper: compute required P2eff for each match, then map to power%
|
|
|
|
|
const powerPercentFromEff = (P2effReq: number) => {
|
|
|
|
|
// P2eff = W2 * p2 * eta2 => p2 = P2eff / (W2*eta2)
|
2026-07-10 17:22:41 -04:00
|
|
|
return P2effReq / W2;
|
2026-07-09 21:07:34 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (mode === 'vector') {
|
|
|
|
|
// Match energy per length: P1eff / v1 = P2eff / v2
|
|
|
|
|
const P2effReq = P1eff * (v2 / v1);
|
|
|
|
|
p2Frac = powerPercentFromEff(P2effReq);
|
|
|
|
|
if (preferSpeedAdjust && p2Frac > 1) {
|
2026-07-10 17:22:41 -04:00
|
|
|
suggestedSpeed = v1 * W2 / (W1 * p1); // from p2<=1
|
2026-07-09 21:07:34 -04:00
|
|
|
p2Frac = 1;
|
|
|
|
|
}
|
|
|
|
|
} else if (mode === 'raster') {
|
|
|
|
|
// Match energy per area: P1eff/(v1*h1) = P2eff/(v2*h2)
|
|
|
|
|
const P2effReq = P1eff * ((v2 * h2) / (v1 * h1));
|
|
|
|
|
p2Frac = powerPercentFromEff(P2effReq);
|
|
|
|
|
if (preferSpeedAdjust && p2Frac > 1) {
|
2026-07-10 17:22:41 -04:00
|
|
|
suggestedSpeed = v1 * W2 * (h1 / h2) / (W1 * p1);
|
2026-07-09 21:07:34 -04:00
|
|
|
p2Frac = 1;
|
|
|
|
|
}
|
|
|
|
|
} else if (mode === 'irradiance') {
|
|
|
|
|
// Match irradiance: (P1eff/A1) = (P2eff/A2) => P2eff = P1eff*(A2/A1)
|
|
|
|
|
const P2effReq = P1eff * aFac;
|
|
|
|
|
p2Frac = powerPercentFromEff(P2effReq);
|
|
|
|
|
// no speed suggestion; consider lens/field change if >100%
|
|
|
|
|
} else if (mode === 'pulse') {
|
|
|
|
|
// Match pulse energy: Ep1 = P1eff / f1 (kHz → Hz)
|
|
|
|
|
const f1 = f1k * 1e3, f2 = f2k * 1e3;
|
|
|
|
|
const Ep1 = P1eff / f1; // J
|
|
|
|
|
// Require P2eff = Ep1 * f2
|
|
|
|
|
const P2effReq = Ep1 * f2;
|
|
|
|
|
p2Frac = powerPercentFromEff(P2effReq);
|
|
|
|
|
|
|
|
|
|
if (preferSpeedAdjust && p2Frac > 1) {
|
|
|
|
|
// Suggest lowering f2 to keep p2<=1: P2eff_max = W2*eta2*1
|
|
|
|
|
// f2_req = P2eff_max / Ep1
|
2026-07-10 17:22:41 -04:00
|
|
|
const f2_req = W2 / Ep1; // Hz
|
2026-07-09 21:07:34 -04:00
|
|
|
suggestedFreq_kHz = Math.max(f2_req / 1e3, 0.1);
|
|
|
|
|
p2Frac = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute pulse metrics (for display) using **destination** settings
|
|
|
|
|
const p2Clamped = clamp(p2Frac, 0, 2);
|
2026-07-10 17:22:41 -04:00
|
|
|
const P2eff = W2 * p2Clamped;
|
2026-07-09 21:07:34 -04:00
|
|
|
const f2Hz = f2k * 1e3;
|
|
|
|
|
const tau2_s = tau2_ns * 1e-9;
|
|
|
|
|
const Ep2 = P2eff / f2Hz; // J
|
|
|
|
|
const Ppeak2 = Ep2 / Math.max(tau2_s, 1e-12); // W, shape factor ~1 assumed
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
p2Percent: clamp(p2Clamped * 100, 0, 200),
|
|
|
|
|
suggestedSpeed,
|
|
|
|
|
suggestedFreq_kHz,
|
|
|
|
|
P1eff,
|
|
|
|
|
P2eff,
|
|
|
|
|
Ep2,
|
|
|
|
|
Ppeak2,
|
|
|
|
|
aFac,
|
|
|
|
|
};
|
|
|
|
|
}, [
|
|
|
|
|
mode, wSrc, wDst, pSrc, vSrc, vDst, hSrc, hDst, fSrc, fDst, tauSrc, tauDst,
|
2026-07-10 17:22:41 -04:00
|
|
|
fieldSrc, fieldDst, preferSpeedAdjust,
|
2026-07-09 21:07:34 -04:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ToolShell
|
|
|
|
|
title="Power, Frequency & Lens Scaler"
|
2026-07-10 17:22:41 -04:00
|
|
|
description="Translate a starting recipe by matching energy per length, energy per area, spot irradiance, or pulse energy."
|
2026-07-09 21:07:34 -04:00
|
|
|
>
|
|
|
|
|
<Card className="mb-6">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Match Mode</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Quantity to Match</Label>
|
|
|
|
|
<Select value={mode} onValueChange={(v: Mode) => setMode(v)}>
|
|
|
|
|
<SelectTrigger><SelectValue placeholder="Mode" /></SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="vector">Vector: Energy per length (J/mm)</SelectItem>
|
|
|
|
|
<SelectItem value="raster">Raster: Energy per area (J/mm²)</SelectItem>
|
2026-07-10 17:22:41 -04:00
|
|
|
<SelectItem value="irradiance">Spot irradiance: W/mm²</SelectItem>
|
2026-07-09 21:07:34 -04:00
|
|
|
<SelectItem value="pulse">Pulse energy: J (fiber)</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
id="preferSpeed"
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="h-4 w-4"
|
|
|
|
|
checked={preferSpeedAdjust}
|
|
|
|
|
onChange={(e) => setPreferSpeedAdjust(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm">If Power % > 100, prefer adjusting speed/frequency</span>
|
|
|
|
|
</label>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Source */}
|
|
|
|
|
<Card className="mb-6">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Source (what you have)</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="grid gap-4 md:grid-cols-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Rated power (W)</Label>
|
|
|
|
|
<Input value={wSrc} onChange={(e) => setWSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Power (%)</Label>
|
|
|
|
|
<Input value={pSrc} onChange={(e) => setPSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Frequency (kHz)</Label>
|
|
|
|
|
<Input value={fSrc} onChange={(e) => setFSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Pulse width (ns)</Label>
|
|
|
|
|
<Input value={tauSrc} onChange={(e) => setTauSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={cn(mode !== 'irradiance' ? 'block' : 'hidden')}>
|
|
|
|
|
<Label className="text-sm">Speed (mm/s)</Label>
|
|
|
|
|
<Input value={vSrc} onChange={(e) => setVSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={cn(mode === 'raster' ? 'block' : 'hidden')}>
|
|
|
|
|
<Label className="text-sm">Line spacing h (mm)</Label>
|
|
|
|
|
<Input value={hSrc} onChange={(e) => setHSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-07-10 17:22:41 -04:00
|
|
|
<Label className="text-sm">1/e² spot diameter (µm)</Label>
|
2026-07-09 21:07:34 -04:00
|
|
|
<Input value={fieldSrc} onChange={(e) => setFieldSrc(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Destination */}
|
|
|
|
|
<Card className="mb-6">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Destination (what you want to run on)</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="grid gap-4 md:grid-cols-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Rated power (W)</Label>
|
|
|
|
|
<Input value={wDst} onChange={(e) => setWDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Frequency (kHz)</Label>
|
|
|
|
|
<Input value={fDst} onChange={(e) => setFDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-sm">Pulse width (ns)</Label>
|
|
|
|
|
<Input value={tauDst} onChange={(e) => setTauDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={cn(mode !== 'irradiance' ? 'block' : 'hidden')}>
|
|
|
|
|
<Label className="text-sm">Speed (mm/s)</Label>
|
|
|
|
|
<Input value={vDst} onChange={(e) => setVDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={cn(mode === 'raster' ? 'block' : 'hidden')}>
|
|
|
|
|
<Label className="text-sm">Line spacing h (mm)</Label>
|
|
|
|
|
<Input value={hDst} onChange={(e) => setHDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-07-10 17:22:41 -04:00
|
|
|
<Label className="text-sm">1/e² spot diameter (µm)</Label>
|
2026-07-09 21:07:34 -04:00
|
|
|
<Input value={fieldDst} onChange={(e) => setFieldDst(e.target.value)} inputMode="decimal" />
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Result */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Result</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
<div className="text-2xl font-semibold">
|
|
|
|
|
Suggested Power (dest): {result.p2Percent.toFixed(1)}%
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{typeof result.suggestedSpeed === 'number' && mode !== 'pulse' && (
|
|
|
|
|
<p className="text-sm">
|
|
|
|
|
To keep Power ≤ 100%, try destination speed ≈{' '}
|
|
|
|
|
<span className="font-medium">{result.suggestedSpeed.toFixed(1)} mm/s</span>.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{typeof result.suggestedFreq_kHz === 'number' && mode === 'pulse' && (
|
|
|
|
|
<p className="text-sm">
|
|
|
|
|
To keep Power ≤ 100%, try destination frequency ≈{' '}
|
|
|
|
|
<span className="font-medium">{result.suggestedFreq_kHz.toFixed(0)} kHz</span>.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-07-10 17:22:41 -04:00
|
|
|
<div className="mt-3 grid gap-2 md:grid-cols-2 text-sm">
|
2026-07-09 21:07:34 -04:00
|
|
|
<div>
|
|
|
|
|
<div className="text-muted-foreground">Dest pulse energy</div>
|
|
|
|
|
<div className="font-medium">
|
|
|
|
|
{(result.Ep2 >= 1e-3 ? (result.Ep2 * 1e3).toFixed(3) + ' mJ' : (result.Ep2 * 1e6).toFixed(1) + ' µJ')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-muted-foreground">Dest peak power</div>
|
|
|
|
|
<div className="font-medium">{(result.Ppeak2 / 1000).toFixed(1)} kW</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
2026-07-10 17:22:41 -04:00
|
|
|
Assumptions: displayed power percentage scales average output linearly, spots are circular,
|
|
|
|
|
and peak power uses a rectangular pulse approximation. Confirm the result with a low-power test;
|
|
|
|
|
real sources can have model-specific power limits versus frequency and pulse width.
|
2026-07-09 21:07:34 -04:00
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</ToolShell>
|
|
|
|
|
);
|
|
|
|
|
}
|