Skip to content
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

ENH: Tensor-valued DiscreteLp #1238

Open
wants to merge 29 commits into
base: master
Choose a base branch
from

Conversation

kohr-h
Copy link
Member

@kohr-h kohr-h commented Nov 14, 2017

TODOs:

  • Rebase on master and resolve conflicts

  • Settle on a reasonable behavior of weights when removing/adding/slicing axes. Currently we have this (I need to check that this is correct):

    Weighting add axes remove axes slice
    ConstWeighting keep keep keep
    PerAxisWeighting add 1.0 factors subset of factors slice arrays / keep constants
    ArrayWeighting fallback no weighting fallback no weighting slice array (1)
    others fallback no weighing fallback no weighing fallback no weighing

    (1) only applies if no axes are added/removed.

  • Add byaxis_out in DiscreteLp

  • Add support for dtype with shape in TensorSpace.astype

  • Use Weighting.__getitem__ to slice, rather than an external function. Having my doubts, see below. Maybe I'll do this after all.

  • Add support for None (new axis) in various __getitem__ methods.

    • TensorSpace and its elements: no restriction
    • Weighting: array -> fallback to None, others should work w/o problems
    • DiscreteLp and its elements: new axes only in the "output" part of the dimensions, if left of everything they turn e.g. scalar functions into (1, ..., 1) tensor-valued ones.
  • Add methods newaxis and dropaxis for fine-grained control w.r.t. weighting and domain min/max

  • Change behavior of astype with shaped dtype as discussed

  • Migrate to default PerAxisWeighting everywhere

  • Fix large-scale tests

  • Decide 'threshold' vs. 'edgeitems' for printing, or make a separate issue.

  • Rename astra_cuda_bp_scaling_factor to correction_factor or so

  • Clarify meaning of "simulate" in simulate_slicing

  • More tests, in particular for the last-minute added functionality

    • All kinds of slicing of spaces and elements
    • __getitem__ and __setitem__ with more complex inputs than in the doctests
    • Weight propagation (done for reduce and outer in ufuncs)
    • ProductSpace.astype

Copy link
Member

@adler-j adler-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments. Much needed functionality that should go in soon if possible! Looking great to me


x[indices] in space[indices]

Space indexing does not work with index arrays, boolean arrays and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not index arrays?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index arrays produce flat arrays when used with Numpy arrays, and I don't know the right space for such a flat array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works in some cases anyway:

arr =np.ones([5, 5])

arr[[1, 2], :]
Out[3]: 
array([[ 1.,  1.,  1.,  1.,  1.],
       [ 1.,  1.,  1.,  1.,  1.]])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works here, too. What I mean by index arrays (seems to need clarification) is

>>> arr[[0, 1, 4], [3, 0, 0]]
array([ 1.,  1.,  1.])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what the name of that one either :/

Copy link
Member

@adler-j adler-j Nov 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it, that should actually (however weird it feels) be a flat TensorSpace with the correct dtype. I.e. we should exactly mirror the numpy behaviour.

Edit: This is not a requirement for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it should actually be an easy change. Decent suggestion anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only headache with this is that DiscreteLp does not necessarily know how to construct a TensorSpace. We can, of course, assume that shape and dtype should always be enough, but in the case of CuPy spaces (see #1231) we would also match the device, or for pyTorch spaces (#1229) usage of pinned versus virtual memory should be the same.

So I don't think threre's a better way to do it than to drop down to the tensors and let them handle the space wrapping.

self.tensor[indices] = values.tensor
if isinstance(indices, type(self)):
indices = indices.tensor.data
res_tens = self.tensor.__getitem__(indices)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why getitem instead of actual indexing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yeah 🙄

try:
res_space = self.space[indices]
except (ValueError, TypeError):
# TODO: adapt error type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be quite careful here, i guess this is OK for now, but we need to document it (similar to numpy arrays returning flat arrays in some cases, which is confusing). We also expect stuff like

vec[complicated_indices] = 2 * vec[complicated_indices]

to work.

Copy link
Member Author

@kohr-h kohr-h Dec 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That code would call __setitem__ anyway and not be affected by this. But yeah, catching to too many exceptions is a bit dangerous.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any tests that verifies that the above code actually works?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are unit tests with "complicated" indices for __setitem__, but I'll check again.

rn(2)
"""
new_shape, _, _ = simulate_slicing(self.shape, indices)
return type(self)(shape=new_shape, dtype=self.dtype)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure we want a default implementation here, this could be very implementation specific (see e.g. weighting)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, I'll remove the default implementation.

@@ -496,6 +568,11 @@ def available_dtypes():
"""
raise NotImplementedError('abstract method')

@property
def array_type(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed for this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, I used it in DiscreteLp.__getitem__, but that can be done in a different way. Removed.

@@ -2159,6 +2190,73 @@ def norm(self, x):
return float(_pnorm_diagweight(x, self.exponent, self.array))


class PerAxisWeighting(Weighting):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave out partial implementation from this PR if possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I do it here already, but no, I'll make a separate PR.

odl/test/discr/lp_discr_test.py Show resolved Hide resolved
assert discr.axis_labels == ()
assert discr.tangent_bundle == odl.ProductSpace(field=odl.RealNumbers())
assert discr.complex_space == odl.uniform_discr([], [], (), dtype=complex)
hash(discr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well check repeatability assert hash(discr) == hash(discr)

>>> # arr[[2, 0], [3, 3], [0, 1], [5, 2]]
>>> simulate_slicing(shape, ([2, 0], [3, 3], [0, 1], [5, 2]))
((2,), (1, 2, 3), 0)
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show usage with np.s_ which makes stuff like np.s_[..., 2] much easier to read

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump on this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I'd done this ...

@kohr-h kohr-h force-pushed the issue-908__discr_tens_valued branch 2 times, most recently from 7af758f to 6e1c24e Compare December 7, 2017 17:57
@pep8speaks
Copy link

pep8speaks commented Dec 11, 2017

Checking updated PR...

No PEP8 issues.

Comment last updated on July 02, 2018 at 23:13 Hours UTC

@kohr-h kohr-h force-pushed the issue-908__discr_tens_valued branch 2 times, most recently from 53d02c4 to bb31079 Compare December 11, 2017 19:46
@kohr-h kohr-h changed the title WIP: Tensor-valued DiscreteLp ENH: Tensor-valued DiscreteLp Dec 16, 2017
@kohr-h kohr-h moved this from Work in progress to Needs review in PR Status Dec 16, 2017
@kohr-h
Copy link
Member Author

kohr-h commented Dec 16, 2017

Bump

Copy link
Member

@adler-j adler-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review. More to come.

For spaces of discretized vector- or tensor-valued functions,
this includes the output components as ::

shape = fspace.out_shape + partition.shape
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this an Examples?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

odl/discr/lp_discr.py Show resolved Hide resolved
odl/discr/lp_discr.py Show resolved Hide resolved
indices = normalized_index_expression(indices, self.shape)
# Avoid array comparison with `==` if `indices.contains()` is used
if any(idx is None for idx in indices):
raise ValueError('creating new axes is not supported.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be a feature for the future? Perhaps we could create something like min_pt=[None, 0, 0], max_pt=[None, 1, 1] for this case and call it a degenerate axis?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about having "invalid" entries in min_pt and max_pt. As an alternative, we could have a method newaxis that takes an index (or a bunch of indices) and min and max.

Copy link
Member Author

@kohr-h kohr-h Jan 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the TODOs regarding this. IMO we can allow adding axes to the output part of the dimensions, i.e. users can do

>>> space = odl.uniform_discr(0, 1, 10)
>>> space[None, ...]
uniform_discr(0.0, 1.0, 10, dtype=(float, (1,)))

or

>>> space = odl.uniform_discr(0, 1, 10, dtype=(float, (2, 3)))
>>> space[None, ...]
uniform_discr(0.0, 1.0, 10, dtype=(float, (1, 2, 3)))
>>> space[:, None, None, ...]
uniform_discr(0.0, 1.0, 10, dtype=(float, (2, 1, 1, 3)))

For new axes in the "in" part of the dimensions, I'd prefer an explicit methods that takes min and max in the new axis (axes), as I wrote above.

if any(idx is None for idx in indices):
raise ValueError('creating new axes is not supported.')

indices_out = indices[:len(self.shape_out)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ndim_out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course

if values in self.space:
self.tensor[indices] = values.tensor
if isinstance(indices, type(self)):
indices = indices.tensor.data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we're truly starting to assume quite a bit about the backend. Is there no way to push this behavior down into the Tensor backend?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see what I can do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion here is to make data abstract in Tensor and therefore safe to rely on.

try:
iter(indices)
except TypeError:
if indices is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done before the try?

try:
res_space = self.space[indices]
except (ValueError, TypeError):
# TODO: adapt error type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any tests that verifies that the above code actually works?

weighting=weighting)
if (weighting is None and
is_numeric_dtype(dtype) and
exponent != float('inf')):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about -inf?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that should be covered as well.

odl/space/base_tensors.py Show resolved Hide resolved
@kohr-h kohr-h moved this from Needs review to In revision in PR Status Dec 18, 2017
Copy link
Member

@adler-j adler-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some stuff left to do, but looks great!

odl/space/npy_tensors.py Show resolved Hide resolved

x[indices] in space[indices]

is ``True``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps too "legal" of a formulation, why not

For all supported cases, indexing is implemented such that for an element ``x in space`` ::

    x[indices] in space[indices]

# broadcast, i.e. between scalar and full-blown in dimensionality?

def slice_weighting(weighting, space_shape, indices):
"""Return a weighting for a space after indexing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be implemented via indexing of the weighting? It seems this style is not scalable nor extendable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this in mind as well. That would mean putting __getitem__ and byaxis on the weighting classes, and to raise an exception where it doesn't make sense. I'd be okay with that, it would also be more extensible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel it should be done that way, much more future proof

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code again regarding this one, I'm having doubts about __getitem__. The issue is that in quite a few cases, the result after slicing is not the same class as before. That would seem okay for such simple things like (array of 1 element -> scalar) transformations, but here we make much bigger changes, like ArrayWeighting -> None. This would feel wrong in a __getitem__.

conjugate and ":math:`\odot`" for pointwise product.

- For other exponents, only norm and dist are defined. In the
case of exponent :math:`\\infty`, the weighted norm is defined
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we allow -inf?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add that, it should behave the same.

.. math::
\| a \|_{v, \\infty} := \| a \|_{\\infty},

otherwise it is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the order of this docstring, e.g. default first then special case

odl/test/trafos/fourier_test.py Show resolved Hide resolved
# Copy data to GPU memory
# Get adjoint weighting functions, using an extra correction
# factor accounting for inconsistencies in certain ASTRA versions
extra_factor = astra_cuda_bp_scaling_factor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer calling it "astra_correction" or similar

odl/tomo/operators/ray_trafo.py Show resolved Hide resolved
def simulate_slicing(shape, indices):
"""Simulate slicing into a Numpy array with given indices.

This function is intended to simulate indexing of a Numpy array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "simulate" mean here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like "pretending to do it, but really only looking at the resulting shape and stuff".

>>> # arr[[2, 0], [3, 3], [0, 1], [5, 2]]
>>> simulate_slicing(shape, ([2, 0], [3, 3], [0, 1], [5, 2]))
((2,), (1, 2, 3), 0)
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump on this

@kohr-h kohr-h force-pushed the issue-908__discr_tens_valued branch from a3be5df to f3d8880 Compare January 31, 2018 12:37
@adler-j
Copy link
Member

adler-j commented Jun 29, 2018

So, this would be a lovely addition, whats the status?

@kohr-h
Copy link
Member Author

kohr-h commented Jun 29, 2018

So, this would be a lovely addition, whats the status?

I guess I'm also gonna have my rebasing adventure... I'll try to bring it a bit up to speed this weekend.

@kohr-h kohr-h force-pushed the issue-908__discr_tens_valued branch 3 times, most recently from a3a03f7 to 90671c1 Compare June 30, 2018 23:52
@kohr-h
Copy link
Member Author

kohr-h commented Jun 30, 2018

We're back on track here again. Rebased and loads of issues fixed.

Major points are the weighting propagation and the tests yet to be written as mentioned in the TODOs.

I also kept the adjoint_weightings helper in there, since I think that in this form it's a much less intrusive addition than #1177, which still feels a bit shaky conceptually. Here we just return two functions that can used by an adjoint implementation. It's really useful to implement the correct weighting for RayTransform, so I'd like to keep it in.

@kohr-h
Copy link
Member Author

kohr-h commented Jul 2, 2018

Here's a question: If we call space.astype((float, (2,))) (i.e. with a shaped dtype), we have the following behavior currently:

  • rn(3).astype((float, (2,)))) -> rn((2, 3))
  • uniform_discr(0, 1, 3).astype((float, (2,))) -> uniform_discr(0, 1, 3, dtype=(float, (2,))
  • uniform_discr(0, 1, 3, dtype=(float, (4,))).astype((float, (2,))) -> uniform_discr(0, 1, 3, dtype=(float, (2,))

So for rn we add dtype.shape to the left as usual, but for uniform_discr we replace the current shape of the dtype with the new one. I'm wondering if this is reasonable or not -- the alternative would be to always add to the left.

@adler-j
Copy link
Member

adler-j commented Jul 3, 2018

Excellent question. To me, astype implies replacing the type, not the shape. Hence I'd say:

  • rn(3).astype((float, (2,)))) should not work

But these are all good:

  • uniform_discr(0, 1, 3).astype((float, (2,))) -> uniform_discr(0, 1, 3, dtype=(float, (2,))
  • uniform_discr(0, 1, 3, dtype=(float, (4,))).astype((float, (2,))) -> uniform_discr(0, 1, 3, dtype=(float, (2,))

@kohr-h
Copy link
Member Author

kohr-h commented Jul 3, 2018

Thanks for the answer. I think that's the most reasonable suggestions, too. In Numpy, you can't do np.ones(2).astype((float, (1,))) because the result doesn't broadcast.

So I'll go with what you write above. I was already about to implement that, but realized that removing that capability would break DiscreteLp.astype with shaped dtype. So I think I'll need to add something like TensorSpace.resize to have a method for resizing. The alternative would be to create the new space in the DiscreteLp method, which wouldn't be great (would assume knowledge of the tensor impl).

@kohr-h kohr-h added this to In Progress in Release 1.0.0 Sep 11, 2018
Holger Kohr and others added 2 commits September 12, 2018 21:42
Changes in detail:
- Add dtype with shape to DiscreteLp (mostly __repr__, factory
  functions and some downstream methods). As a consequence,
  `shape_[in,out]` and `ndim_[in,out]` are added for the
  different types of axes, as well as `scalar_dtype`.
- Add `PerAxisWeighting` and make it the default for
  `DiscreteLp` type spaces. Reason: this way the `tspace`
  knows how to deal with removed axes etc. This is important
  for a smooth experience with indexing and reductions over
  axes.
  Helpers for slicing weightings help structure this task.
- Implement `__getitem__` for `TensorSpace` and `DiscreteLp`,
  including (hopefully) reasonable propagation of weights.
  The new `simulate_slicing` utility function simplifies
  this task.
- Allow indexing with ODL tensors of boolean or integer dtype.
- Implement correct weighting for backprojections with
  non-uniform angles, using per-axis weighting and a new
  helper `adjoint_weightings` to apply the weightings in an
  efficient way.
  The correct weighting from the range of `RayTransform`
  is determined by the new `proj_space_weighting` helper.
- Change the space `_*_impl` methods to always expect and
  return Numpy arrays, and adapt the calling code.
- Change behavior of `norm` and `dist` to ignoring weights
  for `exponent=inf`, in accordance with math.
- Improve speed of `all_equal` for comparison of arrays.
- Account for `None` entries in indices in the
  `normalized_index_expression` helper, thus allowing
  creation of new axes.
- Remove `dicsr_sequence_space`, it was largely unused and
  just a maintenance burden. Use a regular `uniform-discr`
  from zero to `shape` instead.
- Remove `Weighting.equiv()` mehtods, never used and hard
  to maintain (n^2 possibilities).
- Remove the (largely useless) `_weighting` helper to create
  weighting instances since it would have been ambiguous
  with sequences of scalars (array or per axis?). Also remove
  the `npy_weighted_*` functions, they were useless, too.
- Remove some dead code from tomo/util.
- A bunch of minor fixes, as usual.

Closes: odlgroup#908
Closes: odlgroup#907
Closes: odlgroup#1113
Closes: odlgroup#965
Closes: odlgroup#286
Closes: odlgroup#267
Closes: odlgroup#1001
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
PR Status
In revision
Release 1.0.0
  
In Progress
Development

Successfully merging this pull request may close these issues.

None yet

3 participants