1# =====================================================
2# Imports
3# =====================================================
4from typing import Annotated
5
6from pydantic import computed_field, Field, field_validator, PrivateAttr
7
8from mt_metadata.base import MetadataBase
9
10
11# =====================================================
12
13
14class HMeasurement(MetadataBase):
15 id: Annotated[
16 float | str | None,
17 Field(
18 default=0.0,
19 description="Channel number, could be location.channel_number.",
20 alias=None,
21 json_schema_extra={
22 "units": None,
23 "required": True,
24 "examples": ["1"],
25 },
26 ),
27 ]
28
29 chtype: Annotated[
30 str,
31 Field(
32 default="",
33 description="channel type, should start with an 'h' or 'b'",
34 alias=None,
35 pattern=r"^(RR|rr|[hHbB])[a-zA-Z0-9_]+$",
36 json_schema_extra={
37 "units": None,
38 "required": True,
39 "examples": ["hx"],
40 },
41 ),
42 ]
43
44 x: Annotated[
45 float,
46 Field(
47 default=0.0,
48 description="location of sensor relative center point in north direction",
49 alias=None,
50 json_schema_extra={
51 "units": "meters",
52 "required": True,
53 "examples": ["100.0"],
54 },
55 ),
56 ]
57
58 y: Annotated[
59 float,
60 Field(
61 default=0.0,
62 description="location of sensor relative center point in east direction",
63 alias=None,
64 json_schema_extra={
65 "units": "meters",
66 "required": True,
67 "examples": ["100.0"],
68 },
69 ),
70 ]
71
72 z: Annotated[
73 float,
74 Field(
75 default=0.0,
76 description="location of sensor relative center point in depth",
77 alias=None,
78 json_schema_extra={
79 "units": "meters",
80 "required": True,
81 "examples": ["100.0"],
82 },
83 ),
84 ]
85
86 azm: Annotated[
87 float,
88 Field(
89 default=0.0,
90 description="orientation of the sensor relative to coordinate system, clockwise positive.",
91 alias=None,
92 json_schema_extra={
93 "units": "degrees",
94 "required": True,
95 "examples": ["100.0"],
96 },
97 ),
98 ]
99
100 dip: Annotated[
101 float,
102 Field(
103 default=0.0,
104 description="orientation of the sensor relative to horizontal = 0",
105 alias=None,
106 json_schema_extra={
107 "units": "degrees",
108 "required": True,
109 "examples": ["100.0"],
110 },
111 ),
112 ]
113
114 acqchan: Annotated[
115 str,
116 Field(
117 default="",
118 description="description of acquired channel",
119 alias=None,
120 json_schema_extra={
121 "units": None,
122 "required": True,
123 "examples": ["100.0"],
124 },
125 ),
126 ]
127
128 _fmt_dict: dict[str, str] = PrivateAttr(
129 default={
130 "id": "<",
131 "chtype": "<",
132 "x": "<.2f",
133 "y": "<.2f",
134 "z": "<.2f",
135 "azm": "<.2f",
136 "dip": "<.2f",
137 "acqchan": "<",
138 }
139 )
140
141 @field_validator("id", mode="before")
142 @classmethod
143 def validate_id(cls, value: float | str | None) -> float:
144 """Ensure id is a float or None, convert if necessary"""
145 if isinstance(value, str):
146 try:
147 value = float(value)
148 except ValueError:
149 raise ValueError("id must be a number or convertible to float")
150 elif not isinstance(value, (float, int, type(None))):
151 raise TypeError("id must be a number or None")
152 if value is None:
153 value = 0.0 # Default to 0.0 if None
154 return value
155
156 def __str__(self):
157 return "\n".join([f"{k} = {v}" for k, v in self.to_dict(single=True).items()])
158
159 def __repr__(self):
160 return self.__str__()
161
162 @computed_field
163 @property
164 def channel_number(self) -> int:
165 """Extract channel number from acqchan."""
166 if self.acqchan is not None:
167 if not isinstance(self.acqchan, (int, float)):
168 try:
169 return int("".join(i for i in self.acqchan if i.isdigit()))
170 except (IndexError, ValueError):
171 return 0
172 return int(self.acqchan)
173 return 0
174
175 def write_meas_line(self):
176 """
177 write string
178 :return: DESCRIPTION
179 :rtype: TYPE
180
181 """
182
183 line = [">hmeas".upper()]
184
185 for mkey, mfmt in self._fmt_dict.items():
186 try:
187 line.append(f"{mkey.upper()}={getattr(self, mkey):{mfmt}}")
188 except (ValueError, TypeError):
189 line.append(f"{mkey.upper()}={0.0:{mfmt}}")
190
191 return f"{' '.join(line)}\n"