Морда O(1) + GPU без удушения одним потоком Python
Дашборд: /api/overview занимал 5.2с на 630к строк (сканы всей таблицы на каждый рефреш при автообновлении 3с — морда хронически отставала, и с ростом базы становилось бы хуже). Теперь: - счётчики/гистограмма/причины/режимы — инкрементальные таблицы stats_*, обновляются В ТРАНЗАКЦИИ вставки (insert_runs), чтение O(1); ensure_stats() мигрирует старую базу одним пересчётом - топы — частичные индексы (efficiency/exit_velocity DESC WHERE feasible=1) - хвост лога — чтение последних 64КБ с конца, а не всего файла GPU: сборка записей (decode+build_detail+JSON+стоимость) вынесена в пул процессов (gpu_record_worker), GPU больше не ждёт один поток Python между батчами. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -156,10 +156,16 @@ def _simulate_states(xp, states: list) -> None:
|
|||||||
st.cur_x = p["coil_center"] + float(exit_x[k])
|
st.cur_x = p["coil_center"] + float(exit_x[k])
|
||||||
|
|
||||||
|
|
||||||
def evaluate_genomes_gpu(xp, genomes: list, db: ComponentDatabase, bounds: SearchBounds) -> list:
|
def evaluate_genomes_gpu(
|
||||||
|
xp, genomes: list, db: ComponentDatabase, bounds: SearchBounds, build_details: bool = True
|
||||||
|
) -> list:
|
||||||
"""Оценка списка геномов ОДНИМ батчем (для эволюции): та же физика и та же
|
"""Оценка списка геномов ОДНИМ батчем (для эволюции): та же физика и та же
|
||||||
формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных
|
формула фитнеса, что в objective.evaluate (КПД либо -1+доля пройденных
|
||||||
ступеней), но разряды всех геномов интегрируются вместе на GPU/numpy.
|
ступеней), но разряды всех геномов интегрируются вместе на GPU/numpy.
|
||||||
|
|
||||||
|
build_details=False — не строить detail/стоимость (фитнесу они не нужны):
|
||||||
|
их параллельно собирает пул процессов (`gpu_record_worker`), иначе один
|
||||||
|
поток Python душит GPU (тот простаивал на ~2/3 времени поколения).
|
||||||
"""
|
"""
|
||||||
from gausse.optim.objective import EvaluationResult, compute_cost_rub
|
from gausse.optim.objective import EvaluationResult, compute_cost_rub
|
||||||
|
|
||||||
@@ -175,12 +181,12 @@ def evaluate_genomes_gpu(xp, genomes: list, db: ComponentDatabase, bounds: Searc
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for st in states:
|
for st in states:
|
||||||
cost = compute_cost_rub(st.config, db)
|
cost = compute_cost_rub(st.config, db) if build_details else None
|
||||||
ok = st.alive and st.energy_in_j > 0
|
ok = st.alive and st.energy_in_j > 0
|
||||||
detail = build_detail(
|
detail = build_detail(
|
||||||
st.config, _FEASIBLE if ok else None, db, st.initial_x_m, st.initial_v_mps,
|
st.config, _FEASIBLE if ok else None, db, st.initial_x_m, st.initial_v_mps,
|
||||||
st.genome.tube_inner_d_m, st.genome.tube_wall_m,
|
st.genome.tube_inner_d_m, st.genome.tube_wall_m,
|
||||||
)
|
) if build_details else 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(
|
||||||
@@ -204,6 +210,41 @@ def evaluate_genomes_gpu(xp, genomes: list, db: ComponentDatabase, bounds: Searc
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_record_worker(payload) -> RunRecord:
|
||||||
|
"""Собирает RunRecord (decode + build_detail + стоимость + JSON) в процессе
|
||||||
|
пула — параллельно с GPU-оценкой следующих батчей. Тяжёлая часть записи
|
||||||
|
(детализация + сериализация) не должна душить GPU одним потоком Python.
|
||||||
|
|
||||||
|
Работает в воркере ProcessPoolExecutor с worker_context.init_worker
|
||||||
|
(база компонентов уже загружена в процессе).
|
||||||
|
"""
|
||||||
|
from gausse.optim import worker_context
|
||||||
|
from gausse.optim.objective import MODEL_VERSION, compute_cost_rub
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
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),
|
||||||
|
feasible=outcome["feasible"],
|
||||||
|
model_version=MODEL_VERSION,
|
||||||
|
infeasible_reason=outcome["reason"],
|
||||||
|
failed_stage_index=outcome["failed_stage_index"],
|
||||||
|
efficiency=outcome["efficiency"],
|
||||||
|
exit_velocity_mps=outcome["exit_velocity_mps"],
|
||||||
|
cost_rub=compute_cost_rub(config, db),
|
||||||
|
energy_breakdown_json=json.dumps(outcome["energy_breakdown"]) if outcome["energy_breakdown"] else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_gpu_sweep(
|
def run_gpu_sweep(
|
||||||
db_path: Path,
|
db_path: Path,
|
||||||
n_runs: int,
|
n_runs: int,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
компонентов.
|
компонентов.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import copy
|
import copy
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import random
|
import random
|
||||||
@@ -141,28 +140,39 @@ def run_evolution(
|
|||||||
n_evaluated = 0
|
n_evaluated = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
executor_cm = (
|
# пул нужен в ОБОИХ режимах: на CPU он оценивает геномы, в GPU-режиме
|
||||||
ProcessPoolExecutor(
|
# параллельно собирает записи (detail+JSON) — иначе один поток Python
|
||||||
max_workers=n_workers,
|
# душит GPU (карта простаивала ~2/3 времени поколения)
|
||||||
mp_context=ctx,
|
with ProcessPoolExecutor(
|
||||||
initializer=worker_context.init_worker,
|
max_workers=n_workers,
|
||||||
initargs=(data_dir, bounds),
|
mp_context=ctx,
|
||||||
)
|
initializer=worker_context.init_worker,
|
||||||
if gpu_xp is None
|
initargs=(data_dir, bounds),
|
||||||
else contextlib.nullcontext()
|
) as executor:
|
||||||
)
|
|
||||||
with executor_cm as executor:
|
|
||||||
for generation in range(n_generations):
|
for generation in range(n_generations):
|
||||||
if gpu_xp is not None:
|
if gpu_xp is not None:
|
||||||
from gausse.gpu.batch_sweep import evaluate_genomes_gpu
|
from gausse.gpu.batch_sweep import evaluate_genomes_gpu, gpu_record_worker
|
||||||
|
|
||||||
pairs = list(zip(population, evaluate_genomes_gpu(gpu_xp, population, db, bounds)))
|
results = evaluate_genomes_gpu(gpu_xp, population, db, bounds, build_details=False)
|
||||||
|
pairs = list(zip(population, results))
|
||||||
|
payload = [
|
||||||
|
(g, {
|
||||||
|
"feasible": r.feasible,
|
||||||
|
"efficiency": r.efficiency,
|
||||||
|
"exit_velocity_mps": r.exit_velocity_mps,
|
||||||
|
"reason": r.reason,
|
||||||
|
"failed_stage_index": r.failed_stage_index,
|
||||||
|
"energy_breakdown": r.energy_breakdown,
|
||||||
|
}, mode)
|
||||||
|
for g, r in pairs
|
||||||
|
]
|
||||||
|
records = executor.map(gpu_record_worker, payload, chunksize=64)
|
||||||
else:
|
else:
|
||||||
pairs = list(executor.map(_evaluate_genome, population))
|
pairs = list(executor.map(_evaluate_genome, population))
|
||||||
|
records = (build_run_record(g, r, db, search_mode=mode) for g, r in pairs)
|
||||||
n_evaluated += len(pairs)
|
n_evaluated += len(pairs)
|
||||||
|
|
||||||
for genome, result in pairs:
|
for record, (genome, result) in zip(records, pairs):
|
||||||
record: RunRecord = build_run_record(genome, result, db, search_mode=mode)
|
|
||||||
queue.put(record)
|
queue.put(record)
|
||||||
logger.update(result.feasible, result.efficiency)
|
logger.update(result.feasible, result.efficiency)
|
||||||
if best_pair is None or result.fitness > best_pair[1].fitness:
|
if best_pair is None or result.fitness > best_pair[1].fitness:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import sys
|
|||||||
from dataclasses import asdict, fields
|
from dataclasses import asdict, fields
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from gausse.storage.schema import CREATE_INDEXES_SQL, CREATE_RUNS_TABLE_SQL, RunRecord
|
from gausse.storage.schema import CREATE_INDEXES_SQL, CREATE_RUNS_TABLE_SQL, CREATE_STATS_SQL, RunRecord
|
||||||
|
|
||||||
_COLUMNS = [f.name for f in fields(RunRecord)]
|
_COLUMNS = [f.name for f in fields(RunRecord)]
|
||||||
|
|
||||||
@@ -25,10 +25,101 @@ def open_connection(db_path: Path) -> sqlite3.Connection:
|
|||||||
conn.execute(CREATE_RUNS_TABLE_SQL)
|
conn.execute(CREATE_RUNS_TABLE_SQL)
|
||||||
for statement in CREATE_INDEXES_SQL:
|
for statement in CREATE_INDEXES_SQL:
|
||||||
conn.execute(statement)
|
conn.execute(statement)
|
||||||
|
for statement in CREATE_STATS_SQL:
|
||||||
|
conn.execute(statement)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def short_reason(reason: str | None) -> str | None:
|
||||||
|
"""Причина отказа без деталей конкретного прогона (для группировки)."""
|
||||||
|
return reason.split(":", 1)[0] if reason else reason
|
||||||
|
|
||||||
|
|
||||||
|
def _efficiency_bucket(efficiency: float | None) -> int | None:
|
||||||
|
"""Корзина гистограммы КПД 0..9 (та же логика, что раньше в SQL-агрегате)."""
|
||||||
|
if efficiency is None:
|
||||||
|
return None
|
||||||
|
b = min(int(efficiency * 10), 9)
|
||||||
|
return b if 0 <= b <= 9 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_stats(conn: sqlite3.Connection, records: list[RunRecord]) -> None:
|
||||||
|
"""Инкремент агрегатов дашборда — вызывается внутри транзакции вставки."""
|
||||||
|
n_feasible = sum(1 for r in records if r.feasible)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO stats_counters(key, value) VALUES('total', ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value = value + excluded.value",
|
||||||
|
(len(records),),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO stats_counters(key, value) VALUES('feasible', ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value = value + excluded.value",
|
||||||
|
(n_feasible,),
|
||||||
|
)
|
||||||
|
hist: dict[int, int] = {}
|
||||||
|
reasons: dict[str, int] = {}
|
||||||
|
modes: dict[str, int] = {}
|
||||||
|
for r in records:
|
||||||
|
modes[r.search_mode] = modes.get(r.search_mode, 0) + 1
|
||||||
|
if r.feasible:
|
||||||
|
b = _efficiency_bucket(r.efficiency)
|
||||||
|
if b is not None:
|
||||||
|
hist[b] = hist.get(b, 0) + 1
|
||||||
|
elif r.infeasible_reason:
|
||||||
|
key = short_reason(r.infeasible_reason)
|
||||||
|
reasons[key] = reasons.get(key, 0) + 1
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO stats_hist(bucket, count) VALUES(?, ?) "
|
||||||
|
"ON CONFLICT(bucket) DO UPDATE SET count = count + excluded.count",
|
||||||
|
list(hist.items()),
|
||||||
|
)
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO stats_reasons(reason, count) VALUES(?, ?) "
|
||||||
|
"ON CONFLICT(reason) DO UPDATE SET count = count + excluded.count",
|
||||||
|
list(reasons.items()),
|
||||||
|
)
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO stats_modes(mode, count) VALUES(?, ?) "
|
||||||
|
"ON CONFLICT(mode) DO UPDATE SET count = count + excluded.count",
|
||||||
|
list(modes.items()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_stats(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Пересобирает агрегаты из таблицы runs (миграция старой базы; один раз)."""
|
||||||
|
conn.execute("DELETE FROM stats_counters")
|
||||||
|
conn.execute("DELETE FROM stats_hist")
|
||||||
|
conn.execute("DELETE FROM stats_reasons")
|
||||||
|
conn.execute("DELETE FROM stats_modes")
|
||||||
|
total = conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0]
|
||||||
|
feasible = conn.execute("SELECT COUNT(*) FROM runs WHERE feasible=1").fetchone()[0]
|
||||||
|
conn.execute("INSERT INTO stats_counters VALUES('total', ?)", (total,))
|
||||||
|
conn.execute("INSERT INTO stats_counters VALUES('feasible', ?)", (feasible,))
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO stats_hist SELECT b, COUNT(*) FROM "
|
||||||
|
"(SELECT MIN(CAST(efficiency*10 AS INT), 9) AS b FROM runs WHERE feasible=1 AND efficiency IS NOT NULL) "
|
||||||
|
"WHERE b BETWEEN 0 AND 9 GROUP BY b"
|
||||||
|
)
|
||||||
|
for reason, cnt in conn.execute(
|
||||||
|
"SELECT infeasible_reason, COUNT(*) FROM runs WHERE feasible=0 AND infeasible_reason IS NOT NULL "
|
||||||
|
"GROUP BY infeasible_reason"
|
||||||
|
).fetchall():
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO stats_reasons VALUES(?, ?) ON CONFLICT(reason) DO UPDATE SET count = count + excluded.count",
|
||||||
|
(short_reason(reason), cnt),
|
||||||
|
)
|
||||||
|
conn.execute("INSERT INTO stats_modes SELECT search_mode, COUNT(*) FROM runs GROUP BY search_mode")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_stats(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Если агрегаты пусты, а прогоны есть — база со старой схемой: пересобрать."""
|
||||||
|
has_stats = conn.execute("SELECT COUNT(*) FROM stats_counters").fetchone()[0] > 0
|
||||||
|
if not has_stats and conn.execute("SELECT EXISTS(SELECT 1 FROM runs)").fetchone()[0]:
|
||||||
|
rebuild_stats(conn)
|
||||||
|
|
||||||
|
|
||||||
def _record_to_params(record: RunRecord) -> dict:
|
def _record_to_params(record: RunRecord) -> dict:
|
||||||
row = asdict(record)
|
row = asdict(record)
|
||||||
row["feasible"] = int(row["feasible"])
|
row["feasible"] = int(row["feasible"])
|
||||||
@@ -54,6 +145,9 @@ def insert_runs(conn: sqlite3.Connection, records: list[RunRecord]) -> None:
|
|||||||
f"INSERT INTO runs ({columns}) VALUES ({placeholders})",
|
f"INSERT INTO runs ({columns}) VALUES ({placeholders})",
|
||||||
[_record_to_params(r) for r in records],
|
[_record_to_params(r) for r in records],
|
||||||
)
|
)
|
||||||
|
# агрегаты дашборда — в той же транзакции: либо и строки, и счётчики,
|
||||||
|
# либо ничего (иначе цифры на морде разъедутся с таблицей)
|
||||||
|
_apply_stats(conn, records)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,21 @@ CREATE_INDEXES_SQL = [
|
|||||||
"CREATE INDEX IF NOT EXISTS idx_runs_efficiency ON runs(efficiency)",
|
"CREATE INDEX IF NOT EXISTS idx_runs_efficiency ON runs(efficiency)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_runs_velocity ON runs(exit_velocity_mps)",
|
"CREATE INDEX IF NOT EXISTS idx_runs_velocity ON runs(exit_velocity_mps)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_runs_search_mode ON runs(search_mode)",
|
"CREATE INDEX IF NOT EXISTS idx_runs_search_mode ON runs(search_mode)",
|
||||||
|
# частичные индексы под топы дашборда: ORDER BY ... DESC при feasible=1
|
||||||
|
# без них top-15 на сотнях тысяч строк занимал ~1.2с на каждый рефреш
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_runs_feas_eff ON runs(efficiency DESC) WHERE feasible=1",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_runs_feas_vel ON runs(exit_velocity_mps DESC) WHERE feasible=1",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Инкрементальные агрегаты для дашборда: счётчики/гистограмма/причины/режимы
|
||||||
|
# обновляются В ТОЙ ЖЕ транзакции, что и вставка прогонов (insert_runs), и
|
||||||
|
# читаются за O(1) — иначе каждый рефреш морды сканировал ВСЮ таблицу runs
|
||||||
|
# (5.2с на 630к строк, а база растёт годами).
|
||||||
|
CREATE_STATS_SQL = [
|
||||||
|
"CREATE TABLE IF NOT EXISTS stats_counters (key TEXT PRIMARY KEY, value INTEGER NOT NULL)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS stats_hist (bucket INTEGER PRIMARY KEY, count INTEGER NOT NULL)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS stats_reasons (reason TEXT PRIMARY KEY, count INTEGER NOT NULL)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS stats_modes (mode TEXT PRIMARY KEY, count INTEGER NOT NULL)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,22 @@ import json
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from gausse.storage.database import fetch_run_by_id, fetch_runs, open_connection
|
from gausse.storage.database import ensure_stats, fetch_run_by_id, fetch_runs, open_connection
|
||||||
|
|
||||||
|
|
||||||
def _tail_log(log_path: Path, n_lines: int = 40) -> list[str]:
|
def _tail_log(log_path: Path, n_lines: int = 40) -> list[str]:
|
||||||
|
"""Последние строки лога чтением С КОНЦА файла (лог растёт годами —
|
||||||
|
перечитывать его целиком на каждый рефреш нельзя)."""
|
||||||
if not log_path.exists():
|
if not log_path.exists():
|
||||||
return []
|
return []
|
||||||
lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
with open(log_path, "rb") as f:
|
||||||
|
f.seek(0, 2)
|
||||||
|
size = f.tell()
|
||||||
|
f.seek(max(0, size - 64 * 1024))
|
||||||
|
chunk = f.read().decode("utf-8", errors="replace")
|
||||||
|
lines = chunk.splitlines()
|
||||||
|
if size > 64 * 1024 and lines:
|
||||||
|
lines = lines[1:] # первая строка чанка может быть обрезана
|
||||||
return lines[-n_lines:]
|
return lines[-n_lines:]
|
||||||
|
|
||||||
|
|
||||||
@@ -32,34 +41,32 @@ def _gpu_stat(db_path: Path) -> dict | None:
|
|||||||
|
|
||||||
|
|
||||||
def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> dict:
|
def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> dict:
|
||||||
"""Сводка для дашборда: счётчики, гистограмма КПД, топ конфигураций, хвост лога."""
|
"""Сводка для дашборда: счётчики, гистограмма КПД, топ конфигураций, хвост лога.
|
||||||
|
|
||||||
|
Счётчики/гистограмма/причины/режимы читаются из инкрементальных
|
||||||
|
агрегатов (stats_*, ведутся в транзакции вставки) за O(1) — раньше
|
||||||
|
каждый рефреш сканировал всю таблицу runs (5.2с на 630к строк).
|
||||||
|
"""
|
||||||
conn = open_connection(db_path)
|
conn = open_connection(db_path)
|
||||||
try:
|
try:
|
||||||
total = conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0]
|
ensure_stats(conn) # миграция базы со старой схемой (один раз)
|
||||||
feasible = conn.execute("SELECT COUNT(*) FROM runs WHERE feasible=1").fetchone()[0]
|
counters = dict(conn.execute("SELECT key, value FROM stats_counters"))
|
||||||
|
total = counters.get("total", 0)
|
||||||
|
feasible = counters.get("feasible", 0)
|
||||||
infeasible = total - feasible
|
infeasible = total - feasible
|
||||||
|
|
||||||
# распределение по типу датчика ступени 0 (реализуемо / всего)
|
by_mode = dict(conn.execute("SELECT mode, count FROM stats_modes"))
|
||||||
by_mode = {}
|
|
||||||
for mode, cnt in conn.execute("SELECT search_mode, COUNT(*) FROM runs GROUP BY search_mode"):
|
|
||||||
by_mode[mode] = cnt
|
|
||||||
|
|
||||||
# причины отказа (топ)
|
reasons = [
|
||||||
reasons = []
|
{"reason": reason, "count": cnt}
|
||||||
for reason, cnt in conn.execute(
|
for reason, cnt in conn.execute(
|
||||||
"SELECT infeasible_reason, COUNT(*) c FROM runs WHERE feasible=0 AND infeasible_reason IS NOT NULL "
|
"SELECT reason, count FROM stats_reasons ORDER BY count DESC LIMIT 10"
|
||||||
"GROUP BY infeasible_reason ORDER BY c DESC LIMIT 10"
|
)
|
||||||
):
|
]
|
||||||
short = reason.split(":", 1)[0] if reason else reason
|
|
||||||
reasons.append({"reason": short, "count": cnt})
|
|
||||||
|
|
||||||
# гистограмма КПД — агрегируем в SQL (не тянем все строки в питон)
|
|
||||||
buckets = [0] * 10 # 0-10%,...,90-100%
|
buckets = [0] * 10 # 0-10%,...,90-100%
|
||||||
for bucket, cnt in conn.execute(
|
for bucket, cnt in conn.execute("SELECT bucket, count FROM stats_hist"):
|
||||||
"SELECT MIN(CAST(efficiency*10 AS INT), 9) AS b, COUNT(*) FROM runs "
|
if 0 <= bucket <= 9:
|
||||||
"WHERE feasible=1 AND efficiency IS NOT NULL GROUP BY b"
|
|
||||||
):
|
|
||||||
if bucket is not None and 0 <= bucket <= 9:
|
|
||||||
buckets[int(bucket)] = cnt
|
buckets[int(bucket)] = cnt
|
||||||
|
|
||||||
def _summarize(rows):
|
def _summarize(rows):
|
||||||
|
|||||||
Reference in New Issue
Block a user