Spaces:
Running
Running
Download src/physics.js from AK51/PlasmaToroid: direct link, hf CLI and curl.
- Browser
- Download file 9.4 kB
-
https://huggingface.co/spaces/AK51/PlasmaToroid/resolve/main/src/physics.js
- Command line
-
hf download hf://spaces/AK51/PlasmaToroid/src/physics.js
-
curl -L -o physics.js https://huggingface.co/spaces/AK51/PlasmaToroid/resolve/main/src/physics.js
9.4 kB
| // Simplified plasma toroid physics model (pure ESM, no three.js). | |
| // All equations and constants follow .agents/tasks/plan.md. | |
| // Educational approximation only — not engineering guidance. | |
| export const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x)); | |
| export function smoothstep(e0, e1, x) { | |
| const t = clamp((x - e0) / (e1 - e0), 0, 1); | |
| return t * t * (3 - 2 * t); | |
| } | |
| // Soft window: 1 inside [a, b], fading to 0 over ~15 % outside each edge. | |
| export function windowFn(x, a, b) { | |
| return smoothstep(a * 0.85, a, x) * (1 - smoothstep(b, b * 1.15, x)); | |
| } | |
| // Townsend/Paschen coefficients A (1/cm/Torr), B (V/cm/Torr), first ionization energy, | |
| // glow color and empirical mode thresholds (pressures in Torr, powers in W). | |
| export const GASES = { | |
| He: { name: 'Helium', A: 3, B: 34, ionization_eV: 24.59, color: '#ffb08a', pRingMin: 1.0, pRingPower: 60, pBean: null, n0: null }, | |
| Ne: { name: 'Neon', A: 4, B: 100, ionization_eV: 21.56, color: '#ff4d6d', pRingMin: 0.5, pRingPower: 45, pBean: 2.5, n0: 14 }, | |
| Ar: { name: 'Argon', A: 12, B: 180, ionization_eV: 15.76, color: '#b46cff', pRingMin: 0.2, pRingPower: 25, pBean: 3.0, n0: 12 }, | |
| Kr: { name: 'Krypton', A: 17, B: 240, ionization_eV: 14.0, color: '#d9d4ff', pRingMin: 0.1, pRingPower: 18, pBean: 1.8, n0: 12 }, | |
| Xe: { name: 'Xenon', A: 26, B: 350, ionization_eV: 12.13, color: '#8f7bff', pRingMin: 0.08, pRingPower: 15, pBean: 1.2, n0: 13 }, | |
| }; | |
| // Bead-count limits for the plasma-bean ring. | |
| export const BEAD_MIN = 6; | |
| export const BEAD_MAX = 32; | |
| // dFactor: characteristic discharge length / vessel diameter (torus = tube bore). | |
| export const SHAPES = { | |
| sphere: { name: 'Sphere', dFactor: 1.0, coupling: 0.7, beanShift: 1.0 }, | |
| cylinder: { name: 'Cylinder', dFactor: 1.0, coupling: 0.65, beanShift: 1.1 }, | |
| torus: { name: 'Torus (donut)', dFactor: 0.38, coupling: 0.8, beanShift: 0.9 }, | |
| }; | |
| export const COOLING_OPTIONS = { | |
| none: 'None', | |
| heatsink: 'Heatsink', | |
| fan: 'Heatsink + fan', | |
| }; | |
| export const PARAM_RANGES = { | |
| shape: { options: Object.keys(SHAPES), default: 'sphere', unit: '' }, | |
| vesselDiameter_mm: { min: 60, max: 130, step: 5, default: 80, unit: 'mm' }, | |
| gas: { options: Object.keys(GASES), default: 'Xe', unit: '' }, | |
| pressure_torr: { min: 0.01, max: 10, step: null, log: true, default: 0.5, unit: 'Torr' }, | |
| supplyVoltage_V: { min: 6, max: 36, step: 0.5, default: 24, unit: 'V' }, | |
| frequency_MHz: { min: 5, max: 20, step: 0.1, default: 12, unit: 'MHz' }, | |
| coilTurns: { min: 2, max: 8, step: 1, default: 4, unit: 'turns' }, | |
| drive_pct: { min: 0, max: 100, step: 1, default: 70, unit: '%' }, | |
| currentLimit_A: { min: 1, max: 5, step: 0.1, default: 3.5, unit: 'A' }, | |
| ambient_C: { min: 0, max: 40, step: 1, default: 25, unit: '°C' }, | |
| cooling: { options: Object.keys(COOLING_OPTIONS), default: 'fan', unit: '' }, | |
| mosfetLimit_C: { min: 80, max: 175, step: 1, default: 125, unit: '°C' }, | |
| autoShutdown: { type: 'boolean', default: true }, | |
| autoIgnite: { type: 'boolean', default: true }, | |
| timeSpeed: { options: [1, 2, 5, 10, 30, 60], default: 1, unit: '×' }, | |
| }; | |
| // Circuit constants. | |
| export const Q_TANK = 4; | |
| export const R_EQ = 6; // Ω, equivalent load seen by the supply | |
| export const GAMMA = 0.01; // secondary emission coefficient | |
| const LN_LN_GAMMA = Math.log(Math.log(1 + 1 / GAMMA)); // ≈ 1.529 | |
| export const SPARK_FACTOR = 0.6; // spark-assisted ignition threshold (× V_b) | |
| export const SUSTAIN_FACTOR = 0.55; // sustain threshold once lit (× V_b) | |
| export const RDS_TEMPCO = 0.004; // MOSFET loss rise per °C of junction temperature above 25 °C (R_ds(on) derating) | |
| /** Resonant frequency of the tank in MHz (L = 0.125 µH·N², 4 turns → 12 MHz). */ | |
| export const resonanceMHz = (N) => 12 * (4 / N); | |
| /** Raizer-form Paschen breakdown voltage (V). Infinity left of the Paschen curve. */ | |
| export function breakdownVoltage(gas, p_torr, d_cm) { | |
| const g = typeof gas === 'string' ? GASES[gas] : gas; | |
| const pd = p_torr * d_cm; | |
| if (!(pd > 0)) return Infinity; | |
| const den = Math.log(g.A * pd) - LN_LN_GAMMA; | |
| if (den <= 0.05) return Infinity; | |
| return (g.B * pd) / den; | |
| } | |
| /** Ignition / sustain state machine step. Returns the new lit state. */ | |
| export function ignitionCheck({ lit, sparkActive, driverOn, V_coil, V_b }) { | |
| if (!driverOn || !Number.isFinite(V_b)) return false; | |
| if (lit) return V_coil >= SUSTAIN_FACTOR * V_b; | |
| return V_coil >= V_b || (!!sparkActive && V_coil >= SPARK_FACTOR * V_b); | |
| } | |
| /** | |
| * Compute the electrical operating point, losses, plasma state and visual targets. | |
| * @param {object} params simulation parameters (see PARAM_RANGES keys) | |
| * @param {{lit?:boolean, driverOn?:boolean, tempJ?:number, mosfetFailed?:boolean}} st | |
| */ | |
| export function computeOperatingPoint(params, { lit = false, driverOn = true, tempJ = 25, mosfetFailed = false } = {}) { | |
| const gas = GASES[params.gas]; | |
| const shape = SHAPES[params.shape]; | |
| const Vs = params.supplyVoltage_V; | |
| const f = params.frequency_MHz; | |
| const N = params.coilTurns; | |
| const p = params.pressure_torr; | |
| const D = params.vesselDiameter_mm; | |
| const u = clamp(params.drive_pct / 100, 0, 1); | |
| const on = driverOn && !mosfetFailed; | |
| const isLit = on && lit; | |
| const f0 = resonanceMHz(N); | |
| const g = 1 / Math.sqrt(1 + (Q_TANK * (f / f0 - f0 / f)) ** 2); | |
| const V_coil = on ? 36 * Vs * u * g * Math.sqrt(N / 4) : 0; | |
| const L = 0.125e-6 * N * N; | |
| const I_coil = V_coil / (2 * Math.PI * f * 1e6 * L); | |
| const d_cm = (D / 10) * shape.dFactor; | |
| const V_b = breakdownVoltage(gas, p, d_cm); | |
| const P_lim = Vs * params.currentLimit_A; | |
| const P_demand = on ? ((Vs * u) ** 2 / R_EQ) * (0.3 + 0.7 * g) * (isLit ? 1.3 : 1.0) : 0; | |
| const P_in = Math.min(P_demand, P_lim); | |
| const currentLimited = on && P_demand > P_lim; | |
| const I_in = Vs > 0 ? P_in / Vs : 0; | |
| const eta = clamp(0.8 - 0.025 * (f - 8), 0.55, 0.85); | |
| const P_mos0 = P_in * (1 - eta); // MOSFET loss at T_j = 25 °C | |
| const P_mos = P_mos0 * (1 + RDS_TEMPCO * (tempJ - 25)); | |
| const P_plasma = isLit ? P_in * eta * shape.coupling : 0; | |
| const R_ac = 0.05 * (N / 4) * Math.sqrt(f / 10); | |
| const P_coil = I_coil * I_coil * R_ac + 0.04 * P_plasma; | |
| const P_glass = 0.6 * P_plasma; | |
| const B = clamp(P_plasma / 60, 0, 1.5); | |
| const m_s = Number.isFinite(V_b) ? V_coil / (SUSTAIN_FACTOR * V_b) : 0; | |
| const inst = isLit | |
| ? clamp(Math.max((1.15 - m_s) / 0.15, (0.6 - g) / 0.3, currentLimited && P_demand > 1.25 * P_lim ? 0.6 : 0), 0, 1) | |
| : 0; | |
| const pb = gas.pBean != null ? gas.pBean * shape.beanShift : null; | |
| // Plasma state (priority order). | |
| let state; | |
| let reason = ''; | |
| if (!isLit) { | |
| state = 'off'; | |
| if (mosfetFailed) reason = 'MOSFET failed'; | |
| else if (!driverOn) reason = 'driver off'; | |
| else if (!Number.isFinite(V_b)) reason = 'pressure too low to break down (left of Paschen minimum)'; | |
| else if (V_coil < V_b) reason = `below breakdown: need ${Math.round(V_b)} V, have ${Math.round(V_coil)} V`; | |
| else reason = 'not ignited'; | |
| } else if (inst >= 0.5) { | |
| state = 'unstable'; | |
| reason = g < 0.6 ? 'detuned from resonance' : m_s < 1.15 ? 'barely sustained' : 'overdriven / current limited'; | |
| } else if (p < gas.pRingMin || P_plasma < gas.pRingPower) { | |
| state = 'diffuse'; | |
| reason = p < gas.pRingMin ? 'pressure below ring threshold' : 'plasma power below ring threshold'; | |
| } else if (pb != null && p >= pb && P_plasma >= 20 && P_plasma <= 80) { | |
| state = 'bean'; | |
| } else { | |
| state = 'ring'; | |
| } | |
| // Beads only show in the 'bean' state (keeps the visuals consistent with the HUD): half-formed at p_bean, | |
| // fully formed by 1.1 × p_bean. | |
| const beanMix = state === 'bean' ? 0.5 + 0.5 * smoothstep(pb, 1.1 * pb, p) : 0; | |
| // Azimuthal mode number m ∝ √(p·D) (striation length shrinks with p·R), weak power term. | |
| const beadCount = pb != null | |
| ? clamp(Math.round(gas.n0 * Math.sqrt((p / pb) * (D / 80)) * (P_plasma / 40) ** 0.3), BEAD_MIN, BEAD_MAX) | |
| : BEAD_MIN; // no bean mode for this gas; minimum keeps shader math finite | |
| const Bc = Math.min(B, 1); | |
| let ringRadiusFrac, ringHeightFrac; | |
| if (params.shape === 'sphere') { | |
| ringRadiusFrac = 0.62 - 0.08 * B; | |
| ringHeightFrac = -0.35 + 0.25 * Bc; | |
| } else if (params.shape === 'cylinder') { | |
| ringRadiusFrac = 0.6; | |
| ringHeightFrac = -0.6 + 0.2 * Bc; | |
| } else { | |
| ringRadiusFrac = 1.0; // torus: fraction of the major radius | |
| ringHeightFrac = 0; | |
| } | |
| // minorFrac in vessel-radius units; torus capped at 0.8 × tube radius (tube = 0.3 R). | |
| let minorFrac = clamp(0.12 * (0.5 / p) ** 0.35, 0.03, 0.35); | |
| if (params.shape === 'torus') minorFrac = Math.min(minorFrac, 0.8 * 0.3); | |
| const rotSpeed = 0.4 + 2.5 * B * Math.sqrt(f / 10); | |
| const beadSpeed = 0.6 * rotSpeed; | |
| const wobble = 0.02 + 0.15 * inst; | |
| const flicker = inst; | |
| const columnStretch = params.shape === 'cylinder' && (state === 'diffuse' || p > 2) ? 1 + 1.5 * Bc : 1; | |
| const diffuseAmount = { diffuse: 1.0, unstable: 0.5, ring: 0.15, bean: 0.1, off: 0 }[state]; | |
| return { | |
| g, f0, V_coil, I_coil, V_b, P_demand, P_in, I_in, currentLimited, eta, P_mos, P_mos0, P_coil, P_glass, P_plasma, | |
| B, inst, state, reason, beanMix, beadCount, ringRadiusFrac, ringHeightFrac, minorFrac, rotSpeed, beadSpeed, | |
| wobble, flicker, columnStretch, diffuseAmount, color: gas.color, | |
| }; | |
| } | |