11#include " solver_diagnostics.hpp"
2+ #include " capacity_analyzer.hpp"
23#include " ../utils/date_utils.hpp"
34#include < sstream>
45#include < iomanip>
56#include < set>
67#include < chrono>
8+ #include < algorithm>
9+ #include < cmath>
10+ #include < tuple>
711
812namespace jres ::analysis {
913
@@ -135,4 +139,174 @@ std::string explain_assignment_failure(
135139 return ss.str ();
136140}
137141
138- } // namespace jres::analysis
142+ /* *
143+ * @brief Helper to format a list of stint indices into a human-readable string using IDs.
144+ */
145+ std::string formatStintList (const std::vector<int >& indices, const jres::internal::SolverInput& input) {
146+ if (indices.empty ()) return " " ;
147+
148+ std::vector<int > sorted = indices;
149+ std::sort (sorted.begin (), sorted.end ());
150+
151+ std::ostringstream ss;
152+ for (size_t i = 0 ; i < sorted.size (); ++i) {
153+ int startIdx = sorted[i];
154+ int endIdx = startIdx;
155+ while (i + 1 < sorted.size () && sorted[i + 1 ] == endIdx + 1 ) {
156+ endIdx = sorted[++i];
157+ }
158+
159+ if (ss.tellp () > 0 ) ss << " , " ;
160+ if (startIdx == endIdx) ss << input.stints [startIdx].id ;
161+ else ss << input.stints [startIdx].id << " -" << input.stints [endIdx].id ;
162+ }
163+ return ss.str ();
164+ }
165+
166+ std::vector<std::string> formatHumanDiagnostic (
167+ const std::map<int , jres::internal::SlackInfo>& slackInfo,
168+ const std::set<int >& unavailableVars,
169+ const std::map<std::pair<std::string, int >, int >& driverWorkVars,
170+ const std::map<std::pair<std::string, int >, int >& spotterWorkVars,
171+ const std::vector<double >& colValues,
172+ const jres::internal::SolverInput& input,
173+ const std::vector<jres::internal::TeamMember>& driverPool,
174+ const std::vector<jres::internal::TeamMember>& spotterPool)
175+ {
176+ using namespace jres ::internal;
177+ std::vector<std::string> report;
178+
179+ // --- Roster Health Check (Drivers) ---
180+ if (!driverPool.empty ()) {
181+ auto cap = CapacityAnalyzer::calculate_max_potential_capacity (driverPool, input);
182+ if (cap.totalCapacity < (int )input.stints .size ()) {
183+ std::ostringstream ss;
184+ ss << " Driver Roster Understaffed: You have " << cap.totalCapacity
185+ << " stints of coverage for a " << input.stints .size () << " stint race." ;
186+ report.push_back (ss.str ());
187+ }
188+ }
189+
190+ // --- Collect Violations per Stint ---
191+ struct ViolationKey {
192+ int priority;
193+ std::string reason;
194+ std::string member;
195+
196+ bool operator <(const ViolationKey& other) const {
197+ return std::tie (priority, reason, member) < std::tie (other.priority , other.reason , other.member );
198+ }
199+ };
200+
201+ std::map<ViolationKey, std::vector<int >> groupedViolations;
202+ std::vector<std::string> globalViolations;
203+
204+ // Build reverse map for variable index -> (Member, Stint, Role)
205+ std::map<int , std::tuple<std::string, int , std::string>> varToInfo;
206+
207+ for (const auto & [key, varIdx] : driverWorkVars) {
208+ varToInfo[varIdx] = std::make_tuple (key.first , key.second , " Driver" );
209+ }
210+ for (const auto & [key, varIdx] : spotterWorkVars) {
211+ varToInfo[varIdx] = std::make_tuple (key.first , key.second , " Spotter" );
212+ }
213+
214+ // Check Unavailable (Priority 0)
215+ for (int varIdx : unavailableVars) {
216+ if (varIdx < (int )colValues.size () && colValues[varIdx] > 0.5 ) {
217+ if (varToInfo.count (varIdx)) {
218+ auto [member, stintIdx, role] = varToInfo[varIdx];
219+ std::string reason = std::string (" No one could " ) + (role == " Spotter" ? " spot" : " drive" ) +
220+ " without being Unavailable (" + member + " )" ;
221+ groupedViolations[{0 , reason, member}].push_back (stintIdx);
222+ }
223+ }
224+ }
225+
226+ // Check Slack (Rest, Busy, etc)
227+ for (const auto & [varIdx, info] : slackInfo) {
228+ if (varIdx < (int )colValues.size () && colValues[varIdx] > 0.001 ) {
229+ int priority = 3 ;
230+ if (info.type .find (" Busy" ) != std::string::npos) priority = 1 ;
231+ else if (info.type .find (" Rest" ) != std::string::npos) priority = 2 ;
232+ else if (info.type .find (" Fair Share" ) != std::string::npos) priority = 2 ;
233+
234+ if (info.stintIndex >= 0 ) {
235+ std::string reason;
236+ if (priority == 1 ) reason = " Max Busy Time exceeded (" + info.memberName + " )" ;
237+ else if (priority == 2 ) {
238+ if (info.type .find (" Fair Share" ) != std::string::npos) reason = " Fair Share Rules violated (" + info.memberName + " )" ;
239+ else reason = " Rest Rules violated (" + info.memberName + " )" ;
240+ }
241+ else reason = info.type + " (" + info.memberName + " )" ;
242+
243+ groupedViolations[{priority, reason, info.memberName }].push_back (info.stintIndex );
244+ } else {
245+ // Try to attribute global violation to stints
246+ bool assignedAny = false ;
247+ for (const auto & [vIdx, tupleInfo] : varToInfo) {
248+ auto [memName, stintIdx, role] = tupleInfo;
249+ if (memName == info.memberName && colValues[vIdx] > 0.5 ) {
250+ std::string reason;
251+ if (priority == 1 ) reason = " Max Busy Time exceeded (" + memName + " )" ;
252+ else if (priority == 2 ) {
253+ if (info.type .find (" Fair Share" ) != std::string::npos) reason = " Fair Share Rules violated (" + memName + " )" ;
254+ else reason = " Rest Rules violated (" + memName + " )" ;
255+ }
256+ else reason = info.type + " (" + memName + " )" ;
257+
258+ groupedViolations[{priority, reason, memName}].push_back (stintIdx);
259+ assignedAny = true ;
260+ }
261+ }
262+
263+ if (!assignedAny) {
264+ std::string reason = info.type ;
265+ if (priority == 2 ) {
266+ if (info.type .find (" Fair Share" ) != std::string::npos)
267+ reason = " Fair Share Rules violated" ;
268+ else
269+ reason = " Rest Rules violated" ;
270+ }
271+ globalViolations.push_back (reason + " (" + info.memberName + " )" );
272+ }
273+ }
274+ }
275+ }
276+
277+ // --- Sort & Format Grouped Violations ---
278+ std::vector<ViolationKey> sortedKeys;
279+ for (const auto & [key, _] : groupedViolations) sortedKeys.push_back (key);
280+ std::sort (sortedKeys.begin (), sortedKeys.end ());
281+
282+ for (const auto & key : sortedKeys) {
283+ const auto & stints = groupedViolations[key];
284+ std::string stintStr = formatStintList (stints, input);
285+
286+ std::ostringstream ss;
287+ if (stints.size () == 1 ) ss << " Stint " << stintStr << " : " << key.reason ;
288+ else ss << " Stints " << stintStr << " : " << key.reason ;
289+ report.push_back (ss.str ());
290+
291+ // Actionable Advice
292+ if (key.priority == 0 ) {
293+ report.push_back (" -> Advice: Mark " + key.member + " as Available during this window." );
294+ } else if (key.priority == 1 ) {
295+ report.push_back (" -> Advice: Increase 'Maximum Busy Hours' (currently " + std::to_string (input.maximumBusyHours ) + " h) or add more participants." );
296+ } else if (key.priority == 2 ) {
297+ if (key.reason .find (" Fair Share" ) != std::string::npos)
298+ report.push_back (" -> Advice: Ensure " + key.member + " is Available for more stints to meet the minimum driving time." );
299+ else
300+ report.push_back (" -> Advice: Reduce 'Minimum Rest Hours' (currently " + std::to_string (input.minimumRestHours ) + " h) or increase availability of other participants to cover the gap." );
301+ }
302+ }
303+
304+ // Append Global Violations (Unattributed)
305+ for (const auto & g : globalViolations) {
306+ report.push_back (" Global Violation: " + g);
307+ }
308+
309+ return report;
310+ }
311+
312+ } // namespace jres::analysis
0 commit comments