diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..688988d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +elevator_system/__pycache__/main.cpython-312.pyc +elevator_system/__pycache__/test_elevator.cpython-312-pytest-7.4.4.pyc +elevator_system/__pycache__/test_elevator.cpython-312-pytest-8.4.1.pyc diff --git a/elevator_system/Elevator_Prediction_System_Design.pdf b/elevator_system/Elevator_Prediction_System_Design.pdf new file mode 100644 index 0000000..3ec0d08 Binary files /dev/null and b/elevator_system/Elevator_Prediction_System_Design.pdf differ diff --git a/elevator_system/README.md b/elevator_system/README.md new file mode 100644 index 0000000..36b7e69 --- /dev/null +++ b/elevator_system/README.md @@ -0,0 +1,111 @@ +# Elevator Prediction System + +A focused system for collecting elevator data to train ML models that predict optimal resting floors. + +## Overview + +This system implements the **"Golden Event"** approach for elevator prediction: +- **Core Concept**: When an elevator is resting on floor X at time T, and the next call comes from floor Y +- **ML Goal**: Predict the most likely call floor based on current resting floor and time +- **Data Design**: Single table capturing the critical resting→call relationship + +## The Golden Event + +The system focuses on capturing the most important data point for ML prediction: + +``` +Elevator resting on floor X at time T → Next call from floor Y +``` + +This relationship is logged in the `demand_log` table with all the features needed for ML training. + +## Database Schema + +### DemandLog Table (Single Table Design) + +| Column | Type | ML Purpose | +|--------|------|------------| +| id | Integer | Primary key | +| timestamp_rested | DateTime | **Feature**: When elevator became idle | +| timestamp_called | DateTime | **Feature**: When next call was received | +| resting_floor | Integer | **Feature**: Floor elevator was resting on | +| call_floor | Integer | **Label**: Floor the call came from | +| destination_floor | Integer | **Feature**: Where user wants to go | +| day_of_week | Integer | **Feature**: Day of week (0-6) | +| hour_of_day | Integer | **Feature**: Hour of day (0-23) | + +## API Endpoints + +### POST /call +Call the elevator from a floor (triggers golden event logging if elevator is resting). + +### POST /step +Advance simulation by one time unit. + +### GET /status +Get current elevator status and total logs. + +### GET /logs +Get demand logs for ML training. + +### GET /stats +Get statistics for ML insights. + +## Installation + +1. **Install dependencies:** +```bash +pip install -r requirements.txt +``` + +2. **Run the application:** +```bash +python main.py +``` +The server will start on `http://localhost:5000` + +### Running Tests + +```bash +python -m pytest test_elevator.py -v +``` + +### Running the Demo + +```bash +python demo.py +``` + +The demo simulates a typical office building day and shows: +- Golden event capture +- ML data analysis + +## ML Training Data + +The system generates perfect training data for ML models: + +### Features (X): +- `resting_floor`: Current resting floor +- `timestamp_rested`: When elevator became idle +- `day_of_week`: Day of the week (0-6) +- `hour_of_day`: Hour of the day (0-23) +- `destination_floor`: Where user wants to go (optional) + +### Label (y): +- `call_floor`: The floor the next call comes from + +### Example ML Use Cases: +- **Classification**: Predict most likely call floor +- **Regression**: Predict call probability for each floor +- **Time Series**: Predict call patterns over time + +## Key Design Principles + +1. **Focus on Golden Event**: Only log when elevator is resting and receives a call +2. **Discrete Time**: Step-based simulation for predictability and testing +3. **Single Table**: All ML data in one optimized table +4. **Time Features**: Automatic extraction of day_of_week and hour_of_day +5. **Simple API**: Minimal endpoints for simulation control + +## Final notes +I have included the PDF "Elevator_Prediction_System_Design.pdf" explaining my proposed solution. \ No newline at end of file diff --git a/elevator_system/demo.py b/elevator_system/demo.py new file mode 100644 index 0000000..b6d9fce --- /dev/null +++ b/elevator_system/demo.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +Elevator Prediction System Demo +This script demonstrates the ML-focused elevator system that captures the "golden event". +""" + +import requests +import time +import random + +BASE_URL = "http://localhost:5000" + +def call_elevator(floor, destination_floor=None): + """Call the elevator from a specific floor""" + data = {'floor': floor} + if destination_floor: + data['destination_floor'] = destination_floor + + response = requests.post(f"{BASE_URL}/call", json=data) + if response.status_code == 201: + return response.json() + else: + print(f"Error calling elevator: {response.text}") + return None + +def step_simulation(): + """Advance the simulation by one time unit""" + response = requests.post(f"{BASE_URL}/step") + if response.status_code == 200: + return response.json() + else: + print(f"Error stepping simulation: {response.text}") + return None + +def get_status(): + """Get current elevator status""" + response = requests.get(f"{BASE_URL}/status") + if response.status_code == 200: + return response.json() + else: + print(f"Error getting status: {response.text}") + return None + +def get_logs(): + """Get demand logs for ML training""" + response = requests.get(f"{BASE_URL}/logs") + if response.status_code == 200: + return response.json() + else: + print(f"Error getting logs: {response.text}") + return None + +def get_stats(): + """Get statistics for ML insights""" + response = requests.get(f"{BASE_URL}/stats") + if response.status_code == 200: + return response.json() + else: + print(f"Error getting stats: {response.text}") + return None + +def simulate_elevator_cycle(call_floor, destination_floor): + """Simulate a complete elevator cycle""" + print(f"\n=== Elevator Cycle ===") + print(f"Call from floor: {call_floor}") + print(f"Destination: {destination_floor}") + + # 1. Call the elevator + result = call_elevator(call_floor, destination_floor) + + # 2. Step simulation until elevator reaches call floor + current_floor = result['elevator_state']['current_floor'] # Get actual elevator position from state + print(f"Current floor: {current_floor}") + ini_current_floor = current_floor + # If elevator is already at call floor, we need one step to "arrive" there + if current_floor == call_floor: + step_result = step_simulation() + print(f"Step result (arriving at call floor)...") + current_floor = step_result['elevator_state']['current_floor'] + else: + # Step until elevator reaches call floor + while current_floor != call_floor: + step_result = step_simulation() + # print(f"Step result (until call floor): {step_result}") + if step_result and step_result['moved']: + current_floor = step_result['elevator_state']['current_floor'] + print(f"Moved to floor {current_floor} (until call floor)") + else: + # Elevator arrived at call floor + current_floor = step_result['elevator_state']['current_floor'] + if current_floor == call_floor: + print(f"Arrived at call floor {current_floor}") + break + + # 3. Step simulation until elevator reaches destination + # First, ensure we've properly arrived at the call floor + if current_floor == call_floor: + # Take one more step to ensure the elevator has processed the call floor arrival + step_result = step_simulation() + print(f"Step result (processing call floor arrival)...") + current_floor = step_result['elevator_state']['current_floor'] + if ini_current_floor == call_floor: + print(f"Moved to floor {current_floor} (until destination floor)") + # Now step until we reach the destination + while current_floor != destination_floor: + step_result = step_simulation() + # print(f"Step result (until destination floor): {step_result}") + if step_result and step_result['moved']: + current_floor = step_result['elevator_state']['current_floor'] + print(f"Moved to floor {current_floor} (until destination floor)") + else: + # Elevator arrived at destination or stopped + current_floor = step_result['elevator_state']['current_floor'] + if current_floor == destination_floor: + print(f"Arrived at destination floor {current_floor}") + break + + # 4. Final step to arrive and start resting + step_result = step_simulation() + if step_result: + print(f"Arrived and started resting") + + return current_floor + +def simulate_office_building(): + """Simulate a typical office building day""" + print("=== Office Building Elevator Simulation ===") + + floors = list(range(1, 11)) # 10 floors + + # Morning rush hour (8-9 AM) - people coming to work + print("\n--- Morning Rush Hour (8-9 AM) ---") + for i in range(6): + # Most people come from ground floor to various floors + destination_floor = random.choice(floors[1:]) # Not ground floor + simulate_elevator_cycle(1, destination_floor) + time.sleep(0.1) # Small delay between cycles + + # Mid-morning (9-11 AM) - some movement between floors + print("\n--- Mid-Morning (9-11 AM) ---") + for i in range(2): + call_floor = random.choice(floors) + destination_floor = random.choice([f for f in floors if f != call_floor]) + simulate_elevator_cycle(call_floor, destination_floor) + time.sleep(0.1) + + # Lunch time (12-1 PM) - people going to cafeteria (floor 2) + print("\n--- Lunch Time (12-1 PM) ---") + for i in range(4): + call_floor = random.choice(floors) + simulate_elevator_cycle(call_floor, 2) # Cafeteria floor + time.sleep(0.1) + + # After lunch (1-2 PM) - people returning to their floors + print("\n--- After Lunch (1-2 PM) ---") + for i in range(4): + destination_floor = random.choice(floors[1:]) # To various floors + simulate_elevator_cycle(2, destination_floor) # From cafeteria + time.sleep(0.1) + + # Afternoon (2-5 PM) - some movement + print("\n--- Afternoon (2-5 PM) ---") + for i in range(2): + call_floor = random.choice(floors) + destination_floor = random.choice([f for f in floors if f != call_floor]) + simulate_elevator_cycle(call_floor, destination_floor) + time.sleep(0.1) + + # Evening rush (5-6 PM) - people leaving work + print("\n--- Evening Rush (5-6 PM) ---") + for i in range(6): + call_floor = random.choice(floors[1:]) # From various floors + simulate_elevator_cycle(call_floor, 1) # To ground floor + time.sleep(0.1) + +def analyze_ml_data(): + """Analyze the collected data for ML insights""" + print("\n=== ML Data Analysis ===") + + # Get logs + logs_data = get_logs() + if not logs_data: + return + + logs = logs_data['logs'] + print(f"\nTotal golden events captured: {len(logs)}") + + if len(logs) == 0: + print("No data collected yet. Run some simulation first.") + return + + # Analyze resting floor patterns + resting_floor_counts = {} + call_floor_counts = {} + + for log in logs: + resting_floor = log['resting_floor'] + call_floor = log['call_floor'] + + resting_floor_counts[resting_floor] = resting_floor_counts.get(resting_floor, 0) + 1 + call_floor_counts[call_floor] = call_floor_counts.get(call_floor, 0) + 1 + + print("\nResting Floor Analysis:") + for floor, count in sorted(resting_floor_counts.items()): + print(f" Floor {floor}: {count} times") + + print("\nCall Floor Analysis:") + for floor, count in sorted(call_floor_counts.items()): + print(f" Floor {floor}: {count} calls") + + # Find most common call floor + if call_floor_counts: + most_common_call_floor = max(call_floor_counts, key=call_floor_counts.get) + print(f"\nMost common call floor: {most_common_call_floor} ({call_floor_counts[most_common_call_floor]} calls)") + + # Analyze time patterns + hour_counts = {} + for log in logs: + hour = log['hour_of_day'] + hour_counts[hour] = hour_counts.get(hour, 0) + 1 + + print("\nHourly Call Patterns:") + for hour in sorted(hour_counts.keys()): + print(f" {hour:02d}:00: {hour_counts[hour]} calls") + + # Get statistics + stats = get_stats() + if stats and stats['resting_patterns']: + print("\nResting → Call Patterns:") + for pattern in stats['resting_patterns'][:5]: # Show top 5 + print(f" Resting on {pattern['resting_floor']} → Call from {pattern['call_floor']}: {pattern['count']} times") + +def main(): + """Main demonstration function""" + print("Starting Elevator Prediction System Demo") + print("Make sure the system is running on http://localhost:5000") + + # Check if system is running + try: + status = get_status() + if status: + print("Elevator system is running") + print(f"Current logs: {status['total_logs']}") + else: + print("Elevator system is running (no activity yet)") + except requests.exceptions.ConnectionError: + print("Error: Cannot connect to elevator system") + print("Please start the system with: python main.py") + return + + # Run simulation + simulate_office_building() + + # Analyze data + analyze_ml_data() + + # Show final status + final_status = get_status() + if final_status: + print(f"\nFinal elevator state:") + elevator_state = final_status['elevator'] + print(f" Current floor: {elevator_state['current_floor']}") + print(f" Is resting: {elevator_state['is_resting']}") + print(f" Direction: {elevator_state['direction']}") + print(f" Total golden events: {final_status['total_logs']}") + + print("\nDemo completed!") + print("The collected data can now be used to train ML models for optimal resting floor prediction.") + print("\nKey ML Features Collected:") + print(" - resting_floor: Where elevator was idle") + print(" - call_floor: Where the next call came from") + print(" - timestamp_rested: When elevator became idle") + print(" - timestamp_called: When call was received") + print(" - day_of_week: Day of the week (0-6)") + print(" - hour_of_day: Hour of the day (0-23)") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/elevator_system/instance/elevator_prediction.db b/elevator_system/instance/elevator_prediction.db new file mode 100644 index 0000000..6ea5f22 Binary files /dev/null and b/elevator_system/instance/elevator_prediction.db differ diff --git a/elevator_system/main.py b/elevator_system/main.py new file mode 100644 index 0000000..5647c52 --- /dev/null +++ b/elevator_system/main.py @@ -0,0 +1,196 @@ +from flask import Flask, request, jsonify +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///elevator_prediction.db' +db = SQLAlchemy(app) + + +class DemandLog(db.Model): + """Single table for ML training data - captures the 'golden event'""" + id = db.Column(db.Integer, primary_key=True) + timestamp_rested = db.Column(db.DateTime, nullable=False) + timestamp_called = db.Column(db.DateTime, nullable=False) + resting_floor = db.Column(db.Integer, nullable=False) + call_floor = db.Column(db.Integer, nullable=False) + destination_floor = db.Column(db.Integer, nullable=True) + day_of_week = db.Column(db.Integer, nullable=False) + hour_of_day = db.Column(db.Integer, nullable=False) + + +class Elevator: + """Simple elevator simulation""" + def __init__(self): + self.current_floor = 1 + self.direction = 'idle' + self.is_resting = True + self.resting_since = None + self.destination_queue = [] + + def get_state(self): + return { + 'current_floor': self.current_floor, + 'direction': self.direction, + 'is_resting': self.is_resting, + 'resting_since': self.resting_since.isoformat() if self.resting_since else None, + 'destination_queue': self.destination_queue + } + + def start_resting(self, timestamp): + self.is_resting = True + self.direction = 'idle' + self.resting_since = timestamp + self.destination_queue = [] + + def receive_call(self, call_floor, destination_floor=None): + if self.is_resting and self.resting_since: + now = datetime.now() + day_of_week = now.weekday() + hour_of_day = now.hour + + log_entry = DemandLog( + timestamp_rested=self.resting_since, + timestamp_called=now, + resting_floor=self.current_floor, + call_floor=call_floor, + destination_floor=destination_floor, + day_of_week=day_of_week, + hour_of_day=hour_of_day + ) + db.session.add(log_entry) + db.session.commit() + + if call_floor not in self.destination_queue: + self.destination_queue.append(call_floor) + if destination_floor and destination_floor not in self.destination_queue: + self.destination_queue.append(destination_floor) + + self.is_resting = False + self.resting_since = None + + def step(self): + if not self.destination_queue: + return False + + next_floor = self.destination_queue[0] + + if self.current_floor < next_floor: + self.direction = 'up' + self.current_floor += 1 + elif self.current_floor > next_floor: + self.direction = 'down' + self.current_floor -= 1 + else: + # Arrived at destination + self.destination_queue.pop(0) + # Stop here for one step to simulate arrival + if not self.destination_queue: + # No more destinations, start resting + self.start_resting(datetime.now()) + # Always return False when arriving at a destination + # This simulates the elevator stopping to pick up/drop of passengers + return False + + return True + + def reset(self): + """Reset the elevator to its initial state""" + self.current_floor = 1 + self.direction = 'idle' + self.is_resting = True + self.resting_since = datetime.now() + self.destination_queue = [] + +elevator = Elevator() + + +@app.route('/call', methods=['POST']) +def call_elevator(): + data = request.get_json() + + if not data or 'floor' not in data: + return jsonify({'error': 'Floor is required'}), 400 + + call_floor = data['floor'] + destination_floor = data.get('destination_floor') + + elevator.receive_call(call_floor, destination_floor) + + return jsonify({ + 'message': 'Call received', + 'call_floor': call_floor, + 'destination_floor': destination_floor, + 'elevator_state': elevator.get_state() + }), 201 + + +@app.route('/step', methods=['POST']) +def step_simulation(): + moved = elevator.step() + + return jsonify({ + 'message': 'Simulation stepped', + 'moved': moved, + 'elevator_state': elevator.get_state() + }) + + +@app.route('/status', methods=['GET']) +def get_status(): + return jsonify({ + 'elevator': elevator.get_state(), + 'total_logs': DemandLog.query.count() + }) + + +@app.route('/logs', methods=['GET']) +def get_logs(): + limit = request.args.get('limit', 100, type=int) + logs = DemandLog.query.order_by(DemandLog.timestamp_called.desc()).limit(limit).all() + + return jsonify({ + 'logs': [{ + 'id': log.id, + 'timestamp_rested': log.timestamp_rested.isoformat(), + 'timestamp_called': log.timestamp_called.isoformat(), + 'resting_floor': log.resting_floor, + 'call_floor': log.call_floor, + 'destination_floor': log.destination_floor, + 'day_of_week': log.day_of_week, + 'hour_of_day': log.hour_of_day, + 'idle_time_seconds': (log.timestamp_called - log.timestamp_rested).total_seconds() + } for log in logs] + }) + + +@app.route('/stats', methods=['GET']) +def get_stats(): + resting_patterns = db.session.query( + DemandLog.resting_floor, + DemandLog.call_floor, + db.func.count(DemandLog.id).label('count') + ).group_by( + DemandLog.resting_floor, + DemandLog.call_floor + ).order_by( + DemandLog.resting_floor, + db.func.count(DemandLog.id).desc() + ).all() + + return jsonify({ + 'resting_patterns': [ + { + 'resting_floor': r.resting_floor, + 'call_floor': r.call_floor, + 'count': r.count + } for r in resting_patterns + ] + }) + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + elevator.start_resting(datetime.now()) + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/elevator_system/requirements.txt b/elevator_system/requirements.txt new file mode 100644 index 0000000..daaffa5 --- /dev/null +++ b/elevator_system/requirements.txt @@ -0,0 +1,5 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +pytest==7.4.3 +pytest-flask==1.3.0 +requests==2.31.0 \ No newline at end of file diff --git a/elevator_system/schema.sql b/elevator_system/schema.sql new file mode 100644 index 0000000..5de27c8 --- /dev/null +++ b/elevator_system/schema.sql @@ -0,0 +1,60 @@ +-- Elevator Prediction System Database Schema +-- Single table design focused on the "Golden Event" for ML training + +CREATE TABLE demand_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp_rested DATETIME NOT NULL, -- When elevator became idle + timestamp_called DATETIME NOT NULL, -- When next call was received + resting_floor INTEGER NOT NULL, -- Floor elevator was resting on + call_floor INTEGER NOT NULL, -- Floor the call came from + destination_floor INTEGER, -- Where user wants to go + day_of_week INTEGER NOT NULL, -- 0-6 (Monday-Sunday) + hour_of_day INTEGER NOT NULL -- 0-23 +); + +-- Create indexes for better query performance +CREATE INDEX idx_demand_log_timestamp_rested ON demand_log(timestamp_rested); +CREATE INDEX idx_demand_log_timestamp_called ON demand_log(timestamp_called); +CREATE INDEX idx_demand_log_resting_floor ON demand_log(resting_floor); +CREATE INDEX idx_demand_log_call_floor ON demand_log(call_floor); +CREATE INDEX idx_demand_log_time_features ON demand_log(day_of_week, hour_of_day); + +-- Example queries for ML data extraction: + +-- 1. Get all training data (features and labels) +SELECT + resting_floor, + day_of_week, + hour_of_day, + destination_floor, + call_floor as label +FROM demand_log +ORDER BY timestamp_called DESC; + +-- 2. Find most common call floor for each resting floor +SELECT + resting_floor, + call_floor, + COUNT(*) as frequency +FROM demand_log +GROUP BY resting_floor, call_floor +ORDER BY resting_floor, frequency DESC; + +-- 3. Analyze time patterns +SELECT + day_of_week, + hour_of_day, + call_floor, + COUNT(*) as call_count +FROM demand_log +GROUP BY day_of_week, hour_of_day, call_floor +ORDER BY day_of_week, hour_of_day, call_count DESC; + +-- 4. Calculate idle time statistics +SELECT + resting_floor, + AVG((julianday(timestamp_called) - julianday(timestamp_rested)) * 24 * 60) as avg_idle_minutes, + COUNT(*) as total_events +FROM demand_log +GROUP BY resting_floor +ORDER BY avg_idle_minutes DESC; \ No newline at end of file diff --git a/elevator_system/test_elevator.py b/elevator_system/test_elevator.py new file mode 100644 index 0000000..1df3174 --- /dev/null +++ b/elevator_system/test_elevator.py @@ -0,0 +1,274 @@ +import pytest +import json +from main import app, db + + +@pytest.fixture +def client(): + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + + with app.test_client() as client: + with app.app_context(): + # Drop all tables and recreate them + db.drop_all() + db.create_all() + + # Reset elevator to initial state + from main import elevator + elevator.reset() + yield client + + +def test_call_elevator(client): + """Test calling the elevator""" + response = client.post('/call', json={'floor': 5, 'destination_floor': 10}) + + assert response.status_code == 201 + data = json.loads(response.data) + assert data['message'] == 'Call received' + assert data['call_floor'] == 5 + assert data['destination_floor'] == 10 + + +def test_call_elevator_missing_floor(client): + """Test calling elevator without floor""" + response = client.post('/call', json={}) + assert response.status_code == 400 + + +def test_step_simulation(client): + """Test stepping the simulation""" + # First call the elevator + client.post('/call', json={'floor': 5}) + + # Then step the simulation + response = client.post('/step') + assert response.status_code == 200 + + data = json.loads(response.data) + assert data['message'] == 'Simulation stepped' + assert data['moved'] == True + + +def test_get_status(client): + """Test getting elevator status""" + response = client.get('/status') + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'elevator' in data + assert 'total_logs' in data + assert data['total_logs'] == 0 # No logs yet + + +def test_golden_event_logging(client): + """Test that the golden event is logged when elevator is resting""" + # Elevator starts resting + response = client.get('/status') + data = json.loads(response.data) + assert data['elevator']['is_resting'] == True + + # Call the elevator + client.post('/call', json={'floor': 5, 'destination_floor': 10}) + + # Check that a log entry was created + response = client.get('/logs') + data = json.loads(response.data) + assert len(data['logs']) == 1 + + log = data['logs'][0] + assert log['resting_floor'] == 1 # Started at floor 1 + assert log['call_floor'] == 5 + assert log['destination_floor'] == 10 + assert log['day_of_week'] >= 0 and log['day_of_week'] <= 6 + assert log['hour_of_day'] >= 0 and log['hour_of_day'] <= 23 + + +def test_no_logging_when_not_resting(client): + """Test that no log is created when elevator is not resting""" + # Call elevator (this will log the first call) + client.post('/call', json={'floor': 5}) + + # Step only 2 times - elevator is still moving (at floor 3) + for _ in range(2): + client.post('/step') + + # Call again while elevator is actually moving (not resting) + client.post('/call', json={'floor': 3}) + + # Check logs - should only have 1 entry (the first call) + response = client.get('/logs') + data = json.loads(response.data) + assert len(data['logs']) == 1 + + # Verify the logs + logs = data['logs'] + assert logs[0]['call_floor'] == 5 # First call + + +def test_no_logging_during_movement(client): + """Test that no log is created when elevator is actively moving""" + # Call elevator (this will log the first call) + client.post('/call', json={'floor': 5}) + + # Step once - elevator is moving (at floor 2) + client.post('/step') + + # Verify elevator is not resting + response = client.get('/status') + data = json.loads(response.data) + assert data['elevator']['is_resting'] == False + + # Call again while elevator is definitely moving + client.post('/call', json={'floor': 3}) + + # Check logs - should only have 1 entry (the first call) + response = client.get('/logs') + data = json.loads(response.data) + assert len(data['logs']) == 1 + + +def test_complete_elevator_cycle(client): + """Test a complete elevator cycle with logging""" + # 1. Elevator is resting at floor 1 + response = client.get('/status') + data = json.loads(response.data) + assert data['elevator']['is_resting'] == True + assert data['elevator']['current_floor'] == 1 + + # 2. Call from floor 5 to floor 10 + client.post('/call', json={'floor': 5, 'destination_floor': 10}) + + # 3. Step until elevator reaches floor 5 + for i in range(4): + response = client.post('/step') + data = json.loads(response.data) + print(f"Step {i+1}: moved={data['moved']}, floor={data['elevator_state']['current_floor']}") + assert data['moved'] == True + + # 4. Arrive at floor 5 (should stop) + response = client.post('/step') + data = json.loads(response.data) + print(f"Pick up passengers: moved={data['moved']}, floor={data['elevator_state']['current_floor']}") + assert data['moved'] == False # Stopped at floor 5 to pick up passengers + + # 5. Step until elevator reaches floor 10 + for i in range(5): + response = client.post('/step') + data = json.loads(response.data) + print(f"Step {i+5}: moved={data['moved']}, floor={data['elevator_state']['current_floor']}") + assert data['moved'] == True + + # 6. Arrive at floor 10 and start resting + response = client.post('/step') + data = json.loads(response.data) + print(f"Final step: moved={data['moved']}, floor={data['elevator_state']['current_floor']}") + assert data['moved'] == False # No more destinations. Stopped and resting + + # 7. Check that elevator is resting again + response = client.get('/status') + data = json.loads(response.data) + assert data['elevator']['is_resting'] == True + assert data['elevator']['current_floor'] == 10 + + # 8. Check that we have one log entry + response = client.get('/logs') + data = json.loads(response.data) + assert len(data['logs']) == 1 + + +def test_get_stats(client): + """Test getting statistics""" + # Create some test data + client.post('/call', json={'floor': 5, 'destination_floor': 10}) + client.post('/step') # Move to floor 2 + client.post('/step') # Move to floor 3 + client.post('/step') # Move to floor 4 + client.post('/step') # Move to floor 5 + client.post('/step') # Move to floor 6 + client.post('/step') # Move to floor 7 + client.post('/step') # Move to floor 8 + client.post('/step') # Move to floor 9 + client.post('/step') # Move to floor 10 + client.post('/step') # Arrive and start resting + + # Now call from floor 3 + client.post('/call', json={'floor': 3, 'destination_floor': 7}) + + response = client.get('/stats') + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'resting_patterns' in data + assert len(data['resting_patterns']) > 0 + + +def test_ml_training_data_generation(client): + """Test that the system generates proper ML training data""" + # Simulate multiple elevator cycles to generate training data + floors = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # Morning pattern: many calls from floor 1 + for i in range(5): + # Ensure elevator is resting before calling + response = client.get('/status') + data = json.loads(response.data) + if not data['elevator']['is_resting']: + # Wait for elevator to finish current journey + while not data['elevator']['is_resting']: + client.post('/step') + response = client.get('/status') + data = json.loads(response.data) + + # Call from floor 1 to various floors + client.post('/call', json={'floor': 1, 'destination_floor': floors[i % len(floors)]}) + + # Move elevator to destination + for _ in range(floors[i % len(floors)] - 1): + client.post('/step') + client.post('/step') # Arrive and rest + + # Afternoon pattern: calls from middle floors + for i in range(3): + # Ensure elevator is resting before calling + response = client.get('/status') + data = json.loads(response.data) + if not data['elevator']['is_resting']: + # Wait for elevator to finish current journey + while not data['elevator']['is_resting']: + client.post('/step') + response = client.get('/status') + data = json.loads(response.data) + + call_floor = floors[2 + (i % 6)] # Floors 3-8 + client.post('/call', json={'floor': call_floor, 'destination_floor': 1}) + + # Move elevator to destination + for _ in range(abs(call_floor - 1)): + client.post('/step') + client.post('/step') # Arrive and rest + + # Get logs for ML training + response = client.get('/logs') + data = json.loads(response.data) + + # Verify we have meaningful training data + assert len(data['logs']) == 8 # 5 morning + 3 afternoon calls + + # Check that floor 1 has the most calls (morning rush) + floor_1_calls = sum(1 for log in data['logs'] if log['call_floor'] == 1) + assert floor_1_calls >= 5 # At least 5 calls from floor 1 + + # Verify all logs have proper ML features + for log in data['logs']: + assert 'resting_floor' in log + assert 'call_floor' in log + assert 'day_of_week' in log + assert 'hour_of_day' in log + assert 'idle_time_seconds' in log + assert log['idle_time_seconds'] > 0 + + +if __name__ == '__main__': + pytest.main([__file__]) \ No newline at end of file