fix performance calculation. fix floor error in approaching event. fix passenger board wrongly.

This commit is contained in:
Xuwznln
2025-10-06 15:16:35 +08:00
parent 692b853101
commit 1a8063e4fd
8 changed files with 102 additions and 111 deletions

View File

@@ -176,7 +176,7 @@ Dynamic proxy for ``PassengerInfo`` that provides access to passenger informatio
if passenger.status == PassengerStatus.IN_ELEVATOR: if passenger.status == PassengerStatus.IN_ELEVATOR:
print(f"In elevator {passenger.elevator_id}") 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 Read-Only Protection
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~

View File

@@ -277,7 +277,7 @@ Events are generated during tick processing:
EventType.ELEVATOR_APPROACHING, EventType.ELEVATOR_APPROACHING,
{ {
"elevator": elevator.id, "elevator": elevator.id,
"floor": elevator.target_floor, "floor": int(round(elevator.position.current_floor_float)),
"direction": elevator.target_floor_direction.value "direction": elevator.target_floor_direction.value
} }
) )
@@ -487,28 +487,39 @@ Metrics are calculated from passenger data:
.. code-block:: python .. code-block:: python
def _calculate_metrics(self) -> MetricsResponse: def _calculate_metrics(self) -> PerformanceMetrics:
"""Calculate performance metrics""" """Calculate performance metrics"""
completed = [p for p in self.state.passengers.values() completed = [p for p in self.state.passengers.values()
if p.status == PassengerStatus.COMPLETED] if p.status == PassengerStatus.COMPLETED]
wait_times = [float(p.wait_time) for p in completed] floor_wait_times = [float(p.floor_wait_time) for p in completed]
system_times = [float(p.system_time) for p in completed] arrival_wait_times = [float(p.arrival_wait_time) for p in completed]
return MetricsResponse( def average_excluding_top_percent(data: List[float], exclude_percent: int) -> float:
done=len(completed), """计算排除掉最长的指定百分比后的平均值"""
total=len(self.state.passengers), if not data:
avg_wait=sum(wait_times) / len(wait_times) if wait_times else 0, return 0.0
p95_wait=percentile(wait_times, 95), sorted_data = sorted(data)
avg_system=sum(system_times) / len(system_times) if system_times else 0, keep_count = int(len(sorted_data) * (100 - exclude_percent) / 100)
p95_system=percentile(system_times, 95), 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: Key metrics:
- **Wait time**: ``pickup_tick - arrive_tick`` (how long passenger waited) - **Floor wait time**: ``pickup_tick - arrive_tick`` (在楼层等待的时间,从到达到上电梯)
- **System time**: ``dropoff_tick - arrive_tick`` (total time in system) - **Arrival wait time**: ``dropoff_tick - arrive_tick`` (总等待时间,从到达到下电梯)
- **P95**: 95th percentile (worst-case for most passengers) - **P95 metrics**: 排除掉最长的5%时间后计算剩余95%的平均值
Summary Summary
------- -------

View File

@@ -257,10 +257,10 @@ Tracks simulation performance:
class PerformanceMetrics(SerializableModel): class PerformanceMetrics(SerializableModel):
completed_passengers: int = 0 completed_passengers: int = 0
total_passengers: int = 0 total_passengers: int = 0
average_wait_time: float = 0.0 average_floor_wait_time: float = 0.0
p95_wait_time: float = 0.0 # 95th percentile p95_floor_wait_time: float = 0.0 # 95th percentile
average_system_time: float = 0.0 average_arrival_wait_time: float = 0.0
p95_system_time: float = 0.0 # 95th percentile p95_arrival_wait_time: float = 0.0 # 95th percentile
Properties: Properties:

View File

@@ -6,7 +6,7 @@ Unified API Client for Elevator Saga
import json import json
import urllib.error import urllib.error
import urllib.request import urllib.request
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional
from elevator_saga.core.models import ( from elevator_saga.core.models import (
ElevatorState, ElevatorState,
@@ -63,16 +63,8 @@ class ElevatorAPIClient:
# 使用服务端返回的metrics数据 # 使用服务端返回的metrics数据
metrics_data = response_data.get("metrics", {}) metrics_data = response_data.get("metrics", {})
if metrics_data: if metrics_data:
# 转换为PerformanceMetrics格式 # 直接从字典创建PerformanceMetrics对象
metrics = PerformanceMetrics( metrics = PerformanceMetrics.from_dict(metrics_data)
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),
)
else: else:
metrics = PerformanceMetrics() metrics = PerformanceMetrics()
@@ -131,7 +123,7 @@ class ElevatorAPIClient:
else: else:
raise RuntimeError(f"Step failed: {response_data.get('error')}") 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) endpoint = self._get_elevator_endpoint(command)
debug_log( debug_log(
@@ -156,7 +148,7 @@ class ElevatorAPIClient:
debug_log(f"Go to floor failed: {e}") debug_log(f"Go to floor failed: {e}")
return False 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}" base = f"/api/elevators/{command.elevator_id}"

View File

@@ -352,9 +352,7 @@ class ElevatorController(ABC):
if elevator_id is not None and floor_id is not None and direction is not None: 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) elevator_proxy = ProxyElevator(elevator_id, self.api_client)
floor_proxy = ProxyFloor(floor_id, self.api_client) floor_proxy = ProxyFloor(floor_id, self.api_client)
# 服务端发送的direction是字符串直接使用 self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction)
direction_str = direction if isinstance(direction, str) else direction.value
self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction_str)
elif event.type == EventType.ELEVATOR_APPROACHING: elif event.type == EventType.ELEVATOR_APPROACHING:
elevator_id = event.data.get("elevator") 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: 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) elevator_proxy = ProxyElevator(elevator_id, self.api_client)
floor_proxy = ProxyFloor(floor_id, self.api_client) floor_proxy = ProxyFloor(floor_id, self.api_client)
# 服务端发送的direction是字符串直接使用 self.on_elevator_approaching(elevator_proxy, floor_proxy, direction)
direction_str = direction if isinstance(direction, str) else direction.value
self.on_elevator_approaching(elevator_proxy, floor_proxy, direction_str)
elif event.type == EventType.PASSENGER_BOARD: elif event.type == EventType.PASSENGER_BOARD:
elevator_id = event.data.get("elevator") elevator_id = event.data.get("elevator")

View File

@@ -41,6 +41,7 @@ class ElevatorBusExampleController(ElevatorController):
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None: def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
self.all_passengers.append(passenger) self.all_passengers.append(passenger)
print(f"乘客 {passenger.id} F{floor.floor} 请求 {passenger.origin} -> {passenger.destination} ({direction})")
pass pass
def on_elevator_idle(self, elevator: ProxyElevator) -> None: def on_elevator_idle(self, elevator: ProxyElevator) -> None:
@@ -60,10 +61,10 @@ class ElevatorBusExampleController(ElevatorController):
elevator.go_to_floor(elevator.current_floor - 1) elevator.go_to_floor(elevator.current_floor - 1)
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None: 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: 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: def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
pass pass

View File

@@ -174,13 +174,13 @@ class PassengerInfo(SerializableModel):
return PassengerStatus.WAITING return PassengerStatus.WAITING
@property @property
def wait_time(self) -> int: def floor_wait_time(self) -> int:
"""等待时间""" """在楼层等待时间(从到达到上电梯)"""
return self.pickup_tick - self.arrive_tick return self.pickup_tick - self.arrive_tick
@property @property
def system_time(self) -> int: def arrival_wait_time(self) -> int:
"""系统时间(总时间""" """总等待时间(从到达到下电梯"""
return self.dropoff_tick - self.arrive_tick return self.dropoff_tick - self.arrive_tick
@property @property
@@ -331,10 +331,10 @@ class PerformanceMetrics(SerializableModel):
completed_passengers: int = 0 completed_passengers: int = 0
total_passengers: int = 0 total_passengers: int = 0
average_wait_time: float = 0.0 average_floor_wait_time: float = 0.0
p95_wait_time: float = 0.0 p95_floor_wait_time: float = 0.0
average_system_time: float = 0.0 average_arrival_wait_time: float = 0.0
p95_system_time: float = 0.0 p95_arrival_wait_time: float = 0.0
# total_energy_consumption: float = 0.0 # total_energy_consumption: float = 0.0
@property @property

View File

@@ -22,6 +22,7 @@ from elevator_saga.core.models import (
FloorState, FloorState,
PassengerInfo, PassengerInfo,
PassengerStatus, PassengerStatus,
PerformanceMetrics,
SerializableModel, SerializableModel,
SimulationEvent, SimulationEvent,
SimulationState, SimulationState,
@@ -89,19 +90,6 @@ def json_response(data: Any, status: int = 200) -> Response | tuple[Response, in
return response, status 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 @dataclass
class PassengerSummary(SerializableModel): class PassengerSummary(SerializableModel):
"""乘客摘要""" """乘客摘要"""
@@ -120,7 +108,7 @@ class SimulationStateResponse(SerializableModel):
elevators: List[ElevatorState] elevators: List[ElevatorState]
floors: List[FloorState] floors: List[FloorState]
passengers: Dict[int, PassengerInfo] passengers: Dict[int, PassengerInfo]
metrics: MetricsResponse metrics: PerformanceMetrics
class ElevatorSimulation: class ElevatorSimulation:
@@ -292,33 +280,32 @@ class ElevatorSimulation:
# Return events generated this tick # Return events generated this tick
return self.state.events[events_start:] return self.state.events[events_start:]
def _process_passenger_in(self) -> None: def _process_passenger_in(self, elevator: ElevatorState) -> None:
for elevator in self.elevators: current_floor = elevator.current_floor
current_floor = elevator.current_floor # 处于Stopped状态方向也已经清空说明没有调度。
# 处于Stopped状态方向也已经清空说明没有调度。 floor = self.floors[current_floor]
floor = self.floors[current_floor] passengers_to_board: List[int] = []
passengers_to_board: List[int] = [] available_capacity = elevator.max_capacity - len(elevator.passengers)
available_capacity = elevator.max_capacity - len(elevator.passengers) # Board passengers going up (if up indicator is on or no direction set)
# Board passengers going up (if up indicator is on or no direction set) if elevator.target_floor_direction == Direction.UP:
if elevator.target_floor_direction == Direction.UP: passengers_to_board.extend(floor.up_queue[:available_capacity])
passengers_to_board.extend(floor.up_queue[:available_capacity]) floor.up_queue = 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) # Board passengers going down (if down indicator is on or no direction set)
if elevator.target_floor_direction == Direction.DOWN: if elevator.target_floor_direction == Direction.DOWN:
passengers_to_board.extend(floor.down_queue[:available_capacity]) passengers_to_board.extend(floor.down_queue[:available_capacity])
floor.down_queue = floor.down_queue[available_capacity:] floor.down_queue = floor.down_queue[available_capacity:]
# Process boarding # Process boarding
for passenger_id in passengers_to_board: for passenger_id in passengers_to_board:
passenger = self.passengers[passenger_id] passenger = self.passengers[passenger_id]
passenger.pickup_tick = self.tick passenger.pickup_tick = self.tick
passenger.elevator_id = elevator.id passenger.elevator_id = elevator.id
elevator.passengers.append(passenger_id) elevator.passengers.append(passenger_id)
self._emit_event( self._emit_event(
EventType.PASSENGER_BOARD, EventType.PASSENGER_BOARD,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id}, {"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id},
) )
def _update_elevator_status(self) -> None: def _update_elevator_status(self) -> None:
"""更新电梯运行状态""" """更新电梯运行状态"""
@@ -331,7 +318,7 @@ class ElevatorSimulation:
if elevator.next_target_floor is not None: if elevator.next_target_floor is not None:
self._set_elevator_target_floor(elevator, elevator.next_target_floor) self._set_elevator_target_floor(elevator, elevator.next_target_floor)
self._process_passenger_in() self._process_passenger_in(elevator)
elevator.next_target_floor = None elevator.next_target_floor = None
else: else:
continue continue
@@ -411,7 +398,7 @@ class ElevatorSimulation:
EventType.ELEVATOR_APPROACHING, EventType.ELEVATOR_APPROACHING,
{ {
"elevator": elevator.id, "elevator": elevator.id,
"floor": elevator.target_floor, "floor": int(round(elevator.position.current_floor_float)),
"direction": elevator.target_floor_direction.value, "direction": elevator.target_floor_direction.value,
}, },
) )
@@ -547,41 +534,45 @@ class ElevatorSimulation:
metrics=metrics, metrics=metrics,
) )
def _calculate_metrics(self) -> MetricsResponse: def _calculate_metrics(self) -> PerformanceMetrics:
"""Calculate performance metrics""" """Calculate performance metrics"""
# 直接从state中筛选已完成的乘客 # 直接从state中筛选已完成的乘客
completed = [p for p in self.state.passengers.values() if p.status == PassengerStatus.COMPLETED] completed = [p for p in self.state.passengers.values() if p.status == PassengerStatus.COMPLETED]
total_passengers = len(self.state.passengers) total_passengers = len(self.state.passengers)
if not completed: if not completed:
return MetricsResponse( return PerformanceMetrics(
done=0, completed_passengers=0,
total=total_passengers, total_passengers=total_passengers,
avg_wait=0, average_floor_wait_time=0,
p95_wait=0, p95_floor_wait_time=0,
avg_system=0, average_arrival_wait_time=0,
p95_system=0, p95_arrival_wait_time=0,
energy_total=sum(e.energy_consumed for e in self.elevators),
) )
wait_times = [float(p.wait_time) for p in completed] floor_wait_times = [float(p.floor_wait_time) for p in completed]
system_times = [float(p.system_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: if not data:
return 0.0 return 0.0
sorted_data = sorted(data) sorted_data = sorted(data)
index = int(len(sorted_data) * p / 100) # 计算要保留的数据数量(排除掉最长的 exclude_percent
return sorted_data[min(index, len(sorted_data) - 1)] 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( return PerformanceMetrics(
done=len(completed), completed_passengers=len(completed),
total=total_passengers, total_passengers=total_passengers,
avg_wait=sum(wait_times) / len(wait_times) if wait_times else 0, average_floor_wait_time=sum(floor_wait_times) / len(floor_wait_times) if floor_wait_times else 0,
p95_wait=percentile(wait_times, 95), p95_floor_wait_time=average_excluding_top_percent(floor_wait_times, 5),
avg_system=sum(system_times) / len(system_times) if system_times else 0, average_arrival_wait_time=sum(arrival_wait_times) / len(arrival_wait_times) if arrival_wait_times else 0,
p95_system=percentile(system_times, 95), p95_arrival_wait_time=average_excluding_top_percent(arrival_wait_times, 5),
energy_total=sum(e.energy_consumed for e in self.elevators),
) )
def get_events(self, since_tick: int = 0) -> List[SimulationEvent]: def get_events(self, since_tick: int = 0) -> List[SimulationEvent]: