Skip to content

Commit d67a785

Browse files
schroedtertchraibi
andauthored
Add viswalk trajectory loader (#252)
Co-authored-by: Mohcine Chraibi <m.chraibi@fz-juelich.de>
1 parent aab97b4 commit d67a785

6 files changed

Lines changed: 534238 additions & 0 deletions

File tree

notebooks/demo-data/viswalk/example.pp

Lines changed: 266884 additions & 0 deletions
Large diffs are not rendered by default.

notebooks/user_guide.ipynb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,58 @@
411411
"plt.show()"
412412
]
413413
},
414+
{
415+
"cell_type": "markdown",
416+
"metadata": {},
417+
"source": [
418+
"#### Loading from Viswalk trajectory files\n",
419+
"\n",
420+
"It is also possible to load trajectory files from [Viswalk](https://www.ptvgroup.com/en/products/pedestrian-simulation-software-ptv-viswalk) directly into *PedPy*. \n",
421+
"The expected format is a CSV file with `;` as delimiter, and it should contain at least the following columns: `NO`, `SIMSEC`, `COORDCENTX`, `COORDCENTY`.\n",
422+
"Comment lines may start with a `*` and will be ignored.\n",
423+
"\n",
424+
":::{important}\n",
425+
"Currently only Viswalk trajectory files, which use the simulation time (`SIMSEC`) are supported.\n",
426+
":::\n",
427+
"\n",
428+
"\n",
429+
"To make the data usable for *PedPy* use:"
430+
]
431+
},
432+
{
433+
"cell_type": "code",
434+
"execution_count": null,
435+
"metadata": {},
436+
"outputs": [],
437+
"source": [
438+
"from pedpy import (\n",
439+
" TrajectoryData,\n",
440+
" load_trajectory_from_viswalk,\n",
441+
")\n",
442+
"import pathlib\n",
443+
"\n",
444+
"viswalk_file = pathlib.Path(\"demo-data/viswalk/example.pp\")\n",
445+
"\n",
446+
"traj_viswalk = load_trajectory_from_viswalk(trajectory_file=viswalk_file)"
447+
]
448+
},
449+
{
450+
"cell_type": "code",
451+
"execution_count": null,
452+
"metadata": {
453+
"tags": [
454+
"hide-input"
455+
]
456+
},
457+
"outputs": [],
458+
"source": [
459+
"import matplotlib.pyplot as plt\n",
460+
"from pedpy import plot_trajectories\n",
461+
"\n",
462+
"plot_trajectories(traj=traj_viswalk).set_aspect(\"equal\")\n",
463+
"plt.show()"
464+
]
465+
},
414466
{
415467
"cell_type": "markdown",
416468
"metadata": {

pedpy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
load_trajectory_from_jupedsim_sqlite,
2020
load_trajectory_from_ped_data_archive_hdf5,
2121
load_trajectory_from_txt,
22+
load_trajectory_from_viswalk,
2223
load_walkable_area_from_jupedsim_sqlite,
2324
load_walkable_area_from_ped_data_archive_hdf5,
2425
)
@@ -87,6 +88,7 @@
8788
"load_trajectory_from_jupedsim_sqlite",
8889
"load_trajectory_from_ped_data_archive_hdf5",
8990
"load_trajectory_from_txt",
91+
"load_trajectory_from_viswalk",
9092
"load_walkable_area_from_jupedsim_sqlite",
9193
"load_walkable_area_from_ped_data_archive_hdf5",
9294
"compute_classic_density",

pedpy/io/trajectory_loader.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Load trajectories to the internal trajectory data format."""
2+
import math
23
import pathlib
34
import sqlite3
45
from enum import Enum
@@ -449,3 +450,134 @@ def load_walkable_area_from_ped_data_archive_hdf5(
449450
walkable_area = WalkableArea(hdf5_file.attrs["wkt_geometry"])
450451

451452
return walkable_area
453+
454+
455+
def load_trajectory_from_viswalk(
456+
*,
457+
trajectory_file: pathlib.Path,
458+
) -> TrajectoryData:
459+
"""Loads data from Viswalk-csv file as :class:`~trajectory_data.TrajectoryData`.
460+
461+
This function reads a CSV file containing trajectory data from Viswalk simulations and
462+
converts it into a :class:`~trajectory_data.TrajectoryData` object which can be used for
463+
further analysis and processing in the *PedPy* framework.
464+
465+
.. note::
466+
467+
Viswalk data have a time column, that is going to be converted to a frame column for use
468+
with *PedPy*.
469+
470+
.. warning::
471+
472+
Currently only Viswalk files with a time column can be loaded.
473+
474+
Args:
475+
trajectory_file: The full path of the CSV file containing the Viswalk
476+
trajectory data. The expected format is a CSV file with :code:`;` as delimiter, and it
477+
should contain at least the following columns: NO, SIMSEC, COORDCENTX, COORDCENTY.
478+
Comment lines may start with a :code:`*` and will be ignored.
479+
480+
Returns:
481+
:class:`~trajectory_data.TrajectoryData` representation of the file data
482+
483+
Raises:
484+
LoadTrajectoryError: If the provided path does not exist or is not a file.
485+
"""
486+
_validate_is_file(trajectory_file)
487+
488+
traj_dataframe = _load_trajectory_data_from_viswalk(
489+
trajectory_file=trajectory_file
490+
)
491+
traj_dataframe["frame"], traj_frame_rate = _calculate_frames_and_fps(
492+
traj_dataframe
493+
)
494+
495+
return TrajectoryData(
496+
data=traj_dataframe[[ID_COL, FRAME_COL, X_COL, Y_COL]],
497+
frame_rate=traj_frame_rate,
498+
)
499+
500+
501+
def _calculate_frames_and_fps(
502+
traj_dataframe: pd.DataFrame,
503+
) -> Tuple[pd.Series, int]:
504+
"""Calculates fps and frames based on the time column of the dataframe."""
505+
mean_diff = traj_dataframe.groupby(ID_COL)["time"].diff().dropna().mean()
506+
if math.isnan(mean_diff):
507+
raise LoadTrajectoryError(
508+
"Can not determine the frame rate used to write the trajectory file. "
509+
"This may happen, if the file only contains data for a single frame."
510+
)
511+
512+
fps = int(round(1 / mean_diff))
513+
frames = traj_dataframe["time"] * fps
514+
frames = frames.round().astype("int64")
515+
return frames, fps
516+
517+
518+
def _load_trajectory_data_from_viswalk(
519+
*, trajectory_file: pathlib.Path
520+
) -> pd.DataFrame:
521+
"""Parse the trajectory file for trajectory data.
522+
523+
Args:
524+
trajectory_file (pathlib.Path): The file containing the trajectory data.
525+
The expected format is a CSV file with ';' as delimiter, and it should
526+
contain at least the following columns: NO, SIMSEC, COORDCENTX, COORDCENTY.
527+
Comment lines may start with a '*' and will be ignored.
528+
529+
Returns:
530+
The trajectory data as :class:`DataFrame`, the coordinates are
531+
in meter (m).
532+
"""
533+
columns_to_keep = ["NO", "SIMSEC", "COORDCENTX", "COORDCENTY"]
534+
rename_mapping = {
535+
"NO": ID_COL,
536+
"SIMSEC": "time",
537+
"COORDCENTX": X_COL,
538+
"COORDCENTY": Y_COL,
539+
}
540+
common_error_message = (
541+
"The given trajectory file seems to be incorrect or empty. "
542+
"It should contain at least the following columns: "
543+
"NO, SIMSEC, COORDCENTX, COORDCENTY, separated by ';'. "
544+
"Comment lines may start with a '*' and will be ignored. "
545+
f"Please check your trajectory file: {trajectory_file}."
546+
)
547+
try:
548+
data = pd.read_csv(
549+
trajectory_file,
550+
delimiter=";",
551+
skiprows=1, # skip first row containing '$VISION'
552+
comment="*",
553+
dtype={
554+
ID_COL: "int64",
555+
"time": "float64",
556+
X_COL: "float64",
557+
Y_COL: "float64",
558+
},
559+
encoding="utf-8-sig",
560+
)
561+
got_columns = data.columns
562+
cleaned_columns = got_columns.map(
563+
lambda x: x.replace("$PEDESTRIAN:", "")
564+
)
565+
set_columns_to_keep = set(columns_to_keep)
566+
set_cleaned_columns = set(cleaned_columns)
567+
missing_columns = set_columns_to_keep - set_cleaned_columns
568+
if missing_columns:
569+
raise LoadTrajectoryError(
570+
f"{common_error_message}"
571+
f"Missing columns: {', '.join(missing_columns)}."
572+
)
573+
574+
data.columns = cleaned_columns
575+
data = data[columns_to_keep]
576+
data.rename(columns=rename_mapping, inplace=True)
577+
578+
if data.empty:
579+
raise LoadTrajectoryError(common_error_message)
580+
581+
return data
582+
except pd.errors.ParserError as exc:
583+
raise LoadTrajectoryError(common_error_message) from exc

0 commit comments

Comments
 (0)