makearmy-app/components/utilities/laser-toolkit/power-lens-scaler/page.tsx

378 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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));
}
/** Default curve parameters based on rated power (very rough, editable). */
function defaultCurveForRatedW(W: number) {
// Peak frequency guess (kHz). Tune these to your hardware fleet.
let fPeak = 50;
if (W <= 35) fPeak = 25;
else if (W <= 60) fPeak = 50;
else if (W <= 90) fPeak = 75;
else fPeak = 100;
// Log-normal width parameter (dimensionless). Smaller = narrower peak.
const sigma = 0.35;
return { fPeak, sigma };
}
/** Log-normal shaped efficiency curve normalized to 1 at fPeak. */
function etaOfF(f_kHz: number, fPeak_kHz: number, sigma: number) {
const f = Math.max(f_kHz, 0.1);
const r = Math.log(f / Math.max(fPeak_kHz, 0.1));
const eta = Math.exp(-0.5 * (r / Math.max(sigma, 0.05)) ** 2);
// Keep within [0.1, 1] to avoid absurd zeros; adjust if you want tails to hit 0.
return clamp(eta, 0.1, 1);
}
/** Area factor from field (proxy for spot area scaling) */
function areaFactorFromField(fieldSrc: number, fieldDst: number) {
if (fieldSrc <= 0 || fieldDst <= 0) return 1;
const r = fieldDst / fieldSrc;
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
const [fieldSrc, setFieldSrc] = useState('110'); // mm
// 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
const [fieldDst, setFieldDst] = useState('70'); // mm
// Curve tuning / advanced
const [advanced, setAdvanced] = useState(false);
const srcDefaults = defaultCurveForRatedW(num(wSrc, 50));
const dstDefaults = defaultCurveForRatedW(num(wDst, 50));
const [fPeakSrc, setFPeakSrc] = useState(String(srcDefaults.fPeak));
const [sigmaSrc, setSigmaSrc] = useState(String(srcDefaults.sigma));
const [fPeakDst, setFPeakDst] = useState(String(dstDefaults.fPeak));
const [sigmaDst, setSigmaDst] = useState(String(dstDefaults.sigma));
// 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 tau1_ns = Math.max(num(tauSrc, 0), 0.1);
const tau2_ns = Math.max(num(tauDst, 0), 0.1);
const aFac = areaFactorFromField(num(fieldSrc, 0), num(fieldDst, 0));
const fpk1 = Math.max(num(fPeakSrc, defaultCurveForRatedW(W1).fPeak), 0.1);
const sig1 = Math.max(num(sigmaSrc, defaultCurveForRatedW(W1).sigma), 0.05);
const fpk2 = Math.max(num(fPeakDst, defaultCurveForRatedW(W2).fPeak), 0.1);
const sig2 = Math.max(num(sigmaDst, defaultCurveForRatedW(W2).sigma), 0.05);
// Efficiency factors (0..1)
const eta1 = etaOfF(f1k, fpk1, sig1);
const eta2 = etaOfF(f2k, fpk2, sig2);
// Effective average power (W) after frequency efficiency
const P1eff = W1 * p1 * eta1;
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)
return P2effReq / (W2 * eta2);
};
if (mode === 'vector') {
// Match energy per length: P1eff / v1 = P2eff / v2
const P2effReq = P1eff * (v2 / v1);
p2Frac = powerPercentFromEff(P2effReq);
if (preferSpeedAdjust && p2Frac > 1) {
suggestedSpeed = v1 * (W2 * eta2) / (W1 * eta1 * p1); // from p2<=1
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) {
suggestedSpeed = v1 * (W2 * eta2) * (h1 / h2) / (W1 * eta1 * p1);
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
const f2_req = (W2 * eta2) / Ep1; // Hz
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);
const P2eff = W2 * p2Clamped * eta2;
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,
eta1,
eta2,
P1eff,
P2eff,
Ep2,
Ppeak2,
aFac,
};
}, [
mode, wSrc, wDst, pSrc, vSrc, vDst, hSrc, hDst, fSrc, fDst, tauSrc, tauDst,
fieldSrc, fieldDst, preferSpeedAdjust, fPeakSrc, sigmaSrc, fPeakDst, sigmaDst,
]);
return (
<ToolShell
title="Power, Frequency & Lens Scaler"
description="Match settings across different lasers and lenses using effective power with a frequency efficiency curve. Includes pulse width to report pulse energy and peak power."
>
<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>
<SelectItem value="irradiance">Irradiance: W/mm² (spot/field)</SelectItem>
<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 % &gt; 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>
<Label className="text-sm">Lens field size (mm)</Label>
<Input value={fieldSrc} onChange={(e) => setFieldSrc(e.target.value)} inputMode="decimal" />
</div>
</CardContent>
<CardContent className="pt-0">
<button
className="text-xs underline text-muted-foreground"
onClick={() => setAdvanced((s) => !s)}
>
{advanced ? 'Hide' : 'Show'} advanced frequency curve
</button>
<div className={cn('mt-3 grid gap-4 md:grid-cols-3', advanced ? 'block' : 'hidden')}>
<div>
<Label className="text-sm">Peak freq fₚ (kHz)</Label>
<Input value={fPeakSrc} onChange={(e) => setFPeakSrc(e.target.value)} inputMode="decimal" />
</div>
<div>
<Label className="text-sm">Curve width σ (log-normal)</Label>
<Input value={sigmaSrc} onChange={(e) => setSigmaSrc(e.target.value)} inputMode="decimal" />
</div>
<div className="flex items-end text-xs text-muted-foreground">
η(f) is log-normal; 1.0 at fₚ, rolls off by σ.
</div>
</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>
<Label className="text-sm">Lens field size (mm)</Label>
<Input value={fieldDst} onChange={(e) => setFieldDst(e.target.value)} inputMode="decimal" />
</div>
</CardContent>
<CardContent className={cn('pt-0', advanced ? 'block' : 'hidden')}>
<div className="mt-3 grid gap-4 md:grid-cols-3">
<div>
<Label className="text-sm">Peak freq fₚ (kHz)</Label>
<Input value={fPeakDst} onChange={(e) => setFPeakDst(e.target.value)} inputMode="decimal" />
</div>
<div>
<Label className="text-sm">Curve width σ (log-normal)</Label>
<Input value={sigmaDst} onChange={(e) => setSigmaDst(e.target.value)} inputMode="decimal" />
</div>
<div className="flex items-end text-xs text-muted-foreground">
Adjust if you know your machines real powerfrequency curve.
</div>
</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>
)}
<div className="mt-3 grid gap-2 md:grid-cols-3 text-sm">
<div>
<div className="text-muted-foreground">η(f) source / dest</div>
<div className="font-medium">{result.eta1.toFixed(3)} / {result.eta2.toFixed(3)}</div>
</div>
<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">
Assumptions: Effective power includes a frequency efficiency factor η(f). Peak power uses a rectangular pulse
approximation (shape factor 1). For real MOPA sources, pulse shape and
true powerfrequency maps vary by model; adjust f<sub>p</sub> and σ if you have vendor curves.
</p>
</CardContent>
</Card>
</ToolShell>
);
}