From 5c119990b72ee9d0ec886b93d4dd7c3c329b32ec Mon Sep 17 00:00:00 2001 From: jze9 Date: Sat, 11 Jul 2026 21:50:54 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A5=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20genome=5Fjson+=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D1=8F=D1=80=D1=8B=20=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D0=BE=D0=BD,=20=D0=B4=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=81=D1=82=D1=80=D0=BE=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=B5=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decoded_summary_json раньше писал полную build_detail() (геометрия, URL, электрика по каждой ступени) на каждый из миллионов прогонов — это и душило CPU при GPU-эволюции (пул процессов нужен был только чтобы успевать за GPU), и разогнало базу до 70ГБ на 12.9М строк, пока диск не забился под завязку. genome_json уже достаточен для полного воспроизведения — теперь пишем дешёвую decoded_summary(), а run_detail() на веб-морде пересчитывает полную физику по клику (как уже делает render.py для графиков/анимации). --- src/gausse/gpu/batch_sweep.py | 31 ++++++++-------- src/gausse/optim/objective.py | 18 ++++++---- src/gausse/storage/schema.py | 5 +++ src/gausse/web/stats.py | 66 +++++++++++++++++++++++++---------- 4 files changed, 78 insertions(+), 42 deletions(-) diff --git a/src/gausse/gpu/batch_sweep.py b/src/gausse/gpu/batch_sweep.py index 0c78c82..ad2d334 100644 --- a/src/gausse/gpu/batch_sweep.py +++ b/src/gausse/gpu/batch_sweep.py @@ -314,29 +314,31 @@ def evaluate_genomes_gpu( def gpu_record_worker(payload) -> RunRecord: - """Собирает RunRecord (decode + build_detail + стоимость + JSON) в процессе - пула — параллельно с GPU-оценкой следующих батчей. Тяжёлая часть записи - (детализация + сериализация) не должна душить GPU одним потоком Python. + """Собирает RunRecord (decode + стоимость + JSON) в процессе пула — + параллельно с GPU-оценкой следующих батчей. + + detail НЕ строится (build_detail дорогая — полная физика по каждой + ступени — и не нужна для хранения): genome_json уже достаточен, чтобы + воспроизвести всё через decode()+build_detail() на лету при просмотре + конкретного прогона (см. web/stats.run_detail). Пишем дешёвую + decoded_summary() — иначе на миллионах прогонов это дорогая работа, + съедающая и CPU (душит GPU), и диск (десятки ГБ на пустом месте). Работает в воркере ProcessPoolExecutor с worker_context.init_worker (база компонентов уже загружена в процессе). """ from gausse.optim import worker_context - from gausse.optim.objective import MODEL_VERSION, compute_cost_rub + from gausse.optim.objective import MODEL_VERSION, compute_cost_rub, decoded_summary genome, outcome, search_mode = payload db, bounds = worker_context.db, worker_context.bounds - config, ix, iv = decode(genome, db, bounds) - detail = build_detail( - config, _FEASIBLE if outcome["feasible"] else None, db, ix, iv, - genome.tube_inner_d_m, genome.tube_wall_m, - ) + config, _ix, _iv = decode(genome, db, bounds) return RunRecord( run_id=str(uuid.uuid4()), timestamp=datetime.now(timezone.utc).isoformat(), search_mode=search_mode, genome_json=json.dumps(genome_to_dict(genome)), - decoded_summary_json=json.dumps(detail, ensure_ascii=False), + decoded_summary_json=json.dumps(decoded_summary(genome, db), ensure_ascii=False), feasible=outcome["feasible"], model_version=MODEL_VERSION, infeasible_reason=outcome["reason"], @@ -396,18 +398,15 @@ def run_gpu_sweep( def _record(st: _State, db, bounds: SearchBounds, backend, efficiency, exit_v): + from gausse.optim.objective import decoded_summary + feasible = efficiency is not None - cfg, _, _ = decode(st.genome, db, bounds) - detail = build_detail( - cfg, _FEASIBLE if feasible else None, db, st.pre.initial_x_m, st.pre.initial_v_mps, - st.genome.tube_inner_d_m, st.genome.tube_wall_m, - ) return RunRecord( run_id=str(uuid.uuid4()), timestamp=datetime.now(timezone.utc).isoformat(), search_mode=f"gpu-sweep-{backend}", genome_json=json.dumps(genome_to_dict(st.genome)), - decoded_summary_json=json.dumps(detail, ensure_ascii=False), + decoded_summary_json=json.dumps(decoded_summary(st.genome, db), ensure_ascii=False), feasible=feasible, model_version=MODEL_VERSION, infeasible_reason=st.reason, diff --git a/src/gausse/optim/objective.py b/src/gausse/optim/objective.py index d11e28b..9279028 100644 --- a/src/gausse/optim/objective.py +++ b/src/gausse/optim/objective.py @@ -61,10 +61,11 @@ def _wire_resistance_ohm(resistivity_ohm_m: float, gauge_mm: float, wire_length_ def decoded_summary(genome: Genome, db: ComponentDatabase) -> dict: - """Краткая сводка только по геному (без результатов симуляции). - - Оставлена для обратной совместимости; подробную запись со всей физикой - и результатами по каждой ступени строит `build_detail` (её и пишем в БД). + """Краткая сводка только по геному (без результатов симуляции) — это и + пишется в decoded_summary_json для каждого прогона: genome_json уже + достаточен для полного воспроизведения через build_detail() на лету, + так что хранить дорогую подробную запись для каждого из миллионов + прогонов незачем (см. web/stats.run_detail для просмотра одного прогона). """ material = db.projectile_materials[genome.projectile.material_idx % len(db.projectile_materials)] stages_summary = [] @@ -259,10 +260,15 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> EvaluationResult: + """detail НЕ строится здесь: build_detail — дорогая (полная физика по + каждой ступени) и не нужна ни фитнесу, ни хранению — geneome_json уже + достаточен, чтобы воспроизвести всё через decode()+build_detail() на + лету (см. web/stats.run_detail). build_run_record() кладёт в БД дешёвую + decoded_summary() вместо этого. + """ config, initial_x_m, initial_v_mps = decode(genome, db, bounds) cost_rub = compute_cost_rub(config, db) result = run_coilgun(config) - detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m) if not result.feasible: n_stages = len(config.stages) @@ -275,7 +281,6 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva reason=result.reason, failed_stage_index=result.failed_stage_index, energy_breakdown={}, - detail=detail, ) energy_breakdown = { @@ -291,7 +296,6 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva efficiency=result.efficiency, exit_velocity_mps=result.exit_v_mps, energy_breakdown=energy_breakdown, - detail=detail, ) diff --git a/src/gausse/storage/schema.py b/src/gausse/storage/schema.py index 817cc84..4997136 100644 --- a/src/gausse/storage/schema.py +++ b/src/gausse/storage/schema.py @@ -14,6 +14,11 @@ CREATE TABLE IF NOT EXISTS runs ( timestamp TEXT NOT NULL, search_mode TEXT NOT NULL, genome_json TEXT NOT NULL, + -- ДЕШЁВАЯ сводка (objective.decoded_summary) — НЕ полная физика по + -- ступеням. genome_json сам по себе достаточен, чтобы воспроизвести + -- всё через decode()+build_detail() на лету (см. web/stats.run_detail); + -- хранить дорогую детализацию для каждого из миллионов прогонов + -- бессмысленно — так база росла на пустом месте (70ГБ на 12.9М строк). decoded_summary_json TEXT NOT NULL, feasible INTEGER NOT NULL, infeasible_reason TEXT, diff --git a/src/gausse/web/stats.py b/src/gausse/web/stats.py index 5a015bd..7ef2ebd 100644 --- a/src/gausse/web/stats.py +++ b/src/gausse/web/stats.py @@ -72,11 +72,11 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di def _summarize(rows): out = [] for r in rows: - detail = 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 {} out.append({ "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, - "n_stages": detail.get("n_stages"), "timestamp": r.timestamp, + "n_stages": len(summary.get("stages", [])), "timestamp": r.timestamp, }) return out @@ -115,25 +115,53 @@ def _last_cycle_best(log_lines: list[str]) -> float | None: def run_detail(db_path: Path, run_id: str) -> dict | None: + """Подробности одного прогона — НЕ читаются из БД (там только дешёвая + decoded_summary), а пересчитываются на лету из genome_json тем же + decode()+build_detail(), что и остальной рендер веб-морды (см. + web/render.py). Это единичный запрос по клику, не миллионы прогонов — + пересимулировать дешевле, чем хранить полную физику для каждого.""" + from gausse.components.database import ComponentDatabase + from gausse.optim.objective import build_detail + from gausse.optim.search_space import SearchBounds, decode, genome_from_dict + from gausse.sim.coilgun import run_coilgun + conn = open_connection(db_path) try: row = fetch_run_by_id(conn, run_id) - if row is None: - return None - return { - "run_id": row.run_id, - "search_mode": row.search_mode, - # версия физики обязана быть видна: прогоны разных моделей - # несравнимы, и «лучший» старой версии — не лучший вообще - "model_version": row.model_version, - "feasible": row.feasible, - "infeasible_reason": row.infeasible_reason, - "efficiency": row.efficiency, - "exit_velocity_mps": row.exit_velocity_mps, - "cost_rub": row.cost_rub, - "timestamp": row.timestamp, - "detail": json.loads(row.decoded_summary_json) if row.decoded_summary_json else {}, - "energy_breakdown": json.loads(row.energy_breakdown_json) if row.energy_breakdown_json else {}, - } finally: conn.close() + if row is None: + return None + + genome = genome_from_dict(json.loads(row.genome_json)) + db = ComponentDatabase.load() + bounds = SearchBounds() + config, initial_x_m, initial_v_mps = decode(genome, db, bounds) + result = run_coilgun(config) + detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m) + energy_breakdown = ( + { + "total_energy_in_j": result.total_energy_in_j, + "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 { + "run_id": row.run_id, + "search_mode": row.search_mode, + # версия физики обязана быть видна: прогоны разных моделей + # несравнимы, и «лучший» старой версии — не лучший вообще + "model_version": row.model_version, + "feasible": row.feasible, + "infeasible_reason": row.infeasible_reason, + "efficiency": row.efficiency, + "exit_velocity_mps": row.exit_velocity_mps, + "cost_rub": row.cost_rub, + "timestamp": row.timestamp, + "detail": detail, + "energy_breakdown": energy_breakdown, + }