-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vectorize geometries #1059
Vectorize geometries #1059
Conversation
1e65f17
to
e4176b1
Compare
Ready for review. As often before when working on geometries, there's a lot of new documentation, including doctests, which accounts for the biggest part of the PR. Documentation is way improved now IMO. |
odl/tomo/geometry/detector.py
Outdated
"""Parametrization of the detector reference surface. | ||
"""Return the detector surface point corresponding to ``param``. | ||
|
||
The surface point lies on a circle around ``radius * center_dir`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`center`
odl/tomo/geometry/geometry.py
Outdated
infinity). | ||
The shape of the returned array is as follows: | ||
|
||
- ``mparam`` and ``dparam`` single: ``(ndim,)`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
angle
odl/tomo/geometry/parallel.py
Outdated
contained in `motion_params`. | ||
angle : float or `array-like` | ||
Angle(s) in radians describing the counter-clockwise | ||
rotation of the detector. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs more general formulation that includes multiple angles.
odl/tomo/geometry/parallel.py
Outdated
|
||
# Need custom check here until #861 is in because arrays are | ||
# assumed to be flat in the "point enumeration" axis | ||
# TODO: replace when #861 is in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed this old check, needs to be replaced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One big overall comment about how we should handle arrays of input that needs to be discussed.
Other than that only minor stuff, good code quality as usual.
@@ -431,7 +430,7 @@ def astra_projection_geometry(geometry): | |||
raise ValueError('non-uniform detector sampling is not supported') | |||
|
|||
if (isinstance(geometry, ParallelBeamGeometry) and | |||
isinstance(geometry.detector, FlatDetector) and | |||
isinstance(geometry.detector, (Flat1dDetector, Flat2dDetector)) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait wat. The next line guarantees that its a 1d detector, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well not really. It just says that the whole geometry is embedded in a 2D space, the detector can be anything. This was probably supposed to be the duck-typed version of isinstance(geometry, Parallel2dGeometry)
.
But honestly if somebody decides to write a new geometry that doesn't subclass Parallel2dGeometry
but still fulfills these conditions (whatever that may be), it would require a new conversion implementation anyway. So we might as well just do isinstance(geometry, Parallel2dGeometry)
.
odl/tomo/geometry/conebeam.py
Outdated
if angle not in self.motion_params: | ||
raise ValueError('`angle` {} is not in the valid range {}' | ||
''.format(angle, self.motion_params)) | ||
squeeze_out = np.isscalar(angle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>>> np.isscalar(np.array(1))
False
is this intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I guess I would like to change this to np.shape(angle) == ()
which covers both cases.
if np.linalg.norm(src_to_det_init) <= 1e-10: | ||
raise ValueError('`src_to_det_init` norm {} too close to 0' | ||
''.format(np.linalg.norm(src_to_det_init))) | ||
if np.linalg.norm(src_to_det_init) == 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we removed the close-ness check for what reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect issues regarding scales. If somebody works on electron tomography where lengths are in the range of nanometers, and they decide to not (or forget to) normalize the vector, the old check will produce an unreasonable error.
With all these closeness checks on user input my attitude is to let the user figure out what to provide and how to deal with numerical accuracy issues, instead of us imposing some arbitrary bounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Good change.
odl/tomo/geometry/conebeam.py
Outdated
|
||
Returns | ||
------- | ||
axes : tuple of `numpy.ndarray`'s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return a tuple of arrays instead of simply one large array? Seems much more reasonable and makes the note much more simple
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, yes. I mainly did it in the original version to resemble the input for axes
parameters, i.e., a sequence of vectors.
I'll change it to a big array with shape (num_axes, num_params, ndim)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want the same for the axes
property of detectors? Single array instead of tuple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency yeah i think so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I'll go with it
odl/tomo/geometry/geometry.py
Outdated
Other Parameters | ||
---------------- | ||
check_bounds : bool, optional | ||
If ``True``, methods perform sanity checks on provided input |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat ambiguous what parameters are refered to here, I'd read it as the parameters to this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to come up with a better yet short explanation.
odl/tomo/geometry/parallel.py
Outdated
@@ -1014,9 +1196,9 @@ def frommatrix(cls, apart, dpart, init_matrix): | |||
# Use the standard constructor with these vectors | |||
axis, det_pos, det_axis_0, det_axis_1 = transformed_vecs | |||
if translation.size == 0: | |||
kwargs = {} | |||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just make an if translation.size != 0:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, this was the work-in-progress version just in case I want to switch back. Forgot to change to final.
odl/tomo/util/utility.py
Outdated
if np.isscalar(angles[0]): | ||
return mat.squeeze() | ||
else: | ||
# Move third axis to the beginning |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is obvious from the code. But why do we do it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want mat[0]
to be the first matrix etc. But you're right, the comment should say that rather than state the obvious.
odl/tomo/util/utility.py
Outdated
raise ValueError('zero vector') | ||
|
||
result = np.zeros(vec.shape) | ||
cond = np.any(vec[:, :2] != 0, axis=1) | ||
if np.any(cond): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this "any" make a difference in any way? I guess performance but I'd not over-optimize this right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well this any
goes along the first axis whose size is arbitrary (and can be large) so I think using np.any
makes sense there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point was if the if
statement is needed, or if we can simply go to the next line, where we get an empty assignment. I guess performance wise it shouldn't matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no that actually doesn't work. If the condition is empty, the array cannot be used for indexing.
Hm, now it works. Must have been some issue during the implementation. Will remove the checks.
odl/tomo/util/utility.py
Outdated
result[cond, 1] = vec[cond, 0] | ||
if np.any(~cond): | ||
result[~cond, 0] = 1 | ||
result / np.linalg.norm(result, axis=1, keepdims=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not afraid of zero division here, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cannot happen. The rows in which there are nonzero entries in the first two coordinates we switch them, so the norm cannot be zero. In the other rows we insert a 1
so no way to get zero norm either (including rows that are all zeros).
except ImportError: | ||
PYFFTW_AVAILABLE = False | ||
else: | ||
import pip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like adding pip
as a dependency to ODL, and further you mention this taking some performance.
I'd rather remove this, or as mentioned somewhere else explcitly check against something like
if parse_version(pyfftw.__version__) < parse_version('0.10.3.devSOMETHING'):
warnings.warn('PyFFTW < 0.10.4 is known to cause problems with some '
'ODL functionality, see issue #1002.',
RuntimeWarning)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was just a fix for myself to reduce annoyance. I'll just remove the commit.
odl/tomo/geometry/parallel.py
Outdated
Unit vector(s) along which the detector is aligned. | ||
If ``angles`` is a single pair (or triplet) of Euler angles, | ||
the returned array has shape ``(2, 3)``, otherwise | ||
is ``(2,) + broadcast(*angles).shape + (3,)``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite sure about this ordering. Opinion? Is it better to just add the extra dimensions to the left?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say that they should go to the left, more consistent with other stuff
58b021d
to
116a545
Compare
Rebased, ready for the final round. |
116a545
to
c412be8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking mighty fine!
odl/tomo/geometry/detector.py
Outdated
This default (``space_ndim = ndim + 1``) can be overridden by | ||
subclasses. | ||
""" | ||
return self.ndim + 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if __init__
took an argument space_ndim
, that would make overriding this easier and consistent with e.g. ndim
odl/tomo/geometry/detector.py
Outdated
raise NotImplementedError('normal not defined for ndim >= 3') | ||
raise NotImplementedError( | ||
'no default implementation of `surface_normal` available ' | ||
'for spaces with 4 or more dimensions') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also 0d and 1d space ^.^
odl/tomo/geometry/detector.py
Outdated
zeros_shape = [m if n == 1 and m != 1 else 1 | ||
for n, m in zip(axis_arr.shape, bcast_shape)] | ||
return axis_arr + np.zeros(zeros_shape) | ||
# TODO: use broadcast_to from Numpy when v1.10 is required |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a utility for that
odl/tomo/geometry/detector.py
Outdated
# to broadcast along all axes | ||
center_part = np.multiply.outer(1 - np.cos(param), self.center_dir) | ||
tangent_part = np.multiply.outer(np.sin(param), self.tangent_at_0) | ||
surf = self.radius * (center_part + tangent_part) | ||
if squeeze_out: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will this ever be needed given the current format?
odl/tomo/geometry/detector.py
Outdated
- ``param.shape[:-1] + (space_ndim,)`` otherwise. | ||
""" | ||
if self.ndim == 1 and self.space_ndim == 2: | ||
return -perpendicular_vector(self.surface_deriv(param)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could extend perpendicular_vector
to take several arguments as input and cover both of these cases
else: | ||
# Produce array of shape `param.shape + (ndim,)` by broadcasting | ||
axis_slc = (None,) * param.ndim + (slice(None),) | ||
# TODO: use broadcast_to from Numpy when v1.10 is required |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have a util for this somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's currently buried somewhere internally. It only exists on the tensor branch.
odl/tomo/geometry/parallel.py
Outdated
Unit vector(s) along which the detector is aligned. | ||
If ``angles`` is a single pair (or triplet) of Euler angles, | ||
the returned array has shape ``(2, 3)``, otherwise | ||
is ``(2,) + broadcast(*angles).shape + (3,)``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say that they should go to the left, more consistent with other stuff
c412be8
to
05777bf
Compare
1fa0054
to
6292fa7
Compare
Added the I'll rebase and if CI is happy, I'll merge. |
The following changes are part of this commit: - All detector methods are now vectorized. - Detectors now take a `check_bounds` parameter to switch off bounds checking for cases where milliseconds count. - The majority of methods has doctests now. - `Geometry` has a new method `surface_normal` which also replaces `normal` in flat detectors. - Implementation of `repr` for detectors is improved. - `CircleSectionDetector` has gained some flexibility (can be defined with a custom circle radius now), plus doctests.
b72b65a
to
4e42a1b
Compare
Great work! |
IncompletePR for vectorization of geometry methods.TODO:
check_bounds
to detectorsrepr
of detectorsnormalized
when only one choice is valid, but the parameter is there for API consistency)Solution: I removed
normalized
fromParallelGeometry.det_to_src
, can't seem to remember what other cases I had in mind.super()
callsSolution: I did like suggested n Use super(cls, self) instead of super() or hard-coded class? #1061
all_contained()
is really needed, seems fishySolution: Turned out a simple transpose did the job
Add unit testsHave already a bunch of doctests, and ASTRA vec geometry generation already uses vectorization, should be good enough?