Skip to content

Commit

Permalink
Merge pull request #256 from arup-group/clean-docstrings-and-refs
Browse files Browse the repository at this point in the history
Clean docstrings and refs
  • Loading branch information
brynpickering authored Sep 25, 2023
2 parents 7aa6b69 + 10617b8 commit 8631079
Show file tree
Hide file tree
Showing 14 changed files with 75 additions and 80 deletions.
8 changes: 4 additions & 4 deletions docs/activity_plans.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Activity Plans

PAM supports arbitrarily complex chains of activities connected by 'legs' (these are equivalent to 'trips').
The main rules are (i) that plans must consist of sequences of alternate `pam.activity.Activity` and `pam.activity.Leg` objects and (ii) that a plan must start and end with an `Activity`:
The main rules are (i) that plans must consist of sequences of alternate [][pam.activity.Activity] and [][pam.activity.Leg] objects and (ii) that a plan must start and end with an `Activity`:

``` python
from pam.core import Person
Expand Down Expand Up @@ -130,9 +130,9 @@ This need not be a `home` activity, for example in the case of night workers.
We have encountered many variations of sequences for plans, including wrapping and wrapping.
Although they are generally edge cases, they exists and generally represent real people.
We are therefore endeavoring to support all these cases in our plan modifiers.
This is resulting some difficult to follow logic (eg `pam.activity.Plan.fill_plan()`).
This is resulting some difficult to follow logic (e.g., [][pam.activity.Plan.fill_plan]).

## Plan cropping
The `pam.cropping` module allows to spatially subset populations, by simplifying plan components that take place outside the "core" area.
The [`pam.operations.cropping`](reference/pam/operations/cropping.md) module allows to spatially subset populations, by simplifying plan components that take place outside the "core" area.
Any activities or legs that do not affect that core area are removed from the agents' plans, and agents with fully-external plans are removed from the population.
Examples of using the module can be found in the `18_plan_cropping.ipynb` notebook.
Examples of using the module can be found in the [][plan-cropping] notebook.
22 changes: 11 additions & 11 deletions docs/modelling_policy_impact.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Modelling the impact of policies on populations

PAM uses **policies** to model change to a population.
For example, based on social distancing requirements we might want to reflect that people are expected to make less shared shopping trips or tours.
PAM uses **policies** to model change to a population.
For example, based on social distancing requirements we might want to reflect that people are expected to make less shared shopping trips or tours.
We can do this using the following policy, if you want to define the policy from first principles:

``` python
Expand All @@ -27,23 +27,23 @@ This policy removes all but one the household's shared shopping tours:
In general, a policy is defined in the following way:

1. You first select the level at which is should be applied:
- `HouseholdPolicy`
- `PersonPolicy`
- `ActivityPolicy`
- [HouseholdPolicy][pam.policy.policies.HouseholdPolicy]
- [PersonPolicy][pam.policy.policies.PersonPolicy]
- [ActivityPolicy][pam.policy.policies.ActivityPolicy]
2. You then select the modifier, which performs the actions to a person's activities
- `RemoveActivity`
- `ReduceSharedActivity`
- `MoveActivityTourToHomeLocation`
3. Finally, you give a likelihood value with which the policy should be applied with.
- [RemoveActivity][pam.policy.modifiers.RemoveActivity]
- [ReduceSharedActivity][pam.policy.modifiers.ReduceSharedActivity]
- [MoveActivityTourToHomeLocation][pam.policy.modifiers.MoveActivityTourToHomeLocation]
3. Finally, you give a likelihood value with which the policy should be applied with.
You have a few choices here:
- a number greater than 0 and less or equal to 1.
- a number greater than 0 and less or equal to 1.
This will be understood to be at the level at which the policy is applied.
E.g. `#!python PersonPolicy(RemoveActivity(['work']), 0.5)` will give each person a fifty-fifty chance of having their work activities removed.

- you can explicitly define at which level a number greater than 0 and less or equal to 1 will be applied.
E.g. `#!python HouseholdPolicy(RemoveActivity(['work']), PersonProbability(0.5))` will apply a probability of 0.5 per person in a household, and apply the policy to all persons within a household if selected.

- you can also pass a function that operates on a `core.Household`, `core.Person` or `core.Activity` object and returns a number between 0 and 1.
- you can also pass a function that operates on a [Household][pam.core.Household], [Person][pam.core.Person] or [Activity][pam.activity.Activity] object and returns a number between 0 and 1.
E.g. if our function is:
``` python
def sampler(person):
Expand Down
15 changes: 9 additions & 6 deletions docs/read_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ population.print()

## Read methods

The first step in any application is to load your data into the core pam format (pam.core.Population). We
are trying to support common tabular formats ('travel diaries') using `pam.read.load_travel_diary`. A
The first step in any application is to load your data into the core pam format ([][pam.core.Population]). We
are trying to support common tabular formats ('travel diaries') using [][pam.read.load_travel_diary]. A
travel diary can be composed of three tables:

- `trips` (required) - a trip diary for all people in the population, with rows representing trips
Expand Down Expand Up @@ -99,7 +99,7 @@ The `trips` input **may** also include the following fields:

**'trip purpose' vs 'tour purpose':**

We've encountered a few different ways that trip purpose can be encoded. The preferred way being to encode a trip purpose as being the activity of the destination, so that a trip home would be encoded as `purp = home`. However we've also seen the more complex 'tour purpose' encoding, in which case a return trip from work to home is encoded as `purp = work`. Good news is that the `pam.read.load_travel_diary` will deal ok with either. But it's worth checking.
We've encountered a few different ways that trip purpose can be encoded. The preferred way being to encode a trip purpose as being the activity of the destination, so that a trip home would be encoded as `purp = home`. However we've also seen the more complex 'tour purpose' encoding, in which case a return trip from work to home is encoded as `purp = work`. Good news is that the [][pam.read.load_travel_diary] will deal ok with either. But it's worth checking.

**Using persons_attributes and /or households_attributes**

Expand All @@ -125,7 +125,7 @@ If you are using persons_attributes (`persons_attributes`) this table must conta

**A note about 'freq':**

Frequencies (aka 'weights') for trips, persons or households can optionally be added to the respective input tables using columns called `freq`. We generally assume a frequency to represent expected occurrences in a full population. For example if we use a person frequency (`person.freq`) the the sum of all these frequencies (`population.size`), will equal the expected population size.
Frequencies (aka 'weights') for trips, persons or households can optionally be added to the respective input tables using columns called `freq`. We generally assume a frequency to represent expected occurrences in a full population. For example if we use a person frequency ([][pam.core.Person.freq]) the the sum of all these frequencies ([][pam.core.Population.size]), will equal the expected population size.

Because it is quite common to provide a person or household `freq` in the trips table, there are two special options (`trip_freq_as_person_freq = True` and `trip_freq_as_hh_freq = True`) that can be used to pass the `freq` field from the trips table to either the people or households table instead.

Expand All @@ -141,8 +141,11 @@ for pid, person in household:

### Read/Write/Other formats

PAM can read/write to tabular formats and MATSim xml (`pam.read.read_matsim` and `pam.write.write_matsim`). PAM can also write to segmented OD matrices using `pam.write.write_od_matrices`.
PAM can read/write to tabular formats and MATSim xml ([][pam.read.read_matsim] and [][pam.write.write_matsim]).
PAM can also write to segmented OD matrices using [][pam.write.write_od_matrices].

Benchmark or summary data and cross-tabulations can be extracted with the `pam.write.write_benchmarks` method, passing as arguments the data field(s) to summarise, the dimension(s) to group by, and the aggregation function(s). For example `pam.write_benchmarks(population, dimensions = ['duration_category'], data_fields= ['freq'], aggfunc = [sum]` returns the frequency breakdown of trips' duration. The `write` module also provides a number of wrappers for frequently-used benchmarks under the `write_*****_benchmark` name.
Benchmark or summary data and cross-tabulations can be extracted with the [benchmarking CLI method](api/cli.md#pam-report-benchmarks).
For more fine-grain control, pandas dataframes for specific data field(s), dimension(s) and aggregation function(s) can be generated with [][pam.report.benchmarks.create_benchmark].
For example `pam.report.benchmarks.create_benchmark(population.trips_df(), dimensions = ['duration_category'], data_fields= ['freq'], aggfunc = [sum]` returns the frequency breakdown of trips' duration.

Please get in touch if you would like additional support or feel free to add your own.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ plugins:
signature_crossrefs: true
show_root_toc_entry: false
show_signature_annotations: false
inherited_members: true
import:
- https://docs.python.org/3/objects.inv
- https://shapely.readthedocs.io/en/stable/objects.inv
Expand Down
102 changes: 47 additions & 55 deletions pam/planner/choice_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,20 +261,18 @@ def __init__(self, population: Population, od: OD, zones: pd.DataFrame) -> None:


class DiscretionaryTrips:
"""
Solve discretionary trip location choice of a PAM plan
"""

def __init__(self, plan: Plan, od: OD) -> None:
"""Solve discretionary trip location choice of a PAM plan.
Args:
plan (Plan): PAM plan.
od (OD): An object holding origin-destination matrices.
"""
self._plan = plan
self._od = od

def update_plan(self):
"""Update the locations (in-place) of each non-mandatory activity location in the plan.
Returns:
None
"""
"""Update the locations (in-place) of each non-mandatory activity location in the plan."""
trip_chains = get_trip_chains_either_anchor(self._plan)
for trip_chain in trip_chains:
# if only one achor, convert to round-trip
Expand All @@ -289,22 +287,20 @@ def update_plan(self):

class DiscretionaryTrip(ABC):
def __init__(self, trip_chain: list[Union[Activity, Leg]], od: OD) -> None:
"""
Location choice for discretionary trips
in a trip chain
"""Location choice for discretionary trips in a trip chain.
Cases:
a) O->discretionary->O (DiscretionaryTripRound)
b) O->discretionary->D (DiscretionaryTripOD)
c) O->discretionary->discretionary->O
d) O->discretionary->discretionary->D
1. O->discretionary->O (DiscretionaryTripRound)
2. O->discretionary->D (DiscretionaryTripOD)
3. O->discretionary->discretionary->O
4. O->discretionary->discretionary->D
Chains with multiple discretionary trips are solved
recursively, updating the first location each time,
and then keeping it fixed as we solve downstream.
Chains with multiple discretionary trips are solved recursively,
updating the first location each time, and then keeping it fixed as we solve downstream.
Args:
list[Union[Activity, Leg]]: A trip chain between two long-term activities.
trip_chain (list[Union[Activity, Leg]]): A trip chain between two long-term activities.
od (OD): An object holding origin-destination matrices.
"""
self._trip_chain = trip_chain
self._od = od
Expand All @@ -326,22 +322,17 @@ def __init__(self, trip_chain: list[Union[Activity, Leg]], od: OD) -> None:
raise TypeError("Each even element in the trip chain should be an activity")

@abstractmethod
def choose_destination(self):
"""Selects a destination for the discretionary activity
def choose_destination(self) -> str:
"""Selects a destination for the discretionary activity.
Returns:
str: Selected destination zone name.
"""

def update_plan(self):
"""
Update the PAM activity locations of the first activity activity
in the trip chain,
and continue downstream until the entire chain is solved.
Returns:
None
Update the PAM activity locations of the first activity in the trip chain,
and continue downstream until the entire chain is solved.
"""
if len(self.act_names) > 2:
# update locations
Expand All @@ -362,20 +353,23 @@ def update_plan(self):
DiscretionaryTripOD(trip_chain=self._trip_chain[2:], od=self._od).update_plan()

@property
def od(self):
def od(self) -> OD:
return self._od


class DiscretionaryTripRound(DiscretionaryTrip):
"""
Location choice for a single discretionary trip,
where we have the same anchor at the start and end of the chain.
Location choice for a single discretionary trip, where we have the same anchor at the start and end of the chain.
The class infers the location of the first discretionary
activity in the trip chain.
The class infers the location of the first discretionary activity in the trip chain.
"""

def choose_destination(self) -> str:
"""Selects a destination for the discretionary activity.
Returns:
str: Selected destination zone name.
"""
assert isinstance(self._trip_chain[1], Leg)
destination_p = self._od["od_probs", self.anchor_zone_start, :, self.trmode]
destination_p = destination_p / destination_p.sum()
Expand All @@ -386,19 +380,18 @@ def choose_destination(self) -> str:


class DiscretionaryTripOD(DiscretionaryTrip):
"""
Location choice for a single discretionary trip,
where we have different anchors at the start and end of the chain.
"""Location choice for a single discretionary trip, where we have different anchors at the start and end of the chain.
The class infers the location of the first trip in the trip chain.
Methodology
We combine three conditions:
1) Distribution of leg compared to total trip
2) Diversion factor (compared to direct trips)
3) Zone attraction
Final probabilities are defined as (1) * (2) * (3)
(and then normalised to sum up to 1)
Methodology:
We combine three conditions:
1. Distribution of leg compared to total trip
2. Diversion factor (compared to direct trips)
3. Zone attraction
Final probabilities are defined as (1) * (2) * (3) (and then normalised to sum up to 1).
"""

Expand Down Expand Up @@ -446,8 +439,7 @@ def leg_ratio_p(self) -> np.array:
@property
def diversion_factors(self) -> np.array:
"""
Calculate the diversion factor for each potential destination
(as compared to a direct trip between anchors).
Calculate the diversion factor for each potential destination (as compared to a direct trip between anchors).
Returns:
np.array: An array of the time diversion factors for each candidate intermediate destination.
Expand All @@ -463,10 +455,9 @@ def diversion_factors(self) -> np.array:
return diversion_factors

@property
def diversion_p(self):
def diversion_p(self) -> np.array:
"""Diversion factor probabilities
Returns:
np.array: An array of the time diversion factor probabilities for each candidate intermediate destination.
Expand All @@ -475,20 +466,16 @@ def diversion_p(self):

@property
def attraction_p(self):
"""Attraction probabilities
Returns:
None
"""
"""Attraction probabilities."""
probs = self._od["od_probs", self.anchor_zone_start, :, self.trmode]
probs = probs / probs.sum()
return probs

@property
def destination_p(self) -> np.array:
"""Get the destination probabilities.
Combines the leg ratio, diversion, and attraction factors probabilities
by calculating their product.
Combines the leg ratio, diversion, and attraction factors probabilities by calculating their product.
Returns:
np.array: Final destination probabilities.
Expand All @@ -498,6 +485,11 @@ def destination_p(self) -> np.array:
return p

def choose_destination(self) -> str:
"""Selects a destination for the discretionary activity.
Returns:
str: Selected destination zone name.
"""
zone = sample_weighted(self.destination_p)
area = self._od.labels.destination_zones[zone]

Expand Down
5 changes: 2 additions & 3 deletions pam/planner/od.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing import NamedTuple, Union

import numpy as np
import pandas as pd


class Labels(NamedTuple):
Expand Down Expand Up @@ -107,10 +106,10 @@ def from_matrices(cls, matrices: list[ODMatrix]) -> OD:
@staticmethod
def prepare_labels(matrices: list[ODMatrix]) -> Labels:
labels = Labels(
vars=list(pd.unique([mat.var for mat in matrices])),
vars=list(dict.fromkeys(mat.var for mat in matrices)),
origin_zones=matrices[0].origin_zones,
destination_zones=matrices[0].destination_zones,
mode=list(pd.unique([mat.mode for mat in matrices])),
mode=list(dict.fromkeys(mat.mode for mat in matrices)),
)
return labels

Expand Down
2 changes: 1 addition & 1 deletion pam/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def score_person(
Args:
person (Person):
subpopulation (str, optional): person attribute name for subpopulation. Defaults to "subpopulation".
key (str, optional): person attribute name for subpopulation. Defaults to "subpopulation".
plan_costs (float, optional): Optionally add monetary costs such as tolls. Defaults to None.
Returns:
Expand Down
Binary file removed resources/PAM-examples.pdf
Binary file not shown.
Binary file removed resources/PAM-intro.pdf
Binary file not shown.
Binary file removed resources/PAM-policy-design.pdf
Binary file not shown.
Binary file removed resources/pam-logo.png
Binary file not shown.
Binary file removed resources/plotly_plots/colour_by_pid_plot.png
Binary file not shown.
Binary file removed resources/plotly_plots/default_plot.png
Binary file not shown.
Binary file removed resources/plotly_plots/groupby_hid_plot.png
Binary file not shown.

0 comments on commit 8631079

Please sign in to comment.