pyetsimul 3.2.4__tar.gz → 3.3.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 (206) hide show
  1. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/PKG-INFO +5 -6
  2. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/README.md +3 -4
  3. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/conf.py +2 -1
  4. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/index.rst +3 -3
  5. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/custom_gaze_model.md +8 -8
  6. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/README.md +4 -1
  7. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/data_validation.py +1 -1
  8. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/eye_tracker.py +5 -2
  9. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/stampe1993/stampe1993_gaze_model.py +60 -8
  10. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/composed_variation.py +1 -2
  11. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/types/geometry.py +14 -12
  12. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyproject.toml +13 -11
  13. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_angle2gaze.py +2 -1
  14. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_find_cr.py +2 -1
  15. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_find_cr_simple.py +4 -3
  16. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_geometry_types_arithmetic.py +2 -2
  17. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_glint_noise.py +10 -10
  18. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_spherical_cornea_scaling.py +9 -9
  19. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/uv.lock +28 -1
  20. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/.github/workflows/publish.yml +0 -0
  21. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/.gitignore +0 -0
  22. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/.readthedocs.yaml +0 -0
  23. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/LICENSE +0 -0
  24. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/_static/images/eye_model_schematic.png +0 -0
  25. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/_static/images/eye_tracking_setup.png +0 -0
  26. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/core.rst +0 -0
  27. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/evaluation.rst +0 -0
  28. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/gaze_mapping.rst +0 -0
  29. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/geometry.rst +0 -0
  30. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/index.rst +0 -0
  31. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/optics.rst +0 -0
  32. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/simulation.rst +0 -0
  33. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/types.rst +0 -0
  34. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/api/visualization.rst +0 -0
  35. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/getting_started.rst +0 -0
  36. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/guides/custom_gaze_model.md +0 -0
  37. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/guides/custom_variations.md +0 -0
  38. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/camera_and_lights.rst +0 -0
  39. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/coordinate_systems.rst +0 -0
  40. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/default_parameters.rst +0 -0
  41. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/evaluation_workflow.rst +0 -0
  42. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/eye_model.rst +0 -0
  43. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/gaze_estimation_models.rst +0 -0
  44. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/docs/theory/optical_calculations.rst +0 -0
  45. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/__init__.py +0 -0
  46. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/basic_usage_demo.py +0 -0
  47. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/camera_distortion.py +0 -0
  48. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/cornea_comparison_figure.py +0 -0
  49. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/custom_gaze_model.py +0 -0
  50. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/custom_polynomial_example.py +0 -0
  51. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/data_generation_tutorial.ipynb +0 -0
  52. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/example.py +0 -0
  53. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/__init__.py +0 -0
  54. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/algorithm_comparison.py +0 -0
  55. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/config.py +0 -0
  56. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/data_generation.py +0 -0
  57. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/homography_normalization_demo.py +0 -0
  58. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/experiments/polynomial_demo.py +0 -0
  59. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/eye_anatomy.py +0 -0
  60. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/eyelid_openness_comparison.py +0 -0
  61. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/eyelink1000plus.py +0 -0
  62. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/glint_noise_demo.py +0 -0
  63. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/multi_eye_tracking_setup.py +0 -0
  64. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/polynomial_multi_glint.py +0 -0
  65. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/pupil_decentration.py +0 -0
  66. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/realistic_pupil.py +0 -0
  67. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/realistic_pupil_comparison.py +0 -0
  68. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/run_all_examples.py +0 -0
  69. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/setup_visualization.py +0 -0
  70. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/simple_data_generation.py +0 -0
  71. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/examples/stampe1993.py +0 -0
  72. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/__init__.py +0 -0
  73. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/camera_noise/__init__.py +0 -0
  74. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/camera_noise/glint_noise.py +0 -0
  75. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/__init__.py +0 -0
  76. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/camera.py +0 -0
  77. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/cornea.py +0 -0
  78. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/default_configs.py +0 -0
  79. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/eye.py +0 -0
  80. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/eye_operations.py +0 -0
  81. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/eyelid.py +0 -0
  82. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/light.py +0 -0
  83. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/pupil.py +0 -0
  84. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/core/pupil_decentration.py +0 -0
  85. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/__init__.py +0 -0
  86. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/algorithm_comparison.py +0 -0
  87. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/analysis_utils.py +0 -0
  88. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/calibration_analysis.py +0 -0
  89. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/calibration_utils.py +0 -0
  90. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/evaluation/gaze_accuracy.py +0 -0
  91. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/__init__.py +0 -0
  92. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/eyelink1000plus/__init__.py +0 -0
  93. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/eyelink1000plus/eyelink1000plus_gaze_model.py +0 -0
  94. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/eyelink1000plus/href.py +0 -0
  95. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/homography_normalization/__init__.py +0 -0
  96. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/homography_normalization/gaussian_process.py +0 -0
  97. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/homography_normalization/homography_normalization_gaze_model.py +0 -0
  98. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/homography_normalization/homography_normalization_state.py +0 -0
  99. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/homography_normalization/homography_normalization_utils.py +0 -0
  100. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/__init__.py +0 -0
  101. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/polynomial_descriptor.py +0 -0
  102. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/polynomial_features.py +0 -0
  103. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/polynomial_gaze_model.py +0 -0
  104. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/polynomial_state.py +0 -0
  105. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/polynomial/polynomials.py +0 -0
  106. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/gaze_mapping/stampe1993/__init__.py +0 -0
  107. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/__init__.py +0 -0
  108. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/conversions.py +0 -0
  109. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/intersections.py +0 -0
  110. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/listings_law.py +0 -0
  111. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/plane_detection.py +0 -0
  112. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/geometry/utils.py +0 -0
  113. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/log.py +0 -0
  114. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/optics/__init__.py +0 -0
  115. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/optics/glint_size.py +0 -0
  116. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/optics/pupil_imaging.py +0 -0
  117. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/optics/reflections.py +0 -0
  118. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/optics/refractions.py +0 -0
  119. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/__init__.py +0 -0
  120. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/anatomy.py +0 -0
  121. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/config.py +0 -0
  122. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/core.py +0 -0
  123. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/data_generation.py +0 -0
  124. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/data_loading.py +0 -0
  125. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/eye_position.py +0 -0
  126. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/generic.py +0 -0
  127. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/grid_base.py +0 -0
  128. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/simulation/target_position.py +0 -0
  129. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/types/__init__.py +0 -0
  130. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/types/algorithms.py +0 -0
  131. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/types/imaging.py +0 -0
  132. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/__init__.py +0 -0
  133. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/eye_surface_points.py +0 -0
  134. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/eyelid_surface_points.py +0 -0
  135. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/filename.py +0 -0
  136. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/rotation_helpers.py +0 -0
  137. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/utils/surface_points.py +0 -0
  138. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/__init__.py +0 -0
  139. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/analysis_plots.py +0 -0
  140. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/camera_view.py +0 -0
  141. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/coordinate_utils.py +0 -0
  142. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/eye_anatomy.py +0 -0
  143. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/gaze_accuracy_plots.py +0 -0
  144. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/integrated_plots.py +0 -0
  145. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/interactive.py +0 -0
  146. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/interactive_controls.py +0 -0
  147. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/interactive_gaze_plot.py +0 -0
  148. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/plot_config.py +0 -0
  149. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/setup_plots.py +0 -0
  150. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/pyetsimul/visualization/transforms.py +0 -0
  151. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/resources/Funded_by_EU_Eyes4ICU.png +0 -0
  152. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/__init__.py +0 -0
  153. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_camera_pan_tilt.py +0 -0
  154. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_camera_point_at.py +0 -0
  155. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_camera_project.py +0 -0
  156. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_camera_take_image.py +0 -0
  157. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_camera_unproject.py +0 -0
  158. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_find_refraction.py +0 -0
  159. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_get_pupil.py +0 -0
  160. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_get_pupil_image.py +0 -0
  161. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_look_at.py +0 -0
  162. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_point_within_cornea.py +0 -0
  163. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_pupil_size.py +0 -0
  164. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_eye_refract_ray_dual_surface.py +0 -0
  165. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_find_reflection_conic.py +0 -0
  166. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_find_reflection_sphere.py +0 -0
  167. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_find_refraction_conic.py +0 -0
  168. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_find_refraction_sphere.py +0 -0
  169. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_gaze2angle.py +0 -0
  170. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_intersect_ray_circle.py +0 -0
  171. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_intersect_ray_conic.py +0 -0
  172. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_intersect_ray_plane.py +0 -0
  173. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_intersect_ray_sphere.py +0 -0
  174. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_line_intersect_2d.py +0 -0
  175. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_lines_closest_point.py +0 -0
  176. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_listings_law.py +0 -0
  177. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_polynomial_validation.py +0 -0
  178. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_reflect_ray_circle.py +0 -0
  179. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_reflect_ray_conic.py +0 -0
  180. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_reflect_ray_sphere.py +0 -0
  181. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_refract_ray_conic.py +0 -0
  182. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/unit_tests/test_refract_ray_sphere.py +0 -0
  183. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/__init__.py +0 -0
  184. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/exploration_conic_vs_sphere.py +0 -0
  185. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/exploration_pupil_size_decentration.py +0 -0
  186. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/exploration_realistic_pupil.py +0 -0
  187. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/exploration_screen_and_observer_test.py +0 -0
  188. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_calibration.pdf +0 -0
  189. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_calibration.png +0 -0
  190. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_observer_test.pdf +0 -0
  191. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_observer_test.png +0 -0
  192. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_screen_test.pdf +0 -0
  193. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/demo_screen_test.png +0 -0
  194. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/results_conic_vs_sphere.txt +0 -0
  195. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/results_pupil_size_decentration.txt +0 -0
  196. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/results_realistic_pupil.txt +0 -0
  197. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/results_screen_observer_test.txt +0 -0
  198. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_conic_cornea.pdf +0 -0
  199. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_conic_cornea.png +0 -0
  200. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_pupil_decentration.pdf +0 -0
  201. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_pupil_decentration.png +0 -0
  202. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_realistic_pupil.pdf +0 -0
  203. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/output/validation_realistic_pupil.png +0 -0
  204. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/validation_conic_cornea.py +0 -0
  205. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/validation_pupil_decentration.py +0 -0
  206. {pyetsimul-3.2.4 → pyetsimul-3.3.0}/validation_and_scientific_exploitation/validation_realistic_pupil.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyetsimul
3
- Version: 3.2.4
3
+ Version: 3.3.0
4
4
  Summary: PyEtSimul: An Open-Source Python Software Framework for Eye Tracking Simulation
5
5
  Project-URL: Homepage, https://github.com/mh-salari/pyetsimul
6
6
  Project-URL: Repository, https://github.com/mh-salari/pyetsimul
7
7
  Project-URL: Documentation, https://pyetsimul.readthedocs.io/
8
8
  Project-URL: Issues, https://github.com/mh-salari/pyetsimul/issues
9
- Author-email: Mohammadhossein Salari <Mohammadhossein.Salari@gmail.com>
9
+ Author-email: Mohammadhossein Salari <mohammadhossein.salari@gmail.com>
10
10
  License: GPL-3.0-or-later
11
11
  License-File: LICENSE
12
12
  Keywords: eye-tracker,eye-tracking,gaze-estimation,simulation
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
  [![Downloads](https://static.pepy.tech/badge/pyetsimul)](https://pepy.tech/project/pyetsimul)
28
28
  [![License](https://img.shields.io/badge/License-GPL--3.0-green)](https://github.com/mh-salari/pyetsimul/blob/main/LICENSE)
29
29
  [![Documentation](https://readthedocs.org/projects/pyetsimul/badge/?version=latest)](https://pyetsimul.readthedocs.io/)
30
- [![DOI](https://img.shields.io/badge/DOI-coming_soon-blue)](#citation)
30
+ [![DOI](https://img.shields.io/badge/DOI-10.1145%2F3806023-blue)](https://doi.org/10.1145/3806023)
31
31
 
32
32
  PyEtSimul is an open-source Python framework for simulating video-based eye trackers
33
33
  by generating synthetic eye features through geometric modeling. The framework allows
@@ -87,13 +87,12 @@ After installation, explore the various examples in the `examples/` directory to
87
87
 
88
88
  ## Citation
89
89
 
90
- This work has been accepted for publication at ETRA 2026. The DOI will be added when the proceedings are published.
91
-
92
90
  **Cite as:**
93
91
  ```
94
92
  Salari, M., Niehorster, D. C., Hansen, D. W., & Bednarik, R. (2026).
95
93
  PyEtSimul: An Open-Source Python Framework for Eye-Tracking Simulation.
96
- In Proceedings of the 2026 Eye Tracking Research & Applications (ETRA 2026). ACM.
94
+ Proceedings of the ACM on Human-Computer Interaction (PACMHCI), ETRA 2026. ACM.
95
+ DOI: 10.1145/3806023
97
96
  ```
98
97
 
99
98
  **Also cite the original work:**
@@ -4,7 +4,7 @@
4
4
  [![Downloads](https://static.pepy.tech/badge/pyetsimul)](https://pepy.tech/project/pyetsimul)
5
5
  [![License](https://img.shields.io/badge/License-GPL--3.0-green)](https://github.com/mh-salari/pyetsimul/blob/main/LICENSE)
6
6
  [![Documentation](https://readthedocs.org/projects/pyetsimul/badge/?version=latest)](https://pyetsimul.readthedocs.io/)
7
- [![DOI](https://img.shields.io/badge/DOI-coming_soon-blue)](#citation)
7
+ [![DOI](https://img.shields.io/badge/DOI-10.1145%2F3806023-blue)](https://doi.org/10.1145/3806023)
8
8
 
9
9
  PyEtSimul is an open-source Python framework for simulating video-based eye trackers
10
10
  by generating synthetic eye features through geometric modeling. The framework allows
@@ -64,13 +64,12 @@ After installation, explore the various examples in the `examples/` directory to
64
64
 
65
65
  ## Citation
66
66
 
67
- This work has been accepted for publication at ETRA 2026. The DOI will be added when the proceedings are published.
68
-
69
67
  **Cite as:**
70
68
  ```
71
69
  Salari, M., Niehorster, D. C., Hansen, D. W., & Bednarik, R. (2026).
72
70
  PyEtSimul: An Open-Source Python Framework for Eye-Tracking Simulation.
73
- In Proceedings of the 2026 Eye Tracking Research & Applications (ETRA 2026). ACM.
71
+ Proceedings of the ACM on Human-Computer Interaction (PACMHCI), ETRA 2026. ACM.
72
+ DOI: 10.1145/3806023
74
73
  ```
75
74
 
76
75
  **Also cite the original work:**
@@ -1,13 +1,14 @@
1
1
  """Sphinx configuration for PyEtSimul documentation."""
2
2
 
3
3
  import sys
4
+ from datetime import UTC, datetime
4
5
  from pathlib import Path
5
6
 
6
7
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
7
8
 
8
9
  project = "PyEtSimul"
9
10
  author = "Mohammadhossein Salari"
10
- copyright = "2025, Mohammadhossein Salari" # noqa: A001
11
+ copyright = f"2025-{datetime.now(tz=UTC).year}, {author}" # noqa: A001
11
12
 
12
13
  extensions = [
13
14
  "sphinx.ext.autodoc",
@@ -15,8 +15,8 @@ PyEtSimul Documentation
15
15
  :target: https://github.com/mh-salari/pyetsimul/blob/main/LICENSE
16
16
  :alt: License
17
17
 
18
- .. |DOI| image:: https://img.shields.io/badge/DOI-coming_soon-blue
19
- :target: #
18
+ .. |DOI| image:: https://img.shields.io/badge/DOI-10.1145%2F3806023-blue
19
+ :target: https://doi.org/10.1145/3806023
20
20
  :alt: DOI
21
21
 
22
22
  **PyEtSimul** is an open-source Python framework for simulating video-based eye trackers
@@ -75,7 +75,7 @@ of gaze estimation algorithms across calibrated and uncalibrated settings.
75
75
  by the same authors. If you use PyEtSimul in your research, please cite [2].
76
76
 
77
77
  | [1] Bohme, M., Dorr, M., Graw, M., Martinetz, T., & Barth, E. (2008). A software framework for simulating eye trackers. In *Proceedings of the 2008 Symposium on Eye Tracking Research & Applications (ETRA '08)*, pp. 251-258. ACM. `DOI: 10.1145/1344471.1344529 <https://doi.org/10.1145/1344471.1344529>`_
78
- | [2] Salari, M., Niehorster, D. C., Hansen, D. W., & Bednarik, R. (2026). PyEtSimul: An Open-Source Python Framework for Eye-Tracking Simulation. In *Proceedings of the 2026 Eye Tracking Research & Applications (ETRA 2026)*. ACM. *[Accepted; DOI forthcoming]*
78
+ | [2] Salari, M., Niehorster, D. C., Hansen, D. W., & Bednarik, R. (2026). PyEtSimul: An Open-Source Python Framework for Eye-Tracking Simulation. *Proceedings of the ACM on Human-Computer Interaction (PACMHCI)*, ETRA 2026. ACM. `DOI: 10.1145/3806023 <https://doi.org/10.1145/3806023>`_
79
79
 
80
80
  .. note::
81
81
 
@@ -24,7 +24,7 @@ PyEtSimul gaze estimation models follow a simple pattern:
24
24
  ### Placeholder Code
25
25
 
26
26
  ```python
27
- class MyAlgorithmState(AlgorithmState): # Inherit from pyetsimul.types.algorithms.AlgorithmState
27
+ class MyAlgorithmState(AlgorithmState): # Inherit from pyetsimul.types.algorithms.AlgorithmState
28
28
  """
29
29
  State for your custom gaze model.
30
30
  TODO: Add fields to store your calibration parameters.
@@ -34,7 +34,7 @@ class MyAlgorithmState(AlgorithmState): # Inherit from pyetsimul.types.algorithm
34
34
  """Serialize the state to a dictionary for saving."""
35
35
  # TODO: Add your custom parameters to the dictionary.
36
36
  # Ensure they are JSON-serializable.
37
- return {} # Return a dictionary of your state
37
+ return {} # Return a dictionary of your state
38
38
 
39
39
  @classmethod
40
40
  def deserialize(cls, data: dict) -> "MyAlgorithmState":
@@ -58,7 +58,7 @@ class MyAlgorithmState(AlgorithmState): # Inherit from pyetsimul.types.algorithm
58
58
  ### Placeholder Code
59
59
 
60
60
  ```python
61
- class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
61
+ class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
62
62
  """
63
63
  A template for a custom gaze estimation model.
64
64
  TODO: Describe what your model does.
@@ -66,7 +66,7 @@ class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
66
66
 
67
67
  def __init__(self, **kwargs):
68
68
  super().__init__(**kwargs)
69
- self.algorithm_state = MyAlgorithmState() # Instance of your custom AlgorithmState
69
+ self.algorithm_state = MyAlgorithmState() # Instance of your custom AlgorithmState
70
70
 
71
71
  @property
72
72
  def algorithm_name(self) -> str:
@@ -80,13 +80,13 @@ class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
80
80
  # TODO: Add any specific parameters your model needs for instantiation
81
81
  ) -> "MyGazeModel":
82
82
  """A factory method for clean instantiation."""
83
- return cls() # Return an instance of your model
83
+ return cls() # Return an instance of your model
84
84
 
85
85
  # --- Block 3: Calibration Logic ---
86
86
  # Purpose: Learn the mapping from eye features to world coordinates.
87
87
  # This method modifies the internal state of the tracker, specifically `self.algorithm_state`.
88
88
  # It does not return a value.
89
- def calibrate(self, calibration_measurements: list[EyeMeasurement]) -> None: # EyeMeasurement from pyetsimul.types
89
+ def calibrate(self, calibration_measurements: list[EyeMeasurement]) -> None: # EyeMeasurement from pyetsimul.types
90
90
  """Calibrate the model by learning the mapping from eye features to gaze."""
91
91
  # TODO: Implement your calibration logic here.
92
92
  # This method should update `self.algorithm_state` with learned parameters.
@@ -96,7 +96,7 @@ class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
96
96
  # --- Block 4: Prediction Logic ---
97
97
  # Purpose: Estimate gaze from a single eye measurement using the learned model.
98
98
  # This method returns a `GazePrediction` object.
99
- def predict_gaze(self, measurement: EyeMeasurement) -> GazePrediction: # GazePrediction from pyetsimul.types
99
+ def predict_gaze(self, measurement: EyeMeasurement) -> GazePrediction: # GazePrediction from pyetsimul.types
100
100
  """Predict gaze position from a single eye measurement."""
101
101
  # TODO: Implement your gaze prediction logic here.
102
102
  # Extract features from `measurement`.
@@ -105,7 +105,7 @@ class MyGazeModel(EyeTracker): # Inherit from pyetsimul.core.EyeTracker
105
105
 
106
106
  # Return a GazePrediction object
107
107
  return GazePrediction(
108
- gaze_point=Point3D(0.0, 0.0, 0.0), # Point3D from pyetsimul.types
108
+ gaze_point=Point3D(0.0, 0.0, 0.0), # Point3D from pyetsimul.types
109
109
  confidence=0.0,
110
110
  algorithm_name=self.algorithm_name,
111
111
  processing_time=0.0,
@@ -7,6 +7,7 @@ Create a custom parameter variation in 3 steps:
7
7
  ```python
8
8
  from pyetsimul.simulation import GenericEyeVariation
9
9
 
10
+
10
11
  class MyCustomVariation(GenericEyeVariation):
11
12
  """Custom variation that changes any eye parameter."""
12
13
 
@@ -31,6 +32,7 @@ class MyCustomVariation(GenericEyeVariation):
31
32
  ```python
32
33
  from pyetsimul.core.cornea import ConicCornea
33
34
 
35
+
34
36
  class ConicCorneaKVariation(GenericEyeVariation):
35
37
  def __init__(self, k_range: list[float], num_steps: int = 10):
36
38
  super().__init__("cornea.anterior_k", k_range, num_steps)
@@ -42,6 +44,7 @@ class ConicCorneaKVariation(GenericEyeVariation):
42
44
  def apply_to_eye(self, eye, value: float) -> None:
43
45
  eye.cornea.anterior_k = value
44
46
 
47
+
45
48
  # Usage
46
49
  variation = ConicCorneaKVariation([-0.05, -0.15], 5)
47
50
  ```
@@ -58,7 +61,7 @@ config = ExperimentConfig(
58
61
  eyes=[Eye()], # Add your custom eye setup if needed
59
62
  cameras=[Camera()],
60
63
  lights=[Light()],
61
- output_dir="outputs"
64
+ output_dir="outputs",
62
65
  )
63
66
 
64
67
  # Generate data
@@ -119,7 +119,7 @@ def validate_measurements(data: dict[str, Any]) -> bool:
119
119
  return valid_measurements == total_measurements
120
120
 
121
121
 
122
- def compare_values(actual: Any, expected: Any, field_name: str, tolerance: float = 1e-10) -> bool: # noqa: ANN401, PLR0911
122
+ def compare_values(actual: Any, expected: Any, field_name: str, tolerance: float = 1e-10) -> bool: # noqa: ANN401
123
123
  """Compare actual vs expected values with tolerance."""
124
124
  if actual is None and expected is None:
125
125
  return True
@@ -116,13 +116,16 @@ class EyeTracker(ABC):
116
116
  failed_points = []
117
117
 
118
118
  info(f"Collecting calibration data at {n_points} points...")
119
+ if len(self.cameras) > 1:
120
+ warning(
121
+ f"calibrate() uses only the first of {len(self.cameras)} configured cameras; "
122
+ "multi-camera calibration is not implemented."
123
+ )
119
124
 
120
125
  for i, calib_point in enumerate(self.calib_points):
121
126
  # Make eye look at calibration point
122
127
  eye.look_at(calib_point, legacy=self.use_legacy_look_at)
123
128
 
124
- # Take images from all cameras (currently uses first camera)
125
- # TODO: Multi-camera support requires algorithm-specific implementation
126
129
  if not self.cameras:
127
130
  raise ValueError("No cameras available for calibration")
128
131
 
@@ -87,20 +87,69 @@ class Stampe1993GazeModel(PolynomialGazeModel):
87
87
  )
88
88
 
89
89
  def calibrate(self, calibration_measurements: list[EyeMeasurement]) -> None:
90
- """Two-stage calibration: biquadratic polynomial, then corner correction."""
91
- # Stage 1: Fit the biquadratic polynomial
90
+ """Two-stage calibration matching Stampe (1993) / EyeLink 1000 Plus.
91
+
92
+ Fits the biquadratic polynomial on the **inner** cal points (centre + edges,
93
+ no corners), then a per-quadrant corner correction on the **outer** (corner)
94
+ cal points. This staged split reproduces the EyeLink 1000 Plus stored !CAL
95
+ polynomial coefficients.
96
+ """
92
97
  self.plane_info = detect_calibration_plane(self.calib_points)
93
98
  info(summarize_plane_detection(self.calib_points, self.plane_info))
94
- self._calibrate_same_xy(calibration_measurements)
95
99
 
96
- # Stage 2: Fit corner correction from residuals
97
- self._fit_corner_correction(calibration_measurements)
98
-
99
- def _fit_corner_correction(self, calibration_measurements: list[EyeMeasurement]) -> None:
100
+ # Identify inner (centre+edges) vs outer (corner) calibration points by their
101
+ # 2-D target positions: a "corner" target is off both cardinal axes relative
102
+ # to the calibration-grid centroid (large |Δx · Δy|). For HV9 this picks the
103
+ # 4 corners and leaves the 5 inner points (centre + 4 edges) for the polynomial fit.
104
+ calib_coords_2d = np.array([self.plane_info.extract_2d_coords(pt) for pt in self.calib_points])
105
+ centroid = calib_coords_2d.mean(axis=0)
106
+ off_axis = np.abs((calib_coords_2d[:, 0] - centroid[0]) * (calib_coords_2d[:, 1] - centroid[1]))
107
+ n = len(self.calib_points)
108
+ n_corners = min(4, n - 1)
109
+ if n_corners <= 0 or n <= n_corners:
110
+ inner_idx = list(range(n))
111
+ corner_idx: list[int] = []
112
+ else:
113
+ order = np.argsort(off_axis)
114
+ inner_idx = order[:-n_corners].tolist()
115
+ corner_idx = order[-n_corners:].tolist()
116
+
117
+ # Stage 1: polynomial fit on inner cal points only. Temporarily restrict
118
+ # self.calib_points + the measurement list so the parent fitter sees only the inner subset.
119
+ saved_calib_points = self.calib_points
120
+ try:
121
+ self.calib_points = [saved_calib_points[i] for i in inner_idx]
122
+ inner_measurements = [calibration_measurements[i] for i in inner_idx]
123
+ self._calibrate_same_xy(inner_measurements)
124
+ finally:
125
+ self.calib_points = saved_calib_points
126
+
127
+ # Stage 2: corner correction on the 4 outer cal points (1 per quadrant for HV9).
128
+ if corner_idx:
129
+ self._fit_corner_correction(
130
+ [calibration_measurements[i] for i in corner_idx],
131
+ indices=corner_idx,
132
+ )
133
+ else:
134
+ # No corners (e.g. HV5 monocular geometry): zero out corner correction.
135
+ self.corner_coefficients = np.zeros((4, 2))
136
+ coords_array = np.array([self.plane_info.extract_2d_coords(pt) for pt in self.calib_points])
137
+ self.screen_center_2d = (float(coords_array[:, 0].mean()), float(coords_array[:, 1].mean()))
138
+
139
+ def _fit_corner_correction(
140
+ self,
141
+ calibration_measurements: list[EyeMeasurement],
142
+ indices: list[int] | None = None,
143
+ ) -> None:
100
144
  """Fit per-quadrant corner correction coefficients from polynomial residuals.
101
145
 
102
146
  The screen is divided into 4 quadrants relative to the calibration grid center.
103
147
  Corner correction: X_final = X_poly + cx[q] * (X_poly - Xc) * (Y_poly - Yc)
148
+
149
+ When ``indices`` is provided, position ``i`` in ``calibration_measurements`` is
150
+ paired with target coordinates from ``self.calib_points[indices[i]]``. This lets
151
+ the caller pass only a subset (the outer cal points) while the grid centroid is
152
+ still computed from the full ``self.calib_points`` set.
104
153
  """
105
154
  self.corner_coefficients = np.zeros((4, 2)) # [quadrant][cx, cy]
106
155
 
@@ -112,6 +161,9 @@ class Stampe1993GazeModel(PolynomialGazeModel):
112
161
  cx_center, cy_center = self.screen_center_2d
113
162
  info(f"\nCalibration grid center (2D): ({cx_center:.1f}, {cy_center:.1f}) mm")
114
163
 
164
+ if indices is None:
165
+ indices = list(range(len(calibration_measurements)))
166
+
115
167
  # Group points by quadrant relative to calibration center
116
168
  quadrant_data: dict[int, list[tuple[float, float, float, float]]] = {0: [], 1: [], 2: [], 3: []}
117
169
 
@@ -134,7 +186,7 @@ class Stampe1993GazeModel(PolynomialGazeModel):
134
186
  gaze_2d = coefficient_matrix @ poly_features.features
135
187
  poly_x, poly_y = gaze_2d[0], gaze_2d[1]
136
188
 
137
- target_x, target_y = calib_coords_2d[i]
189
+ target_x, target_y = calib_coords_2d[indices[i]]
138
190
 
139
191
  # Quadrant relative to calibration center
140
192
  dx = poly_x - cx_center
@@ -94,5 +94,4 @@ class SequentialVariation(ParameterVariation):
94
94
  """Generate sequential values from all variations."""
95
95
  for i, variation in enumerate(self.variations):
96
96
  for value in variation.generate_values():
97
- combination = {"variation_index": i, "variation_name": variation.param_name, "value": value}
98
- yield combination
97
+ yield {"variation_index": i, "variation_name": variation.param_name, "value": value}
@@ -4,6 +4,7 @@ Provides type-safe dataclasses that improve code readability and maintain
4
4
  semantic meaning while supporting arithmetic operations and numpy integration.
5
5
  """
6
6
 
7
+ import math
7
8
  from dataclasses import dataclass
8
9
  from typing import Self, Union, overload
9
10
 
@@ -49,7 +50,7 @@ class Point2D:
49
50
  error_msg = f"{msg}: {error_msg}"
50
51
  raise AssertionError(error_msg)
51
52
 
52
- def __sub__(self, other: "Point2D | float | int") -> "Point2D":
53
+ def __sub__(self, other: "Point2D | float") -> "Point2D":
53
54
  """Subtract point or scalar from point."""
54
55
  if isinstance(other, Point2D):
55
56
  return Point2D(self.x - other.x, self.y - other.y)
@@ -57,7 +58,7 @@ class Point2D:
57
58
  return Point2D(self.x - other, self.y - other)
58
59
  return NotImplemented
59
60
 
60
- def __add__(self, other: "Point2D | float | int") -> "Point2D":
61
+ def __add__(self, other: "Point2D | float") -> "Point2D":
61
62
  """Add two points/vectors or add scalar to point."""
62
63
  if isinstance(other, Point2D):
63
64
  return Point2D(self.x + other.x, self.y + other.y)
@@ -161,7 +162,7 @@ class Point3D:
161
162
  return Point3D(self.x - other, self.y - other, self.z - other)
162
163
  return NotImplemented
163
164
 
164
- def __add__(self, other: "Vector3D | float | int") -> "Point3D":
165
+ def __add__(self, other: "Vector3D | float") -> "Point3D":
165
166
  """Add a vector or scalar to a point to get a new point."""
166
167
  if isinstance(other, Vector3D):
167
168
  return Point3D(self.x + other.x, self.y + other.y, self.z + other.z)
@@ -278,7 +279,7 @@ class Vector3D:
278
279
  return Vector3D.from_array(result)
279
280
  return NotImplemented
280
281
 
281
- def __add__(self, other: "Vector3D | float | int") -> "Vector3D":
282
+ def __add__(self, other: "Vector3D | float") -> "Vector3D":
282
283
  """Add two vectors or add scalar to vector."""
283
284
  if isinstance(other, Vector3D):
284
285
  return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
@@ -286,7 +287,7 @@ class Vector3D:
286
287
  return Vector3D(self.x + other, self.y + other, self.z + other)
287
288
  return NotImplemented
288
289
 
289
- def __sub__(self, other: "Vector3D | float | int") -> "Vector3D":
290
+ def __sub__(self, other: "Vector3D | float") -> "Vector3D":
290
291
  """Subtract vector or scalar from vector."""
291
292
  if isinstance(other, Vector3D):
292
293
  return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
@@ -364,9 +365,10 @@ class Position3D:
364
365
  def from_array(cls, arr: np.ndarray) -> "Position3D":
365
366
  """Create from 4D homogeneous array [x,y,z,1] or 3D array [x,y,z]."""
366
367
  if arr.shape == (4,):
367
- if arr[3] != 1.0 and arr[3] != 0.0:
368
- # De-homogenize if needed
369
- return cls(arr[0] / arr[3], arr[1] / arr[3], arr[2] / arr[3])
368
+ w = float(arr[3])
369
+ # w == 1 (point) or w == 0 (direction) → take xyz directly; otherwise de-homogenize.
370
+ if not math.isclose(w, 1.0) and not math.isclose(w, 0.0):
371
+ return cls(arr[0] / w, arr[1] / w, arr[2] / w)
370
372
  return cls(arr[0], arr[1], arr[2])
371
373
  if arr.shape == (3,):
372
374
  return cls(arr[0], arr[1], arr[2])
@@ -422,7 +424,7 @@ class Position3D:
422
424
  return Position3D(self.x - other, self.y - other, self.z - other)
423
425
  return NotImplemented
424
426
 
425
- def __add__(self, other: "Vector3D | Direction3D | Point3D | float | int") -> "Position3D":
427
+ def __add__(self, other: "Vector3D | Direction3D | Point3D | float") -> "Position3D":
426
428
  """Add a vector, direction, point, or scalar to a position to get a new position."""
427
429
  if isinstance(other, (Vector3D, Direction3D, Point3D)):
428
430
  return Position3D(self.x + other.x, self.y + other.y, self.z + other.z)
@@ -438,7 +440,7 @@ class Position3D:
438
440
  """Multiply position by a scalar (reverse order)."""
439
441
  return self.__mul__(scalar)
440
442
 
441
- def __radd__(self, other: "Vector3D | Direction3D | Point3D | float | int") -> "Position3D":
443
+ def __radd__(self, other: "Vector3D | Direction3D | Point3D | float") -> "Position3D":
442
444
  """Add position to a scalar (reverse order)."""
443
445
  return self.__add__(other)
444
446
 
@@ -601,7 +603,7 @@ class Direction3D:
601
603
  # For other operations, defer to numpy
602
604
  return NotImplemented
603
605
 
604
- def __add__(self, other: "Direction3D | float | int") -> "Direction3D":
606
+ def __add__(self, other: "Direction3D | float") -> "Direction3D":
605
607
  """Add two directions or add scalar to direction."""
606
608
  if isinstance(other, Direction3D):
607
609
  return Direction3D(self.x + other.x, self.y + other.y, self.z + other.z)
@@ -609,7 +611,7 @@ class Direction3D:
609
611
  return Direction3D(self.x + other, self.y + other, self.z + other)
610
612
  return NotImplemented
611
613
 
612
- def __sub__(self, other: "Direction3D | float | int") -> "Direction3D":
614
+ def __sub__(self, other: "Direction3D | float") -> "Direction3D":
613
615
  """Subtract direction or scalar from direction."""
614
616
  if isinstance(other, Direction3D):
615
617
  return Direction3D(self.x - other.x, self.y - other.y, self.z - other.z)
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
  name = "pyetsimul"
7
7
  dynamic = ["version"]
8
8
  description = "PyEtSimul: An Open-Source Python Software Framework for Eye Tracking Simulation"
9
- authors = [{name = "Mohammadhossein Salari", email = "Mohammadhossein.Salari@gmail.com"}]
9
+ authors = [{name = "Mohammadhossein Salari", email = "mohammadhossein.salari@gmail.com"}]
10
10
  license = {text = "GPL-3.0-or-later"}
11
11
  readme = "README.md"
12
12
  keywords = ["eye-tracking", "simulation", "eye-tracker", "gaze-estimation"]
@@ -38,6 +38,7 @@ tag-regex = "^(?P<version>\\d+\\.\\d+\\.\\d+)$"
38
38
 
39
39
  [dependency-groups]
40
40
  dev = [
41
+ "ruff>=0.13.0",
41
42
  "jupyterlab>=4.4.7",
42
43
  "pytest>=8.4.1",
43
44
  "scipy-stubs>=1.16.2.0",
@@ -146,27 +147,28 @@ ignore = [
146
147
  "TID252", # Flake8-tidy-imports - Prefer absolute imports over relative imports from parent modules
147
148
  "FBT001", # Flake8-boolean-trap - Boolean-typed positional argument in function definition
148
149
  "FBT002", # Flake8-boolean-trap - Boolean default positional argument in function definition
150
+ "PLR0911", # Pylint - Too many return statements
151
+ "C901", # McCabe - Function is too complex
152
+ "PLR0912", # Pylint - Too many branches
149
153
  "PLR0913", # Pylint - Too many arguments
150
154
  "PLR0914", # Pylint - Too many local variables
151
- "PLR0904", # Pylint - Too many public methods
152
155
  "PLR0915", # Pylint - Too many statements
153
156
  "PLR0917", # Pylint - Too many positional arguments
154
157
  "TRY003", # Tryceratops - Avoid specifying long messages outside the exception class
155
158
  "EM101", # Flake8-errmsg - Exception must not use a string literal, assign to variable first
156
159
  "EM102", # Flake8-errmsg - Exception must not use an f-string literal, assign to variable first
157
160
  "D401", # Pydocstyle - First line of docstring should be in imperative mood
158
- "PLW3201", # Pylint - Bad dunder method name
159
- "PT011", # Flake8-pytest-style - pytest.raises() is too broad
160
- "TD002", # Flake8-todos - Missing author in TODO
161
- "TD003", # Flake8-todos - Missing issue link in TODO
162
- "FIX002", # Flake8-fixme - Line contains TODO
163
- "N803", # PEP8-naming - Argument name should be lowercase
164
- "C901", # McCabe - Function is too complex
165
- "PLR0912", # Pylint - Too many branches
166
- "INP001", # Flake8-no-pep420 - Implicit namespace package (requires __init__.py)
161
+ "N803", # PEP8-naming - Argument name should be lowercase (math vars like X, Y, N are conventional in this codebase)
167
162
  ]
168
163
 
164
+ [tool.ruff.lint.pylint]
165
+ # NumPy array-protocol hooks: NumPy looks these up by exact name when a custom
166
+ # class is passed to np.asarray() / ufuncs. Not typos; not generic Python dunders.
167
+ allow-dunder-method-names = ["__array__", "__array_ufunc__"]
168
+
169
169
  [tool.ruff.lint.per-file-ignores]
170
170
  "unit_tests/**/*.py" = ["S101"]
171
171
  "examples/run_all_examples.py" = ["S404", "S603"]
172
172
  "pyetsimul/log.py" = ["ANN401"]
173
+ "docs/conf.py" = ["INP001"] # Sphinx config file, not a package member
174
+ "pyetsimul/core/eye.py" = ["PLR0904"] # Eye is a domain object with many natural operations (orientation, pupil, cornea, eyelid, fovea, refraction, serialization)
@@ -1,6 +1,7 @@
1
1
  """Unit tests for angle2gaze function."""
2
2
 
3
3
  import numpy as np
4
+ import pytest
4
5
 
5
6
  from pyetsimul.geometry.conversions import angle2gaze
6
7
  from pyetsimul.types import Direction3D, Point2D, RotationMatrix
@@ -51,4 +52,4 @@ def test_output_properties() -> None:
51
52
  # The to_array() method should return a 4D homogeneous vector with the last component as 0
52
53
  result_array = np.array(result)
53
54
  assert result_array.shape == (4,)
54
- assert result_array[3] == 0.0
55
+ assert result_array[3] == pytest.approx(0.0, abs=1e-9)
@@ -1,6 +1,7 @@
1
1
  """Unit tests for Eye.find_cr method."""
2
2
 
3
3
  import numpy as np
4
+ import pytest
4
5
 
5
6
  from pyetsimul.core.camera import Camera
6
7
  from pyetsimul.core.eye import Eye
@@ -104,4 +105,4 @@ def test_output_properties() -> None:
104
105
  # Check types and shapes
105
106
  arr = np.array(cr)
106
107
  assert arr.shape == (4,)
107
- assert arr[3] == 1.0 # Homogeneous coordinate
108
+ assert arr[3] == pytest.approx(1.0) # Homogeneous coordinate
@@ -1,6 +1,7 @@
1
1
  """Unit tests for Eye.find_cr_simple method."""
2
2
 
3
3
  import numpy as np
4
+ import pytest
4
5
 
5
6
  from pyetsimul.core.camera import Camera
6
7
  from pyetsimul.core.eye import Eye
@@ -29,7 +30,7 @@ def test_basic_simple_corneal_reflex() -> None:
29
30
  # Test 4D homogeneous coordinates
30
31
  arr = np.array(cr)
31
32
  assert arr.shape == (4,)
32
- assert arr[3] == 1.0
33
+ assert arr[3] == pytest.approx(1.0)
33
34
 
34
35
 
35
36
  def test_angled_positions() -> None:
@@ -52,7 +53,7 @@ def test_angled_positions() -> None:
52
53
  # Test 4D homogeneous coordinates
53
54
  arr = np.array(cr)
54
55
  assert arr.shape == (4,)
55
- assert arr[3] == 1.0
56
+ assert arr[3] == pytest.approx(1.0)
56
57
 
57
58
 
58
59
  def test_reflex_outside_cornea_boundary() -> None:
@@ -85,4 +86,4 @@ def test_output_properties() -> None:
85
86
  # Check types and shapes
86
87
  arr = np.array(cr)
87
88
  assert arr.shape == (4,)
88
- assert arr[3] == 1.0 # Homogeneous coordinate
89
+ assert arr[3] == pytest.approx(1.0) # Homogeneous coordinate
@@ -183,7 +183,7 @@ class TestVector3D:
183
183
 
184
184
  # Magnitude
185
185
  mag = v.magnitude()
186
- assert mag == 5.0
186
+ assert mag == pytest.approx(5.0)
187
187
 
188
188
  # Normalize
189
189
  normalized = v.normalize()
@@ -253,7 +253,7 @@ class TestPosition3D:
253
253
  pos2 = Position3D(3, 4, 0)
254
254
 
255
255
  distance = pos1.distance_to(pos2)
256
- assert distance == 5.0
256
+ assert distance == pytest.approx(5.0)
257
257
 
258
258
 
259
259
  class TestDirection3D:
@@ -55,19 +55,19 @@ def test_constant_offset_noise() -> None:
55
55
 
56
56
  def test_config_validation() -> None:
57
57
  """Test that GlintNoiseConfig raises errors for invalid configurations."""
58
- with pytest.raises(ValueError):
58
+ with pytest.raises(ValueError, match=r"requires 'std' to be specified"):
59
59
  GlintNoiseConfig(noise_type="gaussian") # Missing std
60
60
 
61
- with pytest.raises(ValueError):
61
+ with pytest.raises(ValueError, match=r"requires 'std' to be specified"):
62
62
  GlintNoiseConfig(noise_type="uniform") # Missing std
63
63
 
64
- with pytest.raises(ValueError):
64
+ with pytest.raises(ValueError, match=r"requires 'offset_x' and 'offset_y'"):
65
65
  GlintNoiseConfig(noise_type="constant_offset", offset_x=1.0) # Missing offset_y
66
66
 
67
- with pytest.raises(ValueError):
67
+ with pytest.raises(ValueError, match=r"requires 'offset_x' and 'offset_y'"):
68
68
  GlintNoiseConfig(noise_type="constant_offset", offset_y=1.0) # Missing offset_x
69
69
 
70
- with pytest.raises(ValueError):
70
+ with pytest.raises(ValueError, match=r"Unknown noise type"):
71
71
  GlintNoiseConfig(noise_type="unknown_noise") # Unknown noise type
72
72
 
73
73
 
@@ -88,21 +88,21 @@ def test_advanced_noise() -> None:
88
88
  def test_advanced_mode_validation() -> None:
89
89
  """Test validation for advanced mode parameters."""
90
90
  # Missing covariance
91
- with pytest.raises(ValueError):
91
+ with pytest.raises(ValueError, match=r"requires both 'mean' and 'covariance'"):
92
92
  GlintNoiseConfig(mean=[0.0, 0.0])
93
93
 
94
94
  # Missing mean
95
- with pytest.raises(ValueError):
95
+ with pytest.raises(ValueError, match=r"requires both 'mean' and 'covariance'"):
96
96
  GlintNoiseConfig(covariance=[[1.0, 0.0], [0.0, 1.0]])
97
97
 
98
98
  # Wrong mean size
99
- with pytest.raises(ValueError):
99
+ with pytest.raises(ValueError, match=r"'mean' must be a 2-element list"):
100
100
  GlintNoiseConfig(mean=[0.0], covariance=[[1.0, 0.0], [0.0, 1.0]])
101
101
 
102
102
  # Wrong covariance size
103
- with pytest.raises(ValueError):
103
+ with pytest.raises(ValueError, match=r"'covariance' must be a 2x2 matrix"):
104
104
  GlintNoiseConfig(mean=[0.0, 0.0], covariance=[[1.0]])
105
105
 
106
106
  # Negative definite covariance
107
- with pytest.raises(ValueError):
107
+ with pytest.raises(ValueError, match=r"positive semidefinite"):
108
108
  GlintNoiseConfig(mean=[0.0, 0.0], covariance=[[-1.0, 0.0], [0.0, 1.0]])