@@ -226,6 +226,51 @@ jres::internal::SolverOutput JresStandardSolver::solve()
226226 }
227227 }
228228 }
229+
230+ // --- Soft Constraint: Adjacency of spotter/driver duties ---
231+ for (const auto & p : m_input.teamMembers ) {
232+ if (p.isDriver && p.isSpotter ) {
233+ for (size_t s = 0 ; s < m_input.stints .size (); ++s) {
234+ if (m_spotterWorkVars.count ({p.name , s}) == 0 ) continue ;
235+
236+ auto spotterVar = m_spotterWorkVars.at ({p.name , s});
237+
238+ // Adjacency with drive stint BEFORE
239+ if (s > 0 && m_driverWorkVars.count ({p.name , s - 1 })) {
240+ auto driverBeforeVar = m_driverWorkVars.at ({p.name , s - 1 });
241+
242+ int adjBeforeVar = m_highs->getNumCol ();
243+ m_highs->addVar (0.0 , 1.0 );
244+ m_highs->changeColIntegrality (adjBeforeVar, HighsVarType::kInteger );
245+ m_highs->changeColCost (adjBeforeVar, -0.5 ); // Reward for adjacency
246+
247+ // z <= x (spotter)
248+ m_highs->addRow (-kHighsInf , 0.0 , 2 , std::vector<int >{adjBeforeVar, spotterVar}.data (), std::vector<double >{1.0 , -1.0 }.data ());
249+ // z <= y (driver before)
250+ m_highs->addRow (-kHighsInf , 0.0 , 2 , std::vector<int >{adjBeforeVar, driverBeforeVar}.data (), std::vector<double >{1.0 , -1.0 }.data ());
251+ // z >= x + y - 1 => z - x - y >= -1
252+ m_highs->addRow (-1.0 , kHighsInf , 3 , std::vector<int >{adjBeforeVar, spotterVar, driverBeforeVar}.data (), std::vector<double >{1.0 , -1.0 , -1.0 }.data ());
253+ }
254+
255+ // Adjacency with drive stint AFTER
256+ if (s < m_input.stints .size () - 1 && m_driverWorkVars.count ({p.name , s + 1 })) {
257+ auto driverAfterVar = m_driverWorkVars.at ({p.name , s + 1 });
258+
259+ int adjAfterVar = m_highs->getNumCol ();
260+ m_highs->addVar (0.0 , 1.0 );
261+ m_highs->changeColIntegrality (adjAfterVar, HighsVarType::kInteger );
262+ m_highs->changeColCost (adjAfterVar, -0.5 ); // Reward for adjacency
263+
264+ // z <= x (spotter)
265+ m_highs->addRow (-kHighsInf , 0.0 , 2 , std::vector<int >{adjAfterVar, spotterVar}.data (), std::vector<double >{1.0 , -1.0 }.data ());
266+ // z <= y (driver after)
267+ m_highs->addRow (-kHighsInf , 0.0 , 2 , std::vector<int >{adjAfterVar, driverAfterVar}.data (), std::vector<double >{1.0 , -1.0 }.data ());
268+ // z >= x + y - 1 => z - x - y >= -1
269+ m_highs->addRow (-1.0 , kHighsInf , 3 , std::vector<int >{adjAfterVar, spotterVar, driverAfterVar}.data (), std::vector<double >{1.0 , -1.0 , -1.0 }.data ());
270+ }
271+ }
272+ }
273+ }
229274 }
230275
231276 auto endSetup = high_resolution_clock::now ();
@@ -275,6 +320,44 @@ jres::internal::SolverOutput JresStandardSolver::solve()
275320 }
276321 spotterSolver.setOptionValue (" mip_rel_gap" , m_options.optimalityGap );
277322 add_participant_model (spotterSolver, m_spotterPool, m_spotterWorkVars);
323+
324+ // --- Soft Constraint: Adjacency of spotter/driver duties ---
325+ for (const auto & p : m_spotterPool) {
326+ const auto member_it = std::find_if (m_input.teamMembers .begin (), m_input.teamMembers .end (),
327+ [&](const jres::internal::TeamMember& tm){ return tm.name == p.name ; });
328+ if (member_it == m_input.teamMembers .end () || !member_it->isDriver ) continue ;
329+
330+ for (size_t s = 0 ; s < m_input.stints .size (); ++s) {
331+ if (m_spotterWorkVars.count ({p.name , s})) {
332+ double cost = 0.0 ;
333+
334+ auto stintStartTime = jres::internal::TimeHelpers::stringToTimePoint (m_input.stints [s].startTime );
335+ std::string availabilityKey = jres::internal::TimeHelpers::timePointToKey (stintStartTime);
336+ auto member_availability_it = m_input.availability .find (p.name );
337+ if (member_availability_it != m_input.availability .end ()) {
338+ auto time_availability_it = member_availability_it->second .find (availabilityKey);
339+ if (time_availability_it != member_availability_it->second .end ()) {
340+ if (time_availability_it->second == jres::internal::Availability::Preferred) {
341+ cost = -1.0 ;
342+ }
343+ }
344+ }
345+
346+ // Adjacency reward
347+ if (s > 0 && output.schedule [s - 1 ].driver == p.name ) {
348+ cost -= 0.5 ;
349+ }
350+ if (s < m_input.stints .size () - 1 && output.schedule [s + 1 ].driver == p.name ) {
351+ cost -= 0.5 ;
352+ }
353+
354+ if (cost != 0.0 ) {
355+ spotterSolver.changeColCost (m_spotterWorkVars.at ({p.name , s}), cost);
356+ }
357+ }
358+ }
359+ }
360+
278361 for (size_t s = 0 ; s < m_input.stints .size (); ++s) {
279362 std::vector<int > indices;
280363 std::vector<double > values;
0 commit comments