From d288cf4f9ef633dab39114008038176f400df112 Mon Sep 17 00:00:00 2001 From: svxf Date: Mon, 12 Nov 2018 15:50:56 +0400 Subject: [PATCH] init --- .gitignore | 6 + Pipfile | 17 + Pipfile.lock | 161 ++++++++++ deploy.sh | 65 ++++ run.sh | 12 + src/manage.py | 15 + src/optimizer/__init__.py | 0 src/optimizer/admin.py | 1 + src/optimizer/apps.py | 5 + src/optimizer/forms.py | 84 +++++ src/optimizer/migrations/0001_initial.py | 54 ++++ src/optimizer/migrations/0002_problem_name.py | 19 ++ .../migrations/0003_auto_20180526_0747.py | 39 +++ .../migrations/0004_auto_20180526_1213.py | 18 ++ .../migrations/0005_auto_20180526_1240.py | 18 ++ .../migrations/0006_auto_20180526_1321.py | 51 +++ .../migrations/0007_auto_20180526_1337.py | 17 + .../migrations/0008_optimizationrun_run_at.py | 21 ++ .../migrations/0009_auto_20180527_1812.py | 27 ++ .../migrations/0010_auto_20180528_0114.py | 18 ++ src/optimizer/migrations/0011_testmodel.py | 21 ++ .../migrations/0012_auto_20180528_1148.py | 18 ++ ...013_optimizationrun_result_xls_filename.py | 18 ++ .../migrations/0014_auto_20180528_2239.py | 32 ++ .../migrations/0015_auto_20180605_0050.py | 23 ++ .../migrations/0016_remove_problem_name.py | 17 + .../0017_remove_optimizationrun_name.py | 17 + src/optimizer/migrations/__init__.py | 0 src/optimizer/models.py | 110 +++++++ src/optimizer/process/__init__.py | 0 src/optimizer/process/params.py | 122 ++++++++ src/optimizer/process/solver.py | 294 ++++++++++++++++++ src/optimizer/process/xlsio.py | 204 ++++++++++++ src/optimizer/templates/base.html | 39 +++ src/optimizer/templates/index.html | 58 ++++ src/optimizer/templates/problem.html | 116 +++++++ src/optimizer/templates/run.html | 192 ++++++++++++ src/optimizer/tests.py | 1 + src/optimizer/urls.py | 21 ++ src/optimizer/views.py | 118 +++++++ src/rzhdweb/__init__.py | 0 src/rzhdweb/settings.py | 143 +++++++++ src/rzhdweb/urls.py | 23 ++ src/rzhdweb/wsgi.py | 16 + src/rzhdweb/zlogging.py | 23 ++ 45 files changed, 2274 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100755 deploy.sh create mode 100755 run.sh create mode 100755 src/manage.py create mode 100644 src/optimizer/__init__.py create mode 100644 src/optimizer/admin.py create mode 100644 src/optimizer/apps.py create mode 100644 src/optimizer/forms.py create mode 100644 src/optimizer/migrations/0001_initial.py create mode 100644 src/optimizer/migrations/0002_problem_name.py create mode 100644 src/optimizer/migrations/0003_auto_20180526_0747.py create mode 100644 src/optimizer/migrations/0004_auto_20180526_1213.py create mode 100644 src/optimizer/migrations/0005_auto_20180526_1240.py create mode 100644 src/optimizer/migrations/0006_auto_20180526_1321.py create mode 100644 src/optimizer/migrations/0007_auto_20180526_1337.py create mode 100644 src/optimizer/migrations/0008_optimizationrun_run_at.py create mode 100644 src/optimizer/migrations/0009_auto_20180527_1812.py create mode 100644 src/optimizer/migrations/0010_auto_20180528_0114.py create mode 100644 src/optimizer/migrations/0011_testmodel.py create mode 100644 src/optimizer/migrations/0012_auto_20180528_1148.py create mode 100644 src/optimizer/migrations/0013_optimizationrun_result_xls_filename.py create mode 100644 src/optimizer/migrations/0014_auto_20180528_2239.py create mode 100644 src/optimizer/migrations/0015_auto_20180605_0050.py create mode 100644 src/optimizer/migrations/0016_remove_problem_name.py create mode 100644 src/optimizer/migrations/0017_remove_optimizationrun_name.py create mode 100644 src/optimizer/migrations/__init__.py create mode 100644 src/optimizer/models.py create mode 100644 src/optimizer/process/__init__.py create mode 100644 src/optimizer/process/params.py create mode 100644 src/optimizer/process/solver.py create mode 100644 src/optimizer/process/xlsio.py create mode 100644 src/optimizer/templates/base.html create mode 100644 src/optimizer/templates/index.html create mode 100644 src/optimizer/templates/problem.html create mode 100644 src/optimizer/templates/run.html create mode 100644 src/optimizer/tests.py create mode 100644 src/optimizer/urls.py create mode 100644 src/optimizer/views.py create mode 100644 src/rzhdweb/__init__.py create mode 100644 src/rzhdweb/settings.py create mode 100644 src/rzhdweb/urls.py create mode 100644 src/rzhdweb/wsgi.py create mode 100644 src/rzhdweb/zlogging.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abf7117 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +db.sqlite3 + +.venv/ +*.log +static/ +__pycache__/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..16cdd77 --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "*" +django-rq = "*" +numpy = "*" +pulp = "*" +pandas = "*" +xlsxwriter = "*" + +[dev-packages] + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..1e2e889 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,161 @@ +{ + "_meta": { + "hash": { + "sha256": "707c3088da92d8c5a3d4d4ace89682526e43dfa8f0104d4709bd0a80453c41cc" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "django": { + "hashes": [ + "sha256:1ffab268ada3d5684c05ba7ce776eaeedef360712358d6a6b340ae9f16486916", + "sha256:dd46d87af4c1bf54f4c926c3cfa41dc2b5c15782f15e4329752ce65f5dad1c37" + ], + "index": "pypi", + "version": "==2.1.3" + }, + "django-rq": { + "hashes": [ + "sha256:982ea7e636ebe328126acfd4cb977dc2cb5ed4181a4124b551052439560fc3e7", + "sha256:fd57a9a33d504dcce0e79a2f08bcf2fb8336d22ca1fff263ab54a530f7596a0a" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "numpy": { + "hashes": [ + "sha256:0df89ca13c25eaa1621a3f09af4c8ba20da849692dcae184cb55e80952c453fb", + "sha256:154c35f195fd3e1fad2569930ca51907057ae35e03938f89a8aedae91dd1b7c7", + "sha256:18e84323cdb8de3325e741a7a8dd4a82db74fde363dce32b625324c7b32aa6d7", + "sha256:1e8956c37fc138d65ded2d96ab3949bd49038cc6e8a4494b1515b0ba88c91565", + "sha256:23557bdbca3ccbde3abaa12a6e82299bc92d2b9139011f8c16ca1bb8c75d1e95", + "sha256:24fd645a5e5d224aa6e39d93e4a722fafa9160154f296fd5ef9580191c755053", + "sha256:36e36b6868e4440760d4b9b44587ea1dc1f06532858d10abba98e851e154ca70", + "sha256:3d734559db35aa3697dadcea492a423118c5c55d176da2f3be9c98d4803fc2a7", + "sha256:416a2070acf3a2b5d586f9a6507bb97e33574df5bd7508ea970bbf4fc563fa52", + "sha256:4a22dc3f5221a644dfe4a63bf990052cc674ef12a157b1056969079985c92816", + "sha256:4d8d3e5aa6087490912c14a3c10fbdd380b40b421c13920ff468163bc50e016f", + "sha256:4f41fd159fba1245e1958a99d349df49c616b133636e0cf668f169bce2aeac2d", + "sha256:561ef098c50f91fbac2cc9305b68c915e9eb915a74d9038ecf8af274d748f76f", + "sha256:56994e14b386b5c0a9b875a76d22d707b315fa037affc7819cda08b6d0489756", + "sha256:73a1f2a529604c50c262179fcca59c87a05ff4614fe8a15c186934d84d09d9a5", + "sha256:7da99445fd890206bfcc7419f79871ba8e73d9d9e6b82fe09980bc5bb4efc35f", + "sha256:99d59e0bcadac4aa3280616591fb7bcd560e2218f5e31d5223a2e12a1425d495", + "sha256:a4cc09489843c70b22e8373ca3dfa52b3fab778b57cf81462f1203b0852e95e3", + "sha256:a61dc29cfca9831a03442a21d4b5fd77e3067beca4b5f81f1a89a04a71cf93fa", + "sha256:b1853df739b32fa913cc59ad9137caa9cc3d97ff871e2bbd89c2a2a1d4a69451", + "sha256:b1f44c335532c0581b77491b7715a871d0dd72e97487ac0f57337ccf3ab3469b", + "sha256:b261e0cb0d6faa8fd6863af26d30351fd2ffdb15b82e51e81e96b9e9e2e7ba16", + "sha256:c857ae5dba375ea26a6228f98c195fec0898a0fd91bcf0e8a0cae6d9faf3eca7", + "sha256:cf5bb4a7d53a71bb6a0144d31df784a973b36d8687d615ef6a7e9b1809917a9b", + "sha256:db9814ff0457b46f2e1d494c1efa4111ca089e08c8b983635ebffb9c1573361f", + "sha256:df04f4bad8a359daa2ff74f8108ea051670cafbca533bb2636c58b16e962989e", + "sha256:ecf81720934a0e18526177e645cbd6a8a21bb0ddc887ff9738de07a1df5c6b61", + "sha256:edfa6fba9157e0e3be0f40168eb142511012683ac3dc82420bee4a3f3981b30e" + ], + "index": "pypi", + "version": "==1.15.4" + }, + "pandas": { + "hashes": [ + "sha256:11975fad9edbdb55f1a560d96f91830e83e29bed6ad5ebf506abda09818eaf60", + "sha256:12e13d127ca1b585dd6f6840d3fe3fa6e46c36a6afe2dbc5cb0b57032c902e31", + "sha256:1c87fcb201e1e06f66e23a61a5fea9eeebfe7204a66d99df24600e3f05168051", + "sha256:242e9900de758e137304ad4b5663c2eff0d798c2c3b891250bd0bd97144579da", + "sha256:26c903d0ae1542890cb9abadb4adcb18f356b14c2df46e4ff657ae640e3ac9e7", + "sha256:2e1e88f9d3e5f107b65b59cd29f141995597b035d17cc5537e58142038942e1a", + "sha256:31b7a48b344c14691a8e92765d4023f88902ba3e96e2e4d0364d3453cdfd50db", + "sha256:4fd07a932b4352f8a8973761ab4e84f965bf81cc750fb38e04f01088ab901cb8", + "sha256:5b24ca47acf69222e82530e89111dd9d14f9b970ab2cd3a1c2c78f0c4fbba4f4", + "sha256:647b3b916cc8f6aeba240c8171be3ab799c3c1b2ea179a3be0bd2712c4237553", + "sha256:66b060946046ca27c0e03e9bec9bba3e0b918bafff84c425ca2cc2e157ce121e", + "sha256:6efa9fa6e1434141df8872d0fa4226fc301b17aacf37429193f9d70b426ea28f", + "sha256:be4715c9d8367e51dbe6bc6d05e205b1ae234f0dc5465931014aa1c4af44c1ba", + "sha256:bea90da782d8e945fccfc958585210d23de374fa9294a9481ed2abcef637ebfc", + "sha256:d318d77ab96f66a59e792a481e2701fba879e1a453aefeebdb17444fe204d1ed", + "sha256:d785fc08d6f4207437e900ffead930a61e634c5e4f980ba6d3dc03c9581748c7", + "sha256:de9559287c4fe8da56e8c3878d2374abc19d1ba2b807bfa7553e912a8e5ba87c", + "sha256:f4f98b190bb918ac0bc0e3dd2ab74ff3573da9f43106f6dba6385406912ec00f", + "sha256:f71f1a7e2d03758f6e957896ed696254e2bc83110ddbc6942018f1a232dd9dad", + "sha256:fb944c8f0b0ab5c1f7846c686bc4cdf8cde7224655c12edcd59d5212cd57bec0" + ], + "index": "pypi", + "version": "==0.23.4" + }, + "pulp": { + "hashes": [ + "sha256:07dffada5bdd02f20f21285388894da9da0be840498789c43f59e10c90205109" + ], + "index": "pypi", + "version": "==1.6.9" + }, + "pyparsing": { + "hashes": [ + "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", + "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" + ], + "version": "==2.3.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", + "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" + ], + "version": "==2.7.5" + }, + "pytz": { + "hashes": [ + "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", + "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" + ], + "version": "==2018.7" + }, + "redis": { + "hashes": [ + "sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb", + "sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f" + ], + "version": "==2.10.6" + }, + "rq": { + "hashes": [ + "sha256:5dd83625ca64b0dbf668ee65a8d38f3f5132aa9b64de4d813ff76f97db194b60", + "sha256:7ac5989a27bdff713dd40517498c1b3bf720f8ebc47305055496f653a29da899" + ], + "version": "==0.12.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "xlsxwriter": { + "hashes": [ + "sha256:7cc07619760641b67112dbe0df938399d4d915d9b9924bb58eb5c17384d29cc6", + "sha256:ae22658a0fc5b9e875fa97c213d1ffd617d86dc49bf08be99ebdac814db7bf36" + ], + "index": "pypi", + "version": "==1.1.2" + } + }, + "develop": {} +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..00e3d1e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,65 @@ +#!/bin/zsh + +NUM_RQWORKERS=4; + +cd /home/misc-user/rzhdweb + +echo "Pulling" +if git pull; then + echo "OK" +else + echo "=====ERROR=====" + exit 1 +fi + +echo "Migrating" +if DEBUG=TRUE ./manage.py migrate; then + echo "OK" +else + echo "=====ERROR=====" + exit 1 +fi + +echo "Restarting a-rzhdweb" +if sudo /bin/systemctl restart a-rzhdweb; then + echo "OK" +else + echo "=====ERROR=====" + exit 1 +fi + +echo "Waiting" +sleep 3 + +echo "Checking a-rzhdweb" +if sudo /bin/systemctl is-active a-rzhdweb; then + echo "OK" +else + echo "=====ERROR=====" + sudo /bin/systemctl status a-rzhdweb + exit 1 +fi + +echo "Restarting rqworkers" +for i in {1.."$NUM_RQWORKERS"}; do + if sudo /bin/systemctl restart a-rzhdweb-rqworker@"$i"; then + echo "$i"" - OK" + else + echo "=====ERROR=====" + exit 1 + fi +done + +echo "Waiting" +sleep 3 + +echo "Checking a worker" +if sudo /bin/systemctl is-active a-rzhdweb-rqworker@"$NUM_RQWORKERS"; then + echo "OK" +else + echo "=====ERROR=====" + sudo /bin/systemctl status a-rzhdweb-rqworker@"$NUM_RQWORKERS" + exit 1 +fi + +echo "Deploy successful" diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..5a0d0d1 --- /dev/null +++ b/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env zsh + +set euxo PIPEFAIL + +export PIPENV_VENV_IN_PROJECT=1 +export PYTHONPATH=src/ + +git pull + +pipenv install + +pipenv run python3.7 src/manage.py runserver 2716 diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..b4155b8 --- /dev/null +++ b/src/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rzhdweb.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/src/optimizer/__init__.py b/src/optimizer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/optimizer/admin.py b/src/optimizer/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/optimizer/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/optimizer/apps.py b/src/optimizer/apps.py new file mode 100644 index 0000000..4ba27ad --- /dev/null +++ b/src/optimizer/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OptimizerConfig(AppConfig): + name = 'optimizer' diff --git a/src/optimizer/forms.py b/src/optimizer/forms.py new file mode 100644 index 0000000..eb2b85a --- /dev/null +++ b/src/optimizer/forms.py @@ -0,0 +1,84 @@ +from django import forms + +from optimizer.models import Problem, CriticalDistanceParameter +from optimizer.process import params +from optimizer.process.xlsio import read_param_set, read_critical_distances + + +class UploadProblemForm(forms.Form): + file = forms.FileField(label='') + + +types_to_fields = { + int: forms.IntegerField, + bool: forms.BooleanField, + float: forms.FloatField, +} + + +class CreateRunForm(forms.Form): + def __init__(self, *args, problem=None, **kwargs): + super().__init__(*args, **kwargs) + + self.param_set = read_param_set(problem.xls_filename) + + self.param_fields = {} + for param in self.param_set.all_params.values(): + field_name = f'param_{param.name}' + field = types_to_fields[param.type_f](label=param.description, initial=param.value) + + self.fields[field_name] = field + self.param_fields[field_name] = param + + critical_distances = read_critical_distances(problem.xls_filename, problem.get_distances()) + self.cd_fields = [] + cd_counter = 1 + for cd in critical_distances: + field_name_1 = f'cd_{cd_counter}_name' + field_1 = forms.CharField(label=f'Критическая дистанция #{cd_counter}', + initial=cd.distance.name, required=False) + self.fields[field_name_1] = field_1 + + field_name_2 = f'cd_{cd_counter}_value' + field_2 = forms.IntegerField(label=f'Максимальное количество тележек', initial=cd.value, + required=False) + self.fields[field_name_2] = field_2 + + self.cd_fields.append([field_name_1, field_name_2]) + + cd_counter += 1 + + for i in range(1, 6): + field_name_1 = f'new_cd_{i}_name' + field_1 = forms.CharField(label=f'Дополнительная критическая дистанция #{i}', required=False) + self.fields[field_name_1] = field_1 + + field_name_2 = f'new_cd_{i}_value' + field_2 = forms.IntegerField(label=f'Максимальное количество тележек', required=False) + self.fields[field_name_2] = field_2 + + self.cd_fields.append([field_name_1, field_name_2]) + + def get_param_set(self): + param_dict = {} + + for field_name, param in self.param_fields.items(): + param_dict[param.name] = self.cleaned_data[field_name] + + return params.build_param_set_from_values(param_dict) + + def get_critical_distances(self, problem: Problem): + distances = {d.name: d for d in problem.get_distances()} + critical_distances = {} + + for field_name_1, field_name_2 in self.cd_fields: + distance_name = self.cleaned_data[field_name_1] + value = self.cleaned_data[field_name_2] + + if distance_name not in distances: + continue + + critical_distances[distance_name] = CriticalDistanceParameter( + run=None, distance=distances[distance_name], value=value) + + return critical_distances diff --git a/src/optimizer/migrations/0001_initial.py b/src/optimizer/migrations/0001_initial.py new file mode 100644 index 0000000..61d7dac --- /dev/null +++ b/src/optimizer/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 2.0.5 on 2018-05-24 17:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Distance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime', models.DateTimeField()), + ], + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('km', models.FloatField()), + ('sp', models.FloatField()), + ('distance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Distance')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Problem')), + ], + ), + migrations.CreateModel( + name='Worker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('hours', models.FloatField()), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Problem')), + ], + ), + migrations.AddField( + model_name='distance', + name='problem', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Problem'), + ), + ] diff --git a/src/optimizer/migrations/0002_problem_name.py b/src/optimizer/migrations/0002_problem_name.py new file mode 100644 index 0000000..bf8ba8d --- /dev/null +++ b/src/optimizer/migrations/0002_problem_name.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.5 on 2018-05-24 18:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='problem', + name='name', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/src/optimizer/migrations/0003_auto_20180526_0747.py b/src/optimizer/migrations/0003_auto_20180526_0747.py new file mode 100644 index 0000000..d90a18e --- /dev/null +++ b/src/optimizer/migrations/0003_auto_20180526_0747.py @@ -0,0 +1,39 @@ +# Generated by Django 2.0.5 on 2018-05-26 03:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0002_problem_name'), + ] + + operations = [ + migrations.AddField( + model_name='problem', + name='xls_filename', + field=models.CharField(default='', max_length=255), + preserve_default=False, + ), + migrations.AlterField( + model_name='distance', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='problem', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='task', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='worker', + name='name', + field=models.CharField(max_length=255), + ), + ] diff --git a/src/optimizer/migrations/0004_auto_20180526_1213.py b/src/optimizer/migrations/0004_auto_20180526_1213.py new file mode 100644 index 0000000..e80aa92 --- /dev/null +++ b/src/optimizer/migrations/0004_auto_20180526_1213.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-05-26 08:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0003_auto_20180526_0747'), + ] + + operations = [ + migrations.AlterField( + model_name='problem', + name='datetime', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/src/optimizer/migrations/0005_auto_20180526_1240.py b/src/optimizer/migrations/0005_auto_20180526_1240.py new file mode 100644 index 0000000..83d6f07 --- /dev/null +++ b/src/optimizer/migrations/0005_auto_20180526_1240.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-05-26 08:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0004_auto_20180526_1213'), + ] + + operations = [ + migrations.RenameField( + model_name='problem', + old_name='datetime', + new_name='created_at', + ), + ] diff --git a/src/optimizer/migrations/0006_auto_20180526_1321.py b/src/optimizer/migrations/0006_auto_20180526_1321.py new file mode 100644 index 0000000..672ede8 --- /dev/null +++ b/src/optimizer/migrations/0006_auto_20180526_1321.py @@ -0,0 +1,51 @@ +# Generated by Django 2.0.5 on 2018-05-26 09:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0005_auto_20180526_1240'), + ] + + operations = [ + migrations.CreateModel( + name='CriticalDistanceParameter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.IntegerField()), + ('distance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Distance')), + ], + ), + migrations.CreateModel( + name='OptimizationParameter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('value', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='OptimizationRun', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('name', models.CharField(max_length=255)), + ('was_run', models.BooleanField()), + ('goal_value', models.FloatField()), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.Problem')), + ], + ), + migrations.AddField( + model_name='optimizationparameter', + name='run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.OptimizationRun'), + ), + migrations.AddField( + model_name='criticaldistanceparameter', + name='run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.OptimizationRun'), + ), + ] diff --git a/src/optimizer/migrations/0007_auto_20180526_1337.py b/src/optimizer/migrations/0007_auto_20180526_1337.py new file mode 100644 index 0000000..676bab8 --- /dev/null +++ b/src/optimizer/migrations/0007_auto_20180526_1337.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.5 on 2018-05-26 09:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0006_auto_20180526_1321'), + ] + + operations = [ + migrations.RenameModel( + old_name='OptimizationParameter', + new_name='OptimizationParameterValue', + ), + ] diff --git a/src/optimizer/migrations/0008_optimizationrun_run_at.py b/src/optimizer/migrations/0008_optimizationrun_run_at.py new file mode 100644 index 0000000..85a3a44 --- /dev/null +++ b/src/optimizer/migrations/0008_optimizationrun_run_at.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.5 on 2018-05-26 15:13 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0007_auto_20180526_1337'), + ] + + operations = [ + migrations.AddField( + model_name='optimizationrun', + name='run_at', + field=models.DateTimeField(default=datetime.datetime(2018, 5, 26, 15, 13, 22, 17432, tzinfo=utc)), + preserve_default=False, + ), + ] diff --git a/src/optimizer/migrations/0009_auto_20180527_1812.py b/src/optimizer/migrations/0009_auto_20180527_1812.py new file mode 100644 index 0000000..cfb3075 --- /dev/null +++ b/src/optimizer/migrations/0009_auto_20180527_1812.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.5 on 2018-05-27 14:12 + +from django.db import migrations, models +import optimizer.models + + +class Migration(migrations.Migration): + dependencies = [ + ('optimizer', '0008_optimizationrun_run_at'), + ] + + operations = [ + migrations.RemoveField( + model_name='optimizationrun', + name='run_at', + ), + migrations.RemoveField( + model_name='optimizationrun', + name='was_run', + ), + migrations.AddField( + model_name='optimizationrun', + name='status', + field=models.CharField( + default=optimizer.models.OptimizationRunStatus('Не запущен').value, max_length=255), + ), + ] diff --git a/src/optimizer/migrations/0010_auto_20180528_0114.py b/src/optimizer/migrations/0010_auto_20180528_0114.py new file mode 100644 index 0000000..bb862b2 --- /dev/null +++ b/src/optimizer/migrations/0010_auto_20180528_0114.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-05-27 21:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0009_auto_20180527_1812'), + ] + + operations = [ + migrations.AlterField( + model_name='optimizationrun', + name='status', + field=models.CharField(choices=[('Не запущен', 'Не запущен'), ('Считается', 'Считается'), ('Посчитан', 'Посчитан')], default='Не запущен', max_length=255), + ), + ] diff --git a/src/optimizer/migrations/0011_testmodel.py b/src/optimizer/migrations/0011_testmodel.py new file mode 100644 index 0000000..5eeec40 --- /dev/null +++ b/src/optimizer/migrations/0011_testmodel.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.5 on 2018-05-28 07:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0010_auto_20180528_0114'), + ] + + operations = [ + migrations.CreateModel( + name='TestModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('f1', models.CharField(max_length=255)), + ('f2', models.IntegerField()), + ], + ), + ] diff --git a/src/optimizer/migrations/0012_auto_20180528_1148.py b/src/optimizer/migrations/0012_auto_20180528_1148.py new file mode 100644 index 0000000..9369cf9 --- /dev/null +++ b/src/optimizer/migrations/0012_auto_20180528_1148.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-05-28 07:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0011_testmodel'), + ] + + operations = [ + migrations.AlterField( + model_name='optimizationrun', + name='goal_value', + field=models.FloatField(default=None, null=True), + ), + ] diff --git a/src/optimizer/migrations/0013_optimizationrun_result_xls_filename.py b/src/optimizer/migrations/0013_optimizationrun_result_xls_filename.py new file mode 100644 index 0000000..17080d2 --- /dev/null +++ b/src/optimizer/migrations/0013_optimizationrun_result_xls_filename.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-05-28 07:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0012_auto_20180528_1148'), + ] + + operations = [ + migrations.AddField( + model_name='optimizationrun', + name='result_xls_filename', + field=models.CharField(default=None, max_length=255, null=True), + ), + ] diff --git a/src/optimizer/migrations/0014_auto_20180528_2239.py b/src/optimizer/migrations/0014_auto_20180528_2239.py new file mode 100644 index 0000000..d7fbe2e --- /dev/null +++ b/src/optimizer/migrations/0014_auto_20180528_2239.py @@ -0,0 +1,32 @@ +# Generated by Django 2.0.5 on 2018-05-28 18:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0013_optimizationrun_result_xls_filename'), + ] + + operations = [ + migrations.CreateModel( + name='OptimizationMetric', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('value', models.CharField(max_length=255)), + ], + ), + migrations.AddField( + model_name='optimizationrun', + name='status', + field=models.CharField(default='Не запущен', max_length=255), + ), + migrations.AddField( + model_name='optimizationmetric', + name='run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='optimizer.OptimizationRun'), + ), + ] diff --git a/src/optimizer/migrations/0015_auto_20180605_0050.py b/src/optimizer/migrations/0015_auto_20180605_0050.py new file mode 100644 index 0000000..10bbb30 --- /dev/null +++ b/src/optimizer/migrations/0015_auto_20180605_0050.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.5 on 2018-06-04 20:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0014_auto_20180528_2239'), + ] + + operations = [ + migrations.AddField( + model_name='optimizationrun', + name='finished_at', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='optimizationrun', + name='started_at', + field=models.DateTimeField(null=True), + ), + ] diff --git a/src/optimizer/migrations/0016_remove_problem_name.py b/src/optimizer/migrations/0016_remove_problem_name.py new file mode 100644 index 0000000..873ce87 --- /dev/null +++ b/src/optimizer/migrations/0016_remove_problem_name.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.5 on 2018-06-04 21:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0015_auto_20180605_0050'), + ] + + operations = [ + migrations.RemoveField( + model_name='problem', + name='name', + ), + ] diff --git a/src/optimizer/migrations/0017_remove_optimizationrun_name.py b/src/optimizer/migrations/0017_remove_optimizationrun_name.py new file mode 100644 index 0000000..ecc9256 --- /dev/null +++ b/src/optimizer/migrations/0017_remove_optimizationrun_name.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.5 on 2018-06-04 21:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('optimizer', '0016_remove_problem_name'), + ] + + operations = [ + migrations.RemoveField( + model_name='optimizationrun', + name='name', + ), + ] diff --git a/src/optimizer/migrations/__init__.py b/src/optimizer/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/optimizer/models.py b/src/optimizer/models.py new file mode 100644 index 0000000..3fa9eeb --- /dev/null +++ b/src/optimizer/models.py @@ -0,0 +1,110 @@ +from enum import Enum + +from django.db import models + +from optimizer.process.params import build_param_set_from_orms + + +class Problem(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + xls_filename = models.CharField(max_length=255) + + def get_workers(self): + return list(Worker.objects.filter(problem=self).all()) + + def get_distances(self): + return list(Distance.objects.filter(problem=self).all()) + + def get_tasks(self): + return list(Task.objects.filter(problem=self).all()) + + def get_runs(self): + return list(OptimizationRun.objects.filter(problem=self).all()) + + def __str__(self): + created_at_str = self.created_at.strftime('%Y-%m-%d') + task_n = len(self.get_tasks()) + distance_n = len(self.get_distances()) + worker_n = len(self.get_workers()) + + return f'#{self.id} @ {created_at_str}: {task_n}тележ/{distance_n}дист/{worker_n}инж' + + +class Worker(models.Model): + problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + hours = models.FloatField() + + +class Distance(models.Model): + problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + + +class Task(models.Model): + problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + distance = models.ForeignKey(Distance, on_delete=models.CASCADE) + km = models.FloatField() + sp = models.FloatField() + + +class OptimizationRunStatus(Enum): + NOT_STARTED = 'Не запущен' + QUEUED = 'В очереди' + IN_PROGRESS = 'Считается' + COMPLETED_OPTIMAL = 'Посчитан, найдено оптимальное решение' + COMPLETED_NOT_SOLVED = 'Посчитан' + COMPLETED_UNBOUNDED = 'Посчитан, оптимизация не ограничена' + COMPLETED_INFEASIBLE = 'Посчитан, решений не существует' + ERROR = 'Ошибка' + + +class OptimizationRun(models.Model): + problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + started_at = models.DateTimeField(null=True) + finished_at = models.DateTimeField(null=True) + + status = models.CharField(max_length=255, default=OptimizationRunStatus.NOT_STARTED.value) + + goal_value = models.FloatField(null=True, default=None) + result_xls_filename = models.CharField(max_length=255, null=True, default=None) + + def __str__(self): + created_at_str = self.created_at.strftime('%Y-%m-%d') + return f'#{self.problem.id} -> #{self.id} @ {created_at_str}: {self.status}' + + def get_param_set(self): + param_orms = list(OptimizationParameterValue.objects.filter(run=self).all()) + + return build_param_set_from_orms(param_orms) + + def get_critical_distances(self): + return list(CriticalDistanceParameter.objects.filter(run=self).all()) + + def get_metrics(self): + return list(OptimizationMetric.objects.filter(run=self).all()) + + +class OptimizationParameterValue(models.Model): + run = models.ForeignKey(OptimizationRun, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + value = models.CharField(max_length=255) + + +class CriticalDistanceParameter(models.Model): + run = models.ForeignKey(OptimizationRun, on_delete=models.CASCADE) + distance = models.ForeignKey(Distance, on_delete=models.CASCADE) + value = models.IntegerField() + + +class OptimizationMetric(models.Model): + run = models.ForeignKey(OptimizationRun, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + value = models.CharField(max_length=255) + + +class TestModel(models.Model): + f1 = models.CharField(max_length=255) + f2 = models.IntegerField() diff --git a/src/optimizer/process/__init__.py b/src/optimizer/process/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/optimizer/process/params.py b/src/optimizer/process/params.py new file mode 100644 index 0000000..eda2565 --- /dev/null +++ b/src/optimizer/process/params.py @@ -0,0 +1,122 @@ +from typing import Dict + + +class OptimizationParameter: + def __init__(self, name, description, type_f, value, + param_orm=None): + self.name = name + self.description = description + self.type_f = type_f + self.value = value + self.param_orm = param_orm + + +class OptimizationParamSet: + def __init__(self): + self.all_params = {} # type: Dict[str, OptimizationParameter] + + def declare_param(name, type_f, value, description): + from optimizer.models import OptimizationParameterValue + + param_orm = OptimizationParameterValue(name=name, value=str(value)) + param = OptimizationParameter(name=name, description=description, type_f=type_f, value=value, + param_orm=param_orm) + + self.all_params[name] = param + + return param + + self.solver_timeout = ( + declare_param('solver_timeout', int, 60, + 'Временное ограничение в секундах')) + + self.restriction_max_km_diff = ( + declare_param('restriction_max_km_diff', float, 100, + 'Ограничение: максимально допустимое отклонение км')) + + self.restriction_max_sp_diff = ( + declare_param('restriction_max_sp_diff', float, 100, + 'Ограничение: максимально допустимое отклонение сп')) + + self.restriction_max_agg_diff = ( + declare_param('restriction_max_agg_diff', float, 100, + 'Ограничение: максимально допустимое отклонение аггрегата (км+сп/4)')) + + self.restriction_max_distance_count = ( + declare_param('restriction_max_distance_count', int, 100, + 'Ограничение: максимально допустимое количество дистанций')) + + self.restriction_avg_distance_count = ( + declare_param('restriction_avg_distance_count', int, 100, + 'Ограничение: максимально допустимое среднее количество дистанций')) + + self.restriction_max_task_count = ( + declare_param('restriction_max_task_count', int, 100, + 'Ограничение: максимально допустимое количество тележек')) + + self.goal_max_km_diff_weight = ( + declare_param('goal_max_km_diff_weight', float, 5, + 'Целевая функция: примерное максимальное отклонение км')) + + self.goal_max_sp_diff_weight = ( + declare_param('goal_max_sp_diff_weight', float, 10, + 'Целевая функция: примерное максимальное отклонение сп')) + + self.goal_max_agg_diff_weight = ( + declare_param('goal_max_agg_diff_weight', float, 10, + 'Целевая функция: примерное максимальное отклонение аггрегата (км+сп/4)')) + + self.goal_max_distance_count_weight = ( + declare_param('goal_max_distance_count_weight', float, 5, + 'Целевая функция: примерное максимальное количества дистанций')) + + self.goal_avg_distance_count_weight = ( + declare_param('goal_avg_distance_count_weight', float, 4.5, + 'Целевая функция: примерное среднее количества дистанций')) + + self.goal_max_task_count_weight = ( + declare_param('goal_max_task_count_weight', float, 10, + 'Целевая функция: примерное максимальное количество тележек')) + + self.shuffle_workers = ( + declare_param('shuffle_workers', bool, True, + 'Дополнительно перемешать инженеров?')) + + self.seed = ( + declare_param('seed', int, -1, + 'Зерно рандома')) + + def change_param(self, name, value, replace_orm=None): + if name not in self.all_params: + return + + param = self.all_params[name] + + param.value = param.type_f(value) + + if replace_orm is not None: + param.param_orm = replace_orm + + param.param_orm.value = str(value) + + def save(self): + for param in self.all_params.values(): + param.param_orm.save() + + +def build_param_set_from_values(param_dict: dict): + param_set = OptimizationParamSet() + + for name, value in param_dict.items(): + param_set.change_param(name=name, value=value) + + return param_set + + +def build_param_set_from_orms(param_orms): + param_set = OptimizationParamSet() + + for param_orm in param_orms: + param_set.change_param(name=param_orm.name, value=param_orm.value, replace_orm=param_orm) + + return param_set diff --git a/src/optimizer/process/solver.py b/src/optimizer/process/solver.py new file mode 100644 index 0000000..0a9474c --- /dev/null +++ b/src/optimizer/process/solver.py @@ -0,0 +1,294 @@ +import numpy as np +import pulp +from django.utils import timezone + +from optimizer.models import Problem, OptimizationRun, OptimizationMetric, OptimizationRunStatus +from optimizer.process import xlsio +from rzhdweb.zlogging import logger + +SP_AGG_WEIGHT = 0.25 + + +def run_solver(problem, run): + lp_solver = LPSolver(problem, run) + lp_solver.solve() + + +class LPSolver: + def __init__(self, problem: Problem, run: OptimizationRun): + self.problem = problem + self.run = run + + self.tasks = self.problem.get_tasks() + self.task_n = len(self.tasks) + + self.distances = self.problem.get_distances() + self.distance_n = len(self.distances) + + self.workers = self.problem.get_workers() + self.worker_n = len(self.workers) + + self.param_set = self.run.get_param_set() + + # init random + if self.param_set.seed.value == -1: + self.param_set.change_param('seed', np.random.randint(1000000000)) + self.param_set.seed.param_orm.save() + logger.info(f'new seed: {self.param_set.seed.value}') + np.random.seed(self.param_set.seed.value) + + if self.param_set.shuffle_workers.value: + logger.info('shuffled') + np.random.shuffle(self.workers) + + self.critical_distances = self.run.get_critical_distances() + self.critical_distances_n = len(self.critical_distances) + + # init np + self._task_kms = np.array([t.km for t in self.tasks], dtype=float) + self._task_sps = np.array([t.sp for t in self.tasks], dtype=float) + self._task_aggs = self._task_kms + self._task_sps * SP_AGG_WEIGHT + + self._distance_name_to_id = {j.name: i for i, j in enumerate(self.distances)} + + self._task_distances = np.array( + [self._distance_name_to_id[t.distance.name] for t in self.tasks], + dtype=int) + + self._expected_worker_ratio = np.array([w.hours for w in self.workers], dtype=float) + self._expected_worker_ratio /= np.sum(self._expected_worker_ratio) + + self._expected_worker_km = self._expected_worker_ratio * np.sum(self._task_kms) + self._expected_worker_sp = self._expected_worker_ratio * np.sum(self._task_sps) + self._expected_worker_agg = self._expected_worker_ratio * np.sum(self._task_aggs) + + self._critical_distances = np.array( + [self._distance_name_to_id[i.distance.name] for i in self.critical_distances], + dtype=int) + self._critical_distance_bounds = np.array( + [i.value for i in self.critical_distances], + dtype=int) + + def formulate_lp(self): + logger.info('Formulating') + self.lp_problem = pulp.LpProblem('test problem', pulp.LpMinimize) + + # Helper variables + max_km_diff = pulp.LpVariable('max_km_diff', 0, None, pulp.LpContinuous) + max_sp_diff = pulp.LpVariable('max_sp_diff', 0, None, pulp.LpContinuous) + max_agg_diff = pulp.LpVariable('max_agg_diff', 0, None, pulp.LpContinuous) + max_distance_count = pulp.LpVariable('max_distance_count', 0, None, pulp.LpContinuous) + avg_distance_count = pulp.LpVariable('avg_distance_count', 0, None, pulp.LpContinuous) + max_task_count = pulp.LpVariable('max_task_count', 0, None, pulp.LpContinuous) + + # Worker x Task binary choice matrix + self._worker_task_choices = np.zeros((self.worker_n, self.task_n), dtype=object) + for w_i in range(self.worker_n): + for t_i in range(self.task_n): + self._worker_task_choices[w_i, t_i] = \ + pulp.LpVariable(f'worker_task_choice[{w_i},{t_i}]', None, None, pulp.LpBinary) + + # Worker task counts + worker_task_counts = np.zeros(self.worker_n, dtype=object) + for w_i in range(self.worker_n): + worker_task_counts[w_i] = pulp.lpSum(self._worker_task_choices[w_i, :]) + + # Restriction: one worker per task + for t_i in range(self.task_n): + self.lp_problem += pulp.lpSum(self._worker_task_choices[:, t_i]) == 1 + + # Worker x Distance task counts + worker_distance_task_counts = np.zeros((self.worker_n, self.distance_n), dtype=object) + for w_i in range(self.worker_n): + for t_i in range(self.task_n): + d_i = self._task_distances[t_i] + worker_distance_task_counts[w_i, d_i] += self._worker_task_choices[w_i, t_i] + + # Worker x Distance binary choice matrix + worker_distance_choices = np.zeros((self.worker_n, self.distance_n), dtype=object) + for w_i in range(self.worker_n): + for d_i in range(self.distance_n): + worker_distance_choices[w_i, d_i] = \ + pulp.LpVariable(f'DistanceChoice[w{w_i},d{d_i}]', None, None, pulp.LpBinary) + + # Restriction: Distance choices are an upper bound on Task choices + for w_i in range(self.worker_n): + for t_i in range(self.task_n): + d_i = self._task_distances[t_i] + self.lp_problem += worker_distance_choices[w_i, d_i] >= self._worker_task_choices[w_i, t_i] + + # Restriction: km, sp and agg diffs are in specified intervals + for w_i in range(self.worker_n): + worker_km = pulp.lpDot(self._worker_task_choices[w_i, :], self._task_kms) + self.lp_problem += worker_km >= self._expected_worker_km[w_i] - max_km_diff + self.lp_problem += worker_km <= self._expected_worker_km[w_i] + max_km_diff + + worker_sp = pulp.lpDot(self._worker_task_choices[w_i, :], self._task_sps) + self.lp_problem += worker_sp >= self._expected_worker_sp[w_i] - max_sp_diff + self.lp_problem += worker_sp <= self._expected_worker_sp[w_i] + max_sp_diff + + worker_agg = pulp.lpDot(self._worker_task_choices[w_i, :], self._task_aggs) + self.lp_problem += worker_agg >= self._expected_worker_agg[w_i] - max_agg_diff + self.lp_problem += worker_agg <= self._expected_worker_agg[w_i] + max_agg_diff + + # Restriction: distance counts are below max + worker_distance_counts = np.zeros(self.worker_n, dtype=object) + for w_i in range(self.worker_n): + worker_distance_counts[w_i] = pulp.lpSum(worker_distance_choices[w_i, :]) + self.lp_problem += worker_distance_counts[w_i] <= max_distance_count + + # Restriction: avg distance count is below max too + worker_avg_dist_count = pulp.lpSum(worker_distance_counts) / self.worker_n + self.lp_problem += worker_avg_dist_count <= avg_distance_count + + # Restriction: task counts are in specified intervals + for w_i in range(self.worker_n): + self.lp_problem += worker_task_counts[w_i] <= max_task_count + + # Restriction: critical distances + for w_i in range(self.worker_n): + for cd_i in range(self.critical_distances_n): + d_i = self._critical_distances[cd_i] + bound = self._critical_distance_bounds[cd_i] + + self.lp_problem += worker_distance_task_counts[w_i, d_i] <= bound + + # Restrictions: params + self.lp_problem += max_km_diff <= self.param_set.restriction_max_km_diff.value + self.lp_problem += max_sp_diff <= self.param_set.restriction_max_sp_diff.value + self.lp_problem += max_agg_diff <= self.param_set.restriction_max_agg_diff.value + self.lp_problem += max_distance_count <= self.param_set.restriction_max_distance_count.value + self.lp_problem += worker_avg_dist_count <= self.param_set.restriction_avg_distance_count.value + self.lp_problem += max_task_count <= self.param_set.restriction_max_task_count.value + + # Aggregation of all goals + compound_bound = pulp.LpVariable('compound_bound', 0, None, pulp.LpContinuous) + scaled_restrictions = [] + + # Goal restrictions + + GOAL_ADJUSTMENT_WEIGHT = 1e-3 + + def add_compound_restriction(restriction, weight_param): + scaled_restriction = restriction * (1 / weight_param.value) + scaled_restrictions.append(scaled_restriction) + + self.lp_problem += compound_bound >= scaled_restriction + + add_compound_restriction(max_km_diff, self.param_set.goal_max_km_diff_weight) + add_compound_restriction(max_sp_diff, self.param_set.goal_max_sp_diff_weight) + add_compound_restriction(max_agg_diff, self.param_set.goal_max_agg_diff_weight) + add_compound_restriction(max_distance_count, self.param_set.goal_max_distance_count_weight) + add_compound_restriction(avg_distance_count, self.param_set.goal_avg_distance_count_weight) + add_compound_restriction(max_task_count, self.param_set.goal_max_task_count_weight) + + goal_adjusted = compound_bound - pulp.lpSum(scaled_restrictions) * GOAL_ADJUSTMENT_WEIGHT + + self.lp_problem += goal_adjusted, 'goal function' + + def make_metrics(self): + self.metrics = [] + + def make_metric(name, value): + m = OptimizationMetric(run=self.run, + name=name, + value=str(value)) + + self.metrics.append(m) + m.save() + + def make_basic_array_metrics(suffix, a, skip_mean=False): + make_metric(f'Разброс {suffix}', '{:.4g} ... {:.4g}'.format(np.min(a), np.max(a))) + if not skip_mean: + make_metric(f'Среднее {suffix}', '{:.4g}'.format(np.mean(a))) + + make_metric('Значение целевой функции', f'{self.goal_value:.4g}') + + make_metric('Решение удовлетворяет ограничениям', self.is_valid) + + worker_km = np.zeros(self.worker_n, dtype=float) + np.add.at(worker_km, self.assignment, self._task_kms) + make_basic_array_metrics('км на инженера', worker_km) + + worker_km_diff = worker_km - self._expected_worker_km + make_basic_array_metrics('отклонения от ожидаемых км на инженера', worker_km_diff, True) + + worker_sp = np.zeros(self.worker_n, dtype=float) + np.add.at(worker_sp, self.assignment, self._task_sps) + make_basic_array_metrics('сп на инженера', worker_sp) + + worker_sp_diff = worker_sp - self._expected_worker_sp + make_basic_array_metrics('отклонения от ожидаемых сп на инженера', worker_sp_diff, True) + + worker_agg = np.zeros(self.worker_n, dtype=float) + np.add.at(worker_agg, self.assignment, self._task_aggs) + make_basic_array_metrics('аггрегата на инженера', worker_agg) + + worker_agg_diff = worker_agg - self._expected_worker_agg + make_basic_array_metrics('отклонения от ожидаемого аггрегата на инженера', worker_agg_diff, + True) + + worker_distance_task_counts = np.zeros((self.worker_n, self.distance_n), dtype=int) + np.add.at( + worker_distance_task_counts, + (self.assignment, self._task_distances), + np.ones(self.task_n)) + + worker_distance_counts = np.count_nonzero(worker_distance_task_counts, axis=1) + make_basic_array_metrics('количества дистанций на инженера', worker_distance_counts) + + worker_task_counts = np.zeros(self.worker_n, dtype=int) + np.add.at(worker_task_counts, self.assignment, np.ones(self.task_n)) + make_basic_array_metrics('количества тележек на инженера', worker_task_counts) + + def solve(self): + try: + logger.info('Starting an lp problem') + self.run.started_at = timezone.now() + self.run.status = OptimizationRunStatus.IN_PROGRESS.value + self.run.save() + + solver = pulp.solvers.PULP_CBC_CMD(maxSeconds=self.param_set.solver_timeout.value, msg=0) + + logger.info('Formulating an lp problem') + self.formulate_lp() + + logger.info('Calling solver') + self.lp_problem.solve(solver) + self.lp_status = pulp.LpStatus[self.lp_problem.status] + logger.info(f'Solved {self.lp_status}') + + worker_task_choices_result = np.vectorize(pulp.value)(self._worker_task_choices) + self.assignment = np.argmax(worker_task_choices_result, axis=0) + logger.info(f'Assignment {self.assignment}') + + self.goal_value = pulp.value(self.lp_problem.objective) + self.run.goal_value = self.goal_value + + self.is_valid = self.lp_problem.valid() + + self.make_metrics() + + filename = xlsio.write_solution(self.assignment, self.tasks, self.workers, self.metrics, + self.run.id) + + self.run.result_xls_filename = filename + + self.run.finished_at = timezone.now() + + if self.lp_status == 'Unbounded': + self.run.status = OptimizationRunStatus.COMPLETED_UNBOUNDED.value + elif self.lp_status == 'Infeasible': + self.run.status = OptimizationRunStatus.COMPLETED_INFEASIBLE.value + elif self.lp_status == 'Not Solved': + self.run.status = OptimizationRunStatus.COMPLETED_NOT_SOLVED.value + elif self.lp_status == 'Optimal': + self.run.status = OptimizationRunStatus.COMPLETED_OPTIMAL.value + else: + raise Exception(f'bad lp status {self.lp_status}') + except Exception as e: + logger.exception(e) + + self.run.status = OptimizationRunStatus.ERROR.value + finally: + self.run.save() diff --git a/src/optimizer/process/xlsio.py b/src/optimizer/process/xlsio.py new file mode 100644 index 0000000..5794b5b --- /dev/null +++ b/src/optimizer/process/xlsio.py @@ -0,0 +1,204 @@ +import os +import uuid +from typing import List + +import numpy as np +import pandas +from django.conf import settings + +from optimizer.models import Problem, Distance, Task, Worker, CriticalDistanceParameter +from optimizer.process.params import build_param_set_from_values +from rzhdweb.zlogging import logger + + +def create_problem_from_xls(file): + os.makedirs(settings.PROBLEM_UPLOAD_DIR, exist_ok=True) + + name = f'input-{uuid.uuid4().hex}.xlsx' + path = os.path.join(settings.PROBLEM_UPLOAD_DIR, name) + + with open(path, 'wb') as destination: + for chunk in file.chunks(): + destination.write(chunk) + + return read_problem_from_file(path) + + +def read_problem_from_file(filename): + problem = Problem(xls_filename=filename) + + distances, tasks = read_distances_and_tasks(filename) + + workers = read_workers(filename) + + # check + read_param_set(filename) + read_critical_distances(filename, distances) + + problem.save() + for i in distances: + i.problem = problem + i.save() + for i in tasks: + i.problem = problem + i.distance_id = i.distance.id + i.save() + for i in workers: + i.problem = problem + i.save() + + return problem + + +def read_distances_and_tasks(filename): + df = pandas.read_excel( + filename, + header=None, + sheet_name='тележки', + names='distance_name task_name task_km task_sp'.split(), + dtype={ + 'distance_name': str, + 'task_name': str, + 'task_km': float, + 'task_sp': float, + }, + skiprows=1 + ) + df.fillna(0, inplace=True) + + df['distance_name'] = df['distance_name'].apply(str).apply(str.upper) + df['task_name'] = df['task_name'].apply(str).apply(str.upper) + + distances = [] + for name in set(df['distance_name']): + distances.append(Distance(name=name)) + + tasks = [] + for _, (distance_name, task_name, task_km, task_sp) in df.iterrows(): + d = next(filter(lambda x: x.name == distance_name, distances)) + + t = Task(distance=d, name=task_name, km=task_km, sp=task_sp) + tasks.append(t) + + return distances, tasks + + +def read_workers(filename): + df = pandas.read_excel( + filename, + header=None, + sheet_name='часы', + names='worker_name hour_count'.split(), + dtype={ + 'worker_name': str, + 'hour_count': float, + }, + skiprows=1 + ) + df['worker_name'] = df['worker_name'].apply(str).apply(str.upper) + + workers = [] + for _, (worker_name, hour_count) in df.iterrows(): + w = Worker(name=worker_name, hours=hour_count) + workers.append(w) + + return workers + + +def read_param_set(filename): + df = pandas.read_excel( + filename, + header=None, + sheet_name='параметры', + names='param_name param_value comment'.split(), + dtype={ + 'param_name': str, + 'param_value': str, + 'comment': str + }, + skiprows=1 + ) + + param_dict = {} + + for _, (param_name, param_value, comment) in df.iterrows(): + param_dict[param_name] = param_value + + return build_param_set_from_values(param_dict) + + +def read_critical_distances(filename, distances): + df = pandas.read_excel( + filename, + header=None, + sheet_name='критические дистанции', + names='distance_name count'.split(), + dtype={ + 'distance_name': str, + 'count': int, + }, + skiprows=1 + ) + + df['distance_name'] = df['distance_name'].apply(str).apply(str.upper) + + critical_distances = [] # type: List[CriticalDistanceParameter] + for _, (distance_name, count) in df.iterrows(): + try: + d = next(filter(lambda x: x.name == distance_name, distances)) + except Exception: + logger.warn(f'no matching distance {distance_name}') + continue + + critical_distances.append(CriticalDistanceParameter(distance=d, value=count)) + + return critical_distances + + +def write_solution(assignment, tasks, workers, metrics, run_id): + os.makedirs(settings.PROBLEM_SOLUTION_DIR, exist_ok=True) + + name = f'Решение-#{run_id:04d}.xlsx' + filename = os.path.join(settings.PROBLEM_SOLUTION_DIR, name) + + xls_writer = pandas.ExcelWriter(f'{filename}', engine='xlsxwriter', + options={'strings_to_numbers': True}) + + task_assignment_table = [] + + for worker_id, task in zip(assignment, tasks): + task_assignment_table.append([ + task.name, + task.distance.name, + workers[int(worker_id)].name, + task.km, + task.sp]) + + task_assignment_table = np.array(task_assignment_table, dtype=str) + + task_assignment_table = task_assignment_table[ + np.argsort(task_assignment_table[:, 1], kind='mergesort')] + task_assignment_table = task_assignment_table[ + np.argsort(task_assignment_table[:, 2], kind='mergesort')] + + task_assignment_table = pandas.DataFrame(task_assignment_table, + columns=['тележка', 'дистанция', 'инженер', 'км', + 'сп']) + task_assignment_table.to_excel(xls_writer, sheet_name='решение', index=False, float_format='.0') + + description_table = [] + for i in metrics: + description_table.append([i.name, i.value]) + description_table = pandas.DataFrame(description_table, columns=['характеристика', 'значение']) + description_table.to_excel(xls_writer, sheet_name='характеристики', index=False) + + # param_table = [] + # for param in self.params.values(): + # param_table.append([param.name, param.value, param.description]) + # + # param_table = pandas.DataFrame(param_table, columns=['название', 'значение', 'описание']) + # param_table.to_excel(xls_writer, sheet_name='параметры', index=False) + + xls_writer.save() + + return filename diff --git a/src/optimizer/templates/base.html b/src/optimizer/templates/base.html new file mode 100644 index 0000000..d4593e3 --- /dev/null +++ b/src/optimizer/templates/base.html @@ -0,0 +1,39 @@ +{% load static %} + + + + + + + {% block title %} + Оптимизатор инженеров + {% endblock %} + + + + + + + + + {% block head %} + {% endblock %} + + +{% block body %} +
+ {% block main %} + {% endblock %} +
+{% endblock %} + + diff --git a/src/optimizer/templates/index.html b/src/optimizer/templates/index.html new file mode 100644 index 0000000..3b57ace --- /dev/null +++ b/src/optimizer/templates/index.html @@ -0,0 +1,58 @@ +{% extends 'base.html' %} + +{% block main %} +
+
+
+
+

Последние задачи

+
+
+ +
+
+
+ +
+
+
+

Последние запуски

+
+
+ +
+
+
+
+ +
+
+

Загрузить новую задачу

+
+
+
+ {% csrf_token %} + {{ upload_problem_form }} + + +
+
+
+ +{% endblock main %} diff --git a/src/optimizer/templates/problem.html b/src/optimizer/templates/problem.html new file mode 100644 index 0000000..ca2afe3 --- /dev/null +++ b/src/optimizer/templates/problem.html @@ -0,0 +1,116 @@ +{% extends 'base.html' %} + +{% block main %} + + + +
+
+

Задача #{{ problem.id }}

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
Создана{{ problem.created_at }}
Тележек{{ tasks|length }}
Дистанций{{ distances|length }}
Инженеров{{ workers|length }}
Файл с условиемЗагрузить
+
+ + +
+
+

Запуски

+
+
+ + {% if runs %} +
+
+

История

+
+
+
    + {% for run in runs reversed %} +
  • + + + {{ run }} + + +
  • + {% endfor %} +
+
+
+ {% endif %} + +
+
+

Новый

+
+
+ +
+ {% csrf_token %} + +
+ +
+
+ + + {{ create_run_form.as_table }} +
+ +
+
+
+ + +
+ +
+
+ +
+
+ +
+
+ +{% endblock %} diff --git a/src/optimizer/templates/run.html b/src/optimizer/templates/run.html new file mode 100644 index 0000000..bbd9b79 --- /dev/null +++ b/src/optimizer/templates/run.html @@ -0,0 +1,192 @@ +{% extends 'base.html' %} + +{% block main %} + + + +
+
+

Запуск #{{ run.id }}

+
+
+ +
+ + + + + + + {% if run.started_at %} + + + + + {% endif %} + + {% if run.finished_at %} + + + + + {% endif %} + + + + + + +
+ {% if run.goal_value %} +
+ + + + {% endif %} + + + {% if run.result_xls_filename %} + + + + + {% endif %} +
Создан{{ run.created_at }}
Запущен{{ run.started_at }}
Посчитан{{ run.finished_at }}
Статус + {{ run.status }} + + {% if run.status == 'Не запущен' %} +
+ +
+ {% endif %} + + {% if duration_total %} +
+
+ 0% +
+
+ + + {% endif %} +
Значение целевой функции{{ run.goal_value }}
Файл с решением + Загрузить +
+
+ + + + {% if metrics %} +
+ +
+
+ + {% for i in metrics %} + + + + + {% endfor %} +
{{ i.name }}{{ i.value }}
+
+
+
+ {% endif %} + + + +
+ +
+
+ + {% for i in critical_distances %} + + + + + {% endfor %} +
{{ i.distance.name }}{{ i.value }}
+
+
+
+ + + +
+ +
+
+ + {% for i in param_set.all_params.values %} + + + + + {% endfor %} +
{{ i.description }}{{ i.value }}
+
+
+
+ +
+
+{% endblock %} diff --git a/src/optimizer/tests.py b/src/optimizer/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/src/optimizer/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/src/optimizer/urls.py b/src/optimizer/urls.py new file mode 100644 index 0000000..2b3744b --- /dev/null +++ b/src/optimizer/urls.py @@ -0,0 +1,21 @@ +from django.urls import path, include + +from optimizer import views + +urlpatterns = [ + # rq + path('django-rq/', include('django_rq.urls')), + + path('', + views.view_index, name='index'), + path('upload_problem', + views.view_upload_problem, name='upload_problem'), + path('problems/', + views.view_problem, name='problem'), + path('problems//create_run', + views.view_problem_create_run, name='problem_create_run'), + path('problems//runs/', + views.view_problem_run, name='problem_run'), + path('problems//runs//start', + views.view_problem_run_start, name='problem_run_start'), +] diff --git a/src/optimizer/views.py b/src/optimizer/views.py new file mode 100644 index 0000000..2cc0487 --- /dev/null +++ b/src/optimizer/views.py @@ -0,0 +1,118 @@ +import django_rq +from django.shortcuts import render, redirect, get_object_or_404 +from django.utils import timezone + +from optimizer.forms import UploadProblemForm, CreateRunForm +from optimizer.models import Problem, OptimizationRun, OptimizationRunStatus +from optimizer.process.solver import run_solver +from optimizer.process.xlsio import create_problem_from_xls +from rzhdweb.zlogging import logger + + +def view_index(request): + return render(request, 'index.html', { + 'latest_problems': Problem.objects.order_by('-created_at')[:5], + 'latest_runs': OptimizationRun.objects.order_by('-created_at')[:5], + 'upload_problem_form': UploadProblemForm() + }) + + +def view_upload_problem(request): + form = UploadProblemForm(request.POST, request.FILES) + + if not form.is_valid(): + return redirect('index') + + try: + problem = create_problem_from_xls(form.files['file']) + except Exception as e: + logger.exception(e) + return redirect('index') + + return redirect('problem', problem_id=problem.id) + + +def view_problem(request, problem_id): + problem = get_object_or_404(Problem, id=problem_id) + + return render(request, 'problem.html', { + 'problem': problem, + 'distances': problem.get_distances(), + 'tasks': problem.get_tasks(), + 'workers': problem.get_workers(), + 'runs': problem.get_runs(), + 'create_run_form': CreateRunForm(problem=problem), + }) + + +def view_problem_create_run(request, problem_id): + problem = get_object_or_404(Problem, id=problem_id) + + form = CreateRunForm(request.POST, problem=problem) + + if not form.is_valid(): + return render(request, 'problem.html', { + 'problem': problem, + 'distances': problem.get_distances(), + 'tasks': problem.get_tasks(), + 'workers': problem.get_workers(), + 'runs': problem.get_runs(), + 'create_run_form': form, + }) + + run = OptimizationRun(problem=problem) + + param_set = form.get_param_set() + + critical_distances = form.get_critical_distances(problem) + + run.save() + + for param in param_set.all_params.values(): + param.param_orm.run = run + + param_set.save() + for i in critical_distances.values(): + i.run = run + i.save() + + run.save() + + return redirect('problem_run', problem_id=problem.id, run_id=run.id) + + +def view_problem_run(request, problem_id, run_id): + problem = get_object_or_404(Problem, id=problem_id) + run = get_object_or_404(OptimizationRun, id=run_id) + + param_set = run.get_param_set() + + ctx = { + 'problem': problem, + 'run': run, + 'param_set': param_set, + 'critical_distances': run.get_critical_distances(), + 'metrics': run.get_metrics() + } + + if run.status == OptimizationRunStatus.IN_PROGRESS.value and run.started_at: + started_at = run.started_at + ctx['duration_total'] = param_set.solver_timeout.value + ctx['duration_passed'] = min((timezone.now() - started_at).total_seconds(), + ctx['duration_total']) + + return render(request, 'run.html', ctx) + + +def view_problem_run_start(request, problem_id, run_id): + problem = get_object_or_404(Problem, id=problem_id) + run = get_object_or_404(OptimizationRun, id=run_id) + + if run.status == OptimizationRunStatus.NOT_STARTED.value: + django_rq.enqueue(run_solver, problem=problem, run=run) + run.status = OptimizationRunStatus.QUEUED.value + run.save() + else: + logger.warn(f'Not starting run {run}') + + return redirect('problem_run', problem_id=problem.id, run_id=run.id) diff --git a/src/rzhdweb/__init__.py b/src/rzhdweb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/rzhdweb/settings.py b/src/rzhdweb/settings.py new file mode 100644 index 0000000..8a6279d --- /dev/null +++ b/src/rzhdweb/settings.py @@ -0,0 +1,143 @@ +""" +Django settings for rzhdweb project. + +Generated by 'django-admin startproject' using Django 2.0.5. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.0/ref/settings/ +""" +import datetime +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'z4-*8__++a(z-2+p-jd6i9j7fnbq7#@$e^efv4en(t0lj4e82o' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get('DEBUG') == 'TRUE' + +ALLOWED_HOSTS = [ + 'rzhd.abra.me', + 'localhost', + '127.0.0.1', +] + +# Application definition + +INSTALLED_APPS = [ + 'optimizer.apps.OptimizerConfig', + + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'rzhdweb.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'rzhdweb.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Europe/Samara' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.0/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'static') +] + +TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates'),) + +# App specific + +PROBLEM_UPLOAD_DIR = 'static/problems/' +PROBLEM_SOLUTION_DIR = 'static/problem_solutions/' + +# RQ + +INSTALLED_APPS += ['django_rq'] +RQ_QUEUES = { + 'default': { + 'HOST': 'localhost', + 'PORT': 6379, + 'DB': 0, + 'DEFAULT_TIMEOUT': datetime.timedelta(days=1).total_seconds(), + } +} diff --git a/src/rzhdweb/urls.py b/src/rzhdweb/urls.py new file mode 100644 index 0000000..527fdb3 --- /dev/null +++ b/src/rzhdweb/urls.py @@ -0,0 +1,23 @@ +"""rzhdweb URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('', include('optimizer.urls')), + + path('admin/', admin.site.urls), +] diff --git a/src/rzhdweb/wsgi.py b/src/rzhdweb/wsgi.py new file mode 100644 index 0000000..4833f20 --- /dev/null +++ b/src/rzhdweb/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for rzhdweb project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rzhdweb.settings") + +application = get_wsgi_application() diff --git a/src/rzhdweb/zlogging.py b/src/rzhdweb/zlogging.py new file mode 100644 index 0000000..e2d2bcf --- /dev/null +++ b/src/rzhdweb/zlogging.py @@ -0,0 +1,23 @@ +import logging + +logger = logging.getLogger('rzhdweb') +logger.setLevel(logging.DEBUG) + +formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(module)s:%(lineno)d - %(levelname)s\n %(message)s') + +debug_file = logging.FileHandler('debug.log') +debug_file.setLevel(logging.DEBUG) +debug_file.setFormatter(formatter) +logger.addHandler(debug_file) + +warning_file = logging.FileHandler('warning.log') +warning_file.setLevel(logging.WARNING) +warning_file.setFormatter(formatter) +logger.addHandler(warning_file) + +info_console = logging.StreamHandler() +info_console.setLevel(logging.INFO) +# info_console.setLevel(logging.WARNING) +info_console.setFormatter(formatter) +logger.addHandler(info_console)