Skip to content

Commit

Permalink
Merge pull request #153 from BayAreaMetro/transit_journey_level_simpl…
Browse files Browse the repository at this point in the history
…ification

Added function to simplify to 2 journey levels
  • Loading branch information
i-am-sijia authored Jul 30, 2024
2 parents 50e1802 + 684c9d9 commit c1ac021
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
Binary file not shown.
77 changes: 76 additions & 1 deletion tm2py/components/network/transit/transit_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,13 @@ def run(self):
self.generate_fromto_approx(network, lines, fare_matrix, fs_data)

self.faresystem_distances(faresystems)
faresystem_groups = self.group_faresystems(faresystems)

if self.config.journey_levels.use_algorithm == True:
faresystem_groups = self.group_faresystems(faresystems)

if self.config.journey_levels.specify_manually == True:
faresystem_groups = self.group_faresystems_simplified(faresystems)

journey_levels, mode_map = self.generate_transfer_fares(
faresystems, faresystem_groups, network
)
Expand Down Expand Up @@ -1502,6 +1508,75 @@ def matching_xfer_fares(xfer_fares_list1, xfer_fares_list2):

return faresystem_groups

def group_faresystems_simplified(self, faresystems):
"""This function allows for manual specification of journey levels/ faresystem groups"""
self._log.append({"type": "header", "content": "Simplified faresystem groups"})

manual_groups = [
groups.group_fare_systems for groups in self.config.journey_levels.manual
]
group_xfer_fares_mode = [([], [], []) for _ in range(len(manual_groups) + 1)]

for fs_id, fs_data in faresystems.items():
fs_modes = fs_data["MODE_SET"]
xfers = fs_data["xfer_fares"]
assigned = False
for i, fs_ids in enumerate(manual_groups):
if fs_id in fs_ids:
group_xfer_fares_mode[i][0].append(xfers)
group_xfer_fares_mode[i][1].append(fs_id)
group_xfer_fares_mode[i][2].extend(fs_modes)
assigned = True
break
if not assigned:
group_xfer_fares_mode[-1][0].append(xfers)
group_xfer_fares_mode[-1][1].append(fs_id)
group_xfer_fares_mode[-1][2].extend(fs_modes)

xfer_fares_table = [["p/q"] + list(faresystems.keys())]
faresystem_groups = []
i = 0
for xfer_fares_list, group, modes in group_xfer_fares_mode:
xfer_fares = {}
for fs_id in faresystems.keys():
to_fares = [f[fs_id] for f in xfer_fares_list if f[fs_id] != "TOO_FAR"]
# fare = to_fares[0] if len(to_fares) > 0 else 0.0
if len(to_fares) == 0:
fare = 0.0
elif all(isinstance(item, float) for item in to_fares):
# caculate the average here becasue of the edits in matching_xfer_fares function
fare = round(sum(to_fares) / len(to_fares), 2)
else:
fare = to_fares[0]
xfer_fares[fs_id] = fare
faresystem_groups.append((group, xfer_fares))
for fs_id in group:
xfer_fares_table.append(
[fs_id] + list(faresystems[fs_id]["xfer_fares"].values())
)
i += 1
self._log.append(
{
"type": "text2",
"content": "Level %s faresystems: %s modes: %s"
% (
i,
", ".join([str(x) for x in group]),
", ".join([str(m) for m in modes]),
),
}
)

self._log.append(
{
"type": "header",
"content": "Transfer fares list by faresystem, sorted by group",
}
)
self._log.append({"content": xfer_fares_table, "type": "table"})

return faresystem_groups

def generate_transfer_fares(self, faresystems, faresystem_groups, network):
self.create_attribute("MODE", "#orig_mode", self.scenario, network, "STRING")
self.create_attribute(
Expand Down
70 changes: 70 additions & 0 deletions tm2py/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,75 @@ class TransitClassConfig(ConfigItem):
required_mode_combo: Optional[Tuple[str, ...]] = Field(default=None)


@dataclass(frozen=True)
class ManualJourneyLevelsConfig(ConfigItem):
"""Manual Journey Level Specification"""

level_id: int
group_fare_systems: Tuple[int, ...]


@dataclass(frozen=True)
class TransitJourneyLevelsConfig(ConfigItem):
"""Transit manual journey levels structure."""

use_algorithm: bool = False
"""
The original translation from Cube to Emme used an algorithm to, as faithfully as possible, reflect transfer fares via journey levels.
The algorithm examines fare costs and proximity of transit services to create a set of journey levels that reflects transfer costs.
While this algorithm works well, the Bay Area's complex fare system results in numerous journey levels specific to operators with low ridership.
The resulting assignment compute therefore expends a lot of resources on these operators.
Set this parameter to `True` to use the algorithm. Exactly one of `use_algorithm` or `specify_manually` must be `True`.
"""
specify_manually: bool = True
"""
An alternative to using an algorithm to specify the journey levels is to use specify them manually.
If this option is set to `True`, the `manual` parameter can be used to assign fare systems to faresystem groups (or journey levels).
Consider, for example, the following three journey levels: 0 - has yet to board transit; 1 - has boarded SF Muni; 2 - has boarded all other transit systems.
To specify this configuration, a single `manual` entry identifying the SF Muni fare systems is needed.
The other faresystem group is automatically generated in the code with the rest of the faresystems which are not specified in any of the groups.
See the `manual` entry for an example.
"""
manual: Optional[Tuple[ManualJourneyLevelsConfig, ...]] = (
ManualJourneyLevelsConfig(level_id=1, group_fare_systems=(25,)),
)
"""
If 'specify_manually' is set to `True`, there should be at least one faresystem group specified here.
The format includes two entries: `level_id`, which is the serial number of the group specified,
and `group_fare_system`, which is a list of all faresystems belonging to that group.
For example, to specify MUNI as one faresystem group, the right configuration would be:
[[transit.journey_levels.manual]]
level_id = 1
group_fare_systems = [25]
If there are multiple groups required to be specified, for example, MUNI in one and Caltrain in the other group,
it can be achieved by adding another entry of `manual`, like:
[[transit.journey_levels.manual]]
level_id = 1
group_fare_systems = [25]
[[transit.journey_levels.manual]]
level_id = 2
group_fare_systems = [12,14]
"""

@validator("specify_manually")
def check_exclusivity(cls, v, values):
"""Valdiates that exactly one of specify_manually and use_algorithm is True"""
use_algorithm = values.get("use_algorithm")
assert (
use_algorithm != v
), 'Exactly one of "use_algorithm" or "specify_manually" must be True.'
return v

@validator("manual", always=True)
def check_manual(cls, v, values):
if values.get("specify_manually"):
assert (
v is not None and len(v) > 0
), "If 'specify_manually' is True, 'manual' cannot be None or empty."
return v


@dataclass(frozen=True)
class AssignmentStoppingCriteriaConfig(ConfigItem):
"Assignment stop configuration parameters."
Expand Down Expand Up @@ -1205,6 +1274,7 @@ class TransitConfig(ConfigItem):

modes: Tuple[TransitModeConfig, ...]
classes: Tuple[TransitClassConfig, ...]
journey_levels: TransitJourneyLevelsConfig
apply_msa_demand: bool
value_of_time: float
walk_speed: float
Expand Down

0 comments on commit c1ac021

Please sign in to comment.