Skip to content

Commit

Permalink
Merge branch 'helmet5-experimental' of github.com:HSLdevcom/helmet-mo…
Browse files Browse the repository at this point in the history
…del-system into feat/helmet5-transit-assignment
  • Loading branch information
samakinen committed Feb 12, 2025
2 parents 7b4d832 + ab46c0f commit 3b04161
Show file tree
Hide file tree
Showing 52 changed files with 4,247 additions and 2,634 deletions.
26 changes: 25 additions & 1 deletion Scripts/assignment/assignment_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pandas

from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from events.model_system_event_listener import EventHandler
import utils.log as log
import parameters.assignment as param
import parameters.zone as zone_param
Expand Down Expand Up @@ -49,14 +50,17 @@ class AssignmentPeriod(Period):
def __init__(self, name: str, emme_scenario: int,
emme_context: EmmeProject,
emme_matrices: Dict[str, Dict[str, Any]],
event_handler: EventHandler,
separate_emme_scenarios: bool = False):
self.name = name
self.event_handler = event_handler
self.emme_scenario: Scenario = emme_context.modeller.emmebank.scenario(
emme_scenario)
self.emme_project = emme_context
self._separate_emme_scenarios = separate_emme_scenarios
self.emme_matrices = emme_matrices
self.dist_unit_cost = param.dist_unit_cost
self.event_handler.on_assignment_period_initialized(self)

def extra(self, attr: str) -> str:
"""Add prefix "@" and time-period suffix.
Expand Down Expand Up @@ -114,6 +118,7 @@ def assign(self, matrices: dict, iteration: Union[int,str]) -> Dict:
Type (time/cost/dist) : dict
Assignment class (car_work/transit/...) : numpy 2-d matrix
"""
self.event_handler.on_assignment_started(self, iteration, matrices)
self._set_emmebank_matrices(matrices, iteration=="last")
if iteration=="init":
self._assign_pedestrians()
Expand All @@ -126,20 +131,24 @@ def assign(self, matrices: dict, iteration: Union[int,str]) -> Dict:
self._calc_extra_wait_time()
self._assign_congested_transit() if param.always_congested else self._assign_transit()
elif iteration==0:
self._set_bike_vdfs()
self._assign_bikes(self.emme_matrices["bike"]["dist"], "all")
self._set_car_and_transit_vdfs()
if not self._separate_emme_scenarios:
self._calc_background_traffic()
self._assign_cars(param.stopping_criteria_coarse)
self._calc_extra_wait_time()
self._assign_congested_transit() if param.always_congested else self._assign_transit()
elif iteration==1:
self._set_bike_vdfs()
if not self._separate_emme_scenarios:
self._set_car_and_transit_vdfs()
self._calc_background_traffic()
self._assign_cars(param.stopping_criteria_coarse)
self._calc_extra_wait_time()
self._assign_congested_transit() if param.always_congested else self._assign_transit()
elif isinstance(iteration, int) and iteration>1:
self._set_bike_vdfs()
if not self._separate_emme_scenarios:
self._set_car_and_transit_vdfs()
self._calc_background_traffic(include_trucks=True)
Expand Down Expand Up @@ -180,6 +189,7 @@ def assign(self, matrices: dict, iteration: Union[int,str]) -> Dict:
if iteration != "last":
for ass_cl in ("car_work", "car_leisure"):
mtxs["cost"][ass_cl] += self.dist_unit_cost * mtxs["dist"][ass_cl]
self.event_handler.on_assignment_complete(self, iteration, matrices, mtxs, self.emme_scenario)
return mtxs

def calc_transit_cost(self,
Expand Down Expand Up @@ -366,11 +376,19 @@ def _set_car_and_transit_vdfs(self):
link.modes |= {main_mode}
elif main_mode in link.modes:
link.modes -= {main_mode}
self.event_handler.on_car_and_transit_vdfs_set(self, network)
self.emme_scenario.publish_network(network)

def _set_bike_vdfs(self):
log.info("Sets bike functions for scenario {}".format(
self.emme_scenario.id))
# Create time period specific extra function parameters for bike assignment
# TODO: create a combined extra atrribute and use a single extra function parameter
self.emme_project.create_extra_function_parameters(el2=self.extra('car_work'),
el3=self.extra('car_leisure'),
el4=self.extra('van'),
el5=self.extra('truck'),
el6=self.extra('trailer_truck'))
network = self.emme_scenario.get_network()
main_mode = network.mode(param.main_mode)
bike_mode = network.mode(param.bike_mode)
Expand Down Expand Up @@ -401,6 +419,7 @@ def _set_bike_vdfs(self):
link.modes |= {main_mode}
elif main_mode in link.modes:
link.modes -= {main_mode}
self.event_handler.on_bike_vdfs_set(self, network)
self.emme_scenario.publish_network(network)

def _set_emmebank_matrices(self,
Expand Down Expand Up @@ -558,6 +577,7 @@ def _calc_background_traffic(self, include_trucks: bool=False):
if include_trucks:
for ass_class in heavy:
link[background_traffic] += link[ass_class]
self.event_handler.on_background_traffic_calculated(self, network)
self.emme_scenario.publish_network(network)

def _calc_road_cost(self):
Expand All @@ -569,6 +589,7 @@ def _calc_road_cost(self):
dist_cost = self.dist_unit_cost * link.length
link[self.extra("toll_cost")] = toll_cost
link[self.extra("total_cost")] = toll_cost + dist_cost
self.event_handler.on_road_cost_calculated(self, network)
self.emme_scenario.publish_network(network)

def _calc_boarding_penalties(self,
Expand All @@ -591,6 +612,7 @@ def _calc_boarding_penalties(self,
if missing_penalties:
missing_penalties_str: str = ", ".join(missing_penalties)
log.warn("No boarding penalty found for transit modes " + missing_penalties_str)
self.event_handler.on_boarding_penalties_calculated(self, network)
self.emme_scenario.publish_network(network)

def _specify(self):
Expand Down Expand Up @@ -729,7 +751,8 @@ def _assign_pedestrians(self):
log.info("Pedestrian assignment started...")
self.emme_project.pedestrian_assignment(
specification=self.walk_spec, scenario=self.emme_scenario)
log.info("Pedestrian assignment performed for scenario " + str(self.emme_scenario.id))
self.event_handler.on_pedestrian_assignment_complete(self, self.emme_scenario)
log.info("Pedestrian assignment performed for scenario " + str(self.emme_scenario.id))

def _calc_extra_wait_time(self):
"""Calculate extra waiting time for one scenario."""
Expand Down Expand Up @@ -789,6 +812,7 @@ def _calc_extra_wait_time(self):
+ b["cspeed"]*cumulative_speed)
# Estimated waiting time addition caused by headway deviation
segment["@wait_time_dev"] = headway_sd**2 / (2.0*line[headway_attr])
self.event_handler.on_transit_wait_time_calculated(self, network)
self.emme_scenario.publish_network(network)

def _assign_transit(self):
Expand Down
4 changes: 3 additions & 1 deletion Scripts/assignment/departure_time.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
import numpy # type: ignore
from datahandling.zonedata import ZoneData
from datatypes.demand import Demand
from datatypes.tour import Tour

Expand Down Expand Up @@ -75,7 +76,8 @@ def add_demand(self, demand: Union[Demand, Tour]):
Travel demand matrix or number of travellers
"""
demand.purpose.name = cast(str,demand.purpose.name) #type checker hint
if demand.mode != "walk" and not demand.is_car_passenger:

if demand.mode != "walk" and demand.mode != "park_and_ride" and not demand.is_car_passenger:
if demand.mode in param.divided_classes:
ass_class = "{}_{}".format(
demand.mode, assignment_classes[demand.purpose.name])
Expand Down
22 changes: 16 additions & 6 deletions Scripts/assignment/emme_assignment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
import pandas
from math import log10

Expand All @@ -10,12 +10,14 @@
import parameters.zone as zone_param
from assignment.abstract_assignment import AssignmentModel
from assignment.assignment_period import AssignmentPeriod
from events.model_system_event_listener import EventHandler
if TYPE_CHECKING:
from assignment.emme_bindings.emme_project import EmmeProject
from assignment.datatypes.transit_fare import TransitFareZoneSpecification
from datahandling.resultdata import ResultsData
from inro.emme.database.scenario import Scenario # type: ignore
from inro.emme.network.Network import Network # type: ignore
import numpy


class EmmeAssignmentModel(AssignmentModel):
Expand Down Expand Up @@ -46,6 +48,7 @@ class EmmeAssignmentModel(AssignmentModel):
def __init__(self,
emme_context: EmmeProject,
first_scenario_id: int,
event_handler: EventHandler,
separate_emme_scenarios: bool=False,
save_matrices: bool=False,
time_periods: List[str]=param.time_periods,
Expand All @@ -57,6 +60,7 @@ def __init__(self,
self.emme_project = emme_context
self.mod_scenario = self.emme_project.modeller.emmebank.scenario(
first_scenario_id)
self._event_handler = event_handler

def prepare_network(self,
car_dist_unit_cost: Optional[float]=None):
Expand Down Expand Up @@ -96,7 +100,7 @@ def prepare_network(self,
tp, i*hundred + self.first_matrix_id, id_ten)
self.assignment_periods.append(AssignmentPeriod(
tp, scen_id, self.emme_project, emme_matrices,
separate_emme_scenarios=self.separate_emme_scenarios))
self._event_handler, separate_emme_scenarios=self.separate_emme_scenarios))
self._create_attributes(self.day_scenario, self._extra)
for ap in self.assignment_periods:
if car_dist_unit_cost is not None:
Expand All @@ -109,9 +113,11 @@ def prepare_network(self,
pass
self.emme_project.modeller.emmebank.create_function(
idx, param.volume_delay_funcs[idx])
self._calculate_gradients()
self.emme_project.create_extra_function_parameters(el1="@kaltevuus")

def init_assign(self,
demand: Dict[str,List[numpy.ndarray]]):
demand: Dict[str,List['numpy.ndarray']]):
"""??? types"""
ap0 = self.assignment_periods[0]
ap0.assign(demand, iteration="init")
Expand Down Expand Up @@ -252,8 +258,8 @@ def aggregate_results(self, resultdata: ResultsData):

def calc_transit_cost(self,
fares: TransitFareZoneSpecification,
peripheral_cost: numpy.ndarray,
default_cost: numpy.ndarray = None):
peripheral_cost: 'numpy.ndarray',
default_cost: 'numpy.ndarray' = None):
"""Calculate transit zone cost matrix.
Perform multiple transit assignments.
Expand Down Expand Up @@ -318,6 +324,11 @@ def _add_bus_stops(self):
modified_network: Network = mnw.add_bus_stops(network)
self.mod_scenario.publish_network(modified_network)

def _calculate_gradients(self):
network: Network = self.mod_scenario.get_network()
modified_network: Network = mnw.calculate_gradients(network)
self.mod_scenario.publish_network(modified_network)

def _create_matrices(self, time_period, id_hundred, id_ten):
"""Create EMME matrices for storing demand and impedance.
Expand Down Expand Up @@ -390,7 +401,6 @@ def _create_attributes(self,
# Create link attributes
ass_classes = list(param.emme_matrices) + ["bus"]
ass_classes.remove("walk")
if TYPE_CHECKING: scenario = cast(Scenario, scenario)
for ass_class in ass_classes:
self.emme_project.create_extra_attribute(
"LINK", extra(ass_class), ass_class + " volume",
Expand Down
2 changes: 2 additions & 0 deletions Scripts/assignment/emme_bindings/emme_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def __init__(self,
"inro.emme.data.extra_attribute.create_extra_attribute")
self.strategy_analysis = self.modeller.tool(
"inro.emme.transit_assignment.extended.strategy_based_analysis")
self.create_extra_function_parameters = self.modeller.tool(
"inro.emme.traffic_assignment.set_extra_function_parameters")

def write(self, message: str):
"""Write to logbook."""
Expand Down
7 changes: 7 additions & 0 deletions Scripts/assignment/emme_bindings/mock_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ def network_results(self, *args, **kwargs):
def strategy_analysis(self, *args, **kwargs):
pass

def create_extra_function_parameters(self, *args, **kwargs):
pass


Modeller = namedtuple("Modeller", "emmebank")

Expand Down Expand Up @@ -712,6 +715,9 @@ def id(self):
def outgoing_segments(self, include_hidden=False):
return (s for s in self.network.transit_segments(include_hidden)
if s.i_node is self)

def incoming_links(self):
return (l for l in self.network.links() if l.j_node is self)

class Link(NetworkObject):
def __init__(self,
Expand Down Expand Up @@ -825,6 +831,7 @@ def __init__(self, network: Network, line: TransitLine, link: Link):
self, network, network._extra_attr["TRANSIT_SEGMENT"])
self.line = line
self.link = link
self.loop_index = 1
self.allow_alightings = False
self.allow_boardings = False
self.transit_time_func = 0
Expand Down
29 changes: 27 additions & 2 deletions Scripts/datahandling/zonedata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union
import numpy # type: ignore
import pandas
Expand All @@ -22,6 +23,8 @@ def __init__(self, data_dir: str, zone_numbers: numpy.array):
external = param.areas["external"]
self.zone_numbers = all_zone_numbers[:all_zone_numbers.searchsorted(
peripheral[1], "right")]
self.zone_numbers_hs15 = all_zone_numbers[:all_zone_numbers.searchsorted(
surrounding[1], "right")]
Zone.counter = 0
self.zones = {number: Zone(number) for number in self.zone_numbers}
first_peripheral = self.zone_numbers.searchsorted(peripheral[0])
Expand All @@ -31,6 +34,10 @@ def __init__(self, data_dir: str, zone_numbers: numpy.array):
schooldata = read_csv_file(data_dir, ".edu", self.zone_numbers, dtype)
landdata = read_csv_file(data_dir, ".lnd", self.zone_numbers, dtype)
parkdata = read_csv_file(data_dir, ".prk", self.zone_numbers, dtype)
try:
pnrdata = read_csv_file(data_dir, ".pnr")
except NameError:
pnrdata = pandas.DataFrame(0, columns=['capacity', 'cost'], index=popdata.index)
self.externalgrowth = read_csv_file(
data_dir, ".ext",
all_zone_numbers[all_zone_numbers.searchsorted(external[0]):all_zone_numbers.searchsorted(external[1],side='right')],
Expand All @@ -53,6 +60,10 @@ def __init__(self, data_dir: str, zone_numbers: numpy.array):
truckdata = read_csv_file(data_dir, ".trk", squeeze=True)
self.trailers_prohibited = list(map(int, truckdata.loc[0, :]))
self.garbage_destination = list(map(int, truckdata.loc[1, :].dropna()))

self['pnr_capacity'] = pnrdata['capacity']
self['pnr_cost'] = pnrdata['cost']

pop = popdata["total"]
self["population"] = pop
self.share["share_age_7-17"] = popdata["sh_7-17"][:first_peripheral]
Expand All @@ -70,7 +81,12 @@ def __init__(self, data_dir: str, zone_numbers: numpy.array):
self.share["share_female"] = pandas.Series(0.5, self.zone_numbers)
self.share["share_male"] = pandas.Series(0.5, self.zone_numbers)
self.nr_zones = len(self.zone_numbers)
self.nr_zones_hs15 = len(self.zone_numbers_hs15)
self["population_density"] = pop / landdata["builtar"]
# Filter indexes where population_density > 50000
high_population_density = self["population_density"].index[self["population_density"] > 50000]
if len(high_population_density) > 0:
log.warn(f"Zone(s) {list(high_population_density)} have an abnormally high population density. Make sure that the builtar values in the .lnd file are calculated correctly.")
wp = workdata.pop("total")
self["workplaces"] = wp
ShareChecker({})["Workplace shares"] = workdata.sum(axis="columns")
Expand Down Expand Up @@ -202,11 +218,11 @@ def get_data(self, key: str, bounds: slice, generation: bool=False) -> Union[pan
val = self._values[key]
except KeyError as err:
keyl: List[str] = key.split('_')
if keyl[1] in ("own", "other"):
if keyl[-1] in ("own", "other"):
# If parameter is only for own municipality or for all
# municipalities except own, array is multiplied by
# bool matrix
return (self[keyl[1]] * self._values[keyl[0]].values)[bounds, :]
return (self[keyl[-1]] * self._values["_".join(keyl[0:-1])].values)[bounds, :]
else:
raise KeyError(err)
if val.ndim == 1: # If not a compound (i.e., matrix)
Expand All @@ -217,6 +233,15 @@ def get_data(self, key: str, bounds: slice, generation: bool=False) -> Union[pan
else: # Return matrix (purpose zones -> all zones)
return val[bounds, :]

def export_data(self, export_file: Path):
"""Export Pandas Series zone data into a single CSV file
Args:
export_file (Path): Path to the destination file
"""
df = pandas.DataFrame({k:v for k,v in self._values.items() if isinstance(v, pandas.Series)})
df.to_csv(export_file)


class BaseZoneData(ZoneData):
def __init__(self, data_dir: str, zone_numbers: numpy.array):
Expand Down
Loading

0 comments on commit 3b04161

Please sign in to comment.