diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/chatgpt/__init__.py b/chatgpt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatgpt/app_tests.py b/chatgpt/app_tests.py index 258a8a6..28c4bdd 100644 --- a/chatgpt/app_tests.py +++ b/chatgpt/app_tests.py @@ -1,10 +1,14 @@ def test_create_demand(client): - response = client.post('/demand', json={'floor': 3}) + response = client.post('/demand', json={'floor': 3,'elevator_id':1}) assert response.status_code == 201 assert response.get_json() == {'message': 'Demand created'} def test_create_state(client): - response = client.post('/state', json={'floor': 5, 'vacant': True}) + response = client.post('/state', json={'floor': 5, 'vacant': True,'elevator_id':1}) assert response.status_code == 201 assert response.get_json() == {'message': 'State created'} + +def test_create_elevator(client): + response = client.post('/elevator',json={'name':"Elevator A"}) + assert response.status_code == 201 \ No newline at end of file diff --git a/chatgpt/elevator.db b/chatgpt/elevator.db new file mode 100644 index 0000000..da30cb5 Binary files /dev/null and b/chatgpt/elevator.db differ diff --git a/chatgpt/main.py b/chatgpt/main.py index 7f97d98..0ec7c66 100644 --- a/chatgpt/main.py +++ b/chatgpt/main.py @@ -1,29 +1,25 @@ -from flask import Flask, request, jsonify +from flask import Flask, Blueprint, request, jsonify from flask_sqlalchemy import SQLAlchemy from datetime import datetime +from models import db, Elevator, ElevatorDemand, ElevatorState +import pandas as pd app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///elevator.db' -db = SQLAlchemy(app) - - -class ElevatorDemand(db.Model): - id = db.Column(db.Integer, primary_key=True) - timestamp = db.Column(db.DateTime, default=datetime.utcnow) - floor = db.Column(db.Integer, nullable=False) - - -class ElevatorState(db.Model): - 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) +db.init_app(app) +@app.route('/elevator', methods=['POST']) +def create_elevator(): + data = request.get_json() + elevator = Elevator(name=data['name']) + db.session.add(elevator) + db.session.commit() + return jsonify({'message': 'Elevator created', 'elevator_id': elevator.id}), 201 @app.route('/demand', methods=['POST']) def create_demand(): data = request.get_json() - new_demand = ElevatorDemand(floor=data['floor']) + new_demand = ElevatorDemand(floor=data['floor'],elevator_id=data['elevator_id']) db.session.add(new_demand) db.session.commit() return jsonify({'message': 'Demand created'}), 201 @@ -32,12 +28,68 @@ def create_demand(): @app.route('/state', methods=['POST']) def create_state(): data = request.get_json() - new_state = ElevatorState(floor=data['floor'], vacant=data['vacant']) + new_state = ElevatorState( + floor=data['floor'], + vacant=data['vacant'], + elevator_id=data['elevator_id'] + ) db.session.add(new_state) db.session.commit() return jsonify({'message': 'State created'}), 201 +@app.route('/dataset', methods=['GET']) +def generate_dataset(): + date_str = request.args.get('date') # YYYY-MM-DD + enddate_str = request.args.get('enddate') # YYYY-MM-DD + holiday_param = request.args.get('holiday', 'false').lower() + if not date_str: + return jsonify({'error': 'You must provide a date in format YYYY-MM-DD'}), 400 + + try: + date = pd.to_datetime(date_str) + except: + return jsonify({'error': 'Invalid date format'}), 400 + + start = pd.Timestamp(date.date()) + if enddate_str: + try: + # end = pd.to_datetime(enddate_str) + end = pd.to_datetime(enddate_str) + pd.Timedelta(days=1) + except: + return jsonify({'error': 'Invalid end date format'}), 400 + else: + end = start + pd.Timedelta(days=1) + is_holiday = holiday_param in ['true', '1', 'yes'] + + demands = ElevatorDemand.query.filter( + ElevatorDemand.timestamp >= start, + ElevatorDemand.timestamp < end + ).all() + + data = [{ + 'timestamp': d.timestamp, + 'floor': d.floor + } for d in demands] + + df = pd.DataFrame(data) + + if df.empty: + return jsonify([]) + + # features + df['hour'] = df['timestamp'].dt.hour + df['minute'] = df['timestamp'].dt.minute + df['weekday'] = df['timestamp'].dt.weekday + df['month'] = df['timestamp'].dt.month + df['is_holiday'] = is_holiday + df['is_weekend'] = df['weekday'].isin([5, 6]) + + df['timestamp'] = df['timestamp'].dt.strftime('%Y-%m-%dT%H:%M:%S') + + return jsonify(df.to_dict(orient='records')) + if __name__ == '__main__': - db.create_all() + with app.app_context(): + db.create_all() app.run(debug=True) diff --git a/chatgpt/models.py b/chatgpt/models.py new file mode 100644 index 0000000..0e07731 --- /dev/null +++ b/chatgpt/models.py @@ -0,0 +1,35 @@ +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +class ElevatorDemand(db.Model): + __tablename__ = 'elevator_demands' + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime, default=datetime.now) + floor = db.Column(db.Integer, nullable=False) + + elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) + elevator = db.relationship('Elevator', back_populates='demands') + + +class ElevatorState(db.Model): + __tablename__ = 'elevator_states' + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime, default=datetime.now) + floor = db.Column(db.Integer, nullable=False) + vacant = db.Column(db.Boolean, nullable=False) + + elevator_id = db.Column(db.Integer, db.ForeignKey('elevators.id'), nullable=False) + elevator = db.relationship('Elevator', back_populates='states') + +class Elevator(db.Model): + __tablename__ = 'elevators' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), nullable=False) + + demands = db.relationship('ElevatorDemand', back_populates='elevator', cascade="all, delete-orphan") + states = db.relationship('ElevatorState', back_populates='elevator', cascade="all, delete-orphan") + + diff --git a/chatgpt/requirements.txt b/chatgpt/requirements.txt index 14d1bb0..193e26b 100644 --- a/chatgpt/requirements.txt +++ b/chatgpt/requirements.txt @@ -2,3 +2,5 @@ Flask==2.0.2 Flask-SQLAlchemy==2.5.1 pytest==6.2.5 pytest-flask==1.2.0 +Werkzeug==2.2.3 +SQLAlchemy==1.4.46 diff --git a/readme.md b/readme.md index ea5e444..e2c86e3 100644 --- a/readme.md +++ b/readme.md @@ -56,3 +56,38 @@ Below is a list of some things from previous submissions that haven't worked out - 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 + + +## Answer + +### Data Structure and Modeling + +Three main tables are defined in the relational database: + - `Elevator`: Represents a single elevator. This enables the system to scale to multiple elevators, each with its own state and demand events. + - `ElevatorDemand` : Logs every time an elevator is requested from a floor. It includes a `timestamp`, `floor`, and `elevator_id`. + - `ElevatorState`: Periodically logs the current state of an elevator (e.g., current floor and whether it's vacancy). Useful for operational tracking, this table is not directly involved in demand prediction. + + ### Key Assumptions + + +1. Demand is Independent of Elevator State: + + - Users do not know the elevator's position when they request it. + + - Therefore, demand is modeled as an external event driven by time patterns, not internal elevator logic. + +2. Time-Based Demand Prediction: + + - The ML model aims to predict the floor from which the next request will likely occur, using features such as: + + - Time of day + - Day of the week + - Whether it is a holiday (is_holiday) + - Whether it is a weekend (is_weekend) + + - The elevator's current location is not used in the prediction. + +3. Proactive Elevator Movement (future feature): + + If an elevator remains idle for a defined period, it could use the ML model to proactively move to the floor most likely to generate a request. +