From 4533910bce394e7a0ee6debfc05ca1221ee51899 Mon Sep 17 00:00:00 2001 From: victor Date: Sat, 24 Oct 2026 15:57:01 +0200 Subject: Astra! --- src/experiments.ts | 192 ----------------------------------------------------- 1 file changed, 192 deletions(-) delete mode 100644 src/experiments.ts (limited to 'src/experiments.ts') diff --git a/src/experiments.ts b/src/experiments.ts deleted file mode 100644 index d4d2bc2..0000000 --- a/src/experiments.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { getLanguage, word } from './i18n'; -import { electricField, type Charge } from './physics/electricField'; -import { capacitor, conductor, forceOn, gaussianFlux, pairEnergy, potential, semiconductor } from './physics/models'; - -export type TestId = 'test-work' | 'test-conservation' | 'test-central' | 'test-assembly' | 'test-uniform' | 'test-dipole'; -export type ExperimentId = 'field' | 'flux' | 'energy' | 'potential' | 'capacitance' | 'dielectric' | 'current' | 'semiconductor' | TestId; -export type Parameters = Record; -export interface Slider { kind: 'range'; id: string; label: string; min: number; max: number; step: number; value: number; unit: string } -export interface Choice { kind: 'select'; id: string; label: string; value: string; options: { value: string; label: string }[] } -export interface Measurement { label: string; value: string; unit: string } -export interface Experiment { - id: ExperimentId; - title: string; - shortTitle: string; - description: string; - controls: (Slider | Choice)[]; - equations: string[]; - explanation: string; - challenge: string; - limits: string; - sceneNote: string; - legend: [string, string][]; - source: { title: string; url: string }; - measure: (p: Parameters) => Measurement[]; -} -export const number = (p: Parameters, key: string): number => Number(p[key]); -const slider = (id: string, label: string, min: number, max: number, step: number, value: number, unit: string): Slider => ({ kind: 'range', id, label, min, max, step, value, unit }); -const choice = (id: string, label: string, value: string, options: [string, string][]): Choice => ({ kind: 'select', id, label, value, options: options.map(([value, label]) => ({ value, label })) }); -export function format(value: number, digits = 2): string { - if (!Number.isFinite(value)) return 'Undefined'; - if (value === 0) return '0'; - if (Math.abs(value) < 0.01 || Math.abs(value) >= 1e5) return value.toExponential(2); - return value.toLocaleString('en', { maximumFractionDigits: digits }); -} -export function formatDisplay(value: number, digits = 2): string { - if (getLanguage() !== 'da') return format(value, digits); - if (!Number.isFinite(value)) return 'Udefineret'; - if (value === 0) return '0'; - const locale = 'da-DK'; - if (Math.abs(value) < 0.01 || Math.abs(value) >= 1e5) return value.toLocaleString(locale, { notation: 'scientific', maximumFractionDigits: 2 }); - return value.toLocaleString(locale, { maximumFractionDigits: digits }); -} -const metric = (label: string, value: number, unit: string): Measurement => ({ label, value: formatDisplay(value), unit }); -const textbook = (page: string) => ({ title: 'OpenStax · University Physics', url: `https://openstax.org/books/university-physics-volume-2/pages/${page}` }); -export function chargePair(p: Parameters): Charge[] { - const half = number(p, 'separation') / 2; - return [ - { x: -half, y: 0, z: 0, coulombs: number(p, 'charge') * 1e-9 }, - { x: half, y: 0, z: 0, coulombs: number(p, 'charge') * 1e-9 * (p.configuration === 'like' ? 1 : -1) }, - ]; -} -export function plateModel(p: Parameters) { - return capacitor({ area: number(p, 'area') * 1e-4, gap: number(p, 'gap') * 1e-3, voltage: number(p, 'voltage'), relativePermittivity: p.permittivity === undefined ? 1 : number(p, 'permittivity'), isolated: p.connection === 'isolated' }); -} -export function wireModel(p: Parameters) { - return conductor({ voltage: number(p, 'voltage'), area: number(p, 'area') * 1e-6, length: number(p, 'length'), resistivity: 1.68e-8, carrierDensity: 8.5e28 }); -} -export function carrierModel(p: Parameters) { - return semiconductor({ intrinsicDensity: 1e16, electronMobility: 0.135, holeMobility: 0.048, netDoping: p.doping === 'intrinsic' ? 0 : 10 ** number(p, 'concentration') * (p.doping === 'p' ? -1 : 1) }); -} -const pairControls = [slider('charge', 'Source charge magnitude', 0.2, 5, 0.1, 2, 'nC'), slider('separation', 'Charge separation', 1, 6, 0.1, 3, 'm'), choice('configuration', 'Charge signs', 'opposite', [['opposite', 'Opposite (+ / −)'], ['like', 'Like (+ / +)']])]; -const plateControls = [slider('area', 'Plate area', 25, 225, 5, 100, 'cm²'), slider('gap', 'Plate separation', 0.5, 5, 0.1, 1, 'mm'), slider('voltage', 'Charging voltage', 1, 20, 1, 10, 'V')]; - -export const experiments: Experiment[] = [ - { - id: 'field', title: 'Electric charge, force and field', shortTitle: 'Charge & field', - description: 'See how signed charges create a vector field and exert forces on one another.', - controls: pairControls, - equations: [String.raw`\vec E = k\sum_i \frac{q_i}{r_i^2}\hat r_i`, String.raw`\vec F = q\vec E,\qquad |\vec F_{12}| = k\frac{|q_1q_2|}{r^2}`], - explanation: 'A positive test charge follows the electric field; a negative charge feels a force in the opposite direction. The large gold arrow shows the force on the right-hand source due only to the left-hand source.', - challenge: 'Double the separation. What happens to the force? Change to like charges and watch attraction become repulsion.', - limits: String.raw`Vacuum point charges. The x–z plane is sampled; points within $0.18\,\mathrm m$ of a source are excluded. Arrow lengths are compressed, and the force arrow is normalized.`, - sceneNote: String.raw`Grid: $1\,\mathrm m$ major squares · small arrows: $E$ · gold arrow: force on right charge`, - legend: [['#ff967b', 'Positive charge'], ['#73d7ff', 'Negative charge'], ['#91e2cb', 'Electric field'], ['#f5d58b', 'Force']], - source: textbook('5-3-coulombs-law'), - measure: p => { - const charges = chargePair(p); - const e = electricField({ x: 0, y: 0, z: 0 }, charges); - const f = forceOn(charges[1], [charges[0]]); - return [metric('|E| at origin', Math.hypot(e.x, e.y, e.z), 'N/C'), metric('|Force on right charge|', Math.hypot(f.x, f.y, f.z) * 1e9, 'nN'), { label: 'Interaction', value: word(p.configuration === 'like' ? 'Repulsive' : 'Attractive'), unit: '' }]; - }, - }, - { - id: 'flux', title: 'Electric flux and charge', shortTitle: 'Flux & Gauss’s law', - description: 'Move a charge through a Gaussian surface and compare local field with net flux.', - controls: [slider('charge', 'Signed source charge', -5, 5, 0.1, 2, 'nC'), slider('radius', 'Gaussian sphere radius', 1, 3, 0.1, 2, 'm'), slider('position', 'Charge x position', -4, 4, 0.1, 0, 'm')], - equations: [String.raw`\Phi_E = \oint \vec E\cdot\hat n\,\mathrm dA`, String.raw`\Phi_E = \frac{Q_{\mathrm{enclosed}}}{\varepsilon_0}`], - explanation: String.raw`The translucent sphere is an imaginary closed surface, not a conductor. Arrows show $E$ at surface samples: mint means outward flux and coral means inward flux. An outside charge still creates a field, but its total flux through the closed surface is zero.`, - challenge: 'Keep the charge inside and enlarge the sphere. Then move the charge outside. Can there be a nonzero field with zero net flux?', - limits: 'The flux readout uses exact Gauss’s law; arrows are discrete field samples, not a numerical integral. A point charge exactly on the surface makes the flux undefined.', - sceneNote: String.raw`Imaginary Gaussian surface · mint: $\vec E\cdot\hat n>0$ · coral: $\vec E\cdot\hat n<0$`, - legend: [['#91e2cb', 'Outward flux'], ['#ff967b', 'Inward flux'], ['#73d7ff', 'Gaussian surface']], - source: textbook('6-2-explaining-gausss-law'), - measure: p => { - const q = { x: number(p, 'position'), y: 0, z: 0, coulombs: number(p, 'charge') * 1e-9 }; - const flux = gaussianFlux([q], number(p, 'radius')); - return [metric('Net electric flux', flux, 'N·m²/C'), metric('Enclosed charge', Number.isNaN(flux) ? NaN : Math.abs(q.x) < number(p, 'radius') ? q.coulombs * 1e9 : 0, 'nC'), { label: 'Source location', value: word(Number.isNaN(flux) ? 'On surface' : Math.abs(q.x) < number(p, 'radius') ? 'Inside' : 'Outside'), unit: '' }]; - }, - }, - { - id: 'energy', title: 'Electric potential energy', shortTitle: 'Potential energy', - description: 'Separate two charges and connect interaction energy with the work done by the field.', - controls: [slider('testCharge', 'Moving charge', -3, 3, 0.1, -2, 'nC'), slider('distance', 'Distance from +3 nC source', 0.75, 6, 0.05, 2, 'm')], - equations: [String.raw`U(r) = \frac{kQq}{r}`, String.raw`W_{\mathrm{field},\,6\,\mathrm m\to r} = U(6\,\mathrm m)-U(r)`], - explanation: 'The source stays on the left. The mint curve plots the interaction energy against separation; the gold marker follows the current setting. Opposite charges have negative energy relative to infinite separation, so pulling them apart requires external work.', - challenge: String.raw`Move an opposite charge closer. Does $U$ rise or fall? Now make the moving charge positive and compare the sign of the field’s work.`, - limits: String.raw`Two prescribed point charges, with $U=0$ at infinity. The curve uses a fixed display scale of 20 nJ per vertical scene unit. The controls set positions; they do not integrate motion.`, - sceneNote: String.raw`Curve: $U$ versus separation · gold point: current $U$ · horizontal zero-energy axis`, - legend: [['#ff967b', 'Fixed +3 nC'], ['#73d7ff', 'Negative charge'], ['#91e2cb', 'Energy curve'], ['#f5d58b', 'Current energy']], - source: textbook('7-1-electric-potential-energy'), - measure: p => { - const q = number(p, 'testCharge') * 1e-9; - const r = number(p, 'distance'); - const u = pairEnergy(3e-9, q, r); - return [metric('Interaction energy', u * 1e9, 'nJ'), metric('Work by field, from 6 m', (pairEnergy(3e-9, q, 6) - u) * 1e9, 'nJ'), metric('|Force on moving charge|', Math.abs(u / r) * 1e9, 'nN')]; - }, - }, - { - id: 'potential', title: 'Electric potential and gradient', shortTitle: 'Potential & gradient', - description: 'Read electric potential as a landscape and connect its downhill direction to the field.', - controls: [...pairControls, slider('probe', 'Probe x position (z = 1 m)', -4, 4, 0.1, 0, 'm')], - equations: [String.raw`V = k\sum_i\frac{q_i}{r_i}`, String.raw`\vec E = -\nabla V,\qquad F_x = -q\frac{\partial V}{\partial x}`], - explanation: String.raw`Warm peaks are positive potential; blue valleys are negative. Mint arrows show the electric field projected in the x–z plane. The gold probe samples $V$ and the x component of $E$. Field arrows point toward decreasing potential.`, - challenge: String.raw`Use opposite charges: the midpoint potential can be zero while the field is nonzero. Compare $V$ and $E_x$ as you move the probe.`, - limits: String.raw`Height $=2\tanh\left(\frac{V}{15\,\mathrm V}\right)$, a compressed potential scale. Holes exclude source singularities. Arrow lengths are compressed. Probe is always at $z=1\,\mathrm m$, away from sources; $V=0$ at infinity.`, - sceneNote: String.raw`Height and colour encode potential · gold probe at $z=1\,\mathrm m$ · arrows: $-\nabla V$`, - legend: [['#ff967b', 'Positive potential'], ['#73d7ff', 'Negative potential'], ['#f5d58b', 'Probe']], - source: textbook('7-4-determining-field-from-potential'), - measure: p => { - const point = { x: number(p, 'probe'), y: 0, z: 1 }; - const charges = chargePair(p); - const field = electricField(point, charges); - return [metric('Potential at probe', potential(point, charges), 'V'), metric('Ex at probe', field.x, 'V/m'), metric('|E| at probe', Math.hypot(field.x, field.y, field.z), 'N/C')]; - }, - }, - { - id: 'capacitance', title: 'Capacitance', shortTitle: 'Capacitance', - description: 'Build a parallel-plate capacitor and see how geometry controls charge storage.', - controls: plateControls, - equations: [String.raw`C = \frac{\varepsilon_0 A}{d},\qquad Q = CV`, String.raw`E = \frac{V}{d},\qquad U = \frac12 CV^2`], - explanation: 'The left plate is positive and the right plate is negative. At a fixed applied voltage, larger plates store more charge, while a wider gap reduces capacitance. The arrows show the uniform field approximation between the plates.', - challenge: 'Double the area, then double the gap. Which measurements return to their original values, and which do not?', - limits: 'Ideal parallel plates in vacuum, connected to a fixed-voltage supply. Fringing is ignored. Plate separation is enlarged 50× relative to lateral dimensions for visibility; charge markers are illustrative. Arrows span the gap to show direction, not magnitude.', - sceneNote: 'Positive plate → field → negative plate · gap enlarged 50×', - legend: [['#ff967b', 'Positive plate'], ['#73d7ff', 'Negative plate'], ['#91e2cb', 'Uniform E']], - source: textbook('8-1-capacitors-and-capacitance'), - measure: p => { const c = plateModel(p); return [metric('Capacitance', c.capacitance * 1e12, 'pF'), metric('Stored free charge |Q|', c.charge * 1e9, 'nC'), metric('Electric field', c.field / 1000, 'kV/m'), metric('Stored energy', c.energy * 1e9, 'nJ')]; }, - }, - { - id: 'dielectric', title: 'Dielectric materials', shortTitle: 'Dielectrics', - description: 'Polarize an insulator and compare insertion with a connected or disconnected supply.', - controls: [slider('permittivity', 'Relative permittivity κ', 1, 8, 0.1, 4, ''), choice('connection', 'Electrical condition', 'connected', [['connected', 'Connected · voltage held fixed'], ['isolated', 'Isolated · free charge held fixed']]), ...plateControls], - equations: [String.raw`C = \frac{\kappa\varepsilon_0 A}{d},\qquad \vec P = \varepsilon_0(\kappa-1)\vec E`, String.raw`\begin{aligned}\text{Fixed }V:&\quad Q\propto\kappa\\\text{Fixed }Q:&\quad V,E\propto\frac1\kappa\end{aligned}`], - explanation: String.raw`Bound dipoles align with the field: their negative ends face the positive plate. The bound charges oppose the field of the free plate charges. With a battery connected, extra free charge enters and $V$ stays fixed. When isolated, $Q$ stays fixed and $V$ falls.`, - challenge: String.raw`Switch to isolated and compare $\kappa$ = 1 with $\kappa$ = 4. Then reconnect the supply and repeat: which quantities are held constant?`, - limits: String.raw`A uniform, linear dielectric completely fills the gap. Isolated $Q$ is defined by charging this geometry in vacuum at the selected charging voltage, then disconnecting and inserting the material. Each setting is a fresh preparation; breakdown and fringing are excluded. Gap enlarged 50×; field arrows show direction, not magnitude.`, - sceneNote: String.raw`Gold volume: dielectric · blue/red pairs: bound dipoles · $\kappa$ = 1: no polarization`, - legend: [['#f5d58b', 'Dielectric'], ['#73d7ff', 'Bound −'], ['#ff967b', 'Bound +'], ['#91e2cb', 'Net field']], - source: textbook('8-4-capacitor-with-a-dielectric'), - measure: p => { const c = plateModel(p); return [metric('Capacitance', c.capacitance * 1e12, 'pF'), metric('Plate voltage', c.voltage, 'V'), metric('Free charge |Q|', c.charge * 1e9, 'nC'), metric('Net field', c.field / 1000, 'kV/m'), metric('Polarization |P|', c.polarization * 1e6, 'µC/m²')]; }, - }, - { - id: 'current', title: 'Current', shortTitle: 'Current & drift', - description: 'Drive electrons through a copper wire and compare their drift with conventional current.', - controls: [slider('voltage', 'Voltage across wire', -0.1, 0.1, 0.005, 0.025, 'V'), slider('length', 'Wire length', 0.5, 3, 0.1, 1, 'm'), slider('area', 'Cross-sectional area', 0.5, 4, 0.1, 1, 'mm²')], - equations: [String.raw`R = \frac{\rho L}{A},\qquad I = \frac{V}{R}`, String.raw`J = \frac{I}{A},\qquad v_e = -\frac{I}{neA}`], - explanation: 'Mint shows conventional current, defined as positive-charge flow. Blue electrons drift in the opposite direction. More voltage increases the drift; a longer wire adds resistance. A wider wire carries more current at the same voltage.', - challenge: 'Reverse the voltage. Then set it to zero. At fixed voltage, increase the wire area: does current increase, and does the electron drift speed change?', - limits: String.raw`Ohmic copper at fixed temperature: $\rho=1.68\times10^{-8}\,\Omega\,\mathrm m$, $n=8.5\times10^{28}\,\mathrm m^{-3}$. Drift is displayed 600× faster and the wire radius is exaggerated. Thermal scattering, heating feedback, and signal propagation are not simulated; $P$ is dissipated power.`, - sceneNote: 'Blue: electron drift · mint: conventional current · speed ×600 · radius exaggerated', - legend: [['#73d7ff', 'Electrons'], ['#91e2cb', 'Conventional current'], ['#f5d58b', 'Copper ions']], - source: textbook('9-2-model-of-conduction-in-metals'), - measure: p => { const w = wireModel(p); return [metric('Resistance', w.resistance * 1000, 'mΩ'), metric('Conventional current', w.current, 'A'), metric('Electron drift, x', w.electronDrift * 1000, 'mm/s'), metric('Dissipated power', w.power, 'W')]; }, - }, - { - id: 'semiconductor', title: 'Semiconducting materials', shortTitle: 'Semiconductors', - description: String.raw`Explore intrinsic, $n$-type and $p$-type silicon through carriers and an energy-band diagram.`, - controls: [choice('doping', 'Silicon sample at 300 K', 'intrinsic', [['intrinsic', 'Intrinsic · undoped'], ['n', 'n-type · donor dopants'], ['p', 'p-type · acceptor dopants']]), slider('concentration', 'Dopant density · log₁₀(N / m⁻³)', 14, 22, 0.25, 20, '')], - equations: [String.raw`np = n_i^2,\qquad n-p = N_D-N_A`, String.raw`\sigma = e(n\mu_n+p\mu_p)`, String.raw`E_F-E_i = k_B T\ln\left(\frac{n}{n_i}\right)`], - explanation: String.raw`Electrons occupy the conduction band; holes are missing electrons in the valence band. Donors raise electron density and the Fermi level; acceptors raise hole density and lower it. The gold line marks $E_F$. Carrier markers show populations on a compressed scale, not individual atoms.`, - challenge: String.raw`Switch from intrinsic to $n$-type, then $p$-type at the same doping. Watch the majority carrier and Fermi shift change. Why are the conductivities different?`, - limits: String.raw`Illustrative silicon reference values at $300\,\mathrm K$: $E_g=1.12\,\mathrm{eV}$, $n_i\approx10^{16}\,\mathrm m^{-3}$, $\mu_n=0.135\,\mathrm{m^2/(V\,s)}$ and $\mu_p=0.048\,\mathrm{m^2/(V\,s)}$. Fully ionized, non-degenerate equilibrium with fixed mobilities. $E_i$ is drawn at approximate midgap. Motion is schematic thermal motion with no applied field or net drift; the density control is inactive for intrinsic silicon.`, - sceneNote: String.raw`Upper blue band: conduction · lower coral band: valence · gold: $E_F$ · no net flow`, - legend: [['#73d7ff', 'Conduction electrons'], ['#ff967b', 'Valence holes'], ['#f5d58b', 'Fermi level']], - source: { title: 'OpenStax · Band theory of solids', url: 'https://openstax.org/books/university-physics-volume-3/pages/9-5-band-theory-of-solids' }, - measure: p => { const s = carrierModel(p); return [metric('Electron density n', s.electrons, 'm⁻³'), metric('Hole density p', s.holes, 'm⁻³'), metric('Conductivity σ', s.conductivity, 'S/m'), metric('Fermi shift EF − Ei', s.fermiShift, 'eV')]; }, - }, -]; - -export function defaults(experiment: Experiment): Parameters { - return Object.fromEntries(experiment.controls.map(control => [control.id, control.value])); -} -- cgit v1.2.3