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, + }