Add schedule solver script and run it inside analytics controller #409

This commit is contained in:
Kendrick Chan 2020-06-11 01:15:58 +08:00
parent 0911a1787e
commit d94e2353b0
6 changed files with 398 additions and 2 deletions

View file

@ -25,6 +25,7 @@
"symfony/framework-bundle": "^4.0",
"symfony/maker-bundle": "^1.0",
"symfony/orm-pack": "^1.0",
"symfony/process": "^4.0",
"symfony/profiler-pack": "^1.0",
"symfony/security-bundle": "^4.0",
"symfony/translation": "^4.0",

51
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "b101ecfbc1f6f2270f0e8ad326035b7e",
"content-hash": "f03b92d48946e8b2ee19466f931c826f",
"packages": [
{
"name": "catalyst/auth-bundle",
@ -4143,6 +4143,55 @@
],
"time": "2019-11-27T16:25:15+00:00"
},
{
"name": "symfony/process",
"version": "v4.4.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5",
"reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5",
"shasum": ""
},
"require": {
"php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2020-05-30T20:06:45+00:00"
},
{
"name": "symfony/profiler-pack",
"version": "v1.0.4",

View file

@ -8,6 +8,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
@ -83,7 +85,20 @@ class AnalyticsController extends Controller
{
$c_weekday = $one_hub['c_weekday'];
$chart_weekday = $this->generateWeekdayData($c_weekday, $today, $overlaps);
$chart_all_weekdays = $this->generateAllWeekData($c_weekday, $today, $overlaps);
// figure out the rider schedules based on the max hour values
$hour_max_values = $chart_all_weekdays['hour_max_values'];
ksort($hour_max_values);
error_log(print_r($hour_max_values, true));
unset($chart_all_weekdays['hour_max_values']);
// run scheduler
$shift = $this->runScheduler($hour_max_values);
$hub_data[$hub_id]['data_weekday'] = $chart_weekday;
$hub_data[$hub_id]['data_all_weekdays'] = $chart_all_weekdays;
$hub_data[$hub_id]['data_shift'] = $shift;
unset($hub_data[$hub_id]['c_weekday']);
}
@ -103,6 +118,51 @@ class AnalyticsController extends Controller
return $this->render('analytics/forecast_submit.html.twig', $params);
}
protected function runScheduler($hour_data)
{
// run python script to solve scheduling for riders
$arg_string = implode('-', $hour_data);
$python_cmd = '/usr/bin/python3';
$sched_script = __DIR__ . '/../../utils/schedule_solver/solver.py';
error_log('running...' . $sched_script);
$proc = new Process([$python_cmd, $sched_script, $arg_string]);
$proc->run();
if (!$proc->isSuccessful())
error_log('SCHEDULER DID NOT RUN PROPERLY');
$res = $proc->getOutput();
error_log($res);
$shifts = [];
$res_lines = explode("\n", $res);
foreach ($res_lines as $line)
{
$hour_data = explode('-', $line);
if (count($hour_data) != 2)
continue;
$start_hour = $hour_data[0];
$rider_count = $hour_data[1];
$display_shift = sprintf('%02d:00', $start_hour);
error_log('allocating ' . $rider_count . ' for ' . $display_shift);
$shifts[] = [
'label' => $display_shift,
'count' => $rider_count,
];
}
return $shifts;
}
protected function generateHubData($em, $hub, $distance_limit, DateTime $today, &$overlaps)
{
$date_start = DateTime::createFromFormat('Y-m-d H:i:s', '2018-01-01 00:00:00');
@ -243,6 +303,79 @@ class AnalyticsController extends Controller
return $chart_year;
}
protected function generateAllWeekData($all_weekday_data, $today, $overlaps)
{
// generate all weekdays, not just one weekday
$data = [];
// build wekdays
$weekdays = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
// build hours
$hours = [];
for ($i = 0; $i < 24; $i++)
$hours[] = sprintf('%02d', $i);
$month = $today->format('m');
$year_data = [];
// gather maximum for each hour
// TODO: make it for all years, for now we only track 2018
$hour_max_values = [];
foreach ($weekdays as $weekday)
{
foreach ($all_weekday_data as $year => $year_data)
{
// go through the hours
foreach ($hours as $hour)
{
$id = $hour + 0;
if (!isset($data[$id]))
$data[$id] = [
'hour' => $hour,
];
// get hour data
if (isset($year_data[$month][$weekday][$hour]))
{
$year_id = 'y' . $year;
$prefix = $year_id . '_' . $weekday;
// calculate the rider value for each JO and use that score as basis
$total_rv = $this->calculateTotalRiderValue($year_data[$month][$weekday][$hour]['jos'], $overlaps);
$rv_average = ceil($total_rv / $year_data[$month][$weekday][$hour]['count']);
$data[$id][$prefix] = $year_data[$month][$weekday][$hour]['total'];
$data[$id][$prefix . '_count'] = $year_data[$month][$weekday][$hour]['count'];
$data[$id][$prefix . '_average'] = ceil($year_data[$month][$weekday][$hour]['total'] / $year_data[$month][$weekday][$hour]['count']);
$data[$id][$prefix . '_rv_average'] = $rv_average;
// set maximum hour data
if (!isset($hour_max_values[$hour]) || $rv_average > $hour_max_values[$hour])
$hour_max_values[$hour] = $rv_average;
}
}
}
}
// error_log(print_r($data, true));
$data['hour_max_values'] = $hour_max_values;
return $data;
}
protected function generateWeekdayData($all_weekday_data, $today, $overlaps)
{
$data = [];
@ -329,8 +462,16 @@ class AnalyticsController extends Controller
$jos = $stmt->fetchAll();
// error_log($sql);
error_log(print_r($jos, true));
// error_log(print_r($jos, true));
return $jos;
}
protected function generateRiderSchedule()
{
}
protected function solveRiderSchedule()
{
}
}

View file

@ -260,6 +260,9 @@
"symfony/polyfill-php73": {
"version": "v1.11.0"
},
"symfony/process": {
"version": "v4.4.9"
},
"symfony/profiler-pack": {
"version": "v1.0.3"
},

View file

@ -41,6 +41,28 @@
<div id="month-weekday-chart-rv-{{ hub.id }}" style="height: 400px;">
</div>
<div id="month-all-weekday-chart-{{ hub.id }}" style="height: 400px;">
</div>
<div id="shift-table-{{ hub.id }}">
<table>
<thead>
<tr>
<th>Shift</th>
<th>Riders</th>
</tr>
</thead>
<tbody>
{% for shift_data in hub.data_shift %}
<tr>
<td>{{ shift_data.label }}</td>
<td>{{ shift_data.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
@ -170,6 +192,68 @@ l2019.strokeWidth = 3;
l2019.dataFields.valueY = "y2019_rv_average";
l2019.dataFields.categoryX = "hour";
//------------------------------------------------------------------------
var chart2 = am4core.create("month-all-weekday-chart-{{ hub.id }}", am4charts.XYChart);
chart2.data = {{ hub.data_all_weekdays|json_encode|raw }};
chart2.legend = new am4charts.Legend();
var xAxis = chart2.xAxes.push(new am4charts.CategoryAxis());
xAxis.dataFields.category = "hour";
xAxis.title.text = "Hour";
var yAxis = chart2.yAxes.push(new am4charts.ValueAxis());
yAxis.title.text = "Orders";
yAxis.maxPrecision = 0;
var lmon = chart2.series.push(new am4charts.LineSeries());
lmon.name = "Monday";
lmon.stroke = am4core.color("#003f5c");
lmon.strokeWidth = 2;
lmon.dataFields.valueY = "y2018_Monday_rv_average";
lmon.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Tuesday";
l.stroke = am4core.color("#374c80");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Tuesday_rv_average";
l.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Wednesday";
l.stroke = am4core.color("#7a5195");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Wednesday_rv_average";
l.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Thursday";
l.stroke = am4core.color("#bc5090");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Thursday_rv_average";
l.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Friday";
l.stroke = am4core.color("#ef5675");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Friday_rv_average";
l.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Saturday";
l.stroke = am4core.color("#ff764a");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Saturday_rv_average";
l.dataFields.categoryX = "hour";
var l = chart2.series.push(new am4charts.LineSeries());
l.name = "Sunday";
l.stroke = am4core.color("#ffa600");
l.strokeWidth = 2;
l.dataFields.valueY = "y2018_Sunday_rv_average";
l.dataFields.categoryX = "hour";
{% endfor %}
</script>

View file

@ -0,0 +1,118 @@
from __future__ import print_function
from ortools.linear_solver import pywraplp
import sys
def main():
# get arguments
hours_string = sys.argv[1]
hours_data = hours_string.split('-')
# initialize hours
hours = [
['00', 0],
['01', 0],
['02', 0],
['03', 0],
['04', 0],
['05', 0],
['06', 0],
['07', 0],
['08', 0],
['09', 0],
['10', 0],
['11', 0],
['12', 0],
['13', 0],
['14', 0],
['15', 0],
['16', 0],
['17', 0],
['18', 0],
['19', 0],
['20', 0],
['21', 0],
['22', 0],
['23', 0]]
# set hours from argument
for i in range(0, len(hours_data)):
hours[i][1] = int(hours_data[i])
# all shifts available
shifts = [
['00 - 09', 0, 1, 2, 3, 4, 5, 6, 7, 8],
['01 - 10', 1, 2, 3, 4, 5, 6, 7, 8, 9],
['02 - 11', 2, 3, 4, 5, 6, 7, 8, 9, 10],
['03 - 12', 3, 4, 5, 6, 7, 8, 9, 10, 11],
['04 - 13', 4, 5, 6, 7, 8, 9, 10, 11, 12],
['05 - 14', 5, 6, 7, 8, 9, 10, 11, 12, 13],
['06 - 15', 6, 7, 8, 9, 10, 11, 12, 13, 14],
['07 - 16', 7, 8, 9, 10, 11, 12, 13, 14, 15],
['08 - 17', 8, 9, 10, 11, 12, 13, 14, 15, 16],
['09 - 18', 9, 10, 11, 12, 13, 14, 15, 16, 17],
['10 - 19', 10, 11, 12, 13, 14, 15, 16, 17, 18],
['11 - 20', 11, 12, 13, 14, 15, 16, 17, 18, 19],
['12 - 21', 12, 13, 14, 15, 16, 17, 18, 19, 20],
['13 - 22', 13, 14, 15, 16, 17, 18, 19, 20, 21],
['14 - 23', 14, 15, 16, 17, 18, 19, 20, 21, 22],
['15 - 00', 15, 16, 17, 18, 19, 20, 21, 22, 23],
['16 - 01', 16, 17, 18, 19, 20, 21, 22, 23, 0],
['17 - 02', 17, 18, 19, 20, 21, 22, 23, 0, 1],
['18 - 03', 18, 19, 20, 21, 22, 23, 0, 1, 2],
['19 - 04', 19, 20, 21, 22, 23, 0, 1, 2, 3],
['20 - 05', 20, 21, 22, 23, 0, 1, 2, 3, 4],
['21 - 06', 21, 22, 23, 0, 1, 2, 3, 4, 5],
['22 - 07', 22, 23, 0, 1, 2, 3, 4, 5, 6],
['23 - 08', 23, 0, 1, 2, 3, 4, 5, 6, 7]]
# instantiate glop solver
solver = pywraplp.Solver('SolveSchedule', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
# solver variables
solv_shifts = [[]] * len(shifts)
# objective
objective = solver.Objective()
# variables for shifts
for i in range(0, len(shifts)):
solv_shifts[i] = solver.IntVar(0, solver.infinity(), shifts[i][0])
# objective is to minimize number of shifts
objective.SetCoefficient(solv_shifts[i], 1)
objective.SetMinimization()
# set constraints
constraints = [0] * len(hours)
for hour_index in range(0, len(hours)):
# hour personnel should be equal or less than shift personnel that covers those hours
constraints[hour_index] = solver.Constraint(hours[hour_index][1], solver.infinity())
for shift_index in range(0, len(shifts)):
# get each shift's hour coverage
for shift_hour_index in range(1, len(shifts[shift_index])):
# check if shift covers that hour
# NOTE: this can still be optimized later via indexing
if shifts[shift_index][shift_hour_index] == hour_index:
# print('hour', hour_index, 'in shift index -', shift_index)
constraints[hour_index].SetCoefficient(solv_shifts[shift_index], 1)
# solve it!
status = solver.Solve()
if status == solver.OPTIMAL:
#print('Optimal solution found!')
#print('Number of variables =', solver.NumVariables())
#print('Number of constraints =', solver.NumConstraints())
for i in range(0, len(shifts)):
result = solv_shifts[i].solution_value()
if result > 0:
print(i, '-', int(solv_shifts[i].solution_value()), sep='')
else:
if status == solver.FEASIBLE:
print('Feasible solution found!')
else:
print('Could not solve problem.')
if __name__ == '__main__':
main()