Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ transfer_functions \ io \ edi \ metadata \ hmeasurement.py: 92%

49 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 

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"