Coverage for local_installation_linux/mumott/data_handling/data_container.py: 86%

306 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2025-11-04 08:14 +0000

1import logging 

2 

3import h5py as h5 

4import numpy as np 

5import os 

6 

7from numpy.typing import NDArray 

8from scipy.spatial.transform import Rotation 

9 

10from mumott.core.deprecation_warning import print_deprecation_warning 

11from mumott.core.geometry import Geometry 

12from mumott.core.projection_stack import ProjectionStack, Projection 

13 

14logger = logging.getLogger(__name__) 

15 

16# used to easily keep track of preferred keys 

17_preferred_keys = dict(rotations='inner_angle', 

18 tilts='outer_angle', 

19 offset_j='j_offset', 

20 offset_k='k_offset', 

21 rot_mat='rotation_matrix') 

22 

23 

24def _deprecated_key_warning(deprecated_key: str): 

25 """Internal method for deprecation warnings of keys.""" 

26 if deprecated_key in _preferred_keys: 

27 # Only print once. 

28 preferred_key = _preferred_keys.pop(deprecated_key) 

29 print_deprecation_warning( 

30 f'Entry name {deprecated_key} is deprecated. Use {preferred_key} instead.') 

31 

32 

33class DataContainer: 

34 

35 """ 

36 Instances of this class represent data read from an input file in a format suitable for further analysis. 

37 The two core components are :attr:`geometry` and :attr:`projections`. 

38 The latter comprises a list of :class:`Projection <mumott.core.projection_stack.Projection>` 

39 instances, each of which corresponds to a single measurement. 

40 

41 By default all data is read, which can be rather time consuming and unnecessary in some cases, 

42 e.g., when aligning data. 

43 In those cases, one can skip loading the actual measurements by setting :attr:`skip_data` to ``True``. 

44 The geometry information and supplementary information such as the diode data will still be read. 

45 

46 Example 

47 ------- 

48 The following code snippet illustrates the basic use of the :class:`DataContainer` class. 

49 

50 First we create a :class:`DataContainer` instance, providing the path to the data file to be read. 

51 

52 >>> from mumott.data_handling import DataContainer 

53 >>> dc = DataContainer('tests/test_full_circle.h5') 

54 

55 One can then print a short summary of the content of the :class:`DataContainer` instance. 

56 

57 >>> print(dc) 

58 ========================================================================== 

59 DataContainer 

60 -------------------------------------------------------------------------- 

61 Corrected for transmission : False 

62 ... 

63 

64 To access individual measurements we can use the :attr:`projections` attribute. 

65 The latter behaves like a list, where the elements of the list are 

66 :class:`Projection <mumott.core.projection_stack.Projection>` objects, 

67 each of which represents an individual measurement. 

68 We can print a summary of the content of the first projection. 

69 

70 >>> print(dc.projections[0]) 

71 -------------------------------------------------------------------------- 

72 Projection 

73 -------------------------------------------------------------------------- 

74 hash_data : 3f0ba8 

75 hash_diode : 808328 

76 hash_weights : 088d39 

77 rotation : [1. 0. 0.], [ 0. -1. 0.], [ 0. 0. -1.] 

78 j_offset : 0.0 

79 k_offset : 0.3 

80 inner_angle : None 

81 outer_angle : None 

82 inner_axis : 0.0, 0.0, -1.0 

83 outer_axis : 1.0, 0.0, 0.0 

84 -------------------------------------------------------------------------- 

85 

86 

87 Parameters 

88 ---------- 

89 data_path : str, optional 

90 Path of the data file relative to the directory of execution. 

91 If None, a data container with an empty :attr:`projections` 

92 attached will be initialized. 

93 data_type : str, optional 

94 The type (or format) of the data file. Supported values are 

95 ``h5`` (default) for hdf5 format and ``None`` for an empty ``DataContainer`` 

96 that can be manually populated. 

97 skip_data : bool, optional 

98 If ``True``, will skip data from individual measurements when loading the file. 

99 This will result in a functioning :attr:`geometry` instance as well as 

100 :attr:`diode` and :attr:`weights` entries in each projection, but 

101 :attr:`data` will be empty. 

102 nonfinite_replacement_value : float, optional 

103 Value to replace nonfinite values (``np.nan``, ``np.inf``, and ``-np.inf``) with in the 

104 data, diode, and weights. If ``None`` (default), an error is raised 

105 if any nonfinite values are present in these input fields. 

106 """ 

107 def __init__(self, 

108 data_path: str = None, 

109 data_type: str = 'h5', 

110 skip_data: bool = False, 

111 nonfinite_replacement_value: float = None): 

112 self._correct_for_transmission_called = False 

113 self._projections = ProjectionStack() 

114 self._geometry_dictionary = dict() 

115 self._skip_data = skip_data 

116 self._nonfinite_replacement_value = nonfinite_replacement_value 

117 if data_path is not None: 

118 if data_type == 'h5': 118 ↛ 121line 118 didn't jump to line 121, because the condition on line 118 was never false

119 self._h5_to_projections(data_path) 

120 else: 

121 raise ValueError(f'Unknown data_type: {data_type} for' 

122 ' load_only_geometry=False.') 

123 

124 def _h5_to_projections(self, file_path: str): 

125 """ 

126 Internal method for loading data from hdf5 file. 

127 """ 

128 h5_data = h5.File(file_path, 'r') 

129 projections = h5_data['projections'] 

130 number_of_projections = len(projections) 

131 max_shape = (0, 0) 

132 inner_axis = np.array((0., 0., -1.)) 

133 outer_axis = np.array((1., 0., 0.)) 

134 found_inner_in_base = False 

135 found_outer_in_base = False 

136 if 'inner_axis' in h5_data: 

137 inner_axis = h5_data['inner_axis'][:] 

138 logger.info('Inner axis found in dataset base directory. This will override the default.') 

139 found_inner_in_base = True 

140 if 'outer_axis' in h5_data: 

141 outer_axis = h5_data['outer_axis'][:] 

142 logger.info('Outer axis found in dataset base directory. This will override the default.') 

143 found_outer_in_base = True 

144 

145 for i in range(number_of_projections): 

146 p = projections[f'{i}'] 

147 if 'diode' in p: 147 ↛ 145line 147 didn't jump to line 145, because the condition on line 147 was never false

148 max_shape = np.max((max_shape, p['diode'].shape), axis=0) 

149 for i in range(number_of_projections): 

150 p = projections[f'{i}'] 

151 if 'diode' in p: 151 ↛ 157line 151 didn't jump to line 157, because the condition on line 151 was never false

152 diode = np.ascontiguousarray(np.copy(p['diode']).astype(np.float64)) 

153 pad_sequence = np.array(((0, max_shape[0] - diode.shape[0]), 

154 (0, max_shape[1] - diode.shape[1]), 

155 (0, 0))) 

156 diode = np.pad(diode, pad_sequence[:-1]) 

157 elif 'data' in p: 

158 diode = None 

159 pad_sequence = np.array(((0, max_shape[0] - p['data'].shape[0]), 

160 (0, max_shape[1] - p['data'].shape[1]), 

161 (0, 0))) 

162 else: 

163 pad_sequence = np.zeros((3, 2)) 

164 if not self._skip_data: 

165 data = np.ascontiguousarray(np.copy(p['data']).astype(np.float64)) 

166 data = np.pad(data, pad_sequence) 

167 if 'weights' in p: 

168 weights = np.ascontiguousarray(np.copy(p['weights']).astype(np.float64)) 

169 if weights.ndim == 2 or (weights.ndim == 3 and weights.shape[-1] == 1): 

170 weights = weights.reshape(weights.shape[:2]) 

171 weights = weights[..., np.newaxis] * \ 

172 np.ones((1, 1, data.shape[-1])).astype(np.float64) 

173 weights = np.pad(weights, pad_sequence) 

174 else: 

175 weights = np.ones_like(data) 

176 else: 

177 data = None 

178 if 'weights' in p: 178 ↛ 182line 178 didn't jump to line 182, because the condition on line 178 was never false

179 weights = np.ascontiguousarray(np.copy(p['weights']).astype(np.float64)) 

180 weights = np.pad(weights, pad_sequence[:weights.ndim]) 

181 else: 

182 if diode is None: 

183 weights = None 

184 else: 

185 weights = np.ones_like(diode) 

186 weights = np.pad(weights, pad_sequence[:weights.ndim]) 

187 # Look for rotation information and load if available 

188 if 'inner_axis' in p: 

189 p_inner_axis = p['inner_axis'][:] 

190 if found_inner_in_base: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true

191 logger.info(f'Inner axis found in projection {i}. This will override ' 

192 'the value found in the base directory for all projections ' 

193 'where it is found.') 

194 found_inner_in_base = False 

195 # override default only if projection zero 

196 elif i == 0: 196 ↛ 203line 196 didn't jump to line 203, because the condition on line 196 was never false

197 logger.info(f'Inner axis found in projection {i}. This will override ' 

198 'the default value for all projections, if they do not specify ' 

199 'another axis.') 

200 inner_axis = p_inner_axis 

201 else: 

202 p_inner_axis = inner_axis 

203 if 'outer_axis' in p: 

204 p_outer_axis = p['outer_axis'][:] 

205 if found_outer_in_base: 205 ↛ 206line 205 didn't jump to line 206, because the condition on line 205 was never true

206 logger.info(f'Outer axis found in projection {i}. This will override ' 

207 'the value found in the base directory for all projections ' 

208 'where it is found.') 

209 found_outer_in_base = False 

210 elif i == 0: 210 ↛ 218line 210 didn't jump to line 218, because the condition on line 210 was never false

211 logger.info(f'Inner axis found in projection {i}. This will override ' 

212 'the default value for all projections, if they do not specify ' 

213 'another axis.') 

214 outer_axis = p_outer_axis 

215 else: 

216 p_outer_axis = outer_axis 

217 

218 inner_angle = None 

219 outer_angle = None 

220 

221 if 'inner_angle' in p: 

222 inner_angle = np.copy(p['inner_angle'][...]).flatten()[0] 

223 # if at least one angle exists, assume other is 0 by default. 

224 if outer_angle is None: 224 ↛ 231line 224 didn't jump to line 231, because the condition on line 224 was never false

225 outer_angle = 0 

226 elif 'rotations' in p: 

227 inner_angle = np.copy(p['rotations'][...]).flatten()[0] 

228 _deprecated_key_warning('rotations') 

229 if outer_angle is None: 229 ↛ 231line 229 didn't jump to line 231, because the condition on line 229 was never false

230 outer_angle = 0 

231 if 'outer_angle' in p: 

232 outer_angle = np.copy(p['outer_angle'][...]).flatten()[0] 

233 if inner_angle is None: 233 ↛ 234line 233 didn't jump to line 234, because the condition on line 233 was never true

234 inner_angle = 0 

235 elif 'tilts' in p: 

236 outer_angle = np.copy(p['tilts'][...]).flatten()[0] 

237 _deprecated_key_warning('tilts') 

238 if inner_angle is None: 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true

239 inner_angle = 0 

240 

241 if 'rotation_matrix' in p: 

242 rotation = p['rotation_matrix'][...] 

243 if i == 0: 

244 logger.info('Rotation matrices were loaded from the input file.') 

245 elif 'rot_mat' in p: 

246 rotation = p['rot_mat'][...] 

247 _deprecated_key_warning('rot_mat') 

248 if i == 0: 

249 logger.info('Rotation matrices were loaded from the input file.') 

250 elif outer_angle is not None: 250 ↛ 259line 250 didn't jump to line 259, because the condition on line 250 was never false

251 R_inner = Rotation.from_rotvec(inner_angle * p_inner_axis).as_matrix() 

252 R_outer = Rotation.from_rotvec(outer_angle * p_outer_axis).as_matrix() 

253 rotation = R_outer @ R_inner 

254 if i == 0: 

255 logger.info('Rotation matrix generated from inner and outer angles,' 

256 ' along with inner and outer rotation axis vectors.' 

257 ' Rotation and tilt angles assumed to be in radians.') 

258 else: 

259 rotation = np.eye(3) 

260 if i == 0: 

261 logger.info('No rotation information found.') 

262 

263 # default to 0-dim array to simplify subsequent code 

264 j_offset = np.array(0) 

265 if 'j_offset' in p: 

266 j_offset = p['j_offset'][...] 

267 elif 'offset_j' in p: 267 ↛ 271line 267 didn't jump to line 271, because the condition on line 267 was never false

268 j_offset = p['offset_j'][...] 

269 _deprecated_key_warning('offset_j') 

270 # offset will be either numpy/size-0 or size-1 array, ravel and extract. 

271 j_offset = j_offset.ravel()[0] 

272 j_offset -= pad_sequence[0, 1] * 0.5 

273 

274 k_offset = np.array(0) 

275 if 'k_offset' in p: 

276 k_offset = p['k_offset'][...] 

277 elif 'offset_k' in p: 277 ↛ 280line 277 didn't jump to line 280, because the condition on line 277 was never false

278 k_offset = p['offset_k'][...] 

279 _deprecated_key_warning('offset_k') 

280 k_offset = k_offset.ravel()[0] 

281 k_offset -= pad_sequence[1, 1] * 0.5 

282 

283 if not self._skip_data: 

284 self._handle_nonfinite_values(data) 

285 self._handle_nonfinite_values(weights) 

286 self._handle_nonfinite_values(diode) 

287 

288 projection = Projection(data=data, 

289 diode=diode, 

290 weights=weights, 

291 rotation=rotation, 

292 j_offset=j_offset, 

293 k_offset=k_offset, 

294 outer_angle=outer_angle, 

295 inner_angle=inner_angle, 

296 inner_axis=p_inner_axis, 

297 outer_axis=p_outer_axis 

298 ) 

299 self._projections.append(projection) 

300 if not self._skip_data: 

301 self._projections.geometry.detector_angles = np.copy(h5_data['detector_angles']) 

302 self._estimate_angular_coverage(self._projections.geometry.detector_angles) 

303 if 'volume_shape' in h5_data.keys(): 303 ↛ 306line 303 didn't jump to line 306, because the condition on line 303 was never false

304 self._projections.geometry.volume_shape = np.copy(h5_data['volume_shape']).astype(int) 

305 else: 

306 self._projections.geometry.volume_shape = np.array(max_shape)[[0, 0, 1]] 

307 # Load sample geometry information 

308 if 'p_direction_0' in h5_data.keys(): # TODO check for orthogonality, normality 

309 self._projections.geometry.p_direction_0 = np.copy(h5_data['p_direction_0'][...]) 

310 self._projections.geometry.j_direction_0 = np.copy(h5_data['j_direction_0'][...]) 

311 self._projections.geometry.k_direction_0 = np.copy(h5_data['k_direction_0'][...]) 

312 logger.info('Sample geometry loaded from file.') 

313 else: 

314 logger.info('No sample geometry information was found. Default mumott geometry assumed.') 

315 

316 # Load detector geometry information 

317 if 'detector_direction_origin' in h5_data.keys(): # TODO check for orthogonality, normality 

318 self._projections.geometry.detector_direction_origin = np.copy( 

319 h5_data['detector_direction_origin'][...]) 

320 self._projections.geometry.detector_direction_positive_90 = np.copy( 

321 h5_data['detector_direction_positive_90'][...]) 

322 logger.info('Detector geometry loaded from file.') 

323 else: 

324 logger.info('No detector geometry information was found. Default mumott geometry assumed.') 

325 

326 # Load scattering angle 

327 if 'two_theta' in h5_data: 

328 self._projections.geometry.two_theta = np.array(h5_data['two_theta']) 

329 logger.info('Scattering angle loaded from data.') 

330 

331 def write(self, filename: str) -> None: 

332 """ 

333 Save data and geometry information to a mumott .h5 file. 

334 

335 Parameters 

336 ---------- 

337 filename 

338 Path of the data file. 

339 

340 Raises 

341 ------ 

342 ValueError 

343 If the file name does not end on ".h5". 

344 """ 

345 

346 extension = os.path.splitext(filename)[-1] 

347 if not extension.lower() in ('.h5', ''): 

348 raise ValueError('Only .h5 files supported. Data was not saved.') 

349 if extension == '': 

350 filename = filename + '.h5' 

351 

352 # Alias for line-length limit 

353 g = self.geometry 

354 

355 # Build file 

356 with h5.File(filename, 'w') as file: 

357 

358 # Assign global parameters 

359 file.create_dataset('detector_angles', data=g.detector_angles) 

360 file.create_dataset('volume_shape', data=g.volume_shape) 

361 

362 file.create_dataset('p_direction_0', data=g.p_direction_0) 

363 file.create_dataset('j_direction_0', data=g.j_direction_0) 

364 file.create_dataset('k_direction_0', data=g.k_direction_0) 

365 

366 file.create_dataset('detector_direction_origin', data=g.detector_direction_origin) 

367 file.create_dataset('detector_direction_positive_90', data=g.detector_direction_positive_90) 

368 

369 # Make data group 

370 grp = file.create_group('projections') 

371 

372 # Loop through projections 

373 for ii, (projection, geom_tpl) in enumerate(zip(self.projections, g)): 

374 

375 # Make a group for each projection 

376 subgrp = grp.create_group(str(ii)) 

377 

378 # Prpjection data 

379 subgrp.create_dataset('data', data=projection.data) 

380 subgrp.create_dataset('diode', data=projection.diode) 

381 subgrp.create_dataset('weights', data=projection.weights) 

382 subgrp.create_dataset('j_offset', data=geom_tpl.j_offset) 

383 subgrp.create_dataset('k_offset', data=geom_tpl.k_offset) 

384 subgrp.create_dataset('rotation_matrix', data=geom_tpl.rotation) 

385 

386 subgrp.create_dataset('inner_axis', data=geom_tpl.inner_axis) 

387 subgrp.create_dataset('inner_angle', data=geom_tpl.inner_angle) 

388 subgrp.create_dataset('outer_axis', data=geom_tpl.outer_axis) 

389 subgrp.create_dataset('outer_angle', data=geom_tpl.outer_angle) 

390 

391 def _estimate_angular_coverage(self, detector_angles: list): 

392 """Check if full circle appears covered in data or not.""" 

393 delta = np.abs(detector_angles[0] - detector_angles[-1] % (2 * np.pi)) 

394 if abs(delta - np.pi) < min(delta, abs(delta - 2 * np.pi)): 

395 self.geometry.full_circle_covered = False 

396 else: 

397 logger.warning('The detector angles appear to cover a full circle. This ' 

398 'is only expected for WAXS data.') 

399 self.geometry.full_circle_covered = True 

400 

401 def _handle_nonfinite_values(self, array): 

402 """ Internal convenience function for handling nonfinite values. """ 

403 if np.any(~np.isfinite(array)): 

404 if self._nonfinite_replacement_value is not None: 

405 np.nan_to_num(array, copy=False, nan=self._nonfinite_replacement_value, 

406 posinf=self._nonfinite_replacement_value, 

407 neginf=self._nonfinite_replacement_value) 

408 else: 

409 raise ValueError('Nonfinite values detected in input, which is not permitted by default. ' 

410 'To permit and replace nonfinite values, please set ' 

411 'nonfinite_replacement_value to desired value.') 

412 

413 def __len__(self) -> int: 

414 """ 

415 Length of the :attr:`projections <mumott.data_handling.projection_stack.ProjectionStack>` 

416 attached to this :class:`DataContainer` instance. 

417 """ 

418 return len(self._projections) 

419 

420 def append(self, f: Projection) -> None: 

421 """ 

422 Appends a :class:`Projection <mumott.core.projection_stack.Projection>` 

423 to the :attr:`projections` attached to this :class:`DataContainer` instance. 

424 """ 

425 self._projections.append(f) 

426 

427 @property 

428 def projections(self) -> ProjectionStack: 

429 """ The projections, containing data and geometry. """ 

430 return self._projections 

431 

432 @property 

433 def geometry(self) -> Geometry: 

434 """ Container of geometry information. """ 

435 return self._projections.geometry 

436 

437 @property 

438 def data(self) -> NDArray[np.float64]: 

439 """ 

440 The data in the :attr:`projections` object 

441 attached to this :class:`DataContainer` instance. 

442 """ 

443 return self._projections.data 

444 

445 @property 

446 def diode(self) -> NDArray[np.float64]: 

447 """ 

448 The diode data in the :attr:`projections` object 

449 attached to this :class:`DataContainer` instance. 

450 """ 

451 return self._projections.diode 

452 

453 @property 

454 def weights(self) -> NDArray[np.float64]: 

455 """ 

456 The weights in the :attr:`projections` object 

457 attached to this :class:`DataContainer` instance. 

458 """ 

459 return self._projections.weights 

460 

461 def correct_for_transmission(self) -> None: 

462 """ 

463 Applies correction from the input provided in the :attr:`diode 

464 <mumott.core.projection_stack.Projection.diode>` field. Should 

465 only be used if this correction has *not* been applied yet. 

466 """ 

467 if self._correct_for_transmission_called: 467 ↛ 468line 467 didn't jump to line 468, because the condition on line 467 was never true

468 logger.info( 

469 'DataContainer.correct_for_transmission() has been called already.' 

470 ' The correction has been applied previously, and the repeat call is ignored.') 

471 return 

472 

473 data = self._projections.data / self._projections.diode[..., np.newaxis] 

474 

475 for i, f in enumerate(self._projections): 

476 f.data = data[i] 

477 self._correct_for_transmission_called = True 

478 

479 def _Rx(self, angle: float) -> NDArray[float]: 

480 """ Generate a rotation matrix for rotations around 

481 the x-axis, following the convention that vectors 

482 have components ordered ``(x, y, z)``. 

483 

484 Parameters 

485 ---------- 

486 angle 

487 The angle of the rotation. 

488 

489 Returns 

490 ------- 

491 R 

492 The rotation matrix. 

493 

494 Notes 

495 ----- 

496 For a vector ``v`` with shape ``(..., 3)`` and a rotation angle :attr:`angle`, 

497 ``np.einsum('ji, ...i', _Rx(angle), v)`` rotates the vector around the 

498 ``x``-axis by :attr:`angle`. If the 

499 coordinate system is being rotated, then 

500 ``np.einsum('ij, ...i', _Rx(angle), v)`` gives the vector in the 

501 new coordinate system. 

502 """ 

503 return Rotation.from_euler('X', angle).as_matrix() 

504 

505 def _Ry(self, angle: float) -> NDArray[float]: 

506 """ Generate a rotation matrix for rotations around 

507 the y-axis, following the convention that vectors 

508 have components ordered ``(x, y, z)``. 

509 

510 Parameters 

511 ---------- 

512 angle 

513 The angle of the rotation. 

514 

515 Returns 

516 ------- 

517 R 

518 The rotation matrix. 

519 

520 Notes 

521 ----- 

522 For a vector ``v`` with shape ``(..., 3)`` and a rotation angle ``angle``, 

523 ``np.einsum('ji, ...i', _Ry(angle), v)`` rotates the vector around the 

524 For a vector ``v`` with shape ``(..., 3)`` and a rotation angle :attr:`angle`, 

525 ``np.einsum('ji, ...i', _Ry(angle), v)`` rotates the vector around the 

526 ``y``-axis by :attr:`angle`. If the 

527 coordinate system is being rotated, then 

528 ``np.einsum('ij, ...i', _Ry(angle), v)`` gives the vector in the 

529 new coordinate system. 

530 """ 

531 return Rotation.from_euler('Y', angle).as_matrix() 

532 

533 def _Rz(self, angle: float) -> NDArray[float]: 

534 """ Generate a rotation matrix for rotations around 

535 the z-axis, following the convention that vectors 

536 have components ordered ``(x, y, z)``. 

537 

538 Parameters 

539 ---------- 

540 angle 

541 The angle of the rotation. 

542 

543 Returns 

544 ------- 

545 R 

546 The rotation matrix. 

547 

548 Notes 

549 ----- 

550 For a vector ``v`` with shape ``(..., 3)`` and a rotation angle :attr:`angle`, 

551 ``np.einsum('ji, ...i', _Rz(angle), v)`` rotates the vector around the 

552 ``z``-axis by :attr:`angle`. If the 

553 coordinate system is being rotated, then 

554 ``np.einsum('ij, ...i', _Rz(angle), v)`` gives the vector in the 

555 new coordinate system. 

556 """ 

557 return Rotation.from_euler('Z', angle).as_matrix() 

558 

559 def _get_str_representation(self, max_lines=50) -> str: 

560 """ Retrieves a string representation of the object with specified 

561 maximum number of lines. 

562 

563 Parameters 

564 ---------- 

565 max_lines 

566 The maximum number of lines to return. 

567 """ 

568 wdt = 74 

569 s = [] 

570 s += ['=' * wdt] 

571 s += ['DataContainer'.center(wdt)] 

572 s += ['-' * wdt] 

573 s += ['{:26} : {}'.format('Corrected for transmission', self._correct_for_transmission_called)] 

574 truncated_s = [] 

575 leave_loop = False 

576 while not leave_loop: 

577 line = s.pop(0).split('\n') 

578 for split_line in line: 

579 if split_line != '': 579 ↛ 581line 579 didn't jump to line 581, because the condition on line 579 was never false

580 truncated_s += [split_line] 

581 if len(truncated_s) > max_lines - 2: 

582 if split_line != '...': 582 ↛ 584line 582 didn't jump to line 584, because the condition on line 582 was never false

583 truncated_s += ['...'] 

584 if split_line != ('=' * wdt): 584 ↛ 586line 584 didn't jump to line 586, because the condition on line 584 was never false

585 truncated_s += ['=' * wdt] 

586 leave_loop = True 

587 break 

588 if len(s) == 0: 

589 leave_loop = True 

590 truncated_s += ['=' * wdt] 

591 return '\n'.join(truncated_s) 

592 

593 def __str__(self) -> str: 

594 return self._get_str_representation() 

595 

596 def _get_html_representation(self, max_lines=25) -> str: 

597 """ Retrieves an html representation of the object with specified 

598 maximum number of lines. 

599 

600 Parameters 

601 ---------- 

602 max_lines 

603 The maximum number of lines to return. 

604 """ 

605 s = [] 

606 s += ['<h3>DataContainer</h3>'] 

607 s += ['<table border="1" class="dataframe">'] 

608 s += ['<thead><tr><th style="text-align: left;">Field</th><th>Size</th></tr></thead>'] 

609 s += ['<tbody>'] 

610 s += ['<tr><td style="text-align: left;">Number of projections</td>'] 

611 s += [f'<td>{len(self._projections)}</td></tr>'] 

612 s += ['<tr><td style="text-align: left;">Corrected for transmission</td>'] 

613 s += [f'<td>{self._correct_for_transmission_called}</td></tr>'] 

614 s += ['</tbody>'] 

615 s += ['</table>'] 

616 truncated_s = [] 

617 line_count = 0 

618 leave_loop = False 

619 while not leave_loop: 

620 line = s.pop(0).split('\n') 

621 for split_line in line: 

622 truncated_s += [split_line] 

623 if '</tr>' in split_line: 

624 line_count += 1 

625 # Catch if last line had ellipses 

626 last_tr = split_line 

627 if line_count > max_lines - 1: 627 ↛ 628line 627 didn't jump to line 628, because the condition on line 627 was never true

628 if last_tr != '<tr><td style="text-align: left;">...</td></tr>': 

629 truncated_s += ['<tr><td style="text-align: left;">...</td></tr>'] 

630 truncated_s += ['</tbody>'] 

631 truncated_s += ['</table>'] 

632 leave_loop = True 

633 break 

634 if len(s) == 0: 

635 leave_loop = True 

636 return '\n'.join(truncated_s) 

637 

638 def _repr_html_(self) -> str: 

639 return self._get_html_representation()