tc-cwi 0.1.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.

Potentially problematic release.


This version of tc-cwi might be problematic. Click here for more details.

tc_cwi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zensor
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.
tc_cwi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: tc-cwi
3
+ Version: 0.1.0
4
+ Summary: Python toolbox to apply EOC compensation algorithms to ultrasonic testing data.
5
+ Keywords: coda wave interferometry,structural health monitoring,temperature compensation,time-stretching,ultrasonic
6
+ Author: Arnaud Deraemaeker (ULB), Damiano Gianotti (Zensor), Jasmien Hassanin (IZFP), Jitendra Kumar Sharma (ULB), Luigi Trezza (Airbus)
7
+ Author-email: Arnaud Deraemaeker (ULB) <Arnaud.Deraemaeker@ulb.be>, Damiano Gianotti (Zensor) <damiano.gianotti@zensor.be>, Jitendra Kumar Sharma (ULB) <jitendra.kumar.sharma@ulb.be>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Requires-Dist: numpy
15
+ Requires-Dist: scipy
16
+ Maintainer: Zensor
17
+ Maintainer-email: Zensor <support@zensor.be>
18
+ Requires-Python: >=3.11
19
+ Project-URL: Repository, https://github.com/Zensor-BE/tc-cwi
20
+ Project-URL: Issues, https://github.com/Zensor-BE/tc-cwi/issues
21
+ Description-Content-Type: text/markdown
22
+
23
+ # `tc-cwi`
24
+
25
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
26
+ [![Build System](https://img.shields.io/badge/build-uv__build-blueviolet)](https://github.com/astral-sh/uv)
27
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Zensor-BE/tc-cwi/blob/main/LICENSE)
28
+
29
+ Lightweight Python toolbox for **Environmental and Operational Condition (EoC) compensation** in ultrasonic Structural Health Monitoring (SHM), using **Coda Wave Interferometry (CWI)** and the **Time-Stretching method**.
30
+
31
+ Temperature variations change the apparent wave velocity of ultrasonic guided waves, which can mask structural damage. `tc-cwi` estimates the stretch factor (`eps`) and/or time shift (`dt`) that best realign a measured signal with a reference, and reports the remaining decorrelation.
32
+
33
+ Developed as part of WP3, Deliverable D3.3.
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ Requirements: **Python ≥ 3.11** (dependencies: `numpy`, `scipy`)
40
+
41
+ ```bash
42
+ pip install tc-cwi
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ import numpy as np
51
+ from tc_cwi import tc_cwi
52
+
53
+ t = np.linspace(0, 1e-3, 1000)
54
+ reference = np.sin(2 * np.pi * 50e3 * t)
55
+ current = np.sin(2 * np.pi * 50e3 * t * (1 + 1e-4)) # slightly stretched
56
+
57
+ # Correlation coefficient without compensation (one value per signal)
58
+ baseline = tc_cwi("getccns", reference, current)
59
+ print(f"Baseline CC: {baseline.CC[0]:.4f}")
60
+
61
+ # Optimal stretch factor (Nelder-Mead from eps = 0) and remaining decorrelation
62
+ result = tc_cwi("findeps", reference, current, t, 0.0, 2)
63
+ print(f"eps: {result.eps:.6e}, decorrelation after stretching: {result.DC:.2e}")
64
+ ```
65
+
66
+ Several signals can be processed at once by passing a 2D array with **one signal per column** (`n_samples × n_signals`):
67
+
68
+ ```python
69
+ signals = np.column_stack([current, current, current])
70
+ batch = tc_cwi("findeps", reference, signals, t, (-1e-2, 1e-2)) # bounded search
71
+ print(batch.EPSf, batch.DCsf)
72
+ ```
73
+
74
+ ---
75
+
76
+ ## API
77
+
78
+ The package exposes two layers.
79
+
80
+ ### MATLAB-compatible dispatch: `tc_cwi(cmd, *args, **kwargs)`
81
+
82
+ Mirrors the original MATLAB toolbox. Results are returned as a `Struct` (`types.SimpleNamespace`), so fields are accessed MATLAB-style with `out.field`.
83
+
84
+ | Command | Arguments | Output fields (single signal / multiple signals) |
85
+ |---|---|---|
86
+ | `getccns` | `y0, y1, ind=None` | `CC`, `DC` |
87
+ | `getccs` | `y0, y1, t, eps, ind=None` | `CC`, `DC` |
88
+ | `getccdt` | `y0, y1, t, dt, ind=None` | `CC`, `DC` |
89
+ | `getccepsdt` | `y0, y1, t, eps, dt, ind=None` | `CC`, `DC` |
90
+ | `getdcs` / `getdcdt` / `getdcepsdt` | same as `getcc*` | `DC` array (returned directly) |
91
+ | `findeps` | `y1, y2, t, e0, method=1, ind=None` | `eps, DC, y, y0` / `EPSf, DCsf, ysf, y0` |
92
+ | `finddt` | `y1, y2, t, dt0, method=1, ind=None` | `dt, DC, y, y0` / `DTf, DCdtf, ydtf, y0` |
93
+ | `findepsdt` | `y1, y2, t, e0, dt0, method=2, ind=None` | `eps, dt, DC, y, y0` / `EPSf, DTf, DCsdtf, ysdtf, y0` |
94
+ | `findeps -ma` | `y1, y2, t, e0, method=1, ind=None, ma=5` | multiple signals only: `EPSma, DCma, ysma, y0, yma` |
95
+ | `stretch` | `y, t, eps` | stretched signal |
96
+ | `shift` | `y, t, dt, method="interp"` | shifted signal (`"point"`, `"interp"` or `"fft"`) |
97
+ | `stretchshift` | `y, t, eps, dt` | shifted, then stretched signal |
98
+
99
+ Conventions:
100
+
101
+ - **`CC`** is the absolute Pearson correlation coefficient, **`DC = 1 - CC`** the decorrelation.
102
+ - **`ind`** restricts the correlation to a window (index array or slice). The stretch/shift is always applied to the full signal.
103
+ - **`method`** selects the optimizer:
104
+ - `1`: bounded scalar search, the start value is an interval `(lower, upper)`.
105
+ - `2`: Nelder-Mead, the start value is a starting point. `findepsdt` only supports this method.
106
+
107
+ When processing several signals, each fit is seeded with the result of the previous one.
108
+ - **`eps > 0`** compresses the wave (`t → t·(1+eps)`), **`dt > 0`** moves it forward (`t → t−dt`).
109
+ - **`findeps -ma`** compares each signal against the mean of the last `ma` compensated signals instead of a fixed reference. `ma` is either a window size, or the `yma` returned by a previous call to continue the same window.
110
+
111
+ ### Core functions
112
+
113
+ For direct use in Python code, without the MATLAB-style dispatch:
114
+
115
+ | Function | Description |
116
+ |---|---|
117
+ | `correlation_coefficient(y0, y1)` | Absolute correlation coefficient between two signals |
118
+ | `stretch(y, t, eps)` | Stretch a signal (not-a-knot cubic spline, zero outside `t`) |
119
+ | `shift(y, t, dt, method="interp")` | Shift a signal by `dt` |
120
+ | `stretchshift(y, t, eps, dt)` | Shift, then stretch a signal |
121
+ | `find_optimum(y0, y, t, process, x0, method=1, ind=None, xatol=1e-8)` | Parameters of `process(y, t, *x)` minimizing the decorrelation with `y0`; returns `(x, decorrelation)` |
122
+ | `MovingAverageReference(n, y_init=None)` | Rolling window of the last `n` signals; `.reference` is their mean, `.update(y)` adds a signal |
123
+
124
+ ```python
125
+ from tc_cwi import find_optimum, stretch
126
+
127
+ x, decorrelation = find_optimum(reference, current, t, stretch, (-1e-2, 1e-2))
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Development
133
+
134
+ This project uses [`uv`](https://github.com/astral-sh/uv) with the `uv_build` backend:
135
+
136
+ ```bash
137
+ git clone https://github.com/Zensor-BE/tc-cwi.git
138
+ cd tc-cwi
139
+ uv sync
140
+ ```
141
+
142
+ ```text
143
+ ├── src/tc_cwi/
144
+ │ ├── __init__.py # Public API
145
+ │ ├── core.py # Stretching, shifting and optimization
146
+ │ └── matlab.py # MATLAB-compatible tc_cwi() dispatch
147
+ ├── tests/
148
+ │ ├── test_core.py # Core functions
149
+ │ ├── test_matlab.py # Every tc_cwi command: fields and error paths
150
+ │ └── test_package.py # Public API
151
+ └── pyproject.toml
152
+ ```
153
+
154
+ Tests, coverage and linting:
155
+
156
+ ```bash
157
+ uv run pytest
158
+ uv run pytest --cov # statements + branches, with the missing lines
159
+ uv run ruff check .
160
+ ```
161
+
162
+ Releasing to PyPI: bump `version` in `pyproject.toml`, then push a matching tag. The [publish workflow](https://github.com/Zensor-BE/tc-cwi/blob/main/.github/workflows/publish.yml) lints, builds, tests the built distributions and publishes them.
163
+
164
+ ```bash
165
+ git tag -a v0.1.0 -m v0.1.0
166
+ git push origin v0.1.0
167
+ ```
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ [MIT](https://github.com/Zensor-BE/tc-cwi/blob/main/LICENSE)
tc_cwi-0.1.0/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # `tc-cwi`
2
+
3
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
4
+ [![Build System](https://img.shields.io/badge/build-uv__build-blueviolet)](https://github.com/astral-sh/uv)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Zensor-BE/tc-cwi/blob/main/LICENSE)
6
+
7
+ Lightweight Python toolbox for **Environmental and Operational Condition (EoC) compensation** in ultrasonic Structural Health Monitoring (SHM), using **Coda Wave Interferometry (CWI)** and the **Time-Stretching method**.
8
+
9
+ Temperature variations change the apparent wave velocity of ultrasonic guided waves, which can mask structural damage. `tc-cwi` estimates the stretch factor (`eps`) and/or time shift (`dt`) that best realign a measured signal with a reference, and reports the remaining decorrelation.
10
+
11
+ Developed as part of WP3, Deliverable D3.3.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ Requirements: **Python ≥ 3.11** (dependencies: `numpy`, `scipy`)
18
+
19
+ ```bash
20
+ pip install tc-cwi
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Quickstart
26
+
27
+ ```python
28
+ import numpy as np
29
+ from tc_cwi import tc_cwi
30
+
31
+ t = np.linspace(0, 1e-3, 1000)
32
+ reference = np.sin(2 * np.pi * 50e3 * t)
33
+ current = np.sin(2 * np.pi * 50e3 * t * (1 + 1e-4)) # slightly stretched
34
+
35
+ # Correlation coefficient without compensation (one value per signal)
36
+ baseline = tc_cwi("getccns", reference, current)
37
+ print(f"Baseline CC: {baseline.CC[0]:.4f}")
38
+
39
+ # Optimal stretch factor (Nelder-Mead from eps = 0) and remaining decorrelation
40
+ result = tc_cwi("findeps", reference, current, t, 0.0, 2)
41
+ print(f"eps: {result.eps:.6e}, decorrelation after stretching: {result.DC:.2e}")
42
+ ```
43
+
44
+ Several signals can be processed at once by passing a 2D array with **one signal per column** (`n_samples × n_signals`):
45
+
46
+ ```python
47
+ signals = np.column_stack([current, current, current])
48
+ batch = tc_cwi("findeps", reference, signals, t, (-1e-2, 1e-2)) # bounded search
49
+ print(batch.EPSf, batch.DCsf)
50
+ ```
51
+
52
+ ---
53
+
54
+ ## API
55
+
56
+ The package exposes two layers.
57
+
58
+ ### MATLAB-compatible dispatch: `tc_cwi(cmd, *args, **kwargs)`
59
+
60
+ Mirrors the original MATLAB toolbox. Results are returned as a `Struct` (`types.SimpleNamespace`), so fields are accessed MATLAB-style with `out.field`.
61
+
62
+ | Command | Arguments | Output fields (single signal / multiple signals) |
63
+ |---|---|---|
64
+ | `getccns` | `y0, y1, ind=None` | `CC`, `DC` |
65
+ | `getccs` | `y0, y1, t, eps, ind=None` | `CC`, `DC` |
66
+ | `getccdt` | `y0, y1, t, dt, ind=None` | `CC`, `DC` |
67
+ | `getccepsdt` | `y0, y1, t, eps, dt, ind=None` | `CC`, `DC` |
68
+ | `getdcs` / `getdcdt` / `getdcepsdt` | same as `getcc*` | `DC` array (returned directly) |
69
+ | `findeps` | `y1, y2, t, e0, method=1, ind=None` | `eps, DC, y, y0` / `EPSf, DCsf, ysf, y0` |
70
+ | `finddt` | `y1, y2, t, dt0, method=1, ind=None` | `dt, DC, y, y0` / `DTf, DCdtf, ydtf, y0` |
71
+ | `findepsdt` | `y1, y2, t, e0, dt0, method=2, ind=None` | `eps, dt, DC, y, y0` / `EPSf, DTf, DCsdtf, ysdtf, y0` |
72
+ | `findeps -ma` | `y1, y2, t, e0, method=1, ind=None, ma=5` | multiple signals only: `EPSma, DCma, ysma, y0, yma` |
73
+ | `stretch` | `y, t, eps` | stretched signal |
74
+ | `shift` | `y, t, dt, method="interp"` | shifted signal (`"point"`, `"interp"` or `"fft"`) |
75
+ | `stretchshift` | `y, t, eps, dt` | shifted, then stretched signal |
76
+
77
+ Conventions:
78
+
79
+ - **`CC`** is the absolute Pearson correlation coefficient, **`DC = 1 - CC`** the decorrelation.
80
+ - **`ind`** restricts the correlation to a window (index array or slice). The stretch/shift is always applied to the full signal.
81
+ - **`method`** selects the optimizer:
82
+ - `1`: bounded scalar search, the start value is an interval `(lower, upper)`.
83
+ - `2`: Nelder-Mead, the start value is a starting point. `findepsdt` only supports this method.
84
+
85
+ When processing several signals, each fit is seeded with the result of the previous one.
86
+ - **`eps > 0`** compresses the wave (`t → t·(1+eps)`), **`dt > 0`** moves it forward (`t → t−dt`).
87
+ - **`findeps -ma`** compares each signal against the mean of the last `ma` compensated signals instead of a fixed reference. `ma` is either a window size, or the `yma` returned by a previous call to continue the same window.
88
+
89
+ ### Core functions
90
+
91
+ For direct use in Python code, without the MATLAB-style dispatch:
92
+
93
+ | Function | Description |
94
+ |---|---|
95
+ | `correlation_coefficient(y0, y1)` | Absolute correlation coefficient between two signals |
96
+ | `stretch(y, t, eps)` | Stretch a signal (not-a-knot cubic spline, zero outside `t`) |
97
+ | `shift(y, t, dt, method="interp")` | Shift a signal by `dt` |
98
+ | `stretchshift(y, t, eps, dt)` | Shift, then stretch a signal |
99
+ | `find_optimum(y0, y, t, process, x0, method=1, ind=None, xatol=1e-8)` | Parameters of `process(y, t, *x)` minimizing the decorrelation with `y0`; returns `(x, decorrelation)` |
100
+ | `MovingAverageReference(n, y_init=None)` | Rolling window of the last `n` signals; `.reference` is their mean, `.update(y)` adds a signal |
101
+
102
+ ```python
103
+ from tc_cwi import find_optimum, stretch
104
+
105
+ x, decorrelation = find_optimum(reference, current, t, stretch, (-1e-2, 1e-2))
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Development
111
+
112
+ This project uses [`uv`](https://github.com/astral-sh/uv) with the `uv_build` backend:
113
+
114
+ ```bash
115
+ git clone https://github.com/Zensor-BE/tc-cwi.git
116
+ cd tc-cwi
117
+ uv sync
118
+ ```
119
+
120
+ ```text
121
+ ├── src/tc_cwi/
122
+ │ ├── __init__.py # Public API
123
+ │ ├── core.py # Stretching, shifting and optimization
124
+ │ └── matlab.py # MATLAB-compatible tc_cwi() dispatch
125
+ ├── tests/
126
+ │ ├── test_core.py # Core functions
127
+ │ ├── test_matlab.py # Every tc_cwi command: fields and error paths
128
+ │ └── test_package.py # Public API
129
+ └── pyproject.toml
130
+ ```
131
+
132
+ Tests, coverage and linting:
133
+
134
+ ```bash
135
+ uv run pytest
136
+ uv run pytest --cov # statements + branches, with the missing lines
137
+ uv run ruff check .
138
+ ```
139
+
140
+ Releasing to PyPI: bump `version` in `pyproject.toml`, then push a matching tag. The [publish workflow](https://github.com/Zensor-BE/tc-cwi/blob/main/.github/workflows/publish.yml) lints, builds, tests the built distributions and publishes them.
141
+
142
+ ```bash
143
+ git tag -a v0.1.0 -m v0.1.0
144
+ git push origin v0.1.0
145
+ ```
146
+
147
+ ---
148
+
149
+ ## License
150
+
151
+ [MIT](https://github.com/Zensor-BE/tc-cwi/blob/main/LICENSE)
@@ -0,0 +1,88 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.1,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "tc-cwi"
7
+ version = "0.1.0"
8
+ description = "Python toolbox to apply EOC compensation algorithms to ultrasonic testing data."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ keywords = [
13
+ "coda wave interferometry",
14
+ "structural health monitoring",
15
+ "temperature compensation",
16
+ "time-stretching",
17
+ "ultrasonic",
18
+ ]
19
+ classifiers = [
20
+ "Intended Audience :: Science/Research",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Topic :: Scientific/Engineering :: Physics",
24
+ ]
25
+ requires-python = ">=3.11"
26
+ dependencies = [
27
+ "numpy",
28
+ "scipy",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Arnaud Deraemaeker (ULB)"
33
+ email = "Arnaud.Deraemaeker@ulb.be"
34
+
35
+ [[project.authors]]
36
+ name = "Damiano Gianotti (Zensor)"
37
+ email = "damiano.gianotti@zensor.be"
38
+
39
+ [[project.authors]]
40
+ name = "Jasmien Hassanin (IZFP)"
41
+
42
+ [[project.authors]]
43
+ name = "Jitendra Kumar Sharma (ULB)"
44
+ email = "jitendra.kumar.sharma@ulb.be"
45
+
46
+ [[project.authors]]
47
+ name = "Luigi Trezza (Airbus)"
48
+
49
+ [[project.maintainers]]
50
+ name = "Zensor"
51
+ email = "support@zensor.be"
52
+
53
+ [project.urls]
54
+ Repository = "https://github.com/Zensor-BE/tc-cwi"
55
+ Issues = "https://github.com/Zensor-BE/tc-cwi/issues"
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "pytest",
60
+ "pytest-cov",
61
+ "ruff",
62
+ ]
63
+
64
+ [tool.ruff]
65
+ target-version = "py311"
66
+
67
+ [tool.ruff.lint]
68
+ select = [
69
+ "E4",
70
+ "E7",
71
+ "E9",
72
+ "F",
73
+ "B",
74
+ "I",
75
+ "UP",
76
+ "C4",
77
+ "FURB122",
78
+ "RUF015",
79
+ "RUF022",
80
+ "RUF046",
81
+ ]
82
+
83
+ [tool.coverage.run]
84
+ source = ["tc_cwi"]
85
+ branch = true
86
+
87
+ [tool.coverage.report]
88
+ show_missing = true
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.1,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "tc-cwi"
7
+ version = "0.1.0"
8
+ description = "Python toolbox to apply EOC compensation algorithms to ultrasonic testing data."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ { name="Arnaud Deraemaeker (ULB)", email="Arnaud.Deraemaeker@ulb.be" },
14
+ { name="Damiano Gianotti (Zensor)", email="damiano.gianotti@zensor.be" },
15
+ { name="Jasmien Hassanin (IZFP)" },
16
+ { name="Jitendra Kumar Sharma (ULB)", email="jitendra.kumar.sharma@ulb.be" },
17
+ { name="Luigi Trezza (Airbus)" },
18
+ ]
19
+ maintainers = [
20
+ { name="Zensor", email="support@zensor.be" },
21
+ ]
22
+ keywords = [
23
+ "coda wave interferometry",
24
+ "structural health monitoring",
25
+ "temperature compensation",
26
+ "time-stretching",
27
+ "ultrasonic",
28
+ ]
29
+ classifiers = [
30
+ "Intended Audience :: Science/Research",
31
+ "Operating System :: OS Independent",
32
+ "Programming Language :: Python :: 3",
33
+ "Topic :: Scientific/Engineering :: Physics",
34
+ ]
35
+ requires-python = ">=3.11"
36
+ dependencies = [
37
+ "numpy",
38
+ "scipy",
39
+ ]
40
+
41
+ [project.urls]
42
+ Repository = "https://github.com/Zensor-BE/tc-cwi"
43
+ Issues = "https://github.com/Zensor-BE/tc-cwi/issues"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest",
48
+ "pytest-cov",
49
+ "ruff",
50
+ ]
51
+
52
+ # Explicit rule set: ruff's implicit defaults grow between releases, so this gate
53
+ # spells out what it checks and must not start failing on an unrelated `uv sync`.
54
+ # SIM is kept out on purpose (it prefers `T[-1] < T * 1.2` over `T * 1.2 > T[-1]`).
55
+ [tool.ruff]
56
+ target-version = "py311"
57
+
58
+ [tool.ruff.lint]
59
+ select = [
60
+ "E4", "E7", "E9", "F", # pycodestyle (errors) + pyflakes
61
+ "B", # bugbear
62
+ "I", "UP", "C4", # import order, modern syntax, comprehensions
63
+ "FURB122", "RUF015", "RUF022", "RUF046", # the few specific rules worth their noise
64
+ ]
65
+
66
+ [tool.coverage.run]
67
+ source = ["tc_cwi"]
68
+ branch = true
69
+
70
+ [tool.coverage.report]
71
+ show_missing = true
@@ -0,0 +1,20 @@
1
+ from .core import (
2
+ MovingAverageReference,
3
+ correlation_coefficient,
4
+ find_optimum,
5
+ shift,
6
+ stretch,
7
+ stretchshift,
8
+ )
9
+ from .matlab import Struct, tc_cwi
10
+
11
+ __all__ = [
12
+ "MovingAverageReference",
13
+ "Struct",
14
+ "correlation_coefficient",
15
+ "find_optimum",
16
+ "shift",
17
+ "stretch",
18
+ "stretchshift",
19
+ "tc_cwi",
20
+ ]
@@ -0,0 +1,114 @@
1
+ from collections import deque
2
+ from collections.abc import Callable
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+ from scipy.interpolate import interp1d
7
+ from scipy.optimize import minimize, minimize_scalar
8
+
9
+ FloatDType = np.dtype[np.float64]
10
+
11
+ Array1D = np.ndarray[tuple[int], FloatDType]
12
+ Array2D = np.ndarray[tuple[int, int], FloatDType]
13
+ IndexArray = np.ndarray[tuple[int], np.dtype[np.int_]]
14
+
15
+ ShiftMethodOptions = Literal["point", "interp", "fft"]
16
+
17
+
18
+ def _interp1d(y: Array1D, t: Array1D, t_new: Array1D) -> Array1D:
19
+ """Not-a-knot cubic spline (MATLAB 'spline'), 0 outside of t."""
20
+ return interp1d(
21
+ t,
22
+ y,
23
+ kind="cubic",
24
+ bounds_error=False,
25
+ fill_value=0.0,
26
+ assume_sorted=True,
27
+ )(t_new)
28
+
29
+
30
+ def correlation_coefficient(y0: Array1D, y1: Array1D) -> float:
31
+ return abs(np.corrcoef(y0, y1)[0, 1])
32
+
33
+
34
+ def stretch(y: Array1D, t: Array1D, eps: float) -> Array1D:
35
+ """Stretch the full signal, t -> t*(1+eps). eps > 0 compresses the wave."""
36
+ return _interp1d(y, t, t * (1 + eps))
37
+
38
+
39
+ def shift(
40
+ y: Array1D, t: Array1D, dt: float, method: ShiftMethodOptions = "interp"
41
+ ) -> Array1D:
42
+ """Shift the full signal, t -> t-dt. dt > 0 moves the wave forward."""
43
+ if method == "point": # round half away from zero, like MATLAB
44
+ n = int(np.copysign(np.floor(abs(dt / (t[1] - t[0])) + 0.5), dt))
45
+ return np.roll(y, n)
46
+ if method == "interp":
47
+ return _interp1d(y, t, t - dt)
48
+ if method == "fft":
49
+ freqs = np.fft.fftfreq(len(y), d=t[1] - t[0])
50
+ return np.fft.ifft(np.fft.fft(y) * np.exp(-2j * np.pi * freqs * dt)).real
51
+ raise ValueError(f"Unknown shift method: {method!r}")
52
+
53
+
54
+ def stretchshift(y: Array1D, t: Array1D, eps: float, dt: float) -> Array1D:
55
+ """Shift (interp) then stretch."""
56
+ return stretch(shift(y, t, dt), t, eps)
57
+
58
+
59
+ def find_optimum(
60
+ y0: Array1D,
61
+ y: Array1D,
62
+ t: Array1D,
63
+ process: Callable[..., Array1D],
64
+ x0: float | Array1D,
65
+ method: Literal[1, 2] = 1,
66
+ ind: IndexArray | slice | None = None,
67
+ xatol: float = 1e-8,
68
+ ) -> tuple[Array1D, float]:
69
+ """
70
+ Find the parameters x minimizing the decorrelation (1 - CC) between y0 and
71
+ process(y, t, *x). Correlation is computed over `ind` only; process is
72
+ applied to the full signal. Returns (x, decorrelation).
73
+
74
+ - method 1: bounded scalar minimization, x0 = (lower, upper)
75
+ - method 2: Nelder-Mead, x0 = starting point (one value per parameter)
76
+ """
77
+ ind = slice(None) if ind is None else ind
78
+
79
+ def decorrelation(x) -> float:
80
+ processed = process(y, t, *np.atleast_1d(x))
81
+ return 1 - correlation_coefficient(y0[ind], processed[ind])
82
+
83
+ if method == 1:
84
+ res = minimize_scalar(
85
+ decorrelation,
86
+ bounds=tuple(x0),
87
+ method="bounded",
88
+ options={"xatol": xatol},
89
+ )
90
+ else:
91
+ res = minimize(
92
+ decorrelation,
93
+ np.atleast_1d(x0),
94
+ method="Nelder-Mead",
95
+ options={"xatol": xatol, "fatol": 1e-4},
96
+ )
97
+ return np.atleast_1d(res.x), res.fun
98
+
99
+
100
+ class MovingAverageReference:
101
+ """Rolling window of the last `n` signals; their mean is the reference."""
102
+
103
+ def __init__(self, n: int, y_init: Array1D | None = None):
104
+ self.n = n
105
+ self.signals: deque[Array1D] = deque(maxlen=n)
106
+ if y_init is not None:
107
+ self.signals.append(y_init)
108
+
109
+ @property
110
+ def reference(self) -> Array1D:
111
+ return np.mean(self.signals, axis=0)
112
+
113
+ def update(self, y: Array1D) -> None:
114
+ self.signals.append(y) # the oldest signal is dropped when full
@@ -0,0 +1,163 @@
1
+ """
2
+ tc_cwi: MATLAB-compatible API
3
+ """
4
+
5
+ from types import SimpleNamespace as Struct
6
+
7
+ import numpy as np
8
+
9
+ from .core import (
10
+ MovingAverageReference,
11
+ correlation_coefficient,
12
+ find_optimum,
13
+ shift,
14
+ stretch,
15
+ stretchshift,
16
+ )
17
+
18
+
19
+ def _getcc(process, y0, y1, *params, ind=None) -> Struct:
20
+ """out.CC / out.DC; process(column, *params) is applied on the full signal,
21
+ correlation on ind only; params may be scalars or one value per column."""
22
+ ind = slice(None) if ind is None else ind
23
+ columns = y1[:, None] if y1.ndim == 1 else y1
24
+ params = [np.broadcast_to(p, columns.shape[1]) for p in params]
25
+ cc = np.array(
26
+ [
27
+ correlation_coefficient(
28
+ y0[ind], process(columns[:, i], *(p[i] for p in params))[ind]
29
+ )
30
+ for i in range(columns.shape[1])
31
+ ]
32
+ )
33
+ return Struct(CC=cc, DC=1 - cc)
34
+
35
+
36
+ def getccns(y0, y1, ind=None):
37
+ return _getcc(lambda y: y, y0, y1, ind=ind)
38
+
39
+
40
+ def getccs(y0, y1, t, eps, ind=None):
41
+ return _getcc(lambda y, e: stretch(y, t, e), y0, y1, eps, ind=ind)
42
+
43
+
44
+ def getccdt(y0, y1, t, dt, ind=None):
45
+ return _getcc(lambda y, d: shift(y, t, d), y0, y1, dt, ind=ind)
46
+
47
+
48
+ def getccepsdt(y0, y1, t, eps, dt, ind=None):
49
+ return _getcc(lambda y, e, d: stretchshift(y, t, e, d), y0, y1, eps, dt, ind=ind)
50
+
51
+
52
+ def getdcs(y0, y1, t, eps, ind=None):
53
+ return getccs(y0, y1, t, eps, ind).DC
54
+
55
+
56
+ def getdcdt(y0, y1, t, dt, ind=None):
57
+ return getccdt(y0, y1, t, dt, ind).DC
58
+
59
+
60
+ def getdcepsdt(y0, y1, t, eps, dt, ind=None):
61
+ return getccepsdt(y0, y1, t, eps, dt, ind).DC
62
+
63
+
64
+ def _find(y1, y2, t, process, x0, method, ind, xatol):
65
+ """Fit one signal or each column of y2 in turn. The starting point of each
66
+ fit comes from the previous one (method 1: interval re-centered on it).
67
+ Returns xs (n, n_params), DCs (n,) and processed signals (N, n)."""
68
+ x0 = np.asarray(x0, dtype=float)
69
+ xs, dcs, ys = [], [], []
70
+ for y in (y2[:, None] if y2.ndim == 1 else y2).T:
71
+ x, dc = find_optimum(y1, y, t, process, x0, method, ind, xatol)
72
+ x0 = x0 - x0.mean() + x if method == 1 else x
73
+ xs.append(x), dcs.append(dc), ys.append(process(y, t, *x))
74
+ return np.array(xs), np.array(dcs), np.array(ys).T
75
+
76
+
77
+ def findeps(y1, y2, t, e0, method=1, ind=None):
78
+ xs, dcs, ys = _find(y1, y2, t, stretch, e0, method, ind, 1e-8)
79
+ if y2.ndim == 1:
80
+ return Struct(eps=xs[0, 0], DC=dcs[0], y=ys[:, 0], y0=y1)
81
+ return Struct(DCsf=dcs, EPSf=xs[:, 0], ysf=ys, y0=y1)
82
+
83
+
84
+ def finddt(y1, y2, t, dt0, method=1, ind=None):
85
+ xs, dcs, ys = _find(y1, y2, t, shift, dt0, method, ind, 1e-10)
86
+ if y2.ndim == 1:
87
+ return Struct(dt=xs[0, 0], DC=dcs[0], y=ys[:, 0], y0=y1)
88
+ return Struct(DCdtf=dcs, DTf=xs[:, 0], ydtf=ys, y0=y1)
89
+
90
+
91
+ def findepsdt(y1, y2, t, e0, dt0, method=2, ind=None):
92
+ if method == 1:
93
+ raise ValueError("fminbnd is single variable, use method 2.")
94
+ x0 = [e0, dt0]
95
+ xs, dcs, ys = _find(y1, y2, t, stretchshift, x0, method, ind, 1e-8)
96
+ if y2.ndim == 1:
97
+ return Struct(eps=xs[0, 0], dt=xs[0, 1], DC=dcs[0], y=ys[:, 0], y0=y1)
98
+ return Struct(DCsdtf=dcs, DTf=xs[:, 1], EPSf=xs[:, 0], ysdtf=ys, y0=y1)
99
+
100
+
101
+ def findeps_ma(y1, y2, t, e0, method=1, ind=None, ma: MovingAverageReference | int = 5):
102
+ """
103
+ ma: window size n (MATLAB ma.init=true, window seeded with y1) or the
104
+ out.yma of a previous call (ma.init=false; y1 is then the first reference).
105
+ """
106
+ if y2.ndim == 1:
107
+ raise ValueError("not for single signal")
108
+ if isinstance(ma, int):
109
+ ma = MovingAverageReference(ma, y1)
110
+
111
+ x = np.asarray(e0, dtype=float)
112
+ dc, eps = np.zeros(y2.shape[1]), np.zeros(y2.shape[1])
113
+ ys, refs = np.zeros_like(y2), np.zeros_like(y2)
114
+ y0 = y1
115
+ for i in range(y2.shape[1]):
116
+ res = findeps(y0, y2[:, i], t, x, method, ind)
117
+ eps[i], dc[i], ys[:, i] = res.eps, res.DC, res.y
118
+ x = x - x.mean() + eps[i] if method == 1 else np.atleast_1d(eps[i])
119
+ ma.update(ys[:, i])
120
+ y0 = refs[:, i] = ma.reference
121
+ return Struct(DCma=dc, EPSma=eps, ysma=ys, y0=refs, yma=ma)
122
+
123
+
124
+ def tc_cwi(cmd: str, *args, **kwargs):
125
+ """
126
+ Time-Stretching and Coda Wave Interferometry (CWI) command dispatch.
127
+
128
+ Returns a Struct (SimpleNamespace) supporting attribute access (out.field),
129
+ mirroring MATLAB struct behavior.
130
+
131
+ Commands
132
+ --------
133
+ 'getccns' - Cross-correlation without stretching.
134
+ 'getccs' - Cross-correlation after stretching.
135
+ 'getccdt' - Cross-correlation after time shift.
136
+ 'getccepsdt' - Cross-correlation after stretch and time shift.
137
+ 'findeps' - Find optimal stretch factor eps minimising decorrelation.
138
+ 'findeps -ma' - Find optimal stretch factor eps using moving-average reference.
139
+ 'finddt' - Find optimal time shift dt minimising decorrelation.
140
+ 'findepsdt' - Joint optimization for eps and dt.
141
+ 'stretch' - Apply stretch factor eps to signal(s).
142
+ 'shift' - Apply time shift dt to signal(s).
143
+ 'stretchshift' - Apply time shift then stretch to signal(s).
144
+ """
145
+ dispatch = {
146
+ "finddt": finddt,
147
+ "findeps": findeps,
148
+ "findeps -ma": findeps_ma,
149
+ "findepsdt": findepsdt,
150
+ "getccdt": getccdt,
151
+ "getccepsdt": getccepsdt,
152
+ "getccns": getccns,
153
+ "getccs": getccs,
154
+ "getdcdt": getdcdt,
155
+ "getdcepsdt": getdcepsdt,
156
+ "getdcs": getdcs,
157
+ "shift": shift,
158
+ "stretch": stretch,
159
+ "stretchshift": stretchshift,
160
+ }
161
+ if cmd not in dispatch:
162
+ raise ValueError(f"Unknown command: '{cmd}'")
163
+ return dispatch[cmd](*args, **kwargs)