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.
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .aci import compute_aci
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
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19324133.svg)](https://doi.org/10.5281/zenodo.19324133) ![visitors](https://visitor-badge.laobi.icu/badge?page_id=cyrillemvomo.PyComplexity)<!-- 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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