8 Commits
0.0.2 ... 0.0.5

Author SHA1 Message Date
Xuwznln
b2d03b2510 Bump version: 0.0.4 → 0.0.5 2025-10-06 15:16:47 +08:00
Xuwznln
1a8063e4fd fix performance calculation. fix floor error in approaching event. fix passenger board wrongly. 2025-10-06 15:16:35 +08:00
Xuwznln
692b853101 Update bug report template 2025-10-06 14:36:53 +08:00
Xuwznln
4c86816920 Update docs 2025-10-01 18:32:03 +08:00
Xuwznln
349511e00f Bump version: 0.0.3 → 0.0.4 2025-10-01 18:07:21 +08:00
Xuwznln
cbfae640d1 Bump version: 0.0.2 → 0.0.3 2025-10-01 18:00:14 +08:00
Xuwznln
de3fa68fa6 recover version 2025-10-01 18:00:10 +08:00
Xuwznln
78395349b2 Modify pypi package name 2025-10-01 17:59:35 +08:00
15 changed files with 181 additions and 288 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.2 current_version = 0.0.5
commit = True commit = True
tag = True tag = True
tag_name = v{new_version} tag_name = v{new_version}

View File

@@ -0,0 +1,30 @@
---
name: Bug Report
about: Report a bug in the project
title: "[BUG] "
labels: bug
assignees: ""
---
**Version Information**
- elevator-py version: [e.g. v0.0.5]
- Python version: [e.g. 3.11]
**Bug Description**
A clear and concise description of the bug
**Steps to Reproduce**
1.
2.
3.
**Expected Behavior**
What you expected to happen
**Actual Behavior**
What actually happened
**Additional Context (Optional)**
Any other information that might help, such as error logs, screenshots, etc.

View File

@@ -2,8 +2,8 @@
<div align="center"> <div align="center">
[![PyPI version](https://badge.fury.io/py/elevatorpy.svg)](https://badge.fury.io/py/elevatorpy) [![PyPI version](https://badge.fury.io/py/elevator-py.svg)](https://badge.fury.io/py/elevator-py)
[![Python versions](https://img.shields.io/pypi/pyversions/elevatorpy.svg)](https://pypi.org/project/elevatorpy/) [![Python versions](https://img.shields.io/pypi/pyversions/elevator-py.svg)](https://pypi.org/project/elevator-py/)
[![Build Status](https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg)](https://github.com/ZGCA-Forge/Elevator/actions) [![Build Status](https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg)](https://github.com/ZGCA-Forge/Elevator/actions)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-brightgreen)](https://zgca-forge.github.io/Elevator/) [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-brightgreen)](https://zgca-forge.github.io/Elevator/)
@@ -27,21 +27,7 @@ Elevator Saga is a Python implementation of an elevator [simulation game](https:
### Basic Installation ### Basic Installation
```bash ```bash
pip install elevatorpy pip install elevator-py
```
### With Development Dependencies
```bash
pip install elevatorpy[dev]
```
### From Source
```bash
git clone https://github.com/ZGCA-Forge/Elevator.git
cd Elevator
pip install -e .[dev]
``` ```
## Quick Start ## Quick Start

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
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
@@ -343,25 +343,6 @@ Benefits of Proxy Architecture
5. **Separation of Concerns**: State management handled by proxies, logic in controller 5. **Separation of Concerns**: State management handled by proxies, logic in controller
6. **Testability**: Can mock API client for unit tests 6. **Testability**: Can mock API client for unit tests
Performance Considerations
--------------------------
Each attribute access triggers an API call. For better performance:
.. code-block:: python
# ❌ Inefficient - multiple API calls
if elevator.current_floor < elevator.target_floor:
diff = elevator.target_floor - elevator.current_floor
# ✅ Better - store references
current = elevator.current_floor
target = elevator.target_floor
if current < target:
diff = target - current
However, the API client implements **caching within a single tick**, so multiple accesses during event processing are efficient.
Next Steps Next Steps
---------- ----------

View File

@@ -17,9 +17,9 @@ Architecture Overview
│ api_client │ │ │ │ api_client │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │ │
│ GET /api/state │ │ GET /api/state
│ POST /api/step │ │ POST /api/step
│ POST /api/elevators/:id/go_to_floor │ │ POST /api/elevators/:id/go_to_floor
│ │ │ │
└───────────────────────────────────────┘ └───────────────────────────────────────┘
@@ -394,37 +394,37 @@ Typical communication sequence during one tick:
.. code-block:: text .. code-block:: text
Client Server Client Server
│ │
│ 1. GET /api/state │ │ 1. GET /api/state
├────────────────────────────────────►│ ├────────────────────────────────────►│
│ ◄── SimulationState (cached) │ │ ◄── SimulationState (cached)
│ │
│ 2. Analyze state, make decisions │ │ 2. Analyze state, make decisions
│ │
│ 3. POST /api/elevators/0/go_to_floor │ 3. POST /api/elevators/0/go_to_floor│
├────────────────────────────────────►│ ├────────────────────────────────────►│
│ ◄── {"success": true} │ │ ◄── {"success": true}
│ │
│ 4. GET /api/state (from cache) │ │ 4. GET /api/state (from cache)
│ No HTTP request! │ │ No HTTP request!
│ │
│ 5. POST /api/step │ │ 5. POST /api/step
├────────────────────────────────────►│ ├────────────────────────────────────►│
│ Server processes tick │ │ Server processes tick
│ - Moves elevators │ │ - Moves elevators
│ - Boards/alights passengers │ │ - Boards/alights passengers
│ - Generates events │ │ - Generates events
│ ◄── {tick: 43, events: [...]} │ │ ◄── {tick: 43, events: [...]}
│ │
│ 6. Process events │ │ 6. Process events
│ Cache invalidated │ │ Cache invalidated
│ │
│ 7. GET /api/state (fetches fresh) │ │ 7. GET /api/state (fetches fresh)
├────────────────────────────────────►│ ├────────────────────────────────────►│
│ ◄── SimulationState │ │ ◄── SimulationState
│ │
└─────────────────────────────────────┘ └─────────────────────────────────────
Error Handling Error Handling
-------------- --------------
@@ -476,23 +476,6 @@ The simulator uses a lock to ensure thread-safe access:
This allows Flask to handle concurrent requests safely. This allows Flask to handle concurrent requests safely.
Performance Considerations
--------------------------
**Minimize HTTP Calls**:
.. code-block:: python
# ❌ Bad - 3 HTTP calls
for i in range(3):
state = api_client.get_state()
print(state.tick)
# ✅ Good - 1 HTTP call (cached)
state = api_client.get_state()
for i in range(3):
print(state.tick)
**Batch Commands**: **Batch Commands**:
.. code-block:: python .. code-block:: python

View File

@@ -8,21 +8,21 @@ Simulation Overview
.. code-block:: text .. code-block:: text
┌─────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────
│ Simulation Loop │ │ Simulation Loop │
│ │ │ │
│ Tick N │ │ Tick N │
│ 1. Update elevator status (START_UP → CONSTANT_SPEED)│ │ 1. Update elevator status (START_UP → CONSTANT_SPEED)
│ 2. Process arrivals (new passengers) │ │ 2. Process arrivals (new passengers) │
│ 3. Move elevators (physics simulation) │ │ 3. Move elevators (physics simulation) │
│ 4. Process stops (boarding/alighting) │ │ 4. Process stops (boarding/alighting) │
│ 5. Generate events │ │ 5. Generate events │
│ │ │ │
│ Events sent to client → Client processes → Commands │ │ Events sent to client → Client processes → Commands
│ │ │ │
│ Tick N+1 │ │ Tick N+1 │
│ (repeat...) │ │ (repeat...) │
└─────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────
Tick-Based Execution Tick-Based Execution
-------------------- --------------------
@@ -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
} }
) )
@@ -463,27 +463,6 @@ Here's what happens in a typical tick:
Key Timing Concepts Key Timing Concepts
------------------- -------------------
Command vs. Execution
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# Tick 42: Controller sends command
elevator.go_to_floor(5, immediate=False)
# ← Command queued in elevator.next_target_floor
# Tick 43: Server processes
# ← _update_elevator_status() assigns target
# ← Elevator starts moving
# Tick 44-46: Elevator in motion
# ← Events: PASSING_FLOOR
# Tick 47: Elevator arrives
# ← Event: STOPPED_AT_FLOOR
There's a **one-tick delay** between command and execution (unless ``immediate=True``).
Immediate vs. Queued Immediate vs. Queued
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
@@ -508,69 +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%的平均值
Best Practices
--------------
1. **React to Events**: Don't poll state - implement event handlers
2. **Use Queued Commands**: Default ``immediate=False`` is safer
3. **Track Passengers**: Monitor ``on_passenger_call`` to know demand
4. **Optimize for Wait Time**: Reduce time between arrival and pickup
5. **Consider Load**: Check ``elevator.is_full`` before dispatching
6. **Handle Idle**: Always give idle elevators something to do (even if it's "go to floor 0")
Example: Efficient Dispatch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""Dispatch nearest suitable elevator"""
best_elevator = None
best_cost = float('inf')
for elevator in self.elevators:
# Skip if full
if elevator.is_full:
continue
# Calculate cost (distance + current load)
distance = abs(elevator.current_floor - floor.floor)
load_penalty = elevator.load_factor * 10
cost = distance + load_penalty
# Check if going in right direction
if elevator.target_floor_direction.value == direction:
cost *= 0.5 # Prefer elevators already going that way
if cost < best_cost:
best_cost = cost
best_elevator = elevator
if best_elevator:
best_elevator.go_to_floor(floor.floor)
Summary Summary
------- -------

View File

@@ -1,12 +1,12 @@
Welcome to Elevator Saga's Documentation! Welcome to Elevator Saga's Documentation!
========================================== ==========================================
.. image:: https://badge.fury.io/py/elevatorpy.svg .. image:: https://badge.fury.io/py/elevator-py.svg
:target: https://badge.fury.io/py/elevatorpy :target: https://badge.fury.io/py/elevator-py
:alt: PyPI version :alt: PyPI version
.. image:: https://img.shields.io/pypi/pyversions/elevatorpy.svg .. image:: https://img.shields.io/pypi/pyversions/elevator-py.svg
:target: https://pypi.org/project/elevatorpy/ :target: https://pypi.org/project/elevator-py/
:alt: Python versions :alt: Python versions
.. image:: https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg .. image:: https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg
@@ -40,23 +40,7 @@ Basic Installation
.. code-block:: bash .. code-block:: bash
pip install elevatorpy pip install elevator-py
With Development Dependencies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
pip install elevatorpy[dev]
From Source
~~~~~~~~~~~
.. code-block:: bash
git clone https://github.com/ZGCA-Forge/Elevator.git
cd Elevator
pip install -e .[dev]
Quick Start Quick Start
----------- -----------

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,5 +6,5 @@ A Python implementation of the Elevator Saga game with event-driven architecture
realistic elevator dispatch algorithm development and testing. realistic elevator dispatch algorithm development and testing.
""" """
__version__ = "0.0.2" __version__ = "0.0.5"
__author__ = "ZGCA Team" __author__ = "ZGCA Team"

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]:

View File

@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "elevatorpy" name = "elevator-py"
dynamic = ["version"] dynamic = ["version"]
description = "Python implementation of Elevator Saga game with event system" description = "Python implementation of Elevator Saga game with event system"
readme = "README.md" readme = "README.md"