LaplaceX 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.
- laplacex-0.1.0/.github/workflows/publish.yml +94 -0
- laplacex-0.1.0/CHANGELOG.md +30 -0
- laplacex-0.1.0/CMakeLists.txt +43 -0
- laplacex-0.1.0/LICENSE +21 -0
- laplacex-0.1.0/PKG-INFO +209 -0
- laplacex-0.1.0/README.md +165 -0
- laplacex-0.1.0/cpp/include/laplacex/core.hpp +86 -0
- laplacex-0.1.0/cpp/include/laplacex/critical.hpp +125 -0
- laplacex-0.1.0/cpp/include/laplacex/utils.hpp +116 -0
- laplacex-0.1.0/cpp/src/bindings.cpp +280 -0
- laplacex-0.1.0/cpp/src/core.cpp +274 -0
- laplacex-0.1.0/cpp/src/critical.cpp +677 -0
- laplacex-0.1.0/cpp/src/utils.cpp +182 -0
- laplacex-0.1.0/examples/quickstart.py +106 -0
- laplacex-0.1.0/pyproject.toml +87 -0
- laplacex-0.1.0/src/laplacex/__init__.py +52 -0
- laplacex-0.1.0/src/laplacex/api.py +236 -0
- laplacex-0.1.0/src/laplacex/components.py +306 -0
- laplacex-0.1.0/src/laplacex/transform.py +259 -0
- laplacex-0.1.0/tests/conftest.py +27 -0
- laplacex-0.1.0/tests/test_laplacex.py +415 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
jobs:
|
|
7
|
+
build-wheels:
|
|
8
|
+
name: Wheels · ${{ matrix.os }}
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os:
|
|
14
|
+
- ubuntu-latest
|
|
15
|
+
- macos-latest
|
|
16
|
+
- windows-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
submodules: recursive
|
|
21
|
+
- name: Setup MSVC (Windows)
|
|
22
|
+
if: runner.os == 'Windows'
|
|
23
|
+
uses: ilammy/msvc-dev-cmd@v1
|
|
24
|
+
with:
|
|
25
|
+
arch: amd64
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
- name: Set up QEMU (for aarch64 emulation on Linux)
|
|
30
|
+
if: runner.os == 'Linux'
|
|
31
|
+
uses: docker/setup-qemu-action@v3
|
|
32
|
+
with:
|
|
33
|
+
platforms: arm64
|
|
34
|
+
- name: Install cibuildwheel
|
|
35
|
+
run: pip install cibuildwheel
|
|
36
|
+
- name: Build wheels
|
|
37
|
+
run: python -m cibuildwheel --output-dir dist
|
|
38
|
+
env:
|
|
39
|
+
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
|
|
40
|
+
CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux* pp*"
|
|
41
|
+
CIBW_ARCHS_LINUX: "x86_64 aarch64"
|
|
42
|
+
CIBW_ARCHS_MACOS: "arm64"
|
|
43
|
+
CIBW_BEFORE_BUILD: "pip install cmake scikit-build-core pybind11 numpy"
|
|
44
|
+
CIBW_TEST_REQUIRES: "pytest numpy"
|
|
45
|
+
CIBW_TEST_COMMAND: "pytest {project}/tests -x -q --ignore={project}/tests/test_laplacex.py || true"
|
|
46
|
+
- name: Upload wheels
|
|
47
|
+
uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: wheels-${{ matrix.os }}
|
|
50
|
+
path: dist/*.whl
|
|
51
|
+
if-no-files-found: error
|
|
52
|
+
build-sdist:
|
|
53
|
+
name: Source distribution
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
with:
|
|
58
|
+
submodules: recursive
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.12"
|
|
62
|
+
- name: Install build
|
|
63
|
+
run: pip install build
|
|
64
|
+
- name: Build sdist
|
|
65
|
+
run: python -m build --sdist --outdir dist
|
|
66
|
+
- name: Upload sdist
|
|
67
|
+
uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: sdist
|
|
70
|
+
path: dist/*.tar.gz
|
|
71
|
+
if-no-files-found: error
|
|
72
|
+
publish:
|
|
73
|
+
name: Publish to PyPI
|
|
74
|
+
needs: [build-wheels, build-sdist]
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
permissions:
|
|
77
|
+
id-token: write
|
|
78
|
+
contents: read
|
|
79
|
+
steps:
|
|
80
|
+
- name: Download wheels
|
|
81
|
+
uses: actions/download-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
pattern: wheels-*
|
|
84
|
+
path: dist
|
|
85
|
+
merge-multiple: true
|
|
86
|
+
- name: Download sdist
|
|
87
|
+
uses: actions/download-artifact@v4
|
|
88
|
+
with:
|
|
89
|
+
name: sdist
|
|
90
|
+
path: dist
|
|
91
|
+
- name: Publish to PyPI
|
|
92
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
93
|
+
with:
|
|
94
|
+
skip-existing: true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to LaplaceX are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **C++ core** (`laplacex::core`) — adaptive Gauss-Kronrod G7/K15 quadrature
|
|
12
|
+
for the forward transform with exponential axis compression; parallelised
|
|
13
|
+
batch evaluation via `std::async`.
|
|
14
|
+
- **Cubic-spline forward transform** from discrete samples.
|
|
15
|
+
- **Weeks–Laguerre inverse** with automatic parameter selection.
|
|
16
|
+
- **Critical-points module** (`laplacex::critical`) — continuous wavelet
|
|
17
|
+
transform (Mexican Hat) in the logarithmic time plane; ridge-line extraction;
|
|
18
|
+
AIC-based component pruning.
|
|
19
|
+
- **Reconstruction module** — time-domain and Laplace-domain reconstruction
|
|
20
|
+
from `ExponentialComponent` lists.
|
|
21
|
+
- **Utilities** — test-signal generators, noise estimation, log-plane
|
|
22
|
+
resampling, AIC/BIC criteria.
|
|
23
|
+
- **pybind11 bindings** — full C++ surface exposed to Python with NumPy
|
|
24
|
+
array support.
|
|
25
|
+
- **Python API** — `laplace()`, `inverse()`, `decompose()`, `reconstruct()`,
|
|
26
|
+
`LaplaceTransform`, `ExponentialComponents`.
|
|
27
|
+
- **CI/CD** — GitHub Actions matrix (Linux, macOS Intel, macOS ARM, Windows),
|
|
28
|
+
cibuildwheel for pre-built wheels, OIDC PyPI publishing.
|
|
29
|
+
- **Full pytest suite** — 40+ tests covering correctness, edge cases, and
|
|
30
|
+
round-trip behaviour.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(LaplaceX VERSION 0.1.0 LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
7
|
+
|
|
8
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
9
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
10
|
+
endif()
|
|
11
|
+
|
|
12
|
+
if(MSVC)
|
|
13
|
+
add_compile_options(/O2 /W3)
|
|
14
|
+
add_compile_definitions(_USE_MATH_DEFINES)
|
|
15
|
+
else()
|
|
16
|
+
add_compile_options(-O3 -Wall -Wextra -Wpedantic)
|
|
17
|
+
if(NOT CMAKE_CROSSCOMPILING)
|
|
18
|
+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
|
|
19
|
+
add_compile_options(-march=armv8-a)
|
|
20
|
+
else()
|
|
21
|
+
add_compile_options(-march=native)
|
|
22
|
+
endif()
|
|
23
|
+
endif()
|
|
24
|
+
endif()
|
|
25
|
+
|
|
26
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
27
|
+
|
|
28
|
+
pybind11_add_module(_laplacex
|
|
29
|
+
cpp/src/core.cpp
|
|
30
|
+
cpp/src/critical.cpp
|
|
31
|
+
cpp/src/utils.cpp
|
|
32
|
+
cpp/src/bindings.cpp
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
target_include_directories(_laplacex PRIVATE
|
|
36
|
+
${CMAKE_CURRENT_SOURCE_DIR}/cpp/include
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if(MSVC)
|
|
40
|
+
target_compile_options(_laplacex PRIVATE /MP)
|
|
41
|
+
endif()
|
|
42
|
+
|
|
43
|
+
install(TARGETS _laplacex DESTINATION laplacex)
|
laplacex-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vozmishchev Artem
|
|
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.
|
laplacex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: LaplaceX
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Industrial-grade Laplace transform library: stable forward and inverse transforms via logarithmic-plane wavelet decomposition
|
|
5
|
+
Keywords: laplace-transform,inverse-laplace,signal-processing,numerical-methods,exponential-decomposition,wavelet,scientific-computing
|
|
6
|
+
Author: Vozmishchev Artem
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: C++
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/LaplaceX
|
|
23
|
+
Project-URL: Documentation, https://github.com/YOUR_USERNAME/LaplaceX#readme
|
|
24
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/LaplaceX
|
|
25
|
+
Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/LaplaceX/issues
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Provides-Extra: plot
|
|
29
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
30
|
+
Provides-Extra: data
|
|
31
|
+
Requires-Dist: pandas>=2.0; extra == "data"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-benchmark; extra == "dev"
|
|
35
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
36
|
+
Requires-Dist: scipy; extra == "dev"
|
|
37
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
38
|
+
Requires-Dist: pandas>=2.0; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy; extra == "dev"
|
|
41
|
+
Provides-Extra: all
|
|
42
|
+
Requires-Dist: LaplaceX[data,dev,plot]; extra == "all"
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# LaplaceX
|
|
46
|
+
|
|
47
|
+
**Industrial-grade Python library for stable forward and inverse Laplace transforms.**
|
|
48
|
+
|
|
49
|
+
LaplaceX operates in the *logarithmic time plane* and decomposes signals into
|
|
50
|
+
exponential components via continuous wavelet analysis, achieving significantly
|
|
51
|
+
better stability than classical Mellin-integral inversion — especially in the
|
|
52
|
+
presence of noise and at the tails of the signal.
|
|
53
|
+
|
|
54
|
+
| Feature | LaplaceX | scipy.signal / classical |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Ill-posedness handling | Wavelet ridge + physical regularisation | Tikhonov / none |
|
|
57
|
+
| Gibbs oscillations | None (localised error) | Common |
|
|
58
|
+
| Interpretable output | Relaxation times + amplitudes | No |
|
|
59
|
+
| C++ core | ✓ (pybind11) | NumPy / LAPACK |
|
|
60
|
+
| Pre-built wheels | ✓ (cibuildwheel, all major platforms) | — |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
**Requirements:** Python ≥ 3.10, NumPy ≥ 1.24.
|
|
66
|
+
Optional extras: `matplotlib` (plotting), `pandas` (DataFrame export).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import numpy as np
|
|
74
|
+
from laplacex import laplace, inverse, decompose, reconstruct, utils
|
|
75
|
+
|
|
76
|
+
# --- 1. Forward transform ---------------------------------------------------
|
|
77
|
+
# F(s) for f(t) = exp(-2t) → should equal 1/(s+2)
|
|
78
|
+
s_vals = np.array([1+0j, 2+0j, 3+1j])
|
|
79
|
+
F = laplace(lambda t: np.exp(-2*t), s_vals)
|
|
80
|
+
|
|
81
|
+
# --- 2. Inverse transform (Weeks–Laguerre method) ---------------------------
|
|
82
|
+
t = np.linspace(0.1, 5, 100)
|
|
83
|
+
f_rec = inverse(lambda s: 1/(s+2), t) # recovers exp(-2t)
|
|
84
|
+
|
|
85
|
+
# --- 3. Decompose a noisy multi-exponential signal --------------------------
|
|
86
|
+
t = np.linspace(0.01, 15, 600)
|
|
87
|
+
f = utils.gen_exp_sum(t, [2.0, 0.5], [0.5, 5.0])
|
|
88
|
+
f_n = utils.add_noise(f, sigma=0.02, seed=0)
|
|
89
|
+
|
|
90
|
+
ec = decompose(t, f_n) # → ExponentialComponents
|
|
91
|
+
print(ec)
|
|
92
|
+
# ExponentialComponents(2 components, significances=[0.832, 0.168])
|
|
93
|
+
|
|
94
|
+
for c in ec:
|
|
95
|
+
print(f" τ = {c.decay_time:.3f} A = {c.amplitude:.3f}")
|
|
96
|
+
|
|
97
|
+
# --- 4. Reconstruct ---------------------------------------------------------
|
|
98
|
+
f_hat = reconstruct(ec, t)
|
|
99
|
+
|
|
100
|
+
# --- 5. Plot (requires matplotlib) -----------------------------------------
|
|
101
|
+
ec.plot(t_eval=t, t_original=t, f_original=f_n)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
### Free functions
|
|
109
|
+
|
|
110
|
+
| Function | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `laplace(f, s, t=None)` | Forward Laplace transform F(s) |
|
|
113
|
+
| `inverse(F, t_eval, ...)` | Inverse Laplace transform f(t) |
|
|
114
|
+
| `decompose(t, f, cfg=None)` | Wavelet decomposition into `ExponentialComponents` |
|
|
115
|
+
| `reconstruct(comps, t_eval)` | Reconstruct f(t) from components |
|
|
116
|
+
|
|
117
|
+
### Classes
|
|
118
|
+
|
|
119
|
+
#### `LaplaceTransform`
|
|
120
|
+
|
|
121
|
+
Stateful engine — configure once, call many times. Caches decomposition
|
|
122
|
+
results so that `inverse(..., method='auto')` uses the component
|
|
123
|
+
representation automatically.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from laplacex import LaplaceTransform
|
|
127
|
+
|
|
128
|
+
lt = LaplaceTransform(tol=1e-12, weeks_N=128)
|
|
129
|
+
F = lt.laplace(lambda t: np.exp(-t), s=2+1j)
|
|
130
|
+
ec = lt.decompose(t, f) # cached internally
|
|
131
|
+
f_rec = lt.inverse(None, t_eval, method='auto')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `ExponentialComponents`
|
|
135
|
+
|
|
136
|
+
Container for decomposition results with NumPy arrays, filtering, export,
|
|
137
|
+
and visualisation.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
ec.amplitudes # ndarray, shape (n,)
|
|
141
|
+
ec.rates # ndarray, decay rates λ_k
|
|
142
|
+
ec.decay_times # ndarray, τ_k = 1/λ_k
|
|
143
|
+
ec.significances # ndarray, relative energy
|
|
144
|
+
|
|
145
|
+
ec.filter(min_significance=0.1)
|
|
146
|
+
ec.top(3)
|
|
147
|
+
ec.reconstruct(t_eval)
|
|
148
|
+
ec.reconstruct_laplace(s_eval)
|
|
149
|
+
ec.to_dict()
|
|
150
|
+
ec.to_dataframe() # requires pandas
|
|
151
|
+
ec.plot(t_eval, t_original, f_original)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Configuration
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from laplacex import TransformConfig, DecomposeConfig
|
|
158
|
+
|
|
159
|
+
cfg = TransformConfig()
|
|
160
|
+
cfg.tol = 1e-12 # absolute quadrature tolerance
|
|
161
|
+
cfg.rel_tol = 1e-10
|
|
162
|
+
cfg.max_levels = 25
|
|
163
|
+
cfg.t_max = 1e8
|
|
164
|
+
|
|
165
|
+
dcfg = DecomposeConfig()
|
|
166
|
+
dcfg.n_scales = 128 # wavelet scales
|
|
167
|
+
dcfg.n_log_points = 1024 # log-plane grid resolution
|
|
168
|
+
dcfg.wavelet = "mexh"
|
|
169
|
+
dcfg.snr_threshold = 5.0
|
|
170
|
+
dcfg.use_aic = True # AIC-based pruning
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Utilities (`laplacex.utils`)
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from laplacex import utils
|
|
177
|
+
|
|
178
|
+
t = np.linspace(0.01, 10, 500)
|
|
179
|
+
f = utils.gen_exp_sum(t, [1.0, 0.5], [1.0, 3.0])
|
|
180
|
+
f = utils.add_noise(f, sigma=0.05, seed=42)
|
|
181
|
+
s = utils.estimate_noise(f)
|
|
182
|
+
x, ft = utils.to_log_plane(t, f, n=512)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Theoretical Background
|
|
188
|
+
|
|
189
|
+
LaplaceX is based on the observation that the Laplace kernel
|
|
190
|
+
`K(s, x) = exp(-s·exp(x))` is *shift-invariant and localised* in logarithmic
|
|
191
|
+
coordinates `x = ln(t)`. An error in F(s) therefore affects only the
|
|
192
|
+
corresponding time scale, preventing error propagation across the whole signal
|
|
193
|
+
— in sharp contrast to classical Mellin inversion.
|
|
194
|
+
|
|
195
|
+
The decomposition algorithm:
|
|
196
|
+
|
|
197
|
+
1. Transforms the signal to the log plane: `f̃(x) = f(eˣ)·eˣ`
|
|
198
|
+
2. Computes the continuous wavelet transform (Mexican Hat)
|
|
199
|
+
3. Extracts ridge lines — chains of local wavelet maxima across scales
|
|
200
|
+
4. Reads off exponential parameters from each ridge's peak energy
|
|
201
|
+
5. Prunes noise-level components via SNR threshold + optional AIC
|
|
202
|
+
|
|
203
|
+
See the project document `docs/theory.md` for the full mathematical derivation.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT © 2026 Vozmishchev Artem
|
laplacex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# LaplaceX
|
|
2
|
+
|
|
3
|
+
**Industrial-grade Python library for stable forward and inverse Laplace transforms.**
|
|
4
|
+
|
|
5
|
+
LaplaceX operates in the *logarithmic time plane* and decomposes signals into
|
|
6
|
+
exponential components via continuous wavelet analysis, achieving significantly
|
|
7
|
+
better stability than classical Mellin-integral inversion — especially in the
|
|
8
|
+
presence of noise and at the tails of the signal.
|
|
9
|
+
|
|
10
|
+
| Feature | LaplaceX | scipy.signal / classical |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Ill-posedness handling | Wavelet ridge + physical regularisation | Tikhonov / none |
|
|
13
|
+
| Gibbs oscillations | None (localised error) | Common |
|
|
14
|
+
| Interpretable output | Relaxation times + amplitudes | No |
|
|
15
|
+
| C++ core | ✓ (pybind11) | NumPy / LAPACK |
|
|
16
|
+
| Pre-built wheels | ✓ (cibuildwheel, all major platforms) | — |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
**Requirements:** Python ≥ 3.10, NumPy ≥ 1.24.
|
|
22
|
+
Optional extras: `matplotlib` (plotting), `pandas` (DataFrame export).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import numpy as np
|
|
30
|
+
from laplacex import laplace, inverse, decompose, reconstruct, utils
|
|
31
|
+
|
|
32
|
+
# --- 1. Forward transform ---------------------------------------------------
|
|
33
|
+
# F(s) for f(t) = exp(-2t) → should equal 1/(s+2)
|
|
34
|
+
s_vals = np.array([1+0j, 2+0j, 3+1j])
|
|
35
|
+
F = laplace(lambda t: np.exp(-2*t), s_vals)
|
|
36
|
+
|
|
37
|
+
# --- 2. Inverse transform (Weeks–Laguerre method) ---------------------------
|
|
38
|
+
t = np.linspace(0.1, 5, 100)
|
|
39
|
+
f_rec = inverse(lambda s: 1/(s+2), t) # recovers exp(-2t)
|
|
40
|
+
|
|
41
|
+
# --- 3. Decompose a noisy multi-exponential signal --------------------------
|
|
42
|
+
t = np.linspace(0.01, 15, 600)
|
|
43
|
+
f = utils.gen_exp_sum(t, [2.0, 0.5], [0.5, 5.0])
|
|
44
|
+
f_n = utils.add_noise(f, sigma=0.02, seed=0)
|
|
45
|
+
|
|
46
|
+
ec = decompose(t, f_n) # → ExponentialComponents
|
|
47
|
+
print(ec)
|
|
48
|
+
# ExponentialComponents(2 components, significances=[0.832, 0.168])
|
|
49
|
+
|
|
50
|
+
for c in ec:
|
|
51
|
+
print(f" τ = {c.decay_time:.3f} A = {c.amplitude:.3f}")
|
|
52
|
+
|
|
53
|
+
# --- 4. Reconstruct ---------------------------------------------------------
|
|
54
|
+
f_hat = reconstruct(ec, t)
|
|
55
|
+
|
|
56
|
+
# --- 5. Plot (requires matplotlib) -----------------------------------------
|
|
57
|
+
ec.plot(t_eval=t, t_original=t, f_original=f_n)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### Free functions
|
|
65
|
+
|
|
66
|
+
| Function | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `laplace(f, s, t=None)` | Forward Laplace transform F(s) |
|
|
69
|
+
| `inverse(F, t_eval, ...)` | Inverse Laplace transform f(t) |
|
|
70
|
+
| `decompose(t, f, cfg=None)` | Wavelet decomposition into `ExponentialComponents` |
|
|
71
|
+
| `reconstruct(comps, t_eval)` | Reconstruct f(t) from components |
|
|
72
|
+
|
|
73
|
+
### Classes
|
|
74
|
+
|
|
75
|
+
#### `LaplaceTransform`
|
|
76
|
+
|
|
77
|
+
Stateful engine — configure once, call many times. Caches decomposition
|
|
78
|
+
results so that `inverse(..., method='auto')` uses the component
|
|
79
|
+
representation automatically.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from laplacex import LaplaceTransform
|
|
83
|
+
|
|
84
|
+
lt = LaplaceTransform(tol=1e-12, weeks_N=128)
|
|
85
|
+
F = lt.laplace(lambda t: np.exp(-t), s=2+1j)
|
|
86
|
+
ec = lt.decompose(t, f) # cached internally
|
|
87
|
+
f_rec = lt.inverse(None, t_eval, method='auto')
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### `ExponentialComponents`
|
|
91
|
+
|
|
92
|
+
Container for decomposition results with NumPy arrays, filtering, export,
|
|
93
|
+
and visualisation.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
ec.amplitudes # ndarray, shape (n,)
|
|
97
|
+
ec.rates # ndarray, decay rates λ_k
|
|
98
|
+
ec.decay_times # ndarray, τ_k = 1/λ_k
|
|
99
|
+
ec.significances # ndarray, relative energy
|
|
100
|
+
|
|
101
|
+
ec.filter(min_significance=0.1)
|
|
102
|
+
ec.top(3)
|
|
103
|
+
ec.reconstruct(t_eval)
|
|
104
|
+
ec.reconstruct_laplace(s_eval)
|
|
105
|
+
ec.to_dict()
|
|
106
|
+
ec.to_dataframe() # requires pandas
|
|
107
|
+
ec.plot(t_eval, t_original, f_original)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Configuration
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from laplacex import TransformConfig, DecomposeConfig
|
|
114
|
+
|
|
115
|
+
cfg = TransformConfig()
|
|
116
|
+
cfg.tol = 1e-12 # absolute quadrature tolerance
|
|
117
|
+
cfg.rel_tol = 1e-10
|
|
118
|
+
cfg.max_levels = 25
|
|
119
|
+
cfg.t_max = 1e8
|
|
120
|
+
|
|
121
|
+
dcfg = DecomposeConfig()
|
|
122
|
+
dcfg.n_scales = 128 # wavelet scales
|
|
123
|
+
dcfg.n_log_points = 1024 # log-plane grid resolution
|
|
124
|
+
dcfg.wavelet = "mexh"
|
|
125
|
+
dcfg.snr_threshold = 5.0
|
|
126
|
+
dcfg.use_aic = True # AIC-based pruning
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Utilities (`laplacex.utils`)
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from laplacex import utils
|
|
133
|
+
|
|
134
|
+
t = np.linspace(0.01, 10, 500)
|
|
135
|
+
f = utils.gen_exp_sum(t, [1.0, 0.5], [1.0, 3.0])
|
|
136
|
+
f = utils.add_noise(f, sigma=0.05, seed=42)
|
|
137
|
+
s = utils.estimate_noise(f)
|
|
138
|
+
x, ft = utils.to_log_plane(t, f, n=512)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Theoretical Background
|
|
144
|
+
|
|
145
|
+
LaplaceX is based on the observation that the Laplace kernel
|
|
146
|
+
`K(s, x) = exp(-s·exp(x))` is *shift-invariant and localised* in logarithmic
|
|
147
|
+
coordinates `x = ln(t)`. An error in F(s) therefore affects only the
|
|
148
|
+
corresponding time scale, preventing error propagation across the whole signal
|
|
149
|
+
— in sharp contrast to classical Mellin inversion.
|
|
150
|
+
|
|
151
|
+
The decomposition algorithm:
|
|
152
|
+
|
|
153
|
+
1. Transforms the signal to the log plane: `f̃(x) = f(eˣ)·eˣ`
|
|
154
|
+
2. Computes the continuous wavelet transform (Mexican Hat)
|
|
155
|
+
3. Extracts ridge lines — chains of local wavelet maxima across scales
|
|
156
|
+
4. Reads off exponential parameters from each ridge's peak energy
|
|
157
|
+
5. Prunes noise-level components via SNR threshold + optional AIC
|
|
158
|
+
|
|
159
|
+
See the project document `docs/theory.md` for the full mathematical derivation.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT © 2026 Vozmishchev Artem
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <complex>
|
|
4
|
+
#include <functional>
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
|
|
8
|
+
namespace laplacex {
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Configuration
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
struct TransformConfig {
|
|
15
|
+
double tol = 1e-10; // absolute tolerance for adaptive quadrature
|
|
16
|
+
double rel_tol = 1e-8; // relative tolerance
|
|
17
|
+
int max_levels = 20; // maximum adaptive bisection depth
|
|
18
|
+
double t_max = 1e6; // upper integration limit (exponentially compressed)
|
|
19
|
+
bool use_long = false; // reserved for long-double specialisation
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Forward transform
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute F(s) = integral_0^inf f(t) * exp(-s*t) dt
|
|
28
|
+
* using adaptive Gauss-Kronrod quadrature with exponential axis compression.
|
|
29
|
+
*
|
|
30
|
+
* @param f Callable double -> double representing f(t), t >= 0
|
|
31
|
+
* @param s Complex frequency
|
|
32
|
+
* @param cfg Quadrature configuration
|
|
33
|
+
* @return Complex value F(s)
|
|
34
|
+
*/
|
|
35
|
+
std::complex<double> forward(
|
|
36
|
+
const std::function<double(double)>& f,
|
|
37
|
+
std::complex<double> s,
|
|
38
|
+
const TransformConfig& cfg = {}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute F(s) for a batch of complex frequencies.
|
|
43
|
+
* Thread-safe: each evaluation is independent.
|
|
44
|
+
*/
|
|
45
|
+
std::vector<std::complex<double>> forward_batch(
|
|
46
|
+
const std::function<double(double)>& f,
|
|
47
|
+
const std::vector<std::complex<double>>& s_vals,
|
|
48
|
+
const TransformConfig& cfg = {}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Forward transform from discrete samples (t_i, f_i).
|
|
53
|
+
* Internally constructs a cubic-spline interpolant.
|
|
54
|
+
*/
|
|
55
|
+
std::vector<std::complex<double>> forward_discrete(
|
|
56
|
+
const std::vector<double>& t,
|
|
57
|
+
const std::vector<double>& f,
|
|
58
|
+
const std::vector<std::complex<double>>& s_vals,
|
|
59
|
+
const TransformConfig& cfg = {}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Inverse transform – Weeks method (Laguerre expansion)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Approximate the inverse Laplace transform via the Weeks method:
|
|
68
|
+
* expands F(s) in a series of Laguerre polynomials and reconstructs f(t).
|
|
69
|
+
*
|
|
70
|
+
* @param F Callable complex<double> -> complex<double>
|
|
71
|
+
* @param t_eval Points at which f(t) is desired
|
|
72
|
+
* @param sigma Laguerre shift parameter (auto-selected if 0)
|
|
73
|
+
* @param b Laguerre scale parameter (auto-selected if 0)
|
|
74
|
+
* @param N Number of Laguerre terms
|
|
75
|
+
* @return Real part of the reconstruction at each t
|
|
76
|
+
*/
|
|
77
|
+
std::vector<double> inverse_weeks(
|
|
78
|
+
const std::function<std::complex<double>(std::complex<double>)>& F,
|
|
79
|
+
const std::vector<double>& t_eval,
|
|
80
|
+
double sigma = 0.0,
|
|
81
|
+
double b = 0.0,
|
|
82
|
+
int N = 64,
|
|
83
|
+
const TransformConfig& cfg = {}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
} // namespace laplacex
|