From 1a8063e4fd0c1c3ee113010af196db4d1e218c1c Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:16:35 +0800 Subject: [PATCH] fix performance calculation. fix floor error in approaching event. fix passenger board wrongly. --- docs/client.rst | 2 +- docs/events.rst | 39 ++++--- docs/models.rst | 8 +- elevator_saga/client/api_client.py | 18 +-- elevator_saga/client/base_controller.py | 8 +- elevator_saga/client_examples/bus_example.py | 5 +- elevator_saga/core/models.py | 16 +-- elevator_saga/server/simulator.py | 117 +++++++++---------- 8 files changed, 102 insertions(+), 111 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 54a8138..16b2d6d 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -176,7 +176,7 @@ Dynamic proxy for ``PassengerInfo`` that provides access to passenger informatio if passenger.status == PassengerStatus.IN_ELEVATOR: print(f"In elevator {passenger.elevator_id}") - print(f"Waited {passenger.wait_time} ticks") + print(f"Waited {passenger.floor_wait_time} ticks") Read-Only Protection ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/events.rst b/docs/events.rst index 0b10507..59c3673 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -277,7 +277,7 @@ Events are generated during tick processing: EventType.ELEVATOR_APPROACHING, { "elevator": elevator.id, - "floor": elevator.target_floor, + "floor": int(round(elevator.position.current_floor_float)), "direction": elevator.target_floor_direction.value } ) @@ -487,28 +487,39 @@ Metrics are calculated from passenger data: .. code-block:: python - def _calculate_metrics(self) -> MetricsResponse: + def _calculate_metrics(self) -> PerformanceMetrics: """Calculate performance metrics""" completed = [p for p in self.state.passengers.values() if p.status == PassengerStatus.COMPLETED] - wait_times = [float(p.wait_time) for p in completed] - system_times = [float(p.system_time) for p in completed] + floor_wait_times = [float(p.floor_wait_time) for p in completed] + arrival_wait_times = [float(p.arrival_wait_time) for p in completed] - return MetricsResponse( - done=len(completed), - total=len(self.state.passengers), - avg_wait=sum(wait_times) / len(wait_times) if wait_times else 0, - p95_wait=percentile(wait_times, 95), - avg_system=sum(system_times) / len(system_times) if system_times else 0, - p95_system=percentile(system_times, 95), + def average_excluding_top_percent(data: List[float], exclude_percent: int) -> float: + """计算排除掉最长的指定百分比后的平均值""" + if not data: + return 0.0 + sorted_data = sorted(data) + keep_count = int(len(sorted_data) * (100 - exclude_percent) / 100) + if keep_count == 0: + return 0.0 + kept_data = sorted_data[:keep_count] + return sum(kept_data) / len(kept_data) + + return PerformanceMetrics( + completed_passengers=len(completed), + total_passengers=len(self.state.passengers), + average_floor_wait_time=sum(floor_wait_times) / len(floor_wait_times) if floor_wait_times else 0, + p95_floor_wait_time=average_excluding_top_percent(floor_wait_times, 5), + average_arrival_wait_time=sum(arrival_wait_times) / len(arrival_wait_times) if arrival_wait_times else 0, + p95_arrival_wait_time=average_excluding_top_percent(arrival_wait_times, 5), ) Key metrics: -- **Wait time**: ``pickup_tick - arrive_tick`` (how long passenger waited) -- **System time**: ``dropoff_tick - arrive_tick`` (total time in system) -- **P95**: 95th percentile (worst-case for most passengers) +- **Floor wait time**: ``pickup_tick - arrive_tick`` (在楼层等待的时间,从到达到上电梯) +- **Arrival wait time**: ``dropoff_tick - arrive_tick`` (总等待时间,从到达到下电梯) +- **P95 metrics**: 排除掉最长的5%时间后,计算剩余95%的平均值 Summary ------- diff --git a/docs/models.rst b/docs/models.rst index d249997..33eec2d 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -257,10 +257,10 @@ Tracks simulation performance: class PerformanceMetrics(SerializableModel): completed_passengers: int = 0 total_passengers: int = 0 - average_wait_time: float = 0.0 - p95_wait_time: float = 0.0 # 95th percentile - average_system_time: float = 0.0 - p95_system_time: float = 0.0 # 95th percentile + average_floor_wait_time: float = 0.0 + p95_floor_wait_time: float = 0.0 # 95th percentile + average_arrival_wait_time: float = 0.0 + p95_arrival_wait_time: float = 0.0 # 95th percentile Properties: diff --git a/elevator_saga/client/api_client.py b/elevator_saga/client/api_client.py index 92bcd5a..d75a4ea 100644 --- a/elevator_saga/client/api_client.py +++ b/elevator_saga/client/api_client.py @@ -6,7 +6,7 @@ Unified API Client for Elevator Saga import json import urllib.error import urllib.request -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from elevator_saga.core.models import ( ElevatorState, @@ -63,16 +63,8 @@ class ElevatorAPIClient: # 使用服务端返回的metrics数据 metrics_data = response_data.get("metrics", {}) if metrics_data: - # 转换为PerformanceMetrics格式 - metrics = PerformanceMetrics( - completed_passengers=metrics_data.get("done", 0), - total_passengers=metrics_data.get("total", 0), - average_wait_time=metrics_data.get("avg_wait", 0), - p95_wait_time=metrics_data.get("p95_wait", 0), - average_system_time=metrics_data.get("avg_system", 0), - p95_system_time=metrics_data.get("p95_system", 0), - # total_energy_consumption=metrics_data.get("energy_total", 0), - ) + # 直接从字典创建PerformanceMetrics对象 + metrics = PerformanceMetrics.from_dict(metrics_data) else: metrics = PerformanceMetrics() @@ -131,7 +123,7 @@ class ElevatorAPIClient: else: raise RuntimeError(f"Step failed: {response_data.get('error')}") - def send_elevator_command(self, command: Union[GoToFloorCommand]) -> bool: + def send_elevator_command(self, command: GoToFloorCommand) -> bool: """发送电梯命令""" endpoint = self._get_elevator_endpoint(command) debug_log( @@ -156,7 +148,7 @@ class ElevatorAPIClient: debug_log(f"Go to floor failed: {e}") return False - def _get_elevator_endpoint(self, command: Union[GoToFloorCommand]) -> str: + def _get_elevator_endpoint(self, command: GoToFloorCommand) -> str: """获取电梯命令端点""" base = f"/api/elevators/{command.elevator_id}" diff --git a/elevator_saga/client/base_controller.py b/elevator_saga/client/base_controller.py index 4284468..60b5510 100644 --- a/elevator_saga/client/base_controller.py +++ b/elevator_saga/client/base_controller.py @@ -352,9 +352,7 @@ class ElevatorController(ABC): if elevator_id is not None and floor_id is not None and direction is not None: elevator_proxy = ProxyElevator(elevator_id, self.api_client) floor_proxy = ProxyFloor(floor_id, self.api_client) - # 服务端发送的direction是字符串,直接使用 - direction_str = direction if isinstance(direction, str) else direction.value - self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction_str) + self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction) elif event.type == EventType.ELEVATOR_APPROACHING: elevator_id = event.data.get("elevator") @@ -363,9 +361,7 @@ class ElevatorController(ABC): if elevator_id is not None and floor_id is not None and direction is not None: elevator_proxy = ProxyElevator(elevator_id, self.api_client) floor_proxy = ProxyFloor(floor_id, self.api_client) - # 服务端发送的direction是字符串,直接使用 - direction_str = direction if isinstance(direction, str) else direction.value - self.on_elevator_approaching(elevator_proxy, floor_proxy, direction_str) + self.on_elevator_approaching(elevator_proxy, floor_proxy, direction) elif event.type == EventType.PASSENGER_BOARD: elevator_id = event.data.get("elevator") diff --git a/elevator_saga/client_examples/bus_example.py b/elevator_saga/client_examples/bus_example.py index 72bf6b3..9978785 100644 --- a/elevator_saga/client_examples/bus_example.py +++ b/elevator_saga/client_examples/bus_example.py @@ -41,6 +41,7 @@ class ElevatorBusExampleController(ElevatorController): def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None: self.all_passengers.append(passenger) + print(f"乘客 {passenger.id} F{floor.floor} 请求 {passenger.origin} -> {passenger.destination} ({direction})") pass def on_elevator_idle(self, elevator: ProxyElevator) -> None: @@ -60,10 +61,10 @@ class ElevatorBusExampleController(ElevatorController): elevator.go_to_floor(elevator.current_floor - 1) def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None: - pass + print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}") def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None: - pass + print(f" 乘客{passenger.id} E{elevator.id}⬇️ F{floor.floor}") def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None: pass diff --git a/elevator_saga/core/models.py b/elevator_saga/core/models.py index 7ea3e0a..c9e2715 100644 --- a/elevator_saga/core/models.py +++ b/elevator_saga/core/models.py @@ -174,13 +174,13 @@ class PassengerInfo(SerializableModel): return PassengerStatus.WAITING @property - def wait_time(self) -> int: - """等待时间""" + def floor_wait_time(self) -> int: + """在楼层等待的时间(从到达到上电梯)""" return self.pickup_tick - self.arrive_tick @property - def system_time(self) -> int: - """系统时间(总时间)""" + def arrival_wait_time(self) -> int: + """总等待时间(从到达到下电梯)""" return self.dropoff_tick - self.arrive_tick @property @@ -331,10 +331,10 @@ class PerformanceMetrics(SerializableModel): completed_passengers: int = 0 total_passengers: int = 0 - average_wait_time: float = 0.0 - p95_wait_time: float = 0.0 - average_system_time: float = 0.0 - p95_system_time: float = 0.0 + average_floor_wait_time: float = 0.0 + p95_floor_wait_time: float = 0.0 + average_arrival_wait_time: float = 0.0 + p95_arrival_wait_time: float = 0.0 # total_energy_consumption: float = 0.0 @property diff --git a/elevator_saga/server/simulator.py b/elevator_saga/server/simulator.py index 62cf690..65ade97 100644 --- a/elevator_saga/server/simulator.py +++ b/elevator_saga/server/simulator.py @@ -22,6 +22,7 @@ from elevator_saga.core.models import ( FloorState, PassengerInfo, PassengerStatus, + PerformanceMetrics, SerializableModel, SimulationEvent, SimulationState, @@ -89,19 +90,6 @@ def json_response(data: Any, status: int = 200) -> Response | tuple[Response, in return response, status -@dataclass -class MetricsResponse(SerializableModel): - """性能指标响应""" - - done: int - total: int - avg_wait: float - p95_wait: float - avg_system: float - p95_system: float - energy_total: float - - @dataclass class PassengerSummary(SerializableModel): """乘客摘要""" @@ -120,7 +108,7 @@ class SimulationStateResponse(SerializableModel): elevators: List[ElevatorState] floors: List[FloorState] passengers: Dict[int, PassengerInfo] - metrics: MetricsResponse + metrics: PerformanceMetrics class ElevatorSimulation: @@ -292,33 +280,32 @@ class ElevatorSimulation: # Return events generated this tick return self.state.events[events_start:] - def _process_passenger_in(self) -> None: - for elevator in self.elevators: - current_floor = elevator.current_floor - # 处于Stopped状态,方向也已经清空,说明没有调度。 - floor = self.floors[current_floor] - passengers_to_board: List[int] = [] - available_capacity = elevator.max_capacity - len(elevator.passengers) - # Board passengers going up (if up indicator is on or no direction set) - if elevator.target_floor_direction == Direction.UP: - passengers_to_board.extend(floor.up_queue[:available_capacity]) - floor.up_queue = floor.up_queue[available_capacity:] + def _process_passenger_in(self, elevator: ElevatorState) -> None: + current_floor = elevator.current_floor + # 处于Stopped状态,方向也已经清空,说明没有调度。 + floor = self.floors[current_floor] + passengers_to_board: List[int] = [] + available_capacity = elevator.max_capacity - len(elevator.passengers) + # Board passengers going up (if up indicator is on or no direction set) + if elevator.target_floor_direction == Direction.UP: + passengers_to_board.extend(floor.up_queue[:available_capacity]) + floor.up_queue = floor.up_queue[available_capacity:] - # Board passengers going down (if down indicator is on or no direction set) - if elevator.target_floor_direction == Direction.DOWN: - passengers_to_board.extend(floor.down_queue[:available_capacity]) - floor.down_queue = floor.down_queue[available_capacity:] + # Board passengers going down (if down indicator is on or no direction set) + if elevator.target_floor_direction == Direction.DOWN: + passengers_to_board.extend(floor.down_queue[:available_capacity]) + floor.down_queue = floor.down_queue[available_capacity:] - # Process boarding - for passenger_id in passengers_to_board: - passenger = self.passengers[passenger_id] - passenger.pickup_tick = self.tick - passenger.elevator_id = elevator.id - elevator.passengers.append(passenger_id) - self._emit_event( - EventType.PASSENGER_BOARD, - {"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id}, - ) + # Process boarding + for passenger_id in passengers_to_board: + passenger = self.passengers[passenger_id] + passenger.pickup_tick = self.tick + passenger.elevator_id = elevator.id + elevator.passengers.append(passenger_id) + self._emit_event( + EventType.PASSENGER_BOARD, + {"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id}, + ) def _update_elevator_status(self) -> None: """更新电梯运行状态""" @@ -331,7 +318,7 @@ class ElevatorSimulation: if elevator.next_target_floor is not None: self._set_elevator_target_floor(elevator, elevator.next_target_floor) - self._process_passenger_in() + self._process_passenger_in(elevator) elevator.next_target_floor = None else: continue @@ -411,7 +398,7 @@ class ElevatorSimulation: EventType.ELEVATOR_APPROACHING, { "elevator": elevator.id, - "floor": elevator.target_floor, + "floor": int(round(elevator.position.current_floor_float)), "direction": elevator.target_floor_direction.value, }, ) @@ -547,41 +534,45 @@ class ElevatorSimulation: metrics=metrics, ) - def _calculate_metrics(self) -> MetricsResponse: + def _calculate_metrics(self) -> PerformanceMetrics: """Calculate performance metrics""" # 直接从state中筛选已完成的乘客 completed = [p for p in self.state.passengers.values() if p.status == PassengerStatus.COMPLETED] total_passengers = len(self.state.passengers) if not completed: - return MetricsResponse( - done=0, - total=total_passengers, - avg_wait=0, - p95_wait=0, - avg_system=0, - p95_system=0, - energy_total=sum(e.energy_consumed for e in self.elevators), + return PerformanceMetrics( + completed_passengers=0, + total_passengers=total_passengers, + average_floor_wait_time=0, + p95_floor_wait_time=0, + average_arrival_wait_time=0, + p95_arrival_wait_time=0, ) - wait_times = [float(p.wait_time) for p in completed] - system_times = [float(p.system_time) for p in completed] + floor_wait_times = [float(p.floor_wait_time) for p in completed] + arrival_wait_times = [float(p.arrival_wait_time) for p in completed] - def percentile(data: List[float], p: int) -> float: + def average_excluding_top_percent(data: List[float], exclude_percent: int) -> float: + """计算排除掉最长的指定百分比后的平均值""" if not data: return 0.0 sorted_data = sorted(data) - index = int(len(sorted_data) * p / 100) - return sorted_data[min(index, len(sorted_data) - 1)] + # 计算要保留的数据数量(排除掉最长的 exclude_percent) + keep_count = int(len(sorted_data) * (100 - exclude_percent) / 100) + if keep_count == 0: + return 0.0 + # 只保留前 keep_count 个数据,排除最长的部分 + kept_data = sorted_data[:keep_count] + return sum(kept_data) / len(kept_data) - return MetricsResponse( - done=len(completed), - total=total_passengers, - avg_wait=sum(wait_times) / len(wait_times) if wait_times else 0, - p95_wait=percentile(wait_times, 95), - avg_system=sum(system_times) / len(system_times) if system_times else 0, - p95_system=percentile(system_times, 95), - energy_total=sum(e.energy_consumed for e in self.elevators), + return PerformanceMetrics( + completed_passengers=len(completed), + total_passengers=total_passengers, + average_floor_wait_time=sum(floor_wait_times) / len(floor_wait_times) if floor_wait_times else 0, + p95_floor_wait_time=average_excluding_top_percent(floor_wait_times, 5), + average_arrival_wait_time=sum(arrival_wait_times) / len(arrival_wait_times) if arrival_wait_times else 0, + p95_arrival_wait_time=average_excluding_top_percent(arrival_wait_times, 5), ) def get_events(self, since_tick: int = 0) -> List[SimulationEvent]: