Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ features \ weights \ taper_monotonic_weight_kernel.py: 98%
50 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
1# =====================================================
2# Imports
3# =====================================================
4from typing import Annotated
6import numpy as np
7from numpy._typing import NDArray
8from pydantic import Field
10from mt_metadata.common.enumerations import StrEnumerationBase
11from mt_metadata.features.weights.monotonic_weight_kernel import MonotonicWeightKernel
14# =====================================================
15class HalfWindowStyleEnum(StrEnumerationBase):
16 hamming = "hamming"
17 hann = "hann"
18 rectangle = "rectangle"
19 blackman = "blackman"
22class ActivationStyleEnum(StrEnumerationBase):
23 linear = "linear"
24 sigmoid = "sigmoid"
25 tanh = "tanh"
26 relu = "relu"
27 hard_tanh = "hard_tanh"
28 hard_sigmoid = "hard_sigmoid"
31class TaperMonotonicWeightKernel(MonotonicWeightKernel):
32 half_window_style: Annotated[
33 HalfWindowStyleEnum,
34 Field(
35 default=HalfWindowStyleEnum.rectangle,
36 description="Tapering/activation function to use between transition bounds.",
37 alias=None,
38 json_schema_extra={
39 "units": None,
40 "required": True,
41 "examples": ["hann"],
42 },
43 ),
44 ]
46 def _normalize(self, values: NDArray) -> NDArray:
47 """
48 Normalize input values to the [0, 1] interval based on the transition bounds and threshold direction.
50 This function maps the input array `values` to a normalized scale between 0 and 1, according to the
51 transition_lower_bound and transition_upper_bound attributes. The normalization is performed differently
52 depending on the threshold direction:
54 - If threshold is 'low cut', values below the lower bound are mapped to 0, values above the upper bound are mapped to 1,
55 and values in between are linearly scaled.
56 - If threshold is 'high cut', the mapping is reversed: values below the lower bound are mapped to 1, values above the upper bound to 0,
57 and values in between are linearly scaled in the opposite direction.
59 Parameters
60 ----------
61 values : array-like
62 Input values to be normalized.
64 Returns
65 -------
66 np.ndarray
67 Normalized values in the range [0, 1].
69 Raises
70 ------
71 ValueError
72 If the threshold direction is not recognized.
73 """
74 lb = float(self.transition_lower_bound)
75 ub = float(self.transition_upper_bound)
76 direction = self.threshold
77 transition_range = ub - lb
79 # Handle edge case where transition range is zero (identical bounds)
80 if transition_range == 0:
81 if direction == "low cut":
82 return np.where(values >= lb, 1.0, 0.0)
83 elif direction == "high cut":
84 return np.where(values <= ub, 1.0, 0.0)
85 else:
86 raise ValueError(f"Unknown threshold direction: {direction}")
88 if direction == "low cut":
89 return np.clip((values - lb) / transition_range, 0, 1)
90 elif direction == "high cut":
91 return 1 - np.clip((values - lb) / transition_range, 0, 1)
92 else:
93 raise ValueError(f"Unknown threshold direction: {direction}")
95 def evaluate(self, values: NDArray) -> NDArray:
96 x = self._normalize(values)
97 taper = self.half_window_style
98 if taper == "rectangle":
99 if self.threshold == "low cut":
100 return np.where(values < self.transition_lower_bound, 0.0, 1.0)
101 else:
102 return np.where(values > self.transition_upper_bound, 0.0, 1.0)
103 elif taper == "hann":
104 return 0.5 * (1 - np.cos(np.pi * x))
105 elif taper == "hamming":
106 return 0.54 - 0.46 * np.cos(np.pi * x)
107 elif taper == "blackman":
108 return 0.42 - 0.5 * np.cos(np.pi * x) + 0.08 * np.cos(2 * np.pi * x)
109 else:
110 raise ValueError(f"Unsupported taper style: {taper}")