Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ features \ weights \ monotonic_weight_kernel.py: 100%

30 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._array_like import NDArray 

8from pydantic import computed_field, Field 

9 

10from mt_metadata.common.enumerations import StrEnumerationBase 

11from mt_metadata.features.weights.base import Base 

12 

13 

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

15class ThresholdEnum(StrEnumerationBase): 

16 low_cut = "low cut" 

17 high_cut = "high cut" 

18 

19 

20class StyleEnum(StrEnumerationBase): 

21 taper = "taper" 

22 activation = "activation" 

23 

24 

25class MonotonicWeightKernel(Base): 

26 """ 

27 MonotonicWeightKernel 

28 

29 Base class for monotonic weight kernels. 

30 Handles bounds, normalization, and direction. 

31 

32 A weighting kernel that applies a monotonic activation/taper function between defined 

33 lower and upper bounds, based on a given threshold direction. 

34 

35 There are two main types of monotonic kernels: taper and activation. The taper function 

36 is used to smoothly transition between the lower and upper bounds over some finite interval, 

37 while the activation style offers options that asymptote to 0 or 1, such as sigmoid or tanh. 

38 Thus the activation style supports +/- infinity bounds, while the taper style requires finite bounds. 

39 

40 """ 

41 

42 threshold: Annotated[ 

43 ThresholdEnum, 

44 Field( 

45 default=ThresholdEnum.low_cut, 

46 description="Which side of a threshold should be downweighted.", 

47 alias=None, 

48 json_schema_extra={ 

49 "units": None, 

50 "required": True, 

51 "examples": ["low cut"], 

52 }, 

53 ), 

54 ] 

55 

56 style: Annotated[ 

57 StyleEnum, 

58 Field( 

59 default=StyleEnum.taper, 

60 description="Tapering/activation function to use between transition bounds.", 

61 alias=None, 

62 json_schema_extra={ 

63 "units": None, 

64 "required": True, 

65 "examples": ["activation"], 

66 }, 

67 ), 

68 ] 

69 

70 transition_lower_bound: Annotated[ 

71 float, 

72 Field( 

73 default=-1000000000.0, 

74 description="Start of the taper region (weight begins to change).", 

75 alias=None, 

76 json_schema_extra={ 

77 "units": None, 

78 "required": True, 

79 "examples": ["-inf"], 

80 }, 

81 ), 

82 ] 

83 

84 transition_upper_bound: Annotated[ 

85 float, 

86 Field( 

87 default=1000000000.0, 

88 description="End of the taper region (weight finishes changing).", 

89 alias=None, 

90 json_schema_extra={ 

91 "units": None, 

92 "required": True, 

93 "examples": ["+inf"], 

94 }, 

95 ), 

96 ] 

97 

98 @computed_field 

99 @property 

100 def _has_finite_transition_bounds(self) -> bool: 

101 """ 

102 Check if the transition bounds are finite. 

103 

104 Returns 

105 ------- 

106 bool 

107 True if both transition_lower_bound and transition_upper_bound are finite, False otherwise. 

108 """ 

109 

110 lb = float(self.transition_lower_bound) 

111 ub = float(self.transition_upper_bound) 

112 return np.isfinite(lb) and np.isfinite(ub) 

113 

114 def _normalize(self, values: NDArray) -> NDArray: 

115 """ 

116 Normalize input values to the [0, 1] interval based on finite transition bounds. 

117 

118 Only supports finite lower and upper bounds. Subclasses should override this method 

119 if they wish to support infinite bounds or custom normalization. 

120 

121 Parameters 

122 ---------- 

123 values : array-like 

124 Input values to be normalized. 

125 

126 Returns 

127 ------- 

128 np.ndarray 

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

130 

131 Raises 

132 ------ 

133 ValueError 

134 If either transition bound is infinite. 

135 """ 

136 if self._has_finite_transition_bounds: 

137 lb = float(self.transition_lower_bound) 

138 ub = float(self.transition_upper_bound) 

139 values = np.asarray(values) 

140 return (values - lb) / (ub - lb) 

141 else: 

142 raise ValueError( 

143 "MonotonicWeightKernel only supports finite transition bounds. " 

144 "Override _normalize in subclasses for infinite bounds." 

145 )