def evalute_normal_single_worker_n_job(self, env, job=None): # worker = None, # return score, violated_rules (negative values) # return self.weight * 1 # Now check if this new job can fit into existing score = 1 overall_message = "Job ({}) requires skill ({}), workers: {}. ".format( job.job_code, job.requested_skills, job.scheduled_worker_codes) metrics_detail = {"status_code": "OK"} for worker_code in job.scheduled_worker_codes: worker = env.workers_dict[worker_code] # TODO check skill_keys match as set, which should be faster. worker_skill_key_set = set(worker.skills.keys()) for skill_key in job.requested_skills: if skill_key not in worker_skill_key_set: overall_message += "Skill key({}) is not found in worker!".format( skill_key) score = -1 metrics_detail = {"missing_key": skill_key} break for skill in job.requested_skills[skill_key]: if skill not in worker.skills[skill_key]: overall_message += "({}={}) is not found in worker {}!".format( skill_key, skill, worker_code) score = -1 metrics_detail = {"missing_value": {skill_key: skill}} break score_res = ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, ) return score_res
def evalute_normal_single_worker_n_job(self, env, job=None): # worker = None, # return score, violated_rules (negative values) # return self.weight * 1 # Now check if this new job can fit into existing score = 1 overall_message = "Job ({}) requires items ({}) on workers {}. \n".format( job.job_code, job.requested_items, job.scheduled_worker_codes) metrics_detail = {"status_code": "OK"} total_loaded_items = {k: 0 for k in job.requested_items.keys()} total_requested_items = dict(job.requested_items) inspected_jobs = set() all_slots = [] # First I aggregate all items from all workers for worker_code in job.scheduled_worker_codes: worker = env.workers_dict[worker_code] overlapped_slots = env.slot_server.get_overlapped_slots( worker_id=worker_code, start_minutes=job.scheduled_start_minutes, end_minutes=job.scheduled_start_minutes + job.scheduled_duration_minutes) if len(overlapped_slots) < 1: overall_message += " but no slot was found!" score = -1 metrics_detail = {} break slot = overlapped_slots[0] # It should match only one, but if more, I take only first one. for jc in slot.assigned_job_codes: if jc in inspected_jobs: # This should be summerized only once for more workers. # Skip from this loop continue else: inspected_jobs.add(jc) # move on to check r_items = env.jobs_dict[jc].requested_items for ik in r_items.keys(): if ik not in total_requested_items.keys(): total_requested_items[ik] = r_items[ik] else: total_requested_items[ik] += r_items[ik] if ik not in total_loaded_items.keys(): total_requested_items[ik] = 0 # all_slots[0] is the first worker slot, i.e. primary all_slots.append(slot) # TODO check item_keys match as set, which should be faster. for k in slot.loaded_items.keys(): if k in total_loaded_items.keys(): total_loaded_items[k] += slot.loaded_items[k] # Second I verify that aggregated items list can fulfil job requirement # TODO, duan, 2021-10-23 22:13:35. I should loop through all depots in future. depot_key = list(env.depots_dict.keys())[0] inventory_dict = env.kp_data_adapter.get_depot_item_inventory_dict( depot_id=env.depots_dict[depot_key]["id"], requested_items=list(total_requested_items.keys())) # loaded_items_key_set = set(total_loaded_items.keys()) for item_key in job.requested_items: item_qty = total_loaded_items[item_key] # if item_key in loaded_items_key_set: # item_qty = total_loaded_items[item_key] if job.requested_items[item_key] > item_qty: if job.requested_items[item_key] > inventory_dict[item_key]: # This items is also NOT available in depot/warehouse overall_message += ", requested {} > {}; but only {} found on workers {}, and {} found in depot!".format( item_key, job.requested_items[item_key], item_qty, job.scheduled_worker_codes, inventory_dict[item_key]) score = -1 metrics_detail = {"item": item_key, "status_code": "ERROR"} break else: # This items IS available in depot/warehouse. # Then I will add a virtual replenish job and move on. # The replenish job is only added to primary worker as all_slots[0] # replenish_job = env.mutate_create_replenish_job(slot = all_slots[0], total_loaded_items = total_loaded_items) # if replenish_job is None: # overall_message += ", requested {} > {}; only {} found on workers {}, and failed insert replenish job!".format( # item_key, # job.requested_items[item_key], # item_qty, # job.scheduled_worker_codes, # ) # score = -1 # metrics_detail = {"item": item_key, "status_code":"ERROR" } # break overall_message += ", requested {} > {}; only {} found on workers {}, and a replenishment job is needed!".format( item_key, job.requested_items[item_key], item_qty, job.scheduled_worker_codes, ) score = 0 metrics_detail = { "item": item_key, "status_code": "WARNING", # "slot_code":env.slot_server.get_time_slot_key(slot), "slot": slot, "total_requested_items": total_requested_items, "total_loaded_items": total_loaded_items, } # allows to move on # Move on to check next item. # break score_res = ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, ) return score_res
def evalute_normal_single_worker_n_job(self, env, job=None): # worker = None, # return score, violated_rules (negative values) # return self.weight * 1 # Now check if this new job can fit into existing score = 1 job_requested_items = job.requested_items requested_volume = sum([ job_requested_items[item_key] * env.items_dict[item_key]["volume"] for item_key in job_requested_items.keys() ]) requested_weight = sum([ job_requested_items[item_key] * env.items_dict[item_key]["weight"] for item_key in job_requested_items.keys() ]) overall_message = "Job ({}) produces waste of volume ({}), weight ({}). ".format( job.job_code, requested_volume, requested_weight) metrics_detail = {"status_code": "OK"} if (requested_volume < 0) or (requested_weight < 0): worker_code = job.scheduled_worker_codes[0] overlapped_slots = env.slot_server.get_overlapped_slots( worker_id=worker_code, start_minutes=job.scheduled_start_minutes, end_minutes=job.scheduled_start_minutes + job.scheduled_duration_minutes) if len(overlapped_slots) < 1: overall_message += " but no slot was found!" score = -1 metrics_detail = {} else: slot = overlapped_slots[0] spare_weight = slot.max_weight - sum([ slot.loaded_items[item_key] * env.items_dict[item_key]["weight"] for item_key in slot.loaded_items.keys() ]) spare_volume = slot.max_weight - sum([ slot.loaded_items[item_key] * env.items_dict[item_key]["volume"] for item_key in slot.loaded_items.keys() ]) if (spare_weight + requested_weight < 0) or (spare_volume + requested_volume < 0): overall_message += ", slot spare weight {} and spare volume {} are found!".format( spare_weight, spare_volume, ) score = -1 metrics_detail = {} else: overall_message = "Job ({}) produces no waste.".format( job.job_code) score_res = ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, ) return score_res
def evalute_normal_single_worker_n_job(self, env=None, job=None): # worker = None, overall_message = "" score = 1 metrics_detail = {"status_code": "OK"} for worker_code in job.scheduled_worker_codes: worker = env.workers_dict[worker_code] day_seq = int(job.scheduled_start_minutes / 1440) weekday_i = env.env_encode_day_seq_to_weekday(day_seq) slot_in_day_count = len(worker.weekly_working_slots[weekday_i]) slot_intersected=False slot_covered = False for slot_in_day_i, slot_in_day in enumerate(worker.weekly_working_slots[weekday_i]): working_slot_in_the_day = [ slot_in_day[0] + (24 * 60 * day_seq), slot_in_day[1] + (24 * 60 * day_seq), ] clipped_slot = date_util.clip_time_period( p1=working_slot_in_the_day, p2=[ job.scheduled_start_minutes, job.scheduled_start_minutes + job.scheduled_duration_minutes, ], ) if len(clipped_slot) > 1: slot_intersected = True if (clipped_slot[0] == job.scheduled_start_minutes) & ( clipped_slot[1] == job.scheduled_start_minutes + job.scheduled_duration_minutes ): overall_message = self.success_message_template.format( date_util.minutes_to_time_string(job.scheduled_start_minutes), date_util.minutes_to_time_string( job.scheduled_start_minutes + job.scheduled_duration_minutes ), job.scheduled_start_minutes, job.scheduled_start_minutes + job.scheduled_duration_minutes, ) # move on to next worker slot_covered = True break if slot_covered: continue available_overtime = env.get_worker_available_overtime_minutes( worker_code=worker_code, day_seq=day_seq ) # if self.config["allow_overtime"]: if slot_intersected & (available_overtime > 0): start_with_overtime = working_slot_in_the_day[0] if slot_in_day_i ==0: start_with_overtime -= available_overtime end_with_overtime = working_slot_in_the_day[1] if slot_in_day_i == slot_in_day_count - 1: end_with_overtime += available_overtime if ( ( start_with_overtime < job.scheduled_start_minutes ) & ( end_with_overtime > job.scheduled_start_minutes + job.scheduled_duration_minutes ) & ( working_slot_in_the_day[1] - working_slot_in_the_day[0] + available_overtime > job.scheduled_duration_minutes ) ): score = 0 overall_message = self.overtime_allowed_message_template.format( date_util.minutes_to_time_string(job.scheduled_start_minutes), date_util.minutes_to_time_string( job.scheduled_start_minutes + job.scheduled_duration_minutes ), job.scheduled_start_minutes, job.scheduled_start_minutes + job.scheduled_duration_minutes, available_overtime, ) # move on to next worker print(overall_message) continue # If the time slot were ok (i.e. included in any slot), it should have been skipped by continue # If the start time does not fully fall in one working slot for any worker, reject it instantly. score = -1 overall_message = self.message_template.format( date_util.minutes_to_time_string(job.scheduled_start_minutes), date_util.minutes_to_time_string( job.scheduled_start_minutes + job.scheduled_duration_minutes ), job.scheduled_start_minutes, job.scheduled_start_minutes + job.scheduled_duration_minutes, available_overtime, ) return ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail={"status_code": "ERROR"}, ) return ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, )
def evalute_normal_single_worker_n_job(self, env=None, job=None): # worker = None, # action_dict = env.decode_action_into_dict(action) # if (action_dict['scheduled_start_minutes'] > 14*60 ) | (action_dict['scheduled_start_minutes'] + action_dict['scheduled_duration_minutes']< 12*60 ): # scheduled_start_minutes_local = job.scheduled_start_minutes % (24 * 60) score = 1 metrics_detail = {"status_code": "OK"} action_day = int(job.scheduled_start_minutes / 24 / 60) job_lunch_start = action_day * 24 * 60 + (self.config["lunch_start_hour"] * 60) job_lunch_end = action_day * 24 * 60 + (self.config["lunch_end_hour"] * 60) overlap_lunch = date_util.clip_time_period( [job_lunch_start, job_lunch_end], [ job.scheduled_start_minutes, job.scheduled_start_minutes + job.scheduled_duration_minutes, ], ) if len(overlap_lunch) < 1: overall_message = "The Job is not at lunch time" score_res = ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, ) return score_res overall_message = f"Lunch break > {self.config['lunch_break_minutes']} minutes" # scheduled_start_minutes = job.scheduled_start_minutes job_start_minutes = job.scheduled_start_minutes job_end_minutes = job.scheduled_start_minutes + job.scheduled_duration_minutes # for job_i in range(len(env.workers_dict[worker_code]["assigned_jobs"])): # for worker_id in [job.scheduled_worker_codes[0]] + job["scheduled_secondary_worker_ids"]: for worker_id in job.scheduled_worker_codes: total_avail_lunch_break = job_lunch_end - job_lunch_start overlapped_slots = env.slot_server.get_overlapped_slots( worker_id=worker_id, start_minutes=job_start_minutes, end_minutes=job_end_minutes ) # all_jobs = reduce(lambda x, y: x.assigned_job_codes + y.assigned_job_codes, overlapped_slots) for a_slot in overlapped_slots: ( prev_travel, next_travel, inside_travel, ) = env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes) all_prev_travel_minutes = [prev_travel] + inside_travel # + inside_travel if a_slot.slot_type == TimeSlotType.JOB_FIXED: total_avail_lunch_break -= ( prev_travel + # env.jobs_dict[job.job_code].scheduled_duration_minutes job.scheduled_duration_minutes ) elif a_slot.slot_type == TimeSlotType.FLOATING: for job_seq, job_code in enumerate(a_slot.assigned_job_codes): a_job = env.jobs_dict[job_code] a_job_period = [ a_job.scheduled_start_minutes - all_prev_travel_minutes[job_seq], a_job.scheduled_start_minutes + a_job.scheduled_duration_minutes, ] a_job_period_lunch_overlap = date_util.clip_time_period( [job_lunch_start, job_lunch_end], a_job_period ) if len(a_job_period_lunch_overlap) > 1: total_avail_lunch_break -= ( a_job_period_lunch_overlap[1] - a_job_period_lunch_overlap[0] ) else: # raise LookupError("unknown slot type - E?") pass # lunch break in dairy events/absence if total_avail_lunch_break < self.config["lunch_break_minutes"]: # For now, lunch break does not enforce to -1, lowest is 0, as warning score = 0 overall_message = f"total_avail_lunch_break = {total_avail_lunch_break}, which is less than MINIMUM({self.config['lunch_break_minutes']}) for worker {worker_id}" score_res = ActionEvaluationScore( score=score, score_type=self.title, message=overall_message, metrics_detail=metrics_detail, ) return score_res
def search_action_dict_on_workers( self, a_worker_code_list: List[str], curr_job: BaseJob, max_number_of_matching: int = 9, attempt_unplan_jobs=False, allow_overtime=False, ) -> List[RecommendedAction]: # if "job_gps" not in curr_job.keys(): # curr_job["job_gps"] = ([curr_job.location[0], curr_job.location[1]],) if curr_job.job_code in APPOINTMENT_DEBUG_LIST: log.info( f"appt={curr_job.job_code}, worker_codes:{a_worker_code_list} started searching...") if (curr_job.requested_duration_minutes is None) or (curr_job.requested_duration_minutes < 1): log.error( f"appt={curr_job.job_code}, requested_duration_minutes = {curr_job.requested_duration_minutes}, no recommendation is possible, quitting...") return [] scheduled_duration_minutes = (self.env.get_encode_shared_duration_by_planning_efficiency_factor( requested_duration_minutes=curr_job.requested_duration_minutes, nbr_workers=len(a_worker_code_list), )) # if len(a_worker_code_list) > 1: # else: # scheduled_duration_minutes = curr_job.requested_duration_minutes curr_job.scheduled_duration_minutes = scheduled_duration_minutes # self.env.jobs_dict[ # curr_job.job_code # ].scheduled_duration_minutes = scheduled_duration_minutes result_slot = [] time_slot_list = [] original_travel_minutes_difference = 0 if curr_job.planning_status != JobPlanningStatus.UNPLANNED: for worker_code in curr_job.scheduled_worker_codes: a_slot_group = self.env.slot_server.get_overlapped_slots( worker_id=worker_code, start_minutes=curr_job.scheduled_start_minutes, end_minutes=curr_job.scheduled_start_minutes + curr_job.scheduled_duration_minutes, ) for a_slot in a_slot_group: try: original_travel_minutes_difference += (self.calc_travel_minutes_difference_for_1_job( env=self.env, a_slot=a_slot, curr_job=curr_job)) except ValueError as err: log.debug( f"JOB:{curr_job.job_code}:ERROR:{err}, This job has lost the worker time slot, For now, no more recomemndations. and then I set original_travel_minutes_difference = 0") original_travel_minutes_difference = 0 # For now, no more recomemndations , TODO @duan # return [] for curr_worker_code in a_worker_code_list: if curr_worker_code not in self.env.workers_dict.keys(): log.error( f"WORKER:{curr_worker_code}: Worker is requested to be searched but not found in env.") return [] log.debug( f"Querying slots, worker_id={curr_worker_code}, start_minutes={curr_job.requested_start_min_minutes}, end_minutes={curr_job.requested_start_max_minutes}") curr_overlapped_slot_list = self.env.slot_server.get_overlapped_slots( worker_id=curr_worker_code, start_minutes=curr_job.requested_start_min_minutes, end_minutes=curr_job.requested_start_max_minutes, ) curr_free_slot_list = [] for slot in curr_overlapped_slot_list: log.debug(f"Checking slot: {self.env.slot_server.get_time_slot_key(slot)}") if slot.slot_type == TimeSlotType.FLOATING: if allow_overtime: # If attempt_unplan_jobs is set true, this is already secondary round. # I will also consider the overtime minutes for each worker # new_overtime_minutes = self.env.get_worker_available_overtime_minutes( slot.worker_id, day_seq=int(slot.start_minutes / 1440)) if slot.prev_slot_code is None: slot.start_overtime_minutes += new_overtime_minutes if slot.next_slot_code is None: slot.end_overtime_minutes += new_overtime_minutes if (slot.end_minutes - slot.start_minutes + slot.start_overtime_minutes + slot.end_overtime_minutes) > scheduled_duration_minutes: # This may screen out some apparently no fitting slots, without considering travel time. curr_free_slot_list.append(slot) log.debug( f"worker_codes:{a_worker_code_list}:worker:{curr_worker_code}: identified one free slot ({slot.start_minutes}->{slot.end_minutes}).") if len(curr_free_slot_list) < 1: log.debug( f"appt={curr_job.job_code}, worker_codes:{a_worker_code_list}:worker:{curr_worker_code}: Worker has no free slots left by tolerance ({curr_job.requested_start_min_minutes}->{curr_job.requested_start_max_minutes}) ({self.env.env_decode_from_minutes_to_datetime(curr_job.requested_start_min_minutes)}->{self.env.env_decode_from_minutes_to_datetime(curr_job.requested_start_max_minutes)})." ) return [] # if curr_job.job_code in APPOINTMENT_DEBUG_LIST: # log.debug( # "All free slots identified: " # + str( # [ # f"worker_id = {w.worker_id}, start_minutes= {w.start_minutes}, end_minutes= {w.end_minutes}, assigned_job_codes= {w.assigned_job_codes}" # for w in curr_free_slot_list # ] # ) # ) time_slot_list.append(sorted( curr_free_slot_list, key=lambda item: item.start_minutes, )) if len(time_slot_list) < 1: log.warn("should not happend: len(time_slot_list) < 1") return [] # This appends the customer avavailable slots to be the last timeslot list to screen against all work's timeslots. # I assume that customer does not like current time slot and remove it from available slot, # TODO, @duan, 2020-12-17 19:23:53 Why available_slots is on locaiton? different jobs may differ. orig_avail = copy.deepcopy(curr_job.available_slots) if len(orig_avail) < 1: log.info( f"appt={curr_job.job_code}, job_available_slots:{orig_avail}: No available slots per customer availability.") return [] else: # if curr_job.job_code in APPOINTMENT_DEBUG_LIST: log.debug( f"appt={curr_job.job_code}, len(orig_avail) ={len(orig_avail)}: Found available slots per customer availability.") # This fake_appt_time_slot will used to host the slot intersection result, especially after customer availability fake_appt_time_slot = copy.deepcopy(orig_avail[0]) fake_appt_time_slot.assigned_job_codes = [curr_job.job_code] # This is to exclude those available slots that overlap with current scheduling time. # Make sure that customer do not get same time slots (meaningless, from other technicians) as current one. net_cust_avail_slots = [] slot_to_exclude = ( curr_job.scheduled_start_minutes - 60, curr_job.scheduled_start_minutes + 90, ) for ti in range(len(orig_avail)): slot_ti = (orig_avail[ti].start_minutes, orig_avail[ti].end_minutes) clip = date_util.clip_time_period(slot_ti, slot_to_exclude) if len(clip) < 1: net_cust_avail_slots.append(orig_avail[ti]) else: if clip[0] > orig_avail[ti].start_minutes: a_slot = copy.copy(orig_avail[ti]) a_slot.end_minutes = clip[0] net_cust_avail_slots.append(a_slot) if clip[1] < orig_avail[ti].end_minutes: a_slot = copy.copy(orig_avail[ti]) a_slot.start_minutes = clip[1] net_cust_avail_slots.append(a_slot) # To include the customer available time slots in the search time_slot_list.append(net_cust_avail_slots) # = {net_cust_avail_slots} log.debug(f"Final net_cust_avail_slots len={len(net_cust_avail_slots)}") available_slot_groups = self.env.intersect_geo_time_slots( time_slot_list, curr_job, duration_minutes=scheduled_duration_minutes, max_number_of_matching=max_number_of_matching, ) # if ( free_slot.start_minutes <= start_minutes - travel_minutes ) & ( end_minutes <= free_slot[1]): # if len(available_slot_groups) < 1: # no hit in this day_i # continue log.debug( f"after env.intersect_geo_time_slots, available_slot_groups len={len(available_slot_groups)}.") for avail_slot_group in available_slot_groups: scoped_slot_list = avail_slot_group[2] shared_time_slots_temp = [] for sc_slot in scoped_slot_list[:-1]: try: # TODO, merge this with first filtering. with overtime minutes processing 2021-01-01 10:18:06 sc = self.env.slot_server.get_time_slot_key(sc_slot) temp_slot_ = self.env.slot_server.get_slot( redis_handler=self.env.slot_server.r, slot_code=sc) temp_slot_.start_overtime_minutes = sc_slot.start_overtime_minutes temp_slot_.end_overtime_minutes = sc_slot.end_overtime_minutes shared_time_slots_temp.append(temp_slot_) except Exception as mse: log.error(f"failed to read slot {str(sc)}, error {str(mse)}") if len(shared_time_slots_temp) < 1: # After checking redis, there is no valid slot in the list log.info( f"appt={curr_job.job_code}, After checking redis, there is no valid slot in the list, No available slots.") continue shared_time_slots_temp.append(scoped_slot_list[-1]) shared_time_slots = shared_time_slots_temp.copy() # shared_time_slots_optimized may be changed/mutated during optimization process. Deep copy it. shared_time_slots_optimized = copy.deepcopy(shared_time_slots) for one_working_slot in shared_time_slots_optimized: one_working_slot.assigned_job_codes = sorted( one_working_slot.assigned_job_codes, key=lambda x: self.env.jobs_dict[x].scheduled_start_minutes ) one_working_slot.assigned_job_codes.append(curr_job.job_code) # TODO, then append other related shared code slots... Right now only this one. # This is the last attached fake time slot according to availability search # TODO: WHy two availability? fake_appt_time_slot.start_minutes = avail_slot_group[0] fake_appt_time_slot.end_minutes = avail_slot_group[1] shared_time_slots_optimized.append(fake_appt_time_slot) if self.use_naive_search_for_speed: dispatch_jobs_in_slots_func = self.env.inner_slot_heur.dispatch_jobs_in_slots else: dispatch_jobs_in_slots_func = self.env.inner_slot_opti.dispatch_jobs_in_slots unplanned_job_codes = [] # res = {"status":OptimizerSolutionStatus.INFEASIBLE} res = dispatch_jobs_in_slots_func(shared_time_slots_optimized) while attempt_unplan_jobs & (res.status != OptimizerSolutionStatus.SUCCESS): is_shrinked = False for ts in shared_time_slots_optimized: if len(ts.assigned_job_codes) > 1: j_i = len(ts.assigned_job_codes) - 2 while j_i >= 0: job_code_to_unplan = ts.assigned_job_codes[j_i] if self.env.jobs_dict[job_code_to_unplan].priority < curr_job.priority: unplanned_job_codes.append(job_code_to_unplan) ts.assigned_job_codes = ts.assigned_job_codes[0:j_i] + \ ts.assigned_job_codes[j_i + 1:] is_shrinked = True log.info( f"job={curr_job.job_code}, attempting search after unplanning job {job_code_to_unplan}, to {ts.assigned_job_codes}, priority { self.env.jobs_dict[job_code_to_unplan].priority , curr_job.priority} ...") break j_i -= 1 if is_shrinked: # only unplan one job in each attempt break if not is_shrinked: # No job can be unplanned. break res = dispatch_jobs_in_slots_func(shared_time_slots_optimized) if res.status == OptimizerSolutionStatus.SUCCESS: break if res.status != OptimizerSolutionStatus.SUCCESS: log.info(f"appt={curr_job.job_code}, failed to find solution on slots: " + str( [f"(worker_id = {w.worker_id}, start_datetime= {self.env.env_decode_from_minutes_to_datetime(w.start_minutes)}, end_datetime= {self.env.env_decode_from_minutes_to_datetime(w.end_minutes)}, assigned_job_codes= {w.assigned_job_codes}) " for w in shared_time_slots_optimized])) continue try: new_start_mintues = res.changed_action_dict_by_job_code[curr_job.job_code].scheduled_start_minutes except KeyError: log.error( f"{curr_job.job_code} is not in changed_action_dict_by_job_code. I should have already excluded it from search.") continue new_action_dict = ActionDict( is_forced_action=False, job_code=curr_job.job_code, action_type=ActionType.JOB_FIXED, scheduled_worker_codes=a_worker_code_list, scheduled_start_minutes=new_start_mintues, scheduled_duration_minutes=curr_job.scheduled_duration_minutes, ) rule_check_result = self.env._check_action_on_rule_set( a_dict=new_action_dict, unplanned_job_codes=unplanned_job_codes) # TODO, track Failed number if rule_check_result.status_code == ActionScoringResultType.ERROR: log.info( f"Failed on rule set check after acquiring recommendation. Skipped. appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, messages = {['{}---{}---{}'.format(m.score,m.score_type, m.message) for m in rule_check_result.messages]} " ) continue # This may not be necessary , because I have executed optimize_slot and the slot contains only possible jobs # # cut_off_success_flag, message_dict = self.env.slot_server.cut_off_time_slots( # action_dict=new_action_dict, probe_only=True # ) # if not cut_off_success_flag: # log.info( # f"After rule set, failed on probing cutting off slots. appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, messages = {str(message_dict)} " # ) # continue log.info( f"appt = {curr_job.job_code}, workers = {a_worker_code_list}, start = {new_start_mintues}, The solution passed all rules.") # Now I should trim the last time slot attached as customer availability scoped_slot_code_list_no_cust = [self.env.slot_server.get_time_slot_key( s) for s in scoped_slot_list[0:len(a_worker_code_list)]] # Here i should also assert that len(scoped_slot_code_list) == worker_lenth + 1 new_travel_minutes_difference = 0 for a_slot in shared_time_slots: ( prev_travel, next_travel, inside_travel, ) = self.env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes) new_travel_minutes_difference -= prev_travel + next_travel + sum(inside_travel) for a_slot in shared_time_slots_optimized: ( prev_travel, next_travel, inside_travel, ) = self.env.get_travel_time_jobs_in_slot(a_slot, a_slot.assigned_job_codes) new_travel_minutes_difference += prev_travel + next_travel + sum(inside_travel) total_score = ((self.env.config["scoring_factor_standard_travel_minutes"] - new_travel_minutes_difference + original_travel_minutes_difference) / self.env.config["scoring_factor_standard_travel_minutes"]) - (len(unplanned_job_codes) * 0.5) if allow_overtime: total_score -= 2 travel_message = f"original minutes difference:{original_travel_minutes_difference}, new minutes difference: {new_travel_minutes_difference}" metrics_detail = { "original_minutes_difference": original_travel_minutes_difference, "new_minutes_difference": new_travel_minutes_difference, } travel_score_obj = ActionEvaluationScore( score=total_score, score_type="travel_difference", message=travel_message, metrics_detail=metrics_detail, ) # if curr_job.job_code == "04856415-b6ae-4aeb-9e6d-ff399f00de0d": # log.debug("04856415-b6ae-4aeb-9e6d-ff399f00de0d") a_rec = RecommendedAction( job_code=curr_job.job_code, # action_type=ActionType.JOB_FIXED, # JobType = JOB, which can be acquired from self.jobs_dict[job_code] scheduled_worker_codes=a_worker_code_list, scheduled_start_minutes=new_start_mintues, scheduled_duration_minutes=curr_job.scheduled_duration_minutes, score=total_score, score_detail=[travel_score_obj], scoped_slot_code_list=scoped_slot_code_list_no_cust, job_plan_in_scoped_slots=res.planned_job_sequence[0:len(a_worker_code_list)], unplanned_job_codes=unplanned_job_codes, ) log.info(f"Found solution on slots {a_rec.scoped_slot_code_list}") # TODO for @Xingtong, calculate KPI about recommendation result_slot.append(a_rec) if len(result_slot) >= max_number_of_matching: return result_slot # log.info(f"Partial solutions returned") return result_slot