1+ #include " solver_diagnostics.hpp"
2+ #include " capacity_analyzer.hpp"
3+ #include " ../utils/date_utils.hpp"
4+ #include < sstream>
5+ #include < iomanip>
6+ #include < set>
7+ #include < chrono>
8+ #include < algorithm>
9+ #include < cmath>
10+ #include < tuple>
11+
12+ namespace jres ::analysis {
13+
14+ std::string explain_assignment_failure (
15+ int stintIndex,
16+ const std::string& violationDriver,
17+ const jres::internal::SolverInput& input,
18+ const std::vector<jres::internal::TeamMember>& driverPool,
19+ const std::map<std::pair<std::string, int >, int >& driverWorkVars,
20+ const std::vector<double >& colValues)
21+ {
22+ using namespace jres ::internal;
23+ std::ostringstream ss;
24+
25+ // Time points for the target stint
26+ auto tStart = TimeHelpers::stringToTimePoint (input.stints [stintIndex].startTime );
27+ auto tEnd = TimeHelpers::stringToTimePoint (input.stints [stintIndex].endTime );
28+
29+ // Identify which drivers are assigned to which stints in the CURRENT solution
30+ std::map<std::string, std::set<int >> driverAssignments;
31+ for (size_t s = 0 ; s < input.stints .size (); ++s) {
32+ for (const auto & p : driverPool) {
33+ if (driverWorkVars.count ({p.name , (int )s})) {
34+ int idx = driverWorkVars.at ({p.name , (int )s});
35+ if (idx < (int )colValues.size () && colValues[idx] > 0.5 ) {
36+ driverAssignments[p.name ].insert ((int )s);
37+ }
38+ }
39+ }
40+ }
41+
42+ // Helper: Get stint duration in hours
43+ auto getDuration = [&](int sIdx ) {
44+ auto s = TimeHelpers::stringToTimePoint (input.stints [sIdx ].startTime );
45+ auto e = TimeHelpers::stringToTimePoint (input.stints [sIdx ].endTime );
46+ return std::chrono::duration<double , std::ratio<3600 >>(e - s).count ();
47+ };
48+
49+ ss << " Alternatives Analysis:" ;
50+
51+ for (const auto & candidate : driverPool) {
52+ if (candidate.name == violationDriver) continue ;
53+
54+ ss << " \n - " << candidate.name << " : " ;
55+ std::vector<std::string> reasons;
56+
57+ // Check Availability
58+ bool isUnavailable = false ;
59+ auto it = input.availability .find (candidate.name );
60+ if (it != input.availability .end ()) {
61+ auto cursor = tStart;
62+ while (cursor < tEnd) {
63+ std::string key = TimeHelpers::timePointToKey (cursor);
64+ if (it->second .count (key) && it->second .at (key) == Availability::Unavailable) {
65+ isUnavailable = true ;
66+ break ;
67+ }
68+ cursor += std::chrono::hours (1 );
69+ }
70+ }
71+ if (isUnavailable) {
72+ reasons.push_back (" Also Unavailable" );
73+ }
74+
75+ // Check Consecutive Stints
76+ int consecutiveCount = 1 ;
77+ for (int k = stintIndex - 1 ; k >= 0 ; --k) {
78+ if (driverAssignments[candidate.name ].count (k)) consecutiveCount++;
79+ else break ;
80+ }
81+ for (size_t k = stintIndex + 1 ; k < input.stints .size (); ++k) {
82+ if (driverAssignments[candidate.name ].count ((int )k)) consecutiveCount++;
83+ else break ;
84+ }
85+
86+ if (consecutiveCount > input.consecutiveStints ) {
87+ reasons.push_back (" Max Consecutive Limit (" + std::to_string (consecutiveCount) + " /" + std::to_string (input.consecutiveStints ) + " )" );
88+ }
89+
90+ // Check Minimum Rest
91+ if (input.minimumRestHours > 0 ) {
92+ double minRestSec = input.minimumRestHours * 3600.0 ;
93+ for (int assignedS : driverAssignments[candidate.name ]) {
94+ auto s1_start = TimeHelpers::stringToTimePoint (input.stints [stintIndex].startTime );
95+ auto s1_end = TimeHelpers::stringToTimePoint (input.stints [stintIndex].endTime );
96+ auto s2_start = TimeHelpers::stringToTimePoint (input.stints [assignedS].startTime );
97+ auto s2_end = TimeHelpers::stringToTimePoint (input.stints [assignedS].endTime );
98+
99+ double gap = 0.0 ;
100+ if (s1_end <= s2_start) gap = std::chrono::duration<double >(s2_start - s1_end).count ();
101+ else if (s2_end <= s1_start) gap = std::chrono::duration<double >(s1_start - s2_end).count ();
102+ else gap = -1.0 ;
103+
104+ if (gap < minRestSec - 1.0 ) {
105+ double needed = (minRestSec - gap) / 3600.0 ;
106+ std::ostringstream rss;
107+ rss << std::fixed << std::setprecision (1 ) << " Needs " << needed << " h more rest" ;
108+ reasons.push_back (rss.str ());
109+ break ;
110+ }
111+ }
112+ }
113+
114+ // Check Max Busy Time
115+ if (input.maximumBusyHours > 0 ) {
116+ double busyDuration = getDuration (stintIndex);
117+ for (int k = stintIndex - 1 ; k >= 0 ; --k) {
118+ if (driverAssignments[candidate.name ].count (k)) busyDuration += getDuration (k);
119+ else break ;
120+ }
121+ for (size_t k = stintIndex + 1 ; k < input.stints .size (); ++k) {
122+ if (driverAssignments[candidate.name ].count ((int )k)) busyDuration += getDuration (k);
123+ else break ;
124+ }
125+ if (busyDuration > input.maximumBusyHours ) {
126+ reasons.push_back (" Max Busy Time Exceeded (" + std::to_string (busyDuration) + " h > " + std::to_string (input.maximumBusyHours ) + " h)" );
127+ }
128+ }
129+
130+ if (reasons.empty ()) {
131+ ss << " Available (Unknown constraint or softer optimization preference)" ;
132+ } else {
133+ for (size_t i=0 ; i<reasons.size (); ++i) {
134+ if (i > 0 ) ss << " , " ;
135+ ss << reasons[i];
136+ }
137+ }
138+ }
139+ return ss.str ();
140+ }
141+
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