Grounded in the user's hands-on experience: 8 electrolytics soldered in
parallel drove a nail hard enough to dent a metal PSU wall (~few joules,
~30-60 m/s -- matching the model's output range). So cheap electrolytics
in parallel packs really do deliver high pulse current; the DB's old
values were ripple (continuous) ratings, not single-shot pulse capability.
Updated max_current_a to pulse estimates that scale with size/ESR (bigger,
lower-ESR cans sustain more), clearly labeled ESTIMATE for coilgun pulse
use in each source field. A parallel bank sums these (n_parallel), so an
8-pack models the user's real setup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1. Iron saturation now in the dynamics (was the "13 Tesla" bug). Flux
linkage lambda(x,I) = L_air*I + L_iron*overlap(x)*g(I) with
g(I)=I_sat*tanh(I/I_sat) saturating at the current where iron reaches
B_sat. Both the circuit back-EMF (dlambda/dx) and the force (coenergy,
dW'/dx) derive from the SAME lambda, so energy stays conserved (0.025%
error) AND the field caps at B_sat instead of running to 13 T. Reduces
to the old 0.5*I^2*dL/dx in the low-current limit. On the config that
reported 60% efficiency / 11.6 T, it now gives 40% / 1.90 T.
2. Switch surge-current limit is now a hard feasibility constraint: a
config whose peak discharge current exceeds the switch's pulse rating
(continuous * surge factor per device kind) is infeasible -- otherwise
the optimizer "wins" with configs that vaporize their own thyristor
(e.g. 119 A through a 12 A BT151). Capacitor current stays a warning
(electrolytics tolerate pulses; DB has continuous not pulse ratings).
Regression tests added for both (field cap near saturation, energy
conservation under strong discharge). 87 tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clicking a config in the dashboard now shows its velocity/field/current
plots (rendered on the fly by /api/run/<id>/plot/<kind>.png) plus an
on-demand GIF of the slug flying through the coils
(/api/run/<id>/anim.gif). Matplotlib rendering is serialized behind a
lock since the Agg pyplot state machine isn't thread-safe and the server
is threaded. Verified end to end: endpoints return real PNG/GIF bytes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per user: each stage's energy source is now a bank of real capacitors,
n_series x n_parallel, not a single cap.
- components/schema.py CapacitorBank.from_spec: series multiplies voltage
and ESR and divides capacitance; parallel multiplies capacitance and
max current and divides ESR; price and count scale with total cap count.
Duck-typed as a drop-in for CapacitorSpec in StageConfig.
- search_space: cap_series (1-6) and cap_parallel (1-8) per stage gene,
sampled/mutated/repaired; decode builds the bank; charge voltage capped
by min(bank voltage, switch rating)
- objective.build_detail records the full bank composition (base part,
series/parallel, count, totals) and current-capability diagnostics:
bank_current_over_limit / switch_current_over_limit vs peak discharge
current (a warning, not a hard reject -- DB holds datasheet not pulse
ratings; parallel caps already help for real via lower ESR)
- BOM shows "N шт (Sпосл × Pпар)" with per-unit and total price
Already supported and confirmed present (also user-requested): per-coil
independent wire gauge (wire_idx per stage) and computed wire length
(winding_geometry.total_wire_length_m, drives resistance/cost/DB detail).
83 tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Requested during deployment: make the DB maximally detailed, add an
experiment process log, and a web UI to watch runs live.
- optim/objective.py build_detail(): each stored run now records
inter-coil distances, absolute coil positions along the tube, every
component part number/spec, winding geometry, computed physics
(resistance, air inductance, peak current, peak field), and per-stage
outcome (entry/exit velocity, sensor timing, energy breakdown)
- optim/progress_log.py: append-only <db>.log with progress %, feasible
rate, throughput, ETA, and a line on each new best efficiency; wired
into both sweep and evolve
- src/gausse/web/: stdlib-only (http.server) dashboard `gausse serve` --
self-contained HTML polling /api/overview every 3s: counters, KPD
histogram, top-configs table with per-run drill-down, failure reasons,
live log tail. No new dependencies.
- docker-compose.yml: `web` service on port 8000
- 77 tests pass locally and in Docker
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real 5000-run sweep + 600-genome evolution through docker compose run,
not a synthetic smoke test. Two concrete outcomes worth recording:
1. Quantitative confirmation of the sensor discussion from the original
planning conversation: inductive pickup sensor reached only 0.2%
feasibility (6/2965 configs using it), vs ~45-47% for optical/Hall,
because its trigger threshold sits above the fixed launch velocity.
2. Best honest result after the efficiency-metric fix: single stage,
80.97% efficiency, 26.4 m/s, ~1029 RUB in real parts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A real 5000-run sweep through Docker found the evolutionary optimizer's
best genome reporting efficiency=387%. Root cause: efficiency was
computed as exit_kinetic_energy / capacitor_energy_in, but exit kinetic
energy includes the fixed initial_v_mps launch push (a modeling
assumption, not something paid for by the capacitors) -- for a light
enough projectile, that free energy dominates and the ratio blows past 1.
Fixed by using kinetic_energy_delta (the energy the coils actually added)
instead of absolute exit KE. The per-stage energy identity (energy_in =
remaining_cap + dissipated + kinetic_delta) guarantees kinetic_delta <=
energy_in, so this metric can never exceed 1.0 -- provably, not by luck.
Added a regression test with a light projectile + large initial velocity
reproducing the exact failure mode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cosh(z)**2 overflows past |z|~355, which happens routinely once solve_ivp
evaluates the sensor event far from x_sensor over a wide time budget --
correct result (bump->0) but with a RuntimeWarning on every call, which
would flood stderr across a million-run sweep. Clamp the argument to
±20 (already ~1e-17, physically indistinguishable from further growth)
before computing cosh. Caught by running a real 500-run sweep through
Docker as part of Stage 8 verification, not by unit tests alone -- added
a regression test asserting no warning and a finite result far from the
sensor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- report/plots.py: current/field/velocity plots per stage, Agg backend so
it works headless in Docker/on a server
- report/bom.py: itemized bill of materials with real component prices
- report/summary.py: honest text/JSON summary including a
"model limitations" section and a saturation warning flag per stage
- report/animate.py: the visualization the user explicitly asked for --
a GIF of the slug flying through the tube with each coil glowing by its
instantaneous current, reconstructing the pre-trigger ballistic flight
segment (not just the stored discharge phase) for a continuous timeline
- cli.py: sweep/evolve/simulate/report subcommands now actually call the
underlying modules instead of being stubs
- Extended StageResult/StageOutcome with the coil/entry-state fields the
report layer needed (mu_eff, turns, coil_length, entry_x/v) rather than
recomputing them by other means
Verified end-to-end through `docker compose run`, not just pytest: a real
sweep (30 runs, 11 feasible) followed by `report --animate` produced a
correct GIF/plots/BOM/summary for the actual best result found (32.8%
efficiency, 982 RUB) -- not a synthetic fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- optim/worker_context.py: shared per-process DB/bounds init, extracted
from sweep.py so evolutionary.py doesn't reach into another module's
private state
- optim/objective.py: build_run_record() shared between sweep and
evolutionary so both write identically-shaped rows
- optim/evolutionary.py: tournament selection, whole-stage-swap crossover,
elitism, structural mutation (stage count itself evolves), then a serial
Nelder-Mead polish of the best genome's continuous parameters with
discrete component choices frozen
- Found and fixed a real crash: crossover() indexed into an empty
inter_stage_gaps_m list when both parents had only 1 stage (0 gaps),
raising IndexError. Fixed the fallback to only choose from gaps that
actually exist, defaulting to a neutral value (clipped later by
repair()) when neither parent has one. Added a regression test.
- Verified 59/59 tests pass both locally and inside the Docker image
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
optim/sweep.py: ProcessPoolExecutor workers sample+evaluate genomes in
parallel; each result (feasible or not) is pushed through the
queue-backed single writer built in Stage 5. Verified with real
multiprocessing (not mocks): 12-run sweep across 2 worker processes
lands all 12 rows in SQLite with parseable genome/summary JSON, and two
independent sweeps with the same seed produce byte-identical results.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- optim/search_space.py: Genome encodes only indices into the real
component database plus continuous/discrete geometry parameters; number
of stages is itself mutable via add/remove/duplicate-stage operators.
repair() clips everything back into bounds after mutation/crossover so
decode() never sees an invalid genome.
- optim/objective.py: evaluate() decodes a genome, computes real BOM cost
from wire length/price and component prices, runs the full chain, and
scores fitness = efficiency for feasible runs or a soft penalty scaled
by how many stages it got through for infeasible ones (so the GA gets
gradient instead of a wall).
Tested against the real component database (not synthetic fixtures) —
including an explicit infeasible case using the real inductive sensor's
actual sensitivity/threshold values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- storage/schema.py: `runs` table recording every evaluated configuration,
feasible or not, with an honest infeasible_reason instead of dropping
failures
- storage/database.py: WAL-mode SQLite, single-writer-over-a-queue pattern
for safe concurrent writes from many sweep worker processes
- Testing with real multiprocessing.Process (not mocks) caught a genuine
bug: a duplicate run_id raised inside the writer loop and killed the
writer process, silently halting queue drainage for the rest of a
sweep. Fixed by catching the insert error per-record and logging to
stderr instead of crashing; added a regression test for it.
- Verified locally and inside the Docker image (43/43 tests both ways)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dockerfile (non-root user, UID 1000 to match typical host users so bind
mounts don't end up root-owned) + docker-compose.yml with a ./results
volume for the SQLite database and reports that Stage 5/6 will write.
Verified by actually building the image and running the full test suite
and CLI inside the container, not just writing the files and assuming
they work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wire (Cu/Al ПЭТВ-2), capacitors (350-450V electrolytic bank), switches
(КУ202Н/BT151 SCR, IRFP250/IRFP4468 MOSFET, IRG4PC50F IGBT), sensors
(A3144E Hall, TCST2103 optical, LM393-based inductive pickup), and
projectile materials (Ст3, Сталь 10, армко-железо) sourced from
procontact74.ru/chipdip.ru/cable.ru research.
Every entry's `source` field states plainly whether the number is a
REAL scraped retail price/spec (with URL) or an ОЦЕНКА (estimate) with
its basis (e.g. aluminum magnet wire has no self-service retail price
in RU stores, so it's priced as a fraction of equivalent copper) — no
value is presented as real when it isn't. Added a smoke test proving
the real database runs end-to-end through the full physics + chaining
pipeline, not just against synthetic test fixtures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- sim/coilgun.py: chains StageConfig results through a global coordinate,
stops honestly on the first infeasible stage (with which stage and why),
and computes overall efficiency = exit KE / total capacitor energy in
- Discovered while wiring up the chain: a high-capacitance single-stage test
configuration let current stay high past the coil center, decelerating and
reversing the slug (exit velocity negative). This is a real coilgun tuning
failure mode (not a bug) since KE ~ v^2 doesn't care about direction and
the energy balance still holds to ~1%. Kept it as an explicit regression
test (test_overpowered_discharge_can_fling_slug_backward) instead of
quietly picking parameters that hide it, and tightened the "kinetic energy
increases" test to also require forward (exit_v > 0) motion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- physics/sensors.py: optical/Hall (velocity-independent) and inductive
(velocity-scaled, sech^2 spatial sensitivity) trigger events for solve_ivp
- sim/stage.py: flight-to-trigger -> fire delay -> discharge -> energy
accounting, returning StageResult(feasible=False, reason=...) instead of
raising when a sensor never fires or discharge never commutates
- Found and fixed a real bug caught by the energy-conservation test: the
saturation clamp was applied to the mechanical force but not the
electrical back-EMF term, silently breaking energy balance by ~15%.
Removed the dynamic clamp (documented as a deferred nonlinear-L(x,I)
limitation) and kept saturation as a diagnostic-only warning
(StageResult.saturation_warning) so numbers stay honest rather than
quietly wrong. Balance error is now ~0.02%.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- components/schema.py + database.py: typed loader for real retail parts
(wire, capacitors, switches, sensors, projectile materials)
- physics/inductance.py: multilayer solenoid inductance (Wheeler) with
ferromagnetic-slug coupling via smooth overlap model and analytic dL/dx
- physics/force.py: F = 0.5*I^2*dL/dx with a saturation clamp
- physics/circuit.py: coupled [Q,I,x,v] discharge ODE
- Validated against textbook RLC analytical solutions (under/over/critically
damped) and energy conservation, not just spot-checked by eye
- PLAN.md updated with animation (per-run field visualization) and
GPU-accelerated batch sweep as explicit later stages
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
src-layout Python package with CLI stub (sweep/evolve/simulate/report
subcommands), pyproject.toml pinned to numpy/scipy/matplotlib, and PLAN.md
tracking the implementation roadmap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>