Coverage for local_installation_linux/mumott/core/projection_stack.py: 94%

389 statements  

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

1""" Container for class ProjectionStack. """ 

2import numpy as np 

3from numpy.typing import NDArray 

4from .geometry import Geometry, GeometryTuple 

5from mumott.core.hashing import list_to_hash 

6 

7 

8class Projection: 

9 """Instances of this class contain data and metadata from a single measurement. 

10 Typically they are appended to a 

11 :class:`ProjectionStack <mumott.core.projection_stack.ProjectionStack>` object. 

12 

13 Parameters 

14 ---------- 

15 data 

16 Data from measurement, structured into 3 dimensions representing 

17 the two scanning directions and the detector angle. 

18 diode 

19 Diode or transmission data from measurement, structured into 

20 2 dimensions representing the two scanning directions. 

21 weights 

22 Weights or masking information, represented as a number 

23 between ``0`` and ``1``. ``0`` means mask, ``1`` means 

24 do not mask. Structured the same way as :attr:`data`. 

25 rotation 

26 3-by-3 rotation matrix, representing the rotation of the sample in the 

27 laboratory coordinate system. 

28 j_offset 

29 The offset needed to align the projection in the j-direction. 

30 k_offset 

31 The offset needed to align the projection in the k-direction. 

32 inner_angle 

33 Angle of rotation about :attr:`inner_axis` in radians. 

34 outer_angle 

35 Angle of rotation about :attr:`outer_axis` in radians. 

36 inner_axis 

37 Inner rotation axis. 

38 outer_axis 

39 Outer rotation axis. 

40 """ 

41 def __init__(self, 

42 data: NDArray[float] = None, 

43 diode: NDArray[float] = None, 

44 weights: NDArray[float] = None, 

45 rotation: NDArray[float] = np.eye(3, dtype=float), 

46 j_offset: float = float(0), 

47 k_offset: float = float(0), 

48 inner_angle: float = None, 

49 outer_angle: float = None, 

50 inner_axis: NDArray[float] = None, 

51 outer_axis: NDArray[float] = None): 

52 self._key = None 

53 self._projection_stack = None 

54 self.data = data 

55 self.diode = diode 

56 self.weights = weights 

57 self.j_offset = j_offset 

58 self.k_offset = k_offset 

59 self.rotation = rotation 

60 self.inner_angle = inner_angle 

61 self.outer_angle = outer_angle 

62 self.inner_axis = inner_axis 

63 self.outer_axis = outer_axis 

64 

65 @property 

66 def j_offset(self) -> np.float64: 

67 """ The offset needed to align the projection in the j-direction.""" 

68 if self._projection_stack is None: 

69 return self._j_offset 

70 else: 

71 k = self._projection_stack.index_by_key(self._key) 

72 return self._projection_stack.geometry.j_offsets[k] 

73 

74 @j_offset.setter 

75 def j_offset(self, value) -> None: 

76 self._j_offset = value 

77 if self._projection_stack is not None: 

78 k = self._projection_stack.index_by_key(self._key) 

79 self._projection_stack.geometry.j_offsets[k] = value 

80 

81 @property 

82 def k_offset(self) -> np.float64: 

83 """ The offset needed to align the projection in the k-direction.""" 

84 if self._projection_stack is None: 

85 return self._k_offset 

86 else: 

87 k = self._projection_stack.index_by_key(self._key) 

88 return self._projection_stack.geometry.k_offsets[k] 

89 

90 @k_offset.setter 

91 def k_offset(self, value) -> None: 

92 self._k_offset = value 

93 if self._projection_stack is not None: 93 ↛ 94line 93 didn't jump to line 94, because the condition on line 93 was never true

94 k = self._projection_stack.index_by_key(self._key) 

95 self._projection_stack.geometry.k_offsets[k] = value 

96 

97 @property 

98 def rotation(self) -> NDArray[np.float64]: 

99 """ 3-by-3 rotation matrix, representing the rotation of the sample in the 

100 laboratory coordinate system. """ 

101 if self._projection_stack is None: 

102 return self._rotation 

103 else: 

104 k = self._projection_stack.index_by_key(self._key) 

105 return self._projection_stack.geometry.rotations[k] 

106 

107 @rotation.setter 

108 def rotation(self, value) -> None: 

109 self._rotation = value 

110 if self._projection_stack is not None: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true

111 k = self._projection_stack.index_by_key(self._key) 

112 self._projection_stack.geometry.rotations[k] = value 

113 

114 @property 

115 def inner_angle(self) -> float: 

116 """ Rotation angle about inner axis. """ 

117 if self._projection_stack is None: 

118 return self._inner_angle 

119 else: 

120 k = self._projection_stack.index_by_key(self._key) 

121 return self._projection_stack.geometry.inner_angles[k] 

122 

123 @inner_angle.setter 

124 def inner_angle(self, value: float) -> None: 

125 self._inner_angle = value 

126 if self._projection_stack is not None: 126 ↛ 127line 126 didn't jump to line 127, because the condition on line 126 was never true

127 k = self._projection_stack.index_by_key(self._key) 

128 self._projection_stack.geometry.inner_angles[k] = value 

129 

130 @property 

131 def outer_angle(self) -> float: 

132 """ Rotation angle about inner axis. """ 

133 if self._projection_stack is None: 

134 return self._outer_angle 

135 else: 

136 k = self._projection_stack.index_by_key(self._key) 

137 return self._projection_stack.geometry.outer_angles[k] 

138 

139 @outer_angle.setter 

140 def outer_angle(self, value: float) -> None: 

141 self._outer_angle = value 

142 if self._projection_stack is not None: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true

143 k = self._projection_stack.index_by_key(self._key) 

144 self._projection_stack.geometry.outer_angles[k] = value 

145 

146 @property 

147 def inner_axis(self) -> NDArray[float]: 

148 """ Rotation angle about inner axis. """ 

149 if self._projection_stack is None: 

150 return self._inner_axis 

151 else: 

152 k = self._projection_stack.index_by_key(self._key) 

153 return self._projection_stack.geometry.inner_axes[k] 

154 

155 @inner_axis.setter 

156 def inner_axis(self, value: NDArray[float]) -> None: 

157 self._inner_axis = value 

158 if self._projection_stack is not None: 

159 k = self._projection_stack.index_by_key(self._key) 

160 self._projection_stack.geometry.inner_axes[k] = value 

161 

162 @property 

163 def outer_axis(self) -> NDArray[float]: 

164 """ Rotation angle about inner axis. """ 

165 if self._projection_stack is None: 

166 return self._outer_axis 

167 else: 

168 k = self._projection_stack.index_by_key(self._key) 

169 return self._projection_stack.geometry.outer_axes[k] 

170 

171 @outer_axis.setter 

172 def outer_axis(self, value: NDArray[float]) -> None: 

173 self._outer_axis = value 

174 if self._projection_stack is not None: 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true

175 k = self._projection_stack.index_by_key(self._key) 

176 self._projection_stack.geometry.outer_axes[k] = value 

177 

178 @property 

179 def data(self) -> NDArray: 

180 """ Scattering data, structured ``(j, k, w)``, where ``j`` is the pixel in the j-direction, 

181 ``k`` is the pixel in the k-direction, and ``w`` is the detector segment. 

182 Before the reconstruction, the data should be normalized by the diode. 

183 This may already have been done prior to loading the data. 

184 """ 

185 return np.array([]).reshape(0, 0) if self._data is None else self._data 

186 

187 @data.setter 

188 def data(self, val) -> None: 

189 self._data = val 

190 

191 @property 

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

193 """ The diode readout, used to normalize the data. Can be blank if data is already normalized. 

194 """ 

195 return np.array([]).reshape(0, 0) if self._diode is None else self._diode 

196 

197 @diode.setter 

198 def diode(self, val) -> None: 

199 self._diode = val 

200 

201 @property 

202 def weights(self) -> NDArray: 

203 """ Weights to be applied multiplicatively during optimization. A value of ``0`` 

204 means mask, a value of ``1`` means no weighting, and other values means weighting 

205 each data point either less (``weights < 1``) or more (``weights > 1``) than a weight of ``1``. 

206 """ 

207 return np.array([]).reshape(0, 0) if self._weights is None else self._weights 

208 

209 @weights.setter 

210 def weights(self, val) -> None: 

211 self._weights = val 

212 

213 @property 

214 def attached(self): 

215 """ Returns true if projection is attached to a :class:`ProjectionStack <ProjectionStack>` object. """ 

216 return self._projection_stack is not None 

217 

218 def attach_to_stack(self, projection_stack, index): 

219 """ Used to attach the projection to a projection_stack. 

220 *This method should not be called by users.* 

221 """ 

222 if self.attached: 222 ↛ 223line 222 didn't jump to line 223, because the condition on line 222 was never true

223 raise ValueError('This projection is already attached to a projection_stack') 

224 self._projection_stack = projection_stack 

225 self._key = index 

226 

227 @property 

228 def geometry(self) -> GeometryTuple: 

229 """ Returns geometry information as a named tuple. """ 

230 return GeometryTuple(rotation=self.rotation, 

231 j_offset=self.j_offset, 

232 k_offset=self.k_offset, 

233 inner_angle=self.inner_angle, 

234 outer_angle=self.outer_angle, 

235 inner_axis=self.inner_axis, 

236 outer_axis=self.outer_axis) 

237 

238 @geometry.setter 

239 def geometry(self, value: GeometryTuple) -> None: 

240 self.rotation = value.rotation 

241 self.j_offset = value.j_offset 

242 self.k_offset = value.k_offset 

243 self.inner_angle = value.inner_angle 

244 self.outer_angle = value.outer_angle 

245 self.inner_axis = value.inner_axis 

246 self.outer_axis = value.outer_axis 

247 

248 def detach_from_stack(self): 

249 """ Used to detach the projection from a projection stack. 

250 *This method should not be called by users.* 

251 """ 

252 k = self._projection_stack.index_by_key(self._key) 

253 g = self._projection_stack.geometry[k] 

254 self._rotation = g.rotation 

255 self._j_offset = g.j_offset 

256 self._k_offset = g.k_offset 

257 self._inner_angle = g.inner_angle 

258 self._outer_angle = g.outer_angle 

259 self._inner_axis = g.inner_axis 

260 self._outer_axis = g.outer_axis 

261 self._projection_stack = None 

262 self._key = None 

263 

264 @property 

265 def hash_data(self) -> str: 

266 """ A hash of :attr:`data`.""" 

267 # np.array wrapper in case data is None 

268 return list_to_hash([np.array(self.data)]) 

269 

270 @property 

271 def hash_diode(self) -> str: 

272 """ A sha1 hash of :attr:`diode`.""" 

273 return list_to_hash([np.array(self.diode)]) 

274 

275 @property 

276 def hash_weights(self) -> str: 

277 """ A sha1 hash of :attr:`weights`.""" 

278 return list_to_hash([np.array(self.weights)]) 

279 

280 def __str__(self) -> str: 

281 wdt = 74 

282 s = [] 

283 s += ['-' * wdt] 

284 s += ['Projection'.center(wdt)] 

285 s += ['-' * wdt] 

286 with np.printoptions(threshold=4, precision=5, linewidth=60, edgeitems=2): 

287 s += ['{:18} : {}'.format('hash_data', self.hash_data[:6])] 

288 s += ['{:18} : {}'.format('hash_diode', self.hash_diode[:6])] 

289 s += ['{:18} : {}'.format('hash_weights', self.hash_weights[:6])] 

290 ss = ', '.join([f'{r}' for r in self.rotation]) 

291 s += ['{:18} : {}'.format('rotation', ss)] 

292 s += ['{:18} : {}'.format('j_offset', self.j_offset)] 

293 s += ['{:18} : {}'.format('k_offset', self.k_offset)] 

294 s += ['{:18} : {}'.format('inner_angle', self.inner_angle)] 

295 s += ['{:18} : {}'.format('outer_angle', self.outer_angle)] 

296 ss = ', '.join([f'{r}' for r in np.array(self.inner_axis).ravel()]) 

297 s += ['{:18} : {}'.format('inner_axis', ss)] 

298 ss = ', '.join([f'{r}' for r in np.array(self.outer_axis).ravel()]) 

299 s += ['{:18} : {}'.format('outer_axis', ss)] 

300 s += ['-' * wdt] 

301 return '\n'.join(s) 

302 

303 def _repr_html_(self) -> str: 

304 s = [] 

305 s += ['<h3>Projection</h3>'] 

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

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

308 s += ['<tbody>'] 

309 with np.printoptions(threshold=4, precision=5, linewidth=40, edgeitems=2): 

310 s += ['<tr><td style="text-align: left;">data</td>'] 

311 s += [f'<td>{self.data.shape}</td><td>{self.hash_data[:6]} (hash)</td></tr>'] 

312 s += [f'<tr><td style="text-align: left;">diode</td>' 

313 f'<td>{self.diode.shape}</td>'] 

314 s += [f'<td>{self.hash_diode[:6]} (hash)</td></tr>'] 

315 s += [f'<tr><td style="text-align: left;">weights</td>' 

316 f'<td>{self.weights.shape}</td>'] 

317 s += [f'<td>{self.hash_weights[:6]} (hash)</td></tr>'] 

318 s += [f'<tr><td style="text-align: left;">rotation</td><td>{self.rotation.shape}</td>'] 

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

320 s += ['<tr><td style="text-align: left;">j_offset</td><td>1</td>'] 

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

322 s += ['<tr><td style="text-align: left;">k_offset</td><td>1</td>'] 

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

324 s += ['<tr><td style="text-align: left;">inner_angle</td><td>1</td>'] 

325 s += [f'<td>{self.inner_angle}</td>'] 

326 s += ['<tr><td style="text-align: left;">outer_angle</td><td>1</td>'] 

327 s += [f'<td>{self.outer_angle}</td>'] 

328 s += [f'<tr><td style="text-align: left;">inner_axis</td><td>{self.inner_axis.shape}</td>'] 

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

330 s += [f'<tr><td style="text-align: left;">outer_axis</td><td>{self.outer_axis.shape}</td>'] 

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

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

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

334 return '\n'.join(s) 

335 

336 

337class ProjectionStack: 

338 """Instances of this class contain data, geometry and other pertinent information 

339 for a series of measurements. 

340 The individual measurements are stored as 

341 :class:`Projection <mumott.core.projection_stack.Projection>` objects. 

342 The latter are accessible via list-like operations, which enables, for example, iteration over 

343 measurements but also retrieval of individual measurements by index, in-place modification or deletion. 

344 

345 The geometry information (i.e., rotations and offsets for each projection) 

346 is accessible via the :attr:`geometry` attribute. 

347 Data, diode readouts, and weights can be retrieved as contiguous arrays 

348 via the properties :attr:`data`, :attr:`diode`, and :attr:`weights`, respectively. 

349 

350 Example 

351 ------- 

352 The following code snippet illustrates how individual measurements can be accessed via list operations. 

353 For demonstration, here, we use default ("empty") projections. 

354 In practice the individual measurements are read from a data file via the 

355 :class:`DataContainer <mumott.data_handling.DataContainer>` class, which makes 

356 them available via the 

357 :attr:`DataContainer.projections <mumott.data_handling.DataContainer.projections>` attribute. 

358 

359 First we create an empty projection stack. 

360 

361 >>> from mumott.core.projection_stack import Projection, ProjectionStack 

362 >>> projection_stack = ProjectionStack() 

363 

364 Next we create a projection and attach it to the projection stack. 

365 In order to be able to distinguish this projection during this example, 

366 we assign it a :attr:`Projection.j_offset` of ``0.5``. 

367 

368 >>> projection = Projection(j_offset=0.5) 

369 >>> projection_stack.append(projection) 

370 

371 The geometry information can now be accessed via the projection stack 

372 in several different but equivalent ways, including via the original projection object, 

373 

374 >>> print(projection.j_offset) 

375 

376 via indexing `projection_stack` 

377 

378 >>> print(projection_stack[0].geometry.j_offset) 

379 

380 or by indexing the respective geometry property of the projection stack itself. 

381 

382 >>> print(projection_stack.geometry.j_offsets[0]) 

383 

384 We can modify the geometry parameters via any of these properties with identical outcome. 

385 For example, 

386 

387 >>> projection_stack[0].j_offset = -0.2 

388 >>> print(projection.j_offset, 

389 projection_stack[0].geometry.j_offset, 

390 projection_stack.geometry.j_offsets[0]) 

391 -0.2 -0.2 -0.2 

392 

393 Next consider a situation where several projections are included in the projection stack. 

394 

395 >>> projection_stack.append(Projection(j_offset=0.1)) 

396 >>> projection_stack.append(Projection(j_offset=-0.34)) 

397 >>> projection_stack.append(Projection(j_offset=0.23)) 

398 >>> projection_stack.append(Projection(j_offset=0.78)) 

399 >>> print(projection_stack.geometry.j_offsets) 

400 [-0.2, 0.1, -0.34, 0.23, 0.78] 

401 

402 The summary of the projection stack includes hashes for the data, the diode readout, and the weights. 

403 This allows one to get a quick indication for whether the content of these fields has changed. 

404 

405 >>> print(projection_stack) 

406 -------------------------------------------------------------------------- 

407 ProjectionStack 

408 -------------------------------------------------------------------------- 

409 hash_data : ... 

410 

411 We could, for example, decide to remove an individual projection as we might 

412 have realized that the data from that measurement was corrupted. 

413 

414 >>> del projection_stack[1] 

415 >>> print(projection_stack) 

416 -------------------------------------------------------------------------- 

417 ProjectionStack 

418 -------------------------------------------------------------------------- 

419 hash_data : ... 

420 

421 From the output it is readily apparent that the content of the data field 

422 has changed as a result of this operation. 

423 

424 Finally, note that we can also loop over the projection stack, for example, to print the projections. 

425 

426 >>> for projection in projection_stack: 

427 >>> print(projection) 

428 ... 

429 """ 

430 

431 def __init__(self) -> None: 

432 self._projections = [] 

433 self._keys = [] 

434 self._geometry = Geometry() 

435 

436 def __delitem__(self, k: int) -> None: 

437 """ Removes a projection from the projection_stack. """ 

438 if abs(k) > len(self) - int(k >= 0): 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true

439 raise IndexError(f'Index {k} is out of bounds for ProjectionStack of length {len(self)}.') 

440 self._projections[k].detach_from_stack() 

441 del self._projections[k] 

442 del self._geometry[k] 

443 del self._keys[k] 

444 

445 def append(self, projection: Projection) -> None: 

446 """ 

447 Appends a measurement in the form of a 

448 :class:`Projection <mumott.core.projection_stack.Projection>` object. 

449 Once a projection is attached to a projection_stack, the geometry information of the 

450 projection will be synchronized 

451 with the geometry information of the projection_stack (see :attr:`geometry`). 

452 

453 Parameters 

454 ---------- 

455 projection 

456 :class:`Projection <mumott.core.projection_stack.Projection>` object to be appended. 

457 """ 

458 if projection.attached: 

459 raise ValueError('The projection is already attached to a projection stack') 

460 assert len(self._projections) == len(self._geometry) 

461 if len(self) == 0: 

462 self._geometry.projection_shape = np.array(projection.diode.shape) 

463 elif not np.allclose(self.diode.shape[1:], projection.diode.shape): 

464 raise ValueError('Appended projection diode must have the same shape as other projections,' 

465 f' but its shape is {projection.diode.shape} while other projections' 

466 f' have shape {self.diode.shape[1:]}.') 

467 self._projections.append(projection) 

468 self._geometry.append(GeometryTuple(rotation=projection.rotation, 

469 j_offset=projection.j_offset, 

470 k_offset=projection.k_offset, 

471 inner_angle=projection.inner_angle, 

472 outer_angle=projection.outer_angle, 

473 inner_axis=projection.inner_axis, 

474 outer_axis=projection.outer_axis)) 

475 

476 projection_key = hash(projection) 

477 self._keys.append(projection_key) 

478 projection.attach_to_stack(self, projection_key) 

479 

480 def __setitem__(self, k: int, projection: Projection) -> None: 

481 """ 

482 This allows each projection of the projection stack to be safely modified. 

483 """ 

484 assert len(self._projections) == len(self._geometry) 

485 if abs(k) > len(self) - int(k >= 0): 485 ↛ 486line 485 didn't jump to line 486, because the condition on line 485 was never true

486 raise IndexError(f'Index {k} is out of bounds for projection stack of length {len(self)}.') 

487 

488 if projection.attached: 

489 raise ValueError('The projection is already attached to a projection stack') 

490 if not np.allclose(self.diode.shape[1:], projection.diode.shape): 

491 raise ValueError('New projection diode must have the same shape as other projections,' 

492 f' but its shape is {projection.diode.shape} while other projections' 

493 f' have shape {self.diode.shape[1:]}.') 

494 

495 # detach and delete previous projection 

496 del self[k] 

497 

498 # attach new projection 

499 self._projections.insert(k, projection) 

500 self._geometry.insert(k, GeometryTuple(rotation=projection.rotation, 

501 j_offset=projection.j_offset, 

502 k_offset=projection.k_offset, 

503 inner_angle=projection.inner_angle, 

504 outer_angle=projection.outer_angle, 

505 inner_axis=projection.inner_axis, 

506 outer_axis=projection.outer_axis)) 

507 

508 projection_key = hash(projection) 

509 self._keys.insert(k, projection_key) 

510 projection.attach_to_stack(self, projection_key) 

511 

512 def insert(self, k: int, projection: Projection) -> None: 

513 """ Inserts a projection at a particular index, increasing the indices 

514 of all subsequent projections by 1. """ 

515 assert len(self._projections) == len(self._geometry) 

516 if abs(k) > len(self) - int(k >= 0): 516 ↛ 517line 516 didn't jump to line 517, because the condition on line 516 was never true

517 raise IndexError(f'Index {k} is out of bounds for projection stack of length {len(self)}.') 

518 

519 if projection.attached: 

520 raise ValueError('The projection is already attached to a projection stack.') 

521 if not np.allclose(self.diode.shape[1:], projection.diode.shape): 

522 raise ValueError('Inserted projection diode must have the same shape as other projections,' 

523 f' but its shape is {projection.diode.shape} while other projections' 

524 f' have shape {self.diode.shape[1:]}.') 

525 

526 self._projections.insert(k, projection) 

527 self._geometry.insert(k, GeometryTuple(rotation=projection.rotation, 

528 j_offset=projection.j_offset, 

529 k_offset=projection.k_offset)) 

530 self._geometry.projection_shape = np.array(projection.diode.shape) 

531 projection_key = hash(projection) 

532 self._keys.insert(k, projection_key) 

533 projection.attach_to_stack(self, projection_key) 

534 

535 def __getitem__(self, k: int) -> Projection: 

536 """ 

537 This allows indexing of and iteration over the projection stack. 

538 """ 

539 assert len(self._projections) == len(self._geometry) 

540 if abs(k) > len(self) - round(float(k >= 0)): 

541 raise IndexError(f'Index {k} is out of bounds for projection stack of length {len(self)}.') 

542 return self._projections[k] 

543 

544 def __len__(self) -> int: 

545 return len(self._projections) 

546 

547 @property 

548 def data(self) -> NDArray: 

549 """ Scattering data, structured ``(n, j, k, w)``, where ``n`` is the projection number, 

550 ``j`` is the pixel in the j-direction, ``k`` is the pixel in the k-direction, 

551 and ``w`` is the detector segment. Before the reconstruction, this should 

552 be normalized by the diode. This may already have been done prior to loading the data. 

553 """ 

554 if len(self) == 0: 

555 return np.array([]).reshape(0, 0, 0) 

556 return np.stack([f.data for f in self._projections], axis=0) 

557 

558 @property 

559 def diode(self) -> NDArray: 

560 """ The diode readout, used to normalize the data. Can be blank if data is already normalized. 

561 The diode value should not be normalized per projection, i.e., it is distinct from the 

562 transmission value used in standard tomography.""" 

563 if len(self) == 0: 

564 return np.array([]).reshape(0, 0, 0) 

565 return np.stack([f.diode for f in self._projections], axis=0) 

566 

567 @diode.setter 

568 def diode(self, val) -> None: 

569 assert len(self) == len(val) 

570 for i, projection in enumerate(self._projections): 

571 projection.diode[...] = val[i] 

572 

573 @property 

574 def weights(self) -> NDArray: 

575 """ Weights applied multiplicatively during optimization. A value of ``0`` 

576 means mask, a value of ``1`` means no weighting, and other values means weighting 

577 each data point either less (``weights < 1``) or more (``weights > 1``) than a weight of ``1``. 

578 """ 

579 if len(self) == 0: 

580 return np.array([]).reshape(0, 0, 0) 

581 return np.stack([f.weights for f in self._projections], axis=0) 

582 

583 @weights.setter 

584 def weights(self, val) -> None: 

585 assert len(self) == len(val) 

586 for i, projection in enumerate(self._projections): 

587 projection.weights[...] = val[i] 

588 

589 def _get_str_representation(self, max_lines: int = 25) -> str: 

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

591 maximum number of lines. 

592 

593 Parameters 

594 ---------- 

595 max_lines 

596 The maximum number of lines to return. 

597 """ 

598 s = [] 

599 wdt = 74 

600 s = [] 

601 s += ['-' * wdt] 

602 s += ['ProjectionStack'.center(wdt)] 

603 s += ['-' * wdt] 

604 with np.printoptions(threshold=3, edgeitems=1, precision=3, linewidth=60): 

605 s += ['{:18} : {}'.format('hash_data', self.hash_data[:6])] 

606 s += ['{:18} : {}'.format('hash_diode', self.hash_diode[:6])] 

607 s += ['{:18} : {}'.format('hash_weights', self.hash_weights[:6])] 

608 s += ['{:18} : {}'.format('Number of projections', len(self))] 

609 s += ['{:18} : {}'.format('Number of pixels j', self.diode.shape[1])] 

610 s += ['{:18} : {}'.format('Number of pixels k', self.diode.shape[2])] 

611 truncated_s = [] 

612 leave_loop = False 

613 while not leave_loop: 

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

615 for split_line in line: 

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

617 truncated_s += [split_line] 

618 if len(truncated_s) > max_lines - 2: 

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

620 truncated_s += ['...'] 

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

622 truncated_s += ['=' * wdt] 

623 leave_loop = True 

624 break 

625 if len(s) == 0: 

626 leave_loop = True 

627 truncated_s += ['-' * wdt] 

628 return '\n'.join(truncated_s) 

629 

630 def __str__(self) -> str: 

631 return self._get_str_representation() 

632 

633 @property 

634 def hash_data(self) -> str: 

635 """ A hash of :attr:`data`.""" 

636 # np.array wrapper in case data is None 

637 return list_to_hash([np.array(self.data)]) 

638 

639 @property 

640 def hash_diode(self) -> str: 

641 """ A sha1 hash of :attr:`diode`.""" 

642 return list_to_hash([np.array(self.diode)]) 

643 

644 @property 

645 def hash_weights(self) -> str: 

646 """ A sha1 hash of :attr:`weights`.""" 

647 return list_to_hash([np.array(self.weights)]) 

648 

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

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

651 maximum number of lines. 

652 

653 Parameters 

654 ---------- 

655 max_lines 

656 The maximum number of lines to return. 

657 """ 

658 s = [] 

659 s += ['<h3>ProjectionStack</h3>'] 

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

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

662 s += ['<tbody>'] 

663 with np.printoptions(threshold=3, edgeitems=1, precision=2, linewidth=40): 

664 s += ['<tr><td style="text-align: left;">data</td>'] 

665 s += [f'<td>{self.data.shape}</td><td>{self.hash_data[:6]} (hash)</td></tr>'] 

666 s += ['<tr><td style="text-align: left;">diode</td>'] 

667 s += [f'<td>{self.diode.shape}</td><td>{self.hash_diode[:6]} (hash)</td></tr>'] 

668 s += ['<tr><td style="text-align: left;">weights</td>'] 

669 s += [f'<td>{self.weights.shape}</td><td>{self.hash_weights[:6]} (hash)</td></tr>'] 

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

671 s += ['<td>1</td>'] 

672 s += [f'<td>{self.diode.shape[1]}</td></tr>'] 

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

674 s += ['<td>1</td>'] 

675 s += [f'<td>{self.diode.shape[2]}</td></tr>'] 

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

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

678 truncated_s = [] 

679 line_count = 0 

680 leave_loop = False 

681 while not leave_loop: 

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

683 for split_line in line: 

684 truncated_s += [split_line] 

685 if '</tr>' in split_line: 

686 line_count += 1 

687 # Catch if last line had ellipses 

688 last_tr = split_line 

689 if line_count > max_lines - 1: 

690 if last_tr != '<tr><td style="text-align: left;">...</td></tr>': 690 ↛ 692line 690 didn't jump to line 692, because the condition on line 690 was never false

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

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

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

694 leave_loop = True 

695 break 

696 if len(s) == 0: 

697 leave_loop = True 

698 return '\n'.join(truncated_s) 

699 

700 def _repr_html_(self) -> str: 

701 return self._get_html_representation() 

702 

703 @property 

704 def geometry(self) -> Geometry: 

705 """ Contains geometry information for each projection as well 

706 as information about the geometry of the whole system. """ 

707 return self._geometry 

708 

709 def index_by_key(self, key): 

710 """ Returns an index from a key. """ 

711 return self._keys.index(key)