trajectopy 5.1.4__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.
Files changed (220) hide show
  1. {trajectopy-5.1.4 → trajectopy-6.0.0}/.pre-commit-config.yaml +32 -23
  2. {trajectopy-5.1.4 → trajectopy-6.0.0}/PKG-INFO +1 -1
  3. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_alignment.py +74 -15
  4. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_comparison.py +10 -7
  5. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_context_menus.py +62 -1
  6. trajectopy-6.0.0/test/test_correlation.py +151 -0
  7. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_gui_models.py +101 -0
  8. trajectopy-6.0.0/test/test_gui_operations.py +223 -0
  9. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_gui_smoke.py +15 -0
  10. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_io.py +7 -1
  11. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_manual_alignment.py +15 -0
  12. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_matching.py +128 -1
  13. trajectopy-6.0.0/test/test_project.py +297 -0
  14. trajectopy-6.0.0/test/test_regressions.py +611 -0
  15. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_visualization.py +302 -251
  16. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/util.py +218 -218
  17. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/__init__.py +172 -170
  18. trajectopy-6.0.0/trajectopy/__version__.py +1 -0
  19. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/cli/main.py +11 -10
  20. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/parameters.py +10 -3
  21. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/positions.py +508 -460
  22. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/rotations.py +2 -1
  23. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/settings.py +4 -0
  24. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/trajectory.py +71 -29
  25. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/file_manager.py +34 -15
  26. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/requests.py +2 -1
  27. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/trajectory_manager.py +68 -12
  28. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/ui_manager.py +55 -35
  29. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/entries.py +74 -24
  30. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/result_model.py +14 -1
  31. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/table_model.py +3 -0
  32. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/trajectory_model.py +8 -0
  33. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/project.py +80 -71
  34. trajectopy-6.0.0/trajectopy/gui/project_format.py +254 -0
  35. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/utils.py +56 -0
  36. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/dof_organizer.py +248 -248
  37. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/json_settings_view.py +3 -2
  38. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/main_window.py +26 -5
  39. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/manual_alignment_window.py +32 -16
  40. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/playback_window.py +11 -3
  41. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/preview_plot.py +37 -11
  42. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/preview_plot_3d.py +160 -162
  43. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/result_context_menu.py +16 -0
  44. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/timeline_widget.py +1 -1
  45. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/trajectory_context_menu.py +14 -9
  46. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/ascii.py +1 -1
  47. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/header.py +279 -269
  48. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/kml.py +448 -447
  49. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/results_io.py +261 -204
  50. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/trajectory_io.py +31 -4
  51. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/alignment.py +8 -7
  52. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/approximation.py +17 -1
  53. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/evaluation.py +76 -61
  54. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/interpolation.py +52 -15
  55. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/data.py +38 -24
  56. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/direct.py +6 -2
  57. trajectopy-6.0.0/trajectopy/processing/lib/alignment/egrad_interface.py +103 -0
  58. trajectopy-6.0.0/trajectopy/processing/lib/alignment/estimation.py +609 -0
  59. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/cubic_approximation.py +2 -3
  60. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/voxelizer.py +5 -1
  61. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/matching.py +433 -354
  62. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/merging.py +10 -13
  63. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/sorting.py +6 -4
  64. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/results/ate_result.py +593 -527
  65. trajectopy-6.0.0/trajectopy/results/correlation.py +157 -0
  66. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/results/rpe_result.py +24 -24
  67. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/colors.py +2 -1
  68. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/formatting.py +144 -144
  69. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/linalg.py +1 -1
  70. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/numeric.py +5 -3
  71. trajectopy-6.0.0/trajectopy/utils/statistics.py +172 -0
  72. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/__init__.py +2 -0
  73. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/figure.py +54 -3
  74. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/plots.py +931 -843
  75. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/file_formats.md +5 -3
  76. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/settings.md +204 -203
  77. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/user_guide.md +425 -388
  78. trajectopy-5.1.4/test/test_project.py +0 -157
  79. trajectopy-5.1.4/test/test_regressions.py +0 -60
  80. trajectopy-5.1.4/trajectopy/__version__.py +0 -1
  81. trajectopy-5.1.4/trajectopy/processing/lib/alignment/egrad_interface.py +0 -155
  82. trajectopy-5.1.4/trajectopy/processing/lib/alignment/estimation.py +0 -803
  83. {trajectopy-5.1.4 → trajectopy-6.0.0}/.gitattributes +0 -0
  84. {trajectopy-5.1.4 → trajectopy-6.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  85. {trajectopy-5.1.4 → trajectopy-6.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  86. {trajectopy-5.1.4 → trajectopy-6.0.0}/.github/workflows/ci.yml +0 -0
  87. {trajectopy-5.1.4 → trajectopy-6.0.0}/.github/workflows/release.yml +0 -0
  88. {trajectopy-5.1.4 → trajectopy-6.0.0}/.gitignore +0 -0
  89. {trajectopy-5.1.4 → trajectopy-6.0.0}/.pylintrc +0 -0
  90. {trajectopy-5.1.4 → trajectopy-6.0.0}/CITATION.cff +0 -0
  91. {trajectopy-5.1.4 → trajectopy-6.0.0}/LICENSE +0 -0
  92. {trajectopy-5.1.4 → trajectopy-6.0.0}/README.md +0 -0
  93. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_data/KITTI_ORB.traj +0 -0
  94. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_data/KITTI_SPTAM.traj +0 -0
  95. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_data/KITTI_gt.traj +0 -0
  96. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/align.py +0 -0
  97. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/alignment.py +0 -0
  98. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/ate.py +0 -0
  99. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/deviation_plots.py +0 -0
  100. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/rpe.py +0 -0
  101. {trajectopy-5.1.4 → trajectopy-6.0.0}/example_scripts/trajectory_plot.py +0 -0
  102. {trajectopy-5.1.4 → trajectopy-6.0.0}/installer/trajectopy.iss +0 -0
  103. {trajectopy-5.1.4 → trajectopy-6.0.0}/pyproject.toml +0 -0
  104. {trajectopy-5.1.4 → trajectopy-6.0.0}/requirements.txt +0 -0
  105. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/__init__.py +0 -0
  106. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/abs_dev.result +0 -0
  107. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/align_entry.result +0 -0
  108. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/generated_trajectory.traj +0 -0
  109. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/noisy_trajectory.traj +0 -0
  110. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/open_loop_trajectory.traj +0 -0
  111. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/rel_dev_meter.result +0 -0
  112. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/data/rel_dev_second.result +0 -0
  113. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_approximation.py +0 -0
  114. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_architecture.py +0 -0
  115. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_fast_reading.py +0 -0
  116. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_inplace_contract.py +0 -0
  117. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_kml.py +0 -0
  118. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_merging.py +0 -0
  119. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_pipeline.py +0 -0
  120. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_positions_rotations.py +0 -0
  121. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_processing_edge_cases.py +0 -0
  122. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_processing_errors.py +0 -0
  123. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_readers_edge_cases.py +0 -0
  124. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_readers_errors.py +0 -0
  125. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_settings.py +0 -0
  126. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_settings_edge_cases.py +0 -0
  127. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_settings_serialization.py +0 -0
  128. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_settings_validation.py +0 -0
  129. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_sorting.py +0 -0
  130. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_trajectory.py +0 -0
  131. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_trajectory_validation.py +0 -0
  132. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/test_update_check.py +0 -0
  133. {trajectopy-5.1.4 → trajectopy-6.0.0}/test/testdata.py +0 -0
  134. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/LICENSE +0 -0
  135. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/__main__.py +0 -0
  136. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/cli/__init__.py +0 -0
  137. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/core/__init__.py +0 -0
  138. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/exceptions.py +0 -0
  139. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/__init__.py +0 -0
  140. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/app.py +0 -0
  141. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/__init__.py +0 -0
  142. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/plot_manager.py +0 -0
  143. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/managers/project_manager.py +0 -0
  144. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/__init__.py +0 -0
  145. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/property_model.py +0 -0
  146. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/models/selection.py +0 -0
  147. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/resources/full-icon-poppins.png +0 -0
  148. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/resources/icon-bg.ico +0 -0
  149. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/resources/icon-bg.png +0 -0
  150. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/resources/icon.png +0 -0
  151. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/themes.py +0 -0
  152. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/update_check.py +0 -0
  153. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/__init__.py +0 -0
  154. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/about_window.py +0 -0
  155. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/alignment_edit_window.py +0 -0
  156. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/entry_table_view.py +0 -0
  157. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/kml_dialog.py +0 -0
  158. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/menu_helpers.py +0 -0
  159. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/progress_window.py +0 -0
  160. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/properties_panel.py +0 -0
  161. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/result_selection_window.py +0 -0
  162. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/result_table_view.py +0 -0
  163. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/views/trajectory_table_view.py +0 -0
  164. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/gui/worker.py +0 -0
  165. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/__init__.py +0 -0
  166. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/rosbag.py +0 -0
  167. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/rosmsg.py +0 -0
  168. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/io/utils.py +0 -0
  169. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/__init__.py +0 -0
  170. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/__init__.py +0 -0
  171. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/__init__.py +0 -0
  172. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/equations.py +0 -0
  173. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/alignment/parameters.py +0 -0
  174. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/__init__.py +0 -0
  175. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/processing/lib/approximation/line_approximation.py +0 -0
  176. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/py.typed +0 -0
  177. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/readers/__init__.py +0 -0
  178. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/readers/ascii.py +0 -0
  179. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/readers/rosbag.py +0 -0
  180. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/readers/rosmsg.py +0 -0
  181. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/readers/utils.py +0 -0
  182. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/results/__init__.py +0 -0
  183. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/results/alignment_result.py +0 -0
  184. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/__init__.py +0 -0
  185. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/common.py +0 -0
  186. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/definitions.py +0 -0
  187. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/geometry.py +0 -0
  188. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/utils/resources.py +0 -0
  189. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/_svg_export.py +0 -0
  190. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/axes.py +0 -0
  191. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy/visualization/style.py +0 -0
  192. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/parameters.md +0 -0
  193. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/positions.md +0 -0
  194. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/rotations.md +0 -0
  195. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/settings.md +0 -0
  196. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/core/trajectory.md +0 -0
  197. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/index.md +0 -0
  198. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/kml.md +0 -0
  199. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/results_io.md +0 -0
  200. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/rosbag.md +0 -0
  201. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/io/trajectory_io.md +0 -0
  202. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/alignment.md +0 -0
  203. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/approximation.md +0 -0
  204. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/evaluation.md +0 -0
  205. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/interpolation.md +0 -0
  206. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/matching.md +0 -0
  207. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/merging.md +0 -0
  208. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/processing/sorting.md +0 -0
  209. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/alignment_result.md +0 -0
  210. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/ate_result.md +0 -0
  211. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/results/rpe_result.md +0 -0
  212. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/visualization/display.md +0 -0
  213. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/api/visualization/figures.md +0 -0
  214. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/assets/icon.png +0 -0
  215. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/index.md +0 -0
  216. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/javascripts/mathjax.js +0 -0
  217. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/docs/stylesheets/extra.css +0 -0
  218. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy-docs/mkdocs.yml +0 -0
  219. {trajectopy-5.1.4 → trajectopy-6.0.0}/trajectopy.spec +0 -0
  220. {trajectopy-5.1.4 → 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
- - id: pytest
18
- name: Run unit tests
19
- entry: uv run pytest -q
20
- language: system
21
- pass_filenames: false
22
- stages: [pre-commit]
23
- always_run: true
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: trajectopy
3
- Version: 5.1.4
3
+ Version: 6.0.0
4
4
  Summary: Trajectory Evaluation in Python
5
5
  Project-URL: Homepage, https://gereon-t.github.io/trajectopy/
6
6
  Project-URL: Documentation, https://gereon-t.github.io/trajectopy/
@@ -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 = random_rot * 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 Trajectory.apply_alignment()
151
- trajectory_rotated_back.rotations = sensor_alignment.rotation_set * 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
- """Test alignment of rotation sets and verification of transformation accuracy."""
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
- rotations_2 = (
165
- Rotations.from_euler(
166
- seq="xyz",
167
- angles=[np.random.randint(-360, 360), np.random.randint(-360, 360), np.random.randint(-360, 360)],
168
- degrees=True,
169
- )
170
- * rotations_1
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
- sensor_rot_params = align_rotations(rot_from=rotations_2, rot_to=rotations_1)
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
- rotations_2_to_1 = sensor_rot_params.rotation_set * rotations_2
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
- np.testing.assert_allclose(rpy_trafo_diff, np.zeros_like(rpy_trafo_diff), atol=1e-8, rtol=1e-8)
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
- np.testing.assert_almost_equal(deviations.pos_bias_cross_h, parameters.lever_y.value)
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
- np.testing.assert_almost_equal(-deviations.pos_bias_x, parameters.sim_trans_x.value)
95
- np.testing.assert_almost_equal(-deviations.pos_bias_y, parameters.sim_trans_y.value)
96
- np.testing.assert_almost_equal(-deviations.pos_bias_z, parameters.sim_trans_z.value)
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
- np.testing.assert_almost_equal(dir_devs[:, 1], deviations[:, 1]) # cross-track horizontal
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), Aligned First", "Estimate Alignment", "Interactive Alignment…"):
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()