Add persistent trial history DB (append-only, survives --clean)

- history.db: SQLite append-only, never deleted by --clean
- history.csv: Auto-exported after each trial (live updates)
- Logs: DVs, results, feasibility, status, solve time, iter path
- Cross-study queries: full lineage across all runs/phases
- --clean only resets Optuna DB, history preserved
This commit is contained in:
2026-02-11 14:59:52 +00:00
parent 04fdae26ab
commit 815db0fb8d
2 changed files with 288 additions and 3 deletions

View File

@@ -41,6 +41,7 @@ from geometric_checks import (
FeasibilityResult,
check_feasibility,
)
from history import TrialHistory
from nx_interface import TrialInput, TrialResult, create_solver
from sampling import DV_DEFINITIONS, generate_lhs_samples, points_to_dicts
@@ -103,12 +104,16 @@ def constraints_func(trial: optuna.trial.FrozenTrial) -> list[float]:
def evaluate_trial(
trial: optuna.Trial,
solver: Any,
history: TrialHistory | None = None,
study_name: str = "",
) -> float:
"""Evaluate a single trial: geometric check → NX solve → extract.
Args:
trial: Optuna trial (with parameters already suggested/enqueued).
solver: NX solver instance (stub or real).
history: Persistent trial history logger (append-only).
study_name: Study name for history logging.
Returns:
Objective value (mass in kg). Returns INFEASIBLE_MASS for
@@ -152,6 +157,13 @@ def evaluate_trial(
trial.set_user_attr("ligament", geo_result.ligament)
trial.set_user_attr("web_clearance", geo_result.web_clearance)
params = {
"beam_half_core_thickness": dv1,
"beam_face_thickness": dv2,
"holes_diameter": dv3,
"hole_count": dv4,
}
if not geo_result.feasible:
logger.warning(
"Trial %d: GEOMETRICALLY INFEASIBLE — %s",
@@ -162,6 +174,13 @@ def evaluate_trial(
trial.set_user_attr("tip_displacement", INFEASIBLE_DISPLACEMENT)
trial.set_user_attr("max_von_mises", INFEASIBLE_STRESS)
trial.set_user_attr("mass", INFEASIBLE_MASS)
if history:
history.log_trial(
study_name=study_name, trial_id=trial_num, params=params,
geo_feasible=False, status="GEO_INFEASIBLE",
error_message=geo_result.reason,
iteration_number=trial_num + 1,
)
return INFEASIBLE_MASS
# NX evaluation
@@ -188,6 +207,14 @@ def evaluate_trial(
trial.set_user_attr("tip_displacement", INFEASIBLE_DISPLACEMENT)
trial.set_user_attr("max_von_mises", INFEASIBLE_STRESS)
trial.set_user_attr("mass", INFEASIBLE_MASS)
if history:
history.log_trial(
study_name=study_name, trial_id=trial_num, params=params,
status="FAILED", error_message=nx_result.error_message,
solve_time_s=round(t_elapsed, 2),
iter_path=nx_result.iteration_dir,
iteration_number=trial_num + 1,
)
return INFEASIBLE_MASS
# Record successful results
@@ -214,6 +241,20 @@ def evaluate_trial(
t_elapsed,
)
if history:
history.log_trial(
study_name=study_name, trial_id=trial_num, params=params,
mass_kg=nx_result.mass,
tip_displacement_mm=nx_result.tip_displacement,
max_von_mises_mpa=nx_result.max_von_mises,
geo_feasible=True,
status="COMPLETE",
solve_time_s=round(t_elapsed, 2),
iter_path=nx_result.iteration_dir,
iteration_number=trial_num + 1,
)
history.export_csv() # Live update CSV after each trial
return nx_result.mass
@@ -419,7 +460,7 @@ def run_study(args: argparse.Namespace) -> None:
storage = f"sqlite:///{db_path}"
if args.clean and db_path.exists():
logger.info("--clean flag: deleting existing DB at %s", db_path)
logger.info("--clean flag: deleting Optuna DB at %s (history.db preserved)", db_path)
db_path.unlink()
if args.resume:
@@ -449,13 +490,17 @@ def run_study(args: argparse.Namespace) -> None:
logger.info("Enqueued %d trials (1 baseline + %d LHS)", n_trials, n_trials - 1)
# -----------------------------------------------------------------------
# 3. Create solver
# 3. Create solver + history
# -----------------------------------------------------------------------
solver = create_solver(
backend=args.backend,
model_dir=args.model_dir,
)
# Persistent history — NEVER deleted by --clean
history = TrialHistory(results_dir)
study_name = args.study_name
# -----------------------------------------------------------------------
# 4. Run all trials
# -----------------------------------------------------------------------
@@ -469,7 +514,7 @@ def run_study(args: argparse.Namespace) -> None:
optuna.logging.set_verbosity(optuna.logging.WARNING)
study.optimize(
lambda trial: evaluate_trial(trial, solver),
lambda trial: evaluate_trial(trial, solver, history, study_name),
n_trials=n_trials,
callbacks=[_progress_callback],
)
@@ -497,6 +542,11 @@ def run_study(args: argparse.Namespace) -> None:
# Cleanup
solver.close()
# Final history export + summary
history.close()
hist_summary = history.get_study_summary(study_name)
logger.info("History DB: %d total records across all studies", hist_summary["total"])
def _progress_callback(study: optuna.Study, trial: optuna.trial.FrozenTrial) -> None:
"""Log progress after each trial."""