Skip to content

Commit aeacffc

Browse files
authored
Add pathfinder loader (#472)
Add pathfinder test trajectory file for csv and json files
1 parent 493df54 commit aeacffc

6 files changed

Lines changed: 16447 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,pycharm,jupyternotebooks
22
# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,pycharm,jupyternotebooks
33

4+
*~
45
### JupyterNotebooks ###
56
# gitignore template for Jupyter Notebooks
67
# website: http://jupyter.org/

pedpy/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
load_trajectory,
6666
load_trajectory_from_crowdit,
6767
load_trajectory_from_jupedsim_sqlite,
68+
load_trajectory_from_pathfinder_csv,
69+
load_trajectory_from_pathfinder_json,
6870
load_trajectory_from_ped_data_archive_hdf5,
6971
load_trajectory_from_txt,
7072
load_trajectory_from_vadere,
@@ -166,8 +168,12 @@
166168
"load_walkable_area_from_jupedsim_sqlite",
167169
"load_walkable_area_from_ped_data_archive_hdf5",
168170
"load_walkable_area_from_vadere_scenario",
171+
"load_trajectory_from_pathfinder_csv",
172+
"load_trajectory_from_pathfinder_json",
169173
"load_walkable_area_from_crowdit",
170174
"load_trajectory_from_crowdit",
175+
"load_trajectory_from_pathfinder_csv",
176+
"load_trajectory_from_pathfinder_json",
171177
"compute_classic_density",
172178
"compute_line_density",
173179
"compute_passing_density",

pedpy/io/trajectory_loader.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,249 @@ class TrajectoryUnit(Enum): # pylint: disable=too-few-public-methods
3232
"""centimeter (cm)"""
3333

3434

35+
def load_trajectory_from_pathfinder_json(
36+
*,
37+
trajectory_file: pathlib.Path,
38+
) -> TrajectoryData:
39+
"""Loads Pathfinder-JSON as :class:`~trajectory_data.TrajectoryData`.
40+
41+
This function reads a JSON file containing trajectory data
42+
from Pathfinder simulations and converts it
43+
into a :class:`~trajectory_data.TrajectoryData` object.
44+
45+
.. note::
46+
47+
Pathfinder JSON data have a time-based structure that is going to be
48+
converted to a frame column for use with *PedPy*.
49+
50+
Args:
51+
trajectory_file: The full path of the JSON containing the Pathfinder
52+
trajectory data. The expected format is a JSON file with agent IDs
53+
as top-level keys, and time-stamped position data as nested objects.
54+
55+
Returns:
56+
TrajectoryData: :class:`~trajectory_data.TrajectoryData` representation
57+
of the file data
58+
59+
Raises:
60+
LoadTrajectoryError: If the provided path does not exist or is not a
61+
file, or if the JSON structure is invalid.
62+
"""
63+
_validate_is_file(trajectory_file)
64+
65+
traj_dataframe = _load_trajectory_data_from_pathfinder_json(
66+
trajectory_file=trajectory_file
67+
)
68+
traj_dataframe["frame"], traj_frame_rate = _calculate_frames_and_fps(
69+
traj_dataframe
70+
)
71+
72+
return TrajectoryData(
73+
data=traj_dataframe[[ID_COL, FRAME_COL, X_COL, Y_COL]],
74+
frame_rate=traj_frame_rate,
75+
)
76+
77+
78+
def _load_trajectory_data_from_pathfinder_json(
79+
*, trajectory_file: pathlib.Path
80+
) -> pd.DataFrame:
81+
"""Parse the trajectory JSON file for trajectory data.
82+
83+
Args:
84+
trajectory_file (pathlib.Path): The file containing the trajectory data.
85+
The expected format is a JSON file with agent IDs as top-level keys,
86+
and time-stamped position data as nested objects.
87+
88+
Returns:
89+
The trajectory data as :class:`DataFrame`, the coordinates are
90+
in meter (m).
91+
"""
92+
common_error_message = (
93+
"The given trajectory file seems to be incorrect or empty. "
94+
"It should be a valid JSON file with agent IDs as top-level keys "
95+
"and time-stamped position data containing 'position' objects with "
96+
"'x' and 'y' coordinates. "
97+
f"Please check your trajectory file: {trajectory_file}."
98+
)
99+
100+
try:
101+
with open(trajectory_file, "r", encoding="utf-8") as file:
102+
data = json.load(file)
103+
except (json.JSONDecodeError, FileNotFoundError, PermissionError) as e:
104+
raise LoadTrajectoryError(
105+
f"{common_error_message}\nOriginal error: {e}"
106+
) from e
107+
108+
if not isinstance(data, dict) or not data:
109+
raise LoadTrajectoryError(
110+
f"{common_error_message}\nEmpty or invalid JSON structure."
111+
)
112+
113+
trajectory_records = []
114+
115+
try:
116+
for agent_id_str, time_data in data.items():
117+
agent_id = int(agent_id_str)
118+
119+
if not isinstance(time_data, dict):
120+
continue
121+
122+
for time_str, agent_data in time_data.items():
123+
time_value = float(time_str)
124+
125+
if not isinstance(agent_data, dict):
126+
continue
127+
128+
position = agent_data.get("position")
129+
if not isinstance(position, dict):
130+
continue
131+
132+
x_pos = position.get("x")
133+
y_pos = position.get("y")
134+
135+
if x_pos is not None and y_pos is not None:
136+
trajectory_records.append(
137+
{
138+
ID_COL: agent_id,
139+
TIME_COL: time_value,
140+
X_COL: float(x_pos),
141+
Y_COL: float(y_pos),
142+
}
143+
)
144+
145+
except (ValueError, KeyError, TypeError) as e:
146+
raise LoadTrajectoryError(
147+
f"{common_error_message}\nError parsing JSON structure: {e}"
148+
) from e
149+
150+
if not trajectory_records:
151+
raise LoadTrajectoryError(
152+
f"{common_error_message}\nNo valid trajectory data found."
153+
)
154+
155+
traj_dataframe = pd.DataFrame(trajectory_records)
156+
157+
# Sort by agent ID and time for consistency
158+
traj_dataframe = traj_dataframe.sort_values([ID_COL, TIME_COL]).reset_index(
159+
drop=True
160+
)
161+
162+
return traj_dataframe
163+
164+
165+
def load_trajectory_from_pathfinder_csv(
166+
*,
167+
trajectory_file: pathlib.Path,
168+
) -> TrajectoryData:
169+
"""Loads data from Pathfinder-CSV file as :class:`~trajectory_data.TrajectoryData`.
170+
171+
This function reads a CSV file containing trajectory data from Pathfinder
172+
simulations and converts it into a :class:`~trajectory_data.TrajectoryData`
173+
object.
174+
175+
.. note::
176+
177+
Pathfinder data have a time column, that is going to be converted to a
178+
frame column for use with *PedPy*.
179+
180+
.. warning::
181+
182+
Currently only Pathfinder files with a time column can be loaded.
183+
184+
Args:
185+
trajectory_file: The full path of the CSV file containing the Pathfinder
186+
trajectory data. The expected format is a CSV file with comma
187+
as delimiter, and it should contain at least the following
188+
columns: id, t, x, y.
189+
190+
Returns:
191+
TrajectoryData: :class:`~trajectory_data.TrajectoryData` representation
192+
of the file data
193+
194+
Raises:
195+
LoadTrajectoryError: If the provided path does not exist or is not a
196+
file.
197+
""" # noqa: E501
198+
_validate_is_file(trajectory_file)
199+
200+
traj_dataframe = _load_trajectory_data_from_pathfinder_csv(
201+
trajectory_file=trajectory_file
202+
)
203+
traj_dataframe["frame"], traj_frame_rate = _calculate_frames_and_fps(
204+
traj_dataframe
205+
)
206+
207+
return TrajectoryData(
208+
data=traj_dataframe[[ID_COL, FRAME_COL, X_COL, Y_COL]],
209+
frame_rate=traj_frame_rate,
210+
)
211+
212+
213+
def _load_trajectory_data_from_pathfinder_csv(
214+
*, trajectory_file: pathlib.Path
215+
) -> pd.DataFrame:
216+
"""Parse the trajectory file for trajectory data.
217+
218+
Args:
219+
trajectory_file (pathlib.Path): The file containing the trajectory data.
220+
The expected format is a CSV file with comma as delimiter, and it
221+
should contain at least the following columns: name, t, x, y.
222+
223+
Returns:
224+
The trajectory data as :class:`DataFrame`, the coordinates are
225+
in meter (m).
226+
"""
227+
columns_to_keep = ["id", "t", "x", "y"]
228+
rename_mapping = {
229+
"id": ID_COL,
230+
"t": TIME_COL,
231+
"x": X_COL,
232+
"y": Y_COL,
233+
}
234+
column_types = {"id": int, "time": float, "x": float, "y": float}
235+
236+
common_error_message = (
237+
"The given trajectory file seems to be incorrect or empty. "
238+
"It should contain at least the following columns: "
239+
"id, t, x, y, separated by comma. "
240+
f"Please check your trajectory file: {trajectory_file}."
241+
)
242+
# csv has a unit line. Usually the second line,
243+
# but not 100% sure if this is always the case.
244+
try:
245+
raw = pd.read_csv(
246+
trajectory_file,
247+
encoding="utf-8-sig",
248+
)
249+
except Exception as e:
250+
raise LoadTrajectoryError(
251+
f"{common_error_message}\nOriginal error: {e}"
252+
) from e
253+
# filter out the unit line
254+
data = raw[
255+
raw["t"].apply(lambda v: str(v).replace(".", "", 1).isdigit())
256+
].copy()
257+
missing_columns = set(columns_to_keep) - set(data.columns)
258+
if missing_columns:
259+
raise LoadTrajectoryError(
260+
f"{common_error_message} "
261+
f"Missing columns: {', '.join(missing_columns)}."
262+
)
263+
try:
264+
data = data[columns_to_keep]
265+
data = data.rename(columns=rename_mapping)
266+
data = data.astype(column_types)
267+
except Exception as e:
268+
raise LoadTrajectoryError(
269+
f"{common_error_message}\nOriginal error: {e}"
270+
) from e
271+
272+
if data.empty:
273+
raise LoadTrajectoryError(f"{common_error_message}.\n Empty dataframe.")
274+
275+
return data
276+
277+
35278
def _validate_is_file(file: pathlib.Path) -> None:
36279
"""Validates if the given file is a valid file, if valid raises Exception.
37280

0 commit comments

Comments
 (0)