• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

maurergroup / dfttoolkit / 15065810745

16 May 2025 09:56AM UTC coverage: 30.583% (+8.8%) from 21.747%
15065810745

Pull #59

github

b2c890
web-flow
Merge f52b00038 into e895278a4
Pull Request #59: Vibrations refactor

1164 of 3806 relevant lines covered (30.58%)

0.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.0
dfttoolkit/utils/math_utils.py
1
from copy import deepcopy
1✔
2

3
import numpy as np
1✔
4
import numpy.typing as npt
1✔
5
import scipy
1✔
6

7

8
def get_rotation_matrix(vec_start: npt.NDArray, vec_end: npt.NDArray) -> npt.NDArray:
1✔
9
    """
10
    Calculate the rotation matrix to align two unit vectors.
11

12
    Given a two (unit) vectors, vec_start and vec_end, this function calculates the
13
    rotation matrix U, so that U * vec_start = vec_end.
14

15
    U the is rotation matrix that rotates vec_start to point in the direction
16
    of vec_end.
17

18
    https://math.stackexchange.com/questions/180418/calculate-rotation-matrix-to-align-vector-a-to-vector-b-in-3d/897677
19

20
    Parameters
21
    ----------
22
    vec_start, vec_end : npt.NDArray[np.float64]
23
        Two vectors that should be aligned. Both vectors must have a l2-norm of 1.
24

25
    Returns
26
    -------
27
    R
28
        The rotation matrix U as npt.NDArray with shape (3,3)
29
    """
30
    assert np.isclose(np.linalg.norm(vec_start), 1) and np.isclose(
1✔
31
        np.linalg.norm(vec_end), 1
32
    ), "vec_start and vec_end must be unit vectors!"
33

34
    v = np.cross(vec_start, vec_end)
1✔
35
    c = np.dot(vec_start, vec_end)
1✔
36
    v_x = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
1✔
37
    return np.eye(3) + v_x + v_x.dot(v_x) / (1 + c)
1✔
38

39

40
def get_rotation_matrix_around_axis(axis: npt.NDArray, phi: float) -> npt.NDArray:
1✔
41
    """
42
    Generates a rotation matrix around a given vector.
43

44
    Parameters
45
    ----------
46
    axis : npt.NDArray
47
        Axis around which the rotation is done.
48
    phi : float
49
        Angle of rotation around axis in radiants.
50

51
    Returns
52
    -------
53
    R : npt.NDArray
54
        Rotation matrix
55

56
    """
57
    axis_vec = np.array(axis, dtype=np.float64)
1✔
58
    axis_vec /= np.linalg.norm(axis_vec)
1✔
59

60
    eye = np.eye(3, dtype=np.float64)
1✔
61
    ddt = np.outer(axis_vec, axis_vec)
1✔
62
    skew = np.array(
1✔
63
        [
64
            [0, axis_vec[2], -axis_vec[1]],
65
            [-axis_vec[2], 0, axis_vec[0]],
66
            [axis_vec[1], -axis_vec[0], 0],
67
        ],
68
        dtype=np.float64,
69
    )
70

71
    return ddt + np.cos(phi) * (eye - ddt) + np.sin(phi) * skew
1✔
72

73

74
def get_rotation_matrix_around_z_axis(phi: float) -> npt.NDArray:
1✔
75
    """
76
    Generates a rotation matrix around the z axis.
77

78
    Parameters
79
    ----------
80
    phi : float
81
        Angle of rotation around axis in radiants.
82

83
    Returns
84
    -------
85
    npt.NDArray
86
        Rotation matrix
87

88
    """
89
    return get_rotation_matrix_around_axis(np.array([0.0, 0.0, 1.0]), phi)
×
90

91

92
def get_mirror_matrix(normal_vector: npt.NDArray) -> npt.NDArray:
1✔
93
    """
94
    Generates a transformation matrix for mirroring through plane given by the
95
    normal vector.
96

97
    Parameters
98
    ----------
99
    normal_vector : npt.NDArray
100
        Normal vector of the mirror plane.
101

102
    Returns
103
    -------
104
    M : npt.NDArray
105
        Mirror matrix
106

107
    """
108
    n_vec = normal_vector / np.linalg.norm(normal_vector)
1✔
109
    eps = np.finfo(np.float64).eps
1✔
110
    a = n_vec[0]
1✔
111
    b = n_vec[1]
1✔
112
    c = n_vec[2]
1✔
113
    M = np.array(
1✔
114
        [
115
            [1 - 2 * a**2, -2 * a * b, -2 * a * c],
116
            [-2 * a * b, 1 - 2 * b**2, -2 * b * c],
117
            [-2 * a * c, -2 * b * c, 1 - 2 * c**2],
118
        ]
119
    )
120
    M[np.abs(M) < eps * 10] = 0
1✔
121
    return M
1✔
122

123

124
def get_angle_between_vectors(
1✔
125
    vector_1: npt.NDArray, vector_2: npt.NDArray
126
) -> npt.NDArray:
127
    """
128
    Determines angle between two vectors.
129

130
    Parameters
131
    ----------
132
    vector_1 : npt.NDArray
133
    vector_2 : npt.NDArray
134

135
    Returns
136
    -------
137
    angle : float
138
        Angle in radiants.
139

140
    """
141
    return (
1✔
142
        np.dot(vector_1, vector_2) / np.linalg.norm(vector_1) / np.linalg.norm(vector_2)
143
    )
144

145

146
def get_fractional_coords(
1✔
147
    cartesian_coords: npt.NDArray, lattice_vectors: npt.NDArray
148
) -> npt.NDArray:
149
    """
150
    Transform cartesian coordinates into fractional coordinates.
151

152
    Parameters
153
    ----------
154
    cartesian_coords: [N x N_dim] numpy array
155
        Cartesian coordinates of atoms (can be Nx2 or Nx3)
156
    lattice_vectors: [N_dim x N_dim] numpy array:
157
        Matrix of lattice vectors: Each ROW corresponds to one lattice vector!
158

159
    Returns
160
    -------
161
    fractional_coords: [N x N_dim] numpy array
162
        Fractional coordinates of atoms
163

164
    """
165
    fractional_coords = np.linalg.solve(lattice_vectors.T, cartesian_coords.T)
1✔
166
    return fractional_coords.T
1✔
167

168

169
def get_cartesian_coords(
1✔
170
    frac_coords: npt.NDArray, lattice_vectors: npt.NDArray
171
) -> npt.NDArray:
172
    """
173
    Transform fractional coordinates into cartesian coordinates.
174

175
    Parameters
176
    ----------
177
    frac_coords: [N x N_dim] numpy array
178
        Fractional coordinates of atoms (can be Nx2 or Nx3)
179
    lattice_vectors: [N_dim x N_dim] numpy array:
180
        Matrix of lattice vectors: Each ROW corresponds to one lattice vector!
181

182
    Returns
183
    -------
184
    cartesian_coords: [N x N_dim] numpy array
185
        Cartesian coordinates of atoms
186

187
    """
188
    return np.dot(frac_coords, lattice_vectors)
1✔
189

190

191
def get_triple_product(a: npt.NDArray, b: npt.NDArray, c: npt.NDArray) -> npt.NDArray:
1✔
192
    """Returns the triple product (DE: Spatprodukt): a*(bxc)."""
193
    assert len(a) == 3
1✔
194
    assert len(b) == 3
1✔
195
    assert len(c) == 3
1✔
196
    return np.dot(np.cross(a, b), c)
1✔
197

198

199
def smooth_function(y: npt.NDArray, box_pts: int):
1✔
200
    """
201
    Smooths a function using convolution.
202

203
    Parameters
204
    ----------
205
    y : TYPE
206
        DESCRIPTION.
207
    box_pts : TYPE
208
        DESCRIPTION.
209

210
    Returns
211
    -------
212
    y_smooth : TYPE
213
        DESCRIPTION.
214

215
    """
216
    box = np.ones(box_pts) / box_pts
1✔
217
    return np.convolve(y, box, mode="same")
1✔
218

219

220
def get_cross_correlation_function(
1✔
221
    signal_0: npt.NDArray,
222
    signal_1: npt.NDArray,
223
    detrend: bool = False,
224
) -> npt.NDArray:
225
    """
226
    Calculate the autocorrelation function for a given signal.
227

228
    Parameters
229
    ----------
230
    signal_0 : 1D npt.NDArray
231
        First siganl for which the correlation function should be calculated.
232
    signal_1 : 1D npt.NDArray
233
        Second siganl for which the correlation function should be calculated.
234

235
    Returns
236
    -------
237
    correlation : npt.NDArray
238
        Autocorrelation function from 0 to max_lag.
239

240
    """
241
    if detrend:
1✔
242
        signal_0 = scipy.signal.detrend(signal_0)
×
243
        signal_1 = scipy.signal.detrend(signal_1)
×
244

245
    # cross_correlation = np.correlate(signal_0, signal_1, mode='same')
246
    cross_correlation = np.correlate(signal_0, signal_1, mode="full")
1✔
247
    cross_correlation = cross_correlation[cross_correlation.size // 2 :]
1✔
248

249
    # normalize by number of overlapping data points
250
    cross_correlation /= np.arange(cross_correlation.size, 0, -1)
1✔
251
    cutoff = int(cross_correlation.size * 0.75)
1✔
252
    return cross_correlation[:cutoff]
1✔
253

254

255
def get_autocorrelation_function_manual_lag(
1✔
256
    signal: npt.NDArray, max_lag: int
257
) -> npt.NDArray:
258
    """
259
    Alternative method to determine the autocorrelation function for a given
260
    signal that used numpy.corrcoef. This function allows to set the lag
261
    manually.
262

263
    Parameters
264
    ----------
265
    signal : 1D npt.NDArray
266
        Siganl for which the autocorrelation function should be calculated.
267
    max_lag : Union[None, int]
268
        Autocorrelation will be calculated for a range of 0 to max_lag,
269
        where max_lag is the largest lag for the calculation of the
270
        autocorrelation function
271

272
    Returns
273
    -------
274
    autocorrelation : npt.NDArray
275
        Autocorrelation function from 0 to max_lag.
276

277
    """
278
    lag = npt.NDArray(range(max_lag))
×
279

280
    autocorrelation = np.array([np.nan] * max_lag)
×
281

282
    for i in lag:
×
283
        corr = 1.0 if i == 0 else np.corrcoef(signal[i], signal[:-i])[0][1]
×
284

285
        autocorrelation[i] = corr
×
286

287
    return autocorrelation
×
288

289

290
def get_fourier_transform(signal: npt.NDArray, time_step: float) -> tuple:
1✔
291
    """
292
    Calculate the fourier transform of a given siganl.
293

294
    Parameters
295
    ----------
296
    signal : 1D npt.NDArray
297
        Siganl for which the autocorrelation function should be calculated.
298
    time_step : float
299
        Time step of the signal in seconds.
300

301
    Returns
302
    -------
303
    (npt.NDArray, npt.NDArray)
304
        Frequencs and absolute values of the fourier transform.
305

306
    """
307
    # d = len(signal) * time_step
308

309
    f = scipy.fft.fftfreq(signal.size, d=time_step)
1✔
310
    y = scipy.fft.fft(signal)
1✔
311

312
    L = f >= 0
1✔
313

314
    return f[L], y[L]
1✔
315

316

317
def lorentzian(
1✔
318
    x: float | npt.NDArray, a: float, b: float, c: float
319
) -> float | npt.NDArray:
320
    """
321
    Returns a Lorentzian function.
322

323
    Parameters
324
    ----------
325
    x : Union[float, npt.NDArray]
326
        Argument x of f(x) --> y.
327
    a : float
328
        Maximum of Lorentzian.
329
    b : float
330
        Width of Lorentzian.
331
    c : float
332
        Magnitude of Lorentzian.
333

334
    Returns
335
    -------
336
    f : Union[float, npt.NDArray]
337
        Outupt of a Lorentzian function.
338

339
    """
340
    # f = c / (np.pi * b * (1.0 + ((x - a) / b) ** 2))  # +d
341
    return c / (1.0 + ((x - a) / (b / 2.0)) ** 2)  # +d
1✔
342

343

344
def exponential(x: float | npt.NDArray, a: float, b: float) -> float | npt.NDArray:
1✔
345
    return a * np.exp(x * b)
×
346

347

348
def double_exponential(
1✔
349
    x: float | npt.NDArray, a: float, b: float, c: float
350
) -> float | npt.NDArray:
351
    return a * (np.exp(x * b) + np.exp(x * c))
×
352

353

354
def gaussian_window(N, std=0.4):
1✔
355
    """
356
    Generate a Gaussian window.
357

358
    Parameters
359
    ----------
360
    N : int
361
        Number of points in the window.
362
    std : float
363
        Standard deviation of the Gaussian window, normalized
364
        such that the maximum value occurs at the center of the window.
365

366
    Returns
367
    -------
368
    window : np.array
369
        Gaussian window of length N.
370

371
    """
372
    n = np.linspace(-1, 1, N)
1✔
373
    return np.exp(-0.5 * (n / std) ** 2)
1✔
374

375

376
def apply_gaussian_window(data, std=0.4):
1✔
377
    """
378
    Apply a Gaussian window to an array.
379

380
    Parameters
381
    ----------
382
    data : np.array
383
        Input data array to be windowed.
384
    std : float
385
        Standard deviation of the Gaussian window.
386

387
    Returns
388
    -------
389
    windowed_data : np.array
390
        Windowed data array.
391

392
    """
393
    N = len(data)
1✔
394
    window = gaussian_window(N, std)
1✔
395
    return data * window
1✔
396

397

398
def hann_window(N):
1✔
399
    """
400
    Generate a Hann window.
401

402
    Parameters
403
    ----------
404
    N : int
405
        Number of points in the window.
406

407
    Returns
408
    -------
409
    np.ndarray
410
        Hann window of length N.
411
    """
412
    return 0.5 * (1 - np.cos(2 * np.pi * np.arange(N) / (N - 1)))
×
413

414

415
def apply_hann_window(data):
1✔
416
    """
417
    Apply a Hann window to an array.
418

419
    Parameters
420
    ----------
421
    data : np.ndarray
422
        Input data array to be windowed.
423

424
    Returns
425
    -------
426
    np.ndarray
427
        Windowed data array.
428
    """
429
    N = len(data)
×
430
    window = hann_window(N)
×
431
    return data * window
×
432

433

434
def norm_matrix_by_dagonal(matrix: npt.NDArray) -> npt.NDArray:
1✔
435
    """
436
    Norms a matrix such that the diagonal becomes 1.
437

438
    | a_11 a_12 a_13 |       |   1   a'_12 a'_13 |
439
    | a_21 a_22 a_23 |  -->  | a'_21   1   a'_23 |
440
    | a_31 a_32 a_33 |       | a'_31 a'_32   1   |
441

442
    Parameters
443
    ----------
444
    matrix : npt.NDArray
445
        Matrix that should be normed.
446

447
    Returns
448
    -------
449
    matrix : npt.NDArray
450
        Normed matrix.
451

452
    """
453
    diagonal = np.array(np.diagonal(matrix))
1✔
454
    L = diagonal == 0.0
1✔
455
    diagonal[L] = 1.0
1✔
456

457
    new_matrix = deepcopy(matrix)
1✔
458
    new_matrix /= np.sqrt(
1✔
459
        np.tile(diagonal, (matrix.shape[1], 1)).T
460
        * np.tile(diagonal, (matrix.shape[0], 1))
461
    )
462

463
    return new_matrix
1✔
464

465

466
def mae(delta: np.ndarray) -> np.floating:
1✔
467
    """
468
    Calculated the mean absolute error from a list of value differnces.
469

470
    Parameters
471
    ----------
472
    delta : np.ndarray
473
        Array containing differences
474

475
    Returns
476
    -------
477
    float
478
        mean absolute error
479

480
    """
481
    return np.mean(np.abs(delta))
1✔
482

483

484
def rel_mae(delta: np.ndarray, target_val: np.ndarray) -> np.floating | np.float64 | float:
1✔
485
    """
486
    Calculated the relative mean absolute error from a list of value differnces,
487
    given the target values.
488

489
    Parameters
490
    ----------
491
    delta : np.ndarray
492
        Array containing differences
493
    target_val : np.ndarray
494
        Array of target values against which the difference should be compared
495

496
    Returns
497
    -------
498
    float
499
        relative mean absolute error
500

501
    """
502
    target_norm = np.mean(np.abs(target_val))
1✔
503
    return np.mean(np.abs(delta)).item() / (target_norm + 1e-9)
1✔
504

505

506
def rmse(delta: np.ndarray) -> float:
1✔
507
    """
508
    Calculated the root mean sqare error from a list of value differnces.
509

510
    Parameters
511
    ----------
512
    delta : np.ndarray
513
        Array containing differences
514

515
    Returns
516
    -------
517
    float
518
        root mean square error
519

520
    """
521
    return np.sqrt(np.mean(np.square(delta)))
×
522

523

524
def rel_rmse(delta: np.ndarray, target_val: np.ndarray) -> float:
1✔
525
    """
526
    Calculated the relative root mean sqare error from a list of value differnces,
527
    given the target values.
528

529
    Parameters
530
    ----------
531
    delta : np.ndarray
532
        Array containing differences
533
    target_val : np.ndarray
534
        Array of target values against which the difference should be compared
535

536
    Returns
537
    -------
538
    float
539
        relative root mean sqare error
540

541
    """
542
    target_norm = np.sqrt(np.mean(np.square(target_val)))
×
543
    return np.sqrt(np.mean(np.square(delta))) / (target_norm + 1e-9)
×
544

545

546
def get_moving_average(signal: npt.NDArray[np.float64], window_size: int):
1✔
547
    """
548
    Cacluated the moving average and the variance around the moving average.
549

550
    Parameters
551
    ----------
552
    signal : npt.NDArray[np.float64]
553
        Signal for which the moving average should be calculated.
554
    window_size : int
555
        Window size for the mocing average.
556

557
    Returns
558
    -------
559
    moving_avg : TYPE
560
        Moving average.
561
    variance : TYPE
562
        Variance around the moving average.
563

564
    """
565
    moving_avg = np.convolve(signal, np.ones(window_size) / window_size, mode="valid")
×
566
    variance = np.array(
×
567
        [
568
            np.var(signal[i : i + window_size])
569
            for i in range(len(signal) - window_size + 1)
570
        ]
571
    )
572

573
    return moving_avg, variance
×
574

575

576
def get_maxima_in_moving_interval(
1✔
577
    function_values, interval_size, step_size, filter_value
578
):
579
    """
580
    Moves an interval along the function and filters out all points smaller
581
    than filter_value time the maximal value in the interval.
582

583
    Parameters
584
    ----------
585
    function_values : array-like
586
        1D array of function values.
587
    interval_size : int
588
        Size of the interval (number of points).
589
    step_size : int
590
        Step size for moving the interval.
591

592
    Returns
593
    -------
594
        np.ndarray: Filtered array where points outside the threshold are set
595
        to NaN.
596

597
    """
598
    # Convert input to a NumPy array
599
    function_values = np.asarray(function_values)
×
600

601
    # Initialize an array to store the result
602
    filtered_indices = []
×
603

604
    # Slide the interval along the function
605
    for start in range(0, len(function_values) - interval_size + 1, step_size):
×
606
        # Define the end of the interval
607
        end = start + interval_size
×
608

609
        # Extract the interval
610
        interval = function_values[start:end]
×
611

612
        # Find the maximum value in the interval
613
        max_value = np.max(interval)
×
614

615
        # Apply the filter
616
        threshold = filter_value * max_value
×
617

618
        indices = np.where(interval >= threshold)
×
619
        filtered_indices += list(start + indices[0])
×
620

621
    return np.array(list(set(filtered_indices)))
×
622

623

624
def get_pearson_correlation_coefficient(x, y):
1✔
625
    mean_x = np.mean(x)
×
626
    mean_y = np.mean(y)
×
627
    std_x = np.std(x)
×
628
    std_y = np.std(y)
×
629

630
    return np.mean((x - mean_x) * (y - mean_y)) / std_x / std_y
×
631

632

633
def get_t_test(x, y):
1✔
634
    r = get_pearson_correlation_coefficient(x, y)
×
635

636
    n = len(x)
×
637

638
    return np.abs(r) * np.sqrt((n - 2) / (1 - r**2))
×
639

640

641
def probability_density(t, n):
1✔
642
    degrees_of_freedom = n - 2
×
643

644
    return (
×
645
        scipy.special.gamma((degrees_of_freedom + 1.0) / 2.0)
646
        / (
647
            np.sqrt(np.pi * degrees_of_freedom)
648
            * scipy.special.gamma(degrees_of_freedom / 2.0)
649
        )
650
        * (1 + t**2 / degrees_of_freedom) ** (-(degrees_of_freedom + 1.0) / 2.0)
651
    )
652

653

654
def get_significance(x, t):
1✔
655
    n = len(x)
×
656

657
    return scipy.integrate.quad(probability_density, -np.inf, t, args=(n))[0]
×
658

659

660
def squared_exponential_kernel(x1_vec, x2_vec, tau):
1✔
661
    """
662
    A simlpe squared exponential kernel to determine a similarity measure.
663

664
    Parameters
665
    ----------
666
    x1_vec : np.array
667
        Descriptor for first set of data of shape
668
        [data points, descriptor dimensions].
669
    x2_vec : np.array
670
        Descriptor for second set of data of shape
671
        [data points, descriptor dimensions].
672
    tau : float
673
        Correlation length for the descriptor.
674

675
    Returns
676
    -------
677
    K : np.array
678
        Matrix contianing pairwise kernel values.
679

680
    """
681
    # Ensure inputs are at least 2D (n_samples, n_features)
682
    x1 = np.atleast_2d(x1_vec)
×
683
    x2 = np.atleast_2d(x2_vec)
×
684

685
    # If they are 1D row-vectors, convert to column vectors
686
    if x1.shape[0] == 1 or x1.shape[1] == 1:
×
687
        x1 = x1.reshape(-1, 1)
×
688
    if x2.shape[0] == 1 or x2.shape[1] == 1:
×
689
        x2 = x2.reshape(-1, 1)
×
690

691
    # Compute squared distances (broadcasting works for both 1D and 2D now)
692
    diff = x1[:, np.newaxis, :] - x2[np.newaxis, :, :]
×
693
    sq_dist = np.sum(diff**2, axis=2)
×
694

695
    # Apply the RBF formula
696
    return np.exp(-0.5 * sq_dist / tau**2)
×
697

698

699
class GPR:
1✔
700
    def __init__(self, x, y, tau, sigma):
1✔
701
        """
702
        A very simple GPR designed for interpolation and smoothing of data.
703

704
        Parameters
705
        ----------
706
        x : np.array
707
            Descriptor of shape [data points, descriptor dimensions].
708
        y : np.array
709
            Data to be learned.
710
        tau : float
711
            Correlation length for the descriptor.
712
        sigma : float
713
            Uncertainity of the input data.
714

715
        Returns
716
        -------
717
        None.
718

719
        """
720
        K1 = squared_exponential_kernel(x, x, tau)
×
721

722
        self.K1_inv = np.linalg.inv(K1 + np.eye(len(x)) * sigma)
×
723
        self.x = x
×
724
        self.y = y
×
725
        self.tau = tau
×
726
        self.sigma = sigma
×
727

728
    def __call__(self, x):
1✔
729
        return self.predict(x)
×
730

731
    def predict(self, x):
1✔
732
        K2 = squared_exponential_kernel(x, self.x, self.tau)
×
733
        y_test0 = K2.dot(self.K1_inv)
×
734

735
        return y_test0.dot(self.y)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc