aboutsummaryrefslogtreecommitdiff
path: root/src/experiments.ts
blob: d4d2bc27cbb41dc1ab2fc65cb7396fe17fa029fd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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<string, number | string>;
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]));
}