Skip to content

Commit 9006231

Browse files
authored
Merge pull request #618 from HSLdevcom/feat/results-refactor
Major results refactor All results are now based on events and do pollute the main loop anymore. Fully independence is not guaranteed so check that in case you want to parallelize results handling.
2 parents 4fc0e29 + 6c70c9c commit 9006231

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1579
-481
lines changed

.github/workflows/pythonapp.yml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,53 @@
11
# This workflow will install Python dependencies, run tests and lint with a single version of Python
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
33

4-
name: Python application
4+
on:
5+
push:
6+
pull_request:
7+
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
58

6-
on: [push, pull_request]
7-
89
jobs:
9-
build:
10+
# This job figures out whether the ref has an associated draft PR
11+
context:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
isDraftOnPush: ${{ steps.detect.outputs.is_draft_on_push }}
15+
steps:
16+
- name: Detect draft PR for this push (if any)
17+
id: detect
18+
uses: actions/github-script@v7
19+
with:
20+
script: |
21+
const eventName = context.eventName;
22+
// Default: not a draft
23+
let isDraftOnPush = false;
1024
25+
if (eventName === 'push') {
26+
// Branch name like "feature/foo" (strip "refs/heads/")
27+
const branch = context.ref.replace(/^refs\/heads\//, '');
28+
// Find open PRs from this branch in this repo
29+
const { data: prs } = await github.rest.pulls.list({
30+
owner: context.repo.owner,
31+
repo: context.repo.repo,
32+
state: 'open',
33+
head: `${context.repo.owner}:${branch}`
34+
});
35+
// If there is an open PR and it's draft, mark it
36+
if (prs.length > 0 && prs[0].draft) {
37+
isDraftOnPush = true;
38+
}
39+
}
40+
core.setOutput('is_draft_on_push', String(isDraftOnPush));
41+
42+
build:
43+
needs: context
1144
runs-on: windows-latest
45+
# Skip rules:
46+
# - If event is pull_request: skip when draft
47+
# - If event is push: skip when a corresponding open PR exists AND it's draft
48+
if: >
49+
(github.event_name == 'pull_request' && !github.event.pull_request.draft)
50+
|| (github.event_name == 'push' && needs.context.outputs.isDraftOnPush != 'true')
1251
1352
steps:
1453
- uses: actions/checkout@v2

Scripts/assignment/departure_time.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ def __init__(self,
2727
self.time_periods = time_periods
2828
self.demand: Optional[Union[int,Dict[str,Dict[str,numpy.ndarray]]]] = None
2929
self.old_car_demand: Union[int,numpy.ndarray] = 0
30-
self.init_demand()
30+
self.init_demand_and_get_gaps()
3131

32-
def init_demand(self) -> Dict[str,float]:
32+
def init_demand_and_get_gaps(self) -> Dict[str,float]:
3333
"""Initialize/reset demand for all time periods.
3434
3535
Includes all transport classes, each being set to zero.

Scripts/assignment/emme_assignment.py

Lines changed: 22 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -164,21 +164,21 @@ def aggregate_results(self, resultdata: ResultsData):
164164
if res != "transit_volumes":
165165
self._node_24h(
166166
transit_class, param.segment_results[res])
167-
ass_classes = list(param.emme_matrices) + ["bus", "aux_transit"]
168-
ass_classes.remove("walk")
169-
for ass_class in ass_classes:
167+
self.res_ass_classes = list(param.emme_matrices) + ["bus", "aux_transit"] #result assignment classes
168+
self.res_ass_classes.remove("walk")
169+
for ass_class in self.res_ass_classes:
170170
self._link_24h(ass_class)
171171

172172
# Aggregate and print vehicle kms and link lengths
173-
kms = dict.fromkeys(ass_classes, 0.0)
173+
kms = dict.fromkeys(self.res_ass_classes, 0.0)
174174
vdfs = {param.roadclasses[linktype].volume_delay_func
175175
for linktype in param.roadclasses}
176176
vdfs.add(0) # Links with car traffic prohibited
177177
vdf_kms = {ass_class: pandas.Series(0.0, vdfs)
178-
for ass_class in ass_classes}
178+
for ass_class in self.res_ass_classes}
179179
areas = zone_param.area_aggregation
180180
area_kms = {ass_class: pandas.Series(0.0, areas)
181-
for ass_class in ass_classes}
181+
for ass_class in self.res_ass_classes}
182182
vdf_area_kms = {vdf: pandas.Series(0.0, areas) for vdf in vdfs}
183183
#The following line only works well in Python 3.7+
184184
linktypes = list(dict.fromkeys(param.roadtypes.values())) + list(dict.fromkeys(param.railtypes.values()))
@@ -194,7 +194,7 @@ def aggregate_results(self, resultdata: ResultsData):
194194
else:
195195
vdf = 0
196196
area = belongs_to_area(link.i_node)
197-
for ass_class in ass_classes:
197+
for ass_class in self.res_ass_classes:
198198
veh_kms = link[self._extra(ass_class)] * link.length
199199
kms[ass_class] += veh_kms
200200
if vdf in vdfs:
@@ -209,55 +209,23 @@ def aggregate_results(self, resultdata: ResultsData):
209209
linklengths[param.railtypes[linktype]] += link.length
210210
else:
211211
linklengths[param.roadtypes[vdf]] += link.length / 2
212-
213-
self._event_handler.on_daily_results_aggregated(self, network)
212+
213+
network_aggregations = {"kms": kms,
214+
"vdfs": vdfs,
215+
"areas": areas,
216+
"linktypes": linktypes,
217+
"linklengths": linklengths,
218+
"veh_kms": veh_kms,
219+
"vdf_kms": vdf_kms,
220+
"area_kms": area_kms,
221+
"vdf_area_kms": vdf_area_kms}
222+
223+
self._event_handler.on_daily_results_aggregated(self, network, network_aggregations)
214224

215225
if faulty_kela_code_nodes:
216226
s = "Municipality KELA code not found for nodes: " + ", ".join(
217227
faulty_kela_code_nodes)
218228
log.warn(s)
219-
resultdata.print_line("\nVehicle kilometres", "result_summary")
220-
for ass_class in ass_classes:
221-
resultdata.print_line(
222-
"{}:\t{:1.0f}".format(ass_class, kms[ass_class]),
223-
"result_summary")
224-
resultdata.print_data(
225-
vdf_kms[ass_class], "vehicle_kms_vdfs.txt", ass_class)
226-
resultdata.print_data(
227-
area_kms[ass_class], "vehicle_kms_areas.txt", ass_class)
228-
for vdf in vdf_area_kms:
229-
resultdata.print_data(
230-
vdf_area_kms[vdf], "vehicle_kms_vdfs_areas.txt", vdf)
231-
resultdata.print_data(linklengths, "link_lengths.txt", "length")
232-
233-
# Aggregate and print numbers of stations
234-
stations = pandas.Series(0, param.station_ids)
235-
for node in network.regular_nodes():
236-
for mode in param.station_ids:
237-
if (node.data2 == param.station_ids[mode]
238-
and node[self._extra("transit_won_boa")] > 0):
239-
stations[mode] += 1
240-
break
241-
resultdata.print_data(stations, "transit_stations.txt", "number")
242-
243-
# Aggregate and print transit vehicle kms
244-
transit_modes = [veh.description for veh in network.transit_vehicles()]
245-
dists = pandas.Series(0.0, transit_modes)
246-
times = pandas.Series(0.0, transit_modes)
247-
for ap in self.assignment_periods:
248-
network = ap.emme_scenario.get_network()
249-
volume_factor = param.volume_factors["bus"][ap.name]
250-
for line in network.transit_lines():
251-
mode = line.vehicle.description
252-
headway = line[ap.extra("hw")]
253-
if 0 < headway < 900:
254-
departures = volume_factor * 60/headway
255-
for segment in line.segments():
256-
dists[mode] += departures * segment.link.length
257-
times[mode] += (departures
258-
* segment[ap.extra("base_timtr")])
259-
resultdata.print_data(dists, "transit_kms.txt", "dist")
260-
resultdata.print_data(times, "transit_kms.txt", "time")
261229

262230
def calc_transit_cost(self,
263231
fares: TransitFareZoneSpecification,
@@ -403,9 +371,9 @@ def _create_attributes(self,
403371
(e.g., self._extra)
404372
"""
405373
# Create link attributes
406-
ass_classes = list(param.emme_matrices) + ["bus"]
407-
ass_classes.remove("walk")
408-
for ass_class in ass_classes:
374+
self.emme_ass_classes = list(param.emme_matrices) + ["bus"]
375+
self.emme_ass_classes.remove("walk")
376+
for ass_class in self.emme_ass_classes:
409377
self.emme_project.create_extra_attribute(
410378
"LINK", extra(ass_class), ass_class + " volume",
411379
overwrite=True, scenario=scenario)

Scripts/datahandling/zonedata.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22
from pathlib import Path
3-
from typing import Any, Dict, List, Tuple, Union
3+
from typing import Any, Dict, List, Tuple, Union, TYPE_CHECKING
44
import numpy # type: ignore
55
import pandas
66

7-
from events.event_handler import EventHandler
7+
if TYPE_CHECKING:
8+
from events.event_handler import EventHandler
9+
810
import parameters.zone as param
911
from utils.read_csv_file import read_csv_file
1012
from utils.zone_interval import ZoneIntervals, zone_interval

Scripts/datatypes/demand.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(self,
3131
"""
3232
self.purpose = purpose
3333
self.mode = mode
34+
self.tour_matrix = matrix #need to also preserve the passenger matrix
3435
if mode == "car" and purpose.name in param.car_driver_share:
3536
self.matrix = param.car_driver_share[purpose.name] * matrix
3637
else:

Scripts/datatypes/purpose.py

Lines changed: 5 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,6 @@ def __init__(self,
7070
def zone_numbers(self):
7171
return self.zone_data.zone_numbers[self.bounds]
7272

73-
def print_data(self):
74-
self.resultdata.print_data(
75-
pandas.Series(
76-
sum(self.generated_tours.values()), self.zone_numbers),
77-
"generation.txt", self.name)
78-
self.resultdata.print_data(
79-
pandas.Series(
80-
sum(self.attracted_tours.values()),
81-
self.zone_data.zone_numbers),
82-
"attraction.txt", self.name)
83-
demsums = {mode: self.generated_tours[mode].sum()
84-
for mode in self.modes}
85-
demand_all = float(sum(demsums.values()))
86-
mode_shares = {mode: demsums[mode] / demand_all for mode in demsums}
87-
self.resultdata.print_data(
88-
pandas.Series(mode_shares),
89-
"mode_share.txt", self.name)
90-
91-
9273
class TourPurpose(Purpose):
9374
"""Standard two-way tour purpose.
9475
@@ -135,19 +116,6 @@ def __init__(self, specification, zone_data, resultdata):
135116
else:
136117
self.park_and_ride_model = None
137118

138-
def print_data(self, pnr_iteration=0):
139-
Purpose.print_data(self)
140-
for mode in self.histograms:
141-
self.resultdata.print_data(
142-
self.histograms[mode].histogram, "trip_lengths.txt",
143-
"{}_{}".format(self.name, mode[0]))
144-
self.resultdata.print_matrix(
145-
self.aggregates[mode].matrix, "aggregated_demand",
146-
"{}_{}".format(self.name, mode), pnr_iteration)
147-
self.resultdata.print_data(
148-
self.own_zone_aggregates[mode].array,
149-
"own_zone_demand.txt", "{}_{}".format(self.name, mode[0]))
150-
151119
def init_sums(self):
152120
for mode in self.modes:
153121
self.generated_tours[mode] = numpy.zeros_like(self.zone_numbers)
@@ -165,17 +133,11 @@ def calc_prob(self, impedance):
165133
Mode (car/transit/bike/walk) : dict
166134
Type (time/cost/dist) : numpy 2d matrix
167135
"""
168-
def print_pnr_utility(pnr_utility: numpy.ndarray, result_path: Path):
169-
# TODO: This is a temporary solution to print the park and ride utility
170-
omx_file = omx.open_file(result_path / 'park_and_ride_utility.omx', 'w')
171-
omx_file.create_mapping('zone_number', self.zone_data.zone_numbers)
172-
omx_file['park_and_ride_utility'] = pnr_utility
173-
omx_file.close()
136+
#Add park and ride impedance for models that include it
174137
if self.park_and_ride_model is not None:
175-
pnr_utility = self.park_and_ride_model.get_logsum()
176-
impedance['park_and_ride'] = {'utility': pnr_utility,
138+
self.pnr_utility = self.park_and_ride_model.get_logsum()
139+
impedance['park_and_ride'] = {'utility': self.pnr_utility,
177140
'dist': impedance['car']['dist']}
178-
print_pnr_utility(pnr_utility, Path(self.resultdata.path))
179141
self.prob = self.model.calc_prob(impedance)
180142
self.dist = impedance["car"]["dist"]
181143

@@ -218,9 +180,6 @@ def calc_pnr_demand(self, population, estimation_mode=False, log=None):
218180
self.zone_data.zone_index(tour.dest)] += 1
219181
log.info(f"Matrix contains {od_matrix.sum()} park and ride tours")
220182
demand = {}
221-
# if estimation_mode:
222-
# omx_file = omx.open_file(f"{self.resultdata.path}/estimation/demand_{self.name}.omx","w")
223-
# omx_file.create_mapping("zone_number",self.zone_data.all_zone_numbers)
224183

225184
car_demand, transit_demand = self.park_and_ride_model.distribute_demand(od_matrix)
226185
pnr_purpose = ParkAndRidePseudoPurpose(self)
@@ -240,38 +199,28 @@ def calc_demand(self, estimation_mode=False, add_sec_dest: bool = True, pnr_iter
240199
"""
241200
tours = self.gen_model.get_tours()
242201
demand = {}
243-
if estimation_mode:
244-
omx_file = omx.open_file(f"{self.resultdata.path}/estimation/demand_{self.name}.omx","w")
245-
omx_file.create_mapping("zone_number",self.zone_data.all_zone_numbers)
246202
for mode in self.modes:
247203
mtx = (self.prob.pop(mode) * tours).T
204+
self.orig_purpose_demand = mtx
248205
if mode == "park_and_ride":
249206
car_demand, transit_demand = self.park_and_ride_model.distribute_demand(mtx)
250207
pnr_purpose = ParkAndRidePseudoPurpose(self)
251208
demand["pnr_car"] = Demand(pnr_purpose, "car", car_demand)
252209
demand["pnr_transit"] = Demand(pnr_purpose, "transit", transit_demand)
253-
# if True:
254-
# omx_file["pnr_car"] = car_demand
255-
# omx_file["pnr_transit"] = transit_demand
256210
else:
257211
if add_sec_dest:
258212
try:
259213
self.sec_dest_purpose.gen_model.add_tours(mtx, mode, self)
260214
except AttributeError: #If the tour does not generate sec_dest tours
261215
pass
262216
demand[mode] = Demand(self, mode, mtx)
263-
if estimation_mode:
264-
omx_file[mode] = mtx
265217
self.attracted_tours[mode] = mtx.sum(0)
266218
self.generated_tours[mode] = mtx.sum(1)
267219
self.histograms[mode].count_tour_dists(mtx, self.dist)
268220
self.aggregates[mode].aggregate(pandas.DataFrame(
269221
mtx, self.zone_numbers, self.zone_data.zone_numbers))
270222
self.own_zone_aggregates[mode].aggregate(pandas.Series(
271223
numpy.diag(mtx), self.zone_numbers))
272-
self.print_data(pnr_iteration)
273-
if estimation_mode:
274-
omx_file.close()
275224
return demand
276225

277226

@@ -380,11 +329,4 @@ def calc_prob(self, mode, impedance, orig, dests):
380329
dest_imp[mtx_type] = (impedance[mtx_type][dests, :]
381330
+ impedance[mtx_type][:, orig]
382331
- impedance[mtx_type][dests, orig][:, numpy.newaxis])
383-
return self.model.calc_prob(mode, dest_imp, orig, dests)
384-
385-
def print_data(self):
386-
self.resultdata.print_data(
387-
pandas.Series(
388-
sum(self.attracted_tours.values()),
389-
self.zone_data.zone_numbers),
390-
"attraction.txt", self.name)
332+
return self.model.calc_prob(mode, dest_imp, orig, dests)

Scripts/datatypes/tour.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def choose_mode(self, is_car_user: bool):
143143

144144
@property
145145
def sustainable_access(self):
146-
return -self.purpose.sustainable_access[self.orig]
146+
if self.orig in self.purpose.sustainable_access:
147+
return (-1)*self.purpose.sustainable_access[self.orig]
148+
return -1 #Fallback
147149

148150
def choose_destination(self, sec_dest_tours: Dict[str, Dict[int, Dict[int, List['Tour']]]]):
149151
"""Choose primary destination for the tour.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ def generate_tours(self):
164164
self.purpose_dict[purpose].gen_model.tours += nr_tours
165165
nr_tours_sums["-".join(combination)] = nr_tours.sum()
166166
result_data[age] = nr_tours_sums.sort_index()
167-
self.resultdata.print_matrix(
168-
result_data, "tour_combinations", "tour_combinations")
167+
self.tour_generation_model.result = result_data
169168

170169
def generate_tour_probs(self) -> Dict[Tuple[int,int], numpy.ndarray]:
171170
"""Generate matrices of cumulative tour combination probabilities.

0 commit comments

Comments
 (0)