From 799bf8da4838d775f4a29c4892b055c21f4bbba6 Mon Sep 17 00:00:00 2001 From: Macphail Date: Thu, 22 May 2025 21:14:20 +0300 Subject: [PATCH 01/11] init point --- readme.md | 64 +++++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/readme.md b/readme.md index ea5e444..42741db 100644 --- a/readme.md +++ b/readme.md @@ -1,58 +1,32 @@ -# Dev Test +# Elevator resting floor prediction model solution -## Elevators -When an elevator is empty and not moving this is known as it's resting floor. -The ideal resting floor to be positioned on depends on the likely next floor that the elevator will be called from. +## Problem statement: +### From my understanding were trying to predict the best possible floor the elevator should rest on according to certain factors such as time of day, week, month, season, etc in order to anticipate which floor demand will possibly come from next so we can provide the best possible and efficient service for users in that particular building -We can build a prediction engine to predict the likely next floor based on historical demand, if we have the data. +## Prediction target: +### In order to not only predict the next possible floor at a particular time but provide probabilitiies for all floors at a particular time, i would go for a mutliclass outcome, i think this would allow the elevator to be adjusted to a certain level of accuracy as desired by users -The goal of this project is to model an elevator and save the data that could later be used to build a prediction engine for which floor is the best resting floor at any time -- When people call an elevator this is considered a demand -- When the elevator is vacant and not moving between floors, the current floor is considered its resting floor -- When the elevator is vacant, it can stay at the current position or move to a different floor -- The prediction model will determine what is the best floor to rest on +## What factors could possibly influence the demand of the elevator (both obvious and non obvious) +- ### Time of day: + #### We might find that there is a higher demand on certain floors in the mornings, lunch or evenings, e,g a working building where people need the elevator closer to ground floors as people come into work and closer to certain floors as people leave work or go out for lunch -_The requirement isn't to complete this system but to start building a system that would feed into the training and prediction -of an ML system_ +- ### Day of the week + #### We might find that more people come into work on monday mornings as compared to friday, if the building is multipurpose, certain floors might have working people coming in while other days house residents and thus less activity on certain floors +- ### Week of the month + #### We might discover that certain weeks of the month have more people/traffic as compared to other weeks, e.g retreats, field work weeks, etc +- ### Maintainance schedule that week (if any) + #### This is one of those non obvious factors, we might discover that an elevator that is not regularly maintained is less trusted by users and so they decide maybe to take the stairs, only users that need to travel safer distances might prefer it, e.g a floor above the ground floor, this would also help avoid overfitting and help our model generalize better +- ### Current weather season +- #### We might find that during winter, christmas or summer, users might not prefer the elevator, or certain floors experience more demand -You will need to talk through your approach, how you modelled the data and why you thought that data was important, provide endpoints to collect the data and -a means to store the data. Testing is important and will be used verify your system -## A note on AI generated code -This project isn't about writing code, AI can and will do that for you. -The next step in this process is to talk through your solution and the decisions you made to come to them. It makes for an awkward and rather boring interview reviewing chatgpt's solution. +### Data access +#### Next i need to understand what historical data can actually be collected and fed into our system during training as well as the kind of data that the system will have access to during actual live prediction +#### Data such as time of day, day of week, week of month, maintainance schedule and current weather season are available natively to our system and can be enhanced by installing addidtional packages. -If you use a tool to help you write code, that's fine, but we want to see _your_ thought process. -Provided under the chatgpt folder is the response you get back from chat4o. -If your intention isn't to complete the project but to get an AI to spec it for you please, feel free to submit this instead of wasting OpenAI's server resources. -## Problem statement recap -This is a domain modeling problem to build a fit for purpose data storage with a focus on ai data ingestion -- Model the problem into a storage schema (SQL DB schema or whatever you prefer) -- CRUD some data -- Add some flair with a business rule or two -- Have the data in a suitable format to feed to a prediction training algorithm ---- -#### To start -- Fork this repo and begin from there -- For your submission, PR into the main repo. We will review it, a offer any feedback and give you a pass / fail if it passes PR -- Don't spend more than 4 hours on this. Projects that pass PR are paid at the standard hourly rate - -#### Marking -- You will be marked on how well your tests cover the code and how useful they would be in a prod system -- You will need to provide storage of some sort. This could be as simple as a sqlite or as complicated as a docker container with a migrations file -- Solutions will be marked against the position you are applying for, a Snr Dev will be expected to have a nearly complete solution and to have thought out the domain and built a schema to fit any issues that could arise -A Jr. dev will be expected to provide a basic design and understand how ML systems like to ingest data - - -#### Trip-ups from the past -Below is a list of some things from previous submissions that haven't worked out -- Built a prediction engine -- Built a full website with bells and whistles -- Spent more than the time allowed (you won't get bonus points for creating an intricate solution, we want a fit for purpose solution) -- Overcomplicated the system mentally and failed to start From 58ccb20b2a3abb653b9c69b8c588ae5b3cd11d41 Mon Sep 17 00:00:00 2001 From: Macphail Date: Sun, 25 May 2025 13:32:49 +0300 Subject: [PATCH 02/11] add api endpoints, models and business logic --- chatgpt/__pycache__/main.cpython-312.pyc | Bin 0 -> 23235 bytes chatgpt/instance/elevator_data.db | Bin 0 -> 36864 bytes chatgpt/main.py | 433 +++++++++++++++++++++-- chatgpt/requirements.txt | 8 +- 4 files changed, 414 insertions(+), 27 deletions(-) create mode 100644 chatgpt/__pycache__/main.cpython-312.pyc create mode 100644 chatgpt/instance/elevator_data.db diff --git a/chatgpt/__pycache__/main.cpython-312.pyc b/chatgpt/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29a0c20d0dd7fbfb5f146f68a9c9b20246d60361 GIT binary patch literal 23235 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 PFZAteI!a$o7SsO&u<4IV literal 0 HcmV?d00001 diff --git a/chatgpt/instance/elevator_data.db b/chatgpt/instance/elevator_data.db new file mode 100644 index 0000000000000000000000000000000000000000..41f643cc82ea54305888af2a1304639b989ef7b7 GIT binary patch literal 36864 zcmeI)D+)qU5C-76z6C|A?M9Qu4Y&Y{7|aF*g9vhQ4@Nhs+i*@W8ZGh-{G5UFfb%V2 zwwv{-In>9?^}KKDWlW>0VpZ1>)hr^0nO^e~009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1pZi{pMNVe{>r?+ z(swgu)?Yi*$|wQ^2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ ifB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{hXQY str: + if month in [12, 1, 2]: + return 'winter' + elif month in [3, 4, 5]: + return 'spring' + elif month in [6, 7, 8]: + return 'summer' + else: + return 'autumn' +class ElevatorMovement(db.Model): + __tablename__ = 'elevator_movements' + + id = db.Column(db.Integer, primary_key=True) + elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) + from_floor = db.Column(db.Integer, nullable=False) + to_floor = db.Column(db.Integer, nullable=False) + movement_start = db.Column(db.DateTime, default=datetime.utcnow) + movement_end = db.Column(db.DateTime) + movement_type = db.Column(db.String(20)) + load_before = db.Column(db.Integer, default=0) + load_after = db.Column(db.Integer, default=0) -class ElevatorState(db.Model): +class FloorDemandSummary(db.Model): + __tablename__ = 'floor_demand_summary' + id = db.Column(db.Integer, primary_key=True) - timestamp = db.Column(db.DateTime, default=datetime.utcnow) - floor = db.Column(db.Integer, nullable=False) - vacant = db.Column(db.Boolean, nullable=False) + building_id = db.Column(db.Integer, db.ForeignKey('buildings.id'), nullable=False) + floor_number = db.Column(db.Integer, nullable=False) + date = db.Column(db.Date, nullable=False) + hour = db.Column(db.Integer, nullable=False) + call_count = db.Column(db.Integer, default=0) + avg_response_time = db.Column(db.Float) + peak_load_time = db.Column(db.Time) + created_at = db.Column(db.DateTime, default=datetime.utcnow) +class ElevatorBusinessRules: + @staticmethod + def validate_floor_range(floor: int, building_id: int) -> bool: + building = Building.query.get(building_id) + if not building: + return False + return 1 <= floor <= building.total_floors + + @staticmethod + def calculate_response_time(call_time: datetime, elevator_position: int, called_floor: int) -> float: + floors_to_travel = abs(elevator_position - called_floor) + return floors_to_travel * 3.0 + 2.0 + + @staticmethod + def should_trigger_maintenance_alert(elevator: Elevator) -> bool: + if not elevator.last_maintenance: + return True + days_since_maintenance = (datetime.utcnow() - elevator.last_maintenance).days + return days_since_maintenance > 30 -@app.route('/demand', methods=['POST']) -def create_demand(): +@app.route('/api/buildings', methods=['POST']) +def create_building(): + """Create a new building""" data = request.get_json() - new_demand = ElevatorDemand(floor=data['floor']) - db.session.add(new_demand) - db.session.commit() - return jsonify({'message': 'Demand created'}), 201 + + if not data or 'name' not in data or 'total_floors' not in data: + return jsonify({'error': 'Missing required fields: name, total_floors'}), 400 + + building = Building( + name=data['name'], + total_floors=data['total_floors'], + building_type=data.get('building_type', 'mixed') + ) + + try: + db.session.add(building) + db.session.commit() + return jsonify({ + 'id': building.id, + 'name': building.name, + 'total_floors': building.total_floors, + 'building_type': building.building_type + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 +@app.route('/api/buildings//elevators', methods=['POST']) +def create_elevator(building_id): + """Add an elevator to a building""" + building = Building.query.get_or_404(building_id) + data = request.get_json() + + if not data or 'elevator_number' not in data: + return jsonify({'error': 'Missing required field: elevator_number'}), 400 + + elevator = Elevator( + building_id=building_id, + elevator_number=data['elevator_number'], + max_capacity=data.get('max_capacity', 1000), + current_floor=data.get('current_floor', 1) + ) + + try: + db.session.add(elevator) + db.session.commit() + return jsonify({ + 'id': elevator.id, + 'elevator_number': elevator.elevator_number, + 'current_floor': elevator.current_floor, + 'building_id': building_id + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 -@app.route('/state', methods=['POST']) -def create_state(): +@app.route('/api/elevators//call', methods=['POST']) +def record_elevator_call(elevator_id): + """Record an elevator call (demand event)""" + elevator = Elevator.query.get_or_404(elevator_id) data = request.get_json() - new_state = ElevatorState(floor=data['floor'], vacant=data['vacant']) - db.session.add(new_state) - db.session.commit() - return jsonify({'message': 'State created'}), 201 + + if not data or 'called_from_floor' not in data: + return jsonify({'error': 'Missing required field: called_from_floor'}), 400 + + called_from_floor = data['called_from_floor'] + + # Business rule validation + if not ElevatorBusinessRules.validate_floor_range(called_from_floor, elevator.building_id): + return jsonify({'error': 'Invalid floor number'}), 400 + + # Calculate response time + response_time = ElevatorBusinessRules.calculate_response_time( + datetime.utcnow(), elevator.current_floor, called_from_floor + ) + + call = ElevatorCall( + elevator_id=elevator_id, + called_from_floor=called_from_floor, + destination_floor=data.get('destination_floor'), + elevator_position_at_call=elevator.current_floor, + response_time=response_time, + estimated_passengers=data.get('estimated_passengers', 1), + call_type=data.get('call_type', 'normal'), + weather_condition=data.get('weather_condition') + ) + + try: + db.session.add(call) + db.session.commit() + + # Update elevator position if destination provided + if data.get('destination_floor'): + elevator.current_floor = data['destination_floor'] + elevator.is_moving = False + db.session.commit() + + return jsonify({ + 'call_id': call.id, + 'estimated_response_time': response_time, + 'call_time': call.call_time.isoformat(), + 'temporal_features': { + 'day_of_week': call.day_of_week, + 'hour_of_day': call.hour_of_day, + 'week_of_month': call.week_of_month, + 'season': call.season + } + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@app.route('/api/elevators//move', methods=['POST']) +def record_elevator_movement(elevator_id): + """Record elevator movement (repositioning)""" + elevator = Elevator.query.get_or_404(elevator_id) + data = request.get_json() + + if not data or 'to_floor' not in data: + return jsonify({'error': 'Missing required field: to_floor'}), 400 + + to_floor = data['to_floor'] + + if not ElevatorBusinessRules.validate_floor_range(to_floor, elevator.building_id): + return jsonify({'error': 'Invalid floor number'}), 400 + + movement = ElevatorMovement( + elevator_id=elevator_id, + from_floor=elevator.current_floor, + to_floor=to_floor, + movement_type=data.get('movement_type', 'repositioning'), + load_before=elevator.current_load, + load_after=data.get('load_after', elevator.current_load) + ) + + try: + db.session.add(movement) + + # Update elevator state + elevator.current_floor = to_floor + elevator.current_load = data.get('load_after', elevator.current_load) + elevator.is_moving = False + + db.session.commit() + + return jsonify({ + 'movement_id': movement.id, + 'from_floor': movement.from_floor, + 'to_floor': movement.to_floor, + 'elevator_current_floor': elevator.current_floor + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@app.route('/api/elevators//status', methods=['GET']) +def get_elevator_status(elevator_id): + """Get current elevator status""" + elevator = Elevator.query.get_or_404(elevator_id) + + # Check maintenance status + maintenance_alert = ElevatorBusinessRules.should_trigger_maintenance_alert(elevator) + + return jsonify({ + 'elevator_id': elevator.id, + 'elevator_number': elevator.elevator_number, + 'current_floor': elevator.current_floor, + 'current_load': elevator.current_load, + 'is_moving': elevator.is_moving, + 'maintenance_status': elevator.maintenance_status, + 'maintenance_alert': maintenance_alert, + 'building_id': elevator.building_id, + 'max_floors': elevator.building.total_floors + }) + +@app.route('/api/ml-data/features/', methods=['GET']) +def get_ml_features(building_id): + """Get data formatted for ML training""" + building = Building.query.get_or_404(building_id) + + days_back = request.args.get('days_back', 30, type=int) + include_weather = request.args.get('include_weather', False, type=bool) + + cutoff_date = datetime.utcnow() - timedelta(days=days_back) + + calls_query = db.session.query(ElevatorCall).join(Elevator).filter( + Elevator.building_id == building_id, + ElevatorCall.call_time >= cutoff_date + ).all() + + features = [] + for call in calls_query: + feature_row = { + 'target_floor': call.called_from_floor, + 'time_features': { + 'hour_of_day': call.hour_of_day, + 'day_of_week': call.day_of_week, + 'week_of_month': call.week_of_month, + 'season': call.season + }, + 'elevator_features': { + 'elevator_position_at_call': call.elevator_position_at_call, + 'estimated_passengers': call.estimated_passengers, + 'call_type': call.call_type + }, + 'contextual_features': { + 'response_time': call.response_time, + 'building_type': building.building_type, + 'total_floors': building.total_floors + } + } + + if include_weather and call.weather_condition: + feature_row['contextual_features']['weather_condition'] = call.weather_condition + + features.append(feature_row) + + return jsonify({ + 'building_id': building_id, + 'total_samples': len(features), + 'date_range': { + 'from': cutoff_date.isoformat(), + 'to': datetime.utcnow().isoformat() + }, + 'features': features + }) + +@app.route('/api/analytics/demand-patterns/', methods=['GET']) +def get_demand_patterns(building_id): + """Get demand patterns for analysis""" + building = Building.query.get_or_404(building_id) + + demand_query = db.session.query( + ElevatorCall.called_from_floor, + ElevatorCall.hour_of_day, + ElevatorCall.day_of_week, + db.func.count(ElevatorCall.id).label('call_count'), + db.func.avg(ElevatorCall.response_time).label('avg_response_time') + ).join(Elevator).filter( + Elevator.building_id == building_id + ).group_by( + ElevatorCall.called_from_floor, + ElevatorCall.hour_of_day, + ElevatorCall.day_of_week + ).all() + + patterns = [] + for row in demand_query: + patterns.append({ + 'floor': row.called_from_floor, + 'hour': row.hour_of_day, + 'day_of_week': row.day_of_week, + 'call_count': row.call_count, + 'avg_response_time': round(row.avg_response_time, 2) if row.avg_response_time else None + }) + + return jsonify({ + 'building_id': building_id, + 'demand_patterns': patterns + }) + +# Health check endpoint +@app.route('/health', methods=['GET']) +def health_check(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.utcnow().isoformat(), + 'database': 'connected' + }) + +_initialized = False +@app.before_request +def before_request(): + global _initialized + if not _initialized: + db.create_all() + + # Create sample data if none exists + if Building.query.count() == 0: + sample_building = Building( + name="Sample Office Building", + total_floors=10, + building_type="office" + ) + db.session.add(sample_building) + db.session.commit() + + sample_elevator = Elevator( + building_id=sample_building.id, + elevator_number="ELV-01", + current_floor=1 + ) + db.session.add(sample_elevator) + db.session.commit() + + _initialized = True if __name__ == '__main__': - db.create_all() - app.run(debug=True) + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/chatgpt/requirements.txt b/chatgpt/requirements.txt index 14d1bb0..0397b32 100644 --- a/chatgpt/requirements.txt +++ b/chatgpt/requirements.txt @@ -1,4 +1,6 @@ -Flask==2.0.2 -Flask-SQLAlchemy==2.5.1 +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +SQLAlchemy==2.0.21 +Werkzeug==2.3.7 pytest==6.2.5 -pytest-flask==1.2.0 +pytest-flask==1.2.0 \ No newline at end of file From aba55cd75647353540c0cba531be9aabac755ae4 Mon Sep 17 00:00:00 2001 From: Macphail Date: Mon, 26 May 2025 22:06:58 +0300 Subject: [PATCH 03/11] Remove __pycache__ from git tracking --- chatgpt/__pycache__/main.cpython-312.pyc | Bin 23235 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 chatgpt/__pycache__/main.cpython-312.pyc diff --git a/chatgpt/__pycache__/main.cpython-312.pyc b/chatgpt/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 29a0c20d0dd7fbfb5f146f68a9c9b20246d60361..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23235 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 PFZAteI!a$o7SsO&u<4IV From 70430aad31b7bc5322907772cf2aca3f480347ba Mon Sep 17 00:00:00 2001 From: Macphail Date: Mon, 26 May 2025 22:07:34 +0300 Subject: [PATCH 04/11] added tests --- .gitignore | 46 +++ chatgpt/app_tests.py | 559 +++++++++++++++++++++++++++++- chatgpt/instance/elevator_data.db | Bin 36864 -> 36864 bytes chatgpt/main.py | 131 +------ readme.md | 52 ++- 5 files changed, 663 insertions(+), 125 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e453be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment +.env +.venv +env/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Database +*.sqlite3 +*.db + diff --git a/chatgpt/app_tests.py b/chatgpt/app_tests.py index 258a8a6..622e52f 100644 --- a/chatgpt/app_tests.py +++ b/chatgpt/app_tests.py @@ -1,10 +1,553 @@ -def test_create_demand(client): - response = client.post('/demand', json={'floor': 3}) - assert response.status_code == 201 - assert response.get_json() == {'message': 'Demand created'} +import unittest +import json +import tempfile +import os +from datetime import datetime, timedelta +from main import app, db, Building, Elevator, ElevatorCall, ElevatorBusinessRules +class ElevatorSystemTestCase(unittest.TestCase): + def setUp(self): + """Set up test fixtures before each test method.""" + self.db_fd, app.config['DATABASE'] = tempfile.mkstemp() + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE'] + app.config['TESTING'] = True + app.config['WTF_CSRF_ENABLED'] = False + + self.app = app.test_client() + self.app_context = app.app_context() + self.app_context.push() + + db.create_all() + + # Create test data + self.building = Building( + name="Test Building", + total_floors=5, + building_type="office" + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="TEST-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() -def test_create_state(client): - response = client.post('/state', json={'floor': 5, 'vacant': True}) - assert response.status_code == 201 - assert response.get_json() == {'message': 'State created'} + def tearDown(self): + """Clean up after each test method.""" + db.session.remove() + db.drop_all() + self.app_context.pop() + os.close(self.db_fd) + os.unlink(app.config['DATABASE']) + + def test_health_check(self): + """Test the health check endpoint.""" + response = self.app.get('/health') + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['status'], 'healthy') + self.assertIn('timestamp', data) + + def test_create_building(self): + """Test building creation.""" + building_data = { + 'name': 'New Test Building', + 'total_floors': 5, + 'building_type': 'residential' + } + + response = self.app.post('/api/buildings', + data=json.dumps(building_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertEqual(data['name'], 'New Test Building') + self.assertEqual(data['total_floors'], 5) + self.assertEqual(data['building_type'], 'residential') + + def test_create_building_missing_data(self): + """Test building creation with missing required fields.""" + building_data = {'name': 'Incomplete Building'} # Missing total_floors + + response = self.app.post('/api/buildings', + data=json.dumps(building_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertIn('error', data) + + def test_create_elevator(self): + """Test elevator creation.""" + elevator_data = { + 'elevator_number': 'TEST-02', + 'max_capacity': 1200, + 'current_floor': 3 + } + + response = self.app.post(f'/api/buildings/{self.building.id}/elevators', + data=json.dumps(elevator_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertEqual(data['elevator_number'], 'TEST-02') + self.assertEqual(data['current_floor'], 3) + + def test_record_elevator_call(self): + """Test recording an elevator call.""" + call_data = { + 'called_from_floor': 3, + 'destination_floor': 1, + 'estimated_passengers': 2, + 'call_type': 'normal' + } + + response = self.app.post(f'/api/elevators/{self.elevator.id}/call', + data=json.dumps(call_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertIn('call_id', data) + self.assertIn('estimated_response_time', data) + self.assertIn('temporal_features', data) + + # Verify temporal features are populated + temporal = data['temporal_features'] + self.assertIn('day_of_week', temporal) + self.assertIn('hour_of_day', temporal) + self.assertIn('season', temporal) + + def test_record_elevator_call_invalid_floor(self): + """Test recording call with invalid floor.""" + call_data = { + 'called_from_floor': 6, # Building only has 5 floors + 'destination_floor': 1 + } + + response = self.app.post(f'/api/elevators/{self.elevator.id}/call', + data=json.dumps(call_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertIn('Invalid floor number', data['error']) + + + def test_get_elevator_status(self): + """Test getting elevator status.""" + response = self.app.get(f'/api/elevators/{self.elevator.id}/status') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['elevator_id'], self.elevator.id) + self.assertEqual(data['current_floor'], 1) + self.assertIn('maintenance_alert', data) + + def test_get_ml_features(self): + """Test ML features endpoint.""" + # Create some test calls first + call1 = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=2, + elevator_position_at_call=1 + ) + call2 = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=3, + elevator_position_at_call=2 + ) + db.session.add_all([call1, call2]) + db.session.commit() + + response = self.app.get(f'/api/ml-data/features/{self.building.id}') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['building_id'], self.building.id) + self.assertGreaterEqual(data['total_samples'], 2) + self.assertIn('features', data) + + # Verify feature structure + if data['features']: + feature = data['features'][0] + self.assertIn('target_floor', feature) + self.assertIn('time_features', feature) + self.assertIn('elevator_features', feature) + self.assertIn('contextual_features', feature) + +class BusinessRulesTestCase(unittest.TestCase): + """Test business logic and validation rules.""" + + def setUp(self): + """Set up test fixtures before each test method.""" + self.app_context = app.app_context() + self.app_context.push() + + db.create_all() + + self.building = Building( + name="Business Rules Test Building", + total_floors=5 + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="TEST-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() + + def tearDown(self): + """Clean up after each test method.""" + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_validate_floor_range_valid(self): + """Test floor range validation with valid floors.""" + self.assertTrue(ElevatorBusinessRules.validate_floor_range(1, self.building.id)) + self.assertTrue(ElevatorBusinessRules.validate_floor_range(3, self.building.id)) + self.assertTrue(ElevatorBusinessRules.validate_floor_range(5, self.building.id)) + + def test_validate_floor_range_invalid(self): + """Test floor range validation with invalid floors.""" + self.assertFalse(ElevatorBusinessRules.validate_floor_range(0, self.building.id)) + self.assertFalse(ElevatorBusinessRules.validate_floor_range(6, self.building.id)) + self.assertFalse(ElevatorBusinessRules.validate_floor_range(-1, self.building.id)) + + def test_validate_floor_range_nonexistent_building(self): + """Test floor range validation with nonexistent building.""" + self.assertFalse(ElevatorBusinessRules.validate_floor_range(5, 99999)) + + def test_calculate_response_time(self): + """Test response time calculation.""" + call_time = datetime.utcnow() + + # Same floor - minimum time (door operation) + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 5) + self.assertEqual(response_time, 2.0) + + # One floor away + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 4) + self.assertEqual(response_time, 5.0) # 1 floor * 3 seconds + 2 seconds door + + # Multiple floors + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 1, 5) + self.assertEqual(response_time, 14.0) # 4 floors * 3 seconds + 2 seconds door + + def test_maintenance_alert_no_previous_maintenance(self): + """Test maintenance alert for elevator with no maintenance history.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-01", + last_maintenance=None + ) + db.session.add(elevator) + db.session.commit() + + self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_maintenance_alert_recent_maintenance(self): + """Test maintenance alert for recently maintained elevator.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-02", + last_maintenance=datetime.utcnow() - timedelta(days=15) + ) + db.session.add(elevator) + db.session.commit() + + self.assertFalse(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_maintenance_alert_overdue_maintenance(self): + """Test maintenance alert for overdue maintenance.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-03", + last_maintenance=datetime.utcnow() - timedelta(days=35) + ) + db.session.add(elevator) + db.session.commit() + + self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_can_elevator_access_floor_valid_range(self): + """Test elevator access within valid floor range.""" + + freight_elevator = Elevator( + building_id=self.building.id, + elevator_number="FREIGHT", + current_floor=1 + ) + db.session.add(freight_elevator) + db.session.commit() + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) + self.assertTrue(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 3) + self.assertTrue(result) + + def test_can_elevator_access_floor_outside_range(self): + """Test elevator access outside building floor range.""" + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 0) + self.assertFalse(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 6) + self.assertFalse(result) + + def test_can_elevator_access_floor_freight_restrictions(self): + """Test freight elevator floor restrictions.""" + freight_elevator = Elevator( + building_id=self.building.id, + elevator_number="FREIGHT", + current_floor=1 + ) + db.session.add(freight_elevator) + db.session.commit() + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) + self.assertTrue(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 6) + self.assertFalse(result) + + def test_can_elevator_access_floor_nonexistent_elevator(self): + """Test with non-existent elevator.""" + result = ElevatorBusinessRules.can_elevator_access_floor(99999, 3) + self.assertFalse(result) + +class ElevatorCallModelTestCase(unittest.TestCase): + """Test the ElevatorCall model and its automatic feature generation.""" + + def setUp(self): + self.app_context = app.app_context() + self.app_context.push() + db.create_all() + + self.building = Building(name="Model Test Building", total_floors=5) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="MODEL-01" + ) + db.session.add(self.elevator) + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_temporal_features_auto_population(self): + """Test that temporal features are automatically populated.""" + # Create a call with a specific datetime + test_date = datetime(2024, 7, 15, 14, 30) # Monday, July 15, 2024, 2:30 PM + + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=3, + call_time=test_date + ) + + self.assertEqual(call.day_of_week, 0) # Monday + self.assertEqual(call.hour_of_day, 14) # 2 PM + self.assertEqual(call.week_of_month, 3) # Third week of July + self.assertEqual(call.season, 'summer') + + def test_season_calculation_winter(self): + """Test season calculation for winter months.""" + winter_dates = [ + datetime(2024, 12, 15), # December + datetime(2024, 1, 15), # January + datetime(2024, 2, 15) # February + ] + + for test_date in winter_dates: + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, + call_time=test_date + ) + self.assertEqual(call.season, 'winter', f"Failed for {test_date}") + + def test_season_calculation_all_seasons(self): + """Test season calculation for all seasons.""" + season_tests = [ + (datetime(2024, 3, 15), 'spring'), + (datetime(2024, 6, 15), 'summer'), + (datetime(2024, 9, 15), 'autumn'), + (datetime(2024, 12, 15), 'winter') + ] + + for test_date, expected_season in season_tests: + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, + call_time=test_date + ) + self.assertEqual(call.season, expected_season) + +class MLDataIntegrationTestCase(unittest.TestCase): + """Integration tests for ML data preparation and export.""" + + def setUp(self): + self.app_context = app.app_context() + self.app_context.push() + self.app = app.test_client() + + db.create_all() + + # Create comprehensive test data + self.building = Building( + name="ML Integration Test Building", + total_floors=5, + building_type="mixed" + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="ML-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() + + # Create diverse call patterns + self._create_test_call_patterns() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def _create_test_call_patterns(self): + """Create realistic call patterns for testing.""" + import random + + # Morning rush pattern (7-9 AM) + for day in range(5): # Weekdays + for hour in [7, 8]: + for _ in range(random.randint(3, 8)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, # Ground floor calls in morning + destination_floor=random.randint(2, 5), + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 4) + ) + db.session.add(call) + + # Lunch pattern (12-1 PM) + for day in range(5): + for hour in [12]: + for _ in range(random.randint(2, 5)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=random.randint(2, 5), # Various floors going down + destination_floor=1, + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 3) + ) + db.session.add(call) + + # Evening pattern (5-7 PM) + for day in range(5): + for hour in [17, 18]: # 5-6 PM + for _ in range(random.randint(4, 9)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=random.randint(2, 5), # Various floors going down + destination_floor=1, + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 3) + ) + db.session.add(call) + + db.session.commit() + + def test_ml_data_export_structure(self): + """Test that ML data export has correct structure.""" + response = self.app.get(f'/api/ml-data/features/{self.building.id}') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + + # Verify top-level structure + required_keys = ['building_id', 'total_samples', 'date_range', 'features'] + for key in required_keys: + self.assertIn(key, data) + + # Verify we have samples + self.assertGreater(data['total_samples'], 0) + + # Verify feature structure + if data['features']: + feature = data['features'][0] + required_feature_keys = ['target_floor', 'time_features', 'elevator_features', 'contextual_features'] + for key in required_feature_keys: + self.assertIn(key, feature) + + # Verify time features + time_features = feature['time_features'] + time_required = ['hour_of_day', 'day_of_week', 'week_of_month', 'season'] + for key in time_required: + self.assertIn(key, time_features) + + + def test_ml_data_filtering(self): + """Test ML data filtering by date range.""" + # Test with shorter time range + response = self.app.get(f'/api/ml-data/features/{self.building.id}?days_back=7') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + + # Should still have structure but possibly fewer samples + self.assertIn('total_samples', data) + self.assertIn('features', data) + +if __name__ == '__main__': + test_suite = unittest.TestSuite() + + test_classes = [ + ElevatorSystemTestCase, + BusinessRulesTestCase, + ElevatorCallModelTestCase, + MLDataIntegrationTestCase + ] + + for test_class in test_classes: + tests = unittest.TestLoader().loadTestsFromTestCase(test_class) + test_suite.addTests(tests) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(test_suite) + + # Summary + print(f"\n{'='*50}") + print(f"TESTS RUN: {result.testsRun}") + print(f"FAILURES: {len(result.failures)}") + print(f"ERRORS: {len(result.errors)}") + print(f"SUCCESS RATE: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%") + print(f"{'='*50}") \ No newline at end of file diff --git a/chatgpt/instance/elevator_data.db b/chatgpt/instance/elevator_data.db index 41f643cc82ea54305888af2a1304639b989ef7b7..9660a10e32261dbe70e7ee68906f728d847c4505 100644 GIT binary patch delta 85 zcmZozz|^pSX@ZmxdkO;s11Au(0Wk*y18dnt9U}>#s9sqoFHndM1av1~|82B}KC-SgyKIaJE Z*x2SjF@Rf;9jX|l5yWQQED`X+9spr04kG{n diff --git a/chatgpt/main.py b/chatgpt/main.py index a1635a1..e865a4b 100644 --- a/chatgpt/main.py +++ b/chatgpt/main.py @@ -8,7 +8,6 @@ app = Flask(__name__) -# Configuration app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///elevator_data.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'dev-secret-key' @@ -41,7 +40,6 @@ class Elevator(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) calls = db.relationship('ElevatorCall', backref='elevator', lazy=True) - movements = db.relationship('ElevatorMovement', backref='elevator', lazy=True) class ElevatorCall(db.Model): __tablename__ = 'elevator_calls' @@ -80,40 +78,30 @@ def _get_season(self, month: int) -> str: else: return 'autumn' -class ElevatorMovement(db.Model): - __tablename__ = 'elevator_movements' - - id = db.Column(db.Integer, primary_key=True) - elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) - from_floor = db.Column(db.Integer, nullable=False) - to_floor = db.Column(db.Integer, nullable=False) - movement_start = db.Column(db.DateTime, default=datetime.utcnow) - movement_end = db.Column(db.DateTime) - movement_type = db.Column(db.String(20)) - load_before = db.Column(db.Integer, default=0) - load_after = db.Column(db.Integer, default=0) - -class FloorDemandSummary(db.Model): - __tablename__ = 'floor_demand_summary' - - id = db.Column(db.Integer, primary_key=True) - building_id = db.Column(db.Integer, db.ForeignKey('buildings.id'), nullable=False) - floor_number = db.Column(db.Integer, nullable=False) - date = db.Column(db.Date, nullable=False) - hour = db.Column(db.Integer, nullable=False) - call_count = db.Column(db.Integer, default=0) - avg_response_time = db.Column(db.Float) - peak_load_time = db.Column(db.Time) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - class ElevatorBusinessRules: @staticmethod def validate_floor_range(floor: int, building_id: int) -> bool: + """Validate if a floor number is valid for a given building.""" building = Building.query.get(building_id) if not building: return False return 1 <= floor <= building.total_floors - + + @staticmethod + def can_elevator_access_floor(elevator_id: int, floor: int) -> bool: + """Check if an elevator can access a specific floor.""" + elevator = Elevator.query.get(elevator_id) + if not elevator: + return False + + if not ElevatorBusinessRules.validate_floor_range(floor, elevator.building_id): + return False + + if elevator.elevator_number == "FREIGHT" and floor > 5: + return False + + return True + @staticmethod def calculate_response_time(call_time: datetime, elevator_position: int, called_floor: int) -> float: floors_to_travel = abs(elevator_position - called_floor) @@ -156,7 +144,6 @@ def create_building(): @app.route('/api/buildings//elevators', methods=['POST']) def create_elevator(building_id): """Add an elevator to a building""" - building = Building.query.get_or_404(building_id) data = request.get_json() if not data or 'elevator_number' not in data: @@ -184,7 +171,7 @@ def create_elevator(building_id): @app.route('/api/elevators//call', methods=['POST']) def record_elevator_call(elevator_id): - """Record an elevator call (demand event)""" + """Record an elevator call """ elevator = Elevator.query.get_or_404(elevator_id) data = request.get_json() @@ -193,11 +180,9 @@ def record_elevator_call(elevator_id): called_from_floor = data['called_from_floor'] - # Business rule validation if not ElevatorBusinessRules.validate_floor_range(called_from_floor, elevator.building_id): return jsonify({'error': 'Invalid floor number'}), 400 - # Calculate response time response_time = ElevatorBusinessRules.calculate_response_time( datetime.utcnow(), elevator.current_floor, called_from_floor ) @@ -217,7 +202,6 @@ def record_elevator_call(elevator_id): db.session.add(call) db.session.commit() - # Update elevator position if destination provided if data.get('destination_floor'): elevator.current_floor = data['destination_floor'] elevator.is_moving = False @@ -238,55 +222,11 @@ def record_elevator_call(elevator_id): db.session.rollback() return jsonify({'error': str(e)}), 500 -@app.route('/api/elevators//move', methods=['POST']) -def record_elevator_movement(elevator_id): - """Record elevator movement (repositioning)""" - elevator = Elevator.query.get_or_404(elevator_id) - data = request.get_json() - - if not data or 'to_floor' not in data: - return jsonify({'error': 'Missing required field: to_floor'}), 400 - - to_floor = data['to_floor'] - - if not ElevatorBusinessRules.validate_floor_range(to_floor, elevator.building_id): - return jsonify({'error': 'Invalid floor number'}), 400 - - movement = ElevatorMovement( - elevator_id=elevator_id, - from_floor=elevator.current_floor, - to_floor=to_floor, - movement_type=data.get('movement_type', 'repositioning'), - load_before=elevator.current_load, - load_after=data.get('load_after', elevator.current_load) - ) - - try: - db.session.add(movement) - - # Update elevator state - elevator.current_floor = to_floor - elevator.current_load = data.get('load_after', elevator.current_load) - elevator.is_moving = False - - db.session.commit() - - return jsonify({ - 'movement_id': movement.id, - 'from_floor': movement.from_floor, - 'to_floor': movement.to_floor, - 'elevator_current_floor': elevator.current_floor - }), 201 - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - @app.route('/api/elevators//status', methods=['GET']) def get_elevator_status(elevator_id): """Get current elevator status""" elevator = Elevator.query.get_or_404(elevator_id) - # Check maintenance status maintenance_alert = ElevatorBusinessRules.should_trigger_maintenance_alert(elevator) return jsonify({ @@ -353,41 +293,6 @@ def get_ml_features(building_id): 'features': features }) -@app.route('/api/analytics/demand-patterns/', methods=['GET']) -def get_demand_patterns(building_id): - """Get demand patterns for analysis""" - building = Building.query.get_or_404(building_id) - - demand_query = db.session.query( - ElevatorCall.called_from_floor, - ElevatorCall.hour_of_day, - ElevatorCall.day_of_week, - db.func.count(ElevatorCall.id).label('call_count'), - db.func.avg(ElevatorCall.response_time).label('avg_response_time') - ).join(Elevator).filter( - Elevator.building_id == building_id - ).group_by( - ElevatorCall.called_from_floor, - ElevatorCall.hour_of_day, - ElevatorCall.day_of_week - ).all() - - patterns = [] - for row in demand_query: - patterns.append({ - 'floor': row.called_from_floor, - 'hour': row.hour_of_day, - 'day_of_week': row.day_of_week, - 'call_count': row.call_count, - 'avg_response_time': round(row.avg_response_time, 2) if row.avg_response_time else None - }) - - return jsonify({ - 'building_id': building_id, - 'demand_patterns': patterns - }) - -# Health check endpoint @app.route('/health', methods=['GET']) def health_check(): return jsonify({ diff --git a/readme.md b/readme.md index 42741db..2e1e2aa 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,25 @@ + +# MY THOUGHT PROCESS +### Whenever im faced with an AI modeling problem, i often love to revert to the fundamentals of developing models then expand from there, i have found this to be a good way to approach problems in a methodical and structured way, because anything that can be broken down into a process can be meausred and anything that can be measured can be improved, this i believe is the very heart of artifiicial intelligence + +### My methodical steps are as follows: + +### 1. Identify and fully understand the problem + +### 2. Identify the prediction target or goal + +### 3. Identify the available data that we have access to + +### 4. Identify obvious and non-obvious factors that could influence the prediction target + +### 5. Select the appropriate model that would help best solve the problem, understand its limitations and constraints as well as strategies and techniques to improve performance + +### 6. Prepare the data for training and testing + +### 7. Evaluate the models performance and based on results decide whether to iterate and improve training data or use a different approach and model entirely and keep iterating and benchmarking, understanding trade offs and beneffits between approaches + + + # Elevator resting floor prediction model solution ## Problem statement: @@ -17,13 +39,35 @@ #### We might discover that certain weeks of the month have more people/traffic as compared to other weeks, e.g retreats, field work weeks, etc - ### Maintainance schedule that week (if any) #### This is one of those non obvious factors, we might discover that an elevator that is not regularly maintained is less trusted by users and so they decide maybe to take the stairs, only users that need to travel safer distances might prefer it, e.g a floor above the ground floor, this would also help avoid overfitting and help our model generalize better +- ### Elevator load + #### We might find that when the elevator is heavy, at a particular time, the elevator tends to move to a certain floor with more people in it. + - ### Current weather season -- #### We might find that during winter, christmas or summer, users might not prefer the elevator, or certain floors experience more demand + #### We might find that during winter, christmas or summer, users might not prefer the elevator, or certain floors experience more demand + +- ### Current elevator floor + #### This is another one of those non obvious ones, naturally i wouldnt think a current elevator floor could influence where people would like to go next, but maybe we might discover a pattern of if the elevator is called from the ground floor at a particular time, users are moving to a working floor for example in a multipurpose building + +- ### Number of calls that day on all floors + #### Understanding how many calls are made on each floor in a day could help us decipher busy floors, this could in our overall prediction, during live prediction this could be accumulated through the day live. + +## Data access + ### Next i need to understand what historical data can actually be collected and fed into our system during training as well as the kind of data that the system will have access to during actual live prediction, the availability of data affects or constraints the model we want to use. + ### Data such as time of day, day of week, week of month, maintainance schedule and current weather season are available natively to our system and can be enhanced by installing addidtional packages, other remaining feattures are usually available in elevator systems + ### With the kind of data i have access to, now would be the best time to plan the model that would work well with this data, i would research on best performing models for my use case and available data as well as time-frame, and in my current scenario that would a Gradient boosting model, specifically CatBoost which has been known to work well with categorical features such as day of week, etc without needing encoding. In terms of timeframe, the categorical handling conveniennce of CatBoost often outweighs the speed cost, my second alternative would be Random forest, with the drawback of having to manually encode categorical features. + + +## Storage Schema + +### For the storage schema i would create a relational database with the following entities and relationships, + +### - Building: Meta data about the building with a one to many relationship with the Elevator, because buildings ideally hold elevators +### - Elevator: Meta data about the elevator with a one to many relationship too ElevatorCall + +### - ElevatorCall: Meta data about the elevator call with a many to one relationship with the Elevator to track call demands + -### Data access -#### Next i need to understand what historical data can actually be collected and fed into our system during training as well as the kind of data that the system will have access to during actual live prediction -#### Data such as time of day, day of week, week of month, maintainance schedule and current weather season are available natively to our system and can be enhanced by installing addidtional packages. From 13646e0f66104eba0ea815cc1f7d06dbb9ae9e6e Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:12:28 +0300 Subject: [PATCH 05/11] finalize readme and improved featuure extraction endpoint --- app/app_tests.py | 555 +++++++++++++++++++++++++++++++++++++++++++ app/db.sql | 12 + app/main.py | 359 ++++++++++++++++++++++++++++ app/requirements.txt | 7 + 4 files changed, 933 insertions(+) create mode 100644 app/app_tests.py create mode 100644 app/db.sql create mode 100644 app/main.py create mode 100644 app/requirements.txt diff --git a/app/app_tests.py b/app/app_tests.py new file mode 100644 index 0000000..c98f12a --- /dev/null +++ b/app/app_tests.py @@ -0,0 +1,555 @@ +import unittest +import json +import tempfile +import os +from datetime import datetime, timedelta +from main import app, db, Building, Elevator, ElevatorCall, ElevatorBusinessRules + +class ElevatorSystemTestCase(unittest.TestCase): + def setUp(self): + """Set up test fixtures before each test method.""" + self.db_fd, app.config['DATABASE'] = tempfile.mkstemp() + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE'] + app.config['TESTING'] = True + app.config['WTF_CSRF_ENABLED'] = False + + self.app = app.test_client() + self.app_context = app.app_context() + self.app_context.push() + + db.create_all() + + # Create test data + self.building = Building( + name="Test Building", + total_floors=5, + building_type="office" + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="TEST-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() + + def tearDown(self): + """Clean up after each test method.""" + db.session.remove() + db.drop_all() + self.app_context.pop() + os.close(self.db_fd) + os.unlink(app.config['DATABASE']) + + def test_health_check(self): + """Test the health check endpoint.""" + response = self.app.get('/health') + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['status'], 'healthy') + self.assertIn('timestamp', data) + + def test_create_building(self): + """Test building creation.""" + building_data = { + 'name': 'New Test Building', + 'total_floors': 5, + 'building_type': 'residential' + } + + response = self.app.post('/api/buildings', + data=json.dumps(building_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertEqual(data['name'], 'New Test Building') + self.assertEqual(data['total_floors'], 5) + self.assertEqual(data['building_type'], 'residential') + + def test_create_building_missing_data(self): + """Test building creation with missing required fields.""" + building_data = {'name': 'Incomplete Building'} # Missing total_floors + + response = self.app.post('/api/buildings', + data=json.dumps(building_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertIn('error', data) + + def test_create_elevator(self): + """Test elevator creation.""" + elevator_data = { + 'elevator_number': 'TEST-02', + 'max_capacity': 1200, + 'current_floor': 3 + } + + response = self.app.post(f'/api/buildings/{self.building.id}/elevators', + data=json.dumps(elevator_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertEqual(data['elevator_number'], 'TEST-02') + self.assertEqual(data['current_floor'], 3) + + def test_record_elevator_call(self): + """Test recording an elevator call.""" + call_data = { + 'called_from_floor': 3, + 'destination_floor': 1, + 'estimated_passengers': 2, + 'call_type': 'normal' + } + + response = self.app.post(f'/api/elevators/{self.elevator.id}/call', + data=json.dumps(call_data), + content_type='application/json') + + self.assertEqual(response.status_code, 201) + data = json.loads(response.data) + self.assertIn('call_id', data) + self.assertIn('estimated_response_time', data) + self.assertIn('temporal_features', data) + + # Verify temporal features are populated + temporal = data['temporal_features'] + self.assertIn('day_of_week', temporal) + self.assertIn('hour_of_day', temporal) + self.assertIn('season', temporal) + + def test_record_elevator_call_invalid_floor(self): + """Test recording call with invalid floor.""" + call_data = { + 'called_from_floor': 6, # Building only has 5 floors + 'destination_floor': 1 + } + + response = self.app.post(f'/api/elevators/{self.elevator.id}/call', + data=json.dumps(call_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertIn('Invalid floor number', data['error']) + + + def test_get_elevator_status(self): + """Test getting elevator status.""" + response = self.app.get(f'/api/elevators/{self.elevator.id}/status') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['elevator_id'], self.elevator.id) + self.assertEqual(data['current_floor'], 1) + self.assertIn('maintenance_alert', data) + + def test_get_ml_features(self): + """Test ML features endpoint.""" + # Create some test calls first + call1 = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=2, + elevator_position_at_call=1 + ) + call2 = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=3, + elevator_position_at_call=2 + ) + db.session.add_all([call1, call2]) + db.session.commit() + + response = self.app.get(f'/api/ml-data/features/{self.building.id}') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertEqual(data['building_id'], self.building.id) + self.assertGreaterEqual(data['total_samples'], 2) + self.assertIn('features', data) + + # Verify feature structure + if data['features']: + feature = data['features'][0] + self.assertIn('target_floor', feature) + self.assertIn('time_features', feature) + self.assertIn('elevator_features', feature) + self.assertIn('contextual_features', feature) + +class BusinessRulesTestCase(unittest.TestCase): + """Test business logic and validation rules.""" + + def setUp(self): + """Set up test fixtures before each test method.""" + self.app_context = app.app_context() + self.app_context.push() + + db.create_all() + + self.building = Building( + name="Business Rules Test Building", + total_floors=5 + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="TEST-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() + + def tearDown(self): + """Clean up after each test method.""" + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_validate_floor_range_valid(self): + """Test floor range validation with valid floors.""" + self.assertTrue(ElevatorBusinessRules.validate_floor_range(1, self.building.id)) + self.assertTrue(ElevatorBusinessRules.validate_floor_range(3, self.building.id)) + self.assertTrue(ElevatorBusinessRules.validate_floor_range(5, self.building.id)) + + def test_validate_floor_range_invalid(self): + """Test floor range validation with invalid floors.""" + self.assertFalse(ElevatorBusinessRules.validate_floor_range(0, self.building.id)) + self.assertFalse(ElevatorBusinessRules.validate_floor_range(6, self.building.id)) + self.assertFalse(ElevatorBusinessRules.validate_floor_range(-1, self.building.id)) + + def test_validate_floor_range_nonexistent_building(self): + """Test floor range validation with nonexistent building.""" + self.assertFalse(ElevatorBusinessRules.validate_floor_range(5, 99999)) + + def test_calculate_response_time(self): + """Test response time calculation.""" + call_time = datetime.utcnow() + + # Same floor - minimum time (door operation) + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 5) + self.assertEqual(response_time, 2.0) + + # One floor away + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 4) + self.assertEqual(response_time, 5.0) # 1 floor * 3 seconds + 2 seconds door + + # Multiple floors + response_time = ElevatorBusinessRules.calculate_response_time(call_time, 1, 5) + self.assertEqual(response_time, 14.0) # 4 floors * 3 seconds + 2 seconds door + + def test_maintenance_alert_no_previous_maintenance(self): + """Test maintenance alert for elevator with no maintenance history.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-01", + last_maintenance=None + ) + db.session.add(elevator) + db.session.commit() + + self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_maintenance_alert_recent_maintenance(self): + """Test maintenance alert for recently maintained elevator.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-02", + last_maintenance=datetime.utcnow() - timedelta(days=15) + ) + db.session.add(elevator) + db.session.commit() + + self.assertFalse(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_maintenance_alert_overdue_maintenance(self): + """Test maintenance alert for overdue maintenance.""" + elevator = Elevator( + building_id=self.building.id, + elevator_number="MAINT-03", + last_maintenance=datetime.utcnow() - timedelta(days=35) + ) + db.session.add(elevator) + db.session.commit() + + self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) + + def test_can_elevator_access_floor_valid_range(self): + """Test elevator access within valid floor range.""" + + freight_elevator = Elevator( + building_id=self.building.id, + elevator_number="FREIGHT", + current_floor=1 + ) + db.session.add(freight_elevator) + db.session.commit() + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) + self.assertTrue(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 3) + self.assertTrue(result) + + def test_can_elevator_access_floor_outside_range(self): + """Test elevator access outside building floor range.""" + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 0) + self.assertFalse(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 6) + self.assertFalse(result) + + def test_can_elevator_access_floor_freight_restrictions(self): + """Test freight elevator floor restrictions.""" + freight_elevator = Elevator( + building_id=self.building.id, + elevator_number="FREIGHT", + current_floor=1 + ) + db.session.add(freight_elevator) + db.session.commit() + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) + self.assertTrue(result) + + result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 6) + self.assertFalse(result) + + def test_can_elevator_access_floor_nonexistent_elevator(self): + """Test with non-existent elevator.""" + result = ElevatorBusinessRules.can_elevator_access_floor(99999, 3) + self.assertFalse(result) + +class ElevatorCallModelTestCase(unittest.TestCase): + """Test the ElevatorCall model and its automatic feature generation.""" + + def setUp(self): + self.app_context = app.app_context() + self.app_context.push() + db.create_all() + + self.building = Building(name="Model Test Building", total_floors=5) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="MODEL-01" + ) + db.session.add(self.elevator) + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_temporal_features_auto_population(self): + """Test that temporal features are automatically populated.""" + # Create a call with a specific datetime + test_date = datetime(2024, 7, 15, 14, 30) # Monday, July 15, 2024, 2:30 PM + + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=3, + call_time=test_date + ) + + self.assertEqual(call.day_of_week, 0) # Monday + self.assertEqual(call.hour_of_day, 14) # 2 PM + self.assertEqual(call.week_of_month, 3) # Third week of July + self.assertEqual(call.season, 'summer') + + def test_season_calculation_winter(self): + """Test season calculation for winter months.""" + winter_dates = [ + datetime(2024, 12, 15), # December + datetime(2024, 1, 15), # January + datetime(2024, 2, 15) # February + ] + + for test_date in winter_dates: + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, + call_time=test_date + ) + self.assertEqual(call.season, 'winter', f"Failed for {test_date}") + + def test_season_calculation_all_seasons(self): + """Test season calculation for all seasons.""" + season_tests = [ + (datetime(2024, 3, 15), 'spring'), + (datetime(2024, 6, 15), 'summer'), + (datetime(2024, 9, 15), 'autumn'), + (datetime(2024, 12, 15), 'winter') + ] + + for test_date, expected_season in season_tests: + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, + call_time=test_date + ) + self.assertEqual(call.season, expected_season) + +class MLDataIntegrationTestCase(unittest.TestCase): + """Integration tests for ML data preparation and export.""" + + def setUp(self): + self.app_context = app.app_context() + self.app_context.push() + self.app = app.test_client() + + db.create_all() + + # Create comprehensive test data + self.building = Building( + name="ML Integration Test Building", + total_floors=5, + building_type="mixed" + ) + db.session.add(self.building) + db.session.commit() + + self.elevator = Elevator( + building_id=self.building.id, + elevator_number="ML-01", + current_floor=1 + ) + db.session.add(self.elevator) + db.session.commit() + + # Create diverse call patterns + self._create_test_call_patterns() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def _create_test_call_patterns(self): + """Create realistic call patterns for testing.""" + import random + + # Morning rush pattern (7-9 AM) + for day in range(5): # Weekdays + for hour in [7, 8]: + for _ in range(random.randint(3, 8)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=1, # Ground floor calls in morning + destination_floor=random.randint(2, 5), + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 4) + ) + db.session.add(call) + + # Lunch pattern (12-1 PM) + for day in range(5): + for hour in [12]: + for _ in range(random.randint(2, 5)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=random.randint(2, 5), # Various floors going down + destination_floor=1, + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 3) + ) + db.session.add(call) + + # Evening pattern (5-7 PM) + for day in range(5): + for hour in [17, 18]: # 5-6 PM + for _ in range(random.randint(4, 9)): + call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) + call = ElevatorCall( + elevator_id=self.elevator.id, + called_from_floor=random.randint(2, 5), # Various floors going down + destination_floor=1, + call_time=call_time, + elevator_position_at_call=random.randint(1, 5), + estimated_passengers=random.randint(1, 3) + ) + db.session.add(call) + + db.session.commit() + + def test_export_ml_features_csv(self): + """Test exporting ML features as CSV""" + response = self.app.get(f'/api/ml-data/features/{self.building.id}?format=csv') + self.assertEqual(response.status_code, 200) + self.assertIn('text/csv', response.content_type) + self.assertIn('Content-Disposition', response.headers) + self.assertIn('target_floor', response.get_data(as_text=True)) + + def test_export_ml_features_json(self): + """Test exporting ML features as JSON""" + response = self.app.get(f'/api/ml-data/features/{self.building.id}?format=json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content_type, 'application/json') + data = json.loads(response.data) + self.assertIn('building_id', data) + self.assertIn('total_samples', data) + self.assertIn('features', data) + if data['total_samples'] > 0: + self.assertIn('target_floor', data['features'][0]) + + def test_export_ml_features_missing_building_id(self): + """Test export with missing building_id""" + response = self.app.get('/api/ml-data/features/') + self.assertEqual(response.status_code, 404) # Changed from 400 to 404 as the route requires building_id + + def test_export_ml_features_nonexistent_building(self): + """Test export with non-existent building_id""" + response = self.app.get('/api/ml-data/features/9999') + self.assertEqual(response.status_code, 404) + + + def test_ml_data_filtering(self): + """Test ML data filtering by date range.""" + # Test with shorter time range + response = self.app.get(f'/api/ml-data/features/{self.building.id}?days_back=7') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + + # Should still have structure but possibly fewer samples + self.assertIn('total_samples', data) + self.assertIn('features', data) + +if __name__ == '__main__': + test_suite = unittest.TestSuite() + + test_classes = [ + ElevatorSystemTestCase, + BusinessRulesTestCase, + ElevatorCallModelTestCase, + MLDataIntegrationTestCase + ] + + for test_class in test_classes: + tests = unittest.TestLoader().loadTestsFromTestCase(test_class) + test_suite.addTests(tests) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(test_suite) + + # Summary + print(f"\n{'='*50}") + print(f"TESTS RUN: {result.testsRun}") + print(f"FAILURES: {len(result.failures)}") + print(f"ERRORS: {len(result.errors)}") + print(f"SUCCESS RATE: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%") + print(f"{'='*50}") \ No newline at end of file diff --git a/app/db.sql b/app/db.sql new file mode 100644 index 0000000..1555ffe --- /dev/null +++ b/app/db.sql @@ -0,0 +1,12 @@ +CREATE TABLE elevator_demands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + floor INTEGER +); + +CREATE TABLE elevator_states ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + floor INTEGER, + vacant BOOLEAN +); diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..47fbbfc --- /dev/null +++ b/app/main.py @@ -0,0 +1,359 @@ +from flask import Flask, request, jsonify, g +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime, timedelta +import sqlite3 +import os +from typing import Dict, List, Optional +import json +import pandas as pd +from io import StringIO + + + +app = Flask(__name__) + +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///elevator_data.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = 'dev-secret-key' + +db = SQLAlchemy(app) + +class Building(db.Model): + __tablename__ = 'buildings' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + total_floors = db.Column(db.Integer, nullable=False) + building_type = db.Column(db.String(50)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + elevators = db.relationship('Elevator', backref='building', lazy=True) + +class Elevator(db.Model): + __tablename__ = 'elevators' + + id = db.Column(db.Integer, primary_key=True) + building_id = db.Column(db.Integer, db.ForeignKey('buildings.id'), nullable=False) + elevator_number = db.Column(db.String(10), nullable=False) + max_capacity = db.Column(db.Integer, default=1000) + current_floor = db.Column(db.Integer, default=1) + current_load = db.Column(db.Integer, default=0) + is_moving = db.Column(db.Boolean, default=False) + maintenance_status = db.Column(db.String(20), default='operational') + last_maintenance = db.Column(db.DateTime) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + calls = db.relationship('ElevatorCall', backref='elevator', lazy=True) + +class ElevatorCall(db.Model): + __tablename__ = 'elevator_calls' + + id = db.Column(db.Integer, primary_key=True) + elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) + called_from_floor = db.Column(db.Integer, nullable=False) + destination_floor = db.Column(db.Integer) + call_time = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + response_time = db.Column(db.Float) + elevator_position_at_call = db.Column(db.Integer) + estimated_passengers = db.Column(db.Integer, default=1) + call_type = db.Column(db.String(20), default='normal') + day_of_week = db.Column(db.Integer) + hour_of_day = db.Column(db.Integer) + week_of_month = db.Column(db.Integer) + season = db.Column(db.String(10)) + weather_condition = db.Column(db.String(20)) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + call_dt = self.call_time or datetime.utcnow() + self.day_of_week = call_dt.weekday() + self.hour_of_day = call_dt.hour + self.week_of_month = (call_dt.day - 1) // 7 + 1 + self.season = self._get_season(call_dt.month) + + def _get_season(self, month: int) -> str: + if month in [12, 1, 2]: + return 'winter' + elif month in [3, 4, 5]: + return 'spring' + elif month in [6, 7, 8]: + return 'summer' + else: + return 'autumn' + +class ElevatorBusinessRules: + @staticmethod + def validate_floor_range(floor: int, building_id: int) -> bool: + """Validate if a floor number is valid for a given building.""" + building = Building.query.get(building_id) + if not building: + return False + return 1 <= floor <= building.total_floors + + @staticmethod + def can_elevator_access_floor(elevator_id: int, floor: int) -> bool: + """Check if an elevator can access a specific floor.""" + elevator = Elevator.query.get(elevator_id) + if not elevator: + return False + + if not ElevatorBusinessRules.validate_floor_range(floor, elevator.building_id): + return False + + if elevator.elevator_number == "FREIGHT" and floor > 5: + return False + + return True + + @staticmethod + def calculate_response_time(call_time: datetime, elevator_position: int, called_floor: int) -> float: + floors_to_travel = abs(elevator_position - called_floor) + return floors_to_travel * 3.0 + 2.0 + + @staticmethod + def should_trigger_maintenance_alert(elevator: Elevator) -> bool: + if not elevator.last_maintenance: + return True + days_since_maintenance = (datetime.utcnow() - elevator.last_maintenance).days + return days_since_maintenance > 30 + +@app.route('/api/buildings', methods=['POST']) +def create_building(): + """Create a new building""" + data = request.get_json() + + if not data or 'name' not in data or 'total_floors' not in data: + return jsonify({'error': 'Missing required fields: name, total_floors'}), 400 + + building = Building( + name=data['name'], + total_floors=data['total_floors'], + building_type=data.get('building_type', 'mixed') + ) + + try: + db.session.add(building) + db.session.commit() + return jsonify({ + 'id': building.id, + 'name': building.name, + 'total_floors': building.total_floors, + 'building_type': building.building_type + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@app.route('/api/buildings//elevators', methods=['POST']) +def create_elevator(building_id): + """Add an elevator to a building""" + data = request.get_json() + + if not data or 'elevator_number' not in data: + return jsonify({'error': 'Missing required field: elevator_number'}), 400 + + elevator = Elevator( + building_id=building_id, + elevator_number=data['elevator_number'], + max_capacity=data.get('max_capacity', 1000), + current_floor=data.get('current_floor', 1) + ) + + try: + db.session.add(elevator) + db.session.commit() + return jsonify({ + 'id': elevator.id, + 'elevator_number': elevator.elevator_number, + 'current_floor': elevator.current_floor, + 'building_id': building_id + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@app.route('/api/elevators//call', methods=['POST']) +def record_elevator_call(elevator_id): + """Record an elevator call """ + elevator = Elevator.query.get_or_404(elevator_id) + data = request.get_json() + + if not data or 'called_from_floor' not in data: + return jsonify({'error': 'Missing required field: called_from_floor'}), 400 + + called_from_floor = data['called_from_floor'] + + if not ElevatorBusinessRules.validate_floor_range(called_from_floor, elevator.building_id): + return jsonify({'error': 'Invalid floor number'}), 400 + + response_time = ElevatorBusinessRules.calculate_response_time( + datetime.utcnow(), elevator.current_floor, called_from_floor + ) + + call = ElevatorCall( + elevator_id=elevator_id, + called_from_floor=called_from_floor, + destination_floor=data.get('destination_floor'), + elevator_position_at_call=elevator.current_floor, + response_time=response_time, + estimated_passengers=data.get('estimated_passengers', 1), + call_type=data.get('call_type', 'normal'), + weather_condition=data.get('weather_condition') + ) + + try: + db.session.add(call) + db.session.commit() + + if data.get('destination_floor'): + elevator.current_floor = data['destination_floor'] + elevator.is_moving = False + db.session.commit() + + return jsonify({ + 'call_id': call.id, + 'estimated_response_time': response_time, + 'call_time': call.call_time.isoformat(), + 'temporal_features': { + 'day_of_week': call.day_of_week, + 'hour_of_day': call.hour_of_day, + 'week_of_month': call.week_of_month, + 'season': call.season + } + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@app.route('/api/elevators//status', methods=['GET']) +def get_elevator_status(elevator_id): + """Get current elevator status""" + elevator = Elevator.query.get_or_404(elevator_id) + + maintenance_alert = ElevatorBusinessRules.should_trigger_maintenance_alert(elevator) + + return jsonify({ + 'elevator_id': elevator.id, + 'elevator_number': elevator.elevator_number, + 'current_floor': elevator.current_floor, + 'current_load': elevator.current_load, + 'is_moving': elevator.is_moving, + 'maintenance_status': elevator.maintenance_status, + 'maintenance_alert': maintenance_alert, + 'building_id': elevator.building_id, + 'max_floors': elevator.building.total_floors + }) + +@app.route('/api/ml-data/features/', methods=['GET']) +def get_ml_features(building_id): + """Get ML features for a building, optionally export as CSV""" + export_format = request.args.get('format', 'json').lower() + building = Building.query.get_or_404(building_id) + + days_back = request.args.get('days_back', 30, type=int) + include_weather = request.args.get('include_weather', False, type=bool) + + cutoff_date = datetime.utcnow() - timedelta(days=days_back) + + calls_query = db.session.query(ElevatorCall).join(Elevator).filter( + Elevator.building_id == building_id, + ElevatorCall.call_time >= cutoff_date + ).all() + + features = [] + for call in calls_query: + feature_row = { + 'target_floor': call.called_from_floor, + 'time_features': { + 'hour_of_day': call.hour_of_day, + 'day_of_week': call.day_of_week, + 'week_of_month': call.week_of_month, + 'season': call.season + }, + 'elevator_features': { + 'elevator_position_at_call': call.elevator_position_at_call, + 'estimated_passengers': call.estimated_passengers, + 'call_type': call.call_type + }, + 'contextual_features': { + 'response_time': call.response_time, + 'building_type': building.building_type, + 'total_floors': building.total_floors + } + } + + if include_weather and call.weather_condition: + feature_row['contextual_features']['weather_condition'] = call.weather_condition + + features.append(feature_row) + + flat_features = [] + for item in features: + flat_item = { + 'target_floor': item['target_floor'], + **item['time_features'], + **{f"elevator_{k}": v for k, v in item['elevator_features'].items()}, + **{f"ctx_{k}": v for k, v in item['contextual_features'].items()} + } + flat_features.append(flat_item) + + + if export_format == 'csv': + df = pd.DataFrame(flat_features) + output = StringIO() + df.to_csv(output, index=False) + output.seek(0) + return output.getvalue(), 200, { + 'Content-Type': 'text/csv', + 'Content-Disposition': f'attachment; filename=elevator_features_building_{building_id}.csv' + } + else: + return jsonify({ + 'building_id': building_id, + 'total_samples': len(features), + 'date_range': { + 'from': cutoff_date.isoformat(), + 'to': datetime.utcnow().isoformat() + }, + 'features': features + }) + +@app.route('/health', methods=['GET']) +def health_check(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.utcnow().isoformat(), + 'database': 'connected' + }) + +_initialized = False + +@app.before_request +def before_request(): + global _initialized + if not _initialized: + db.create_all() + + # Create sample data if none exists + if Building.query.count() == 0: + sample_building = Building( + name="Sample Office Building", + total_floors=10, + building_type="office" + ) + db.session.add(sample_building) + db.session.commit() + + sample_elevator = Elevator( + building_id=sample_building.id, + elevator_number="ELV-01", + current_floor=1 + ) + db.session.add(sample_elevator) + db.session.commit() + + _initialized = True + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..887f2d5 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,7 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +SQLAlchemy==2.0.21 +Werkzeug==2.3.7 +pytest==6.2.5 +pytest-flask==1.2.0 +pandas==2.1.4 \ No newline at end of file From fab541473cc9a75899c8d4fc5dc365dffcc42530 Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:15:42 +0300 Subject: [PATCH 06/11] finalize readme and improved featuure extraction endpoint --- .github/workflows/python-tests.yml | 29 ++ chatgpt/app_tests.py | 553 ----------------------------- 2 files changed, 29 insertions(+), 553 deletions(-) create mode 100644 .github/workflows/python-tests.yml delete mode 100644 chatgpt/app_tests.py diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..d828ddc --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,29 @@ +name: Python Tests + +on: + pull_request: + branches: [ master ] + push: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest + + - name: Run tests + working-directory: ./app + run: python -m pytest app_tests.py -v diff --git a/chatgpt/app_tests.py b/chatgpt/app_tests.py deleted file mode 100644 index 622e52f..0000000 --- a/chatgpt/app_tests.py +++ /dev/null @@ -1,553 +0,0 @@ -import unittest -import json -import tempfile -import os -from datetime import datetime, timedelta -from main import app, db, Building, Elevator, ElevatorCall, ElevatorBusinessRules - -class ElevatorSystemTestCase(unittest.TestCase): - def setUp(self): - """Set up test fixtures before each test method.""" - self.db_fd, app.config['DATABASE'] = tempfile.mkstemp() - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE'] - app.config['TESTING'] = True - app.config['WTF_CSRF_ENABLED'] = False - - self.app = app.test_client() - self.app_context = app.app_context() - self.app_context.push() - - db.create_all() - - # Create test data - self.building = Building( - name="Test Building", - total_floors=5, - building_type="office" - ) - db.session.add(self.building) - db.session.commit() - - self.elevator = Elevator( - building_id=self.building.id, - elevator_number="TEST-01", - current_floor=1 - ) - db.session.add(self.elevator) - db.session.commit() - - def tearDown(self): - """Clean up after each test method.""" - db.session.remove() - db.drop_all() - self.app_context.pop() - os.close(self.db_fd) - os.unlink(app.config['DATABASE']) - - def test_health_check(self): - """Test the health check endpoint.""" - response = self.app.get('/health') - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - self.assertEqual(data['status'], 'healthy') - self.assertIn('timestamp', data) - - def test_create_building(self): - """Test building creation.""" - building_data = { - 'name': 'New Test Building', - 'total_floors': 5, - 'building_type': 'residential' - } - - response = self.app.post('/api/buildings', - data=json.dumps(building_data), - content_type='application/json') - - self.assertEqual(response.status_code, 201) - data = json.loads(response.data) - self.assertEqual(data['name'], 'New Test Building') - self.assertEqual(data['total_floors'], 5) - self.assertEqual(data['building_type'], 'residential') - - def test_create_building_missing_data(self): - """Test building creation with missing required fields.""" - building_data = {'name': 'Incomplete Building'} # Missing total_floors - - response = self.app.post('/api/buildings', - data=json.dumps(building_data), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - data = json.loads(response.data) - self.assertIn('error', data) - - def test_create_elevator(self): - """Test elevator creation.""" - elevator_data = { - 'elevator_number': 'TEST-02', - 'max_capacity': 1200, - 'current_floor': 3 - } - - response = self.app.post(f'/api/buildings/{self.building.id}/elevators', - data=json.dumps(elevator_data), - content_type='application/json') - - self.assertEqual(response.status_code, 201) - data = json.loads(response.data) - self.assertEqual(data['elevator_number'], 'TEST-02') - self.assertEqual(data['current_floor'], 3) - - def test_record_elevator_call(self): - """Test recording an elevator call.""" - call_data = { - 'called_from_floor': 3, - 'destination_floor': 1, - 'estimated_passengers': 2, - 'call_type': 'normal' - } - - response = self.app.post(f'/api/elevators/{self.elevator.id}/call', - data=json.dumps(call_data), - content_type='application/json') - - self.assertEqual(response.status_code, 201) - data = json.loads(response.data) - self.assertIn('call_id', data) - self.assertIn('estimated_response_time', data) - self.assertIn('temporal_features', data) - - # Verify temporal features are populated - temporal = data['temporal_features'] - self.assertIn('day_of_week', temporal) - self.assertIn('hour_of_day', temporal) - self.assertIn('season', temporal) - - def test_record_elevator_call_invalid_floor(self): - """Test recording call with invalid floor.""" - call_data = { - 'called_from_floor': 6, # Building only has 5 floors - 'destination_floor': 1 - } - - response = self.app.post(f'/api/elevators/{self.elevator.id}/call', - data=json.dumps(call_data), - content_type='application/json') - - self.assertEqual(response.status_code, 400) - data = json.loads(response.data) - self.assertIn('Invalid floor number', data['error']) - - - def test_get_elevator_status(self): - """Test getting elevator status.""" - response = self.app.get(f'/api/elevators/{self.elevator.id}/status') - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - self.assertEqual(data['elevator_id'], self.elevator.id) - self.assertEqual(data['current_floor'], 1) - self.assertIn('maintenance_alert', data) - - def test_get_ml_features(self): - """Test ML features endpoint.""" - # Create some test calls first - call1 = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=2, - elevator_position_at_call=1 - ) - call2 = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=3, - elevator_position_at_call=2 - ) - db.session.add_all([call1, call2]) - db.session.commit() - - response = self.app.get(f'/api/ml-data/features/{self.building.id}') - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - self.assertEqual(data['building_id'], self.building.id) - self.assertGreaterEqual(data['total_samples'], 2) - self.assertIn('features', data) - - # Verify feature structure - if data['features']: - feature = data['features'][0] - self.assertIn('target_floor', feature) - self.assertIn('time_features', feature) - self.assertIn('elevator_features', feature) - self.assertIn('contextual_features', feature) - -class BusinessRulesTestCase(unittest.TestCase): - """Test business logic and validation rules.""" - - def setUp(self): - """Set up test fixtures before each test method.""" - self.app_context = app.app_context() - self.app_context.push() - - db.create_all() - - self.building = Building( - name="Business Rules Test Building", - total_floors=5 - ) - db.session.add(self.building) - db.session.commit() - - self.elevator = Elevator( - building_id=self.building.id, - elevator_number="TEST-01", - current_floor=1 - ) - db.session.add(self.elevator) - db.session.commit() - - def tearDown(self): - """Clean up after each test method.""" - db.session.remove() - db.drop_all() - self.app_context.pop() - - def test_validate_floor_range_valid(self): - """Test floor range validation with valid floors.""" - self.assertTrue(ElevatorBusinessRules.validate_floor_range(1, self.building.id)) - self.assertTrue(ElevatorBusinessRules.validate_floor_range(3, self.building.id)) - self.assertTrue(ElevatorBusinessRules.validate_floor_range(5, self.building.id)) - - def test_validate_floor_range_invalid(self): - """Test floor range validation with invalid floors.""" - self.assertFalse(ElevatorBusinessRules.validate_floor_range(0, self.building.id)) - self.assertFalse(ElevatorBusinessRules.validate_floor_range(6, self.building.id)) - self.assertFalse(ElevatorBusinessRules.validate_floor_range(-1, self.building.id)) - - def test_validate_floor_range_nonexistent_building(self): - """Test floor range validation with nonexistent building.""" - self.assertFalse(ElevatorBusinessRules.validate_floor_range(5, 99999)) - - def test_calculate_response_time(self): - """Test response time calculation.""" - call_time = datetime.utcnow() - - # Same floor - minimum time (door operation) - response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 5) - self.assertEqual(response_time, 2.0) - - # One floor away - response_time = ElevatorBusinessRules.calculate_response_time(call_time, 5, 4) - self.assertEqual(response_time, 5.0) # 1 floor * 3 seconds + 2 seconds door - - # Multiple floors - response_time = ElevatorBusinessRules.calculate_response_time(call_time, 1, 5) - self.assertEqual(response_time, 14.0) # 4 floors * 3 seconds + 2 seconds door - - def test_maintenance_alert_no_previous_maintenance(self): - """Test maintenance alert for elevator with no maintenance history.""" - elevator = Elevator( - building_id=self.building.id, - elevator_number="MAINT-01", - last_maintenance=None - ) - db.session.add(elevator) - db.session.commit() - - self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) - - def test_maintenance_alert_recent_maintenance(self): - """Test maintenance alert for recently maintained elevator.""" - elevator = Elevator( - building_id=self.building.id, - elevator_number="MAINT-02", - last_maintenance=datetime.utcnow() - timedelta(days=15) - ) - db.session.add(elevator) - db.session.commit() - - self.assertFalse(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) - - def test_maintenance_alert_overdue_maintenance(self): - """Test maintenance alert for overdue maintenance.""" - elevator = Elevator( - building_id=self.building.id, - elevator_number="MAINT-03", - last_maintenance=datetime.utcnow() - timedelta(days=35) - ) - db.session.add(elevator) - db.session.commit() - - self.assertTrue(ElevatorBusinessRules.should_trigger_maintenance_alert(elevator)) - - def test_can_elevator_access_floor_valid_range(self): - """Test elevator access within valid floor range.""" - - freight_elevator = Elevator( - building_id=self.building.id, - elevator_number="FREIGHT", - current_floor=1 - ) - db.session.add(freight_elevator) - db.session.commit() - - result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) - self.assertTrue(result) - - result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 3) - self.assertTrue(result) - - def test_can_elevator_access_floor_outside_range(self): - """Test elevator access outside building floor range.""" - result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 0) - self.assertFalse(result) - - result = ElevatorBusinessRules.can_elevator_access_floor(self.elevator.id, 6) - self.assertFalse(result) - - def test_can_elevator_access_floor_freight_restrictions(self): - """Test freight elevator floor restrictions.""" - freight_elevator = Elevator( - building_id=self.building.id, - elevator_number="FREIGHT", - current_floor=1 - ) - db.session.add(freight_elevator) - db.session.commit() - - result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 3) - self.assertTrue(result) - - result = ElevatorBusinessRules.can_elevator_access_floor(freight_elevator.id, 6) - self.assertFalse(result) - - def test_can_elevator_access_floor_nonexistent_elevator(self): - """Test with non-existent elevator.""" - result = ElevatorBusinessRules.can_elevator_access_floor(99999, 3) - self.assertFalse(result) - -class ElevatorCallModelTestCase(unittest.TestCase): - """Test the ElevatorCall model and its automatic feature generation.""" - - def setUp(self): - self.app_context = app.app_context() - self.app_context.push() - db.create_all() - - self.building = Building(name="Model Test Building", total_floors=5) - db.session.add(self.building) - db.session.commit() - - self.elevator = Elevator( - building_id=self.building.id, - elevator_number="MODEL-01" - ) - db.session.add(self.elevator) - db.session.commit() - - def tearDown(self): - db.session.remove() - db.drop_all() - self.app_context.pop() - - def test_temporal_features_auto_population(self): - """Test that temporal features are automatically populated.""" - # Create a call with a specific datetime - test_date = datetime(2024, 7, 15, 14, 30) # Monday, July 15, 2024, 2:30 PM - - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=3, - call_time=test_date - ) - - self.assertEqual(call.day_of_week, 0) # Monday - self.assertEqual(call.hour_of_day, 14) # 2 PM - self.assertEqual(call.week_of_month, 3) # Third week of July - self.assertEqual(call.season, 'summer') - - def test_season_calculation_winter(self): - """Test season calculation for winter months.""" - winter_dates = [ - datetime(2024, 12, 15), # December - datetime(2024, 1, 15), # January - datetime(2024, 2, 15) # February - ] - - for test_date in winter_dates: - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=1, - call_time=test_date - ) - self.assertEqual(call.season, 'winter', f"Failed for {test_date}") - - def test_season_calculation_all_seasons(self): - """Test season calculation for all seasons.""" - season_tests = [ - (datetime(2024, 3, 15), 'spring'), - (datetime(2024, 6, 15), 'summer'), - (datetime(2024, 9, 15), 'autumn'), - (datetime(2024, 12, 15), 'winter') - ] - - for test_date, expected_season in season_tests: - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=1, - call_time=test_date - ) - self.assertEqual(call.season, expected_season) - -class MLDataIntegrationTestCase(unittest.TestCase): - """Integration tests for ML data preparation and export.""" - - def setUp(self): - self.app_context = app.app_context() - self.app_context.push() - self.app = app.test_client() - - db.create_all() - - # Create comprehensive test data - self.building = Building( - name="ML Integration Test Building", - total_floors=5, - building_type="mixed" - ) - db.session.add(self.building) - db.session.commit() - - self.elevator = Elevator( - building_id=self.building.id, - elevator_number="ML-01", - current_floor=1 - ) - db.session.add(self.elevator) - db.session.commit() - - # Create diverse call patterns - self._create_test_call_patterns() - - def tearDown(self): - db.session.remove() - db.drop_all() - self.app_context.pop() - - def _create_test_call_patterns(self): - """Create realistic call patterns for testing.""" - import random - - # Morning rush pattern (7-9 AM) - for day in range(5): # Weekdays - for hour in [7, 8]: - for _ in range(random.randint(3, 8)): - call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=1, # Ground floor calls in morning - destination_floor=random.randint(2, 5), - call_time=call_time, - elevator_position_at_call=random.randint(1, 5), - estimated_passengers=random.randint(1, 4) - ) - db.session.add(call) - - # Lunch pattern (12-1 PM) - for day in range(5): - for hour in [12]: - for _ in range(random.randint(2, 5)): - call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=random.randint(2, 5), # Various floors going down - destination_floor=1, - call_time=call_time, - elevator_position_at_call=random.randint(1, 5), - estimated_passengers=random.randint(1, 3) - ) - db.session.add(call) - - # Evening pattern (5-7 PM) - for day in range(5): - for hour in [17, 18]: # 5-6 PM - for _ in range(random.randint(4, 9)): - call_time = datetime.utcnow().replace(hour=hour, minute=random.randint(0, 59)) - call = ElevatorCall( - elevator_id=self.elevator.id, - called_from_floor=random.randint(2, 5), # Various floors going down - destination_floor=1, - call_time=call_time, - elevator_position_at_call=random.randint(1, 5), - estimated_passengers=random.randint(1, 3) - ) - db.session.add(call) - - db.session.commit() - - def test_ml_data_export_structure(self): - """Test that ML data export has correct structure.""" - response = self.app.get(f'/api/ml-data/features/{self.building.id}') - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - - # Verify top-level structure - required_keys = ['building_id', 'total_samples', 'date_range', 'features'] - for key in required_keys: - self.assertIn(key, data) - - # Verify we have samples - self.assertGreater(data['total_samples'], 0) - - # Verify feature structure - if data['features']: - feature = data['features'][0] - required_feature_keys = ['target_floor', 'time_features', 'elevator_features', 'contextual_features'] - for key in required_feature_keys: - self.assertIn(key, feature) - - # Verify time features - time_features = feature['time_features'] - time_required = ['hour_of_day', 'day_of_week', 'week_of_month', 'season'] - for key in time_required: - self.assertIn(key, time_features) - - - def test_ml_data_filtering(self): - """Test ML data filtering by date range.""" - # Test with shorter time range - response = self.app.get(f'/api/ml-data/features/{self.building.id}?days_back=7') - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - - # Should still have structure but possibly fewer samples - self.assertIn('total_samples', data) - self.assertIn('features', data) - -if __name__ == '__main__': - test_suite = unittest.TestSuite() - - test_classes = [ - ElevatorSystemTestCase, - BusinessRulesTestCase, - ElevatorCallModelTestCase, - MLDataIntegrationTestCase - ] - - for test_class in test_classes: - tests = unittest.TestLoader().loadTestsFromTestCase(test_class) - test_suite.addTests(tests) - - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(test_suite) - - # Summary - print(f"\n{'='*50}") - print(f"TESTS RUN: {result.testsRun}") - print(f"FAILURES: {len(result.failures)}") - print(f"ERRORS: {len(result.errors)}") - print(f"SUCCESS RATE: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%") - print(f"{'='*50}") \ No newline at end of file From 05b20735c4ac11efbfbca5672de3892aa9156e86 Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:19:53 +0300 Subject: [PATCH 07/11] finalize readme and improved featuure extraction endpoint --- .gitignore | 4 +- chatgpt/db.sql | 12 -- chatgpt/instance/elevator_data.db | Bin 36864 -> 0 bytes chatgpt/main.py | 333 ------------------------------ chatgpt/requirements.txt | 6 - readme.md | 179 +++++++++++++--- 6 files changed, 151 insertions(+), 383 deletions(-) delete mode 100644 chatgpt/db.sql delete mode 100644 chatgpt/instance/elevator_data.db delete mode 100644 chatgpt/main.py delete mode 100644 chatgpt/requirements.txt diff --git a/.gitignore b/.gitignore index e453be6..151609a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ env/ venv/ ENV/ +# Flask instance folder +instance/ + # IDE .idea/ .vscode/ @@ -43,4 +46,3 @@ Thumbs.db # Database *.sqlite3 *.db - diff --git a/chatgpt/db.sql b/chatgpt/db.sql deleted file mode 100644 index 1555ffe..0000000 --- a/chatgpt/db.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE elevator_demands ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - floor INTEGER -); - -CREATE TABLE elevator_states ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - floor INTEGER, - vacant BOOLEAN -); diff --git a/chatgpt/instance/elevator_data.db b/chatgpt/instance/elevator_data.db deleted file mode 100644 index 9660a10e32261dbe70e7ee68906f728d847c4505..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 zcmeI)y9okO5Cza#b^RdXmaw6bi49nQi5M6QDj0~ch&>ourENGHY-ngAXW%jq-Ur?( zV0PQh<$kKqx8{1d*Q= str: - if month in [12, 1, 2]: - return 'winter' - elif month in [3, 4, 5]: - return 'spring' - elif month in [6, 7, 8]: - return 'summer' - else: - return 'autumn' - -class ElevatorBusinessRules: - @staticmethod - def validate_floor_range(floor: int, building_id: int) -> bool: - """Validate if a floor number is valid for a given building.""" - building = Building.query.get(building_id) - if not building: - return False - return 1 <= floor <= building.total_floors - - @staticmethod - def can_elevator_access_floor(elevator_id: int, floor: int) -> bool: - """Check if an elevator can access a specific floor.""" - elevator = Elevator.query.get(elevator_id) - if not elevator: - return False - - if not ElevatorBusinessRules.validate_floor_range(floor, elevator.building_id): - return False - - if elevator.elevator_number == "FREIGHT" and floor > 5: - return False - - return True - - @staticmethod - def calculate_response_time(call_time: datetime, elevator_position: int, called_floor: int) -> float: - floors_to_travel = abs(elevator_position - called_floor) - return floors_to_travel * 3.0 + 2.0 - - @staticmethod - def should_trigger_maintenance_alert(elevator: Elevator) -> bool: - if not elevator.last_maintenance: - return True - days_since_maintenance = (datetime.utcnow() - elevator.last_maintenance).days - return days_since_maintenance > 30 - -@app.route('/api/buildings', methods=['POST']) -def create_building(): - """Create a new building""" - data = request.get_json() - - if not data or 'name' not in data or 'total_floors' not in data: - return jsonify({'error': 'Missing required fields: name, total_floors'}), 400 - - building = Building( - name=data['name'], - total_floors=data['total_floors'], - building_type=data.get('building_type', 'mixed') - ) - - try: - db.session.add(building) - db.session.commit() - return jsonify({ - 'id': building.id, - 'name': building.name, - 'total_floors': building.total_floors, - 'building_type': building.building_type - }), 201 - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - -@app.route('/api/buildings//elevators', methods=['POST']) -def create_elevator(building_id): - """Add an elevator to a building""" - data = request.get_json() - - if not data or 'elevator_number' not in data: - return jsonify({'error': 'Missing required field: elevator_number'}), 400 - - elevator = Elevator( - building_id=building_id, - elevator_number=data['elevator_number'], - max_capacity=data.get('max_capacity', 1000), - current_floor=data.get('current_floor', 1) - ) - - try: - db.session.add(elevator) - db.session.commit() - return jsonify({ - 'id': elevator.id, - 'elevator_number': elevator.elevator_number, - 'current_floor': elevator.current_floor, - 'building_id': building_id - }), 201 - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - -@app.route('/api/elevators//call', methods=['POST']) -def record_elevator_call(elevator_id): - """Record an elevator call """ - elevator = Elevator.query.get_or_404(elevator_id) - data = request.get_json() - - if not data or 'called_from_floor' not in data: - return jsonify({'error': 'Missing required field: called_from_floor'}), 400 - - called_from_floor = data['called_from_floor'] - - if not ElevatorBusinessRules.validate_floor_range(called_from_floor, elevator.building_id): - return jsonify({'error': 'Invalid floor number'}), 400 - - response_time = ElevatorBusinessRules.calculate_response_time( - datetime.utcnow(), elevator.current_floor, called_from_floor - ) - - call = ElevatorCall( - elevator_id=elevator_id, - called_from_floor=called_from_floor, - destination_floor=data.get('destination_floor'), - elevator_position_at_call=elevator.current_floor, - response_time=response_time, - estimated_passengers=data.get('estimated_passengers', 1), - call_type=data.get('call_type', 'normal'), - weather_condition=data.get('weather_condition') - ) - - try: - db.session.add(call) - db.session.commit() - - if data.get('destination_floor'): - elevator.current_floor = data['destination_floor'] - elevator.is_moving = False - db.session.commit() - - return jsonify({ - 'call_id': call.id, - 'estimated_response_time': response_time, - 'call_time': call.call_time.isoformat(), - 'temporal_features': { - 'day_of_week': call.day_of_week, - 'hour_of_day': call.hour_of_day, - 'week_of_month': call.week_of_month, - 'season': call.season - } - }), 201 - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - -@app.route('/api/elevators//status', methods=['GET']) -def get_elevator_status(elevator_id): - """Get current elevator status""" - elevator = Elevator.query.get_or_404(elevator_id) - - maintenance_alert = ElevatorBusinessRules.should_trigger_maintenance_alert(elevator) - - return jsonify({ - 'elevator_id': elevator.id, - 'elevator_number': elevator.elevator_number, - 'current_floor': elevator.current_floor, - 'current_load': elevator.current_load, - 'is_moving': elevator.is_moving, - 'maintenance_status': elevator.maintenance_status, - 'maintenance_alert': maintenance_alert, - 'building_id': elevator.building_id, - 'max_floors': elevator.building.total_floors - }) - -@app.route('/api/ml-data/features/', methods=['GET']) -def get_ml_features(building_id): - """Get data formatted for ML training""" - building = Building.query.get_or_404(building_id) - - days_back = request.args.get('days_back', 30, type=int) - include_weather = request.args.get('include_weather', False, type=bool) - - cutoff_date = datetime.utcnow() - timedelta(days=days_back) - - calls_query = db.session.query(ElevatorCall).join(Elevator).filter( - Elevator.building_id == building_id, - ElevatorCall.call_time >= cutoff_date - ).all() - - features = [] - for call in calls_query: - feature_row = { - 'target_floor': call.called_from_floor, - 'time_features': { - 'hour_of_day': call.hour_of_day, - 'day_of_week': call.day_of_week, - 'week_of_month': call.week_of_month, - 'season': call.season - }, - 'elevator_features': { - 'elevator_position_at_call': call.elevator_position_at_call, - 'estimated_passengers': call.estimated_passengers, - 'call_type': call.call_type - }, - 'contextual_features': { - 'response_time': call.response_time, - 'building_type': building.building_type, - 'total_floors': building.total_floors - } - } - - if include_weather and call.weather_condition: - feature_row['contextual_features']['weather_condition'] = call.weather_condition - - features.append(feature_row) - - return jsonify({ - 'building_id': building_id, - 'total_samples': len(features), - 'date_range': { - 'from': cutoff_date.isoformat(), - 'to': datetime.utcnow().isoformat() - }, - 'features': features - }) - -@app.route('/health', methods=['GET']) -def health_check(): - return jsonify({ - 'status': 'healthy', - 'timestamp': datetime.utcnow().isoformat(), - 'database': 'connected' - }) - -_initialized = False - -@app.before_request -def before_request(): - global _initialized - if not _initialized: - db.create_all() - - # Create sample data if none exists - if Building.query.count() == 0: - sample_building = Building( - name="Sample Office Building", - total_floors=10, - building_type="office" - ) - db.session.add(sample_building) - db.session.commit() - - sample_elevator = Elevator( - building_id=sample_building.id, - elevator_number="ELV-01", - current_floor=1 - ) - db.session.add(sample_elevator) - db.session.commit() - - _initialized = True - -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/chatgpt/requirements.txt b/chatgpt/requirements.txt deleted file mode 100644 index 0397b32..0000000 --- a/chatgpt/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==2.3.3 -Flask-SQLAlchemy==3.0.5 -SQLAlchemy==2.0.21 -Werkzeug==2.3.7 -pytest==6.2.5 -pytest-flask==1.2.0 \ No newline at end of file diff --git a/readme.md b/readme.md index 2e1e2aa..95599fb 100644 --- a/readme.md +++ b/readme.md @@ -12,59 +12,176 @@ ### 4. Identify obvious and non-obvious factors that could influence the prediction target -### 5. Select the appropriate model that would help best solve the problem, understand its limitations and constraints as well as strategies and techniques to improve performance +### 5. Select the appropriate model that would help best solve the problem, understand its limitations and constraints as well as research strategies and techniques to improve performance ### 6. Prepare the data for training and testing -### 7. Evaluate the models performance and based on results decide whether to iterate and improve training data or use a different approach and model entirely and keep iterating and benchmarking, understanding trade offs and beneffits between approaches +### 7. Evaluate the models performance and based on results decide whether to iterate and improve training data or use a different approach and model entirely and keep iterating and benchmarking, understanding trade offs and benefits between approaches # Elevator resting floor prediction model solution -## Problem statement: -### From my understanding were trying to predict the best possible floor the elevator should rest on according to certain factors such as time of day, week, month, season, etc in order to anticipate which floor demand will possibly come from next so we can provide the best possible and efficient service for users in that particular building +## 1. Problem statement: +### From my understanding were trying to improve the efficiency and service delivery of a buildings elevator by predicting the best floor for the elevator to rest in-between calls. -## Prediction target: -### In order to not only predict the next possible floor at a particular time but provide probabilitiies for all floors at a particular time, i would go for a mutliclass outcome, i think this would allow the elevator to be adjusted to a certain level of accuracy as desired by users +## 2. Prediction target: +### The building consists of multiple floors, and because of this multiple options to consider when predicting the best floor to rest in-between calls, i believe a multiclass predicition target would be the best option for this problem because it not only gives us alternatives but levels of accuracy we can use to adjust service delivery. -## What factors could possibly influence the demand of the elevator (both obvious and non obvious) +## 3. Available data: +### The data we have available is as follows: + +- ### Time of day +- ### Day of week +- ### Week of month +- ### Maintainance schedule (if any) +- ### Elevator load +- ### Current weather season +- ### Current elevator floor + +## 4. What factors could possibly influence the demand of the elevator (both obvious and non obvious) - ### Time of day: - #### We might find that there is a higher demand on certain floors in the mornings, lunch or evenings, e,g a working building where people need the elevator closer to ground floors as people come into work and closer to certain floors as people leave work or go out for lunch + ### We might find that there is a higher demand on certain floors in the mornings, lunch or evenings, e.g a working building where people need the elevator closer to ground floors as people come into work and closer to certain floors as people leave work or go out for lunch - ### Day of the week - #### We might find that more people come into work on monday mornings as compared to friday, if the building is multipurpose, certain floors might have working people coming in while other days house residents and thus less activity on certain floors + ### We might find that more people come into work on monday mornings as compared to friday, if the building is multipurpose, certain floors might have working people coming in while other days house residents and thus less activity on certain floors - ### Week of the month - #### We might discover that certain weeks of the month have more people/traffic as compared to other weeks, e.g retreats, field work weeks, etc -- ### Maintainance schedule that week (if any) - #### This is one of those non obvious factors, we might discover that an elevator that is not regularly maintained is less trusted by users and so they decide maybe to take the stairs, only users that need to travel safer distances might prefer it, e.g a floor above the ground floor, this would also help avoid overfitting and help our model generalize better + ### We might discover that certain weeks of the month have more people/traffic as compared to other weeks, e.g retreats, field work weeks, etc +- ### Maintainance schedule (if any) + ### This is one of those non obvious factors, we might discover that an elevator that is not regularly maintained is less trusted by users and so they decide maybe to take the stairs, only users that need to travel safer distances might prefer it, e.g a floor above the ground floor, this would also help avoid overfitting and help our model generalize better - ### Elevator load - #### We might find that when the elevator is heavy, at a particular time, the elevator tends to move to a certain floor with more people in it. + ### We might find that when the elevator is heavy, at a particular time, the elevator tends to move to a certain floor with more people in it. - ### Current weather season - #### We might find that during winter, christmas or summer, users might not prefer the elevator, or certain floors experience more demand + ### We might find that during winter, christmas or summer, users might not prefer the elevator, or certain floors experience more demand - ### Current elevator floor - #### This is another one of those non obvious ones, naturally i wouldnt think a current elevator floor could influence where people would like to go next, but maybe we might discover a pattern of if the elevator is called from the ground floor at a particular time, users are moving to a working floor for example in a multipurpose building - -- ### Number of calls that day on all floors - #### Understanding how many calls are made on each floor in a day could help us decipher busy floors, this could in our overall prediction, during live prediction this could be accumulated through the day live. - -## Data access - ### Next i need to understand what historical data can actually be collected and fed into our system during training as well as the kind of data that the system will have access to during actual live prediction, the availability of data affects or constraints the model we want to use. - ### Data such as time of day, day of week, week of month, maintainance schedule and current weather season are available natively to our system and can be enhanced by installing addidtional packages, other remaining feattures are usually available in elevator systems - ### With the kind of data i have access to, now would be the best time to plan the model that would work well with this data, i would research on best performing models for my use case and available data as well as time-frame, and in my current scenario that would a Gradient boosting model, specifically CatBoost which has been known to work well with categorical features such as day of week, etc without needing encoding. In terms of timeframe, the categorical handling conveniennce of CatBoost often outweighs the speed cost, my second alternative would be Random forest, with the drawback of having to manually encode categorical features. - - -## Storage Schema - -### For the storage schema i would create a relational database with the following entities and relationships, + ### This is another one of those non obvious ones, naturally i wouldnt think a current elevator floor could influence where people would like to go next, but maybe we might discover a pattern of if the elevator is called from the ground floor at a particular time, users are moving to a working floor for example in a multipurpose building + + + +## Model Selection +### Since i now know our problem, prediction target, available data as well as influencing factors towards our prediction target, both obvious and non-obvious, i would research on the best model for the job, which i found to be the CatBoost model, reason being it handles categorical features such as days of week, etc without needing encoding, this to me is the best place to start experimenting and testing an ideal model for the problem at hand. + + +## Prepare data for training and testing and come up with a suitable database model/schema +### My first thought is to design a relational database with tables as follows: + +### Building: +``` +class Building(db.Model): + __tablename__ = 'buildings' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + total_floors = db.Column(db.Integer, nullable=False) + building_type = db.Column(db.String(50)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + elevators = db.relationship('Elevator', backref='building', lazy=True) + + ``` + + ### This contains metadata about the building as well as a one to many relationship with the Elevator table + + + ### Elevator: + +``` +class Elevator(db.Model): + __tablename__ = 'elevators' + + id = db.Column(db.Integer, primary_key=True) + building_id = db.Column(db.Integer, db.ForeignKey('buildings.id'), nullable=False) + elevator_number = db.Column(db.String(10), nullable=False) + max_capacity = db.Column(db.Integer, default=1000) + current_floor = db.Column(db.Integer, default=1) + current_load = db.Column(db.Integer, default=0) + is_moving = db.Column(db.Boolean, default=False) + maintenance_status = db.Column(db.String(20), default='operational') + last_maintenance = db.Column(db.DateTime) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + calls = db.relationship('ElevatorCall', backref='elevator', lazy=True) +``` + +### This table contains metadata about the elevator as well as a one to many relationship with the Elevator calls table + +### Elevator Calls +``` +class ElevatorCall(db.Model): + __tablename__ = 'elevator_calls' + + id = db.Column(db.Integer, primary_key=True) + elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) + called_from_floor = db.Column(db.Integer, nullable=False) + destination_floor = db.Column(db.Integer) + call_time = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + response_time = db.Column(db.Float) + elevator_position_at_call = db.Column(db.Integer) + estimated_passengers = db.Column(db.Integer, default=1) + call_type = db.Column(db.String(20), default='normal') + day_of_week = db.Column(db.Integer) + hour_of_day = db.Column(db.Integer) + week_of_month = db.Column(db.Integer) + season = db.Column(db.String(10)) + weather_condition = db.Column(db.String(20)) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + call_dt = self.call_time or datetime.utcnow() + self.day_of_week = call_dt.weekday() + self.hour_of_day = call_dt.hour + self.week_of_month = (call_dt.day - 1) + self.season = self._get_season(call_dt.month) + + def _get_season(self, month: int) -> str: + if month in [12, 1, 2]: + return 'winter' + elif month in [3, 4, 5]: + return 'spring' + elif month in [6, 7, 8]: + return 'summer' + else: + return 'autumn' + +``` + +### I also created an endpoint to export training data in a suitable format for training the model, i added options for csv or json, this makes it a lot easier when working within my Notebook to have the right data. + + +## Evaluate the models performance + +``` +eval_metric=[ + 'MultiClass', + 'Accuracy', + 'TotalF1:average=Weighted', + 'AUC:type=MultiClass', + 'Precision:average=Weighted', + 'Recall:average=Weighted' + ], +``` + +### The CatBoost model already comes with pre-built evaluation metrics +### I would initially start with Accuracy as the base metric because it is simpler to track performance over time with statements such, the model predictions the right floor about 70% of the time, i would combine this with user satisfaction score on service delivery efficiency and quality to get a well rounded view of how the model is performing. + +### This would be my process, i would keep iterating, researching until i find an approach and model that not only satifisfies the prediction target but also works well on new data and can generalize well. + + +## Interesting parts of this project +### Modeling how people behave with elevators is interesting, people can be unpredicatable, and simply make a decision out of feelings, maybe one day they simply dont feel like taking an elevator, for no specific reason or influence from external factors + + +## Challenging parts of this project + +### - Network connectivity is a problem, we might need a failsafe to prevent the elevator from not working at all, + +### - What happens when unusual or sudden events occurs, like surprise inspections from authorities at odd times or evacuations -### - Building: Meta data about the building with a one to many relationship with the Elevator, because buildings ideally hold elevators -### - Elevator: Meta data about the elevator with a one to many relationship too ElevatorCall -### - ElevatorCall: Meta data about the elevator call with a many to one relationship with the Elevator to track call demands From e1d8735c8a54685b5845cccc0c69f922fc74db3b Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:34:59 +0300 Subject: [PATCH 08/11] improve readme thought proocess --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 95599fb..aa4d334 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # MY THOUGHT PROCESS -### Whenever im faced with an AI modeling problem, i often love to revert to the fundamentals of developing models then expand from there, i have found this to be a good way to approach problems in a methodical and structured way, because anything that can be broken down into a process can be meausred and anything that can be measured can be improved, this i believe is the very heart of artifiicial intelligence +### Whenever im faced with an AI modeling problem, i often love to revert to the fundamentals of developing models then expand from there, i have found this to be a good way to approach problems in a methodical and structured way, because anything that can be broken down into a process can be measured and anything that can be measured can be improved, this i believe is the very heart of artifiicial intelligence ### My methodical steps are as follows: @@ -8,7 +8,7 @@ ### 2. Identify the prediction target or goal -### 3. Identify the available data that we have access to +### 3. Identify the type of available data that we have access to ### 4. Identify obvious and non-obvious factors that could influence the prediction target @@ -16,7 +16,7 @@ ### 6. Prepare the data for training and testing -### 7. Evaluate the models performance and based on results decide whether to iterate and improve training data or use a different approach and model entirely and keep iterating and benchmarking, understanding trade offs and benefits between approaches +### 7. Evaluate the models performance and based on results and decide whether to iterate and improve training data or use a different approach and model entirely and keep iterating and benchmarking, understanding trade offs and benefits between approaches From 97da45f3b4f49b252041c821926555ffaaac8dbc Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:38:51 +0300 Subject: [PATCH 09/11] Update workflow to run tests on macphail_devtest branch --- .github/workflows/python-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index d828ddc..b32038a 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -2,9 +2,9 @@ name: Python Tests on: pull_request: - branches: [ master ] + branches: [ master, macphail_devtest ] push: - branches: [ master ] + branches: [ master, macphail_devtest ] jobs: test: From f1a6d3701ea1e76f430a41abd577c2afab83773d Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:41:49 +0300 Subject: [PATCH 10/11] Update workflow to use app/requirements.txt --- .github/workflows/python-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index b32038a..ef9dfa4 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -19,11 +19,11 @@ jobs: python-version: '3.x' - name: Install dependencies + working-directory: ./app run: | python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install pytest + pip install -r requirements.txt - name: Run tests working-directory: ./app - run: python -m pytest app_tests.py -v + run: python -m pytest app_tests.py -v \ No newline at end of file From ffbfbdd93d37f89e18e3bc0bfdf188c96a74a410 Mon Sep 17 00:00:00 2001 From: Macphail Date: Tue, 27 May 2025 11:47:47 +0300 Subject: [PATCH 11/11] fix(ci): update workflow for Python 3.11 compatibility --- .github/workflows/python-tests.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index ef9dfa4..950cf23 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -13,12 +13,17 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.11' - - name: Install dependencies + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3-dev build-essential + + - name: Install Python dependencies working-directory: ./app run: | python -m pip install --upgrade pip