init
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
.venv/
|
||||||
|
*.log
|
||||||
|
static/
|
||||||
|
__pycache__/
|
17
Pipfile
Normal file
17
Pipfile
Normal 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
161
Pipfile.lock
generated
Normal 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
65
deploy.sh
Executable 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
12
run.sh
Executable 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
15
src/manage.py
Executable 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)
|
0
src/optimizer/__init__.py
Normal file
0
src/optimizer/__init__.py
Normal file
1
src/optimizer/admin.py
Normal file
1
src/optimizer/admin.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Register your models here.
|
5
src/optimizer/apps.py
Normal file
5
src/optimizer/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizerConfig(AppConfig):
|
||||||
|
name = 'optimizer'
|
84
src/optimizer/forms.py
Normal file
84
src/optimizer/forms.py
Normal 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
|
54
src/optimizer/migrations/0001_initial.py
Normal file
54
src/optimizer/migrations/0001_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
19
src/optimizer/migrations/0002_problem_name.py
Normal file
19
src/optimizer/migrations/0002_problem_name.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
39
src/optimizer/migrations/0003_auto_20180526_0747.py
Normal file
39
src/optimizer/migrations/0003_auto_20180526_0747.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
src/optimizer/migrations/0004_auto_20180526_1213.py
Normal file
18
src/optimizer/migrations/0004_auto_20180526_1213.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
src/optimizer/migrations/0005_auto_20180526_1240.py
Normal file
18
src/optimizer/migrations/0005_auto_20180526_1240.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
51
src/optimizer/migrations/0006_auto_20180526_1321.py
Normal file
51
src/optimizer/migrations/0006_auto_20180526_1321.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
17
src/optimizer/migrations/0007_auto_20180526_1337.py
Normal file
17
src/optimizer/migrations/0007_auto_20180526_1337.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
21
src/optimizer/migrations/0008_optimizationrun_run_at.py
Normal file
21
src/optimizer/migrations/0008_optimizationrun_run_at.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
27
src/optimizer/migrations/0009_auto_20180527_1812.py
Normal file
27
src/optimizer/migrations/0009_auto_20180527_1812.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
src/optimizer/migrations/0010_auto_20180528_0114.py
Normal file
18
src/optimizer/migrations/0010_auto_20180528_0114.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
21
src/optimizer/migrations/0011_testmodel.py
Normal file
21
src/optimizer/migrations/0011_testmodel.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
src/optimizer/migrations/0012_auto_20180528_1148.py
Normal file
18
src/optimizer/migrations/0012_auto_20180528_1148.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
32
src/optimizer/migrations/0014_auto_20180528_2239.py
Normal file
32
src/optimizer/migrations/0014_auto_20180528_2239.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
23
src/optimizer/migrations/0015_auto_20180605_0050.py
Normal file
23
src/optimizer/migrations/0015_auto_20180605_0050.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
17
src/optimizer/migrations/0016_remove_problem_name.py
Normal file
17
src/optimizer/migrations/0016_remove_problem_name.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
17
src/optimizer/migrations/0017_remove_optimizationrun_name.py
Normal file
17
src/optimizer/migrations/0017_remove_optimizationrun_name.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
0
src/optimizer/migrations/__init__.py
Normal file
0
src/optimizer/migrations/__init__.py
Normal file
110
src/optimizer/models.py
Normal file
110
src/optimizer/models.py
Normal 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()
|
0
src/optimizer/process/__init__.py
Normal file
0
src/optimizer/process/__init__.py
Normal file
122
src/optimizer/process/params.py
Normal file
122
src/optimizer/process/params.py
Normal 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
|
294
src/optimizer/process/solver.py
Normal file
294
src/optimizer/process/solver.py
Normal 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()
|
204
src/optimizer/process/xlsio.py
Normal file
204
src/optimizer/process/xlsio.py
Normal 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
|
39
src/optimizer/templates/base.html
Normal file
39
src/optimizer/templates/base.html
Normal 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>
|
58
src/optimizer/templates/index.html
Normal file
58
src/optimizer/templates/index.html
Normal 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 %}
|
116
src/optimizer/templates/problem.html
Normal file
116
src/optimizer/templates/problem.html
Normal 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 %}
|
192
src/optimizer/templates/run.html
Normal file
192
src/optimizer/templates/run.html
Normal 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
1
src/optimizer/tests.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Create your tests here.
|
21
src/optimizer/urls.py
Normal file
21
src/optimizer/urls.py
Normal 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
118
src/optimizer/views.py
Normal 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
0
src/rzhdweb/__init__.py
Normal file
143
src/rzhdweb/settings.py
Normal file
143
src/rzhdweb/settings.py
Normal 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
23
src/rzhdweb/urls.py
Normal 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
16
src/rzhdweb/wsgi.py
Normal 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
23
src/rzhdweb/zlogging.py
Normal 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)
|
Reference in New Issue
Block a user