Coverage for src/arcade_collection/output/convert_model_units.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2024-12-09 19:07 +0000

1from __future__ import annotations 

2 

3import re 

4from typing import TYPE_CHECKING 

5 

6if TYPE_CHECKING: 

7 import pandas as pd 

8 

9 

10def convert_model_units( 

11 data: pd.DataFrame, 

12 ds: float | None, 

13 dt: float | None, 

14 regions: list[str] | str | None = None, 

15) -> None: 

16 """ 

17 Convert data from simulation units to true units. 

18 

19 Simulations use spatial unit of voxels and temporal unit of ticks. Spatial 

20 resolution (microns/voxel) and temporal resolution (hours/tick) are used to 

21 convert data to true units. If spatial or temporal resolution is not given, 

22 they will be estimated from the ``KEY`` column of the data. 

23 

24 The following columns are added to the data: 

25 

26 ============= =================== ============================= 

27 Target column Source column(s) Conversion 

28 ============= =================== ============================= 

29 ``time`` ``TICK`` ``dt * TICK`` 

30 ``volume`` ``NUM_VOXELS`` ``ds * ds * ds * NUM_VOXELS`` 

31 ``height`` ``MAX_Z`` ``MIN_Z`` ``ds * (MAX_Z - MIN_Z + 1)`` 

32 ``cx`` ``CENTER_X`` ``ds * CENTER_X`` 

33 ``cy`` ``CENTER_Y`` ``ds * CENTER_Y`` 

34 ``cz`` ``CENTER_Z`` ``ds * CENTER_Z`` 

35 ============= =================== ============================= 

36 

37 For each region (other than ``DEFAULT``), the following columns are added to the data: 

38 

39 ================= ================================= ========================================== 

40 Target column Source column(s) Conversion 

41 ================= ================================= ========================================== 

42 ``volume.REGION`` ``NUM_VOXELS.REGION`` ``ds * ds * ds * NUM_VOXELS.REGION`` 

43 ``height.REGION`` ``MAX_Z.REGION`` ``MIN_Z.REGION`` ``ds * (MAX_Z.REGION - MIN_Z.REGION + 1)`` 

44 ================= ================================= ========================================== 

45 

46 The following property columns are rescaled: 

47 

48 ===================== ===================== ========================== 

49 Target column Source column(s) Conversion 

50 ===================== ===================== ========================== 

51 ``area`` ``area`` ``ds * ds * area`` 

52 ``perimeter`` ``perimeter`` ``ds * perimeter`` 

53 ``axis_major_length`` ``axis_major_length`` ``ds * axis_major_length`` 

54 ``axis_minor_length`` ``axis_minor_length`` ``ds * axis_minor_length`` 

55 ===================== ===================== ========================== 

56 

57 Parameters 

58 ---------- 

59 data 

60 Simulation data. 

61 ds 

62 Spatial resolution in microns/voxel, use None to estimate from keys. 

63 dt 

64 Temporal resolution in hours/tick, use None to estimate from keys. 

65 regions 

66 List of regions. 

67 """ 

68 

69 if dt is None: 

70 dt = data["KEY"].apply(estimate_temporal_resolution) 

71 

72 if ds is None: 

73 ds = data["KEY"].apply(estimate_spatial_resolution) 

74 

75 convert_temporal_units(data, dt) 

76 convert_spatial_units(data, ds) 

77 

78 if regions is None: 

79 return 

80 

81 if isinstance(regions, str): 

82 regions = [regions] 

83 

84 for region in regions: 

85 if region == "DEFAULT": 

86 continue 

87 

88 convert_spatial_units(data, ds, region) 

89 

90 

91def convert_temporal_units(data: pd.DataFrame, dt: float) -> None: 

92 """ 

93 Convert temporal data from simulation units to true units. 

94 

95 Simulations use temporal unit of ticks. Temporal resolution (hours/tick) is 

96 used to convert data to true units. 

97 

98 The following temporal columns are converted: 

99 

100 ============= ================ ============= 

101 Target column Source column(s) Conversion 

102 ============= ================ ============= 

103 ``time`` ``TICK`` ``dt * TICK`` 

104 ============= ================ ============= 

105 

106 Parameters 

107 ---------- 

108 data 

109 Simulation data. 

110 dt 

111 Temporal resolution in hours/tick. 

112 """ 

113 

114 if "TICK" in data.columns: 

115 data["time"] = round(dt * data["TICK"], 2) 

116 

117 

118def convert_spatial_units(data: pd.DataFrame, ds: float, region: str | None = None) -> None: 

119 """ 

120 Convert spatial data from simulation units to true units. 

121 

122 Simulations use spatial unit of voxels. Spatial resolution (microns/voxel) 

123 is used to convert data to true units. 

124 

125 The following spatial columns are converted: 

126 

127 ===================== ===================== ============================= 

128 Target column Source column(s) Conversion 

129 ===================== ===================== ============================= 

130 ``volume`` ``NUM_VOXELS`` ``ds * ds * ds * NUM_VOXELS`` 

131 ``height`` ``MAX_Z`` ``MIN_Z`` ``ds * (MAX_Z - MIN_Z + 1)`` 

132 ``cx`` ``CENTER_X`` ``ds * CENTER_X`` 

133 ``cy`` ``CENTER_Y`` ``ds * CENTER_Y`` 

134 ``cz`` ``CENTER_Z`` ``ds * CENTER_Z`` 

135 ``area`` ``area`` ``ds * ds * area`` 

136 ``perimeter`` ``perimeter`` ``ds * perimeter`` 

137 ``axis_major_length`` ``axis_major_length`` ``ds * axis_major_length`` 

138 ``axis_minor_length`` ``axis_minor_length`` ``ds * axis_minor_length`` 

139 ===================== ===================== ============================= 

140 

141 Note that the centroid columns (``cx``, ``cy``, and ``cz``) are only 

142 converted for the entire cell (``region == None``). 

143 

144 Parameters 

145 ---------- 

146 data 

147 Simulation data. 

148 ds 

149 Spatial resolution in microns/voxel. 

150 region 

151 Name of region. 

152 """ 

153 

154 suffix = "" if region is None else f".{region}" 

155 

156 if f"NUM_VOXELS{suffix}" in data.columns: 

157 data[f"volume{suffix}"] = ds * ds * ds * data[f"NUM_VOXELS{suffix}"] 

158 

159 if f"MAX_Z{suffix}" in data.columns and f"MIN_Z{suffix}" in data.columns: 

160 data[f"height{suffix}"] = ds * (data[f"MAX_Z{suffix}"] - data[f"MIN_Z{suffix}"] + 1) 

161 

162 if "CENTER_X" in data.columns and region is None: 

163 data["cx"] = ds * data["CENTER_X"] 

164 

165 if "CENTER_Y" in data.columns and region is None: 

166 data["cy"] = ds * data["CENTER_Y"] 

167 

168 if "CENTER_Z" in data.columns and region is None: 

169 data["cz"] = ds * data["CENTER_Z"] 

170 

171 property_conversions = [ 

172 ("area", ds * ds), 

173 ("perimeter", ds), 

174 ("axis_major_length", ds), 

175 ("axis_minor_length", ds), 

176 ] 

177 

178 for name, conversion in property_conversions: 

179 column = f"{name}{suffix}" 

180 

181 if column not in data.columns: 

182 continue 

183 

184 data[column] = data[column] * conversion 

185 

186 

187def estimate_temporal_resolution(key: str) -> float: 

188 """ 

189 Estimate temporal resolution based on condition key. 

190 

191 If the key contains ``DT##``, where ``##`` denotes the temporal resolution 

192 in minutes/tick, temporal resolution is estimated from ``##``. Otherwise, 

193 the default temporal resolution is 1 hours/tick. 

194 

195 Parameters 

196 ---------- 

197 key 

198 Condition key. 

199 

200 Returns 

201 ------- 

202 : 

203 Temporal resolution (hours/tick). 

204 """ 

205 

206 matches = [re.fullmatch(r"DT([0-9]+)", k) for k in key.split("_")] 

207 return next((float(match.group(1)) / 60 for match in matches if match is not None), 1.0) 

208 

209 

210def estimate_spatial_resolution(key: str) -> float: 

211 """ 

212 Estimate spatial resolution based on condition key. 

213 

214 If the key contains ``DS##``, where ``##`` denotes the spatial resolution 

215 in micron/voxel, spatial resolution is estimated from ``##``. Otherwise, 

216 the default spatial resolution is 1 micron/voxel. 

217 

218 Parameters 

219 ---------- 

220 key 

221 Condition key. 

222 

223 Returns 

224 ------- 

225 : 

226 Spatial resolution (micron/voxel). 

227 """ 

228 

229 matches = [re.fullmatch(r"DS([0-9]+)", k) for k in key.split("_")] 

230 return next((float(match.group(1)) for match in matches if match is not None), 1.0)