Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions RL/base_reach.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,25 @@ def can_reach_base(reach, flight, pairing_start_time, max_pairing_days,
if duty_period + c > max_duty_periods:
return False
return True

def build_base_reaches(flights, base_airports, constraint):
"""허용된 각 home base에 대한 reachability를 구성함."""
return {
base: build_base_reach(flights, base, constraint)
for base in dict.fromkeys(base_airports)
}


def can_reach_any_base(reaches, flight, pairing_start_time, max_pairing_days,
duty_period=None, max_duty_periods=None):
"""동일한 resource budget으로 허용 home base 중 하나에 복귀 가능한지 확인함."""
if not reaches:
return False
return any(
can_reach_base(
reach, flight, pairing_start_time, max_pairing_days,
duty_period=duty_period,
max_duty_periods=max_duty_periods,
)
for reach in reaches.values()
)
45 changes: 22 additions & 23 deletions RL/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,13 @@ def get_mask(state, flights, assigned, constraint=None, stage=3):
# base_remaining does not depend on the candidate flight f (loop-invariant),
# so it is computed once outside the loop rather than recomputed per
# candidate (which would be O(N^2)).
base_ap = c.get("base_airport", config.DEFAULT_CONSTRAINTS["base_airport"])

# Hard base-return enforcement (decode-time feasibility masking, Eq. 6).
# require_base_return=True (1) masks any leg that would make base return
# infeasible and (2) forbids EndPairing away from the base -- so every
# completed pairing is structurally guaranteed to start and end at the
# base, matching the backward-reachability-from-base mechanism described
# in the paper. _base_reach is precomputed once per (flights, base) by
# the caller in rollout.py and passed through the constraint dict.
require_return = c.get("require_base_return", False)
base_reach = c.get("_base_reach") if require_return else None
base_ap = c["base_airport"]

# CPP pairing이 base 복귀 가능성을 잃는 action을 항상 제거함.
base_reach = c.get("_base_reach")
if base_reach is None:
# CPP 실행에는 base 복귀 가능성 자료가 필수이며 누락은 구성 오류로 처리함.
raise ValueError("CPP constraint에는 _base_reach가 필요합니다.")
max_pd = c.get("max_pairing_days", config.DEFAULT_CONSTRAINTS["max_pairing_days"])
max_duty_periods = c.get("max_duty_periods", config.DEFAULT_CONSTRAINTS["max_duty_periods"])

Expand All @@ -81,7 +77,7 @@ def get_mask(state, flights, assigned, constraint=None, stage=3):

# 1. Airport-continuity check
if pairing_start:
if base_remaining and f["origin"] != base_ap:
if f["origin"] != base_ap:
valid = False
elif f["origin"] != state["current_airport"]:
valid = False
Expand Down Expand Up @@ -116,12 +112,12 @@ def get_mask(state, flights, assigned, constraint=None, stage=3):
if elapsed_days > max_pd:
valid = False

# 5. Base-reachability (only when require_base_return is set)
# 5. Base 복귀 가능성
# Checked via duty_period/max_duty_periods, not max_legs -- EndDuty can
# always grant a fresh leg budget, so per-duty leg count is not the
# binding resource for reaching the base; remaining overnight/rest
# opportunities are.
if valid and base_reach is not None:
if valid:
ps_time = f["dep_time"] if pairing_start else pairing_start_time
if not can_reach_base(
base_reach, f, ps_time, max_pd,
Expand Down Expand Up @@ -151,10 +147,8 @@ def get_mask(state, flights, assigned, constraint=None, stage=3):
state.get("total_legs", 0) >= min_pairing_legs
and pairing_elapsed_days <= c.get("max_pairing_days", config.DEFAULT_CONSTRAINTS["max_pairing_days"])
)
# Failing to return to base is otherwise a soft penalty applied as reward
# in step(), not a hard mask. When require_base_return is set, EndPairing
# away from the base is masked out (hard constraint).
if require_return and state["current_airport"] != base_ap:
# CPP pairing은 base에 도착한 상태에서만 종료 가능함.
if state["current_airport"] != base_ap:
can_end_pairing = False
if can_end_pairing:
mask[config.END_PAIRING] = 1
Expand All @@ -179,9 +173,13 @@ def step(state, action, flights, assigned, constraint=None):
"""
c = constraint if constraint else config.DEFAULT_CONSTRAINTS
N = len(flights)
if action < 0 or action >= N + 2:
raise IndexError("action이 허용 범위를 벗어났습니다.")

# EndDuty -> enter rest, pairing continues
if action == N:
if not get_mask(state, flights, assigned, c)[config.END_DUTY]:
raise ValueError("현재 상태에서는 EndDuty를 선택할 수 없습니다.")
min_rest = c.get("min_rest", config.DEFAULT_CONSTRAINTS["min_rest"])
next_state = {
**state,
Expand All @@ -205,18 +203,17 @@ def step(state, action, flights, assigned, constraint=None):

# EndPairing -> charge pairing cost, then start a new pairing (or end the episode)
if action == N + 1:
if not get_mask(state, flights, assigned, c)[config.END_PAIRING]:
raise ValueError("CPP 제약을 만족하지 않은 pairing은 종료할 수 없습니다.")
p_cost = c.get("pairing_cost", config.DEFAULT_CONSTRAINTS["pairing_cost"])
base_penalty = c.get("base_penalty", config.DEFAULT_CONSTRAINTS["base_penalty"])
# constraint["base_airport"] is injected per episode
base = c.get("base_airport", config.DEFAULT_CONSTRAINTS["base_airport"])
base = c["base_airport"]

total_legs = state.get("total_legs", 0)
reward = -p_cost + total_legs * config.LEG_PER_PAIRING_BONUS

if total_legs < config.MIN_LEGS_FOR_PAIRING:
reward += config.MIN_LEGS_PENALTY
if state["current_airport"] != base:
reward -= base_penalty

unassigned = [f for f in flights if not assigned[f["id"]]]
if not unassigned:
Expand All @@ -242,7 +239,9 @@ def step(state, action, flights, assigned, constraint=None):
}
return next_state, reward, False

# Select a flight leg
# flight action도 직접 호출 시 hard mask legality를 다시 확인함.
if not get_mask(state, flights, assigned, c)[action]:
raise ValueError("CPP 제약을 위반한 flight는 선택할 수 없습니다.")
f = flights[action]
assigned[f["id"]] = True
flight_time = f["arr_time"] - f["dep_time"]
Expand Down
Loading