-
Notifications
You must be signed in to change notification settings - Fork 17
Add avoidance and intrusion number computation #279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
schroedtert
wants to merge
20
commits into
main
Choose a base branch
from
avoidance-intrusion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
96fe39b
Add intrusion computation
schroedtert 3de4359
Add avoidance computation
schroedtert 3913f39
Show new functions in API reference
schroedtert d0d2888
Fix stuff
schroedtert 09d404c
Fix stuff
schroedtert 7f8a850
What's going on here?
schroedtert d11587f
Fix operator precedence bug in v_rel computation
chraibi 5da45ee
Fix cos_alpha computation in TTC formula
chraibi a8009d6
Add neighbor cutoff r_ij <= 3*r_soc to intrusion computation
chraibi a74b582
Rename foo_calculator to dimensionless_number_calculator
chraibi 76d89f2
Add docstrings for intrusion and avoidance functions
chraibi 1b4f219
Add tests for intrusion and avoidance computation
chraibi 2e990de
Add Intrusion and Avoidance section to user guide
chraibi c230d56
Apply ruff formatting to dimensionless number calculator and tests
chraibi 64b03eb
Import pandas as pd per project convention (ICN001)
chraibi 85199c4
Use .merge() method instead of pd.merge() function (PD015)
chraibi 0cde8d4
Apply ruff formatting to user guide notebook
chraibi fa0729c
formating hell
chraibi ea16ebf
Address PR #279 review feedback
chraibi 6bf6cda
Fix avoidance radius default and export column constants
chraibi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| """Dimensionless numbers for pedestrian crowd classification. | ||
|
|
||
| Implements the Intrusion number and Avoidance number defined in | ||
| Cordes et al., PNAS Nexus 2024 (https://doi.org/10.1093/pnasnexus/pgae120). | ||
| """ | ||
|
|
||
| from enum import Enum | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| import shapely | ||
|
|
||
| from pedpy.column_identifier import ( | ||
| AVOIDANCE_COL, | ||
| FRAME_COL, | ||
| ID_COL, | ||
| INTRUSION_COL, | ||
| ) | ||
| from pedpy.data.trajectory_data import TrajectoryData | ||
| from pedpy.methods.method_utils import ( | ||
| SpeedCalculation, | ||
| compute_individual_distances, | ||
| ) | ||
|
chraibi marked this conversation as resolved.
|
||
| from pedpy.methods.speed_calculator import compute_individual_speed | ||
|
|
||
|
|
||
| class IntrusionMethod(Enum): # pylint: disable=too-few-public-methods | ||
| """Identifier of method to compute the intrusion.""" | ||
|
|
||
| MAX = "max" | ||
| """Use the max value in neighborhood as intrusion.""" | ||
| SUM = "sum" | ||
| """Use the sum of all values in neighborhood as intrusion.""" | ||
|
|
||
|
|
||
| def compute_intrusion( | ||
| *, | ||
| traj_data: TrajectoryData, | ||
| r_soc: float = 0.8, | ||
| l_min: float = 0.2, | ||
| method: IntrusionMethod = IntrusionMethod.SUM, | ||
| ) -> pd.DataFrame: | ||
| r"""Compute the intrusion number for each pedestrian per frame. | ||
|
|
||
| The intrusion variable :math:`\mathcal{I}n_i` quantifies how much | ||
| other agents encroach on pedestrian *i*'s personal space | ||
| (Cordes et al. 2024, Eq. 1): | ||
|
|
||
| .. math:: | ||
|
|
||
| \mathcal{I}n_i = \sum_{j \in \mathcal{N}_i} | ||
| \left(\frac{r_\text{soc} - \ell_\text{min}} | ||
| {r_{ij} - \ell_\text{min}}\right)^{k_I}, | ||
|
|
||
| with :math:`k_I = 2`. The neighbor set :math:`\mathcal{N}_i` contains | ||
| all agents *j* with :math:`r_{ij} \leq 3\,r_\text{soc}`. | ||
|
|
||
| Args: | ||
| traj_data (TrajectoryData): trajectory data to analyze | ||
| r_soc (float): social radius in m (default 0.8) | ||
| l_min (float): pedestrian diameter in m (default 0.2) | ||
| method (IntrusionMethod): aggregation over neighbors, | ||
| ``SUM`` (default, as in the paper) or ``MAX`` | ||
|
|
||
| Returns: | ||
| DataFrame with columns 'id', 'frame', and 'intrusion'. | ||
| Agents with no neighbors within the cutoff (or only at | ||
| exactly ``l_min`` distance) are absent from the result. | ||
| """ | ||
| intrusion = compute_individual_distances(traj_data=traj_data) | ||
| intrusion = intrusion.loc[(intrusion.distance <= 3 * r_soc) & (intrusion.distance > l_min)].copy() | ||
| intrusion.loc[:, INTRUSION_COL] = ((r_soc - l_min) / (intrusion.distance - l_min)) ** 2 | ||
| intrusion = ( | ||
| intrusion.groupby(by=[ID_COL, FRAME_COL]) | ||
| .agg( | ||
| intrusion=(INTRUSION_COL, method.value), | ||
| ) | ||
| .reset_index() | ||
| ) | ||
|
|
||
| return intrusion | ||
|
|
||
|
|
||
| def compute_avoidance( | ||
| *, | ||
| traj_data: TrajectoryData, | ||
| frame_step: int, | ||
| radius: float = 0.4, | ||
| tau_0: float, | ||
| ) -> pd.DataFrame: | ||
| r"""Compute the avoidance number for each pedestrian per frame. | ||
|
|
||
| The avoidance variable :math:`\mathcal{A}v_i` quantifies the | ||
| imminence of collisions that pedestrian *i* faces, based on | ||
| the time-to-collision (TTC) with neighbors | ||
| (Cordes et al. 2024, Eq. 2): | ||
|
|
||
| .. math:: | ||
|
|
||
| \mathcal{A}v_i = \sum_{j \in \mathcal{N}'_i} | ||
| \left(\frac{\tau_0}{\tau_{ij}}\right)^{k_A}, | ||
|
|
||
| with :math:`k_A = 1`. The neighbor set :math:`\mathcal{N}'_i` is | ||
| restricted to the agent with the shortest TTC :math:`\tau_{ij}`, | ||
| implemented by taking the ``max`` over :math:`\tau_0 / \tau_{ij}`. | ||
|
|
||
| Args: | ||
| traj_data (TrajectoryData): trajectory data to analyze | ||
| frame_step (int): number of frames used for velocity computation | ||
| radius (float): disc diameter :math:`\ell_\text{soc}` for TTC | ||
| computation in m (default 0.4, as in the paper) | ||
| tau_0 (float): reference timescale in s (paper uses 3.0) | ||
|
|
||
| Returns: | ||
| DataFrame with columns 'id', 'frame', and 'avoidance' | ||
| """ | ||
| velocity = compute_individual_speed( | ||
| traj_data=traj_data, | ||
| frame_step=frame_step, | ||
| compute_velocity=True, | ||
| speed_calculation=SpeedCalculation.BORDER_SINGLE_SIDED, | ||
| ) | ||
|
|
||
| data = traj_data.data.merge(velocity, on=[ID_COL, FRAME_COL]) | ||
| data["velocity"] = shapely.points(data.v_x, data.v_y) | ||
|
|
||
| matrix = data.merge(data, how="inner", on=FRAME_COL, suffixes=("", "_neighbor")) | ||
| matrix = matrix[matrix[ID_COL] != matrix[f"{ID_COL}_neighbor"]] | ||
|
|
||
| pos = shapely.get_coordinates(matrix.point) | ||
| pos_neighbor = shapely.get_coordinates(matrix.point_neighbor) | ||
| distance = np.linalg.norm(pos - pos_neighbor, axis=1) | ||
|
|
||
| delta_v = shapely.get_coordinates(matrix.velocity) - shapely.get_coordinates(matrix.velocity_neighbor) | ||
| delta_v_norm = np.linalg.norm(delta_v, axis=1) | ||
|
|
||
| # only compute for pairs with nonzero distance and nonzero relative velocity | ||
| computable = (distance > 0) & (delta_v_norm > 0) | ||
|
|
||
| ttc = np.full(matrix.shape[0], np.inf) | ||
|
|
||
| if np.any(computable): | ||
| d_c = distance[computable] | ||
| dv_c = delta_v[computable] | ||
| dv_norm_c = delta_v_norm[computable] | ||
|
|
||
| e_v = (pos[computable] - pos_neighbor[computable]) / d_c[:, np.newaxis] | ||
| v_rel_hat = dv_c / dv_norm_c[:, np.newaxis] | ||
| cos_alpha = np.sum(e_v * v_rel_hat, axis=1) | ||
|
|
||
| capital_a = (cos_alpha**2 - 1) * d_c**2 + radius**2 | ||
| sqrt_a_safe = np.sqrt(np.maximum(capital_a, 0.0)) | ||
|
|
||
| valid = (capital_a >= 0) & (-cos_alpha * d_c - sqrt_a_safe >= 0) | ||
|
|
||
| idx = np.where(computable)[0][valid] | ||
| ttc[idx] = (-cos_alpha[valid] * d_c[valid] - sqrt_a_safe[valid]) / dv_norm_c[valid] | ||
|
|
||
| matrix = matrix.copy() | ||
| matrix[AVOIDANCE_COL] = tau_0 / ttc | ||
|
|
||
| avoidance = matrix.groupby(by=[ID_COL, FRAME_COL], as_index=False).agg(avoidance=(AVOIDANCE_COL, "max")) | ||
| return avoidance | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.