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

1# ===================================================== 

2# Imports 

3# ===================================================== 

4from typing import Annotated 

5 

6import numpy as np 

7from numpy._typing import NDArray 

8from pydantic import Field 

9 

10from mt_metadata.common.enumerations import StrEnumerationBase 

11from mt_metadata.features.weights.monotonic_weight_kernel import MonotonicWeightKernel 

12 

13 

14# ===================================================== 

15class HalfWindowStyleEnum(StrEnumerationBase): 

16 hamming = "hamming" 

17 hann = "hann" 

18 rectangle = "rectangle" 

19 blackman = "blackman" 

20 

21 

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" 

29 

30 

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 ] 

45 

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. 

49 

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: 

53 

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. 

58 

59 Parameters 

60 ---------- 

61 values : array-like 

62 Input values to be normalized. 

63 

64 Returns 

65 ------- 

66 np.ndarray 

67 Normalized values in the range [0, 1]. 

68 

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 

78 

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}") 

87 

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}") 

94 

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}")