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,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: torchregdiff
3
+ Version: 3.7
4
+ Summary: Regularized differentiation of time-series as pytorch tensors
5
+ Requires-Dist: numpy>=2.5.4
6
+ Requires-Dist: scipy>=1.18.1
7
+ Requires-Dist: torch>=2.15.0
8
+ Requires-Python: >=3.14
@@ -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,)