diff --git a/src/data_read.py b/src/data_read.py index 9b3ce7e..3d107d1 100644 --- a/src/data_read.py +++ b/src/data_read.py @@ -48,7 +48,13 @@ def load_data(filepath): for t in data.get("user_tasks", []) ] time_blocks = [ - TimeBlock(start=_parse_datetime(b["start"]), end=_parse_datetime(b["end"]), daily=b.get("daily", True), name=b.get("name", ""), id=b.get("id")) + TimeBlock( + start=_parse_datetime(b["start"]), + end=_parse_datetime(b["end"]), + daily=b.get("daily", True), + name=b.get("name", ""), + id=b.get("id"), + ) for b in data.get("time_blocks", []) ] diff --git a/src/restrictions.py b/src/restrictions.py index 195f49e..97e2ae0 100644 --- a/src/restrictions.py +++ b/src/restrictions.py @@ -16,7 +16,7 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes raise ValueError(f"min_horizon_days must be greater than 0, got {min_horizon_days}") steps_per_day = 1440 // step_minutes - + # Calculate a pessimistic safe maximum bound to generate time windows base_horizon = sum(task.duration_steps for task in user_tasks) max_deadline = max( @@ -24,7 +24,6 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes ) pessimistic_max = max(base_horizon * 3 + steps_per_day, min_horizon_days * steps_per_day, max_deadline) - blocked_intervals = generate_blocked_intervals(time_blocks, pessimistic_max, step_minutes) free_windows = [] curr = 0 @@ -35,7 +34,6 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes if curr < pessimistic_max: free_windows.append((curr, pessimistic_max)) - task_by_id = {t.id: t for t in user_tasks if getattr(t, "id", None) is not None} in_degree = {t.id: 0 for t in user_tasks if getattr(t, "id", None) is not None} adj = {t.id: [] for t in user_tasks if getattr(t, "id", None) is not None} @@ -72,13 +70,15 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes if getattr(t, "id", None) is None or t.id not in added: sorted_tasks.append(t) - release_times = {t.id: getattr(t, "start_steps", 0) for t in user_tasks if getattr(t, "id", None) is not None} simulated_horizon = 0 for task in sorted_tasks: chunks = [] - if getattr(task, "min_chunk_duration_steps", None) is not None and task.duration_steps > task.min_chunk_duration_steps: + if ( + getattr(task, "min_chunk_duration_steps", None) is not None + and task.duration_steps > task.min_chunk_duration_steps + ): max_chunks = math.ceil(task.duration_steps / task.min_chunk_duration_steps) remainder = task.duration_steps - (max_chunks - 1) * task.min_chunk_duration_steps for _ in range(max_chunks - 1): @@ -87,7 +87,11 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes else: chunks.append(task.duration_steps + task.break_duration_steps) - t_curr = release_times.get(task.id, getattr(task, "start_steps", 0)) if getattr(task, "id", None) is not None else getattr(task, "start_steps", 0) + t_curr = ( + release_times.get(task.id, getattr(task, "start_steps", 0)) + if getattr(task, "id", None) is not None + else getattr(task, "start_steps", 0) + ) deadline = task.deadline_steps if getattr(task, "deadline_steps", None) is not None else math.inf for chunk_size in chunks: @@ -103,7 +107,7 @@ def calculate_horizon(user_tasks, time_blocks, min_horizon_days=14, step_minutes new_windows.append((w_start, start_time)) if w_end > start_time + chunk_size: new_windows.append((start_time + chunk_size, w_end)) - new_windows.extend(free_windows[i+1:]) + new_windows.extend(free_windows[i + 1 :]) free_windows = new_windows placed = True break diff --git a/src/routine_expansion.py b/src/routine_expansion.py index 4047c30..2b94625 100644 --- a/src/routine_expansion.py +++ b/src/routine_expansion.py @@ -36,7 +36,7 @@ def expand_routines(routines, now, horizon_minutes, step_minutes=1): continue # Check if routine should be skipped - if getattr(routine, 'resume_after', None) and current_date <= routine.resume_after: + if getattr(routine, "resume_after", None) and current_date <= routine.resume_after: continue # It's a valid day for this routine @@ -51,7 +51,9 @@ def expand_routines(routines, now, horizon_minutes, step_minutes=1): end_steps = start_steps + routine.duration_steps if end_steps > 0 and start_steps <= horizon_minutes: - extra_blocks.append(TimeBlock(start_steps, end_steps, daily=False, name=routine.name, id=routine.id)) + extra_blocks.append( + TimeBlock(start_steps, end_steps, daily=False, name=routine.name, id=routine.id) + ) routine_info.append( { "name": routine.name, diff --git a/src/scheduler.py b/src/scheduler.py index aacb6a0..5eb03f3 100644 --- a/src/scheduler.py +++ b/src/scheduler.py @@ -108,12 +108,14 @@ def _expand_timeblocks_for_export( if curr_end > 0: # at least partially in the future start_dt = now + timedelta(minutes=curr_start * step_minutes) end_dt = now + timedelta(minutes=curr_end * step_minutes) - result.append(ScheduledTimeBlock( - name=tb.name, - start_time=start_dt, - end_time=end_dt, - id=tb.id, - )) + result.append( + ScheduledTimeBlock( + name=tb.name, + start_time=start_dt, + end_time=end_dt, + id=tb.id, + ) + ) curr_start += steps_per_day curr_end += steps_per_day else: @@ -122,12 +124,14 @@ def _expand_timeblocks_for_export( if tb.end > 0: # at least partially in the future start_dt = now + timedelta(minutes=tb.start * step_minutes) end_dt = now + timedelta(minutes=tb.end * step_minutes) - result.append(ScheduledTimeBlock( - name=tb.name, - start_time=start_dt, - end_time=end_dt, - id=tb.id, - )) + result.append( + ScheduledTimeBlock( + name=tb.name, + start_time=start_dt, + end_time=end_dt, + id=tb.id, + ) + ) return result @@ -215,17 +219,23 @@ def solve( steps_per_day = 1440 // self.step_minutes base_horizon = sum(task.duration_steps for task in active_tasks) max_deadline = max( - (getattr(t, "deadline_steps", 0) for t in self.tasks if getattr(t, "deadline_steps", None) is not None), default=0 + (getattr(t, "deadline_steps", 0) for t in self.tasks if getattr(t, "deadline_steps", None) is not None), + default=0, ) pessimistic_max = max(base_horizon * 3 + steps_per_day, actual_horizon_days * steps_per_day, max_deadline) - + sim_extra_tasks, sim_extra_blocks, _ = expand_routines(self.routines, now, pessimistic_max, self.step_minutes) combined_sim_tasks = active_tasks + sim_extra_tasks combined_sim_blocks = processed_blocks + sim_extra_blocks - + # 2. Calculate mathematical minimum horizon using simulation (tracks only non-routine tasks) - simulated_horizon = calculate_horizon(combined_sim_tasks, combined_sim_blocks, min_horizon_days=actual_horizon_days, step_minutes=self.step_minutes) - + simulated_horizon = calculate_horizon( + combined_sim_tasks, + combined_sim_blocks, + min_horizon_days=actual_horizon_days, + step_minutes=self.step_minutes, + ) + # 3. Add 1 day of slack and snap to end of day to give the solver breathing room horizon = simulated_horizon + steps_per_day horizon = math.ceil(horizon / steps_per_day) * steps_per_day @@ -234,8 +244,10 @@ def solve( extra_tasks, extra_blocks, routine_info = expand_routines(self.routines, now, horizon, self.step_minutes) # Pre-filter expired routine tasks - expired_routine_tasks = [t for t in extra_tasks if getattr(t, 'deadline_steps', None) is not None and t.deadline_steps <= 0] - extra_tasks = [t for t in extra_tasks if getattr(t, 'deadline_steps', None) is None or t.deadline_steps > 0] + expired_routine_tasks = [ + t for t in extra_tasks if getattr(t, "deadline_steps", None) is not None and t.deadline_steps <= 0 + ] + extra_tasks = [t for t in extra_tasks if getattr(t, "deadline_steps", None) is None or t.deadline_steps > 0] # Combine base and extra data combined_tasks = active_tasks + extra_tasks @@ -260,7 +272,7 @@ def solve( packer_status = solver.solve(model) if packer_status == cp_model.MODEL_INVALID: print("MODEL_INVALID Details:", model.Validate()) - + gravity_status = None if packer_status in (cp_model.OPTIMAL, cp_model.FEASIBLE): @@ -288,9 +300,7 @@ def solve( result = ScheduleResult(packer_status_name=packer_str, gravity_status_name=gravity_str) result.horizon = horizon * self.step_minutes - result.scheduled_timeblocks = _expand_timeblocks_for_export( - combined_blocks, horizon, now, self.step_minutes - ) + result.scheduled_timeblocks = _expand_timeblocks_for_export(combined_blocks, horizon, now, self.step_minutes) # Add pre-filtered expired items to skipped lists for task in expired_tasks: diff --git a/src/utils.py b/src/utils.py index 395ee2a..cc91479 100644 --- a/src/utils.py +++ b/src/utils.py @@ -68,7 +68,11 @@ def process_time_blocks(time_blocks, now, step_minutes=1): if end_min > 0: new_block = TimeBlock( - start=math.floor(start_min / step_minutes), end=math.ceil(end_min / step_minutes), daily=False, name=b.name, id=b.id + start=math.floor(start_min / step_minutes), + end=math.ceil(end_min / step_minutes), + daily=False, + name=b.name, + id=b.id, ) processed_blocks.append(new_block)