From 5f40b12c3d7e1e843f35a0d78dad8e81842bd7b8 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Mon, 19 Jan 2026 14:02:35 -0500 Subject: [PATCH 01/19] wrote calculate_velocity_metrics --- src/graphomotor/core/models.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 5b2401e..ab9321d 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -236,3 +236,30 @@ def calculate_path_optimality( if optimal_distance > 0: self.path_optimality = optimal_distance / self.distance return + + def calculate_velocity_metrics(self, ink_points: pd.DataFrame) -> None: + """Get velocity metrics of a LineSegment. + + Args: + self: LineSegment object to calculate velocities for. + ink_points: DataFrame of ink points with 'x', 'y', and 'seconds' columns. + """ + dx = np.diff(ink_points["x"].values) + dy = np.diff(ink_points["y"].values) + dt = np.diff(ink_points["seconds"].values) + + # NOTE: need to change the way this case is handled + # Avoid division by zero + dt[dt == 0] = 1e-6 + + distances = np.sqrt(dx**2 + dy**2) + velocities = distances / dt + self.velocities = velocities.tolist() + + self.mean_speed = np.mean(velocities) + self.speed_variance = np.var(velocities) + + if len(velocities) >= 2: + self.accelerations = np.diff(velocities).tolist() + + return From de323372be38ac88bb04a4f0eccc6b1fdbd62e84 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Mon, 19 Jan 2026 14:08:11 -0500 Subject: [PATCH 02/19] add distance assign --- src/graphomotor/core/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index ab9321d..364a386 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -253,6 +253,8 @@ def calculate_velocity_metrics(self, ink_points: pd.DataFrame) -> None: dt[dt == 0] = 1e-6 distances = np.sqrt(dx**2 + dy**2) + self.distance = np.sum(distances) + velocities = distances / dt self.velocities = velocities.tolist() From 591a7522e9b36fb74d18227f034be54ff04f5247 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:05:35 -0500 Subject: [PATCH 03/19] new classmethod for validating dataframe does not contain duplicate timestamps --- src/graphomotor/core/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 364a386..73fcd92 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -73,6 +73,26 @@ def validate_metadata(cls, v: dict) -> dict: return v + @pydantic.field_validator("data") + @classmethod + def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: + """Validate that the data DataFrame does not contain duplicate timestamps. + + Args: + cls: The class. + v: The dataframe to validate. + + Returns: + The dataframe if it does not contain duplicate timestamps. + + Raises: + ValueError: If the dataframe contains duplicate timestamps. + """ + if v["seconds"].duplicated().any(): + raise ValueError("DataFrame contains duplicate timestamps.") + + return v + class SpiralFeatureCategories: """Class to hold valid feature categories for Graphomotor.""" From ac264944d7fb2a94966b774a9783a55a2f47c686 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:21:30 -0500 Subject: [PATCH 04/19] added UTC_timestap check to validator and wrote unit test --- src/graphomotor/core/models.py | 5 ++++- tests/unit/test_models.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 73fcd92..6e90da6 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -89,7 +89,10 @@ def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: ValueError: If the dataframe contains duplicate timestamps. """ if v["seconds"].duplicated().any(): - raise ValueError("DataFrame contains duplicate timestamps.") + raise ValueError("duplicate timestamps in 'seconds'.") + + if v["UTC_Timestamp"].duplicated().any(): + raise ValueError("duplicate timestamps in 'UTC_Timestamp'.") return v diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index e158f47..7bf4e2a 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -61,6 +61,37 @@ def test_invalid_metadata_values( ) +@pytest.mark.parametrize( + "key,time,duplicate_time,expected_error", + [ + ( + "UTC_Timestamp", + datetime.datetime(2024, 1, 1, 12, 0, 0), + datetime.datetime(2024, 1, 1, 12, 0, 0), + "duplicate timestamps in 'UTC_Timestamp'.", + ), + ("seconds", 1.73, 1.73, "duplicate timestamps in 'seconds'."), + ], +) +def test_duplicate_timestamps( + valid_spiral_data: pd.DataFrame, + valid_spiral_metadata: dict[str, str | datetime.datetime], + key: str, + time: datetime.datetime | float, + duplicate_time: datetime.datetime | float, + expected_error: str, +) -> None: + """Test that duplicate timestamps in the DataFrame aren't allowed.""" + invalid_data = valid_spiral_data.copy() + invalid_data[key][0] = time + invalid_data[key][1] = duplicate_time + + with pytest.raises(ValueError, match=expected_error): + models.Drawing( + data=invalid_data, task_name="spiral", metadata=valid_spiral_metadata + ) + + @pytest.fixture def circle() -> models.CircleTarget: """Create a standard circle at origin with radius 10.""" From 0952f4a52ef7eea8d4328482134aa4e7a62a2e58 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:32:39 -0500 Subject: [PATCH 05/19] adjust unit test for validator --- tests/unit/test_models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 7bf4e2a..8ccf356 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -66,8 +66,8 @@ def test_invalid_metadata_values( [ ( "UTC_Timestamp", - datetime.datetime(2024, 1, 1, 12, 0, 0), - datetime.datetime(2024, 1, 1, 12, 0, 0), + 16.596, + 16.596, "duplicate timestamps in 'UTC_Timestamp'.", ), ("seconds", 1.73, 1.73, "duplicate timestamps in 'seconds'."), @@ -77,8 +77,8 @@ def test_duplicate_timestamps( valid_spiral_data: pd.DataFrame, valid_spiral_metadata: dict[str, str | datetime.datetime], key: str, - time: datetime.datetime | float, - duplicate_time: datetime.datetime | float, + time: str | float, + duplicate_time: str | float, expected_error: str, ) -> None: """Test that duplicate timestamps in the DataFrame aren't allowed.""" From 3983c9caf14252bc41c7665820e930dc7780aa97 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:39:05 -0500 Subject: [PATCH 06/19] fixing other unit tests by adding required columns --- tests/unit/test_trails_drawing_metrics.py | 12 +++++- tests/unit/test_trails_utils.py | 48 ++++++++++------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/tests/unit/test_trails_drawing_metrics.py b/tests/unit/test_trails_drawing_metrics.py index cd87758..b447410 100644 --- a/tests/unit/test_trails_drawing_metrics.py +++ b/tests/unit/test_trails_drawing_metrics.py @@ -10,7 +10,11 @@ def test_get_total_errors() -> None: """Test ValueError when total_number_of_errors column doesn't exist.""" - invalid_df = pd.DataFrame({"some_other_column": [0, 1, 2]}) + invalid_df = pd.DataFrame({ + "some_other_column": [0, 1, 2], + "seconds": [0.0, 1.0, 2.0], + "UTC_Timestamp": [0.0, 1.0, 2.0], + }) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} ) @@ -33,7 +37,11 @@ def test_valid_total_errors() -> None: def test_percent_accurate_paths_missing_columns() -> None: """Test ValueError when required columns are missing.""" - invalid_df = pd.DataFrame({"some_other_column": [0, 1, 2]}) + invalid_df = pd.DataFrame({ + "some_other_column": [0, 1, 2], + "seconds": [0.0, 1.0, 2.0], + "UTC_Timestamp": [0.0, 1.0, 2.0], + }) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} ) diff --git a/tests/unit/test_trails_utils.py b/tests/unit/test_trails_utils.py index 8844aea..1bb1bd4 100644 --- a/tests/unit/test_trails_utils.py +++ b/tests/unit/test_trails_utils.py @@ -127,15 +127,13 @@ def test_multiple_unique_paths( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Multiple unique actual_path values produce correct segments.""" - df = pd.DataFrame( - { - "actual_path": ["1 ~ 2", "2 ~ 1"], - "line_number": [0, 1], - "is_error": [False, True], - "x": [0, 1], - "y": [0, 1], - } - ) + df = pd.DataFrame({ + "actual_path": ["1 ~ 2", "2 ~ 1"], + "line_number": [0, 1], + "is_error": [False, True], + "x": [0, 1], + "y": [0, 1], + }) segments = trails_utils.segment_lines(df, "trail2", circles) assert len(segments) == 2 assert segments[0].start_label == "1" @@ -152,15 +150,13 @@ def test_single_path_fallback_line_number( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Single unique path falls back to line_number segmentation.""" - df = pd.DataFrame( - { - "actual_path": ["1 ~ 2", "1 ~ 2"], - "line_number": [0, 1], - "is_error": [False, False], - "x": [0, 1], - "y": [0, 1], - } - ) + df = pd.DataFrame({ + "actual_path": ["1 ~ 2", "1 ~ 2"], + "line_number": [0, 1], + "is_error": [False, False], + "x": [0, 1], + "y": [0, 1], + }) segments = trails_utils.segment_lines(df, "trail2", circles) assert len(segments) == 2 for idx, seg in enumerate(segments): @@ -183,15 +179,13 @@ def test_invalid_trail_id( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Passing an invalid trail_id raises KeyError.""" - df = pd.DataFrame( - { - "x": [0], - "y": [0], - "actual_path": ["1 ~ 2"], - "line_number": [0], - "is_error": [False], - } - ) + df = pd.DataFrame({ + "x": [0], + "y": [0], + "actual_path": ["1 ~ 2"], + "line_number": [0], + "is_error": [False], + }) with pytest.raises(KeyError, match="Trail ID not found in circles dictionary."): trails_utils.segment_lines(df, "invalid", circles) From 3843b466bcd0921b650fb2fdc1aa30517a5f24c2 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:45:25 -0500 Subject: [PATCH 07/19] update validate data function to only apply to trails --- src/graphomotor/core/models.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 6e90da6..87a4907 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -88,11 +88,14 @@ def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: Raises: ValueError: If the dataframe contains duplicate timestamps. """ - if v["seconds"].duplicated().any(): - raise ValueError("duplicate timestamps in 'seconds'.") - - if v["UTC_Timestamp"].duplicated().any(): - raise ValueError("duplicate timestamps in 'UTC_Timestamp'.") + if cls.task_name != "trails": + return v + else: + if v["seconds"].duplicated().any(): + raise ValueError("duplicate timestamps in 'seconds'.") + + if v["UTC_Timestamp"].duplicated().any(): + raise ValueError("duplicate timestamps in 'UTC_Timestamp'.") return v From a3efd898bb3c53377634515e106b674426d1c7a6 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:51:49 -0500 Subject: [PATCH 08/19] revert validator test back to only seconds --- src/graphomotor/core/models.py | 12 +++--------- tests/unit/test_models.py | 6 ------ tests/unit/test_trails_drawing_metrics.py | 2 -- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 87a4907..b4bf09b 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -17,7 +17,7 @@ class Drawing(pydantic.BaseModel): data: DataFrame containing drawing data with required columns (line_number, x, y, UTC_Timestamp, seconds). task_name: Name of the drawing task (e.g., 'spiral', 'trails', etc.). - metadata: Dictionary containing metadata about the spiral: + metadata: Dictionary containing metadata about the drawing: - id: Unique identifier for the participant, - hand: Hand used ('Dom' for dominant, 'NonDom' for non-dominant), - task: Task name, @@ -88,14 +88,8 @@ def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: Raises: ValueError: If the dataframe contains duplicate timestamps. """ - if cls.task_name != "trails": - return v - else: - if v["seconds"].duplicated().any(): - raise ValueError("duplicate timestamps in 'seconds'.") - - if v["UTC_Timestamp"].duplicated().any(): - raise ValueError("duplicate timestamps in 'UTC_Timestamp'.") + if v["seconds"].duplicated().any(): + raise ValueError("duplicate timestamps in 'seconds'.") return v diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 8ccf356..6a5f912 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -64,12 +64,6 @@ def test_invalid_metadata_values( @pytest.mark.parametrize( "key,time,duplicate_time,expected_error", [ - ( - "UTC_Timestamp", - 16.596, - 16.596, - "duplicate timestamps in 'UTC_Timestamp'.", - ), ("seconds", 1.73, 1.73, "duplicate timestamps in 'seconds'."), ], ) diff --git a/tests/unit/test_trails_drawing_metrics.py b/tests/unit/test_trails_drawing_metrics.py index b447410..8f37dea 100644 --- a/tests/unit/test_trails_drawing_metrics.py +++ b/tests/unit/test_trails_drawing_metrics.py @@ -13,7 +13,6 @@ def test_get_total_errors() -> None: invalid_df = pd.DataFrame({ "some_other_column": [0, 1, 2], "seconds": [0.0, 1.0, 2.0], - "UTC_Timestamp": [0.0, 1.0, 2.0], }) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} @@ -40,7 +39,6 @@ def test_percent_accurate_paths_missing_columns() -> None: invalid_df = pd.DataFrame({ "some_other_column": [0, 1, 2], "seconds": [0.0, 1.0, 2.0], - "UTC_Timestamp": [0.0, 1.0, 2.0], }) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} From e0cdea0e0bf05a19743be4f7fd899c45fae10198 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 10:59:42 -0500 Subject: [PATCH 09/19] added new line checking for task name in metadata --- src/graphomotor/core/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index b4bf09b..32dfad4 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -88,8 +88,11 @@ def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: Raises: ValueError: If the dataframe contains duplicate timestamps. """ - if v["seconds"].duplicated().any(): - raise ValueError("duplicate timestamps in 'seconds'.") + if cls.metadata.get("task") not in {"trail1", "trail2", "trail3", "trail4"}: + return v + else: + if v["seconds"].duplicated().any(): + raise ValueError("duplicate timestamps in 'seconds'.") return v From 3489b85169cd06d1210f9374442bee9b26ad572e Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:30:14 -0500 Subject: [PATCH 10/19] remove validator function - will be its own new issue --- src/graphomotor/core/models.py | 23 ----------------------- tests/unit/test_models.py | 25 ------------------------- 2 files changed, 48 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 32dfad4..30a975f 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -73,29 +73,6 @@ def validate_metadata(cls, v: dict) -> dict: return v - @pydantic.field_validator("data") - @classmethod - def validate_timestamps(cls, v: pd.DataFrame) -> pd.DataFrame: - """Validate that the data DataFrame does not contain duplicate timestamps. - - Args: - cls: The class. - v: The dataframe to validate. - - Returns: - The dataframe if it does not contain duplicate timestamps. - - Raises: - ValueError: If the dataframe contains duplicate timestamps. - """ - if cls.metadata.get("task") not in {"trail1", "trail2", "trail3", "trail4"}: - return v - else: - if v["seconds"].duplicated().any(): - raise ValueError("duplicate timestamps in 'seconds'.") - - return v - class SpiralFeatureCategories: """Class to hold valid feature categories for Graphomotor.""" diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 6a5f912..e158f47 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -61,31 +61,6 @@ def test_invalid_metadata_values( ) -@pytest.mark.parametrize( - "key,time,duplicate_time,expected_error", - [ - ("seconds", 1.73, 1.73, "duplicate timestamps in 'seconds'."), - ], -) -def test_duplicate_timestamps( - valid_spiral_data: pd.DataFrame, - valid_spiral_metadata: dict[str, str | datetime.datetime], - key: str, - time: str | float, - duplicate_time: str | float, - expected_error: str, -) -> None: - """Test that duplicate timestamps in the DataFrame aren't allowed.""" - invalid_data = valid_spiral_data.copy() - invalid_data[key][0] = time - invalid_data[key][1] = duplicate_time - - with pytest.raises(ValueError, match=expected_error): - models.Drawing( - data=invalid_data, task_name="spiral", metadata=valid_spiral_metadata - ) - - @pytest.fixture def circle() -> models.CircleTarget: """Create a standard circle at origin with radius 10.""" From eb4b5221a09e460ffc9612adf16c4a92fc942196 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:34:07 -0500 Subject: [PATCH 11/19] ruff reformat --- tests/unit/test_trails_drawing_metrics.py | 20 ++++++---- tests/unit/test_trails_utils.py | 48 +++++++++++++---------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/tests/unit/test_trails_drawing_metrics.py b/tests/unit/test_trails_drawing_metrics.py index 8f37dea..f78cf69 100644 --- a/tests/unit/test_trails_drawing_metrics.py +++ b/tests/unit/test_trails_drawing_metrics.py @@ -10,10 +10,12 @@ def test_get_total_errors() -> None: """Test ValueError when total_number_of_errors column doesn't exist.""" - invalid_df = pd.DataFrame({ - "some_other_column": [0, 1, 2], - "seconds": [0.0, 1.0, 2.0], - }) + invalid_df = pd.DataFrame( + { + "some_other_column": [0, 1, 2], + "seconds": [0.0, 1.0, 2.0], + } + ) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} ) @@ -36,10 +38,12 @@ def test_valid_total_errors() -> None: def test_percent_accurate_paths_missing_columns() -> None: """Test ValueError when required columns are missing.""" - invalid_df = pd.DataFrame({ - "some_other_column": [0, 1, 2], - "seconds": [0.0, 1.0, 2.0], - }) + invalid_df = pd.DataFrame( + { + "some_other_column": [0, 1, 2], + "seconds": [0.0, 1.0, 2.0], + } + ) drawing = models.Drawing( data=invalid_df, task_name="trails", metadata={"id": "5555555"} ) diff --git a/tests/unit/test_trails_utils.py b/tests/unit/test_trails_utils.py index 1bb1bd4..8844aea 100644 --- a/tests/unit/test_trails_utils.py +++ b/tests/unit/test_trails_utils.py @@ -127,13 +127,15 @@ def test_multiple_unique_paths( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Multiple unique actual_path values produce correct segments.""" - df = pd.DataFrame({ - "actual_path": ["1 ~ 2", "2 ~ 1"], - "line_number": [0, 1], - "is_error": [False, True], - "x": [0, 1], - "y": [0, 1], - }) + df = pd.DataFrame( + { + "actual_path": ["1 ~ 2", "2 ~ 1"], + "line_number": [0, 1], + "is_error": [False, True], + "x": [0, 1], + "y": [0, 1], + } + ) segments = trails_utils.segment_lines(df, "trail2", circles) assert len(segments) == 2 assert segments[0].start_label == "1" @@ -150,13 +152,15 @@ def test_single_path_fallback_line_number( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Single unique path falls back to line_number segmentation.""" - df = pd.DataFrame({ - "actual_path": ["1 ~ 2", "1 ~ 2"], - "line_number": [0, 1], - "is_error": [False, False], - "x": [0, 1], - "y": [0, 1], - }) + df = pd.DataFrame( + { + "actual_path": ["1 ~ 2", "1 ~ 2"], + "line_number": [0, 1], + "is_error": [False, False], + "x": [0, 1], + "y": [0, 1], + } + ) segments = trails_utils.segment_lines(df, "trail2", circles) assert len(segments) == 2 for idx, seg in enumerate(segments): @@ -179,13 +183,15 @@ def test_invalid_trail_id( circles: Dict[str, Dict[str, models.CircleTarget]], ) -> None: """Passing an invalid trail_id raises KeyError.""" - df = pd.DataFrame({ - "x": [0], - "y": [0], - "actual_path": ["1 ~ 2"], - "line_number": [0], - "is_error": [False], - }) + df = pd.DataFrame( + { + "x": [0], + "y": [0], + "actual_path": ["1 ~ 2"], + "line_number": [0], + "is_error": [False], + } + ) with pytest.raises(KeyError, match="Trail ID not found in circles dictionary."): trails_utils.segment_lines(df, "invalid", circles) From 4e4ca3ec52b4a0efaa6f6201c2c56350542a2789 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:39:40 -0500 Subject: [PATCH 12/19] remove divide by 0 replacement --- src/graphomotor/core/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 30a975f..bf4157f 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -248,10 +248,6 @@ def calculate_velocity_metrics(self, ink_points: pd.DataFrame) -> None: dy = np.diff(ink_points["y"].values) dt = np.diff(ink_points["seconds"].values) - # NOTE: need to change the way this case is handled - # Avoid division by zero - dt[dt == 0] = 1e-6 - distances = np.sqrt(dx**2 + dy**2) self.distance = np.sum(distances) From a3a191889821604ae8c26bdd767b1c5421919118 Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:48:28 -0500 Subject: [PATCH 13/19] unit tests for velocity function --- tests/unit/test_models.py | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index e158f47..8c12c32 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -135,3 +135,138 @@ def test_path_optimality_non_positive_distance() -> None: segment.calculate_path_optimality(start, end) assert segment.path_optimality == 0.0 + + +def test_uniform_motion() -> None: + """Test with points moving at constant velocity.""" + points = pd.DataFrame({ + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) + segment = models.LineSegment( + start_label="1", + end_label="2", + points=points, + is_error=False, + line_number=1, + ) + + segment.calculate_velocity_metrics(points) + + assert segment.distance == pytest.approx(3.0) + assert segment.mean_speed == pytest.approx(1.0) + assert segment.speed_variance == pytest.approx(0.0) + assert len(segment.velocities) == 3 + assert all(v == pytest.approx(1.0) for v in segment.velocities) + assert len(segment.accelerations) == 2 + assert all(a == pytest.approx(0.0) for a in segment.accelerations) + + +def test_accelerating_motion() -> None: + """Test with motion accelerating over time.""" + points = pd.DataFrame({ + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) + segment = models.LineSegment( + start_label="1", + end_label="2", + points=points, + is_error=False, + line_number=1, + ) + + segment.calculate_velocity_metrics(points) + + assert segment.distance == pytest.approx(9.0) + assert segment.mean_speed == pytest.approx(3.0) + assert segment.speed_variance > 0.0 + assert len(segment.velocities) == 3 + assert segment.velocities[0] == pytest.approx(1.0) + assert segment.velocities[1] == pytest.approx(3.0) + assert segment.velocities[2] == pytest.approx(5.0) + assert len(segment.accelerations) == 2 + assert segment.accelerations[0] == pytest.approx(2.0) + assert segment.accelerations[1] == pytest.approx(2.0) + + +def test_velocity_two_points_only() -> None: + """Test velocity calculation with only two points (one velocity, no acceleration).""" + points = pd.DataFrame({ + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + }) + segment = models.LineSegment( + start_label="1", + end_label="2", + points=points, + is_error=False, + line_number=1, + ) + + segment.calculate_velocity_metrics(points) + + assert segment.distance == pytest.approx(5.0) + assert segment.mean_speed == pytest.approx(2.5) + assert segment.speed_variance == pytest.approx(0.0) + assert len(segment.velocities) == 1 + assert segment.velocities[0] == pytest.approx(2.5) + assert len(segment.accelerations) == 0 + + +def test_decelerating_motion() -> None: + """Test with decelerating motion (negative acceleration).""" + points = pd.DataFrame({ + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) + segment = models.LineSegment( + start_label="1", + end_label="2", + points=points, + is_error=False, + line_number=1, + ) + + segment.calculate_velocity_metrics(points) + + assert segment.distance == pytest.approx(9.0) + assert segment.mean_speed == pytest.approx(3.0) + assert segment.speed_variance > 0.0 + assert len(segment.velocities) == 3 + assert segment.velocities[0] == pytest.approx(4.0) + assert segment.velocities[1] == pytest.approx(3.0) + assert segment.velocities[2] == pytest.approx(2.0) + assert len(segment.accelerations) == 2 + assert segment.accelerations[0] == pytest.approx(-1.0) + assert segment.accelerations[1] == pytest.approx(-1.0) + + +def test_stationary_motion() -> None: + """Test with no movement (all points the same).""" + points = pd.DataFrame({ + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + }) + segment = models.LineSegment( + start_label="1", + end_label="2", + points=points, + is_error=False, + line_number=1, + ) + + segment.calculate_velocity_metrics(points) + + assert segment.distance == pytest.approx(0.0) + assert segment.mean_speed == pytest.approx(0.0) + assert segment.speed_variance == pytest.approx(0.0) + assert len(segment.velocities) == 2 + assert all(v == pytest.approx(0.0) for v in segment.velocities) + assert len(segment.accelerations) == 1 + assert segment.accelerations[0] == pytest.approx(0.0) From 467553cc57afc4f2781e22400d75a9d46bf1d22c Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:55:10 -0500 Subject: [PATCH 14/19] fix small bug from merge from main into branch --- tests/unit/test_models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 98d7779..56bcf0c 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -239,7 +239,7 @@ def test_valid_ink_trajectory( assert result_start == expected_start, f"Start index mismatch for {test_id}" assert result_end == expected_end, f"End index mismatch for {test_id}" - + def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" points = pd.DataFrame({ @@ -251,6 +251,10 @@ def test_uniform_motion() -> None: start_label="1", end_label="2", points=points, + is_error=False, + line_number=1, + ) + segment.calculate_velocity_metrics(points) assert segment.distance == pytest.approx(3.0) @@ -292,7 +296,7 @@ def test_accelerating_motion() -> None: def test_velocity_two_points_only() -> None: - """Test velocity calculation with only two points (one velocity, no acceleration).""" + """Test velocity calculation with only two points.""" points = pd.DataFrame({ "x": [0, 3], "y": [0, 4], @@ -369,4 +373,3 @@ def test_stationary_motion() -> None: assert all(v == pytest.approx(0.0) for v in segment.velocities) assert len(segment.accelerations) == 1 assert segment.accelerations[0] == pytest.approx(0.0) - From 10957f96b9729932c6399c8a109b67bb0eb4c02c Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Tue, 10 Feb 2026 11:58:28 -0500 Subject: [PATCH 15/19] ruff reformat --- tests/unit/test_models.py | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 56bcf0c..fb2be62 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -242,11 +242,13 @@ def test_valid_ink_trajectory( def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" - points = pd.DataFrame({ - "x": [0, 1, 2, 3], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -268,11 +270,13 @@ def test_uniform_motion() -> None: def test_accelerating_motion() -> None: """Test with motion accelerating over time.""" - points = pd.DataFrame({ - "x": [0, 1, 4, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -297,11 +301,13 @@ def test_accelerating_motion() -> None: def test_velocity_two_points_only() -> None: """Test velocity calculation with only two points.""" - points = pd.DataFrame({ - "x": [0, 3], - "y": [0, 4], - "seconds": [0, 2], - }) + points = pd.DataFrame( + { + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -322,11 +328,13 @@ def test_velocity_two_points_only() -> None: def test_decelerating_motion() -> None: """Test with decelerating motion (negative acceleration).""" - points = pd.DataFrame({ - "x": [0, 4, 7, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -351,11 +359,13 @@ def test_decelerating_motion() -> None: def test_stationary_motion() -> None: """Test with no movement (all points the same).""" - points = pd.DataFrame({ - "x": [1, 1, 1], - "y": [1, 1, 1], - "seconds": [0, 1, 2], - }) + points = pd.DataFrame( + { + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2", From b59e5e7e5587649fe60706e1057d465673d0bb7f Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Wed, 11 Feb 2026 14:48:51 -0500 Subject: [PATCH 16/19] resolved comments --- src/graphomotor/core/models.py | 2 +- tests/unit/test_models.py | 122 +++++++++++++-------------------- 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/src/graphomotor/core/models.py b/src/graphomotor/core/models.py index 5e6aa0c..34aaa61 100644 --- a/src/graphomotor/core/models.py +++ b/src/graphomotor/core/models.py @@ -293,7 +293,7 @@ def calculate_path_optimality( return def calculate_velocity_metrics(self, ink_points: pd.DataFrame) -> None: - """Get velocity metrics of a LineSegment. + """Get distance, velocity, and acceleration metrics of a LineSegment. Args: self: LineSegment object to calculate velocities for. diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index fb2be62..8e95661 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -3,6 +3,7 @@ import datetime from typing import Dict, cast +import numpy as np import pandas as pd import pytest @@ -242,13 +243,11 @@ def test_valid_ink_trajectory( def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" - points = pd.DataFrame( - { - "x": [0, 1, 2, 3], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -259,24 +258,20 @@ def test_uniform_motion() -> None: segment.calculate_velocity_metrics(points) - assert segment.distance == pytest.approx(3.0) - assert segment.mean_speed == pytest.approx(1.0) - assert segment.speed_variance == pytest.approx(0.0) - assert len(segment.velocities) == 3 - assert all(v == pytest.approx(1.0) for v in segment.velocities) - assert len(segment.accelerations) == 2 - assert all(a == pytest.approx(0.0) for a in segment.accelerations) + assert segment.distance == 3.0 + assert segment.mean_speed == 1.0 + assert segment.speed_variance == 0.0 + assert np.all(segment.velocities) == 1.0 + assert np.all(segment.accelerations) == 0.0 def test_accelerating_motion() -> None: """Test with motion accelerating over time.""" - points = pd.DataFrame( - { - "x": [0, 1, 4, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -287,27 +282,20 @@ def test_accelerating_motion() -> None: segment.calculate_velocity_metrics(points) - assert segment.distance == pytest.approx(9.0) - assert segment.mean_speed == pytest.approx(3.0) + assert segment.distance == 9.0 + assert segment.mean_speed == 3.0 assert segment.speed_variance > 0.0 - assert len(segment.velocities) == 3 - assert segment.velocities[0] == pytest.approx(1.0) - assert segment.velocities[1] == pytest.approx(3.0) - assert segment.velocities[2] == pytest.approx(5.0) - assert len(segment.accelerations) == 2 - assert segment.accelerations[0] == pytest.approx(2.0) - assert segment.accelerations[1] == pytest.approx(2.0) + assert segment.velocities == [1.0, 3.0, 5.0] + assert segment.accelerations == [2.0, 2.0] def test_velocity_two_points_only() -> None: """Test velocity calculation with only two points.""" - points = pd.DataFrame( - { - "x": [0, 3], - "y": [0, 4], - "seconds": [0, 2], - } - ) + points = pd.DataFrame({ + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -318,23 +306,20 @@ def test_velocity_two_points_only() -> None: segment.calculate_velocity_metrics(points) - assert segment.distance == pytest.approx(5.0) - assert segment.mean_speed == pytest.approx(2.5) - assert segment.speed_variance == pytest.approx(0.0) - assert len(segment.velocities) == 1 - assert segment.velocities[0] == pytest.approx(2.5) - assert len(segment.accelerations) == 0 + assert segment.distance == 5.0 + assert segment.mean_speed == 2.5 + assert segment.speed_variance == 0.0 + assert segment.velocities == [2.5] + assert segment.accelerations == [] def test_decelerating_motion() -> None: """Test with decelerating motion (negative acceleration).""" - points = pd.DataFrame( - { - "x": [0, 4, 7, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -345,27 +330,20 @@ def test_decelerating_motion() -> None: segment.calculate_velocity_metrics(points) - assert segment.distance == pytest.approx(9.0) - assert segment.mean_speed == pytest.approx(3.0) + assert segment.distance == 9.0 + assert segment.mean_speed == 3.0 assert segment.speed_variance > 0.0 - assert len(segment.velocities) == 3 - assert segment.velocities[0] == pytest.approx(4.0) - assert segment.velocities[1] == pytest.approx(3.0) - assert segment.velocities[2] == pytest.approx(2.0) - assert len(segment.accelerations) == 2 - assert segment.accelerations[0] == pytest.approx(-1.0) - assert segment.accelerations[1] == pytest.approx(-1.0) + assert segment.velocities == [4.0, 3.0, 2.0] + assert segment.accelerations == [-1.0, -1.0] def test_stationary_motion() -> None: """Test with no movement (all points the same).""" - points = pd.DataFrame( - { - "x": [1, 1, 1], - "y": [1, 1, 1], - "seconds": [0, 1, 2], - } - ) + points = pd.DataFrame({ + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -376,10 +354,8 @@ def test_stationary_motion() -> None: segment.calculate_velocity_metrics(points) - assert segment.distance == pytest.approx(0.0) - assert segment.mean_speed == pytest.approx(0.0) - assert segment.speed_variance == pytest.approx(0.0) - assert len(segment.velocities) == 2 - assert all(v == pytest.approx(0.0) for v in segment.velocities) - assert len(segment.accelerations) == 1 - assert segment.accelerations[0] == pytest.approx(0.0) + assert segment.distance == 0.0 + assert segment.mean_speed == 0.0 + assert segment.speed_variance == 0.0 + assert segment.velocities == [0.0, 0.0] + assert segment.accelerations == [0.0] From 47faf31446e4114a42dbbfca370a52c2eff3894d Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Wed, 11 Feb 2026 14:49:27 -0500 Subject: [PATCH 17/19] ruff reformat --- tests/unit/test_models.py | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 8e95661..0e77ab4 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -243,11 +243,13 @@ def test_valid_ink_trajectory( def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" - points = pd.DataFrame({ - "x": [0, 1, 2, 3], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -267,11 +269,13 @@ def test_uniform_motion() -> None: def test_accelerating_motion() -> None: """Test with motion accelerating over time.""" - points = pd.DataFrame({ - "x": [0, 1, 4, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -291,11 +295,13 @@ def test_accelerating_motion() -> None: def test_velocity_two_points_only() -> None: """Test velocity calculation with only two points.""" - points = pd.DataFrame({ - "x": [0, 3], - "y": [0, 4], - "seconds": [0, 2], - }) + points = pd.DataFrame( + { + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -315,11 +321,13 @@ def test_velocity_two_points_only() -> None: def test_decelerating_motion() -> None: """Test with decelerating motion (negative acceleration).""" - points = pd.DataFrame({ - "x": [0, 4, 7, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -339,11 +347,13 @@ def test_decelerating_motion() -> None: def test_stationary_motion() -> None: """Test with no movement (all points the same).""" - points = pd.DataFrame({ - "x": [1, 1, 1], - "y": [1, 1, 1], - "seconds": [0, 1, 2], - }) + points = pd.DataFrame( + { + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2", From ad111865ddda6e7de6e7abe19b8856fa509bd69b Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Thu, 12 Feb 2026 10:20:08 -0500 Subject: [PATCH 18/19] change >0.0 to known value --- tests/unit/test_models.py | 62 ++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 0e77ab4..6702a26 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -243,13 +243,11 @@ def test_valid_ink_trajectory( def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" - points = pd.DataFrame( - { - "x": [0, 1, 2, 3], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -269,13 +267,11 @@ def test_uniform_motion() -> None: def test_accelerating_motion() -> None: """Test with motion accelerating over time.""" - points = pd.DataFrame( - { - "x": [0, 1, 4, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -288,20 +284,18 @@ def test_accelerating_motion() -> None: assert segment.distance == 9.0 assert segment.mean_speed == 3.0 - assert segment.speed_variance > 0.0 + assert segment.speed_variance == pytest.approx(2.6666666666666665) assert segment.velocities == [1.0, 3.0, 5.0] assert segment.accelerations == [2.0, 2.0] def test_velocity_two_points_only() -> None: """Test velocity calculation with only two points.""" - points = pd.DataFrame( - { - "x": [0, 3], - "y": [0, 4], - "seconds": [0, 2], - } - ) + points = pd.DataFrame({ + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -321,13 +315,11 @@ def test_velocity_two_points_only() -> None: def test_decelerating_motion() -> None: """Test with decelerating motion (negative acceleration).""" - points = pd.DataFrame( - { - "x": [0, 4, 7, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - } - ) + points = pd.DataFrame({ + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + }) segment = models.LineSegment( start_label="1", end_label="2", @@ -347,13 +339,11 @@ def test_decelerating_motion() -> None: def test_stationary_motion() -> None: """Test with no movement (all points the same).""" - points = pd.DataFrame( - { - "x": [1, 1, 1], - "y": [1, 1, 1], - "seconds": [0, 1, 2], - } - ) + points = pd.DataFrame({ + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + }) segment = models.LineSegment( start_label="1", end_label="2", From 45d50c739ee496ab7a4a50ca06229735f366cafd Mon Sep 17 00:00:00 2001 From: cgmaiorano Date: Thu, 12 Feb 2026 10:20:52 -0500 Subject: [PATCH 19/19] ruff format --- tests/unit/test_models.py | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 6702a26..00875ac 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -243,11 +243,13 @@ def test_valid_ink_trajectory( def test_uniform_motion() -> None: """Test with points moving at constant velocity.""" - points = pd.DataFrame({ - "x": [0, 1, 2, 3], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 2, 3], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -267,11 +269,13 @@ def test_uniform_motion() -> None: def test_accelerating_motion() -> None: """Test with motion accelerating over time.""" - points = pd.DataFrame({ - "x": [0, 1, 4, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 1, 4, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -291,11 +295,13 @@ def test_accelerating_motion() -> None: def test_velocity_two_points_only() -> None: """Test velocity calculation with only two points.""" - points = pd.DataFrame({ - "x": [0, 3], - "y": [0, 4], - "seconds": [0, 2], - }) + points = pd.DataFrame( + { + "x": [0, 3], + "y": [0, 4], + "seconds": [0, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -315,11 +321,13 @@ def test_velocity_two_points_only() -> None: def test_decelerating_motion() -> None: """Test with decelerating motion (negative acceleration).""" - points = pd.DataFrame({ - "x": [0, 4, 7, 9], - "y": [0, 0, 0, 0], - "seconds": [0, 1, 2, 3], - }) + points = pd.DataFrame( + { + "x": [0, 4, 7, 9], + "y": [0, 0, 0, 0], + "seconds": [0, 1, 2, 3], + } + ) segment = models.LineSegment( start_label="1", end_label="2", @@ -339,11 +347,13 @@ def test_decelerating_motion() -> None: def test_stationary_motion() -> None: """Test with no movement (all points the same).""" - points = pd.DataFrame({ - "x": [1, 1, 1], - "y": [1, 1, 1], - "seconds": [0, 1, 2], - }) + points = pd.DataFrame( + { + "x": [1, 1, 1], + "y": [1, 1, 1], + "seconds": [0, 1, 2], + } + ) segment = models.LineSegment( start_label="1", end_label="2",