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
« 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._array_like import NDArray
8from pydantic import computed_field, Field
10from mt_metadata.common.enumerations import StrEnumerationBase
11from mt_metadata.features.weights.base import Base
14# =====================================================
15class ThresholdEnum(StrEnumerationBase):
16 low_cut = "low cut"
17 high_cut = "high cut"
20class StyleEnum(StrEnumerationBase):
21 taper = "taper"
22 activation = "activation"
25class MonotonicWeightKernel(Base):
26 """
27 MonotonicWeightKernel
29 Base class for monotonic weight kernels.
30 Handles bounds, normalization, and direction.
32 A weighting kernel that applies a monotonic activation/taper function between defined
33 lower and upper bounds, based on a given threshold direction.
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.
40 """
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 ]
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 ]
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 ]
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 ]
98 @computed_field
99 @property
100 def _has_finite_transition_bounds(self) -> bool:
101 """
102 Check if the transition bounds are finite.
104 Returns
105 -------
106 bool
107 True if both transition_lower_bound and transition_upper_bound are finite, False otherwise.
108 """
110 lb = float(self.transition_lower_bound)
111 ub = float(self.transition_upper_bound)
112 return np.isfinite(lb) and np.isfinite(ub)
114 def _normalize(self, values: NDArray) -> NDArray:
115 """
116 Normalize input values to the [0, 1] interval based on finite transition bounds.
118 Only supports finite lower and upper bounds. Subclasses should override this method
119 if they wish to support infinite bounds or custom normalization.
121 Parameters
122 ----------
123 values : array-like
124 Input values to be normalized.
126 Returns
127 -------
128 np.ndarray
129 Normalized values in the range [0, 1].
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 )