mirror of
https://github.com/ZGCA-Forge/Elevator.git
synced 2025-12-14 13:04:41 +00:00
Add tests and docs
This commit is contained in:
3
docs/.gitignore
vendored
Normal file
3
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
_build/
|
||||||
|
_static/
|
||||||
|
_templates/
|
||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
61
docs/README.md
Normal file
61
docs/README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Elevator Saga Documentation
|
||||||
|
|
||||||
|
This directory contains the Sphinx documentation for Elevator Saga.
|
||||||
|
|
||||||
|
## Building the Documentation
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build HTML Documentation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make html
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated HTML documentation will be in `_build/html/`.
|
||||||
|
|
||||||
|
### View Documentation
|
||||||
|
|
||||||
|
Open `_build/html/index.html` in your browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Linux
|
||||||
|
xdg-open _build/html/index.html
|
||||||
|
|
||||||
|
# On macOS
|
||||||
|
open _build/html/index.html
|
||||||
|
|
||||||
|
# On Windows
|
||||||
|
start _build/html/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Build Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make latexpdf # Build PDF documentation
|
||||||
|
make epub # Build EPUB documentation
|
||||||
|
make clean # Clean build directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
- `index.rst` - Main documentation index
|
||||||
|
- `models.rst` - Data models documentation
|
||||||
|
- `client.rst` - Client architecture and proxy models
|
||||||
|
- `communication.rst` - HTTP communication protocol
|
||||||
|
- `events.rst` - Event system and tick-based simulation
|
||||||
|
- `api/modules.rst` - Auto-generated API reference
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When adding new documentation:
|
||||||
|
|
||||||
|
1. Create `.rst` files for new topics
|
||||||
|
2. Add them to the `toctree` in `index.rst`
|
||||||
|
3. Follow reStructuredText syntax
|
||||||
|
4. Build locally to verify formatting
|
||||||
|
5. Submit PR with documentation changes
|
||||||
39
docs/api/modules.rst
Normal file
39
docs/api/modules.rst
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
Core Modules
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. automodule:: elevator_saga.core.models
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Client Modules
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. automodule:: elevator_saga.client.api_client
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: elevator_saga.client.proxy_models
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: elevator_saga.client.base_controller
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Server Modules
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. automodule:: elevator_saga.server.simulator
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
370
docs/client.rst
Normal file
370
docs/client.rst
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
Client Architecture and Proxy Models
|
||||||
|
====================================
|
||||||
|
|
||||||
|
The Elevator Saga client provides a powerful abstraction layer that allows you to interact with the simulation using dynamic proxy objects. This architecture provides type-safe, read-only access to simulation state while enabling elevator control commands.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
The client architecture consists of three main components:
|
||||||
|
|
||||||
|
1. **Proxy Models** (``proxy_models.py``): Dynamic proxies that provide transparent access to server state
|
||||||
|
2. **API Client** (``api_client.py``): HTTP client for communicating with the server
|
||||||
|
3. **Base Controller** (``base_controller.py``): Abstract base class for implementing control algorithms
|
||||||
|
|
||||||
|
Proxy Models
|
||||||
|
------------
|
||||||
|
|
||||||
|
Proxy models in ``elevator_saga/client/proxy_models.py`` provide a clever way to access remote state as if it were local. They inherit from the data models but override attribute access to fetch fresh data from the server.
|
||||||
|
|
||||||
|
How Proxy Models Work
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The proxy pattern implementation uses Python's ``__getattribute__`` magic method to intercept attribute access:
|
||||||
|
|
||||||
|
1. When you access an attribute (e.g., ``elevator.current_floor``), the proxy intercepts the call
|
||||||
|
2. The proxy fetches the latest state from the server via API client
|
||||||
|
3. The proxy returns the requested attribute from the fresh state
|
||||||
|
4. All accesses are **read-only** to maintain consistency
|
||||||
|
|
||||||
|
This design ensures you always work with the most up-to-date simulation state without manual refresh calls.
|
||||||
|
|
||||||
|
ProxyElevator
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Dynamic proxy for ``ElevatorState`` that provides access to elevator properties and control methods:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ProxyElevator(ElevatorState):
|
||||||
|
"""
|
||||||
|
Dynamic proxy for elevator state
|
||||||
|
Provides complete type-safe access and control methods
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, elevator_id: int, api_client: ElevatorAPIClient):
|
||||||
|
self._elevator_id = elevator_id
|
||||||
|
self._api_client = api_client
|
||||||
|
self._init_ok = True
|
||||||
|
|
||||||
|
def go_to_floor(self, floor: int, immediate: bool = False) -> bool:
|
||||||
|
"""Command elevator to go to specified floor"""
|
||||||
|
return self._api_client.go_to_floor(self._elevator_id, floor, immediate)
|
||||||
|
|
||||||
|
**Accessible Properties** (from ElevatorState):
|
||||||
|
|
||||||
|
- ``id``: Elevator identifier
|
||||||
|
- ``current_floor``: Current floor number
|
||||||
|
- ``current_floor_float``: Precise position (e.g., 2.5)
|
||||||
|
- ``target_floor``: Destination floor
|
||||||
|
- ``position``: Full Position object
|
||||||
|
- ``passengers``: List of passenger IDs on board
|
||||||
|
- ``max_capacity``: Maximum passenger capacity
|
||||||
|
- ``run_status``: Current ElevatorStatus
|
||||||
|
- ``target_floor_direction``: Direction to target (UP/DOWN/STOPPED)
|
||||||
|
- ``last_tick_direction``: Previous movement direction
|
||||||
|
- ``is_idle``: Whether stopped
|
||||||
|
- ``is_full``: Whether at capacity
|
||||||
|
- ``is_running``: Whether in motion
|
||||||
|
- ``pressed_floors``: Destination floors of current passengers
|
||||||
|
- ``load_factor``: Current load (0.0 to 1.0)
|
||||||
|
- ``indicators``: Up/down indicator lights
|
||||||
|
|
||||||
|
**Control Method**:
|
||||||
|
|
||||||
|
- ``go_to_floor(floor, immediate=False)``: Send elevator to floor
|
||||||
|
|
||||||
|
- ``immediate=True``: Change target immediately
|
||||||
|
- ``immediate=False``: Queue as next target after current destination
|
||||||
|
|
||||||
|
**Example Usage**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Access elevator state
|
||||||
|
print(f"Elevator {elevator.id} at floor {elevator.current_floor}")
|
||||||
|
print(f"Direction: {elevator.target_floor_direction.value}")
|
||||||
|
print(f"Passengers: {len(elevator.passengers)}/{elevator.max_capacity}")
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
if elevator.is_idle:
|
||||||
|
print("Elevator is idle")
|
||||||
|
elif elevator.is_full:
|
||||||
|
print("Elevator is full!")
|
||||||
|
|
||||||
|
# Control elevator
|
||||||
|
if elevator.current_floor == 0:
|
||||||
|
elevator.go_to_floor(5) # Send to floor 5
|
||||||
|
|
||||||
|
ProxyFloor
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Dynamic proxy for ``FloorState`` that provides access to floor information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ProxyFloor(FloorState):
|
||||||
|
"""
|
||||||
|
Dynamic proxy for floor state
|
||||||
|
Provides read-only access to floor information
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, floor_id: int, api_client: ElevatorAPIClient):
|
||||||
|
self._floor_id = floor_id
|
||||||
|
self._api_client = api_client
|
||||||
|
self._init_ok = True
|
||||||
|
|
||||||
|
**Accessible Properties** (from FloorState):
|
||||||
|
|
||||||
|
- ``floor``: Floor number
|
||||||
|
- ``up_queue``: List of passenger IDs waiting to go up
|
||||||
|
- ``down_queue``: List of passenger IDs waiting to go down
|
||||||
|
- ``has_waiting_passengers``: Whether any passengers are waiting
|
||||||
|
- ``total_waiting``: Total number of waiting passengers
|
||||||
|
|
||||||
|
**Example Usage**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
floor = floors[0]
|
||||||
|
print(f"Floor {floor.floor}")
|
||||||
|
print(f"Waiting to go up: {len(floor.up_queue)} passengers")
|
||||||
|
print(f"Waiting to go down: {len(floor.down_queue)} passengers")
|
||||||
|
|
||||||
|
if floor.has_waiting_passengers:
|
||||||
|
print(f"Total waiting: {floor.total_waiting}")
|
||||||
|
|
||||||
|
ProxyPassenger
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Dynamic proxy for ``PassengerInfo`` that provides access to passenger information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ProxyPassenger(PassengerInfo):
|
||||||
|
"""
|
||||||
|
Dynamic proxy for passenger information
|
||||||
|
Provides read-only access to passenger data
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, passenger_id: int, api_client: ElevatorAPIClient):
|
||||||
|
self._passenger_id = passenger_id
|
||||||
|
self._api_client = api_client
|
||||||
|
self._init_ok = True
|
||||||
|
|
||||||
|
**Accessible Properties** (from PassengerInfo):
|
||||||
|
|
||||||
|
- ``id``: Passenger identifier
|
||||||
|
- ``origin``: Starting floor
|
||||||
|
- ``destination``: Target floor
|
||||||
|
- ``arrive_tick``: When passenger appeared
|
||||||
|
- ``pickup_tick``: When passenger boarded (0 if waiting)
|
||||||
|
- ``dropoff_tick``: When passenger reached destination (0 if in transit)
|
||||||
|
- ``elevator_id``: Current elevator ID (None if waiting)
|
||||||
|
- ``status``: Current PassengerStatus
|
||||||
|
- ``wait_time``: Ticks waited before boarding
|
||||||
|
- ``system_time``: Total ticks in system
|
||||||
|
- ``travel_direction``: UP or DOWN
|
||||||
|
|
||||||
|
**Example Usage**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(f"Passenger {passenger.id}")
|
||||||
|
print(f"From floor {passenger.origin} to {passenger.destination}")
|
||||||
|
print(f"Status: {passenger.status.value}")
|
||||||
|
|
||||||
|
if passenger.status == PassengerStatus.IN_ELEVATOR:
|
||||||
|
print(f"In elevator {passenger.elevator_id}")
|
||||||
|
print(f"Waited {passenger.wait_time} ticks")
|
||||||
|
|
||||||
|
Read-Only Protection
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
All proxy models are **read-only**. Attempting to modify attributes will raise an error:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
elevator.current_floor = 5 # ❌ Raises AttributeError
|
||||||
|
elevator.passengers.append(123) # ❌ Raises AttributeError
|
||||||
|
|
||||||
|
This ensures that:
|
||||||
|
|
||||||
|
1. Client cannot corrupt server state
|
||||||
|
2. All state changes go through proper API commands
|
||||||
|
3. State consistency is maintained
|
||||||
|
|
||||||
|
Implementation Details
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The proxy implementation uses a clever pattern with ``_init_ok`` flag:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ProxyElevator(ElevatorState):
|
||||||
|
_init_ok = False
|
||||||
|
|
||||||
|
def __init__(self, elevator_id: int, api_client: ElevatorAPIClient):
|
||||||
|
self._elevator_id = elevator_id
|
||||||
|
self._api_client = api_client
|
||||||
|
self._init_ok = True # Enable proxy behavior
|
||||||
|
|
||||||
|
def __getattribute__(self, name: str) -> Any:
|
||||||
|
# During initialization, use normal attribute access
|
||||||
|
if not name.startswith("_") and self._init_ok and name not in self.__class__.__dict__:
|
||||||
|
# Try to find as a method of this class
|
||||||
|
try:
|
||||||
|
self_attr = object.__getattribute__(self, name)
|
||||||
|
if callable(self_attr):
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
# Fetch fresh state and return attribute
|
||||||
|
elevator_state = self._get_elevator_state()
|
||||||
|
return elevator_state.__getattribute__(name)
|
||||||
|
else:
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
|
# Allow setting during initialization only
|
||||||
|
if not self._init_ok:
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Cannot modify read-only attribute '{name}'")
|
||||||
|
|
||||||
|
This design:
|
||||||
|
|
||||||
|
1. Allows normal initialization of internal fields (``_elevator_id``, ``_api_client``)
|
||||||
|
2. Intercepts access to data attributes after initialization
|
||||||
|
3. Preserves access to class methods (like ``go_to_floor``)
|
||||||
|
4. Blocks all attribute modifications after initialization
|
||||||
|
|
||||||
|
Base Controller
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The ``ElevatorController`` class in ``base_controller.py`` provides the framework for implementing control algorithms:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from elevator_saga.client.base_controller import ElevatorController
|
||||||
|
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
class MyController(ElevatorController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("http://127.0.0.1:8000", auto_run=True)
|
||||||
|
|
||||||
|
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
|
||||||
|
"""Called once at start with all elevators and floors"""
|
||||||
|
print(f"Initialized with {len(elevators)} elevators")
|
||||||
|
|
||||||
|
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
|
||||||
|
"""Called when a passenger presses a button"""
|
||||||
|
print(f"Passenger {passenger.id} at floor {floor.floor} going {direction}")
|
||||||
|
|
||||||
|
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
|
||||||
|
"""Called when elevator stops at a floor"""
|
||||||
|
print(f"Elevator {elevator.id} stopped at floor {floor.floor}")
|
||||||
|
# Implement your dispatch logic here
|
||||||
|
|
||||||
|
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
|
||||||
|
"""Called when elevator becomes idle"""
|
||||||
|
# Send idle elevator somewhere useful
|
||||||
|
elevator.go_to_floor(0)
|
||||||
|
|
||||||
|
The controller provides these event handlers:
|
||||||
|
|
||||||
|
- ``on_init(elevators, floors)``: Initialization
|
||||||
|
- ``on_event_execute_start(tick, events, elevators, floors)``: Before processing tick events
|
||||||
|
- ``on_event_execute_end(tick, events, elevators, floors)``: After processing tick events
|
||||||
|
- ``on_passenger_call(passenger, floor, direction)``: Button press
|
||||||
|
- ``on_elevator_stopped(elevator, floor)``: Elevator arrival
|
||||||
|
- ``on_elevator_idle(elevator)``: Elevator becomes idle
|
||||||
|
- ``on_passenger_board(elevator, passenger)``: Passenger boards
|
||||||
|
- ``on_passenger_alight(elevator, passenger, floor)``: Passenger alights
|
||||||
|
- ``on_elevator_passing_floor(elevator, floor, direction)``: Elevator passes floor
|
||||||
|
- ``on_elevator_approaching(elevator, floor, direction)``: Elevator about to arrive
|
||||||
|
|
||||||
|
Complete Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Here's a simple controller that sends idle elevators to the ground floor:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import List
|
||||||
|
from elevator_saga.client.base_controller import ElevatorController
|
||||||
|
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
|
||||||
|
|
||||||
|
class SimpleController(ElevatorController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("http://127.0.0.1:8000", auto_run=True)
|
||||||
|
self.pending_calls = []
|
||||||
|
|
||||||
|
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
|
||||||
|
print(f"Controlling {len(elevators)} elevators in {len(floors)}-floor building")
|
||||||
|
|
||||||
|
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
|
||||||
|
print(f"Call from floor {floor.floor}, direction {direction}")
|
||||||
|
self.pending_calls.append((floor.floor, direction))
|
||||||
|
# Dispatch nearest idle elevator
|
||||||
|
self._dispatch_to_call(floor.floor)
|
||||||
|
|
||||||
|
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
|
||||||
|
if self.pending_calls:
|
||||||
|
floor, direction = self.pending_calls.pop(0)
|
||||||
|
elevator.go_to_floor(floor)
|
||||||
|
else:
|
||||||
|
# No calls, return to ground floor
|
||||||
|
elevator.go_to_floor(0)
|
||||||
|
|
||||||
|
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
|
||||||
|
print(f"Elevator {elevator.id} at floor {floor.floor}")
|
||||||
|
print(f" Passengers on board: {len(elevator.passengers)}")
|
||||||
|
print(f" Waiting at floor: {floor.total_waiting}")
|
||||||
|
|
||||||
|
def _dispatch_to_call(self, floor: int) -> None:
|
||||||
|
# Find nearest idle elevator and send it
|
||||||
|
# (Simplified - real implementation would be more sophisticated)
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
controller = SimpleController()
|
||||||
|
controller.start()
|
||||||
|
|
||||||
|
Benefits of Proxy Architecture
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
1. **Type Safety**: IDE autocomplete and type checking work perfectly
|
||||||
|
2. **Always Fresh**: No need to manually refresh state
|
||||||
|
3. **Clean API**: Access remote state as if it were local
|
||||||
|
4. **Read-Only Safety**: Cannot accidentally corrupt server state
|
||||||
|
5. **Separation of Concerns**: State management handled by proxies, logic in controller
|
||||||
|
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
|
||||||
|
----------
|
||||||
|
|
||||||
|
- See :doc:`communication` for details on the HTTP API
|
||||||
|
- See :doc:`events` for understanding the event-driven simulation
|
||||||
|
- Check ``client_examples/bus_example.py`` for a complete implementation
|
||||||
544
docs/communication.rst
Normal file
544
docs/communication.rst
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
HTTP Communication Architecture
|
||||||
|
================================
|
||||||
|
|
||||||
|
Elevator Saga uses a **client-server architecture** with HTTP-based communication. The server manages the simulation state and physics, while clients send commands and query state over HTTP.
|
||||||
|
|
||||||
|
Architecture Overview
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Client │ │ Server │
|
||||||
|
│ (Controller) │ │ (Simulator) │
|
||||||
|
├─────────────────┤ ├─────────────────┤
|
||||||
|
│ base_controller │◄──── HTTP ────────►│ simulator.py │
|
||||||
|
│ proxy_models │ │ Flask Routes │
|
||||||
|
│ api_client │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ GET /api/state │
|
||||||
|
│ POST /api/step │
|
||||||
|
│ POST /api/elevators/:id/go_to_floor │
|
||||||
|
│ │
|
||||||
|
└───────────────────────────────────────┘
|
||||||
|
|
||||||
|
The separation provides:
|
||||||
|
|
||||||
|
- **Modularity**: Server and client can be developed independently
|
||||||
|
- **Multiple Clients**: Multiple controllers can compete/cooperate
|
||||||
|
- **Language Flexibility**: Clients can be written in any language
|
||||||
|
- **Network Deployment**: Server and client can run on different machines
|
||||||
|
|
||||||
|
Server Side: Simulator
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The server is implemented in ``elevator_saga/server/simulator.py`` using **Flask** as the HTTP framework.
|
||||||
|
|
||||||
|
API Endpoints
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**GET /api/state**
|
||||||
|
|
||||||
|
Returns complete simulation state:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/state", methods=["GET"])
|
||||||
|
def get_state() -> Response:
|
||||||
|
try:
|
||||||
|
state = simulation.get_state()
|
||||||
|
return json_response(state)
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
Response format:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"tick": 42,
|
||||||
|
"elevators": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"position": {"current_floor": 2, "target_floor": 5, "floor_up_position": 3},
|
||||||
|
"passengers": [101, 102],
|
||||||
|
"max_capacity": 10,
|
||||||
|
"run_status": "constant_speed",
|
||||||
|
"..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"floors": [
|
||||||
|
{"floor": 0, "up_queue": [103], "down_queue": []},
|
||||||
|
"..."
|
||||||
|
],
|
||||||
|
"passengers": {
|
||||||
|
"101": {"id": 101, "origin": 0, "destination": 5, "..."}
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"done": 50,
|
||||||
|
"total": 100,
|
||||||
|
"avg_wait": 15.2,
|
||||||
|
"p95_wait": 30.0,
|
||||||
|
"avg_system": 25.5,
|
||||||
|
"p95_system": 45.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**POST /api/step**
|
||||||
|
|
||||||
|
Advances simulation by specified number of ticks:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/step", methods=["POST"])
|
||||||
|
def step_simulation() -> Response:
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
ticks = data.get("ticks", 1)
|
||||||
|
events = simulation.step(ticks)
|
||||||
|
return json_response({
|
||||||
|
"tick": simulation.tick,
|
||||||
|
"events": events,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{"ticks": 1}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"tick": 43,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"tick": 43,
|
||||||
|
"type": "stopped_at_floor",
|
||||||
|
"data": {"elevator": 0, "floor": 5, "reason": "move_reached"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**POST /api/elevators/:id/go_to_floor**
|
||||||
|
|
||||||
|
Commands an elevator to go to a floor:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/elevators/<int:elevator_id>/go_to_floor", methods=["POST"])
|
||||||
|
def elevator_go_to_floor(elevator_id: int) -> Response:
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
floor = data["floor"]
|
||||||
|
immediate = data.get("immediate", False)
|
||||||
|
simulation.elevator_go_to_floor(elevator_id, floor, immediate)
|
||||||
|
return json_response({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{"floor": 5, "immediate": false}
|
||||||
|
|
||||||
|
- ``immediate=false``: Set as next target after current destination
|
||||||
|
- ``immediate=true``: Change target immediately (cancels current target)
|
||||||
|
|
||||||
|
**POST /api/reset**
|
||||||
|
|
||||||
|
Resets simulation to initial state:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/reset", methods=["POST"])
|
||||||
|
def reset_simulation() -> Response:
|
||||||
|
try:
|
||||||
|
simulation.reset()
|
||||||
|
return json_response({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
**POST /api/traffic/next**
|
||||||
|
|
||||||
|
Loads next traffic scenario:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/traffic/next", methods=["POST"])
|
||||||
|
def next_traffic_round() -> Response:
|
||||||
|
try:
|
||||||
|
full_reset = request.get_json().get("full_reset", False)
|
||||||
|
success = simulation.next_traffic_round(full_reset)
|
||||||
|
if success:
|
||||||
|
return json_response({"success": True})
|
||||||
|
else:
|
||||||
|
return json_response({"success": False, "error": "No more scenarios"}, 400)
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
**GET /api/traffic/info**
|
||||||
|
|
||||||
|
Gets current traffic scenario information:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/traffic/info", methods=["GET"])
|
||||||
|
def get_traffic_info() -> Response:
|
||||||
|
try:
|
||||||
|
info = simulation.get_traffic_info()
|
||||||
|
return json_response(info)
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"current_index": 0,
|
||||||
|
"total_files": 5,
|
||||||
|
"max_tick": 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
Client Side: API Client
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The client is implemented in ``elevator_saga/client/api_client.py`` using Python's built-in ``urllib`` library.
|
||||||
|
|
||||||
|
ElevatorAPIClient Class
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ElevatorAPIClient:
|
||||||
|
"""Unified elevator API client"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
# Caching fields
|
||||||
|
self._cached_state: Optional[SimulationState] = None
|
||||||
|
self._cached_tick: int = -1
|
||||||
|
self._tick_processed: bool = False
|
||||||
|
|
||||||
|
State Caching Strategy
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The API client implements **smart caching** to reduce HTTP requests:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def get_state(self, force_reload: bool = False) -> SimulationState:
|
||||||
|
"""Get simulation state with caching"""
|
||||||
|
# Return cached state if valid
|
||||||
|
if not force_reload and self._cached_state is not None and not self._tick_processed:
|
||||||
|
return self._cached_state
|
||||||
|
|
||||||
|
# Fetch fresh state
|
||||||
|
response_data = self._send_get_request("/api/state")
|
||||||
|
# ... parse and create SimulationState ...
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self._cached_state = simulation_state
|
||||||
|
self._cached_tick = simulation_state.tick
|
||||||
|
self._tick_processed = False # Mark as fresh
|
||||||
|
|
||||||
|
return simulation_state
|
||||||
|
|
||||||
|
def mark_tick_processed(self) -> None:
|
||||||
|
"""Mark current tick as processed, invalidating cache"""
|
||||||
|
self._tick_processed = True
|
||||||
|
|
||||||
|
**Cache Behavior**:
|
||||||
|
|
||||||
|
1. First ``get_state()`` call in a tick fetches from server
|
||||||
|
2. Subsequent calls within same tick return cached data
|
||||||
|
3. After ``step()`` is called, cache is invalidated
|
||||||
|
4. Next ``get_state()`` fetches fresh data
|
||||||
|
|
||||||
|
This provides:
|
||||||
|
|
||||||
|
- **Performance**: Minimize HTTP requests
|
||||||
|
- **Consistency**: All operations in a tick see same state
|
||||||
|
- **Freshness**: New tick always gets new state
|
||||||
|
|
||||||
|
Core API Methods
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**get_state(force_reload=False)**
|
||||||
|
|
||||||
|
Fetches current simulation state:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def get_state(self, force_reload: bool = False) -> SimulationState:
|
||||||
|
if not force_reload and self._cached_state is not None and not self._tick_processed:
|
||||||
|
return self._cached_state
|
||||||
|
|
||||||
|
response_data = self._send_get_request("/api/state")
|
||||||
|
|
||||||
|
# Parse response into data models
|
||||||
|
elevators = [ElevatorState.from_dict(e) for e in response_data["elevators"]]
|
||||||
|
floors = [FloorState.from_dict(f) for f in response_data["floors"]]
|
||||||
|
# ... handle passengers and metrics ...
|
||||||
|
|
||||||
|
simulation_state = SimulationState(
|
||||||
|
tick=response_data["tick"],
|
||||||
|
elevators=elevators,
|
||||||
|
floors=floors,
|
||||||
|
passengers=passengers,
|
||||||
|
metrics=metrics,
|
||||||
|
events=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self._cached_state = simulation_state
|
||||||
|
self._cached_tick = simulation_state.tick
|
||||||
|
self._tick_processed = False
|
||||||
|
|
||||||
|
return simulation_state
|
||||||
|
|
||||||
|
**step(ticks=1)**
|
||||||
|
|
||||||
|
Advances simulation:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def step(self, ticks: int = 1) -> StepResponse:
|
||||||
|
response_data = self._send_post_request("/api/step", {"ticks": ticks})
|
||||||
|
|
||||||
|
# Parse events
|
||||||
|
events = []
|
||||||
|
for event_data in response_data["events"]:
|
||||||
|
event_dict = event_data.copy()
|
||||||
|
if "type" in event_dict:
|
||||||
|
event_dict["type"] = EventType(event_dict["type"])
|
||||||
|
events.append(SimulationEvent.from_dict(event_dict))
|
||||||
|
|
||||||
|
return StepResponse(
|
||||||
|
success=True,
|
||||||
|
tick=response_data["tick"],
|
||||||
|
events=events
|
||||||
|
)
|
||||||
|
|
||||||
|
**go_to_floor(elevator_id, floor, immediate=False)**
|
||||||
|
|
||||||
|
Sends elevator to floor:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def go_to_floor(self, elevator_id: int, floor: int, immediate: bool = False) -> bool:
|
||||||
|
command = GoToFloorCommand(
|
||||||
|
elevator_id=elevator_id,
|
||||||
|
floor=floor,
|
||||||
|
immediate=immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.send_elevator_command(command)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"Go to floor failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
HTTP Request Implementation
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The client uses ``urllib.request`` for HTTP communication:
|
||||||
|
|
||||||
|
**GET Request**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _send_get_request(self, endpoint: str) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=60) as response:
|
||||||
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
|
return data
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
raise RuntimeError(f"GET {url} failed: {e}")
|
||||||
|
|
||||||
|
**POST Request**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _send_post_request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
request_body = json.dumps(data).encode("utf-8")
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=request_body,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=600) as response:
|
||||||
|
response_data = json.loads(response.read().decode("utf-8"))
|
||||||
|
return response_data
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
raise RuntimeError(f"POST {url} failed: {e}")
|
||||||
|
|
||||||
|
Communication Flow
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Typical communication sequence during one tick:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
Client Server
|
||||||
|
│ │
|
||||||
|
│ 1. GET /api/state │
|
||||||
|
├────────────────────────────────────►│
|
||||||
|
│ ◄── SimulationState (cached) │
|
||||||
|
│ │
|
||||||
|
│ 2. Analyze state, make decisions │
|
||||||
|
│ │
|
||||||
|
│ 3. POST /api/elevators/0/go_to_floor │
|
||||||
|
├────────────────────────────────────►│
|
||||||
|
│ ◄── {"success": true} │
|
||||||
|
│ │
|
||||||
|
│ 4. GET /api/state (from cache) │
|
||||||
|
│ No HTTP request! │
|
||||||
|
│ │
|
||||||
|
│ 5. POST /api/step │
|
||||||
|
├────────────────────────────────────►│
|
||||||
|
│ Server processes tick │
|
||||||
|
│ - Moves elevators │
|
||||||
|
│ - Boards/alights passengers │
|
||||||
|
│ - Generates events │
|
||||||
|
│ ◄── {tick: 43, events: [...]} │
|
||||||
|
│ │
|
||||||
|
│ 6. Process events │
|
||||||
|
│ Cache invalidated │
|
||||||
|
│ │
|
||||||
|
│ 7. GET /api/state (fetches fresh) │
|
||||||
|
├────────────────────────────────────►│
|
||||||
|
│ ◄── SimulationState │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
|
||||||
|
Error Handling
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Both client and server implement robust error handling:
|
||||||
|
|
||||||
|
**Server Side**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/api/step", methods=["POST"])
|
||||||
|
def step_simulation() -> Response:
|
||||||
|
try:
|
||||||
|
# ... process request ...
|
||||||
|
return json_response(result)
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({"error": str(e)}, 500)
|
||||||
|
|
||||||
|
**Client Side**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def go_to_floor(self, elevator_id: int, floor: int, immediate: bool = False) -> bool:
|
||||||
|
try:
|
||||||
|
response = self.send_elevator_command(command)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"Go to floor failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
Thread Safety
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The simulator uses a lock to ensure thread-safe access:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ElevatorSimulation:
|
||||||
|
def __init__(self, ...):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def step(self, num_ticks: int = 1) -> List[SimulationEvent]:
|
||||||
|
with self.lock:
|
||||||
|
# ... process ticks ...
|
||||||
|
|
||||||
|
def get_state(self) -> SimulationStateResponse:
|
||||||
|
with self.lock:
|
||||||
|
# ... return state ...
|
||||||
|
|
||||||
|
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**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# ❌ Bad - sequential commands
|
||||||
|
elevator1.go_to_floor(5)
|
||||||
|
time.sleep(0.1) # Wait for response
|
||||||
|
elevator2.go_to_floor(3)
|
||||||
|
|
||||||
|
# ✅ Good - issue commands quickly
|
||||||
|
elevator1.go_to_floor(5)
|
||||||
|
elevator2.go_to_floor(3)
|
||||||
|
# All commands received before next tick
|
||||||
|
|
||||||
|
**Cache Awareness**:
|
||||||
|
|
||||||
|
Use ``mark_tick_processed()`` to explicitly invalidate cache if needed, but normally the framework handles this automatically.
|
||||||
|
|
||||||
|
Testing the API
|
||||||
|
---------------
|
||||||
|
|
||||||
|
You can test the API directly using curl:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Get state
|
||||||
|
curl http://127.0.0.1:8000/api/state
|
||||||
|
|
||||||
|
# Step simulation
|
||||||
|
curl -X POST http://127.0.0.1:8000/api/step \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ticks": 1}'
|
||||||
|
|
||||||
|
# Send elevator to floor
|
||||||
|
curl -X POST http://127.0.0.1:8000/api/elevators/0/go_to_floor \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"floor": 5, "immediate": false}'
|
||||||
|
|
||||||
|
# Reset simulation
|
||||||
|
curl -X POST http://127.0.0.1:8000/api/reset \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
|
- See :doc:`events` for understanding how events drive the simulation
|
||||||
|
- See :doc:`client` for using the API through proxy models
|
||||||
|
- Check the source code for complete implementation details
|
||||||
59
docs/conf.py
Normal file
59
docs/conf.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = "Elevator Saga"
|
||||||
|
copyright = "2025, ZGCA-Forge Team"
|
||||||
|
author = "ZGCA-Forge Team"
|
||||||
|
release = "0.1.0"
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"sphinx.ext.intersphinx",
|
||||||
|
"sphinx.ext.todo",
|
||||||
|
"sphinx_rtd_theme",
|
||||||
|
]
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
|
||||||
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
# Napoleon settings
|
||||||
|
napoleon_google_docstring = True
|
||||||
|
napoleon_numpy_docstring = True
|
||||||
|
napoleon_include_init_with_doc = True
|
||||||
|
napoleon_include_private_with_doc = False
|
||||||
|
napoleon_include_special_with_doc = True
|
||||||
|
napoleon_use_admonition_for_examples = False
|
||||||
|
napoleon_use_admonition_for_notes = False
|
||||||
|
napoleon_use_admonition_for_references = False
|
||||||
|
napoleon_use_ivar = False
|
||||||
|
napoleon_use_param = True
|
||||||
|
napoleon_use_rtype = True
|
||||||
|
napoleon_preprocess_types = False
|
||||||
|
napoleon_type_aliases = None
|
||||||
|
napoleon_attr_annotations = True
|
||||||
|
|
||||||
|
# Intersphinx mapping
|
||||||
|
intersphinx_mapping = {
|
||||||
|
"python": ("https://docs.python.org/3", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Todo extension
|
||||||
|
todo_include_todos = True
|
||||||
592
docs/events.rst
Normal file
592
docs/events.rst
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
Event-Driven Simulation and Tick-Based Execution
|
||||||
|
================================================
|
||||||
|
|
||||||
|
Elevator Saga uses an **event-driven, tick-based** discrete simulation model. The simulation progresses in discrete time steps (ticks), and events are generated to notify the controller about state changes.
|
||||||
|
|
||||||
|
Simulation Overview
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Simulation Loop │
|
||||||
|
│ │
|
||||||
|
│ Tick N │
|
||||||
|
│ 1. Update elevator status (START_UP → CONSTANT_SPEED)│
|
||||||
|
│ 2. Process arrivals (new passengers) │
|
||||||
|
│ 3. Move elevators (physics simulation) │
|
||||||
|
│ 4. Process stops (boarding/alighting) │
|
||||||
|
│ 5. Generate events │
|
||||||
|
│ │
|
||||||
|
│ Events sent to client → Client processes → Commands │
|
||||||
|
│ │
|
||||||
|
│ Tick N+1 │
|
||||||
|
│ (repeat...) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Tick-Based Execution
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
What is a Tick?
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A **tick** is the fundamental unit of simulation time. Each tick represents one discrete time step where:
|
||||||
|
|
||||||
|
1. Physics is updated (elevators move)
|
||||||
|
2. State changes occur (passengers board/alight)
|
||||||
|
3. Events are generated
|
||||||
|
4. Controller receives events and makes decisions
|
||||||
|
|
||||||
|
Think of it like frames in a video game - the simulation updates at discrete intervals.
|
||||||
|
|
||||||
|
Tick Processing Flow
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In ``simulator.py``, the ``step()`` method processes ticks:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def step(self, num_ticks: int = 1) -> List[SimulationEvent]:
|
||||||
|
"""Process one or more simulation ticks"""
|
||||||
|
with self.lock:
|
||||||
|
new_events = []
|
||||||
|
for _ in range(num_ticks):
|
||||||
|
self.state.tick += 1
|
||||||
|
tick_events = self._process_tick()
|
||||||
|
new_events.extend(tick_events)
|
||||||
|
|
||||||
|
# Force complete passengers if max duration reached
|
||||||
|
if self.tick >= self.max_duration_ticks:
|
||||||
|
completed_count = self.force_complete_remaining_passengers()
|
||||||
|
|
||||||
|
return new_events
|
||||||
|
|
||||||
|
Each ``_process_tick()`` executes the four-phase cycle:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _process_tick(self) -> List[SimulationEvent]:
|
||||||
|
"""Process one simulation tick"""
|
||||||
|
events_start = len(self.state.events)
|
||||||
|
|
||||||
|
# Phase 1: Update elevator status
|
||||||
|
self._update_elevator_status()
|
||||||
|
|
||||||
|
# Phase 2: Add new passengers from traffic queue
|
||||||
|
self._process_arrivals()
|
||||||
|
|
||||||
|
# Phase 3: Move elevators
|
||||||
|
self._move_elevators()
|
||||||
|
|
||||||
|
# Phase 4: Process elevator stops and passenger boarding/alighting
|
||||||
|
self._process_elevator_stops()
|
||||||
|
|
||||||
|
# Return events generated this tick
|
||||||
|
return self.state.events[events_start:]
|
||||||
|
|
||||||
|
Elevator State Machine
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Elevators transition through states each tick:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
STOPPED ──(target set)──► START_UP ──(1 tick)──► CONSTANT_SPEED
|
||||||
|
│
|
||||||
|
(near target)
|
||||||
|
▼
|
||||||
|
START_DOWN
|
||||||
|
│
|
||||||
|
(1 tick)
|
||||||
|
▼
|
||||||
|
(arrived) STOPPED
|
||||||
|
|
||||||
|
State Transitions
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Phase 1: Update Elevator Status** (``_update_elevator_status()``):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _update_elevator_status(self) -> None:
|
||||||
|
"""Update elevator operational state"""
|
||||||
|
for elevator in self.elevators:
|
||||||
|
# If no direction, check for next target
|
||||||
|
if elevator.target_floor_direction == Direction.STOPPED:
|
||||||
|
if elevator.next_target_floor is not None:
|
||||||
|
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
|
||||||
|
self._process_passenger_in()
|
||||||
|
elevator.next_target_floor = None
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Transition state machine
|
||||||
|
if elevator.run_status == ElevatorStatus.STOPPED:
|
||||||
|
# Start acceleration
|
||||||
|
elevator.run_status = ElevatorStatus.START_UP
|
||||||
|
elif elevator.run_status == ElevatorStatus.START_UP:
|
||||||
|
# Switch to constant speed after 1 tick
|
||||||
|
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
|
||||||
|
|
||||||
|
**Important Notes**:
|
||||||
|
|
||||||
|
- ``START_UP`` = acceleration (not direction!)
|
||||||
|
- ``START_DOWN`` = deceleration (not direction!)
|
||||||
|
- Actual movement direction is ``target_floor_direction`` (UP/DOWN)
|
||||||
|
- State transitions happen **before** movement
|
||||||
|
|
||||||
|
Movement Physics
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Speed by State
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Elevators move at different speeds depending on their state:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _move_elevators(self) -> None:
|
||||||
|
"""Move all elevators towards their destinations"""
|
||||||
|
for elevator in self.elevators:
|
||||||
|
# Determine speed based on state
|
||||||
|
movement_speed = 0
|
||||||
|
if elevator.run_status == ElevatorStatus.START_UP:
|
||||||
|
movement_speed = 1 # Accelerating: 0.1 floors/tick
|
||||||
|
elif elevator.run_status == ElevatorStatus.START_DOWN:
|
||||||
|
movement_speed = 1 # Decelerating: 0.1 floors/tick
|
||||||
|
elif elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
|
||||||
|
movement_speed = 2 # Full speed: 0.2 floors/tick
|
||||||
|
|
||||||
|
if movement_speed == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Apply movement in appropriate direction
|
||||||
|
if elevator.target_floor_direction == Direction.UP:
|
||||||
|
new_floor = elevator.position.floor_up_position_add(movement_speed)
|
||||||
|
elif elevator.target_floor_direction == Direction.DOWN:
|
||||||
|
new_floor = elevator.position.floor_up_position_add(-movement_speed)
|
||||||
|
|
||||||
|
Position System
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Positions use a **10-unit sub-floor** system:
|
||||||
|
|
||||||
|
- ``current_floor = 2, floor_up_position = 0`` → exactly at floor 2
|
||||||
|
- ``current_floor = 2, floor_up_position = 5`` → halfway between floors 2 and 3
|
||||||
|
- ``current_floor = 2, floor_up_position = 10`` → advances to ``current_floor = 3, floor_up_position = 0``
|
||||||
|
|
||||||
|
This granularity allows smooth movement and precise deceleration timing.
|
||||||
|
|
||||||
|
Deceleration Logic
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Elevators must decelerate before stopping:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _should_start_deceleration(self, elevator: ElevatorState) -> bool:
|
||||||
|
"""Check if should start decelerating"""
|
||||||
|
distance = self._calculate_distance_to_target(elevator)
|
||||||
|
return distance == 1 # Start deceleration 1 position unit before target
|
||||||
|
|
||||||
|
# In _move_elevators():
|
||||||
|
if elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
|
||||||
|
if self._should_start_deceleration(elevator):
|
||||||
|
elevator.run_status = ElevatorStatus.START_DOWN
|
||||||
|
|
||||||
|
This ensures elevators don't overshoot their target floor.
|
||||||
|
|
||||||
|
Event System
|
||||||
|
------------
|
||||||
|
|
||||||
|
Event Types
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
The simulation generates 8 types of events defined in ``EventType`` enum:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class EventType(Enum):
|
||||||
|
UP_BUTTON_PRESSED = "up_button_pressed"
|
||||||
|
DOWN_BUTTON_PRESSED = "down_button_pressed"
|
||||||
|
PASSING_FLOOR = "passing_floor"
|
||||||
|
STOPPED_AT_FLOOR = "stopped_at_floor"
|
||||||
|
ELEVATOR_APPROACHING = "elevator_approaching"
|
||||||
|
IDLE = "idle"
|
||||||
|
PASSENGER_BOARD = "passenger_board"
|
||||||
|
PASSENGER_ALIGHT = "passenger_alight"
|
||||||
|
|
||||||
|
Event Generation
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Events are generated during tick processing:
|
||||||
|
|
||||||
|
**Passenger Arrival**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _process_arrivals(self) -> None:
|
||||||
|
"""Process new passenger arrivals"""
|
||||||
|
while self.traffic_queue and self.traffic_queue[0].tick <= self.tick:
|
||||||
|
traffic_entry = self.traffic_queue.pop(0)
|
||||||
|
passenger = PassengerInfo(
|
||||||
|
id=traffic_entry.id,
|
||||||
|
origin=traffic_entry.origin,
|
||||||
|
destination=traffic_entry.destination,
|
||||||
|
arrive_tick=self.tick,
|
||||||
|
)
|
||||||
|
self.passengers[passenger.id] = passenger
|
||||||
|
|
||||||
|
if passenger.destination > passenger.origin:
|
||||||
|
self.floors[passenger.origin].up_queue.append(passenger.id)
|
||||||
|
# Generate UP_BUTTON_PRESSED event
|
||||||
|
self._emit_event(
|
||||||
|
EventType.UP_BUTTON_PRESSED,
|
||||||
|
{"floor": passenger.origin, "passenger": passenger.id}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.floors[passenger.origin].down_queue.append(passenger.id)
|
||||||
|
# Generate DOWN_BUTTON_PRESSED event
|
||||||
|
self._emit_event(
|
||||||
|
EventType.DOWN_BUTTON_PRESSED,
|
||||||
|
{"floor": passenger.origin, "passenger": passenger.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
**Elevator Movement**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _move_elevators(self) -> None:
|
||||||
|
for elevator in self.elevators:
|
||||||
|
# ... movement logic ...
|
||||||
|
|
||||||
|
# Passing a floor
|
||||||
|
if old_floor != new_floor and new_floor != target_floor:
|
||||||
|
self._emit_event(
|
||||||
|
EventType.PASSING_FLOOR,
|
||||||
|
{
|
||||||
|
"elevator": elevator.id,
|
||||||
|
"floor": new_floor,
|
||||||
|
"direction": elevator.target_floor_direction.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# About to arrive (during deceleration)
|
||||||
|
if self._near_next_stop(elevator):
|
||||||
|
self._emit_event(
|
||||||
|
EventType.ELEVATOR_APPROACHING,
|
||||||
|
{
|
||||||
|
"elevator": elevator.id,
|
||||||
|
"floor": elevator.target_floor,
|
||||||
|
"direction": elevator.target_floor_direction.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Arrived at target
|
||||||
|
if target_floor == new_floor and elevator.position.floor_up_position == 0:
|
||||||
|
elevator.run_status = ElevatorStatus.STOPPED
|
||||||
|
self._emit_event(
|
||||||
|
EventType.STOPPED_AT_FLOOR,
|
||||||
|
{
|
||||||
|
"elevator": elevator.id,
|
||||||
|
"floor": new_floor,
|
||||||
|
"reason": "move_reached"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
**Boarding and Alighting**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _process_elevator_stops(self) -> None:
|
||||||
|
for elevator in self.elevators:
|
||||||
|
if elevator.run_status != ElevatorStatus.STOPPED:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_floor = elevator.current_floor
|
||||||
|
|
||||||
|
# Passengers alight
|
||||||
|
passengers_to_remove = []
|
||||||
|
for passenger_id in elevator.passengers:
|
||||||
|
passenger = self.passengers[passenger_id]
|
||||||
|
if passenger.destination == current_floor:
|
||||||
|
passenger.dropoff_tick = self.tick
|
||||||
|
passengers_to_remove.append(passenger_id)
|
||||||
|
|
||||||
|
for passenger_id in passengers_to_remove:
|
||||||
|
elevator.passengers.remove(passenger_id)
|
||||||
|
self._emit_event(
|
||||||
|
EventType.PASSENGER_ALIGHT,
|
||||||
|
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
**Idle Detection**:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# If elevator stopped with no direction, it's idle
|
||||||
|
if elevator.last_tick_direction == Direction.STOPPED:
|
||||||
|
self._emit_event(
|
||||||
|
EventType.IDLE,
|
||||||
|
{"elevator": elevator.id, "floor": current_floor}
|
||||||
|
)
|
||||||
|
|
||||||
|
Event Processing in Controller
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
The ``ElevatorController`` base class automatically routes events to handler methods:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ElevatorController(ABC):
|
||||||
|
def _execute_events(self, events: List[SimulationEvent]) -> None:
|
||||||
|
"""Process events and route to handlers"""
|
||||||
|
for event in events:
|
||||||
|
if event.type == EventType.UP_BUTTON_PRESSED:
|
||||||
|
passenger_id = event.data["passenger"]
|
||||||
|
floor = self.floors[event.data["floor"]]
|
||||||
|
passenger = ProxyPassenger(passenger_id, self.api_client)
|
||||||
|
self.on_passenger_call(passenger, floor, "up")
|
||||||
|
|
||||||
|
elif event.type == EventType.DOWN_BUTTON_PRESSED:
|
||||||
|
passenger_id = event.data["passenger"]
|
||||||
|
floor = self.floors[event.data["floor"]]
|
||||||
|
passenger = ProxyPassenger(passenger_id, self.api_client)
|
||||||
|
self.on_passenger_call(passenger, floor, "down")
|
||||||
|
|
||||||
|
elif event.type == EventType.STOPPED_AT_FLOOR:
|
||||||
|
elevator = self.elevators[event.data["elevator"]]
|
||||||
|
floor = self.floors[event.data["floor"]]
|
||||||
|
self.on_elevator_stopped(elevator, floor)
|
||||||
|
|
||||||
|
elif event.type == EventType.IDLE:
|
||||||
|
elevator = self.elevators[event.data["elevator"]]
|
||||||
|
self.on_elevator_idle(elevator)
|
||||||
|
|
||||||
|
# ... other event types ...
|
||||||
|
|
||||||
|
Control Flow: Bus Example
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The ``bus_example.py`` demonstrates a simple "bus route" algorithm:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ElevatorBusExampleController(ElevatorController):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("http://127.0.0.1:8000", True)
|
||||||
|
self.all_passengers = []
|
||||||
|
self.max_floor = 0
|
||||||
|
|
||||||
|
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
|
||||||
|
"""Initialize elevators to starting positions"""
|
||||||
|
self.max_floor = floors[-1].floor
|
||||||
|
self.floors = floors
|
||||||
|
|
||||||
|
for i, elevator in enumerate(elevators):
|
||||||
|
# Distribute elevators evenly across floors
|
||||||
|
target_floor = (i * (len(floors) - 1)) // len(elevators)
|
||||||
|
elevator.go_to_floor(target_floor, immediate=True)
|
||||||
|
|
||||||
|
def on_event_execute_start(self, tick: int, events: List[SimulationEvent],
|
||||||
|
elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
|
||||||
|
"""Print state before processing events"""
|
||||||
|
print(f"Tick {tick}: Processing {len(events)} events {[e.type.value for e in events]}")
|
||||||
|
for elevator in elevators:
|
||||||
|
print(
|
||||||
|
f"\t{elevator.id}[{elevator.target_floor_direction.value},"
|
||||||
|
f"{elevator.current_floor_float}/{elevator.target_floor}]"
|
||||||
|
+ "👦" * len(elevator.passengers),
|
||||||
|
end=""
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
|
||||||
|
"""Implement bus route logic"""
|
||||||
|
print(f"🛑 Elevator E{elevator.id} stopped at F{floor.floor}")
|
||||||
|
|
||||||
|
# Bus algorithm: go up to top, then down to bottom, repeat
|
||||||
|
if elevator.last_tick_direction == Direction.UP and elevator.current_floor == self.max_floor:
|
||||||
|
# At top, start going down
|
||||||
|
elevator.go_to_floor(elevator.current_floor - 1)
|
||||||
|
elif elevator.last_tick_direction == Direction.DOWN and elevator.current_floor == 0:
|
||||||
|
# At bottom, start going up
|
||||||
|
elevator.go_to_floor(elevator.current_floor + 1)
|
||||||
|
elif elevator.last_tick_direction == Direction.UP:
|
||||||
|
# Continue upward
|
||||||
|
elevator.go_to_floor(elevator.current_floor + 1)
|
||||||
|
elif elevator.last_tick_direction == Direction.DOWN:
|
||||||
|
# Continue downward
|
||||||
|
elevator.go_to_floor(elevator.current_floor - 1)
|
||||||
|
|
||||||
|
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
|
||||||
|
"""Send idle elevator to floor 1"""
|
||||||
|
elevator.go_to_floor(1)
|
||||||
|
|
||||||
|
Execution Sequence
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Here's what happens in a typical tick:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
Server: Tick 42
|
||||||
|
Phase 1: Update status
|
||||||
|
- Elevator 0: STOPPED → START_UP (has target)
|
||||||
|
Phase 2: Process arrivals
|
||||||
|
- Passenger 101 arrives at floor 0, going to floor 5
|
||||||
|
- Event: UP_BUTTON_PRESSED
|
||||||
|
Phase 3: Move elevators
|
||||||
|
- Elevator 0: floor 2.0 → 2.1 (accelerating)
|
||||||
|
Phase 4: Process stops
|
||||||
|
- (no stops this tick)
|
||||||
|
|
||||||
|
Events: [UP_BUTTON_PRESSED, PASSING_FLOOR]
|
||||||
|
|
||||||
|
Client: Receive events
|
||||||
|
on_event_execute_start(tick=42, events=[...])
|
||||||
|
- Print "Tick 42: Processing 2 events"
|
||||||
|
|
||||||
|
_execute_events():
|
||||||
|
- UP_BUTTON_PRESSED → on_passenger_call()
|
||||||
|
→ Controller decides which elevator to send
|
||||||
|
- PASSING_FLOOR → on_elevator_passing_floor()
|
||||||
|
|
||||||
|
on_event_execute_end(tick=42, events=[...])
|
||||||
|
|
||||||
|
Client: Send commands
|
||||||
|
- elevator.go_to_floor(0) → POST /api/elevators/0/go_to_floor
|
||||||
|
|
||||||
|
Client: Step simulation
|
||||||
|
- POST /api/step → Server processes tick 43
|
||||||
|
|
||||||
|
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
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Queued (default): Wait until current target reached
|
||||||
|
elevator.go_to_floor(5, immediate=False)
|
||||||
|
# ← Sets elevator.next_target_floor = 5
|
||||||
|
# ← Processed when current_floor == target_floor
|
||||||
|
|
||||||
|
# Immediate: Change target right away
|
||||||
|
elevator.go_to_floor(5, immediate=True)
|
||||||
|
# ← Sets elevator.position.target_floor = 5 immediately
|
||||||
|
# ← May interrupt current journey
|
||||||
|
|
||||||
|
Use ``immediate=True`` for emergency redirects, ``immediate=False`` (default) for normal operation.
|
||||||
|
|
||||||
|
Performance Metrics
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Metrics are calculated from passenger data:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def _calculate_metrics(self) -> MetricsResponse:
|
||||||
|
"""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]
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
-------
|
||||||
|
|
||||||
|
The event-driven, tick-based architecture provides:
|
||||||
|
|
||||||
|
- **Deterministic**: Same inputs always produce same results
|
||||||
|
- **Testable**: Easy to create test scenarios with traffic files
|
||||||
|
- **Debuggable**: Clear event trail shows what happened when
|
||||||
|
- **Flexible**: Easy to implement different dispatch algorithms
|
||||||
|
- **Scalable**: Can simulate large buildings and many passengers
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
|
- Study ``bus_example.py`` for a complete working example
|
||||||
|
- Implement your own controller by extending ``ElevatorController``
|
||||||
|
- Experiment with different dispatch algorithms
|
||||||
|
- Analyze performance metrics to optimize your approach
|
||||||
122
docs/index.rst
Normal file
122
docs/index.rst
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
Welcome to Elevator Saga's Documentation!
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. image:: https://badge.fury.io/py/elevatorpy.svg
|
||||||
|
:target: https://badge.fury.io/py/elevatorpy
|
||||||
|
:alt: PyPI version
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/elevatorpy.svg
|
||||||
|
:target: https://pypi.org/project/elevatorpy/
|
||||||
|
:alt: Python versions
|
||||||
|
|
||||||
|
.. image:: https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg
|
||||||
|
:target: https://github.com/ZGCA-Forge/Elevator/actions
|
||||||
|
:alt: Build Status
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/github/stars/ZGCA-Forge/Elevator.svg?style=social&label=Star
|
||||||
|
:target: https://github.com/ZGCA-Forge/Elevator
|
||||||
|
:alt: GitHub stars
|
||||||
|
|
||||||
|
Elevator Saga is a Python implementation of an elevator `simulation game <https://play.elevatorsaga.com/>`_ with an event-driven architecture. Design and optimize elevator control algorithms to efficiently transport passengers in buildings.
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
🏢 **Realistic Simulation**: Physics-based elevator movement with acceleration, deceleration, and realistic timing
|
||||||
|
|
||||||
|
🎮 **Event-Driven Architecture**: React to various events such as button presses, elevator arrivals, and passenger boarding
|
||||||
|
|
||||||
|
🔌 **Client-Server Model**: Separate simulation server from control logic for clean architecture
|
||||||
|
|
||||||
|
📊 **Performance Metrics**: Track wait times, system times, and completion rates
|
||||||
|
|
||||||
|
🎯 **Flexible Control**: Implement your own algorithms using a simple controller interface
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Basic Installation
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install elevatorpy
|
||||||
|
|
||||||
|
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
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Running the Simulation
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Terminal #1: Start the backend simulator**
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m elevator_saga.server.simulator
|
||||||
|
|
||||||
|
**Terminal #2: Start your controller**
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m elevator_saga.client_examples.bus_example
|
||||||
|
|
||||||
|
Architecture Overview
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Elevator Saga follows a **client-server architecture**:
|
||||||
|
|
||||||
|
- **Server** (`simulator.py`): Manages the simulation state, physics, and event generation
|
||||||
|
- **Client** (`base_controller.py`): Implements control algorithms and reacts to events
|
||||||
|
- **Communication** (`api_client.py`): HTTP-based API for state queries and commands
|
||||||
|
- **Data Models** (`models.py`): Unified data structures shared between client and server
|
||||||
|
|
||||||
|
Contents
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: User Guide
|
||||||
|
|
||||||
|
models
|
||||||
|
client
|
||||||
|
communication
|
||||||
|
events
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: API Reference
|
||||||
|
|
||||||
|
api/modules
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
This project is licensed under MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
347
docs/models.rst
Normal file
347
docs/models.rst
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
Data Models
|
||||||
|
===========
|
||||||
|
|
||||||
|
The Elevator Saga system uses a unified data model architecture defined in ``elevator_saga/core/models.py``. These models ensure type consistency and serialization between the client and server components.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
All data models inherit from ``SerializableModel``, which provides:
|
||||||
|
|
||||||
|
- **to_dict()**: Convert model to dictionary
|
||||||
|
- **to_json()**: Convert model to JSON string
|
||||||
|
- **from_dict()**: Create model instance from dictionary
|
||||||
|
- **from_json()**: Create model instance from JSON string
|
||||||
|
|
||||||
|
This unified serialization approach ensures seamless data exchange over HTTP between client and server.
|
||||||
|
|
||||||
|
Core Enumerations
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Direction
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Represents the direction of elevator movement or passenger travel:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class Direction(Enum):
|
||||||
|
UP = "up" # Moving upward
|
||||||
|
DOWN = "down" # Moving downward
|
||||||
|
STOPPED = "stopped" # Not moving
|
||||||
|
|
||||||
|
ElevatorStatus
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Represents the elevator's operational state in the state machine:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ElevatorStatus(Enum):
|
||||||
|
START_UP = "start_up" # Acceleration phase
|
||||||
|
START_DOWN = "start_down" # Deceleration phase
|
||||||
|
CONSTANT_SPEED = "constant_speed" # Constant speed phase
|
||||||
|
STOPPED = "stopped" # Stopped at floor
|
||||||
|
|
||||||
|
**Important**: ``START_UP`` and ``START_DOWN`` refer to **acceleration/deceleration states**, not movement direction. The actual movement direction is determined by the ``target_floor_direction`` property.
|
||||||
|
|
||||||
|
State Machine Transition:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
STOPPED → START_UP → CONSTANT_SPEED → START_DOWN → STOPPED
|
||||||
|
1 tick 1 tick N ticks 1 tick
|
||||||
|
|
||||||
|
PassengerStatus
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Represents the passenger's current state:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class PassengerStatus(Enum):
|
||||||
|
WAITING = "waiting" # Waiting at origin floor
|
||||||
|
IN_ELEVATOR = "in_elevator" # Inside an elevator
|
||||||
|
COMPLETED = "completed" # Reached destination
|
||||||
|
CANCELLED = "cancelled" # Cancelled (unused)
|
||||||
|
|
||||||
|
EventType
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Defines all possible simulation events:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class EventType(Enum):
|
||||||
|
UP_BUTTON_PRESSED = "up_button_pressed"
|
||||||
|
DOWN_BUTTON_PRESSED = "down_button_pressed"
|
||||||
|
PASSING_FLOOR = "passing_floor"
|
||||||
|
STOPPED_AT_FLOOR = "stopped_at_floor"
|
||||||
|
ELEVATOR_APPROACHING = "elevator_approaching"
|
||||||
|
IDLE = "idle"
|
||||||
|
PASSENGER_BOARD = "passenger_board"
|
||||||
|
PASSENGER_ALIGHT = "passenger_alight"
|
||||||
|
|
||||||
|
Core Data Models
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Position
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Represents elevator position with sub-floor granularity:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Position(SerializableModel):
|
||||||
|
current_floor: int = 0 # Current floor number
|
||||||
|
target_floor: int = 0 # Target floor number
|
||||||
|
floor_up_position: int = 0 # Position within floor (0-9)
|
||||||
|
|
||||||
|
- **floor_up_position**: Represents position between floors with 10 units per floor
|
||||||
|
- **current_floor_float**: Returns floating-point floor position (e.g., 2.5 = halfway between floors 2 and 3)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
position = Position(current_floor=2, floor_up_position=5)
|
||||||
|
print(position.current_floor_float) # 2.5
|
||||||
|
|
||||||
|
ElevatorState
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Complete state information for an elevator:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ElevatorState(SerializableModel):
|
||||||
|
id: int
|
||||||
|
position: Position
|
||||||
|
next_target_floor: Optional[int] = None
|
||||||
|
passengers: List[int] = [] # Passenger IDs
|
||||||
|
max_capacity: int = 10
|
||||||
|
speed_pre_tick: float = 0.5
|
||||||
|
run_status: ElevatorStatus = ElevatorStatus.STOPPED
|
||||||
|
last_tick_direction: Direction = Direction.STOPPED
|
||||||
|
indicators: ElevatorIndicators = field(default_factory=ElevatorIndicators)
|
||||||
|
passenger_destinations: Dict[int, int] = {} # passenger_id -> floor
|
||||||
|
energy_consumed: float = 0.0
|
||||||
|
last_update_tick: int = 0
|
||||||
|
|
||||||
|
Key Properties:
|
||||||
|
|
||||||
|
- ``current_floor``: Integer floor number
|
||||||
|
- ``current_floor_float``: Precise position including sub-floor
|
||||||
|
- ``target_floor``: Destination floor
|
||||||
|
- ``target_floor_direction``: Direction to target (UP/DOWN/STOPPED)
|
||||||
|
- ``is_idle``: Whether elevator is stopped
|
||||||
|
- ``is_full``: Whether elevator is at capacity
|
||||||
|
- ``is_running``: Whether elevator is in motion
|
||||||
|
- ``pressed_floors``: List of destination floors for current passengers
|
||||||
|
- ``load_factor``: Current load as fraction of capacity (0.0 to 1.0)
|
||||||
|
|
||||||
|
FloorState
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
State information for a building floor:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FloorState(SerializableModel):
|
||||||
|
floor: int
|
||||||
|
up_queue: List[int] = [] # Passenger IDs waiting to go up
|
||||||
|
down_queue: List[int] = [] # Passenger IDs waiting to go down
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- ``has_waiting_passengers``: Whether any passengers are waiting
|
||||||
|
- ``total_waiting``: Total number of waiting passengers
|
||||||
|
|
||||||
|
PassengerInfo
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Complete information about a passenger:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PassengerInfo(SerializableModel):
|
||||||
|
id: int
|
||||||
|
origin: int # Starting floor
|
||||||
|
destination: int # Target floor
|
||||||
|
arrive_tick: int # When passenger appeared
|
||||||
|
pickup_tick: int = 0 # When passenger boarded elevator
|
||||||
|
dropoff_tick: int = 0 # When passenger reached destination
|
||||||
|
elevator_id: Optional[int] = None
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- ``status``: Current PassengerStatus
|
||||||
|
- ``wait_time``: Ticks waited before boarding
|
||||||
|
- ``system_time``: Total ticks in system (arrive to dropoff)
|
||||||
|
- ``travel_direction``: UP/DOWN based on origin and destination
|
||||||
|
|
||||||
|
SimulationState
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Complete state of the simulation:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SimulationState(SerializableModel):
|
||||||
|
tick: int
|
||||||
|
elevators: List[ElevatorState]
|
||||||
|
floors: List[FloorState]
|
||||||
|
passengers: Dict[int, PassengerInfo]
|
||||||
|
metrics: PerformanceMetrics
|
||||||
|
events: List[SimulationEvent]
|
||||||
|
|
||||||
|
Helper Methods:
|
||||||
|
|
||||||
|
- ``get_elevator_by_id(id)``: Find elevator by ID
|
||||||
|
- ``get_floor_by_number(number)``: Find floor by number
|
||||||
|
- ``get_passengers_by_status(status)``: Filter passengers by status
|
||||||
|
- ``add_event(type, data)``: Add new event to queue
|
||||||
|
|
||||||
|
Traffic and Configuration
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
TrafficEntry
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Defines a single passenger arrival:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrafficEntry(SerializableModel):
|
||||||
|
id: int
|
||||||
|
origin: int
|
||||||
|
destination: int
|
||||||
|
tick: int # When passenger arrives
|
||||||
|
|
||||||
|
TrafficPattern
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Collection of traffic entries defining a test scenario:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrafficPattern(SerializableModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
entries: List[TrafficEntry]
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- ``total_passengers``: Number of passengers in pattern
|
||||||
|
- ``duration``: Tick when last passenger arrives
|
||||||
|
|
||||||
|
Performance Metrics
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
PerformanceMetrics
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Tracks simulation performance:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
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
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- ``completion_rate``: Fraction of passengers completed (0.0 to 1.0)
|
||||||
|
|
||||||
|
API Models
|
||||||
|
----------
|
||||||
|
|
||||||
|
The models also include HTTP API request/response structures:
|
||||||
|
|
||||||
|
- ``APIRequest``: Base request with ID and timestamp
|
||||||
|
- ``APIResponse``: Base response with success flag
|
||||||
|
- ``StepRequest/StepResponse``: Advance simulation time
|
||||||
|
- ``StateRequest``: Query simulation state
|
||||||
|
- ``ElevatorCommand``: Send command to elevator
|
||||||
|
- ``GoToFloorCommand``: Specific command to move elevator
|
||||||
|
|
||||||
|
Example Usage
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Creating a Simulation State
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from elevator_saga.core.models import (
|
||||||
|
create_empty_simulation_state,
|
||||||
|
ElevatorState,
|
||||||
|
Position,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a building with 3 elevators, 10 floors, capacity 8
|
||||||
|
state = create_empty_simulation_state(
|
||||||
|
elevators=3,
|
||||||
|
floors=10,
|
||||||
|
max_capacity=8
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access elevator state
|
||||||
|
elevator = state.elevators[0]
|
||||||
|
print(f"Elevator {elevator.id} at floor {elevator.current_floor}")
|
||||||
|
|
||||||
|
Working with Traffic Patterns
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from elevator_saga.core.models import (
|
||||||
|
create_simple_traffic_pattern,
|
||||||
|
TrafficPattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create traffic pattern: (origin, destination, tick)
|
||||||
|
pattern = create_simple_traffic_pattern(
|
||||||
|
name="morning_rush",
|
||||||
|
passengers=[
|
||||||
|
(0, 5, 10), # Floor 0→5 at tick 10
|
||||||
|
(0, 8, 15), # Floor 0→8 at tick 15
|
||||||
|
(2, 0, 20), # Floor 2→0 at tick 20
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Pattern has {pattern.total_passengers} passengers")
|
||||||
|
print(f"Duration: {pattern.duration} ticks")
|
||||||
|
|
||||||
|
Serialization
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
All models support JSON serialization:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Serialize to JSON
|
||||||
|
elevator = state.elevators[0]
|
||||||
|
json_str = elevator.to_json()
|
||||||
|
|
||||||
|
# Deserialize from JSON
|
||||||
|
restored = ElevatorState.from_json(json_str)
|
||||||
|
|
||||||
|
# Or use dictionaries
|
||||||
|
data = elevator.to_dict()
|
||||||
|
restored = ElevatorState.from_dict(data)
|
||||||
|
|
||||||
|
This enables seamless transmission over HTTP between client and server.
|
||||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
sphinx>=5.0.0
|
||||||
|
sphinx-rtd-theme>=1.0.0
|
||||||
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Elevator Saga Test Suite
|
||||||
|
"""
|
||||||
73
tests/test_imports.py
Normal file
73
tests/test_imports.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Test module imports to ensure all modules can be imported successfully
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_core_models():
|
||||||
|
"""Test importing core data models"""
|
||||||
|
from elevator_saga.core.models import (
|
||||||
|
Direction,
|
||||||
|
ElevatorState,
|
||||||
|
ElevatorStatus,
|
||||||
|
EventType,
|
||||||
|
FloorState,
|
||||||
|
PassengerInfo,
|
||||||
|
PassengerStatus,
|
||||||
|
SimulationState,
|
||||||
|
TrafficEntry,
|
||||||
|
TrafficPattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Direction is not None
|
||||||
|
assert ElevatorState is not None
|
||||||
|
assert ElevatorStatus is not None
|
||||||
|
assert EventType is not None
|
||||||
|
assert FloorState is not None
|
||||||
|
assert PassengerInfo is not None
|
||||||
|
assert PassengerStatus is not None
|
||||||
|
assert SimulationState is not None
|
||||||
|
assert TrafficEntry is not None
|
||||||
|
assert TrafficPattern is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_client_api():
|
||||||
|
"""Test importing client API"""
|
||||||
|
from elevator_saga.client.api_client import ElevatorAPIClient
|
||||||
|
|
||||||
|
assert ElevatorAPIClient is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_proxy_models():
|
||||||
|
"""Test importing proxy models"""
|
||||||
|
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
|
||||||
|
|
||||||
|
assert ProxyElevator is not None
|
||||||
|
assert ProxyFloor is not None
|
||||||
|
assert ProxyPassenger is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_base_controller():
|
||||||
|
"""Test importing base controller"""
|
||||||
|
from elevator_saga.client.base_controller import ElevatorController
|
||||||
|
|
||||||
|
assert ElevatorController is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_simulator():
|
||||||
|
"""Test importing simulator"""
|
||||||
|
from elevator_saga.server.simulator import ElevatorSimulation
|
||||||
|
|
||||||
|
assert ElevatorSimulation is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_client_example():
|
||||||
|
"""Test importing client example"""
|
||||||
|
from elevator_saga.client_examples.bus_example import ElevatorBusExampleController
|
||||||
|
|
||||||
|
assert ElevatorBusExampleController is not None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
Reference in New Issue
Block a user