Merge branch '409-add-analytics-for-forecasting' into 'master'
Resolve "Add analytics for forecasting" Closes #409 See merge request jankstudio/resq!485
This commit is contained in:
commit
69119c6b3a
6 changed files with 743 additions and 9 deletions
|
|
@ -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
51
composer.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -22,6 +24,53 @@ use App\Entity\Hub;
|
|||
|
||||
class AnalyticsController extends Controller
|
||||
{
|
||||
protected $weekdays = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
|
||||
protected $day_shifts = [
|
||||
['Mon - Sat', 0, 1, 2, 3, 4, 5], // Mon - Sat
|
||||
['Tue - Sun', 1, 2, 3, 4, 5, 6], // Tue - Sun
|
||||
['Wed - Mon', 2, 3, 4, 5, 6, 0], // Wed - Mon
|
||||
['Thu - Tue', 3, 4, 5, 6, 0, 1], // Thu - Tue
|
||||
['Fri - Wed', 4, 5, 6, 0, 1, 2], // Fri - Wed
|
||||
['Sat - Thu', 5, 6, 0, 1, 2, 3], // Sat - Thu
|
||||
['Sun - Fri', 6, 0, 1, 2, 3, 4], // Sun - Fri
|
||||
];
|
||||
|
||||
protected $hour_shifts = [
|
||||
['00:00 - 09:00', 0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
['01:00 - 10:00', 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
['02:00 - 11:00', 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
['03:00 - 12:00', 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
['04:00 - 13:00', 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
['05:00 - 14:00', 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||
['06:00 - 15:00', 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
['07:00 - 16:00', 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
['08:00 - 17:00', 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
['09:00 - 18:00', 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
['10:00 - 19:00', 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||
['11:00 - 20:00', 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
['12:00 - 21:00', 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
||||
['13:00 - 22:00', 13, 14, 15, 16, 17, 18, 19, 20, 21],
|
||||
['14:00 - 23:00', 14, 15, 16, 17, 18, 19, 20, 21, 22],
|
||||
['15:00 - 00:00', 15, 16, 17, 18, 19, 20, 21, 22, 23],
|
||||
['16:00 - 01:00', 16, 17, 18, 19, 20, 21, 22, 23, 0],
|
||||
['17:00 - 02:00', 17, 18, 19, 20, 21, 22, 23, 0, 1],
|
||||
['18:00 - 03:00', 18, 19, 20, 21, 22, 23, 0, 1, 2],
|
||||
['19:00 - 04:00', 19, 20, 21, 22, 23, 0, 1, 2, 3],
|
||||
['20:00 - 05:00', 20, 21, 22, 23, 0, 1, 2, 3, 4],
|
||||
['21:00 - 06:00', 21, 22, 23, 0, 1, 2, 3, 4, 5],
|
||||
['22:00 - 07:00', 22, 23, 0, 1, 2, 3, 4, 5, 6],
|
||||
['23:00 - 08:00', 23, 0, 1, 2, 3, 4, 5, 6, 7],
|
||||
];
|
||||
|
||||
/**
|
||||
* @Menu(selected="analytics_forecast")
|
||||
* @IsGranted("analytics.forecast")
|
||||
|
|
@ -56,19 +105,21 @@ class AnalyticsController extends Controller
|
|||
$hub_list = $req->request->get('hub_ids', []);
|
||||
$distances = $req->request->get('distances', []);
|
||||
$today = DateTime::createFromFormat('d M Y', $req->request->get('date'));
|
||||
error_log(print_r($hub_list, true));
|
||||
$month = $today->format('m');
|
||||
// error_log(print_r($hub_list, true));
|
||||
|
||||
// $hub_list = [ 6, 4, 36, 7, 8, 126, 127, 18, 12, 9, 60, 10, 21, 135 ];
|
||||
|
||||
$hub_data = [];
|
||||
$hub_coverage = [];
|
||||
$overlaps = [];
|
||||
foreach ($hub_list as $key => $hub_id)
|
||||
{
|
||||
$dist = $distances[$key];
|
||||
$hub = $em->getRepository(Hub::class)->find($hub_id);
|
||||
$coords = $hub->getCoordinates();
|
||||
|
||||
$dist = $distances[$key];
|
||||
$hub_data[$hub_id] = $this->generateHubData($em, $hub, $dist, $today);
|
||||
$hub_data[$hub_id] = $this->generateHubData($em, $hub, $dist, $today, $overlaps);
|
||||
|
||||
$hub_coverage[] = [
|
||||
'longitude' => $coords->getLongitude(),
|
||||
|
|
@ -77,16 +128,156 @@ class AnalyticsController extends Controller
|
|||
];
|
||||
}
|
||||
|
||||
// reprocess weekday data to account for overlap
|
||||
foreach ($hub_data as $hub_id => $one_hub)
|
||||
{
|
||||
$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
|
||||
$scheduler_data = $chart_all_weekdays['scheduler_data'];
|
||||
// error_log(print_r($scheduler_data, true));
|
||||
unset($chart_all_weekdays['scheduler_data']);
|
||||
|
||||
// run scheduler
|
||||
// send 2018 + month data
|
||||
$sched_res = $this->runScheduler($scheduler_data['2018'][$month]);
|
||||
|
||||
// tally total JOs for the month
|
||||
$total_jos = 0;
|
||||
foreach ($scheduler_data['2018'][$month] as $sday_data)
|
||||
{
|
||||
foreach ($sday_data as $shour_data)
|
||||
{
|
||||
$total_jos += $shour_data;
|
||||
}
|
||||
}
|
||||
|
||||
$hub_data[$hub_id]['data_weekday'] = $chart_weekday;
|
||||
$hub_data[$hub_id]['data_all_weekdays'] = $chart_all_weekdays;
|
||||
$hub_data[$hub_id]['data_shift'] = $sched_res['weekday_shifts'];
|
||||
$hub_data[$hub_id]['total_jos'] = $total_jos;
|
||||
$hub_data[$hub_id]['total_riders'] = $sched_res['total_riders'];
|
||||
|
||||
unset($hub_data[$hub_id]['c_weekday']);
|
||||
}
|
||||
|
||||
// error_log(print_r($overlaps, true));
|
||||
|
||||
// get job orders not covered by hubs
|
||||
$not_covered = $this->generateNotCoveredData($em, $hub_coverage, $today);
|
||||
|
||||
$params = [
|
||||
'date' => $today,
|
||||
'hub_list' => $hub_data,
|
||||
'hub_coverage' => $hub_coverage,
|
||||
'not_covered' => $not_covered,
|
||||
];
|
||||
|
||||
return $this->render('analytics/forecast_submit.html.twig', $params);
|
||||
}
|
||||
|
||||
protected function generateHubData($em, $hub, $distance_limit, DateTime $today)
|
||||
protected function runScheduler($scheduler_data)
|
||||
{
|
||||
// run python script to solve scheduling for riders
|
||||
|
||||
$python_cmd = '/usr/bin/python3';
|
||||
$sched_script = __DIR__ . '/../../utils/schedule_solver/solver.py';
|
||||
|
||||
// go through the days
|
||||
$args = [
|
||||
$python_cmd,
|
||||
$sched_script,
|
||||
];
|
||||
foreach ($scheduler_data as $weekday_data)
|
||||
$args[] = implode('-', $weekday_data);
|
||||
|
||||
error_log(print_r($args, true));
|
||||
|
||||
// error_log('running...' . $sched_script);
|
||||
|
||||
$proc = new Process($args);
|
||||
$proc->run();
|
||||
|
||||
if (!$proc->isSuccessful())
|
||||
error_log('SCHEDULER DID NOT RUN PROPERLY');
|
||||
|
||||
$res = $proc->getOutput();
|
||||
error_log($res);
|
||||
|
||||
|
||||
// segregate into weekdays
|
||||
$day_data = [];
|
||||
$i = 0;
|
||||
foreach ($scheduler_data as $weekday_data)
|
||||
{
|
||||
$total_jos = 0;
|
||||
foreach ($weekday_data as $hourly_jo)
|
||||
$total_jos += $hourly_jo;
|
||||
|
||||
$day_data[$i] = [
|
||||
'weekday' => $this->weekdays[$i],
|
||||
'total_jos' => $total_jos,
|
||||
'total_riders' => 0,
|
||||
'shifts' => [],
|
||||
];
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
$shifts = [];
|
||||
$res_lines = explode("\n", $res);
|
||||
$total_riders = 0;
|
||||
foreach ($res_lines as $line)
|
||||
{
|
||||
// format is day shift - hour shift - number of riders
|
||||
$shift_data = explode('-', $line);
|
||||
if (count($shift_data) != 3)
|
||||
continue;
|
||||
|
||||
$day_shift_index = $shift_data[0];
|
||||
$hour_shift_index = $shift_data[1];
|
||||
$rider_count = $shift_data[2];
|
||||
|
||||
$total_riders += $rider_count;
|
||||
|
||||
$label = $this->day_shifts[$day_shift_index][0] . ' ' . $this->hour_shifts[$hour_shift_index][0];
|
||||
|
||||
// initialize hours
|
||||
$rider_hours = [];
|
||||
for ($i = 0; $i < 24; $i++)
|
||||
$rider_hours[$i] = 0;
|
||||
for ($i = 1; $i < count($this->hour_shifts[$hour_shift_index]); $i++)
|
||||
$rider_hours[$this->hour_shifts[$hour_shift_index][$i]] = 1;
|
||||
|
||||
error_log('allocating ' . $rider_count . ' for ' . $label);
|
||||
|
||||
// add shifts to the weekday
|
||||
for ($i = 1; $i < count($this->day_shifts[$day_shift_index]); $i++)
|
||||
{
|
||||
$day = $this->day_shifts[$day_shift_index][$i];
|
||||
|
||||
$day_data[$day]['shifts'][] = [
|
||||
'label' => $label,
|
||||
'count' => $rider_count,
|
||||
'hours' => $rider_hours,
|
||||
];
|
||||
|
||||
$day_data[$day]['total_riders'] += $rider_count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$data = [
|
||||
'weekday_shifts' => $day_data,
|
||||
'total_riders' => $total_riders,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
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');
|
||||
$date_end = new DateTime();
|
||||
|
|
@ -95,7 +286,7 @@ class AnalyticsController extends Controller
|
|||
// $hub = $em->getRepository(Hub::class)->find($hub_id);
|
||||
$conn = $em->getConnection();
|
||||
|
||||
// get job order data
|
||||
// get job order data (job orders within coverage area)
|
||||
$jos = $this->generateJobOrderData($conn, $hub, $distance_limit);
|
||||
|
||||
// initialize counters
|
||||
|
|
@ -116,6 +307,9 @@ class AnalyticsController extends Controller
|
|||
$hour = $date->format('H');
|
||||
$week = $date->format('W');
|
||||
|
||||
$jo_id = $jo['id'];
|
||||
$hub_id = $hub->getID();
|
||||
|
||||
// year day
|
||||
if (!isset($c_day[$year][$month][$day]))
|
||||
$c_day[$year][$month][$day] = 0;
|
||||
|
|
@ -126,9 +320,11 @@ class AnalyticsController extends Controller
|
|||
{
|
||||
$c_weekday[$year][$month][$weekday][$hour]['total'] = 0;
|
||||
$c_weekday[$year][$month][$weekday][$hour]['count'] = 0;
|
||||
$c_weekday[$year][$month][$weekday][$hour]['jos'] = [];
|
||||
}
|
||||
|
||||
$c_weekday[$year][$month][$weekday][$hour]['total']++;
|
||||
$c_weekday[$year][$month][$weekday][$hour]['jos'][$jo_id] = $jo_id;
|
||||
|
||||
if (!isset($c_week_count[$year][$month][$week][$weekday][$hour]))
|
||||
{
|
||||
|
|
@ -136,12 +332,19 @@ class AnalyticsController extends Controller
|
|||
$c_week_count[$year][$month][$week][$weekday][$hour] = 1;
|
||||
$c_weekday[$year][$month][$weekday][$hour]['count']++;
|
||||
}
|
||||
|
||||
// track overlaps (jo that can be handled by more than one hub)
|
||||
if (!isset($overlaps[$jo_id]))
|
||||
{
|
||||
$overlaps[$jo_id] = [];
|
||||
}
|
||||
$overlaps[$jo_id][$hub_id] = $hub_id;
|
||||
}
|
||||
|
||||
// error_log(print_r($c_weekday, true));
|
||||
|
||||
$chart_year = $this->generateYearData($date_start, $date_end, $c_day);
|
||||
$chart_weekday = $this->generateWeekdayData($c_weekday, $today);
|
||||
// $chart_weekday = $this->generateWeekdayData($c_weekday, $today);
|
||||
|
||||
// error_log(print_r($chart_weekday, true));
|
||||
|
||||
|
|
@ -149,7 +352,9 @@ class AnalyticsController extends Controller
|
|||
'id' => $hub->getID(),
|
||||
'label' => $hub->getName(),
|
||||
'data_year' => $chart_year,
|
||||
'data_weekday' => $chart_weekday,
|
||||
// 'data_weekday' => $chart_weekday,
|
||||
'c_weekday' => $c_weekday, // sending raw weekday data because we need to process overlaps
|
||||
// TODO: refactor this pls
|
||||
];
|
||||
|
||||
return $params;
|
||||
|
|
@ -212,7 +417,69 @@ class AnalyticsController extends Controller
|
|||
return $chart_year;
|
||||
}
|
||||
|
||||
protected function generateWeekdayData($all_weekday_data, $today)
|
||||
protected function generateAllWeekData($all_weekday_data, $today, $overlaps)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
// build hours
|
||||
$hours = [];
|
||||
for ($i = 0; $i < 24; $i++)
|
||||
$hours[] = sprintf('%02d', $i);
|
||||
|
||||
$month = $today->format('m');
|
||||
$year_data = [];
|
||||
$scheduler_data = [];
|
||||
|
||||
// gather maximum for each hour
|
||||
foreach ($this->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
|
||||
$year_id = 'y' . $year;
|
||||
$prefix = $year_id . '_' . $weekday;
|
||||
if (isset($year_data[$month][$weekday][$hour]))
|
||||
{
|
||||
// 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;
|
||||
|
||||
// assign scheduler data
|
||||
$scheduler_data[$year][$month][$weekday][$hour] = $rv_average;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isset($scheduler_data[$year][$month][$weekday][$hour]))
|
||||
{
|
||||
$data[$id][$prefix . '_rv_average'] = 0;
|
||||
$scheduler_data[$year][$month][$weekday][$hour] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['scheduler_data'] = $scheduler_data;
|
||||
|
||||
// error_log(print_r($data, true));
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function generateWeekdayData($all_weekday_data, $today, $overlaps)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
|
|
@ -241,13 +508,73 @@ class AnalyticsController extends Controller
|
|||
{
|
||||
$year_id = 'y' . $year;
|
||||
|
||||
// calculate the rider value for each JO and use that score as basis
|
||||
$total_rv = $this->calculateTotalRiderValue($year_data[$month][$weekday][$hour]['jos'], $overlaps);
|
||||
|
||||
$data[$id][$year_id] = $year_data[$month][$weekday][$hour]['total'];
|
||||
$data[$id][$year_id . '_count'] = $year_data[$month][$weekday][$hour]['count'];
|
||||
$data[$id][$year_id . '_average'] = ceil($year_data[$month][$weekday][$hour]['total'] / $year_data[$month][$weekday][$hour]['count']);
|
||||
$data[$id][$year_id . '_rv_average'] = ceil($total_rv / $year_data[$month][$weekday][$hour]['count']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function calculateTotalRiderValue($jos, $overlaps)
|
||||
{
|
||||
// rider value = 1 / number of hubs (overlaps) that can service JO
|
||||
|
||||
$sum = 0.0;
|
||||
$jo_count = count($jos);
|
||||
|
||||
foreach ($jos as $jo_id)
|
||||
{
|
||||
$hub_count = count($overlaps[$jo_id]);
|
||||
$rv = 1 / $hub_count;
|
||||
$sum += $rv;
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
protected function generateNotCoveredData($em, $hub_coverage, $today)
|
||||
{
|
||||
$conn = $em->getConnection();
|
||||
|
||||
$month = $today->format('m');
|
||||
$weekday = $today->format('N') - 1;
|
||||
|
||||
$wheres = [];
|
||||
foreach ($hub_coverage as $hub_data)
|
||||
{
|
||||
$long = $hub_data['longitude'];
|
||||
$lat = $hub_data['latitude'];
|
||||
$dist = $hub_data['distance'];
|
||||
|
||||
// get areas not covered
|
||||
$wheres[] = "st_distance(coordinates, Point($long, $lat)) * 111195 > $dist";
|
||||
}
|
||||
|
||||
$where_string = implode(' and ', $wheres);
|
||||
|
||||
$sql = "select st_x(coordinates) as longitude, st_y(coordinates) as latitude, id, date_schedule from job_order where $where_string and status <> 'cancelled' and month(date_schedule) = $month and weekday(date_schedule) = $weekday order by date_schedule asc";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->execute();
|
||||
$jos = $stmt->fetchAll();
|
||||
|
||||
// error_log($sql);
|
||||
// error_log(print_r($jos, true));
|
||||
|
||||
return $jos;
|
||||
}
|
||||
|
||||
protected function generateRiderSchedule()
|
||||
{
|
||||
}
|
||||
|
||||
protected function solveRiderSchedule()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,9 @@
|
|||
"symfony/polyfill-php73": {
|
||||
"version": "v1.11.0"
|
||||
},
|
||||
"symfony/process": {
|
||||
"version": "v4.4.9"
|
||||
},
|
||||
"symfony/profiler-pack": {
|
||||
"version": "v1.0.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,26 @@
|
|||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
|
||||
<style>
|
||||
.sched_col {
|
||||
width: 3.5%;
|
||||
}
|
||||
.marked {
|
||||
background-color: #ff0000;
|
||||
}
|
||||
.shift-table {
|
||||
margin-top: 20px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.shift-table th {
|
||||
text-align: center;
|
||||
}
|
||||
.shift-table td {
|
||||
padding: 5px 10px 5px 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
|
@ -38,6 +58,90 @@
|
|||
|
||||
<div id="month-weekday-chart-{{ hub.id }}" style="height: 400px;">
|
||||
</div>
|
||||
|
||||
<div id="month-weekday-chart-rv-{{ hub.id }}" style="height: 400px;">
|
||||
</div>
|
||||
|
||||
<div id="month-all-weekday-chart-{{ hub.id }}" style="height: 400px;">
|
||||
</div>
|
||||
|
||||
{% for day_data in hub.data_shift %}
|
||||
<div id="shift-table-{{ hub.id }}">
|
||||
<table class="shift-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ day_data.weekday }}</th>
|
||||
<th>00</th>
|
||||
<th>01</th>
|
||||
<th>02</th>
|
||||
<th>03</th>
|
||||
<th>04</th>
|
||||
<th>05</th>
|
||||
<th>06</th>
|
||||
<th>07</th>
|
||||
<th>08</th>
|
||||
<th>09</th>
|
||||
<th>10</th>
|
||||
<th>11</th>
|
||||
<th>12</th>
|
||||
<th>13</th>
|
||||
<th>14</th>
|
||||
<th>15</th>
|
||||
<th>16</th>
|
||||
<th>17</th>
|
||||
<th>18</th>
|
||||
<th>19</th>
|
||||
<th>20</th>
|
||||
<th>21</th>
|
||||
<th>22</th>
|
||||
<th>23</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for shift_data in day_data.shifts %}
|
||||
{% for i in 1..shift_data.count %}
|
||||
<tr>
|
||||
<td style="width: 250px;">{{ shift_data.label }}</td>
|
||||
{% for hour_coverage in shift_data.hours %}
|
||||
{% if hour_coverage %}
|
||||
<td class="sched_col marked"></td>
|
||||
{% else %}
|
||||
<td class="sched_col"></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<table class="shift-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 250px;">Day</th>
|
||||
<th style="width: 100px;"># JO</th>
|
||||
<th style="width: 100px;"># Rider</th>
|
||||
<th style="width: 100px;">JO per Rider</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in 0..6 %}
|
||||
<tr>
|
||||
<td>{{ hub.data_shift[i].weekday }}</td>
|
||||
<td>{{ hub.data_shift[i].total_jos }}</td>
|
||||
<td>{{ hub.data_shift[i].total_riders }}</td>
|
||||
<td>{{ (hub.data_shift[i].total_jos / hub.data_shift[i].total_riders) | round(1, 'common') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td>Overall</td>
|
||||
<td>{{ hub.total_jos }}</td>
|
||||
<td>{{ hub.total_riders }}</td>
|
||||
<td>{{ (hub.total_jos / hub.total_riders) | round(1, 'common') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -67,6 +171,15 @@ var map = L.map('map_coverage').setView([{% trans %}default_lat{% endtrans %}, {
|
|||
L.circle([{{ cover.latitude }}, {{ cover.longitude }}], { radius: {{ cover.distance }} }).addTo(map);
|
||||
{% endfor %}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
// display not covered
|
||||
{% for jo_na in not_covered %}
|
||||
L.marker([{{ jo_na.latitude }}, {{ jo_na.longitude }}]).addTo(map);
|
||||
{% endfor %}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
{% for hub in hub_list %}
|
||||
// create chart instance
|
||||
var chart = am4core.create("year-day-chart-{{ hub.id }}", am4charts.XYChart);
|
||||
|
|
@ -110,6 +223,7 @@ s2020.dataFields.categoryX = "date";
|
|||
|
||||
var chart2 = am4core.create("month-weekday-chart-{{ hub.id }}", am4charts.XYChart);
|
||||
chart2.data = {{ hub.data_weekday|json_encode|raw }};
|
||||
chart2.legend = new am4charts.Legend();
|
||||
|
||||
var xAxis = chart2.xAxes.push(new am4charts.CategoryAxis());
|
||||
xAxis.dataFields.category = "hour";
|
||||
|
|
@ -132,6 +246,95 @@ l2019.strokeWidth = 3;
|
|||
l2019.dataFields.valueY = "y2019_average";
|
||||
l2019.dataFields.categoryX = "hour";
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
var chart2 = am4core.create("month-weekday-chart-rv-{{ hub.id }}", am4charts.XYChart);
|
||||
chart2.data = {{ hub.data_weekday|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 l2018 = chart2.series.push(new am4charts.LineSeries());
|
||||
l2018.name = "2018";
|
||||
l2018.stroke = am4core.color("#0000FF");
|
||||
l2018.strokeWidth = 2;
|
||||
l2018.dataFields.valueY = "y2018_rv_average";
|
||||
l2018.dataFields.categoryX = "hour";
|
||||
|
||||
var l2019 = chart2.series.push(new am4charts.LineSeries());
|
||||
l2019.name = "2019";
|
||||
l2019.stroke = am4core.color("#FF0000");
|
||||
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>
|
||||
|
|
|
|||
151
utils/schedule_solver/solver.py
Normal file
151
utils/schedule_solver/solver.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from __future__ import print_function
|
||||
from ortools.linear_solver import pywraplp
|
||||
import sys
|
||||
|
||||
def main():
|
||||
|
||||
# days
|
||||
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
|
||||
# hours
|
||||
hours = [
|
||||
'00',
|
||||
'01',
|
||||
'02',
|
||||
'03',
|
||||
'04',
|
||||
'05',
|
||||
'06',
|
||||
'07',
|
||||
'08',
|
||||
'09',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23']
|
||||
|
||||
# initialize required hours - req_hours[days][hours]
|
||||
req_hours = [[0 for x in range(len(hours))] for y in range(len(days))]
|
||||
|
||||
# get arguments
|
||||
# there will be 7 arguments, monday to sunday schedule
|
||||
for day_index in range(0, len(days)):
|
||||
hours_string = sys.argv[day_index + 1]
|
||||
hours_data = hours_string.split('-')
|
||||
for hour_index in range(0, len(hours)):
|
||||
req_hours[day_index][hour_index] = int(hours_data[hour_index])
|
||||
|
||||
# all hour shifts available
|
||||
hour_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]]
|
||||
|
||||
# all possible days riders come in
|
||||
day_shifts = [
|
||||
['Mon - Sat', 0, 1, 2, 3, 4, 5], # Mon - Sat
|
||||
['Tue - Sun', 1, 2, 3, 4, 5, 6], # Tue - Sun
|
||||
['Wed - Mon', 2, 3, 4, 5, 6, 0], # Wed - Mon
|
||||
['Thu - Tue', 3, 4, 5, 6, 0, 1], # Thu - Tue
|
||||
['Fri - Wed', 4, 5, 6, 0, 1, 2], # Fri - Wed
|
||||
['Sat - Thu', 5, 6, 0, 1, 2, 3], # Sat - Thu
|
||||
['Sun - Fri', 6, 0, 1, 2, 3, 4]] # Sun - Fri
|
||||
|
||||
# build shift lookup index
|
||||
|
||||
|
||||
# instantiate glop solver
|
||||
solver = pywraplp.Solver('SolveSchedule', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
|
||||
|
||||
# solver variables
|
||||
solv_shifts = [[0 for x in range(len(hour_shifts))] for y in range(len(day_shifts))]
|
||||
|
||||
# objective
|
||||
objective = solver.Objective()
|
||||
|
||||
# variables for shifts
|
||||
for day_index in range(0, len(day_shifts)):
|
||||
for hour_index in range(0, len(hour_shifts)):
|
||||
solv_shifts[day_index][hour_index] = solver.IntVar(0, solver.infinity(), day_shifts[day_index][0] + ' ' + hour_shifts[hour_index][0])
|
||||
# objective is to minimize number of shifts
|
||||
objective.SetCoefficient(solv_shifts[day_index][hour_index], 1)
|
||||
objective.SetMinimization()
|
||||
|
||||
# set constraints
|
||||
constraints = [[0 for x in range(len(hours))] for y in range(len(days))]
|
||||
# go through all days
|
||||
for day_index in range(0, len(days)):
|
||||
# go through all hours
|
||||
for hour_index in range(0, len(hours)):
|
||||
# hour personnel should be equal or less than shift personnel that covers those hours
|
||||
# set the required manpower for that day + hour combo
|
||||
# print('setting constraint for', day_index, '-', hour_index, '=', req_hours[day_index][hour_index])
|
||||
constraints[day_index][hour_index] = solver.Constraint(req_hours[day_index][hour_index], solver.infinity())
|
||||
# go through all day shifts
|
||||
for shift_day in range(0, len(day_shifts)):
|
||||
# go through days inside day shift
|
||||
for shift_day_index in range(1, len(day_shifts[shift_day])):
|
||||
# is day shift part of that day?
|
||||
if day_index == day_shifts[shift_day][shift_day_index]:
|
||||
# go through all hour shifts
|
||||
for shift_hour in range(0, len(hour_shifts)):
|
||||
# go through all hours inside hour shift
|
||||
for shift_hour_index in range(1, len(hour_shifts[shift_hour])):
|
||||
# is hour shift part of the hour
|
||||
if hour_index == hour_shifts[shift_hour][shift_hour_index]:
|
||||
# print(day_index, ' - ', hour_index, ' vs ', day_shifts[shift_day][shift_day_index], ' - ', hour_shifts[shift_hour][shift_hour_index])
|
||||
# add it to constraint
|
||||
constraints[day_index][hour_index].SetCoefficient(solv_shifts[shift_day][shift_hour], 1)
|
||||
|
||||
# solve it!
|
||||
status = solver.Solve()
|
||||
#print('Number of variables =', solver.NumVariables())
|
||||
#print('Number of constraints =', solver.NumConstraints())
|
||||
|
||||
if status == solver.OPTIMAL:
|
||||
#print('Optimal solution found!')
|
||||
for day_index in range(0, len(day_shifts)):
|
||||
for hour_index in range(0, len(hour_shifts)):
|
||||
result = solv_shifts[day_index][hour_index].solution_value()
|
||||
if result > 0:
|
||||
print(day_index, hour_index, int(solv_shifts[day_index][hour_index].solution_value()), sep='-')
|
||||
#print(day_shifts[day_index][0], ' ', hour_shifts[hour_index][0], ' = ', int(solv_shifts[day_index][hour_index].solution_value()), sep='')
|
||||
else:
|
||||
if status == solver.FEASIBLE:
|
||||
print('Feasible solution found!')
|
||||
else:
|
||||
print('Could not solve problem.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in a new issue