This commit is contained in:
2018-11-12 15:50:56 +04:00
commit d288cf4f9e
45 changed files with 2274 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
db.sqlite3
.venv/
*.log
static/
__pycache__/

17
Pipfile Normal file
View File

@@ -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"

161
Pipfile.lock generated Normal file
View File

@@ -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": {}
}

65
deploy.sh Executable file
View File

@@ -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"

12
run.sh Executable file
View File

@@ -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

15
src/manage.py Executable file
View File

@@ -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)

View File

1
src/optimizer/admin.py Normal file
View File

@@ -0,0 +1 @@
# Register your models here.

5
src/optimizer/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class OptimizerConfig(AppConfig):
name = 'optimizer'

84
src/optimizer/forms.py Normal file
View File

@@ -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

View File

@@ -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'),
),
]

View File

@@ -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,
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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'),
),
]

View File

@@ -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',
),
]

View File

@@ -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,
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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()),
],
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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',
),
]

View File

110
src/optimizer/models.py Normal file
View File

@@ -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()

View File

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -0,0 +1,39 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}
Оптимизатор инженеров
{% endblock %}
</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
{% block head %}
{% endblock %}
</head>
<body>
{% block body %}
<div class="container">
{% block main %}
{% endblock %}
</div>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% block main %}
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h2>Последние задачи</h2>
</div>
<div class="card-body">
<ul>
{% for problem in latest_problems %}
<li>
<a href="{% url 'problem' problem.id %}">
<strong>{{ problem }}</strong>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
<h2>Последние запуски</h2>
</div>
<div class="card-body">
<ul>
{% for run in latest_runs %}
<li>
<a href="{% url 'problem_run' run.problem.id run.id %}">
<strong>{{ run }}</strong>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Загрузить новую задачу</h2>
</div>
<div class="card-body">
<form action="{% url 'upload_problem' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ upload_problem_form }}
<input class="btn btn-primary" type="submit" value="Загрузить">
</form>
</div>
</div>
{% endblock main %}

View File

@@ -0,0 +1,116 @@
{% extends 'base.html' %}
{% block main %}
<div id='breadcrumbs'>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">
Главная
</a></li>
<li class="breadcrumb-item active">
Задача #{{ problem.id }}
</li>
</ol>
</div>
<div class="card">
<div class="card-header">
<h1>Задача #{{ problem.id }}</h1>
</div>
<div class="card-body">
<!--<editor-fold desc="properties">-->
<div id="properties">
<table class="table table-striped table-bordered">
<tr>
<td>Создана</td>
<td>{{ problem.created_at }}</td>
</tr>
<tr>
<td>Тележек</td>
<td>{{ tasks|length }}</td>
</tr>
<tr>
<td>Дистанций</td>
<td>{{ distances|length }}</td>
</tr>
<tr>
<td>Инженеров</td>
<td>{{ workers|length }}</td>
</tr>
<tr>
<td>Файл с условием</td>
<td><a class="btn btn-primary"
href="/{{ problem.xls_filename|urlencode }}">Загрузить</a></td>
</tr>
</table>
</div>
<!--</editor-fold>-->
<div class="card">
<div class="card-header">
<h2>Запуски</h2>
</div>
<div class="card-body">
{% if runs %}
<div class="card">
<div class="card-header">
<h3>История</h3>
</div>
<div class="card-body">
<ul>
{% for run in runs reversed %}
<li>
<strong>
<a href="{% url 'problem_run' problem.id run.id %}">
{{ run }}
</a>
</strong>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<h3>Новый</h3>
</div>
<div class="card-body">
<form action="{% url 'problem_create_run' problem_id=problem.id %}"
method="post">
{% csrf_token %}
<div class="card">
<div class="card-header">
<a class="card-link" data-toggle="collapse" href="#params-table-collapse">
<h4>Параметры</h4>
</a>
</div>
<div id="params-table-collapse" class="collapse">
<div class="card-body">
<table class="table table-striped table-bordered">
{{ create_run_form.as_table }}
</table>
</div>
</div>
</div>
<input class="btn btn-primary" type="submit" value="Cоздать">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,192 @@
{% extends 'base.html' %}
{% block main %}
<div id='breadcrumbs'>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">
Главная
</a></li>
<li class="breadcrumb-item"><a href="{% url 'problem' problem.id %}">
Задача #{{ problem.id }}
</a></li>
<li class="breadcrumb-item active">
Запуск #{{ run.id }}
</li>
</ol>
</div>
<div class="card">
<div class="card-header">
<h1>Запуск #{{ run.id }}</h1>
</div>
<div class="card-body">
<!--<editor-fold desc="properties">-->
<div id="properties">
<table class="table table-striped table-bordered">
<tr>
<td>Создан</td>
<td>{{ run.created_at }}</td>
</tr>
{% if run.started_at %}
<tr>
<td>Запущен</td>
<td>{{ run.started_at }}</td>
</tr>
{% endif %}
{% if run.finished_at %}
<tr>
<td>Посчитан</td>
<td>{{ run.finished_at }}</td>
</tr>
{% endif %}
<tr>
<td>Статус</td>
<td>
{{ run.status }}
{% if run.status == 'Не запущен' %}
<form action="{% url 'problem_run_start' problem.id run.id %}">
<input type="submit" value="запустить">
</form>
{% endif %}
{% if duration_total %}
<div class="progress">
<div id="run_progress"
class="progress-bar progress-bar-striped active"
role="progressbar"
style="width: 0">
0%
</div>
</div>
<script>
var duration_total = {{ duration_total }} +20;
var duration_passed = {{ duration_passed }};
var started = new Date();
function updateRunProgress() {
var now = new Date();
var passed = (now.getTime() - started.getTime()) / 1000 + duration_passed;
var pct = Math.round(Math.min(passed / duration_total, 1) * 100);
$('#run_progress')
.css('width', pct + '%')
.text(pct + '%');
if (passed < duration_total) {
setTimeout(updateRunProgress, 100);
}
}
updateRunProgress();
</script>
{% endif %}
</td>
</tr>
<div>
{% if run.goal_value %}
<tr>
<td>Значение целевой функции</td>
<td>{{ run.goal_value }}</td>
</tr>
{% endif %}
</div>
{% if run.result_xls_filename %}
<tr>
<td>Файл с решением</td>
<td>
<a class="btn btn-primary"
href="/{{ run.result_xls_filename|urlencode }}">Загрузить</a>
</td>
</tr>
{% endif %}
</table>
</div>
<!--</editor-fold>-->
<!--<editor-fold desc="metrics">-->
{% if metrics %}
<div id='metrics-card' class="card">
<div class="card-header">
<h4>
<a class="card-link" data-toggle="collapse" href="#metrics-table-collapse">
Метрики найденного решения
</a>
</h4>
</div>
<div id="metrics-table-collapse" class="collapse">
<div class="card-body">
<table class="table table-striped table-bordered">
{% for i in metrics %}
<tr>
<td>{{ i.name }}</td>
<td>{{ i.value }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endif %}
<!--</editor-fold>-->
<!--<editor-fold desc="critical distances">-->
<div id='critical-distances-card' class="card">
<div class="card-header">
<h4>
<a class="card-link" data-toggle="collapse" href="#critical-distances-table-collapse">
Критические дистанции
</a>
</h4>
</div>
<div id="critical-distances-table-collapse" class="collapse">
<div class="card-body">
<table class="table table-striped table-bordered">
{% for i in critical_distances %}
<tr>
<td>{{ i.distance.name }}</td>
<td>{{ i.value }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
<!--</editor-fold>-->
<!--<editor-fold desc="params">-->
<div id='params-card' class="card">
<div class="card-header">
<h4>
<a class="card-link" data-toggle="collapse" href="#params-table-collapse">
Параметры
</a>
</h4>
</div>
<div id="params-table-collapse" class="collapse">
<div class="card-body">
<table class="table table-striped table-bordered">
{% for i in param_set.all_params.values %}
<tr>
<td>{{ i.description }}</td>
<td>{{ i.value }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
<!--</editor-fold>-->
</div>
</div>
{% endblock %}

1
src/optimizer/tests.py Normal file
View File

@@ -0,0 +1 @@
# Create your tests here.

21
src/optimizer/urls.py Normal file
View File

@@ -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/<int:problem_id>',
views.view_problem, name='problem'),
path('problems/<int:problem_id>/create_run',
views.view_problem_create_run, name='problem_create_run'),
path('problems/<int:problem_id>/runs/<int:run_id>',
views.view_problem_run, name='problem_run'),
path('problems/<int:problem_id>/runs/<int:run_id>/start',
views.view_problem_run_start, name='problem_run_start'),
]

118
src/optimizer/views.py Normal file
View File

@@ -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)

0
src/rzhdweb/__init__.py Normal file
View File

143
src/rzhdweb/settings.py Normal file
View File

@@ -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(),
}
}

23
src/rzhdweb/urls.py Normal file
View File

@@ -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),
]

16
src/rzhdweb/wsgi.py Normal file
View File

@@ -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()

23
src/rzhdweb/zlogging.py Normal file
View File

@@ -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)