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

SourceTFR #6543

Closed
wants to merge 5 commits into from
Closed

SourceTFR #6543

wants to merge 5 commits into from

Conversation

DiGyt
Copy link
Contributor

@DiGyt DiGyt commented Jul 9, 2019

This is a draft PR for a tentative Version of a SourceTFR class which is used to store time-frequency transformed Source Estimates. It does not yet cover some relevant functions (i.e. plotting or reading from disc).

This PR was mainly created, so @massich can review the code.

DiGyt added 5 commits June 6, 2019 14:36
Added Basic SourceTFR structure and Text

Signed-off-by: Dirk Gütlin <[email protected]>
Added Basic SourceTFR structure and Text

Signed-off-by: Dirk Gütlin <[email protected]>
Updated SourceTFR to version used for reading morlet/multitapers

Signed-off-by: Dirk Gütlin <[email protected]>
#
# Authors: Dirk Gütlin <[email protected]>
# TODO: This file uses a lot of stuff from io/base.py and source_estimate.py
# TODO: Maybe name these persons as authors too?
Copy link
Contributor

Choose a reason for hiding this comment

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

to me, the authors are the list of people that would know something about that file. It is the people I'm going to send them an email when things go south.

People who wrote the file or closely reviewed.

if not (isinstance(vertices, np.ndarray) or
isinstance(vertices, list)):
raise ValueError('Vertices must be a numpy array or a list of '
'arrays')
Copy link
Contributor

Choose a reason for hiding this comment

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

we have functions for checking parameters.

"""

@verbose
def __init__(self, data, vertices=None, tmin=None, tstep=None,
Copy link
Contributor

@massich massich Jul 12, 2019

Choose a reason for hiding this comment

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

break the __init__ with functions. I know that some of our inits are huge, but they should be short easy to read in a glance.

def __init__(foo, bar, data, vertices,  bla):
  _check_param(foo, ...)
  self._data = _prepare_data(data)
  self._vercies = _foo(vertices)
  ...

to me init should be just assigning stuff to the attributes. If _prepare_data has a signature like _prepare_data(data=data, n_col=vertices.shape[0], moon_color=foo) it tels me at a glance how data is related to the other things. Sure I would not know the details but I least I would know the possible relations. If I have to read the code and strip all the value errors out on my mind the task of reading the code is much harder.

keep in mind: code is written once but read endless times and when you read code you want to skim it. Not digest it.

self._update_times()
self.subject = _check_subject(None, subject, False)

def __repr__(self): # noqa: D105
Copy link
Contributor

@massich massich Jul 12, 2019

Choose a reason for hiding this comment

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

Don't use '%' to parse strings (we are no longer in the 90s ;) ).

fstrings are much more readable and they are the way to go (see doc). However, we cannot use them until we drop python 3.5 support. Meanwhile, we could be using 'my name is {name}'.format(name='massich')

https://realpython.com/python-f-strings/

Copy link
Member

Choose a reason for hiding this comment

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

We still use % all over the code base and have not agreed to change to / prefer .format -- I think % is fine for now

"""
if ftype != 'h5':
raise ValueError('%s objects can only be written as HDF5 files.'
% (self.__class__.__name__,))
Copy link
Contributor

Choose a reason for hiding this comment

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

use checks

raise ValueError('%s objects can only be written as HDF5 files.'
% (self.__class__.__name__,))
if not fname.endswith('.h5'):
fname += '-stfr.h5'
Copy link
Contributor

@massich massich Jul 12, 2019

Choose a reason for hiding this comment

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

avoid ifs without elses as much as possible. See this awesome talk by @ Katrina Owen

in a case like this one, I would do something like:

fname = fname if fname.endswith('h5') else '{}-stfr.h5'.format(fname)

or any way you like to build the fname. (I like this one because it has no spaces but maybe fname + '-stfr.h5' is a better choice)

return self._data.shape
return (self._kernel.shape[0],
self._sens_data.shape[1],
self._sens_data.shape[2])
Copy link
Contributor

Choose a reason for hiding this comment

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

use else!!!

Copy link
Contributor

Choose a reason for hiding this comment

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

document what returns.

does this return always the same kind of tuple?


@times.setter
def times(self, value):
raise ValueError('You cannot write to the .times attribute directly. '
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this a value error? isn't it a runtime?

Is there any other way to prevent the setter? maybe not.

from mne.source_tfr import SourceTFR


def _fake_stfr():
Copy link
Contributor

Choose a reason for hiding this comment

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

fixture

def _fake_kernel_stfr():
"""Create a fake kernelized SourceTFR."""
kernel = np.random.rand(100, 40)
sens_data = np.random.rand(40, 20, 10)
Copy link
Contributor

Choose a reason for hiding this comment

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

fixture


# check if data is in correct shape
expected = [100, 10, 30]
assert_allclose([kernel_stfr.shape, full_stfr.shape,
Copy link
Contributor

Choose a reason for hiding this comment

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

why are you testing it like this? by creating a vector that will have no name and when we read the error we will know nothing about it? why allclose? is it possible to have an array with shape 100+epsilon?

assert kernel_stfr.shape == (100, 10, 30)
assert full_stfr.shape == (100, 10, 30)
assert bla.shape == (100, 10, 30)
assert foo.shape == (watever)

will return a meaningful error that I will read and I would understand what is my next step. otherwise, I'll have to put a breakpoint stop the execution, understand the array you have created think about all those things and make a guess of what could be wrong. I'm not smart enough. I would rather prefer pytest telling me where should I look.

assert_equal(stfr._data.shape[-1], n_times)
assert_array_equal(stfr.times, stfr.tmin + np.arange(n_times) * stfr.tstep)

assert_array_almost_equal(
Copy link
Contributor

Choose a reason for hiding this comment

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

avoid almost_equal either equal or allclose


# tstep <= 0 is not allowed
pytest.raises(ValueError, attempt_assignment, stfr, 'tstep', 0)
pytest.raises(ValueError, attempt_assignment, stfr, 'tstep', -1)
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is compact, but when I read this I need to go from
attempt_assignment, stfr, 'tstep', 0 to attempt_assignment(foo=foo, bar=bar..) inside my head. Its better to write with pytest.raises(ValueError, match='AND A MEANINGFUL ERROR MATCH THAT TELLS ME WHY THIS SHOULD FAIL')

pytest.raises(ValueError, attempt_assignment, stfr_kernel, 'data',
[np.arange(100)])
pytest.raises(ValueError, attempt_assignment, stfr_kernel, 'data',
[[[np.arange(100)]]])
Copy link
Contributor

Choose a reason for hiding this comment

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

use with

@requires_h5py
def test_io_stfr_h5():
"""Test IO for stfr files using HDF5."""
for stfr in [_fake_stfr(), _fake_kernel_stfr()]:
Copy link
Contributor

Choose a reason for hiding this comment

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

This screams parametrize

stfr_ = _fake_stfr()
kernel_stfr_ = _fake_kernel_stfr()

for stfr in [stfr_, kernel_stfr_]:
Copy link
Contributor

Choose a reason for hiding this comment

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

so does this one


copy_2 = inst.copy()
assert_allclose(copy_2.crop(tmin=0.2).data, inst.data[:, :, 2:])
assert_allclose(copy_2.times, inst.times[2:])
Copy link
Contributor

Choose a reason for hiding this comment

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

the reference you compare with is the later assert actual == expected. Same thing `assert_allclose(actual, expected)


def test_invalid_params():
"""Test catching invalid parameters for SourceTFR."""
data = np.random.rand(40, 10, 20)
Copy link
Contributor

Choose a reason for hiding this comment

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

either you use a seed and then you compare against it or use np.empty.

assert_allclose(copy_2.times, inst.times[2:])


def test_invalid_params():
Copy link
Contributor

Choose a reason for hiding this comment

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

parametrize

DiGyt added a commit to DiGyt/mne-python that referenced this pull request Aug 5, 2019
...according to review in PR mne-tools#6543

Signed-off-by: Dirk Gütlin <[email protected]>
@DiGyt
Copy link
Contributor Author

DiGyt commented Aug 5, 2019

Thanks for the review @massich and all others!

I didn't implement the changes here, but in PR #6629 . As changes in # 6629 will highly depend on changes here (and vice versa), I think it's the best idea to maintain everything in one comprehensive pull and close this one.

@DiGyt DiGyt closed this Aug 5, 2019
@massich
Copy link
Contributor

massich commented Aug 5, 2019

I didn't implement the changes here, but in PR #6629 . As changes in # 6629 will highly depend on changes here (and vice versa), I think it's the best idea to maintain everything in one comprehensive pull and close this one.

Usually we try to isolate the things, split them as much as possible and write dependencies between them. (makes review easier)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants