PyComplexity-ACI 0.1.0__py3-none-any.whl
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.
- PyComplexity/__init__.py +3 -0
- PyComplexity/aci.py +195 -0
- pycomplexity_aci-0.1.0.dist-info/METADATA +110 -0
- pycomplexity_aci-0.1.0.dist-info/RECORD +7 -0
- pycomplexity_aci-0.1.0.dist-info/WHEEL +5 -0
- pycomplexity_aci-0.1.0.dist-info/licenses/LICENSE +21 -0
- pycomplexity_aci-0.1.0.dist-info/top_level.txt +1 -0
PyComplexity/__init__.py
ADDED
PyComplexity/aci.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Version
|
|
2
|
+
__author__ = 'Cyrille E. Mvomo, https://github.com/cyrillemvomo/PyComplexity'
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__license__ = "MIT"
|
|
5
|
+
|
|
6
|
+
# Importations
|
|
7
|
+
import numpy as np
|
|
8
|
+
import plotly.graph_objects as go
|
|
9
|
+
from scipy.interpolate import make_interp_spline
|
|
10
|
+
|
|
11
|
+
# Required function
|
|
12
|
+
def makestatelocal(signal, hc, n_dim=5, delay=10):
|
|
13
|
+
"""Get Delay-embedded state space.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
signal : array_like, shape (T,) (acceleration magnitude recommended, see references at https://github.com/cyrillemvomo/PyComplexity)
|
|
18
|
+
1D signal.
|
|
19
|
+
hc : array_like, shape (n_strides,)
|
|
20
|
+
Heel strike indices (samples).
|
|
21
|
+
n_dim : int, default=5 (use GFNN to determine based on your data, see https://github.com/cyrillemvomo/PyComplexity)
|
|
22
|
+
Embedding dimension.
|
|
23
|
+
delay : int, default=10 (use AMI to determine based on your data, see https://github.com/cyrillemvomo/PyComplexity)
|
|
24
|
+
Delay in samples.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
state : ndarray, shape ( (n_strides-1)*100 - delay*(n_dim-1), n_dim )
|
|
29
|
+
Delay-embedded state space.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
signal = np.asarray(signal, dtype=np.float64).reshape(-1)
|
|
33
|
+
hc = np.asarray(hc, dtype=np.int64).reshape(-1)
|
|
34
|
+
|
|
35
|
+
# bounds check
|
|
36
|
+
if hc.min() < 0 or hc.max() >= signal.size:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"hc indices out of bounds after conversion. "
|
|
39
|
+
f"min={hc.min()}, max={hc.max()}, len(signal)={signal.size}. "
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
n_strides = int(hc.size)
|
|
43
|
+
n_samples = int((n_strides - 1) * 100)
|
|
44
|
+
|
|
45
|
+
start = int(hc[0])
|
|
46
|
+
stop_inclusive = int(hc[-1])
|
|
47
|
+
signal_new = signal[start:stop_inclusive + 1]
|
|
48
|
+
|
|
49
|
+
t_new = np.arange(1, signal_new.size + 1, dtype=np.float64)
|
|
50
|
+
|
|
51
|
+
t_interp = (np.arange(1, n_samples + 1, dtype=np.float64) / n_samples) * t_new[-1]
|
|
52
|
+
|
|
53
|
+
spline = make_interp_spline(t_new, signal_new, k=3)
|
|
54
|
+
signal_interp = spline(t_interp).astype(np.float64)
|
|
55
|
+
|
|
56
|
+
# state space
|
|
57
|
+
out_len = n_samples - delay * (n_dim - 1)
|
|
58
|
+
if out_len <= 0:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Requested embedding is too long: out_len={out_len}. "
|
|
61
|
+
f"Reduce n_dim and/or delay."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
state = np.empty((out_len, n_dim), dtype=np.float64)
|
|
65
|
+
|
|
66
|
+
for i_dim in range(n_dim):
|
|
67
|
+
start_idx = i_dim * delay
|
|
68
|
+
end_cut = (n_dim - 1 - i_dim) * delay
|
|
69
|
+
col = signal_interp[start_idx:] if end_cut == 0 else signal_interp[start_idx:-end_cut]
|
|
70
|
+
state[:, i_dim] = col
|
|
71
|
+
|
|
72
|
+
return state
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Main function
|
|
77
|
+
def compute_aci(signal, hc, n_dim=5, delay=10, ws=12, fs=100, period=1, min_val= 5, plot=False):
|
|
78
|
+
"""Get ACI (plot divergence curves optional).
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
signal : array_like, shape (T,) (acceleration magnitude recommended, see references at https://github.com/cyrillemvomo/PyComplexity)
|
|
83
|
+
1D signal used to get delay-embedded state space.
|
|
84
|
+
hc : array_like, shape (n_strides,)
|
|
85
|
+
Heel strike indices (samples) used to get delay-embedded state space. (recommended ~70 steady state consecutive strides minimum, see references at https://github.com/cyrillemvomo/PyComplexity)
|
|
86
|
+
n_dim : int, default=5 (use GFNN to determine based on your data, see https://github.com/cyrillemvomo/PyComplexity)
|
|
87
|
+
Embedding dimension used to get delay-embedded state space.
|
|
88
|
+
delay : int, default=10 (use AMI to determine based on your data, see https://github.com/cyrillemvomo/PyComplexity)
|
|
89
|
+
Delay in samples used to get delay-embedded state space.
|
|
90
|
+
ws : int, default=12
|
|
91
|
+
Upper bound of the window size to track the divergence (gait cycles) (recommended ~5-12 see references at https://github.com/cyrillemvomo/PyComplexity)
|
|
92
|
+
fs : int, default=10
|
|
93
|
+
Sample frequency of the resampled data (100 frames per stride to control for differences in gait speed in case of comparisons)
|
|
94
|
+
period : int, default=1
|
|
95
|
+
Period of the resampled signal is 1 (i.e. 1 stride every 100 samples)
|
|
96
|
+
min_val : int, default=5
|
|
97
|
+
Lower bound of the window size to track the divergence (gait cycles) (recommended ~5-12 see references at https://github.com/cyrillemvomo/PyComplexity)
|
|
98
|
+
plot : bool, default=False
|
|
99
|
+
Divergence curve with ACI fit.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
aci : float
|
|
104
|
+
ACI value found.
|
|
105
|
+
|
|
106
|
+
Example
|
|
107
|
+
--------
|
|
108
|
+
>>> # ACI computation and plot divergence curve fit:
|
|
109
|
+
>>> from PyComplexity import compute_aci
|
|
110
|
+
>>> compute_aci(acc_magnitude, hc_frames, n_dim=5, delay=10, ws=12, fs=100, period=1, min_val= 5, plot=True)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# avoid common errors
|
|
115
|
+
if hc.size < 2:
|
|
116
|
+
raise ValueError("hc must contain at least 2 heel strikes.")
|
|
117
|
+
if n_dim < 1:
|
|
118
|
+
raise ValueError("n_dim must be >= 1")
|
|
119
|
+
if delay < 0:
|
|
120
|
+
raise ValueError("delay must be >= 0")
|
|
121
|
+
if (ws > min_val) and (ws < len(hc)+5):
|
|
122
|
+
raise ValueError("Make sure that (ws > min_val) and (ws < ~len(hc)+10)")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# get divergence
|
|
126
|
+
ws_samp = int(np.round(ws * fs))
|
|
127
|
+
|
|
128
|
+
state = makestatelocal(signal, hc, n_dim, delay)
|
|
129
|
+
m, n = state.shape
|
|
130
|
+
|
|
131
|
+
state_ext = np.vstack([state, np.full((ws_samp, n), np.nan)])
|
|
132
|
+
divergence_mat = np.full((m, ws_samp), np.nan)
|
|
133
|
+
difference = np.full((m + ws_samp, n), np.nan)
|
|
134
|
+
|
|
135
|
+
for i_t in range(m):
|
|
136
|
+
for i_d in range(n):
|
|
137
|
+
difference[:, i_d] = (state_ext[:, i_d] - state_ext[i_t, i_d]) ** 2
|
|
138
|
+
|
|
139
|
+
start_index = int(np.round(max(0, i_t - np.round(0.5 * period * fs))))
|
|
140
|
+
stop_index = int(np.round(min(m - 1, i_t + np.round(0.5 * period * fs))))
|
|
141
|
+
difference[start_index:stop_index + 1, :] = np.nan
|
|
142
|
+
difference[i_t, :] = np.nan
|
|
143
|
+
|
|
144
|
+
dist_sum = np.sum(difference, axis=1)
|
|
145
|
+
index = int(np.nanargmin(dist_sum))
|
|
146
|
+
|
|
147
|
+
diff_vec = state_ext[i_t:i_t + ws_samp, :] - state_ext[index:index + ws_samp, :]
|
|
148
|
+
divergence_mat[i_t, :] = np.sqrt(np.sum(diff_vec ** 2, axis=1))
|
|
149
|
+
|
|
150
|
+
divergence = np.nanmean(np.log(divergence_mat), axis=0)
|
|
151
|
+
|
|
152
|
+
# linear fits
|
|
153
|
+
|
|
154
|
+
if ws_samp > min_val * period * fs:
|
|
155
|
+
L2 = int(np.round(min_val * period * fs))
|
|
156
|
+
t_long = np.arange(L2 + 1, ws_samp + 1) / fs
|
|
157
|
+
Pl = np.polyfit(t_long, divergence[L2:], 1)
|
|
158
|
+
else:
|
|
159
|
+
L2 = None
|
|
160
|
+
Pl = np.array([np.nan, np.nan])
|
|
161
|
+
|
|
162
|
+
aci = Pl[0]
|
|
163
|
+
|
|
164
|
+
# Plotly plot
|
|
165
|
+
if plot:
|
|
166
|
+
t_all = np.arange(1, ws_samp + 1) / fs
|
|
167
|
+
|
|
168
|
+
fig = go.Figure()
|
|
169
|
+
|
|
170
|
+
fig.add_trace(go.Scatter(
|
|
171
|
+
x=t_all,
|
|
172
|
+
y=divergence,
|
|
173
|
+
mode="lines",
|
|
174
|
+
name="Divergence curve"
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
if not np.isnan(Pl[0]):
|
|
178
|
+
fig.add_trace(go.Scatter(
|
|
179
|
+
x=t_long,
|
|
180
|
+
y=np.polyval(Pl, t_long),
|
|
181
|
+
mode="lines",
|
|
182
|
+
name=f"Lambda L = {Pl[0]:.4f}"
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
fig.update_layout(
|
|
186
|
+
title="Divergence curve",
|
|
187
|
+
xaxis_title="Stride Number",
|
|
188
|
+
yaxis_title="Ln(divergence)",
|
|
189
|
+
template="simple_white",
|
|
190
|
+
hovermode="x unified"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
fig.show()
|
|
194
|
+
|
|
195
|
+
return aci
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyComplexity-ACI
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python module for computing the Attractor Complexity Index (ACI) from lower-back accelerometer time-series during gait.
|
|
5
|
+
Author-email: "Cyrille E. Mvomo" <cyrille.mvomo@mail.mcgill.ca>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cyrillemvomo/PyComplexity
|
|
8
|
+
Project-URL: Zenodo, https://doi.org/10.5281/zenodo.19324133
|
|
9
|
+
Keywords: gait,biomarker,accelerometry,lyapunov exponent,non-linear dynamics,neural control of locomotion,wearable sensors
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy>=1.26.4
|
|
18
|
+
Requires-Dist: scipy>=1.11.4
|
|
19
|
+
Provides-Extra: viz
|
|
20
|
+
Requires-Dist: plotly>=5.18; extra == "viz"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="src/PyComplexity/Logo.jpg" alt="PyComplexity Logo" width="1000"/>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
# Python module for computing the Attractor Complexity Index (ACI), a non-linear measure of gait automaticity
|
|
28
|
+
|
|
29
|
+
[](https://doi.org/10.5281/zenodo.19324133) <!-- omit in toc -->
|
|
30
|
+
|
|
31
|
+
The Attractor Complexity Index (ACI) corresponds to the long-term largest Lyapunov exponent derived from gait dynamics. Initially introduced as a measure of gait stability, it is now increasingly recognized as a marker of gait automaticity across conditions of graded complexity.
|
|
32
|
+
|
|
33
|
+
ACI quantifies the divergence of trajectories in the reconstructed state space, capturing deviations from stereotyped locomotor patterns. These deviations may arise from external perturbations (e.g., auditory cueing) or internal disturbances (e.g., neural dysfunction). As such, ACI provides a compact representation of how gait departs from fully automated control.
|
|
34
|
+
|
|
35
|
+
This measure is highly promising in geriatrics and neurological populations, where it can serve as a behavioural proxy of alterations in underlying gait control networks.
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="src/PyComplexity/Description_Illustration.jpg" alt="Workflow Illustration" width="1000"/>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
PyComplexity implements a suite of core functions commonly used in the estimation of Lyapunov exponents in gait analysis. The current implementation is adapted from the public MATLAB codebase (https://github.com/SjoerdBruijn/LocalDynamicStability).
|
|
42
|
+
|
|
43
|
+
This library translates these methods into Python to facilitate broader access to ACI computation in an open-source environment, particularly for researchers with limited resources or technical background.
|
|
44
|
+
|
|
45
|
+
The current version provides a minimal and reproducible pipeline for deriving ACI from lower-back accelerometer time-series during gait, using fixed embedding parameters.
|
|
46
|
+
|
|
47
|
+
Planned updates for Summer 2026 will extend the framework to include data-driven phase space reconstruction, with Python implementations of Global False Nearest Neighbours for optimal embedding dimension selection and Average Mutual Information for time delay estimation. These additions will be based on the following MATLAB implementation (https://github.com/danm0nster/mdembedding).
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
pip install PyComplexity
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# ACI computation and plot divergence curve fit:
|
|
60
|
+
from PyComplexity import compute_aci
|
|
61
|
+
compute_aci(acc_magnitude, hc_frames, n_dim=5, delay=10, ws=12, fs=100, period=1, min_val= 5, plot=True)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## How to cite
|
|
67
|
+
|
|
68
|
+
If you use this package, please cite (mandatory):
|
|
69
|
+
|
|
70
|
+
Mvomo, C. E. (2025). PyComplexity: A Python module for computing the Attractor Complexity Index (ACI) from lower-back accelerometer time-series during gait (Version v0.1.0). Zenodo. http://doi.org/10.5281/zenodo.19324133
|
|
71
|
+
|
|
72
|
+
and:
|
|
73
|
+
|
|
74
|
+
Mvomo, C. E., Njiki, J. B. S., Leibovich, D., Guedes, C., Potvin-Desrochers, A., Dixon, P. C., Awai, C. E., & Paquette, C. (2026). Gait-Related Digital Mobility Outcomes in Parkinson’s Disease: New Insights into Convergent Validity? medRxiv. https://doi.org/10.64898/2026.03.07.26347847
|
|
75
|
+
|
|
76
|
+
You can also cite BibTeX (optional):
|
|
77
|
+
```bibtex
|
|
78
|
+
@software{cyrillemvomo_2025_19324133,
|
|
79
|
+
author = {Cyrille E. Mvomo},
|
|
80
|
+
title = {PyComplexity: A Python module for computing the Attractor Complexity Index (ACI) from lower-back accelerometer time-series during gait},
|
|
81
|
+
month = July,
|
|
82
|
+
year = 2025,
|
|
83
|
+
publisher = {Zenodo},
|
|
84
|
+
version = {v0.1.0},
|
|
85
|
+
doi = {10.5281/zenodo.19324133},
|
|
86
|
+
url = {https://doi.org/10.5281/zenodo.19324133}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## References
|
|
91
|
+
|
|
92
|
+
Mvomo, C. E., Njiki, J. B. S., Leibovich, D., Guedes, C., Potvin-Desrochers, A., Dixon, P. C., Awai, C. E., & Paquette, C. (2026). Gait-Related Digital Mobility Outcomes in Parkinson’s Disease: New Insights into Convergent Validity? medRxiv. https://doi.org/10.64898/2026.03.07.26347847
|
|
93
|
+
|
|
94
|
+
Piergiovanni, S., & Terrier, P. (2024a). Effects of metronome walking on long-term attractor divergence and correlation structure of gait. Scientific Reports, 14, 15784. https://doi.org/10.1038/s41598-024-65662-5
|
|
95
|
+
|
|
96
|
+
Piergiovanni, S., & Terrier, P. (2024b). Validity of linear and nonlinear measures of gait variability using a single lower back accelerometer. Sensors, 24(23), 7427. https://doi.org/10.3390/s24237427
|
|
97
|
+
|
|
98
|
+
Rosenstein, M. T., Collins, J. J., & De Luca, C. J. (1993). A practical method for calculating largest Lyapunov exponents from small data sets. Physica D, 65(1–2), 117–134
|
|
99
|
+
|
|
100
|
+
Terrier, P. (2019). The attractor complexity index is sensitive to gait synchronization. PeerJ, 7, e7417
|
|
101
|
+
|
|
102
|
+
Bruijn, S. M. (2023). LocalDynamicStability MATLAB implementation. Zenodo. http://doi.org/10.5281/zenodo.7593972
|
|
103
|
+
|
|
104
|
+
Wallot, S., & Mønster, D. (2018). Average mutual information and false-nearest neighbors for embedding. Frontiers in Psychology, 9, 1679
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Contact
|
|
108
|
+
|
|
109
|
+
Cyrille E. Mvomo, PhD Candidate at McGill (Canada) and Lake Lucerne Institute (Switzerland)
|
|
110
|
+
cyrille.mvomo@mail.mcgill.ca
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
PyComplexity/__init__.py,sha256=40ErgF_47PpY_PucoeYimeaCHh9DniQpI7wJhn1VacY,51
|
|
2
|
+
PyComplexity/aci.py,sha256=KmlXVcgjEDRepVMoYfufLJqrHGqPet3jTqJturIoPOw,6893
|
|
3
|
+
pycomplexity_aci-0.1.0.dist-info/licenses/LICENSE,sha256=xNk8Zf5ZBu-ftXfVeGEI6jyTHHN4ZG8fwIK06dM2Iag,1069
|
|
4
|
+
pycomplexity_aci-0.1.0.dist-info/METADATA,sha256=Vzs5OqRww6lWpDmw8eqNtPT___qUD9GrxMpWMWZh7Cg,6104
|
|
5
|
+
pycomplexity_aci-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
pycomplexity_aci-0.1.0.dist-info/top_level.txt,sha256=3CeL23nXzCbTWHY1_Bl0HdRorEZuT19wOc2fDH0wWLA,13
|
|
7
|
+
pycomplexity_aci-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 cyrillemvomo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyComplexity
|