trajectopy 5.1.3__tar.gz → 6.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.pre-commit-config.yaml +32 -23
- {trajectopy-5.1.3 → trajectopy-6.0.0}/PKG-INFO +1 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_alignment.py +74 -15
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_comparison.py +10 -7
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_context_menus.py +62 -1
- trajectopy-6.0.0/test/test_correlation.py +151 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_gui_models.py +101 -0
- trajectopy-6.0.0/test/test_gui_operations.py +223 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_gui_smoke.py +15 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_io.py +7 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_manual_alignment.py +15 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_matching.py +128 -1
- trajectopy-6.0.0/test/test_project.py +297 -0
- trajectopy-6.0.0/test/test_regressions.py +611 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_visualization.py +302 -228
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/util.py +218 -218
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/__init__.py +172 -170
- trajectopy-6.0.0/trajectopy/__version__.py +1 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/cli/main.py +11 -10
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/parameters.py +10 -3
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/positions.py +508 -460
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/rotations.py +2 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/settings.py +4 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/trajectory.py +71 -29
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/file_manager.py +34 -15
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/requests.py +2 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/trajectory_manager.py +68 -12
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/ui_manager.py +55 -35
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/entries.py +74 -24
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/result_model.py +14 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/table_model.py +3 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/trajectory_model.py +8 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/project.py +80 -71
- trajectopy-6.0.0/trajectopy/gui/project_format.py +254 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/utils.py +56 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/dof_organizer.py +248 -248
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/json_settings_view.py +3 -2
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/main_window.py +26 -5
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/manual_alignment_window.py +32 -16
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/playback_window.py +11 -3
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/preview_plot.py +37 -11
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/preview_plot_3d.py +160 -162
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/result_context_menu.py +16 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/timeline_widget.py +1 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/trajectory_context_menu.py +14 -9
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/ascii.py +1 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/header.py +279 -269
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/kml.py +448 -447
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/results_io.py +261 -204
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/trajectory_io.py +31 -4
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/alignment.py +8 -7
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/approximation.py +17 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/evaluation.py +76 -61
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/interpolation.py +52 -15
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/data.py +38 -24
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/direct.py +6 -2
- trajectopy-6.0.0/trajectopy/processing/lib/alignment/egrad_interface.py +103 -0
- trajectopy-6.0.0/trajectopy/processing/lib/alignment/estimation.py +609 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/cubic_approximation.py +2 -3
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/voxelizer.py +5 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/matching.py +433 -354
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/merging.py +10 -13
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/sorting.py +8 -4
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/results/ate_result.py +593 -527
- trajectopy-6.0.0/trajectopy/results/correlation.py +157 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/results/rpe_result.py +24 -24
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/colors.py +2 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/formatting.py +144 -144
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/linalg.py +1 -1
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/numeric.py +5 -3
- trajectopy-6.0.0/trajectopy/utils/statistics.py +172 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/__init__.py +2 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/figure.py +55 -4
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/plots.py +931 -813
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/file_formats.md +5 -3
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/settings.md +204 -203
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/user_guide.md +425 -388
- trajectopy-5.1.3/test/test_project.py +0 -157
- trajectopy-5.1.3/test/test_regressions.py +0 -60
- trajectopy-5.1.3/trajectopy/__version__.py +0 -1
- trajectopy-5.1.3/trajectopy/processing/lib/alignment/egrad_interface.py +0 -155
- trajectopy-5.1.3/trajectopy/processing/lib/alignment/estimation.py +0 -803
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.gitattributes +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.github/workflows/ci.yml +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.github/workflows/release.yml +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.gitignore +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/.pylintrc +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/CITATION.cff +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/LICENSE +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/README.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_data/KITTI_ORB.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_data/KITTI_SPTAM.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_data/KITTI_gt.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/align.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/alignment.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/ate.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/deviation_plots.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/rpe.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/example_scripts/trajectory_plot.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/installer/trajectopy.iss +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/pyproject.toml +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/requirements.txt +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/abs_dev.result +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/align_entry.result +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/generated_trajectory.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/noisy_trajectory.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/open_loop_trajectory.traj +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/rel_dev_meter.result +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/data/rel_dev_second.result +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_approximation.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_architecture.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_fast_reading.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_inplace_contract.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_kml.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_merging.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_pipeline.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_positions_rotations.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_processing_edge_cases.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_processing_errors.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_readers_edge_cases.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_readers_errors.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_settings.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_settings_edge_cases.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_settings_serialization.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_settings_validation.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_sorting.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_trajectory.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_trajectory_validation.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/test_update_check.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/test/testdata.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/LICENSE +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/__main__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/cli/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/core/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/exceptions.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/app.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/plot_manager.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/managers/project_manager.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/property_model.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/models/selection.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/resources/full-icon-poppins.png +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/resources/icon-bg.ico +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/resources/icon-bg.png +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/resources/icon.png +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/themes.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/update_check.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/about_window.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/alignment_edit_window.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/entry_table_view.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/kml_dialog.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/menu_helpers.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/progress_window.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/properties_panel.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/result_selection_window.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/result_table_view.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/views/trajectory_table_view.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/gui/worker.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/rosbag.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/rosmsg.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/io/utils.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/equations.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/parameters.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/line_approximation.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/py.typed +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/readers/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/readers/ascii.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/readers/rosbag.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/readers/rosmsg.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/readers/utils.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/results/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/results/alignment_result.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/__init__.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/common.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/definitions.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/geometry.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/utils/resources.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/_svg_export.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/axes.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy/visualization/style.py +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/parameters.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/positions.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/rotations.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/settings.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/trajectory.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/index.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/kml.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/results_io.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/rosbag.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/trajectory_io.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/alignment.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/approximation.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/evaluation.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/interpolation.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/matching.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/merging.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/sorting.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/alignment_result.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/ate_result.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/rpe_result.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/visualization/display.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/api/visualization/figures.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/assets/icon.png +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/index.md +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/javascripts/mathjax.js +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/docs/stylesheets/extra.css +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy-docs/mkdocs.yml +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/trajectopy.spec +0 -0
- {trajectopy-5.1.3 → trajectopy-6.0.0}/uv.lock +0 -0
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
repos:
|
|
2
|
-
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
-
rev: v0.14.0
|
|
4
|
-
hooks:
|
|
5
|
-
- id: ruff-check
|
|
6
|
-
args: [--fix]
|
|
7
|
-
- id: ruff-format
|
|
8
|
-
|
|
9
|
-
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
10
|
-
rev: 0.7.6
|
|
11
|
-
hooks:
|
|
12
|
-
- id: uv-lock
|
|
13
|
-
- id: uv-export
|
|
14
|
-
|
|
15
|
-
- repo: local
|
|
16
|
-
hooks:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.14.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
10
|
+
rev: 0.7.6
|
|
11
|
+
hooks:
|
|
12
|
+
- id: uv-lock
|
|
13
|
+
- id: uv-export
|
|
14
|
+
|
|
15
|
+
- repo: local
|
|
16
|
+
hooks:
|
|
17
|
+
# same check as the CI lint job: the whole package with the GUI packages, whose types trajectopy.gui needs
|
|
18
|
+
- id: mypy
|
|
19
|
+
name: Type check (mypy)
|
|
20
|
+
entry: uv run --extra gui mypy
|
|
21
|
+
language: system
|
|
22
|
+
pass_filenames: false
|
|
23
|
+
types: [python]
|
|
24
|
+
stages: [pre-commit]
|
|
25
|
+
|
|
26
|
+
- id: pytest
|
|
27
|
+
name: Run unit tests
|
|
28
|
+
entry: uv run pytest -q
|
|
29
|
+
language: system
|
|
30
|
+
pass_filenames: false
|
|
31
|
+
stages: [pre-commit]
|
|
32
|
+
always_run: true
|
|
@@ -8,6 +8,7 @@ from trajectopy.core.parameters import AlignmentParameters
|
|
|
8
8
|
from trajectopy.core.rotations import Rotations
|
|
9
9
|
from trajectopy.core.settings import (
|
|
10
10
|
AlignmentEstimationSettings,
|
|
11
|
+
AlignmentPreprocessing,
|
|
11
12
|
AlignmentSettings,
|
|
12
13
|
AlignmentStochastics,
|
|
13
14
|
MatchingSettings,
|
|
@@ -17,6 +18,7 @@ from trajectopy.processing.alignment import (
|
|
|
17
18
|
estimate_alignment,
|
|
18
19
|
manual_alignment,
|
|
19
20
|
)
|
|
21
|
+
from trajectopy.processing.lib.alignment.data import AlignmentData
|
|
20
22
|
from trajectopy.processing.lib.alignment.direct import align_rotations
|
|
21
23
|
|
|
22
24
|
|
|
@@ -141,14 +143,14 @@ class TestAlignment(unittest.TestCase):
|
|
|
141
143
|
angles=[np.random.randint(-360, 360), np.random.randint(-360, 360), np.random.randint(-360, 360)],
|
|
142
144
|
degrees=True,
|
|
143
145
|
)
|
|
144
|
-
trajectory_rotated.rotations =
|
|
146
|
+
trajectory_rotated.rotations = trajectory_rotated.rotations * random_rot
|
|
145
147
|
|
|
146
148
|
sensor_alignment = align_rotations(trajectory_rotated.rotations, trajectory.rotations)
|
|
147
149
|
|
|
148
150
|
trajectory_rotated_back = trajectory_rotated.copy()
|
|
149
151
|
|
|
150
|
-
# apply as in
|
|
151
|
-
trajectory_rotated_back.rotations =
|
|
152
|
+
# apply as in apply_alignment()
|
|
153
|
+
trajectory_rotated_back.rotations = trajectory_rotated_back.rotations * sensor_alignment.rotation_set
|
|
152
154
|
alignment_between_back_and_original = align_rotations(trajectory_rotated_back.rotations, trajectory.rotations)
|
|
153
155
|
|
|
154
156
|
np.testing.assert_allclose(
|
|
@@ -159,23 +161,80 @@ class TestAlignment(unittest.TestCase):
|
|
|
159
161
|
)
|
|
160
162
|
|
|
161
163
|
def test_rotation_alignment(self):
|
|
162
|
-
"""
|
|
164
|
+
"""The sensor rotation is a constant rotation in the body frame, also with changing orientations."""
|
|
163
165
|
rotations_1 = Rotations.from_euler(seq="xyz", angles=np.random.rand(100, 3) * 360, degrees=True)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
sensor_rotation = Rotations.from_euler(seq="xyz", angles=[2.0, -3.0, 5.0], degrees=True)
|
|
167
|
+
rotations_2 = rotations_1 * sensor_rotation
|
|
168
|
+
|
|
169
|
+
sensor_rot_params = align_rotations(rot_from=rotations_1, rot_to=rotations_2)
|
|
170
|
+
|
|
171
|
+
np.testing.assert_allclose(np.rad2deg(sensor_rot_params.rotation), [2.0, -3.0, 5.0], atol=1e-8)
|
|
172
|
+
rotations_1_to_2 = rotations_1 * sensor_rot_params.rotation_set
|
|
173
|
+
np.testing.assert_allclose((rotations_2 - rotations_1_to_2).rotangle, 0, atol=1e-8)
|
|
174
|
+
|
|
175
|
+
def test_sensor_rotation_with_similarity_transformation(self):
|
|
176
|
+
"""A mounting rotation is recovered after the similarity transformation and removed when applied."""
|
|
177
|
+
transformed, _ = transform_randomly(
|
|
178
|
+
open_loop_trajectory, similarity_enabled=True, time_shift_enabled=False, lever_enabled=False
|
|
179
|
+
)
|
|
180
|
+
sensor_rotation = Rotations.from_euler(seq="xyz", angles=[2.0, -3.0, 5.0], degrees=True)
|
|
181
|
+
transformed.rotations = transformed.rotations * sensor_rotation
|
|
182
|
+
|
|
183
|
+
settings = AlignmentSettings(
|
|
184
|
+
estimation_settings=AlignmentEstimationSettings.from_components(similarity=True, sensor_rotation=True)
|
|
185
|
+
)
|
|
186
|
+
result = estimate_alignment(open_loop_trajectory.copy(), transformed, settings, MatchingSettings())
|
|
187
|
+
np.testing.assert_allclose(np.rad2deg(result.rotation_parameters.rotation), [2.0, -3.0, 5.0], atol=1e-3)
|
|
188
|
+
|
|
189
|
+
aligned = apply_alignment(open_loop_trajectory, result, inplace=False)
|
|
190
|
+
np.testing.assert_allclose((transformed.rotations - aligned.rotations).rotangle, 0, atol=1e-5)
|
|
191
|
+
|
|
192
|
+
def test_robust_estimation_against_outliers(self):
|
|
193
|
+
"""Outliers are down-weighted, so that they hardly affect the estimated parameters."""
|
|
194
|
+
transformed, groundtruth = transform_randomly(
|
|
195
|
+
open_loop_trajectory, similarity_enabled=True, time_shift_enabled=False, lever_enabled=False
|
|
171
196
|
)
|
|
197
|
+
rng = np.random.default_rng(1)
|
|
198
|
+
transformed.positions.xyz += rng.normal(0, 0.02, transformed.positions.xyz.shape)
|
|
199
|
+
outliers = rng.choice(len(transformed), len(transformed) // 50, replace=False)
|
|
200
|
+
transformed.positions.xyz[outliers] += rng.normal(0, 3.0, (len(outliers), 3))
|
|
201
|
+
|
|
202
|
+
errors = {}
|
|
203
|
+
for robust in (False, True):
|
|
204
|
+
settings = AlignmentSettings(
|
|
205
|
+
estimation_settings=AlignmentEstimationSettings.from_components(similarity=True),
|
|
206
|
+
stochastics=AlignmentStochastics(robust_estimation=robust),
|
|
207
|
+
preprocessing=AlignmentPreprocessing(max_samples=0),
|
|
208
|
+
)
|
|
209
|
+
result = estimate_alignment(open_loop_trajectory.copy(), transformed, settings, MatchingSettings())
|
|
210
|
+
self.assertTrue(result.converged)
|
|
211
|
+
errors[robust] = np.max(
|
|
212
|
+
np.abs(result.position_parameters.helmert.translation - groundtruth.helmert.translation)
|
|
213
|
+
)
|
|
214
|
+
self.assertLess(errors[True], 0.005)
|
|
215
|
+
self.assertLess(errors[True], errors[False] / 5)
|
|
172
216
|
|
|
173
|
-
|
|
217
|
+
def test_time_window_relative_to_first_common_timestamp(self):
|
|
218
|
+
"""time_start/time_end refer to the first common timestamp, not to the start of the aligned trajectory."""
|
|
219
|
+
reference = open_loop_trajectory.copy()
|
|
220
|
+
first_common = reference.timestamps[0] + 50.0
|
|
221
|
+
reference.mask(reference.timestamps >= first_common)
|
|
222
|
+
settings = AlignmentSettings(preprocessing=AlignmentPreprocessing(time_start=10.0, time_end=20.0))
|
|
174
223
|
|
|
175
|
-
|
|
176
|
-
rpy_trafo_diff = (rotations_2_to_1 - rotations_1).as_euler(seq="xyz", degrees=True)
|
|
224
|
+
data = AlignmentData(open_loop_trajectory.copy(), reference, settings, MatchingSettings())
|
|
177
225
|
|
|
178
|
-
|
|
226
|
+
self.assertGreaterEqual(data.tstamps.min(), first_common + 10.0)
|
|
227
|
+
self.assertLessEqual(data.tstamps.max(), first_common + 20.0)
|
|
228
|
+
|
|
229
|
+
def test_nothing_to_estimate_counts_as_converged(self):
|
|
230
|
+
"""Without least squares parameters (e.g. only the sensor rotation), the result is not flagged."""
|
|
231
|
+
settings = AlignmentSettings(
|
|
232
|
+
estimation_settings=AlignmentEstimationSettings.from_components(similarity=False, sensor_rotation=True)
|
|
233
|
+
)
|
|
234
|
+
result = estimate_alignment(
|
|
235
|
+
open_loop_trajectory.copy(), open_loop_trajectory.copy(), settings, MatchingSettings()
|
|
236
|
+
)
|
|
237
|
+
self.assertTrue(result.converged)
|
|
179
238
|
|
|
180
239
|
def _verify_alignment(
|
|
181
240
|
self, target: AlignmentParameters, estimation: AlignmentParameters, lazy: bool = False
|
|
@@ -72,7 +72,8 @@ class TestComparison(unittest.TestCase):
|
|
|
72
72
|
deviations = compare_trajectories_abs(traj_ref=trajectory, traj_test=transformed)
|
|
73
73
|
|
|
74
74
|
np.testing.assert_almost_equal(deviations.pos_bias_along, parameters.lever_x.value)
|
|
75
|
-
|
|
75
|
+
# the body y axis points to the left, horizontal cross-track is positive to the right
|
|
76
|
+
np.testing.assert_almost_equal(deviations.pos_bias_cross_h, -parameters.lever_y.value)
|
|
76
77
|
np.testing.assert_almost_equal(deviations.pos_bias_cross_v, parameters.lever_z.value)
|
|
77
78
|
|
|
78
79
|
def test_xyz_deviations(self) -> None:
|
|
@@ -91,9 +92,10 @@ class TestComparison(unittest.TestCase):
|
|
|
91
92
|
|
|
92
93
|
deviations = compare_trajectories_abs(traj_ref=trajectory, traj_test=transformed)
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
np.testing.assert_almost_equal(
|
|
96
|
-
np.testing.assert_almost_equal(
|
|
95
|
+
# test minus reference: the test trajectory is shifted by the translation
|
|
96
|
+
np.testing.assert_almost_equal(deviations.pos_bias_x, parameters.sim_trans_x.value)
|
|
97
|
+
np.testing.assert_almost_equal(deviations.pos_bias_y, parameters.sim_trans_y.value)
|
|
98
|
+
np.testing.assert_almost_equal(deviations.pos_bias_z, parameters.sim_trans_z.value)
|
|
97
99
|
|
|
98
100
|
def test_cross_track_deviations_no_rot_no_slope(self) -> None:
|
|
99
101
|
# reference trajectory positions in direction 1,0 without slope
|
|
@@ -118,7 +120,8 @@ class TestComparison(unittest.TestCase):
|
|
|
118
120
|
test_trajectory_xyz = ref_trajectory_xyz + deviations
|
|
119
121
|
dir_devs = _derive_dev_directions_no_rot(xyz_ref=ref_trajectory_xyz, xyz_test=test_trajectory_xyz)
|
|
120
122
|
np.testing.assert_almost_equal(dir_devs[:, 0], deviations[:, 0]) # along-track
|
|
121
|
-
|
|
123
|
+
# travelling along x, y points to the left and horizontal cross-track is positive to the right
|
|
124
|
+
np.testing.assert_almost_equal(dir_devs[:, 1], -deviations[:, 1]) # cross-track horizontal
|
|
122
125
|
np.testing.assert_almost_equal(dir_devs[:, 2], deviations[:, 2]) # cross-track vertical
|
|
123
126
|
|
|
124
127
|
def test_cross_track_deviations_no_rot_slope(self) -> None:
|
|
@@ -147,8 +150,8 @@ class TestComparison(unittest.TestCase):
|
|
|
147
150
|
# with a slope of 45 degrees, the deviations are split equally into vertical and along-track components
|
|
148
151
|
np.testing.assert_almost_equal(dir_devs[:, 0], -dir_devs[:, 2])
|
|
149
152
|
|
|
150
|
-
# horizontal cross-track deviations remain the same
|
|
151
|
-
np.testing.assert_almost_equal(dir_devs[:, 1], deviations[:, 1])
|
|
153
|
+
# horizontal cross-track deviations remain the same (positive to the right, i.e. -y)
|
|
154
|
+
np.testing.assert_almost_equal(dir_devs[:, 1], -deviations[:, 1])
|
|
152
155
|
|
|
153
156
|
# in total, the deviations are the same as the input deviations
|
|
154
157
|
np.testing.assert_almost_equal(np.linalg.norm(dir_devs, axis=1), np.linalg.norm(deviations, axis=1))
|
|
@@ -41,7 +41,7 @@ class TestContextMenus(unittest.TestCase):
|
|
|
41
41
|
def test_reference_entries_explain_why_they_are_unavailable(self):
|
|
42
42
|
self.menu.populate(self._selection(self.trajectory))
|
|
43
43
|
actions = self._actions()
|
|
44
|
-
for text in ("Absolute Error (ATE),
|
|
44
|
+
for text in ("Absolute Error (ATE), with Alignment", "Estimate Alignment", "Interactive Alignment…"):
|
|
45
45
|
self.assertFalse(actions[text].isEnabled(), text)
|
|
46
46
|
self.assertIn("Set a reference first", actions[text].toolTip())
|
|
47
47
|
self.assertTrue(any(text.startswith("ⓘ") for text in actions)) # visible note in the submenu
|
|
@@ -52,6 +52,33 @@ class TestContextMenus(unittest.TestCase):
|
|
|
52
52
|
self.assertTrue(actions["Estimate Alignment"].isEnabled())
|
|
53
53
|
self.assertFalse(any(text.startswith("ⓘ") for text in actions))
|
|
54
54
|
|
|
55
|
+
def test_laps_need_a_spatially_sorted_trajectory(self):
|
|
56
|
+
from trajectopy.utils.definitions import Sorting
|
|
57
|
+
|
|
58
|
+
self.menu.populate(self._selection(self.trajectory))
|
|
59
|
+
self.assertFalse(self._actions()["Divide into Laps"].isEnabled())
|
|
60
|
+
|
|
61
|
+
self.trajectory.trajectory.path_lengths = self.trajectory.trajectory.path_lengths[::-1].copy()
|
|
62
|
+
self.trajectory.trajectory.set_sorting(Sorting.PATH_LENGTH)
|
|
63
|
+
self.menu.populate(self._selection(self.trajectory))
|
|
64
|
+
self.assertTrue(self._actions()["Divide into Laps"].isEnabled())
|
|
65
|
+
|
|
66
|
+
def test_kml_export_of_local_positions_with_a_known_frame(self):
|
|
67
|
+
import numpy as np
|
|
68
|
+
|
|
69
|
+
from trajectopy.core.trajectory import Trajectory
|
|
70
|
+
from trajectopy.gui.models.entries import TrajectoryEntry
|
|
71
|
+
|
|
72
|
+
xyz = np.c_[np.linspace(364000, 364100, 10), np.full(10, 5621000.0), np.full(10, 100.0)]
|
|
73
|
+
trajectory = Trajectory.from_arrays(xyz, epsg=25832)
|
|
74
|
+
trajectory.positions.to_local() # EPSG 0, but the frame is known
|
|
75
|
+
self.menu.populate(self._selection(TrajectoryEntry(full_filename="", trajectory=trajectory)))
|
|
76
|
+
self.assertTrue(self._actions()["Export to Google Earth (KML)…"].isEnabled())
|
|
77
|
+
|
|
78
|
+
unknown = Trajectory.from_arrays(xyz) # EPSG 0 without a frame
|
|
79
|
+
self.menu.populate(self._selection(TrajectoryEntry(full_filename="", trajectory=unknown)))
|
|
80
|
+
self.assertFalse(self._actions()["Export to Google Earth (KML)…"].isEnabled())
|
|
81
|
+
|
|
55
82
|
def test_selection_size(self):
|
|
56
83
|
self.menu.populate(self._selection(self.trajectory))
|
|
57
84
|
actions = self._actions()
|
|
@@ -116,6 +143,40 @@ class TestContextMenus(unittest.TestCase):
|
|
|
116
143
|
self.assertTrue(actions["Plot"].isEnabled()) # deviations are plotted together
|
|
117
144
|
self.assertFalse(actions["Export…"].isEnabled())
|
|
118
145
|
self.assertIn("single result", actions["Export…"].toolTip())
|
|
146
|
+
self.assertFalse(actions["Switch Order: Time ↔ Path"].isEnabled()) # chronological trajectory
|
|
147
|
+
self.assertIn("spatially sorted", actions["Switch Order: Time ↔ Path"].toolTip())
|
|
148
|
+
|
|
149
|
+
def test_switch_order_of_spatially_sorted_ate_result(self):
|
|
150
|
+
from trajectopy.gui.managers.requests import ResultModelRequest, ResultModelRequestType
|
|
151
|
+
from trajectopy.gui.models.entries import AbsoluteDeviationEntry
|
|
152
|
+
from trajectopy.gui.models.result_model import ResultTableModel
|
|
153
|
+
from trajectopy.gui.models.selection import ResultSelection
|
|
154
|
+
from trajectopy.gui.views.menu_helpers import iter_actions
|
|
155
|
+
from trajectopy.gui.views.result_context_menu import ResultContextMenu
|
|
156
|
+
from trajectopy.processing import evaluation
|
|
157
|
+
from trajectopy.processing.sorting import sort_spatially
|
|
158
|
+
from trajectopy.utils.definitions import Sorting
|
|
159
|
+
|
|
160
|
+
sorted_trajectory = sort_spatially(self.trajectory.trajectory, inplace=False)
|
|
161
|
+
entry = AbsoluteDeviationEntry(
|
|
162
|
+
deviations=evaluation.ate(trajectory=sorted_trajectory, other=sorted_trajectory, align=False)
|
|
163
|
+
)
|
|
164
|
+
selection = ResultSelection(entries=[entry])
|
|
165
|
+
menu = ResultContextMenu()
|
|
166
|
+
received = []
|
|
167
|
+
menu.result_model_request.connect(received.append)
|
|
168
|
+
menu.populate(selection)
|
|
169
|
+
action = {action.text(): action for action in iter_actions(menu)}["Switch Order: Time ↔ Path"]
|
|
170
|
+
self.assertTrue(action.isEnabled())
|
|
171
|
+
action.trigger()
|
|
172
|
+
self.assertEqual([request.type for request in received], [ResultModelRequestType.SWITCH_SORTING])
|
|
173
|
+
|
|
174
|
+
model = ResultTableModel()
|
|
175
|
+
model.add_entries(ResultModelRequest(type=ResultModelRequestType.ADD, selection=selection))
|
|
176
|
+
model.handle_request(received[0])
|
|
177
|
+
self.assertEqual(entry.deviations.trajectory.sorting, Sorting.TIME)
|
|
178
|
+
model.handle_request(ResultModelRequest(type=ResultModelRequestType.SWITCH_SORTING, selection=selection))
|
|
179
|
+
self.assertEqual(entry.deviations.trajectory.sorting, Sorting.PATH_LENGTH)
|
|
119
180
|
|
|
120
181
|
|
|
121
182
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Temporal correlation of deviations: known correlation times are found, gaps and irregular sampling are handled."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from trajectopy.core.trajectory import Trajectory
|
|
8
|
+
from trajectopy.processing.evaluation import ate
|
|
9
|
+
from trajectopy.utils.definitions import Sorting
|
|
10
|
+
from trajectopy.utils.statistics import (
|
|
11
|
+
autocorrelation,
|
|
12
|
+
correlation_time,
|
|
13
|
+
effective_samples,
|
|
14
|
+
white_noise_split,
|
|
15
|
+
windowed_std,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _gauss_markov(timestamps: np.ndarray, tau: float, rng: np.random.Generator) -> np.ndarray:
|
|
20
|
+
"""First-order Gauss-Markov process (exponential autocorrelation with the correlation time tau)."""
|
|
21
|
+
values = np.empty(len(timestamps))
|
|
22
|
+
values[0] = rng.normal()
|
|
23
|
+
for i in range(1, len(timestamps)):
|
|
24
|
+
factor = np.exp(-(timestamps[i] - timestamps[i - 1]) / tau)
|
|
25
|
+
values[i] = factor * values[i - 1] + rng.normal(0, np.sqrt(1 - factor**2))
|
|
26
|
+
return values
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestAutocorrelation(unittest.TestCase):
|
|
30
|
+
def test_correlation_time_of_a_gauss_markov_process(self):
|
|
31
|
+
rng = np.random.default_rng(0)
|
|
32
|
+
timestamps = np.arange(0, 3600, 1 / 19) # one hour at 19 Hz
|
|
33
|
+
for tau in (0.5, 5.0):
|
|
34
|
+
with self.subTest(tau=tau):
|
|
35
|
+
lags, rho, _ = autocorrelation(timestamps, _gauss_markov(timestamps, tau, rng), max_lag=60)
|
|
36
|
+
self.assertAlmostEqual(correlation_time(lags, rho), tau, delta=0.15 * tau)
|
|
37
|
+
|
|
38
|
+
def test_gaps_and_jitter(self):
|
|
39
|
+
rng = np.random.default_rng(1)
|
|
40
|
+
timestamps = np.arange(0, 3600, 1 / 19) + rng.normal(0, 0.002, 68400)
|
|
41
|
+
values = _gauss_markov(np.sort(timestamps), 5.0, rng)
|
|
42
|
+
keep = np.ones(len(timestamps), dtype=bool)
|
|
43
|
+
for start in rng.integers(0, len(timestamps), 20):
|
|
44
|
+
keep[start : start + 300] = False # gaps of about 16 s, e.g. losses of lock
|
|
45
|
+
|
|
46
|
+
lags, rho, pairs = autocorrelation(np.sort(timestamps)[keep], values[keep], max_lag=60)
|
|
47
|
+
|
|
48
|
+
self.assertAlmostEqual(correlation_time(lags, rho), 5.0, delta=0.75)
|
|
49
|
+
self.assertTrue(np.all(pairs[1:] < pairs[0]))
|
|
50
|
+
|
|
51
|
+
def test_no_correlation_across_a_gap(self):
|
|
52
|
+
# 10 s blocks with alternating signs, separated by 20 s gaps: within a block, the values are perfectly
|
|
53
|
+
# correlated; empty grid cells of the gaps must not produce pairs
|
|
54
|
+
timestamps = np.concatenate([np.arange(0, 10, 0.1) + 30 * block for block in range(40)])
|
|
55
|
+
values = np.concatenate([np.full(100, (-1.0) ** block) for block in range(40)])
|
|
56
|
+
|
|
57
|
+
lags, rho, pairs = autocorrelation(timestamps, values)
|
|
58
|
+
|
|
59
|
+
np.testing.assert_allclose(rho[(lags > 0) & (lags < 9.95)], 1.0)
|
|
60
|
+
self.assertTrue(np.all(pairs[(lags > 10.05) & (lags < 19.95)] == 0)) # only gaps at these lags
|
|
61
|
+
self.assertTrue(np.all(rho[(lags > 20.05) & (lags < 29.95)] < 0)) # neighboring blocks, opposite signs
|
|
62
|
+
|
|
63
|
+
def test_lags_up_to_a_tenth_of_the_samples(self):
|
|
64
|
+
timestamps = np.arange(1000) * 0.1
|
|
65
|
+
lags, _, _ = autocorrelation(timestamps, np.random.default_rng(6).normal(size=1000), max_lag=600)
|
|
66
|
+
self.assertAlmostEqual(lags[-1], 10.0) # 100 sampling intervals
|
|
67
|
+
|
|
68
|
+
def test_correlation_time_beyond_the_largest_lag(self):
|
|
69
|
+
timestamps = np.arange(1000) * 0.1
|
|
70
|
+
lags, rho, _ = autocorrelation(timestamps, np.linspace(-1, 1, 1000)) # a trend: correlated throughout
|
|
71
|
+
self.assertEqual(correlation_time(lags, rho), float("inf"))
|
|
72
|
+
|
|
73
|
+
def test_white_noise_and_correlated_part(self):
|
|
74
|
+
rng = np.random.default_rng(7)
|
|
75
|
+
timestamps = np.arange(0, 3600, 1 / 19)
|
|
76
|
+
correlated = _gauss_markov(timestamps, 5.0, rng)
|
|
77
|
+
white = rng.normal(size=len(timestamps))
|
|
78
|
+
for white_share in (0.0, 0.7):
|
|
79
|
+
with self.subTest(white_share=white_share):
|
|
80
|
+
values = np.sqrt(1 - white_share) * correlated + np.sqrt(white_share) * white
|
|
81
|
+
lags, rho, pairs = autocorrelation(timestamps, values, max_lag=60)
|
|
82
|
+
share = white_noise_split(lags, rho, pairs)
|
|
83
|
+
|
|
84
|
+
self.assertAlmostEqual(1 - share, white_share, delta=0.05)
|
|
85
|
+
self.assertAlmostEqual(correlation_time(lags, rho, level=share), 5.0, delta=0.75)
|
|
86
|
+
|
|
87
|
+
lags, rho, pairs = autocorrelation(timestamps, white, max_lag=60)
|
|
88
|
+
self.assertEqual(white_noise_split(lags, rho, pairs), 0.0)
|
|
89
|
+
|
|
90
|
+
def test_white_noise_is_not_correlated(self):
|
|
91
|
+
rng = np.random.default_rng(2)
|
|
92
|
+
timestamps = np.arange(5000) * 0.01
|
|
93
|
+
lags, rho, _ = autocorrelation(timestamps, rng.normal(size=5000))
|
|
94
|
+
self.assertLess(correlation_time(lags, rho), 0.01)
|
|
95
|
+
|
|
96
|
+
def test_effective_samples(self):
|
|
97
|
+
self.assertEqual(effective_samples(1000, 100.0, 5.0), 10.0)
|
|
98
|
+
self.assertEqual(effective_samples(1000, 100.0, 0.0), 1000.0)
|
|
99
|
+
self.assertEqual(effective_samples(1000, 100.0, np.inf), 1.0)
|
|
100
|
+
|
|
101
|
+
def test_windowed_std(self):
|
|
102
|
+
timestamps = np.arange(0, 20, 0.1)
|
|
103
|
+
values = np.r_[np.random.default_rng(3).normal(0, 1, 100), np.random.default_rng(4).normal(0, 3, 100)]
|
|
104
|
+
centers, stds = windowed_std(timestamps, values, window=10.0)
|
|
105
|
+
np.testing.assert_allclose(centers, [5.0, 15.0])
|
|
106
|
+
self.assertLess(stds[0], stds[1])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestDeviationCorrelation(unittest.TestCase):
|
|
110
|
+
def _result(self, sorting: Sorting = Sorting.TIME):
|
|
111
|
+
rng = np.random.default_rng(5)
|
|
112
|
+
t = np.arange(0, 600, 0.1)
|
|
113
|
+
reference = Trajectory.from_arrays(np.c_[t, 0 * t, 0 * t], rpy=np.zeros((len(t), 3)), timestamps=t)
|
|
114
|
+
test = reference.copy()
|
|
115
|
+
# cross-track deviations correlated over 5 s, along-track white noise
|
|
116
|
+
test.positions.xyz += np.c_[rng.normal(0, 0.01, len(t)), _gauss_markov(t, 5.0, rng) * 0.02, 0 * t]
|
|
117
|
+
result = ate(test, reference, align=False)
|
|
118
|
+
if sorting == Sorting.PATH_LENGTH:
|
|
119
|
+
result.trajectory.path_lengths = result.trajectory.path_lengths[::-1].copy()
|
|
120
|
+
result.set_sorting(Sorting.PATH_LENGTH)
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
def test_components_and_properties(self):
|
|
124
|
+
result = self._result()
|
|
125
|
+
correlation = result.correlation
|
|
126
|
+
|
|
127
|
+
self.assertAlmostEqual(correlation.components["cross_h"].correlation_time, 5.0, delta=1.5)
|
|
128
|
+
self.assertEqual(correlation.components["along"].correlation_time, 0.0) # white noise
|
|
129
|
+
self.assertGreater(correlation.components["along"].white_noise_share, 0.9)
|
|
130
|
+
self.assertLess(correlation.components["cross_h"].white_noise_share, 0.1)
|
|
131
|
+
self.assertLess(correlation.components["cross_h"].effective_samples, 100)
|
|
132
|
+
self.assertIn("roll", correlation.components)
|
|
133
|
+
self.assertIn("Correlation Time Along / Cross-H / Cross-V [s]", result.property_dict)
|
|
134
|
+
self.assertIn("White Noise Along / Cross-H / Cross-V [%]", result.property_dict)
|
|
135
|
+
|
|
136
|
+
def test_spatially_sorted_results_are_analyzed_in_chronological_order(self):
|
|
137
|
+
by_time, by_path = self._result(), self._result(Sorting.PATH_LENGTH)
|
|
138
|
+
self.assertAlmostEqual(
|
|
139
|
+
by_time.correlation.components["cross_h"].correlation_time,
|
|
140
|
+
by_path.correlation.components["cross_h"].correlation_time,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def test_removing_deviations_updates_the_correlation(self):
|
|
144
|
+
result = self._result()
|
|
145
|
+
samples = result.correlation.num_samples
|
|
146
|
+
result.remove_ate_above(0.02)
|
|
147
|
+
self.assertLess(result.correlation.num_samples, samples)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
unittest.main()
|
|
@@ -136,6 +136,107 @@ class TestSubWindows(unittest.TestCase):
|
|
|
136
136
|
self._process_deferred_deletes()
|
|
137
137
|
self.assertEqual(self.main_window.findChildren(QtWidgets.QMainWindow), [])
|
|
138
138
|
|
|
139
|
+
def _visible_windows(self) -> list:
|
|
140
|
+
return [window for window in self.main_window.findChildren(QtWidgets.QMainWindow) if window.isVisible()]
|
|
141
|
+
|
|
142
|
+
def _close_all(self) -> None:
|
|
143
|
+
for window in self.main_window.findChildren(QtWidgets.QMainWindow):
|
|
144
|
+
window.close()
|
|
145
|
+
self._process_deferred_deletes()
|
|
146
|
+
|
|
147
|
+
def test_settings_of_a_trajectory_open_only_once(self):
|
|
148
|
+
manager = UIManager(parent=self.main_window)
|
|
149
|
+
request = UIRequest(type=UIRequestType.TRAJ_SETTINGS, trajectory_selection=self.selection)
|
|
150
|
+
manager.handle_request(request)
|
|
151
|
+
first = self._visible_windows()
|
|
152
|
+
manager.handle_request(request)
|
|
153
|
+
|
|
154
|
+
self.assertEqual(self._visible_windows(), first)
|
|
155
|
+
self.assertEqual(len(first), 1)
|
|
156
|
+
|
|
157
|
+
# once closed, it opens again
|
|
158
|
+
self._close_all()
|
|
159
|
+
manager.handle_request(request)
|
|
160
|
+
self.assertEqual(len(self._visible_windows()), 1)
|
|
161
|
+
self._close_all()
|
|
162
|
+
|
|
163
|
+
def test_a_window_for_the_current_selection_replaces_the_open_one(self):
|
|
164
|
+
manager = UIManager(parent=self.main_window)
|
|
165
|
+
request = UIRequest(type=UIRequestType.DOF_ORGANIZER, trajectory_selection=self.selection)
|
|
166
|
+
manager.handle_request(request)
|
|
167
|
+
first = self._visible_windows()
|
|
168
|
+
manager.handle_request(request)
|
|
169
|
+
second = self._visible_windows()
|
|
170
|
+
|
|
171
|
+
self.assertEqual(len(second), 1)
|
|
172
|
+
self.assertIsNot(second[0], first[0])
|
|
173
|
+
self._close_all()
|
|
174
|
+
|
|
175
|
+
def test_applying_settings_keeps_values_outside_of_the_usual_range(self):
|
|
176
|
+
from trajectopy.core.settings import ProcessingSettings
|
|
177
|
+
from trajectopy.gui.views.json_settings_view import JSONViewer
|
|
178
|
+
|
|
179
|
+
settings = ProcessingSettings()
|
|
180
|
+
settings.alignment.preprocessing.max_samples = 250_000
|
|
181
|
+
settings.alignment.preprocessing.time_start = -2.5
|
|
182
|
+
viewer = JSONViewer(parent=self.main_window, settings=settings)
|
|
183
|
+
viewer.apply_changes()
|
|
184
|
+
viewer.close()
|
|
185
|
+
self._process_deferred_deletes()
|
|
186
|
+
|
|
187
|
+
self.assertEqual(settings.alignment.preprocessing.max_samples, 250_000)
|
|
188
|
+
self.assertEqual(settings.alignment.preprocessing.time_start, -2.5)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@unittest.skipIf(QtWidgets is None, "GUI dependencies not installed")
|
|
192
|
+
class TestUniqueIds(unittest.TestCase):
|
|
193
|
+
@classmethod
|
|
194
|
+
def setUpClass(cls) -> None:
|
|
195
|
+
cls.app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
|
|
196
|
+
|
|
197
|
+
def test_a_result_file_opened_twice_gives_two_independent_entries(self):
|
|
198
|
+
import tempfile
|
|
199
|
+
|
|
200
|
+
from trajectopy.gui.models.entries import AbsoluteDeviationEntry
|
|
201
|
+
from trajectopy.gui.models.result_model import ResultTableModel
|
|
202
|
+
from trajectopy.processing.evaluation import ate
|
|
203
|
+
|
|
204
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
205
|
+
filename = os.path.join(directory, "ate.result")
|
|
206
|
+
AbsoluteDeviationEntry(deviations=ate(noisy_trajectory, noisy_trajectory, align=False)).to_file(filename)
|
|
207
|
+
first, second = AbsoluteDeviationEntry.from_file(filename), AbsoluteDeviationEntry.from_file(filename)
|
|
208
|
+
|
|
209
|
+
model = ResultTableModel()
|
|
210
|
+
model.add(first)
|
|
211
|
+
model.add(second)
|
|
212
|
+
model.remove([second.entry_id])
|
|
213
|
+
|
|
214
|
+
self.assertEqual(model.items, [first])
|
|
215
|
+
|
|
216
|
+
def test_operation_results_keep_the_current_reference(self):
|
|
217
|
+
import copy
|
|
218
|
+
|
|
219
|
+
from trajectopy.gui.managers.requests import TrajectoryModelRequest, TrajectoryModelRequestType
|
|
220
|
+
|
|
221
|
+
model = TrajectoryTableModel()
|
|
222
|
+
first, second = _entry("first"), _entry("second")
|
|
223
|
+
first.set_as_reference = True
|
|
224
|
+
model.add(first)
|
|
225
|
+
model.add(second)
|
|
226
|
+
result = copy.deepcopy(first) # e.g. an EPSG change, computed while the reference is changed
|
|
227
|
+
model.handle_request(
|
|
228
|
+
TrajectoryModelRequest(
|
|
229
|
+
type=TrajectoryModelRequestType.SET_REFERENCE, selection=TrajectorySelection(entries=[second])
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
model.handle_request(
|
|
233
|
+
TrajectoryModelRequest(
|
|
234
|
+
type=TrajectoryModelRequestType.UPDATE, selection=TrajectorySelection(entries=[result])
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
self.assertEqual([entry.set_as_reference for entry in model.items], [False, True])
|
|
239
|
+
|
|
139
240
|
|
|
140
241
|
if __name__ == "__main__":
|
|
141
242
|
unittest.main()
|