init
This commit is contained in:
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)
|
Reference in New Issue
Block a user