torchregdiff 3.7__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.
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "torchregdiff"
|
|
3
|
+
version = "3.7"
|
|
4
|
+
description = "Regularized differentiation of time-series as pytorch tensors"
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"numpy>=2.5.4",
|
|
8
|
+
"scipy>=1.18.1",
|
|
9
|
+
"torch>=2.15.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
regdiff = "torchregdiff:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = ["pytest>=9.1.1"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "torchregdiff"
|
|
3
|
+
version = "3.7"
|
|
4
|
+
description = "Regularized differentiation of time-series as pytorch tensors"
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"numpy>=2.5.4",
|
|
8
|
+
"scipy>=1.18.1",
|
|
9
|
+
"torch>=2.15.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
regdiff = "torchregdiff:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=9.1.1",
|
|
22
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .torchregdiff import matrix_anti_diff, matrix_diff, reg_diff # noqa: F401
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
# NOTE - all matrix/vector operations are called using torch.matvel/torch.subtract, etc. Only scalars should multiply ndarrays.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def matrix_diff(n, dx=1.0, dtype=None, device='cpu'):
|
|
9
|
+
"""return the matrix representations of the first-order central finite first difference, first-order central second
|
|
10
|
+
finite difference, and simple finite difference for a specified size and delta x increment.
|
|
11
|
+
:param n: the size of the requested matrix
|
|
12
|
+
:param dx: the time step
|
|
13
|
+
:param dtype: the dtype of the data tensor
|
|
14
|
+
:param device: the device for the data tensor
|
|
15
|
+
|
|
16
|
+
:return: diff -- first order central finite first difference matrix of size n with free boundary conditions
|
|
17
|
+
diff2 -- first order central finite second difference matrix of size n with free boundary conditions
|
|
18
|
+
d -- simple finite differnceing matrix (nxn-1)
|
|
19
|
+
"""
|
|
20
|
+
if dtype is None:
|
|
21
|
+
dtype=torch.get_default_dtype()
|
|
22
|
+
# first central difference
|
|
23
|
+
mul_step = 1 / (2 * dx)
|
|
24
|
+
c = mul_step * np.ones(n - 1)
|
|
25
|
+
diff = sp.sparse.diags_array([-c, c], offsets=[-1, 1]).toarray()
|
|
26
|
+
diff[0, 0:3] = mul_step * np.asarray([-3, 4, -1])
|
|
27
|
+
diff[-1, -3:] = mul_step * np.asarray([1, -4, 3])
|
|
28
|
+
|
|
29
|
+
# second central difference
|
|
30
|
+
mul_step = 1 / (dx**2)
|
|
31
|
+
c = mul_step * np.ones(n)
|
|
32
|
+
diff2 = sp.sparse.diags_array(
|
|
33
|
+
[c[0:-1], -2 * c, c[0:-1]], offsets=[-1, 0, 1]
|
|
34
|
+
).toarray()
|
|
35
|
+
diff2[0, 0:4] = mul_step * np.asarray([2, -5, 4, -1])
|
|
36
|
+
diff2[-1, -4:] = mul_step * np.asarray([-1, 4, -5, 2])
|
|
37
|
+
|
|
38
|
+
# first order differencing matrix
|
|
39
|
+
mul_step = 1 / dx
|
|
40
|
+
c = mul_step * np.ones(n)
|
|
41
|
+
d = sp.sparse.diags_array([-c, c], offsets=[0, 1], shape=[n - 1, n]).toarray()
|
|
42
|
+
|
|
43
|
+
diff = torch.from_numpy(diff)
|
|
44
|
+
diff2 = torch.from_numpy(diff2)
|
|
45
|
+
d = torch.from_numpy(d)
|
|
46
|
+
|
|
47
|
+
diff = diff.to(copy=True,dtype=dtype,device=device)
|
|
48
|
+
diff2 = diff2.to(copy=True,dtype=dtype,device=device)
|
|
49
|
+
d = d.to(copy=True,dtype=dtype,device=device)
|
|
50
|
+
|
|
51
|
+
return diff, diff2, d
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def matrix_anti_diff(n, dx=1.0, dtype=None, device='cpu'):
|
|
55
|
+
"""return the matrix representation of the antidifferentiation operator for an input of size n
|
|
56
|
+
:param n: the size of the requested matrix
|
|
57
|
+
:param dx: the time step
|
|
58
|
+
:param dtype: the dtype of the data tensor
|
|
59
|
+
:param device: the device for the data tensor
|
|
60
|
+
|
|
61
|
+
:return: a -- cumulative trapezoidal integration matrix that when multiplied by a vector yields the cumulative integral (without the first index) (nxn-1)
|
|
62
|
+
a2 -- cumulative trapezoidal double integration matrix that when multiplied by a vector yields the cumulative double integral (without the first index) (nxn-1)
|
|
63
|
+
"""
|
|
64
|
+
if dtype is None:
|
|
65
|
+
dtype=torch.get_default_dtype()
|
|
66
|
+
c = 0.5 * dx * np.ones(n)
|
|
67
|
+
a = np.add(dx * np.tri(n, k=-1), np.diag(c))
|
|
68
|
+
a[:, 0] = 0.5 * dx
|
|
69
|
+
a[0, 0] = 0
|
|
70
|
+
|
|
71
|
+
a2 = np.matmul(a, a)
|
|
72
|
+
|
|
73
|
+
a = torch.from_numpy(a[1:, :])
|
|
74
|
+
a2 = torch.from_numpy(a2[1:, :])
|
|
75
|
+
|
|
76
|
+
a = a.to(copy=True,dtype=dtype,device=device)
|
|
77
|
+
a2 = a2.to(copy=True,dtype=dtype,device=device)
|
|
78
|
+
|
|
79
|
+
return a, a2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reg_diff(
|
|
83
|
+
raw_data: torch.Tensor,
|
|
84
|
+
alpha,
|
|
85
|
+
alpha2=None,
|
|
86
|
+
dx=1.,
|
|
87
|
+
u0=None,
|
|
88
|
+
min_iter=5,
|
|
89
|
+
max_iter=100000,
|
|
90
|
+
delta_cost_tol=1e-6,
|
|
91
|
+
delta_norm_tol=1e-6,
|
|
92
|
+
ep=1e-8,
|
|
93
|
+
skip_first_deriv=False,
|
|
94
|
+
diag=False,
|
|
95
|
+
):
|
|
96
|
+
"""differentation function that takes a vector of position data and returns the first and second derivatives, evaluated using TV-regularized differentiation.
|
|
97
|
+
:param data: the input data
|
|
98
|
+
:param alpha: the regularization coefficient for the first derivative
|
|
99
|
+
:param alpha2: the regularization coeffiient for the second derivative. If left blank only the first derivative will be calculated.
|
|
100
|
+
:param dx: the time step for the data
|
|
101
|
+
:param u0: an initial guess for the derivative(s) as a 1-D tensor (for the first derivative) or as a 2-D tensor (for both derivatives)
|
|
102
|
+
:param min_iter: minimal number of iterations to calculate before returning. Default value 5.
|
|
103
|
+
:param max_iter: maximum number of iterations to calculate before returning. Default value is 1000000.
|
|
104
|
+
:param delta_cost_tol: relative delta cost to reach iteratively before returning (applies to both derivatives). Default value is 1e-6.
|
|
105
|
+
:param delta_norm_tol: relative delta norm of step to reach iteratively before returning (applies to both derivatives). Default value is 1e-6.
|
|
106
|
+
:param ep: epsilon to use to smooth the absolute value function. Devault value is 1e-8
|
|
107
|
+
:param skip_first_deriv: if True, use the value of u0 for the first derivative and do not calculate it
|
|
108
|
+
:param diag: if True, prints diagnostic information during execution. Default False.
|
|
109
|
+
|
|
110
|
+
:return: u -- the first derivative
|
|
111
|
+
v -- the second derivative
|
|
112
|
+
"""
|
|
113
|
+
data = raw_data.squeeze()
|
|
114
|
+
if data.ndim != 1:
|
|
115
|
+
raise ValueError("Expected a data tensor that can be squeezed to 1D")
|
|
116
|
+
n = len(data)
|
|
117
|
+
data_mean = data.mean()
|
|
118
|
+
data_st_dev = data.std(correction=0)
|
|
119
|
+
data = (data - data_mean) / data_st_dev
|
|
120
|
+
|
|
121
|
+
if alpha2:
|
|
122
|
+
second_deriv = True
|
|
123
|
+
else:
|
|
124
|
+
second_deriv = False
|
|
125
|
+
|
|
126
|
+
diff, diff2, d = matrix_diff(n, dtype=data.dtype, device=data.device)
|
|
127
|
+
d_tp = torch.transpose(d,0,1)
|
|
128
|
+
|
|
129
|
+
a, a2 = matrix_anti_diff(n, dtype=data.dtype, device=data.device)
|
|
130
|
+
a_tp = torch.transpose(a,0,1)
|
|
131
|
+
a_tp_a = torch.matmul(a_tp, a)
|
|
132
|
+
a_tp_a2 = torch.matmul(a_tp, a2)
|
|
133
|
+
|
|
134
|
+
# bunch of code for handling an input u0
|
|
135
|
+
u = None
|
|
136
|
+
v = None
|
|
137
|
+
if u0 is None and skip_first_deriv:
|
|
138
|
+
raise RuntimeError("If first derivative calculation should be skipped, u0 should be provided")
|
|
139
|
+
if u0 is not None:
|
|
140
|
+
if torch.is_tensor(u0):
|
|
141
|
+
if u0.device != data.device:
|
|
142
|
+
raise ValueError("u0 and data must use the same device")
|
|
143
|
+
if u0.ndim == 1:
|
|
144
|
+
if len(u0) == n:
|
|
145
|
+
u = u0 * dx / data_st_dev
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError("Expected a first-derivative u0 tensor that is the same size as the data tensor")
|
|
148
|
+
elif u0.shape[0] == 2 and u0.shape[1] == n:
|
|
149
|
+
u = u0[0] * dx / data_st_dev
|
|
150
|
+
v = u0[1] * (dx**2) / data_st_dev
|
|
151
|
+
elif u0.shape[0] == n and u0.shape[1] == 2:
|
|
152
|
+
u = u0[:,0] * dx / data_st_dev
|
|
153
|
+
v = u0[:,1] * (dx**2) / data_st_dev
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError("Bad u0 tensor size")
|
|
156
|
+
else:
|
|
157
|
+
raise ValueError("u0 should be a tensor")
|
|
158
|
+
if not torch.is_tensor(u):
|
|
159
|
+
u = torch.mv(diff, data)
|
|
160
|
+
if not torch.is_tensor(v) and second_deriv:
|
|
161
|
+
v = torch.mv(diff2, data)
|
|
162
|
+
|
|
163
|
+
# start calculating u
|
|
164
|
+
ofst_1 = data[0]
|
|
165
|
+
if not skip_first_deriv:
|
|
166
|
+
data_comp = data[1:] - ofst_1
|
|
167
|
+
a_tp_b = torch.mv(a_tp, data_comp)
|
|
168
|
+
if diag:
|
|
169
|
+
u_cost = [
|
|
170
|
+
torch.linalg.vector_norm(torch.subtract(torch.cumulative_trapezoid(u), data_comp))
|
|
171
|
+
+ alpha * torch.linalg.vector_norm(torch.diff(u), ord=1)
|
|
172
|
+
]
|
|
173
|
+
prev_cost = None
|
|
174
|
+
for iter in range(max_iter):
|
|
175
|
+
c = torch.reciprocal(torch.sqrt(torch.pow(torch.diff(u), 2) + ep))
|
|
176
|
+
En = torch.diag(c)
|
|
177
|
+
Ln = torch.mm(torch.mm(d_tp, En), d)
|
|
178
|
+
gn = torch.add(torch.subtract(torch.mv(a_tp_a, u), a_tp_b), alpha * torch.mv(Ln, u))
|
|
179
|
+
Hn = torch.add(a_tp_a, alpha * Ln)
|
|
180
|
+
s = torch.linalg.solve(Hn, gn)
|
|
181
|
+
u = torch.subtract(u, s)
|
|
182
|
+
cost_n = (
|
|
183
|
+
torch.linalg.vector_norm(torch.subtract(torch.cumulative_trapezoid(u), data_comp))
|
|
184
|
+
+ alpha * torch.linalg.vector_norm(torch.diff(u), ord=1)
|
|
185
|
+
)
|
|
186
|
+
if diag:
|
|
187
|
+
print(
|
|
188
|
+
f"u, iteration {iter + 1}: cost: {cost_n}, relative change: {torch.linalg.vector_norm(s) / torch.linalg.vector_norm(u)}"
|
|
189
|
+
)
|
|
190
|
+
u_cost.append(cost_n)
|
|
191
|
+
if iter >= min_iter and torch.linalg.vector_norm(s) / torch.linalg.vector_norm(u) < delta_norm_tol:
|
|
192
|
+
if diag:
|
|
193
|
+
print("u, delta norm tolerance reached")
|
|
194
|
+
break
|
|
195
|
+
if (
|
|
196
|
+
prev_cost
|
|
197
|
+
and iter >= min_iter
|
|
198
|
+
and (prev_cost - cost_n) / prev_cost < delta_cost_tol
|
|
199
|
+
):
|
|
200
|
+
if diag:
|
|
201
|
+
print("u, relative cost tolerance reached")
|
|
202
|
+
break
|
|
203
|
+
prev_cost = cost_n
|
|
204
|
+
|
|
205
|
+
if second_deriv:
|
|
206
|
+
ofst_2 = u[0]
|
|
207
|
+
data_comp = torch.subtract(data[1:] - ofst_1, ofst_2 * torch.arange(1, n))
|
|
208
|
+
a_tp_b = torch.mv(a_tp, data_comp)
|
|
209
|
+
if diag:
|
|
210
|
+
v_cost = [
|
|
211
|
+
torch.linalg.vector_norm(
|
|
212
|
+
torch.subtract(
|
|
213
|
+
torch.cumulative_trapezoid(
|
|
214
|
+
torch.cat((
|
|
215
|
+
torch.tensor([0]),
|
|
216
|
+
torch.cumulative_trapezoid(v)
|
|
217
|
+
))
|
|
218
|
+
),
|
|
219
|
+
data_comp,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
+ alpha2 * torch.linalg.vector_norm(torch.diff(v), ord=1)
|
|
223
|
+
]
|
|
224
|
+
prev_cost = None
|
|
225
|
+
for iter in range(max_iter):
|
|
226
|
+
c = torch.reciprocal(torch.sqrt(torch.pow(torch.diff(v), 2) + ep))
|
|
227
|
+
En = torch.diag(c)
|
|
228
|
+
Ln = torch.mm(torch.mm(d_tp, En), d)
|
|
229
|
+
gn = torch.add(
|
|
230
|
+
torch.subtract(torch.mv(a_tp_a2, v), a_tp_b), alpha2 * torch.mv(Ln, v)
|
|
231
|
+
)
|
|
232
|
+
Hn = torch.add(a_tp_a2, alpha2 * Ln)
|
|
233
|
+
s = torch.linalg.solve(Hn, gn)
|
|
234
|
+
v = torch.subtract(v, s)
|
|
235
|
+
cost_n = (
|
|
236
|
+
torch.linalg.vector_norm(
|
|
237
|
+
torch.subtract(
|
|
238
|
+
torch.cumulative_trapezoid(
|
|
239
|
+
torch.cat((
|
|
240
|
+
torch.tensor([0]),
|
|
241
|
+
torch.cumulative_trapezoid(v)
|
|
242
|
+
))
|
|
243
|
+
),
|
|
244
|
+
data_comp,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
+ alpha2 * torch.linalg.vector_norm(torch.diff(v), ord=1)
|
|
248
|
+
)
|
|
249
|
+
if diag:
|
|
250
|
+
print(
|
|
251
|
+
f"v, iteration {iter + 1}: cost: {cost_n}, relative change: {torch.linalg.vector_norm(s) / torch.linalg.vector_norm(v)}"
|
|
252
|
+
)
|
|
253
|
+
v_cost.append(cost_n)
|
|
254
|
+
if iter >= min_iter and torch.linalg.vector_norm(s) / torch.linalg.vector_norm(v) < delta_norm_tol:
|
|
255
|
+
if diag:
|
|
256
|
+
print("v, delta norm tolerance reached")
|
|
257
|
+
break
|
|
258
|
+
if (
|
|
259
|
+
prev_cost
|
|
260
|
+
and iter >= min_iter
|
|
261
|
+
and (prev_cost - cost_n) / prev_cost < delta_cost_tol
|
|
262
|
+
):
|
|
263
|
+
if diag:
|
|
264
|
+
print("v, relative cost tolerance reached")
|
|
265
|
+
break
|
|
266
|
+
prev_cost = cost_n
|
|
267
|
+
v = v * data_st_dev / (dx**2)
|
|
268
|
+
returns = []
|
|
269
|
+
if not skip_first_deriv:
|
|
270
|
+
u = u * data_st_dev / dx
|
|
271
|
+
returns.append(u)
|
|
272
|
+
if second_deriv:
|
|
273
|
+
returns.append(v)
|
|
274
|
+
return (*returns,)
|