44import math
55import sys
66
7- def assign_tables (employees_data , tables_data , constraints_config , time_limit_sec = 30.0 ):
7+ def assign_tables (search_workers_count , employees_data , tables_data , constraints_config , time_limit_sec = 30.0 ):
88 """
99 Assigns employee groups to tables to maximize diversity using Google OR-Tools.
1010 Handles groups of people represented by a single employee record with an "Anzahl" > 1.
@@ -39,7 +39,14 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
3939
4040 if total_seats_required > total_capacity :
4141 print (f"Error: Not enough capacity ({ total_capacity } ) for all people ({ total_seats_required } )." )
42- return employees_data
42+ sys .exit (2 )
43+
44+ # Check for feasibility issues
45+ max_group_size = df_employees ['Anzahl' ].max ()
46+ max_table_capacity = df_tables ['Capacity' ].max ()
47+ if max_group_size > max_table_capacity :
48+ print (f"Error: Largest group ({ max_group_size } ) exceeds largest table capacity ({ max_table_capacity } )." )
49+ sys .exit (3 )
4350
4451 # --- 2. Create the CP-SAT Model ---
4552 model = cp_model .CpModel ()
@@ -50,9 +57,11 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
5057 assignment [(g_idx , t_idx )] = model .NewBoolVar (f'assign_g{ g_idx } _t{ t_idx } ' )
5158
5259 # --- 3. Add Core Constraints ---
60+ # Each group assigned to exactly one table
5361 for g_idx in range (num_groups ):
5462 model .AddExactlyOne ([assignment [(g_idx , t_idx )] for t_idx in range (num_tables )])
5563
64+ # Table capacity constraints
5665 for t_idx in range (num_tables ):
5766 table_capacity = df_tables .loc [t_idx , 'Capacity' ]
5867 groups_at_table = [df_employees .loc [g_idx , 'Anzahl' ] * assignment [(g_idx , t_idx )] for g_idx in range (num_groups )]
@@ -63,10 +72,16 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
6372
6473 # Add a penalty for each table used to encourage filling tables
6574 for t_idx in range (num_tables ):
75+ table_capacity = df_tables .loc [t_idx , 'Capacity' ]
76+
77+ # Calculate actual occupancy
78+ groups_at_table = [df_employees .loc [g_idx , 'Anzahl' ] * assignment [(g_idx , t_idx )] for g_idx in range (num_groups )]
79+ total_seated_at_table = cp_model .LinearExpr .Sum (groups_at_table )
80+
81+ # Encourage table usage with diminishing returns
6682 is_table_used = model .NewBoolVar (f'table_used_{ t_idx } ' )
6783 assignments_to_table = [assignment [(g_idx , t_idx )] for g_idx in range (num_groups )]
6884 model .AddMaxEquality (is_table_used , assignments_to_table )
69- objective_terms .append (- 1 * is_table_used )
7085
7186 # Add a penalty for each empty seat to encourage fuller tables
7287 groups_at_table = [df_employees .loc [g_idx , 'Anzahl' ] * assignment [(g_idx , t_idx )] for g_idx in range (num_groups )]
@@ -76,10 +91,9 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
7691
7792 objective_terms .append (- 0.1 * wasted_space )
7893
79-
8094 for attr , weight in constraints_config .items ():
8195 if weight == 0 :
82- continue
96+ continue
8397
8498 if attr == 'last neighborhood' :
8599 if 'last neighborhood' not in df_employees .columns or 'ProfileID' not in df_employees .columns :
@@ -105,70 +119,165 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
105119 continue
106120 processed_pairs .add (pair )
107121
122+ # IMPROVED: Softer constraint implementation
108123 for t_idx in range (num_tables ):
109124 seated_together = model .NewBoolVar (f'seated_together_g{ g_idx } _n{ neighbor_g_idx } _t{ t_idx } ' )
110125 model .Add (seated_together <= assignment [(g_idx , t_idx )])
111126 model .Add (seated_together <= assignment [(neighbor_g_idx , t_idx )])
112127 model .Add (seated_together >= assignment [(g_idx , t_idx )] + assignment [(neighbor_g_idx , t_idx )] - 1 )
113- objective_terms .append (- weight * seated_together )
114-
128+
129+ # Use absolute weight value to ensure proper direction
130+ objective_terms .append (- abs (weight ) * seated_together )
131+
115132 else :
133+ # IMPROVED: Diversity constraints with better handling
116134 if attr not in df_employees .columns :
117135 print (f"Warning: Diversity attribute '{ attr } ' not found. Skipping." )
118136 continue
119-
137+
138+ # Skip if all values are the same (no diversity possible)
139+ unique_values = df_employees [attr ].dropna ().unique ()
140+ if len (unique_values ) <= 1 :
141+ print (f"Warning: Attribute '{ attr } ' has no diversity (only { len (unique_values )} unique values). Skipping." )
142+ continue
143+
120144 print (f"Applying '{ attr } ' diversity constraint with weight: { weight } " )
145+
121146 for t_idx in range (num_tables ):
122147 table_id = df_tables .loc [t_idx , 'TableNr' ]
148+
149+ # IMPROVED: Only consider non-null values for diversity
123150 for value in df_employees [attr ].unique ():
124- is_value_present_at_table = model .NewBoolVar (f'attr_{ attr } _val_{ str (value )} _table_{ table_id } ' )
151+ is_value_present_at_table = model .NewBoolVar (f'attr_{ attr } _val_{ str (value ). replace ( " " , "_" ) } _table_{ table_id } ' )
125152 groups_with_value_indices = df_employees [df_employees [attr ] == value ].index .tolist ()
126153 assignments_for_value_at_table = [assignment [(g_idx , t_idx )] for g_idx in groups_with_value_indices ]
127-
154+
128155 if assignments_for_value_at_table :
129156 model .AddMaxEquality (is_value_present_at_table , assignments_for_value_at_table )
157+ objective_terms .append (abs (weight ) * is_value_present_at_table )
130158 else :
131159 model .Add (is_value_present_at_table == 0 )
132-
133- objective_terms .append (weight * is_value_present_at_table )
134160
135161 # --- 5. Set the Combined Objective ---
136162 if objective_terms :
137163 model .Maximize (cp_model .LinearExpr .Sum (objective_terms ))
164+ print (f"Created { len (objective_terms )} objective terms" )
138165 else :
139- print ("Warning: No objective terms were created." )
166+ print ("Warning: No objective terms were created. Using basic feasibility. " )
140167
141- # --- 6. Solve the Model ---
168+ # --- 6. IMPROVED: Solve with better parameters ---
142169 solver = cp_model .CpSolver ()
143170 solver .parameters .max_time_in_seconds = float (time_limit_sec )
171+ solver .parameters .num_search_workers = search_workers_count # We can use multiple cores
172+ solver .parameters .log_search_progress = True
173+ solver .parameters .cp_model_presolve = True
174+
175+ print ("Starting solver..." )
144176 status = solver .Solve (model )
145177
146- # --- 7. Process the Results ---
178+ # --- 7. IMPROVED: Better result processing and debugging ---
179+ print (f"Solver finished with status: { solver .StatusName (status )} " )
180+ print (f"Solver statistics:" )
181+ print (f" - Conflicts: { solver .NumConflicts ()} " )
182+ print (f" - Branches: { solver .NumBranches ()} " )
183+ print (f" - Wall time: { solver .WallTime ():.2f} s" )
184+
147185 if status == cp_model .OPTIMAL or status == cp_model .FEASIBLE :
148186 status_name = 'OPTIMAL' if status == cp_model .OPTIMAL else 'FEASIBLE'
149187 print (f"\n Solution found with status: { status_name } " )
150188 if objective_terms :
151189 print (f"Total objective score achieved: { solver .ObjectiveValue ()} " )
152190
191+ # Assign tables to groups
153192 for g_idx in range (num_groups ):
154193 for t_idx in range (num_tables ):
155194 if solver .Value (assignment [(g_idx , t_idx )]) == 1 :
156195 assigned_table_id = df_tables .loc [t_idx , 'TableNr' ]
157196 group_size = df_employees .loc [g_idx , 'Anzahl' ]
158-
197+
159198 current_event_assignment = [str (assigned_table_id )] * group_size
160199 employees_data [g_idx ]['TableNr' ] = current_event_assignment
161200 break
201+
202+ # IMPROVED: Validation of solution
203+ print ("\n --- Solution Validation ---" )
204+ table_occupancy = {}
205+ for g_idx in range (num_groups ):
206+ for t_idx in range (num_tables ):
207+ if solver .Value (assignment [(g_idx , t_idx )]) == 1 :
208+ table_id = df_tables .loc [t_idx , 'TableNr' ]
209+ if table_id not in table_occupancy :
210+ table_occupancy [table_id ] = 0
211+ table_occupancy [table_id ] += df_employees .loc [g_idx , 'Anzahl' ]
212+
213+ for table_id , occupancy in table_occupancy .items ():
214+ table_capacity = df_tables [df_tables ['TableNr' ] == table_id ]['Capacity' ].iloc [0 ]
215+ print (f"Table { table_id } : { occupancy } /{ table_capacity } seats" )
216+ if occupancy > table_capacity :
217+ print (f"ERROR: Table { table_id } is over capacity!" )
218+
219+ return employees_data
220+
221+ elif status == cp_model .INFEASIBLE :
222+ print (f"\n PROBLEM IS INFEASIBLE!" )
223+ print ("Debugging information:" )
224+ print (f"Total people to seat: { total_seats_required } " )
225+ print (f"Total capacity: { total_capacity } " )
226+ print (f"Largest group: { max_group_size } " )
227+ print (f"Largest table: { max_table_capacity } " )
228+
229+ # IMPROVED: Try to identify infeasibility source
230+ print ("\n Table capacities vs group sizes:" )
231+ for idx , row in df_tables .iterrows ():
232+ groups_that_fit = sum (1 for _ , emp_row in df_employees .iterrows () if emp_row ['Anzahl' ] <= row ['Capacity' ])
233+ print (f"Table { row ['TableNr' ]} (capacity { row ['Capacity' ]} ): { groups_that_fit } groups can fit" )
234+
235+ # Try a relaxed version to help debug
236+ print ("\n Trying relaxed model for debugging..." )
237+ relaxed_model = cp_model .CpModel ()
238+ relaxed_assignment = {}
239+
240+ for g_idx in range (num_groups ):
241+ for t_idx in range (num_tables ):
242+ relaxed_assignment [(g_idx , t_idx )] = relaxed_model .NewBoolVar (f'assign_g{ g_idx } _t{ t_idx } ' )
243+
244+ # Only add basic constraints
245+ for g_idx in range (num_groups ):
246+ relaxed_model .AddExactlyOne ([relaxed_assignment [(g_idx , t_idx )] for t_idx in range (num_tables )])
247+
248+ # Relaxed capacity constraints (allow slight overflow)
249+ for t_idx in range (num_tables ):
250+ table_capacity = df_tables .loc [t_idx , 'Capacity' ]
251+ groups_at_table = [df_employees .loc [g_idx , 'Anzahl' ] * relaxed_assignment [(g_idx , t_idx )] for g_idx in range (num_groups )]
252+ overflow = relaxed_model .NewIntVar (0 , max_group_size , f'overflow_t{ t_idx } ' )
253+ relaxed_model .Add (cp_model .LinearExpr .Sum (groups_at_table ) <= table_capacity + overflow )
254+ relaxed_model .Minimize (overflow ) # Try to minimize overflow
255+
256+ relaxed_solver = cp_model .CpSolver ()
257+ relaxed_solver .parameters .max_time_in_seconds = 10.0
258+ relaxed_status = relaxed_solver .Solve (relaxed_model )
259+
260+ if relaxed_status in [cp_model .OPTIMAL , cp_model .FEASIBLE ]:
261+ print ("Relaxed model found solution - problem likely with soft constraints being too restrictive" )
262+ else :
263+ print ("Even relaxed model failed - fundamental capacity/assignment issue" )
264+
162265 return employees_data
163266 else :
164267 status_name = solver .StatusName (status )
165- print (f"\n No solution found. Solver status: { status_name } " )
166- sys .exit (- 1 )
268+ print (f"\n Solver could not find solution. Status: { status_name } " )
269+ if status == cp_model .MODEL_INVALID :
270+ print ("Model is invalid - check constraint definitions" )
271+ elif status == cp_model .UNKNOWN :
272+ print ("Solver timed out or encountered other issues" )
167273
168- # --- Main Execution Block ---
274+ return employees_data
275+
276+ # --- Main Execution Block (unchanged) ---
169277if __name__ == "__main__" :
170- if len (sys .argv ) != 5 :
171- print ("Usage: python algo.py <input_json_path> <table_json_path> <config_json_path> <output_json_path>" )
278+ if len (sys .argv ) != 7 :
279+ print ("Usage: python algo.py <input_json_path> <table_json_path> <config_json_path> <output_json_path> <num_search_workers> <solver_time_limit>" )
280+ print (sys .argv )
172281 sys .exit (1 )
173282
174283 # --- Configuration ---
@@ -180,9 +289,8 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
180289 TABLES_JSON_PATH = sys .argv [2 ]
181290 CONSTRAINTS_JSON_PATH = sys .argv [3 ]
182291 OUTPUT_JSON_PATH = sys .argv [4 ]
183-
184- # Set a time limit for the solver
185- SOLVER_TIME_LIMIT = 300.0
292+ num_search_workers = int (sys .argv [5 ])
293+ SOLVER_TIME_LIMIT = int (sys .argv [6 ])
186294
187295 print ("--- Starting Table Assignment from File ---" )
188296 try :
@@ -196,10 +304,11 @@ def assign_tables(employees_data, tables_data, constraints_config, time_limit_se
196304 constraints_config = json .load (f )
197305
198306 assigned_employees = assign_tables (
307+ num_search_workers ,
199308 employees_data = employee_list ,
200309 tables_data = table_list ,
201310 constraints_config = constraints_config ,
202- time_limit_sec = SOLVER_TIME_LIMIT
311+ time_limit_sec = SOLVER_TIME_LIMIT ,
203312 )
204313
205314 if assigned_employees and all ('TableNr' in r for r in assigned_employees ):
0 commit comments