@@ -256,7 +256,7 @@ def _get_progression_vector(self, trial: model.Trial) -> xr.DataArray:
256
256
def _get_sagittal_vector (self , trial : model .Trial ) -> xr .DataArray :
257
257
"""Calculate the sagittal vector for a trial.
258
258
259
- The sagittal vector is the vector normal to the sagittal plane.
259
+ The sagittal vector is the vector normal to the sagittal plane. Note that this vector will always be pointing towards the right side of the body.
260
260
261
261
Args:
262
262
trial: The trial for which to calculate the sagittal vector.
@@ -507,19 +507,23 @@ class SpatialFeatures(_PointDependentFeature):
507
507
- step_width
508
508
- minimal_toe_clearance
509
509
- AP_margin_of_stability
510
+ - AP_base_of_support
511
+ - AP_xcom
510
512
- ML_margin_of_stability
513
+ - ML_base_of_support
514
+ - ML_xcom
511
515
"""
512
516
513
517
def _calculate (self , trial : model .Trial ) -> xr .DataArray :
514
518
"""Calculate the spatial features for a trial.
515
519
516
520
Definitions of the spatial features:
517
521
Step length & Step width: Hollmann et al. 2011 (doi: 10.1016/j.gaitpost.2011.03.024)
518
- Margin of stability: Jinfeng et al. 2021 (doi: 10.1152/jn.00091.2021)
522
+ Margin of stability: Jinfeng et al. 2021 (doi: 10.1152/jn.00091.2021), Curtze et al. 2024 (doi: 10.1016/j.jbiomech.2024.112045)
519
523
Minimal toe clearance: Schulz 2017 (doi: 10.1016/j.jbiomech.2017.02.024)
520
524
521
525
Args:
522
- trial: The trial for which to calculate the features.
526
+ trial: The trial for which to calculate the features.
523
527
524
528
Returns:
525
529
An xarray DataArray containing the calculated features.
@@ -567,9 +571,9 @@ def _calculate(self, trial: model.Trial) -> xr.DataArray:
567
571
results_dict .update (
568
572
self ._calculate_ap_margin_of_stability (
569
573
trial ,
570
- marker_dict ["ipsi_toe_2 " ], # type: ignore
574
+ marker_dict ["ipsi_heel " ], # type: ignore
571
575
marker_dict ["contra_toe_2" ], # type: ignore
572
- marker_dict ["xcom" ],
576
+ marker_dict ["xcom" ]
573
577
)
574
578
)
575
579
@@ -578,7 +582,7 @@ def _calculate(self, trial: model.Trial) -> xr.DataArray:
578
582
trial ,
579
583
marker_dict ["ipsi_ankle" ],
580
584
marker_dict ["contra_ankle" ],
581
- marker_dict ["xcom" ],
585
+ marker_dict ["xcom" ]
582
586
)
583
587
)
584
588
except KeyError :
@@ -704,7 +708,7 @@ def _calculate_stride_length(
704
708
ipsi_marker : mapping .MappedMarkers ,
705
709
contra_marker : mapping .MappedMarkers ,
706
710
) -> dict [str , np .ndarray ]:
707
- """Calculate the stride length for a trial.
711
+ """Calculate the stride length for a trial. It is computed as the two consecutive step lengths constituting the gait cycle.
708
712
709
713
Args:
710
714
trial: The trial for which to calculate the stride length.
@@ -838,26 +842,28 @@ def _find_mtc_index(
838
842
839
843
return None if not mtc_i else min (mtc_i , key = lambda i : toe_z [i ]) # type: ignore
840
844
841
- def _calculate_ap_margin_of_stability (
842
- self ,
843
- trial : model .Trial ,
844
- ipsi_toe_marker : mapping .MappedMarkers ,
845
- contra_toe_marker : mapping .MappedMarkers ,
846
- xcom_marker : mapping .MappedMarkers ,
847
- ) -> dict [str , np .ndarray ]:
848
- """Calculate the anterior-posterior margin of stability at heel strike
845
+ def _calculate_ap_margin_of_stability (self ,
846
+ trial : model .Trial ,
847
+ ipsi_heel_marker : mapping .MappedMarkers ,
848
+ contra_toe_marker : mapping .MappedMarkers ,
849
+ xcom_marker : mapping .MappedMarkers ,
850
+ ) -> dict [str , np .ndarray ]:
851
+ """Calculate the anterio-posterior margin of stability at heel strike. Result should be interpreted according to Curtze et al. (2024)
849
852
Args:
850
853
trial: The trial for which to calculate the AP margin of stability
851
- ipsi_toe_marker : The ipsi-lateral toe marker
852
- contra_toe_marker : The contra-lateral toe marker
854
+ ipsi_heel_marker : The ipsi-lateral heel marker
855
+ contra_marker : The contra-lateral toe marker
853
856
xcom_marker: The extrapolated center of mass marker
854
857
855
858
Returns:
856
- The calculated anterior-posterior margin of stability in a dict
859
+ dict: A dictionary containing:
860
+ - "AP_margin_of_stability": The calculated anterio-posterior margin of stability.
861
+ - "AP_base_of_support": The calculated anterio-posterior base of support.
862
+ - "AP_XCOM": The calculated anterio-posterior position of the extrapolated center of mass relative to the back foot.
857
863
"""
858
864
event_times = self .get_event_times (trial .events )
859
865
860
- ipsi_toe = self ._get_marker_data (trial , ipsi_toe_marker ).sel (
866
+ ipsi_heel = self ._get_marker_data (trial , ipsi_heel_marker ).sel (
861
867
time = event_times [0 ], method = "nearest"
862
868
)
863
869
contra_toe = self ._get_marker_data (trial , contra_toe_marker ).sel (
@@ -866,37 +872,42 @@ def _calculate_ap_margin_of_stability(
866
872
xcom = self ._get_marker_data (trial , xcom_marker ).sel (
867
873
time = event_times [0 ], method = "nearest"
868
874
)
869
-
875
+
870
876
progress_axis = self ._get_progression_vector (trial )
871
877
progress_axis = linalg .normalize_vector (progress_axis )
872
-
873
- projected_ipsi = linalg .project_point_on_vector (ipsi_toe , progress_axis )
874
- projected_contra = linalg .project_point_on_vector (contra_toe , progress_axis )
875
- projected_xcom = linalg .project_point_on_vector (xcom , progress_axis )
876
-
877
- bos_len = linalg .calculate_distance (projected_ipsi , projected_contra ).values
878
- xcom_len = linalg .calculate_distance (projected_contra , projected_xcom ).values
879
-
880
- mos = bos_len - xcom_len
881
-
882
- return {"AP_margin_of_stability" : mos }
883
-
884
- def _calculate_ml_margin_of_stability (
885
- self ,
886
- trial : model .Trial ,
887
- ipsi_ankle_marker : mapping .MappedMarkers ,
888
- contra_ankle_marker : mapping .MappedMarkers ,
889
- xcom_marker : mapping .MappedMarkers ,
890
- ) -> dict [str , np .ndarray ]:
891
- """Calculate the medio-lateral margin of stability at heel strike
878
+
879
+ front_marker = linalg .get_point_in_front (ipsi_heel , contra_toe , progress_axis )
880
+ back_marker = linalg .get_point_behind (ipsi_heel , contra_toe , progress_axis )
881
+
882
+ bos_vect = front_marker - back_marker
883
+ xcom_vect = xcom - back_marker
884
+
885
+ bos_proj = abs (linalg .signed_projection_norm (bos_vect , progress_axis ))
886
+ xcom_proj = linalg .signed_projection_norm (xcom_vect , progress_axis )
887
+ mos = bos_proj - xcom_proj
888
+
889
+ return {"AP_margin_of_stability" : mos ,
890
+ "AP_base_of_support" : bos_proj ,
891
+ "AP_xcom" : xcom_proj }
892
+
893
+ def _calculate_ml_margin_of_stability (self ,
894
+ trial : model .Trial ,
895
+ ipsi_ankle_marker : mapping .MappedMarkers ,
896
+ contra_ankle_marker : mapping .MappedMarkers ,
897
+ xcom_marker : mapping .MappedMarkers
898
+ ) -> dict [str , np .ndarray ]:
899
+ """Calculate the medio-lateral margin of stability at heel strike. Result should be interpreted according to Curtze et al. (2024)
892
900
Args:
893
- trial: The trial for which to calculate the AP margin of stability
894
- ipsi_ankle_marker : The ipsi-lateral lateral ankle marker
895
- contra_ankle_marker : The contra-lateral lateral ankle marker
901
+ trial: The trial for which to calculate the ml margin of stability
902
+ ipsi_toe_marker : The ipsi-lateral lateral ankle marker
903
+ contra_marker : The contra-lateral lateral ankle marker
896
904
xcom_marker: The extrapolated center of mass marker
897
905
898
906
Returns:
899
- The calculated anterio-posterior margin of stability in a dict
907
+ dict: A dictionary containing:
908
+ - "ML_margin_of_stability": The calculated medio-lateral margin of stability.
909
+ - "ML_base_of_support": The calculated medio-lateral base of support.
910
+ - "ML_xcom": The calculated medio-lateral position of the extrapolated center of mass relative to the back foot.
900
911
"""
901
912
event_times = self .get_event_times (trial .events )
902
913
@@ -908,18 +919,27 @@ def _calculate_ml_margin_of_stability(
908
919
)
909
920
xcom = self ._get_marker_data (trial , xcom_marker ).sel (
910
921
time = event_times [0 ], method = "nearest"
911
- )
922
+ )
912
923
913
924
sagittal_axis = self ._get_sagittal_vector (trial )
914
925
sagittal_axis = linalg .normalize_vector (sagittal_axis )
915
-
916
- projected_ipsi = linalg .project_point_on_vector (ipsi_ankle , sagittal_axis )
917
- projected_contra = linalg .project_point_on_vector (contra_ankle , sagittal_axis )
918
- projected_xcom = linalg .project_point_on_vector (xcom , sagittal_axis )
919
-
920
- bos_len = linalg .calculate_distance (projected_contra , projected_ipsi ).values
921
- xcom_len = linalg .calculate_distance (projected_contra , projected_xcom ).values
922
-
923
- mos = bos_len - xcom_len
924
-
925
- return {"ML_margin_of_stability" : mos }
926
+
927
+ if trial .events .attrs ["context" ] == "Left" :
928
+ #Rotate sagittal axis so it points towards the left side of the body
929
+ sagittal_axis = - sagittal_axis
930
+
931
+ # Lateral is the furthest point in the direction of the sagittal axis
932
+ lateral_point = linalg .get_point_in_front (ipsi_ankle , contra_ankle , sagittal_axis )
933
+ # Medial is the closest point in the direction of the sagittal axis
934
+ medial_point = linalg .get_point_behind (ipsi_ankle , contra_ankle , sagittal_axis )
935
+
936
+ bos_vect = lateral_point - medial_point
937
+ xcom_vect = xcom - medial_point
938
+
939
+ bos_proj = abs (linalg .signed_projection_norm (bos_vect , sagittal_axis ))
940
+ xcom_proj = linalg .signed_projection_norm (xcom_vect , sagittal_axis )
941
+ mos = bos_proj - xcom_proj
942
+
943
+ return {"ML_margin_of_stability" : mos ,
944
+ "ML_base_of_support" : bos_proj ,
945
+ "ML_xcom" : xcom_proj }
0 commit comments