From 0733a8d18a50b0be9d0586700528288b981d4916 Mon Sep 17 00:00:00 2001 From: popmonkey Date: Thu, 27 Nov 2025 11:49:14 -0800 Subject: [PATCH] Enhance output to always display complexity and timing information in the CLI, regardless of solver success or failure. Fixes #19 --- cmd/solver/cli.cpp | 66 +++++++++++++++++++----------------- src/jres_solver.cpp | 8 +++++ src/jres_solver_types.hpp | 11 ++++++ src/jres_standard_solver.cpp | 46 ++++++++++++++----------- 4 files changed, 81 insertions(+), 50 deletions(-) diff --git a/cmd/solver/cli.cpp b/cmd/solver/cli.cpp index 36c3842..052aea6 100644 --- a/cmd/solver/cli.cpp +++ b/cmd/solver/cli.cpp @@ -166,43 +166,47 @@ int main(int argc, char **argv) } else { // --- Standard Schedule Output Handling --- - if (resultCode == 0) { - // Success - if (!quiet) { - - // Print Metadata / Settings - if (resultJson.contains("metadata")) { - std::cout << "\n--- 🔧 Solver Settings ---" << std::endl; - json meta = resultJson["metadata"]; - std::cout << "Time Limit: " << meta.value("timeLimit", 0) << "s | " - << "Gap: " << meta.value("optimalityGap", 0.0) << " | " - << "Spotter: " << meta.value("spotterMode", "unknown") << std::endl; - } + if (!quiet) { + // Print Metadata / Settings + if (resultJson.contains("metadata")) { + std::cout << "\n--- 🔧 Solver Settings ---" << std::endl; + json meta = resultJson["metadata"]; + std::cout << "Time Limit: " << meta.value("timeLimit", 0) << "s | " + << "Gap: " << meta.value("optimalityGap", 0.0) << " | " + << "Spotter: " << meta.value("spotterMode", "unknown") << std::endl; + } - // Print Complexity - if (resultJson.contains("complexity")) { - std::cout << "\n--- 🧠 Complexity ---" << std::endl; - json c = resultJson["complexity"]; - std::cout << "Rows: " << c.value("modelRows", 0) << " | " - << "Cols: " << c.value("modelColumns", 0) << " | " - << "Nodes: " << c.value("searchNodes", 0) << std::endl; - std::cout << "Big-M Rest Constraints: " << c.value("numRestConstraints", 0) << std::endl; + // Print Complexity (regardless of success/failure) + if (resultJson.contains("complexity")) { + std::cout << "\n--- 🧠 Complexity ---" << std::endl; + json c = resultJson["complexity"]; + std::cout << "Rows: " << c.value("modelRows", 0) << " | " + << "Cols: " << c.value("modelColumns", 0) << " | " + << "Nodes: " << c.value("searchNodes", 0) << std::endl; + std::cout << "Big-M Rest Constraints: " << c.value("numRestConstraints", 0) << std::endl; + if (!c["finalGap"].is_null()) { std::cout << "Final Gap: " << c.value("finalGap", 0.0) << std::endl; } + } - // Print Timing - if (resultJson.contains("timing")) { - std::cout << "\n--- ⏱️ Timing Performance ---" << std::endl; - json t = resultJson["timing"]; - std::cout << std::fixed << std::setprecision(2); - std::cout << "Setup/Model Build : " << std::setw(8) << t.value("setupMs", 0.0) << " ms" << std::endl; - std::cout << "Driver Solve : " << std::setw(8) << t.value("driverSolveMs", 0.0) << " ms" << std::endl; - if (t.contains("spotterSolveMs")) { - std::cout << "Spotter Solve : " << std::setw(8) << t.value("spotterSolveMs", 0.0) << " ms" << std::endl; - } - std::cout << "Total Wall Time : " << std::setw(8) << t.value("totalSeconds", 0.0) << " s" << std::endl; + // Print Timing (regardless of success/failure) + if (resultJson.contains("timing")) { + std::cout << "\n--- ⏱️ Timing Performance ---" << std::endl; + json t = resultJson["timing"]; + std::cout << std::fixed << std::setprecision(2); + std::cout << "Setup/Model Build : " << std::setw(8) << t.value("setupMs", 0.0) << " ms" << std::endl; + std::cout << "Driver Solve : " << std::setw(8) << t.value("driverSolveMs", 0.0) << " ms" << std::endl; + if (t.contains("spotterSolveMs")) { + std::cout << "Spotter Solve : " << std::setw(8) << t.value("spotterSolveMs", 0.0) << " ms" << std::endl; } + std::cout << "Total Wall Time : " << std::setw(8) << t.value("totalSeconds", 0.0) << " s" << std::endl; + } + } + // Check if the solve was successful + if (resultCode == 0) { + // Success + if (!quiet) { // Print the schedule std::cout << "\n--- 🏁 Race Schedule ---" << std::endl; bool hasSpotters = (solverOptions.spotterMode != JRES_SPOTTER_MODE_NONE); diff --git a/src/jres_solver.cpp b/src/jres_solver.cpp index a5c1614..0d05337 100644 --- a/src/jres_solver.cpp +++ b/src/jres_solver.cpp @@ -52,7 +52,15 @@ int solve_race_schedule(const char* raceDataJson, *outputJson = create_output_string(resultJson.dump(4)); return 0; + } catch (const SolverException& e) { + // Custom exception with metadata - preserve timing/complexity + json errorJson = e.metadata; + errorJson["success"] = false; + errorJson["error"] = e.what(); + *outputJson = create_output_string(errorJson.dump(4)); + return -1; } catch (const std::exception& e) { + // Generic exception - minimal error JSON json errorJson; errorJson["success"] = false; errorJson["error"] = e.what(); diff --git a/src/jres_solver_types.hpp b/src/jres_solver_types.hpp index b98113e..7b4959e 100644 --- a/src/jres_solver_types.hpp +++ b/src/jres_solver_types.hpp @@ -80,6 +80,17 @@ struct ComplexityMetrics { double finalGap = 0.0; }; +/** + * @brief Custom exception that carries solver metadata for better error reporting. + */ +class SolverException : public std::runtime_error { +public: + json metadata; + + SolverException(const std::string& message, const json& meta = json::object()) + : std::runtime_error(message), metadata(meta) {} +}; + // --- JSON Deserialization Declarations --- void from_json(const json &j, TeamMember &member); void from_json(const json &j, RaceData &data); diff --git a/src/jres_standard_solver.cpp b/src/jres_standard_solver.cpp index 9004a63..cfc1197 100644 --- a/src/jres_standard_solver.cpp +++ b/src/jres_standard_solver.cpp @@ -141,15 +141,21 @@ json JresStandardSolver::solve() metrics.finalGap = info.mip_gap; // Relative gap HighsModelStatus status = m_highs->getModelStatus(); - if (status != HighsModelStatus::kOptimal && status != HighsModelStatus::kTimeLimit) { - // Strict check: if it's infeasible or unbounded - if (status == HighsModelStatus::kInfeasible) { - throw std::runtime_error("Model is infeasible. No solution exists."); - } - // Note: HiGHS might return kTimeLimit but still have a valid feasible solution + + // --- Calculate Total Time --- + auto endTotal = high_resolution_clock::now(); + double totalDurationSeconds = duration(endTotal - startTotal).count(); + + // --- Build Timing JSON (always include, even on failure) --- + json timingJson; + timingJson["setupMs"] = setupDurationMs; + timingJson["driverSolveMs"] = driverSolveDurationMs; + if (m_ctx.spotterMode == SpotterMode::Sequential) { + timingJson["spotterSolveMs"] = spotterSolveDurationMs; } + timingJson["totalSeconds"] = totalDurationSeconds; - // --- 6. Process Results --- + // --- Build base output JSON (always include, even on failure) --- json outputJson; json metaJson; to_json(metaJson, m_ctx); @@ -158,8 +164,20 @@ json JresStandardSolver::solve() json complexityJson; to_json(complexityJson, metrics); outputJson["complexity"] = complexityJson; + outputJson["timing"] = timingJson; outputJson["raceData"] = m_ctx.raceData; + // Check for infeasibility - throw exception with metadata for error handler + if (status != HighsModelStatus::kOptimal && status != HighsModelStatus::kTimeLimit) { + // Strict check: if it's infeasible or unbounded + if (status == HighsModelStatus::kInfeasible) { + throw SolverException("Model is infeasible. No solution exists.", outputJson); + } + // Note: HiGHS might return kTimeLimit but still have a valid feasible solution + } + + // --- 6. Process Results (only for successful solves) --- + std::vector schedule; // Get solution vector @@ -257,21 +275,11 @@ json JresStandardSolver::solve() auto spotterEnd = high_resolution_clock::now(); spotterSolveDurationMs = duration(spotterEnd - spotterStart).count(); - } - - auto endTotal = high_resolution_clock::now(); - double totalDurationSeconds = duration(endTotal - startTotal).count(); - - // Timing JSON - json timingJson; - timingJson["setupMs"] = setupDurationMs; - timingJson["driverSolveMs"] = driverSolveDurationMs; - if (m_ctx.spotterMode == SpotterMode::Sequential) { + + // Update timing JSON with spotter solve time timingJson["spotterSolveMs"] = spotterSolveDurationMs; } - timingJson["totalSeconds"] = totalDurationSeconds; - outputJson["timing"] = timingJson; json scheduleJson = json::array(); bool hasSpotters = (m_ctx.spotterMode != SpotterMode::None); for(const auto& entry : schedule) {