Coverage for local_installation_linux/mumott/core/projection_stack.py: 94%
389 statements
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +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
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.
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
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]
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
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]
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
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]
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
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]
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
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]
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
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]
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
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]
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
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
187 @data.setter
188 def data(self, val) -> None:
189 self._data = val
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
197 @diode.setter
198 def diode(self, val) -> None:
199 self._diode = val
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
209 @weights.setter
210 def weights(self, val) -> None:
211 self._weights = val
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
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
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)
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
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
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)])
270 @property
271 def hash_diode(self) -> str:
272 """ A sha1 hash of :attr:`diode`."""
273 return list_to_hash([np.array(self.diode)])
275 @property
276 def hash_weights(self) -> str:
277 """ A sha1 hash of :attr:`weights`."""
278 return list_to_hash([np.array(self.weights)])
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)
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)
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.
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.
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.
359 First we create an empty projection stack.
361 >>> from mumott.core.projection_stack import Projection, ProjectionStack
362 >>> projection_stack = ProjectionStack()
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``.
368 >>> projection = Projection(j_offset=0.5)
369 >>> projection_stack.append(projection)
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,
374 >>> print(projection.j_offset)
376 via indexing `projection_stack`
378 >>> print(projection_stack[0].geometry.j_offset)
380 or by indexing the respective geometry property of the projection stack itself.
382 >>> print(projection_stack.geometry.j_offsets[0])
384 We can modify the geometry parameters via any of these properties with identical outcome.
385 For example,
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
393 Next consider a situation where several projections are included in the projection stack.
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]
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.
405 >>> print(projection_stack)
406 --------------------------------------------------------------------------
407 ProjectionStack
408 --------------------------------------------------------------------------
409 hash_data : ...
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.
414 >>> del projection_stack[1]
415 >>> print(projection_stack)
416 --------------------------------------------------------------------------
417 ProjectionStack
418 --------------------------------------------------------------------------
419 hash_data : ...
421 From the output it is readily apparent that the content of the data field
422 has changed as a result of this operation.
424 Finally, note that we can also loop over the projection stack, for example, to print the projections.
426 >>> for projection in projection_stack:
427 >>> print(projection)
428 ...
429 """
431 def __init__(self) -> None:
432 self._projections = []
433 self._keys = []
434 self._geometry = Geometry()
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]
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`).
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))
476 projection_key = hash(projection)
477 self._keys.append(projection_key)
478 projection.attach_to_stack(self, projection_key)
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)}.')
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:]}.')
495 # detach and delete previous projection
496 del self[k]
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))
508 projection_key = hash(projection)
509 self._keys.insert(k, projection_key)
510 projection.attach_to_stack(self, projection_key)
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)}.')
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:]}.')
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)
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]
544 def __len__(self) -> int:
545 return len(self._projections)
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)
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)
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]
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)
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]
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.
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)
630 def __str__(self) -> str:
631 return self._get_str_representation()
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)])
639 @property
640 def hash_diode(self) -> str:
641 """ A sha1 hash of :attr:`diode`."""
642 return list_to_hash([np.array(self.diode)])
644 @property
645 def hash_weights(self) -> str:
646 """ A sha1 hash of :attr:`weights`."""
647 return list_to_hash([np.array(self.weights)])
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.
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)
700 def _repr_html_(self) -> str:
701 return self._get_html_representation()
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
709 def index_by_key(self, key):
710 """ Returns an index from a key. """
711 return self._keys.index(key)