Ревью хранения: честная детализация прогона + общий rebuild + чистка мёртвого кода
По итогам код-ревью коммита5c11999: - run_detail: шапка и энергобаланс — СОХРАНЁННЫЕ при прогоне значения (пересчитанный на лету detail мог противоречить им: GPU-строки и старые model_version считались другим движком/моделью, а energy_breakdown прятался за feasible пересчёта). Пересчёт теперь явно помечен recomputed_with_model_version и обёрнут в try/except — /api/run был единственным эндпоинтом, где падение пересимуляции роняло ответ. - web/rebuild.py: один общий путь «строка БД -> симуляция» для детализации и рендера (раньше два дубля; клик по прогону гонял одну и ту же симуляцию до 5 раз и перечитывал базу компонентов с диска на каждый запрос — теперь кэш компонентов + кэш последних симуляций по run_id). - Мёртвый код после5c11999: ветка build_details=True (единственный вызов передавал False; дефолт True возвращал бы как раз ту дорогую запись, от которой уходили), _FEASIBLE, параметр bounds у _record, поле EvaluationResult.detail и его мёртвая ветка в build_run_record. - overview: n_stages=None (морда покажет «—»), а не 0, когда сводки нет. - PLAN.md/schema.py: убраны утверждения, что полный build_detail хранится в каждой записи (устарело с5c11999), отмечена старая форма записей.
This commit is contained in:
8
PLAN.md
8
PLAN.md
@@ -55,7 +55,7 @@ L(x, I)-модель с coenergy-выводом силы — в разделе "
|
|||||||
- **Найден и исправлен реальный баг именно на этом этапе**: КПД лучшего генома после `evolve` показал 387% — оказалось, `efficiency` считался как `exit_kinetic_energy / energy_in`, а `exit_kinetic_energy` включает фиксированный "бесплатный" толчок `initial_v_mps`, не учтённый в `energy_in`. Для лёгкого снаряда этот толчок доминировал и КПД пробивал 100%. Исправлено на `kinetic_energy_delta / energy_in` (энергия, реально добавленная катушками) — эта величина математически не может превысить 1 (следует из поэтапного энергобаланса). После исправления лучший результат на тот момент: **1 ступень, КПД 80.97%, скорость 26.4 м/с, снаряд Ст3 ⌀4мм x 30мм, стоимость 1029₽** (провод cu-petv2-1.0mm, конденсатор 22мкФ/400В, ключ IRG4PC50F, оптический датчик). *(Историческая запись: это физика v1 БЕЗ потерь в железе, скина и трения — после их добавления такие КПД невозможны, см. «хронологию честности» ниже.)*
|
- **Найден и исправлен реальный баг именно на этом этапе**: КПД лучшего генома после `evolve` показал 387% — оказалось, `efficiency` считался как `exit_kinetic_energy / energy_in`, а `exit_kinetic_energy` включает фиксированный "бесплатный" толчок `initial_v_mps`, не учтённый в `energy_in`. Для лёгкого снаряда этот толчок доминировал и КПД пробивал 100%. Исправлено на `kinetic_energy_delta / energy_in` (энергия, реально добавленная катушками) — эта величина математически не может превысить 1 (следует из поэтапного энергобаланса). После исправления лучший результат на тот момент: **1 ступень, КПД 80.97%, скорость 26.4 м/с, снаряд Ст3 ⌀4мм x 30мм, стоимость 1029₽** (провод cu-petv2-1.0mm, конденсатор 22мкФ/400В, ключ IRG4PC50F, оптический датчик). *(Историческая запись: это физика v1 БЕЗ потерь в железе, скина и трения — после их добавления такие КПД невозможны, см. «хронологию честности» ниже.)*
|
||||||
- **Честное сравнение датчиков** (запрос из первого обсуждения плана) — на 5000 прогонах: индукционный датчик (`inductive-pickup-lm393`) дал **0.2% реализуемых** конфигураций (6 из 2965, где он стоял хотя бы на одной ступени), тогда как оптический (TCST2103) и Холла (A3144E) — **~45-47%** реализуемых каждый. Это количественно подтверждает опасение, высказанное в самом начале обсуждения плана: индукционная катушка-датчик ненадёжна именно потому, что требует скорости выше её порога (здесь — фиксированный старт 3 м/с как раз ниже порога срабатывания реального компонента), тогда как оптика/Холл не зависят от скорости.
|
- **Честное сравнение датчиков** (запрос из первого обсуждения плана) — на 5000 прогонах: индукционный датчик (`inductive-pickup-lm393`) дал **0.2% реализуемых** конфигураций (6 из 2965, где он стоял хотя бы на одной ступени), тогда как оптический (TCST2103) и Холла (A3144E) — **~45-47%** реализуемых каждый. Это количественно подтверждает опасение, высказанное в самом начале обсуждения плана: индукционная катушка-датчик ненадёжна именно потому, что требует скорости выше её порога (здесь — фиксированный старт 3 м/с как раз ниже порога срабатывания реального компонента), тогда как оптика/Холл не зависят от скорости.
|
||||||
- Итог: сквозная цепочка (реальные компоненты → физика → поиск → SQLite → отчёт с графиками/BOM/анимацией) работает и произвела не выдуманный, а посчитанный и перепроверенный результат.
|
- Итог: сквозная цепочка (реальные компоненты → физика → поиск → SQLite → отчёт с графиками/BOM/анимацией) работает и произвела не выдуманный, а посчитанный и перепроверенный результат.
|
||||||
- [x] **Обогащённая БД + лог процесса + веб-морда** (по запросу пользователя при деплое): каждая запись `runs.decoded_summary_json` теперь содержит расстояния между катушками (`inter_stage_gaps_m`), абсолютные позиции катушек вдоль трубы, все номиналы деталей, геометрию намотки, вычисленную физику (индуктивность/сопротивление/пиковый ток/поле) и результат каждой ступени (вход/выход скорость, тайминги датчика, энергобаланс) — см. `optim/objective.py::build_detail`. Ход эксперимента пишется в `<db>.log` (`optim/progress_log.py`): прогресс, доля реализуемых, скорость, ETA, отметки нового лучшего КПД. Веб-дашборд `gausse serve` (`src/gausse/web/`, только stdlib `http.server`, self-contained HTML) — счётчики, гистограмма КПД, топ конфигураций с drill-down, причины отказа, хвост лога, автообновление 3с. В `docker-compose.yml` сервис `web` на порту 8000. 77 тестов, проверено в Docker.
|
- [x] **Обогащённая БД + лог процесса + веб-морда** (по запросу пользователя при деплое): каждая запись `runs.decoded_summary_json` теперь содержит расстояния между катушками (`inter_stage_gaps_m`), абсолютные позиции катушек вдоль трубы, все номиналы деталей, геометрию намотки, вычисленную физику (индуктивность/сопротивление/пиковый ток/поле) и результат каждой ступени (вход/выход скорость, тайминги датчика, энергобаланс) — см. `optim/objective.py::build_detail`. *(УСТАРЕЛО с 2026-07-11, коммит 5c11999: полный build_detail больше НЕ хранится на каждый прогон — база раздувалась до 70ГБ; в decoded_summary_json теперь дешёвая сводка decoded_summary(), а полная детализация пересимулируется на лету из genome_json при просмотре прогона — web/rebuild.py + web/stats.run_detail.)* Ход эксперимента пишется в `<db>.log` (`optim/progress_log.py`): прогресс, доля реализуемых, скорость, ETA, отметки нового лучшего КПД. Веб-дашборд `gausse serve` (`src/gausse/web/`, только stdlib `http.server`, self-contained HTML) — счётчики, гистограмма КПД, топ конфигураций с drill-down, причины отказа, хвост лога, автообновление 3с. В `docker-compose.yml` сервис `web` на порту 8000. 77 тестов, проверено в Docker.
|
||||||
|
|
||||||
## Батареи конденсаторов + графики в веб-морде (по запросу пользователя)
|
## Батареи конденсаторов + графики в веб-морде (по запросу пользователя)
|
||||||
|
|
||||||
@@ -168,11 +168,13 @@ L(x, I)-модель с coenergy-выводом силы — в разделе "
|
|||||||
катушку — repair()).
|
катушку — repair()).
|
||||||
- **Ссылки на магазины** (`url`) у каждого компонента: в JSON-базе, BOM
|
- **Ссылки на магазины** (`url`) у каждого компонента: в JSON-базе, BOM
|
||||||
(«Купить») и в детализации прогона.
|
(«Купить») и в детализации прогона.
|
||||||
- **Максимально детальная запись** в БД (`build_detail`): труба
|
- **Максимально детальная запись** (`build_detail`): труба
|
||||||
внутр/стенка/внеш, масса снаряда в граммах и влезает-ли-в-бор,
|
внутр/стенка/внеш, масса снаряда в граммах и влезает-ли-в-бор,
|
||||||
внутр/внеш диаметры катушки, состав банки конденсаторов, DC/AC
|
внутр/внеш диаметры катушки, состав банки конденсаторов, DC/AC
|
||||||
сопротивление обмотки, все позиции датчиков и катушек вдоль трубы,
|
сопротивление обмотки, все позиции датчиков и катушек вдоль трубы,
|
||||||
энергобаланс каждой ступени.
|
энергобаланс каждой ступени. *(С 2026-07-11 НЕ хранится в БД построчно,
|
||||||
|
а строится на лету из genome_json при просмотре прогона/отчёте —
|
||||||
|
хранение полной детализации на 12.9М строк раздуло базу до 70ГБ.)*
|
||||||
- **Анимация переделана** (была «время вырезается, результата не видно»):
|
- **Анимация переделана** (была «время вырезается, результата не видно»):
|
||||||
кадры на равномерной сетке ФИЗИЧЕСКОГО времени (а не по индексам массива,
|
кадры на равномерной сетке ФИЗИЧЕСКОГО времени (а не по индексам массива,
|
||||||
из-за чего плотные точки разряда съедали все кадры и подлёт выпадал),
|
из-за чего плотные точки разряда съедали все кадры и подлёт выпадал),
|
||||||
|
|||||||
@@ -24,17 +24,14 @@ from pathlib import Path
|
|||||||
from gausse.components.database import ComponentDatabase
|
from gausse.components.database import ComponentDatabase
|
||||||
from gausse.gpu.backend import get_backend, to_cpu
|
from gausse.gpu.backend import get_backend, to_cpu
|
||||||
from gausse.gpu.batch_integrator import NO_SATURATION_I_SAT, BatchDischargeParams, integrate_batch_discharge
|
from gausse.gpu.batch_integrator import NO_SATURATION_I_SAT, BatchDischargeParams, integrate_batch_discharge
|
||||||
from gausse.optim.objective import MODEL_VERSION, build_detail
|
from gausse.optim.objective import MODEL_VERSION
|
||||||
from gausse.optim.progress_log import ProgressLogger, default_log_path
|
from gausse.optim.progress_log import ProgressLogger, default_log_path
|
||||||
from gausse.optim.search_space import SearchBounds, decode, genome_to_dict, sample_genome
|
from gausse.optim.search_space import SearchBounds, decode, genome_to_dict, sample_genome
|
||||||
from gausse.physics.constants import switch_pulse_limit_a
|
from gausse.physics.constants import switch_pulse_limit_a
|
||||||
from gausse.sim.coilgun import CoilgunResult
|
|
||||||
from gausse.sim.stage import build_stage_physics
|
from gausse.sim.stage import build_stage_physics
|
||||||
from gausse.storage.database import insert_runs, open_connection
|
from gausse.storage.database import insert_runs, open_connection
|
||||||
from gausse.storage.schema import RunRecord
|
from gausse.storage.schema import RunRecord
|
||||||
|
|
||||||
_FEASIBLE = CoilgunResult(feasible=True, stage_outcomes=[])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StagePre:
|
class StagePre:
|
||||||
@@ -252,20 +249,19 @@ def _simulate_states(xp, states: list) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def evaluate_genomes_gpu(
|
def evaluate_genomes_gpu(
|
||||||
xp, genomes: list, db: ComponentDatabase, bounds: SearchBounds,
|
xp, genomes: list, db: ComponentDatabase, bounds: SearchBounds, executor=None,
|
||||||
build_details: bool = True, executor=None,
|
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Оценка списка геномов ОДНИМ батчем (для эволюции): та же физика и та же
|
"""Оценка списка геномов ОДНИМ батчем (для эволюции): та же физика и та же
|
||||||
формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных
|
формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных
|
||||||
ступеней), но разряды всех геномов интегрируются вместе на GPU/numpy.
|
ступеней), но разряды всех геномов интегрируются вместе на GPU/numpy.
|
||||||
|
|
||||||
build_details=False — не строить detail/стоимость (фитнесу они не нужны):
|
Возвращает только то, что нужно фитнесу (без detail/стоимости): записи
|
||||||
их параллельно собирает пул процессов (`gpu_record_worker`).
|
для БД параллельно собирает пул процессов (`gpu_record_worker`).
|
||||||
executor — пул для ПАРАЛЛЕЛЬНОГО предвычисления физики ступеней
|
executor — пул для ПАРАЛЛЕЛЬНОГО предвычисления физики ступеней
|
||||||
(precompute_worker): без него один поток Python готовит физику всей
|
(precompute_worker): без него один поток Python готовит физику всей
|
||||||
популяции и GPU простаивает, ожидая данные.
|
популяции и GPU простаивает, ожидая данные.
|
||||||
"""
|
"""
|
||||||
from gausse.optim.objective import EvaluationResult, compute_cost_rub
|
from gausse.optim.objective import EvaluationResult
|
||||||
|
|
||||||
if executor is not None:
|
if executor is not None:
|
||||||
pres = list(executor.map(precompute_worker, genomes, chunksize=64))
|
pres = list(executor.map(precompute_worker, genomes, chunksize=64))
|
||||||
@@ -280,35 +276,25 @@ def evaluate_genomes_gpu(
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for st in states:
|
for st in states:
|
||||||
if build_details:
|
|
||||||
cfg, _, _ = decode(st.genome, db, bounds)
|
|
||||||
cost = compute_cost_rub(cfg, db)
|
|
||||||
ok = st.alive and st.energy_in_j > 0
|
ok = st.alive and st.energy_in_j > 0
|
||||||
detail = build_detail(
|
|
||||||
cfg, _FEASIBLE if ok else None, db, st.pre.initial_x_m, st.pre.initial_v_mps,
|
|
||||||
st.genome.tube_inner_d_m, st.genome.tube_wall_m,
|
|
||||||
) if build_details else None
|
|
||||||
if not build_details:
|
|
||||||
cost = None
|
|
||||||
if ok:
|
if ok:
|
||||||
eff = st.kinetic_delta_j / st.energy_in_j
|
eff = st.kinetic_delta_j / st.energy_in_j
|
||||||
results.append(EvaluationResult(
|
results.append(EvaluationResult(
|
||||||
feasible=True, cost_rub=cost, fitness=eff, efficiency=eff,
|
feasible=True, cost_rub=None, fitness=eff, efficiency=eff,
|
||||||
exit_velocity_mps=st.cur_v,
|
exit_velocity_mps=st.cur_v,
|
||||||
energy_breakdown={
|
energy_breakdown={
|
||||||
"total_energy_in_j": st.energy_in_j,
|
"total_energy_in_j": st.energy_in_j,
|
||||||
"total_kinetic_energy_delta_j": st.kinetic_delta_j,
|
"total_kinetic_energy_delta_j": st.kinetic_delta_j,
|
||||||
},
|
},
|
||||||
detail=detail,
|
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
n_stages = len(st.pre.stages)
|
n_stages = len(st.pre.stages)
|
||||||
progress = (st.failed_stage_index or 0) / n_stages if n_stages else 0.0
|
progress = (st.failed_stage_index or 0) / n_stages if n_stages else 0.0
|
||||||
results.append(EvaluationResult(
|
results.append(EvaluationResult(
|
||||||
feasible=False, cost_rub=cost, fitness=-1.0 + progress,
|
feasible=False, cost_rub=None, fitness=-1.0 + progress,
|
||||||
reason=st.reason or "нет ни одной сработавшей ступени",
|
reason=st.reason or "нет ни одной сработавшей ступени",
|
||||||
failed_stage_index=st.failed_stage_index,
|
failed_stage_index=st.failed_stage_index,
|
||||||
energy_breakdown={}, detail=detail,
|
energy_breakdown={},
|
||||||
))
|
))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -383,10 +369,10 @@ def run_gpu_sweep(
|
|||||||
for st in states:
|
for st in states:
|
||||||
if st.alive and st.energy_in_j > 0:
|
if st.alive and st.energy_in_j > 0:
|
||||||
eff = st.kinetic_delta_j / st.energy_in_j
|
eff = st.kinetic_delta_j / st.energy_in_j
|
||||||
records.append(_record(st, db, bounds, backend, eff, st.cur_v))
|
records.append(_record(st, db, backend, eff, st.cur_v))
|
||||||
n_feasible += 1
|
n_feasible += 1
|
||||||
else:
|
else:
|
||||||
records.append(_record(st, db, bounds, backend, None, None))
|
records.append(_record(st, db, backend, None, None))
|
||||||
insert_runs(conn, records)
|
insert_runs(conn, records)
|
||||||
for r in records:
|
for r in records:
|
||||||
logger.update(r.feasible, r.efficiency)
|
logger.update(r.feasible, r.efficiency)
|
||||||
@@ -397,7 +383,7 @@ def run_gpu_sweep(
|
|||||||
return {"n_runs": done, "n_feasible": n_feasible, "backend": backend}
|
return {"n_runs": done, "n_feasible": n_feasible, "backend": backend}
|
||||||
|
|
||||||
|
|
||||||
def _record(st: _State, db, bounds: SearchBounds, backend, efficiency, exit_v):
|
def _record(st: _State, db, backend, efficiency, exit_v):
|
||||||
from gausse.optim.objective import decoded_summary
|
from gausse.optim.objective import decoded_summary
|
||||||
|
|
||||||
feasible = efficiency is not None
|
feasible = efficiency is not None
|
||||||
|
|||||||
@@ -168,9 +168,7 @@ def run_evolution(
|
|||||||
if gpu_xp is not None:
|
if gpu_xp is not None:
|
||||||
from gausse.gpu.batch_sweep import evaluate_genomes_gpu, gpu_record_worker
|
from gausse.gpu.batch_sweep import evaluate_genomes_gpu, gpu_record_worker
|
||||||
|
|
||||||
results = evaluate_genomes_gpu(
|
results = evaluate_genomes_gpu(gpu_xp, flat, db, bounds, executor=executor)
|
||||||
gpu_xp, flat, db, bounds, build_details=False, executor=executor
|
|
||||||
)
|
|
||||||
pairs_flat = list(zip(flat, results))
|
pairs_flat = list(zip(flat, results))
|
||||||
payload = [
|
payload = [
|
||||||
(g, {
|
(g, {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class EvaluationResult:
|
|||||||
efficiency: float | None = None
|
efficiency: float | None = None
|
||||||
exit_velocity_mps: float | None = None
|
exit_velocity_mps: float | None = None
|
||||||
energy_breakdown: dict = None
|
energy_breakdown: dict = None
|
||||||
detail: dict = None
|
|
||||||
|
|
||||||
|
|
||||||
def compute_cost_rub(config, db: ComponentDatabase) -> float:
|
def compute_cost_rub(config, db: ComponentDatabase) -> float:
|
||||||
@@ -261,7 +260,7 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i
|
|||||||
|
|
||||||
def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> EvaluationResult:
|
def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> EvaluationResult:
|
||||||
"""detail НЕ строится здесь: build_detail — дорогая (полная физика по
|
"""detail НЕ строится здесь: build_detail — дорогая (полная физика по
|
||||||
каждой ступени) и не нужна ни фитнесу, ни хранению — geneome_json уже
|
каждой ступени) и не нужна ни фитнесу, ни хранению — genome_json уже
|
||||||
достаточен, чтобы воспроизвести всё через decode()+build_detail() на
|
достаточен, чтобы воспроизвести всё через decode()+build_detail() на
|
||||||
лету (см. web/stats.run_detail). build_run_record() кладёт в БД дешёвую
|
лету (см. web/stats.run_detail). build_run_record() кладёт в БД дешёвую
|
||||||
decoded_summary() вместо этого.
|
decoded_summary() вместо этого.
|
||||||
@@ -300,7 +299,7 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva
|
|||||||
|
|
||||||
|
|
||||||
def build_run_record(genome: Genome, result: EvaluationResult, db: ComponentDatabase, search_mode: str) -> RunRecord:
|
def build_run_record(genome: Genome, result: EvaluationResult, db: ComponentDatabase, search_mode: str) -> RunRecord:
|
||||||
detail = result.detail if result.detail is not None else decoded_summary(genome, db)
|
detail = decoded_summary(genome, db)
|
||||||
return RunRecord(
|
return RunRecord(
|
||||||
run_id=str(uuid.uuid4()),
|
run_id=str(uuid.uuid4()),
|
||||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ CREATE TABLE IF NOT EXISTS runs (
|
|||||||
-- всё через decode()+build_detail() на лету (см. web/stats.run_detail);
|
-- всё через decode()+build_detail() на лету (см. web/stats.run_detail);
|
||||||
-- хранить дорогую детализацию для каждого из миллионов прогонов
|
-- хранить дорогую детализацию для каждого из миллионов прогонов
|
||||||
-- бессмысленно — так база росла на пустом месте (70ГБ на 12.9М строк).
|
-- бессмысленно — так база росла на пустом месте (70ГБ на 12.9М строк).
|
||||||
|
-- В базах, созданных ДО 2026-07-11, эта колонка содержит старый полный
|
||||||
|
-- build_detail (другая форма записей ступеней) — учитывать при чтении.
|
||||||
decoded_summary_json TEXT NOT NULL,
|
decoded_summary_json TEXT NOT NULL,
|
||||||
feasible INTEGER NOT NULL,
|
feasible INTEGER NOT NULL,
|
||||||
infeasible_reason TEXT,
|
infeasible_reason TEXT,
|
||||||
|
|||||||
60
src/gausse/web/rebuild.py
Normal file
60
src/gausse/web/rebuild.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Восстановление симуляции прогона из genome_json — ОДИН общий путь для
|
||||||
|
детализации (stats.run_detail) и рендера графиков/анимации (render.py).
|
||||||
|
|
||||||
|
Раньше эти два места дублировали decode()+run_coilgun() независимо, причём
|
||||||
|
каждый клик по прогону запускал одну и ту же симуляцию до 5 раз (детализация
|
||||||
|
+ 3 графика + GIF), а run_detail ещё и перечитывал базу компонентов с диска
|
||||||
|
на каждый запрос. Здесь: кэш базы компонентов + маленький кэш последних
|
||||||
|
симуляций по run_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from gausse.components.database import ComponentDatabase
|
||||||
|
from gausse.optim.search_space import SearchBounds, decode, genome_from_dict
|
||||||
|
from gausse.sim.coilgun import run_coilgun
|
||||||
|
from gausse.storage.database import fetch_run_by_id, open_connection
|
||||||
|
|
||||||
|
_db_cache: ComponentDatabase | None = None
|
||||||
|
_sim_lock = threading.Lock()
|
||||||
|
_sim_cache: dict[str, tuple] = {} # run_id -> (row, genome, config, result, x0, v0)
|
||||||
|
_SIM_CACHE_MAX = 8 # клик по прогону = detail + 3 графика + GIF того же run_id
|
||||||
|
|
||||||
|
|
||||||
|
def components_db() -> ComponentDatabase:
|
||||||
|
global _db_cache
|
||||||
|
if _db_cache is None:
|
||||||
|
_db_cache = ComponentDatabase.load()
|
||||||
|
return _db_cache
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_run(db_path: Path, run_id: str):
|
||||||
|
"""(row, genome, config, result, initial_x_m, initial_v_mps) либо None,
|
||||||
|
если run_id нет в базе. Ошибки пересимуляции пробрасываются наружу —
|
||||||
|
вызывающий решает, что показать (детализация не должна ронять сервер).
|
||||||
|
"""
|
||||||
|
with _sim_lock:
|
||||||
|
cached = _sim_cache.get(run_id)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
conn = open_connection(db_path)
|
||||||
|
try:
|
||||||
|
row = fetch_run_by_id(conn, run_id)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
genome = genome_from_dict(json.loads(row.genome_json))
|
||||||
|
config, initial_x_m, initial_v_mps = decode(genome, components_db(), SearchBounds())
|
||||||
|
result = run_coilgun(config)
|
||||||
|
|
||||||
|
entry = (row, genome, config, result, initial_x_m, initial_v_mps)
|
||||||
|
with _sim_lock:
|
||||||
|
if len(_sim_cache) >= _SIM_CACHE_MAX:
|
||||||
|
_sim_cache.clear()
|
||||||
|
_sim_cache[run_id] = entry
|
||||||
|
return entry
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -18,15 +17,11 @@ matplotlib.use("Agg")
|
|||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from gausse.components.database import ComponentDatabase
|
|
||||||
from gausse.optim.search_space import SearchBounds, decode, genome_from_dict
|
|
||||||
from gausse.report.animate import animate_run
|
from gausse.report.animate import animate_run
|
||||||
from gausse.report.plots import plot_stage_currents, plot_stage_fields, plot_velocity_vs_position
|
from gausse.report.plots import plot_stage_currents, plot_stage_fields, plot_velocity_vs_position
|
||||||
from gausse.sim.coilgun import run_coilgun
|
from gausse.web.rebuild import rebuild_run
|
||||||
from gausse.storage.database import fetch_run_by_id, open_connection
|
|
||||||
|
|
||||||
_render_lock = threading.Lock()
|
_render_lock = threading.Lock()
|
||||||
_db_cache: ComponentDatabase | None = None
|
|
||||||
|
|
||||||
_PLOT_BUILDERS = {
|
_PLOT_BUILDERS = {
|
||||||
"velocity": plot_velocity_vs_position,
|
"velocity": plot_velocity_vs_position,
|
||||||
@@ -35,24 +30,12 @@ _PLOT_BUILDERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _components_db() -> ComponentDatabase:
|
|
||||||
global _db_cache
|
|
||||||
if _db_cache is None:
|
|
||||||
_db_cache = ComponentDatabase.load()
|
|
||||||
return _db_cache
|
|
||||||
|
|
||||||
|
|
||||||
def _rebuild_result(db_path: Path, run_id: str):
|
def _rebuild_result(db_path: Path, run_id: str):
|
||||||
conn = open_connection(db_path)
|
rebuilt = rebuild_run(db_path, run_id) # общий кэш симуляций с run_detail
|
||||||
try:
|
if rebuilt is None:
|
||||||
row = fetch_run_by_id(conn, run_id)
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
if row is None:
|
|
||||||
return None
|
return None
|
||||||
genome = genome_from_dict(json.loads(row.genome_json))
|
_row, _genome, config, result, _ix, _iv = rebuilt
|
||||||
config, _ix, _iv = decode(genome, _components_db(), SearchBounds())
|
return config, result
|
||||||
return config, run_coilgun(config)
|
|
||||||
|
|
||||||
|
|
||||||
def render_plot_png(db_path: Path, run_id: str, kind: str) -> bytes | None:
|
def render_plot_png(db_path: Path, run_id: str, kind: str) -> bytes | None:
|
||||||
|
|||||||
@@ -73,10 +73,13 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di
|
|||||||
out = []
|
out = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
summary = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {}
|
summary = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {}
|
||||||
|
stages = summary.get("stages")
|
||||||
out.append({
|
out.append({
|
||||||
"run_id": r.run_id, "search_mode": r.search_mode, "efficiency": r.efficiency,
|
"run_id": r.run_id, "search_mode": r.search_mode, "efficiency": r.efficiency,
|
||||||
"exit_velocity_mps": r.exit_velocity_mps, "cost_rub": r.cost_rub,
|
"exit_velocity_mps": r.exit_velocity_mps, "cost_rub": r.cost_rub,
|
||||||
"n_stages": len(summary.get("stages", [])), "timestamp": r.timestamp,
|
# None (не 0), когда сводки нет — морда покажет «—»
|
||||||
|
"n_stages": len(stages) if stages is not None else None,
|
||||||
|
"timestamp": r.timestamp,
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -115,15 +118,18 @@ def _last_cycle_best(log_lines: list[str]) -> float | None:
|
|||||||
|
|
||||||
|
|
||||||
def run_detail(db_path: Path, run_id: str) -> dict | None:
|
def run_detail(db_path: Path, run_id: str) -> dict | None:
|
||||||
"""Подробности одного прогона — НЕ читаются из БД (там только дешёвая
|
"""Подробности одного прогона. Шапка (feasible/КПД/скорость/энергобаланс)
|
||||||
decoded_summary), а пересчитываются на лету из genome_json тем же
|
— СОХРАНЁННЫЕ при прогоне значения: они посчитаны тем движком и той
|
||||||
decode()+build_detail(), что и остальной рендер веб-морды (см.
|
версией физики, что записали строку, и только они сравнимы между собой.
|
||||||
web/render.py). Это единичный запрос по клику, не миллионы прогонов —
|
Полная физика по ступеням в БД не хранится (см. schema.py) — она
|
||||||
пересимулировать дешевле, чем хранить полную физику для каждого."""
|
пересимулируется на лету из genome_json через общий web/rebuild.py.
|
||||||
from gausse.components.database import ComponentDatabase
|
Пересчёт идёт ТЕКУЩЕЙ моделью на CPU и может расходиться с сохранёнными
|
||||||
from gausse.optim.objective import build_detail
|
числами (GPU-строки, старые model_version) — поэтому detail явно помечен
|
||||||
from gausse.optim.search_space import SearchBounds, decode, genome_from_dict
|
версией пересчёта, а его ошибка не роняет запрос (раньше /api/run был
|
||||||
from gausse.sim.coilgun import run_coilgun
|
единственным эндпоинтом без защиты)."""
|
||||||
|
from gausse.optim.objective import MODEL_VERSION, build_detail
|
||||||
|
|
||||||
|
from gausse.web.rebuild import components_db, rebuild_run
|
||||||
|
|
||||||
conn = open_connection(db_path)
|
conn = open_connection(db_path)
|
||||||
try:
|
try:
|
||||||
@@ -133,22 +139,15 @@ def run_detail(db_path: Path, run_id: str) -> dict | None:
|
|||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
genome = genome_from_dict(json.loads(row.genome_json))
|
try:
|
||||||
db = ComponentDatabase.load()
|
_, genome, config, result, initial_x_m, initial_v_mps = rebuild_run(db_path, run_id)
|
||||||
bounds = SearchBounds()
|
detail = build_detail(
|
||||||
config, initial_x_m, initial_v_mps = decode(genome, db, bounds)
|
config, result, components_db(), initial_x_m, initial_v_mps,
|
||||||
result = run_coilgun(config)
|
genome.tube_inner_d_m, genome.tube_wall_m,
|
||||||
detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m)
|
)
|
||||||
energy_breakdown = (
|
detail["recomputed_with_model_version"] = MODEL_VERSION
|
||||||
{
|
except Exception as exc: # деталь — вспомогательная, шапка важнее
|
||||||
"total_energy_in_j": result.total_energy_in_j,
|
detail = {"error": f"пересимуляция не удалась: {exc}"}
|
||||||
"total_energy_dissipated_j": result.total_energy_dissipated_j,
|
|
||||||
"total_kinetic_energy_delta_j": result.total_kinetic_energy_delta_j,
|
|
||||||
"exit_kinetic_energy_j": result.exit_kinetic_energy_j,
|
|
||||||
}
|
|
||||||
if result.feasible
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"run_id": row.run_id,
|
"run_id": row.run_id,
|
||||||
@@ -163,5 +162,6 @@ def run_detail(db_path: Path, run_id: str) -> dict | None:
|
|||||||
"cost_rub": row.cost_rub,
|
"cost_rub": row.cost_rub,
|
||||||
"timestamp": row.timestamp,
|
"timestamp": row.timestamp,
|
||||||
"detail": detail,
|
"detail": detail,
|
||||||
"energy_breakdown": energy_breakdown,
|
# сохранённый при записи баланс — консистентен с feasible/КПД шапки
|
||||||
|
"energy_breakdown": json.loads(row.energy_breakdown_json) if row.energy_breakdown_json else {},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user