The user caught the evolution reporting 83.9% efficiency, which is
unphysical (real coilguns are single-digit %). Root cause: the model's
only loss channel was copper resistance; iron had no losses at all, so
the reluctance force came "for free" and the optimizer climbed into that
corner.
Adds eddy-current loss: the solid steel slug acts as a shorted secondary
(1-turn transformer), and its reflected resistance R_eddy = (wM)^2/R_e
is added to the circuit while the slug is inside the coil (x overlap(x)).
Energy now honestly goes to slug heating instead of kinetic. The formula
matches the classical solid-cylinder eddy loss (P ~ sigma*w^2*B^2*a^4),
so it's physically grounded, not tuned to a target. Wired through
schema/JSON (slug resistivity), losses.py, circuit.py, stage.py, and the
GPU batch integrator; energy conservation still holds (0.025%).
Effect: best efficiency 83.9% -> ~47%, and the distribution is now
realistic (median ~0%, most configs single-digit). 47% is still an
optimistic ceiling -- it's the optimizer's single best exploit, and the
model still omits tube friction, air drag, skin-effect field penetration
(skin depth ~0.44mm < slug radius), and timing imperfection. Hysteresis
computed but not in the dynamics (negligible, ~1e-4 J vs eddy). 89 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The expensive part of the sim (the 4-state [Q,I,x,v] discharge ODE) now
has a vectorized fixed-step RK4 integrator that runs N configs at once as
arrays, with the same saturating-iron physics as the scipy path. One code
path runs on numpy (CPU) or cupy (GPU) via a backend shim
(gpu/backend.py) -- so it's developed and validated WITHOUT a GPU, then
runs on GPU unchanged.
Honesty gate (per the project's no-smoke-and-mirrors rule): validated
against the scipy solve_ivp reference on 4 configs -- exit velocity, peak
current, and dissipated energy all match within ~1-3%, plus a batched
energy-conservation check. GPU speed is worthless if the numbers differ,
so this test is the point.
Benchmark: 5000 configs in 6.8s = 733/s single-threaded numpy, already
faster than the 5-core scipy sweep (~430/s); cupy parallelizes the same
arithmetic for the real win at large N.
Not yet batched: flight-to-trigger, multi-stage chaining, genome decode
(so this isn't a drop-in sweep replacement yet -- it's the validated
core). Next: batch the full pipeline + run cupy on the server GPU.
89 tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Web was slow because run_detail and the plot/GIF renderers loaded the
ENTIRE runs table to find one row by id (O(n) on a 200k-row DB, seconds
per request). Now query by PRIMARY KEY (run_id): run_detail drops from
seconds to ~1ms. The KPD histogram is aggregated in SQL instead of
streaming every feasible row into Python.
- Dashboard now toggles the top table between "по КПД" and "по скорости"
(top_by_velocity, indexed on exit_velocity_mps); the best-of card shows
both best efficiency and best exit velocity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>