{
const {aprime, omega0, windowWidth} = unruh_controls;
// Волна, идущая навстречу ускоренному наблюдателю:
// ω'(τ) = ω exp(a' τ), Φ(τ) = (ω/a') (exp(a' τ) - 1)
const tauMin = -windowWidth / 2;
const tauMax = windowWidth / 2;
const phase = (tau) => (omega0 / aprime) * (Math.exp(aprime * tau) - 1);
const instFreq = (tau) => omega0 * Math.exp(aprime * tau);
const OmaxRaw = 1.15 * instFreq(tauMax);
const Omax = Math.max(8, Math.min(20, OmaxRaw));
const maxPhaseRate = instFreq(tauMax) + Omax;
const samplesPerPeriod = 14;
const estimatedN = Math.ceil(windowWidth * maxPhaseRate * samplesPerPeriod / (2 * Math.PI)) + 1;
const N = Math.max(2400, Math.min(30000, estimatedN));
const isResolutionCapped = estimatedN > N;
const dt = (tauMax - tauMin) / (N - 1);
const taus = Array.from({length: N}, (_, i) => tauMin + i * dt);
const tukeyAlpha = 0.16;
const taper = (i) => {
if (tukeyAlpha <= 0) return 1;
const u = i / (N - 1);
if (u < tukeyAlpha / 2) {
return 0.5 * (1 - Math.cos(2 * Math.PI * u / tukeyAlpha));
}
if (u > 1 - tukeyAlpha / 2) {
return 0.5 * (1 - Math.cos(2 * Math.PI * (1 - u) / tukeyAlpha));
}
return 1;
};
const weights = Array.from({length: N}, (_, i) => {
const edgeWeight = (i === 0 || i === N - 1) ? 0.5 : 1;
return edgeWeight * taper(i) * dt;
});
const signalData = taus.map((tau) => ({
tau,
value: Math.cos(phase(tau))
}));
const freqData = taus.map((tau) => ({
tau,
value: instFreq(tau)
}));
const NO = 300;
const omegas = Array.from({length: NO}, (_, i) => i * Omax / (NO - 1));
const spectrumRaw = omegas.map((Omega) => {
let re = 0;
let im = 0;
for (let i = 0; i < N; i++) {
const tau = taus[i];
const theta = Omega * tau + phase(tau);
re += Math.cos(theta) * weights[i];
im += Math.sin(theta) * weights[i];
}
return {
Omega,
value: re * re + im * im
};
});
const Pmax = Math.max(...spectrumRaw.map((d) => d.value), 1e-14);
const spectrumData = spectrumRaw.map((d) => ({
Omega: d.Omega,
value: d.value / Pmax
}));
const omegaFloor = Math.max(Omax / (NO - 1), 1e-6);
const planckRaw = omegas.map((Omega) => {
const Om = Math.max(Omega, omegaFloor);
return {
Omega,
value: 1 / (Math.exp(2 * Math.PI * Om / aprime) - 1)
};
});
const PplanckMax = Math.max(...planckRaw.map((d) => d.value), 1e-14);
const planckData = planckRaw.map((d) => ({
Omega: d.Omega,
value: d.value / PplanckMax
}));
const resolutionNote = isResolutionCapped
? `N=${N.toLocaleString("ru-RU")} из требуемых ${estimatedN.toLocaleString("ru-RU")}: правый край окна всё ещё недоразрешён.`
: `N=${N.toLocaleString("ru-RU")}, трапецоидальная квадратура и окно Тьюки.`;
const ns = "http://www.w3.org/2000/svg";
const width = 900;
const height = 548;
const uid = Math.random().toString(36).slice(2);
const color = {
signal: "#ffb347",
freq: "#8ec5ff",
spectrum: "#ff6b6b",
planck: "#7CFC00",
grid: "#1e2a3b",
axis: "#d7dde8",
text: "#d7dde8",
muted: "#9fb0c7",
panel: "#0b111b",
border: "#303b4f"
};
const wrap = document.createElement("div");
wrap.className = "unruh-board";
const style = document.createElement("style");
style.textContent = `
.unruh-controls { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin: 0 0 0.35rem; }
.unruh-controls label { display: grid; grid-template-columns: 1.8rem 10.5rem 3.7rem; align-items: center; gap: 0.5rem; margin: 0; color: #f2f2f2; font-size: 0.46em; }
.unruh-controls input[type="range"] { width: 10.5rem; accent-color: #ffb347; }
.unruh-controls output { min-width: 3.7rem; padding: 0.08rem 0.25rem; border: 1px solid #334155; border-radius: 6px; background: #101722; color: #f2f2f2; font-variant-numeric: tabular-nums; text-align: right; }
.unruh-board { width: min(100%, 900px); margin: 0 auto; }
.unruh-board svg { width: 100%; height: auto; display: block; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
`;
wrap.append(style);
const svg = document.createElementNS(ns, "svg");
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", "Экспоненциальный chirp и спектр ускоренного детектора");
wrap.append(svg);
function add(name, attrs = {}, parent = svg) {
const node = document.createElementNS(ns, name);
for (const [key, value] of Object.entries(attrs)) {
if (key === "text") {
node.textContent = value;
} else if (value !== undefined && value !== null && value !== "") {
node.setAttribute(key, value);
}
}
parent.append(node);
return node;
}
const fmt = (x) => {
if (Math.abs(x) < 1e-9) return "0";
if (Math.abs(x) >= 10) return x.toFixed(0);
if (Math.abs(x) >= 1) return x.toFixed(1);
return x.toFixed(2);
};
const fmtLog = (x) => {
if (x <= 0) return "0";
if (x < 0.01) return x.toExponential(0);
return fmt(x);
};
const linePath = (data, xField, yField, sx, sy) => {
let d = "";
let started = false;
for (const point of data) {
const x = point[xField];
const y = point[yField];
if (!Number.isFinite(x) || !Number.isFinite(y)) {
started = false;
continue;
}
d += `${started ? "L" : "M"} ${sx(x)} ${sy(y)} `;
started = true;
}
return d;
};
let chartIndex = 0;
function drawChart({
x,
y,
w,
h,
title,
xLabel,
yLabel,
xDomain,
yDomain,
yLog = false,
series
}) {
const clipId = `${uid}-clip-${chartIndex++}`;
const defs = svg.querySelector("defs") || add("defs");
const clip = add("clipPath", {id: clipId}, defs);
add("rect", {x, y, width: w, height: h}, clip);
add("rect", {x, y, width: w, height: h, rx: "8", fill: color.panel, stroke: color.border, "stroke-width": "1.2"});
const sx = (value) => x + (value - xDomain[0]) / (xDomain[1] - xDomain[0]) * w;
const logMin = Math.log(Math.max(yDomain[0], 1e-9));
const logMax = Math.log(Math.max(yDomain[1], yDomain[0] + 1e-9));
const sy = (value) => {
if (yLog) {
const safe = Math.max(value, Math.exp(logMin));
return y + h - (Math.log(safe) - logMin) / (logMax - logMin) * h;
}
return y + h - (value - yDomain[0]) / (yDomain[1] - yDomain[0]) * h;
};
for (let i = 0; i <= 4; i++) {
const tx = xDomain[0] + (xDomain[1] - xDomain[0]) * i / 4;
const ty = yLog
? Math.exp(logMin + (logMax - logMin) * i / 4)
: yDomain[0] + (yDomain[1] - yDomain[0]) * i / 4;
add("line", {x1: sx(tx), y1: y, x2: sx(tx), y2: y + h, stroke: color.grid, "stroke-width": "1"});
add("line", {x1: x, y1: sy(ty), x2: x + w, y2: sy(ty), stroke: color.grid, "stroke-width": "1"});
add("text", {x: sx(tx), y: y + h + 18, fill: color.muted, "font-size": "11", "text-anchor": "middle", text: fmt(tx)});
add("text", {x: x - 8, y: sy(ty) + 4, fill: color.muted, "font-size": "11", "text-anchor": "end", text: yLog ? fmtLog(ty) : fmt(ty)});
}
add("line", {x1: x, y1: y + h, x2: x + w, y2: y + h, stroke: color.axis, "stroke-width": "1.4"});
add("line", {x1: x, y1: y, x2: x, y2: y + h, stroke: color.axis, "stroke-width": "1.4"});
add("text", {x, y: y - 9, fill: "#ffffff", "font-size": "16", "font-weight": "700", text: title});
add("text", {x: x + w, y: y + h + 35, fill: color.text, "font-size": "13", "text-anchor": "end", text: xLabel});
add("text", {x: x + 8, y: y + 15, fill: color.text, "font-size": "13", text: yLabel});
const layer = add("g", {"clip-path": `url(#${clipId})`});
for (const item of series) {
add("path", {
d: linePath(item.data, item.xField, item.yField, sx, sy),
fill: "none",
stroke: item.stroke,
"stroke-width": item.strokeWidth || "2.8",
"stroke-dasharray": item.dash,
opacity: item.opacity || "1"
}, layer);
}
}
add("rect", {x: "0", y: "0", width, height, rx: "12", fill: "#070a0f", stroke: "#243244", "stroke-width": "1"});
drawChart({
x: 58,
y: 34,
w: 345,
h: 150,
title: "Сигнал детектора",
xLabel: "τ",
yLabel: "s(τ)",
xDomain: [tauMin, tauMax],
yDomain: [-1.1, 1.1],
series: [{data: signalData, xField: "tau", yField: "value", stroke: color.signal, strokeWidth: "3"}]
});
drawChart({
x: 510,
y: 34,
w: 332,
h: 150,
title: "Мгновенная частота",
xLabel: "τ",
yLabel: "ω′(τ)",
xDomain: [tauMin, tauMax],
yDomain: [Math.max(omega0 * Math.exp(aprime * tauMin), 1e-4), omega0 * Math.exp(aprime * tauMax)],
yLog: true,
series: [{data: freqData, xField: "tau", yField: "value", stroke: color.freq, strokeWidth: "3"}]
});
drawChart({
x: 58,
y: 244,
w: 784,
h: 178,
title: "Оконный спектр",
xLabel: "Ω",
yLabel: "лог норм.",
xDomain: [0, Omax],
yDomain: [1e-2, 1.05],
yLog: true,
series: [
{data: planckData, xField: "Omega", yField: "value", stroke: color.planck, strokeWidth: "2.2", dash: "8 6", opacity: "0.9"},
{data: spectrumData, xField: "Omega", yField: "value", stroke: color.spectrum, strokeWidth: "3.2"}
]
});
add("line", {x1: "70", y1: "458", x2: "98", y2: "458", stroke: color.signal, "stroke-width": "4"});
add("text", {x: "106", y: "463", fill: color.text, "font-size": "14", text: "s(τ) = cos Φ(τ)"});
add("line", {x1: "260", y1: "458", x2: "288", y2: "458", stroke: color.freq, "stroke-width": "4"});
add("text", {x: "296", y: "463", fill: color.text, "font-size": "14", text: "ω′(τ) = ω exp(a′τ)"});
add("line", {x1: "508", y1: "458", x2: "536", y2: "458", stroke: color.spectrum, "stroke-width": "4"});
add("text", {x: "544", y: "463", fill: color.text, "font-size": "14", text: "|A(Ω)|²"});
add("line", {x1: "650", y1: "458", x2: "686", y2: "458", stroke: color.planck, "stroke-width": "3", "stroke-dasharray": "8 6"});
add("text", {x: "694", y: "463", fill: color.text, "font-size": "14", text: "1/(exp(2πΩ/a′) - 1)"});
add("text", {x: "70", y: "498", fill: color.muted, "font-size": "14", text: "Φ(τ) = (ω/a′)(exp(a′τ) - 1), AΔτ(Ω) = ∫ dτ exp(iΩτ) exp(iΦ(τ))."});
add("text", {x: "70", y: "523", fill: color.muted, "font-size": "13", text: "Красная кривая вычислена на конечном окне Δτ, поэтому она только приближается к тепловой форме."});
add("text", {x: "70", y: "541", fill: color.muted, "font-size": "12", text: resolutionNote});
return wrap;
}