Skip to content

Measurement MWP#21

Open
damskii9992 wants to merge 31 commits intodevelopfrom
Measurement_MWP
Open

Measurement MWP#21
damskii9992 wants to merge 31 commits intodevelopfrom
Measurement_MWP

Conversation

@damskii9992
Copy link
Collaborator

No description provided.

@damskii9992 damskii9992 added [scope] significant Breaking or major changes (MAJOR.minor.patch) [priority] high Should be prioritized soon [area] Measurement The data-container and viewer labels Feb 9, 2026
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This pull request does not contain a valid label. Please add one of the following labels: ['chore', 'fix', 'bugfix', 'bug', 'enhancement', 'feature', 'dependencies', 'documentation']

@damskii9992 damskii9992 added the chore temporary label Feb 9, 2026
Copy link
Member

@henrikjacobsenfys henrikjacobsenfys left a comment

Choose a reason for hiding this comment

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

Only minor nitpicks and questions.

It may be nice to provide an example notebook to see how it all works in practice.

self._data_array.coords['y'] = y_positions
self._has_physical_coords = True

def delete_physical_coord_positions(self) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Would a restore_physical_coord_positions method make sense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I don't think so? Then we could have a "restore" method for any attribute you can set, which is essentially an undo/redo stack. In any event, that is something which could be added in the future, it is not a critical feature atm. :)

if not isinstance(dimensions, dict):
raise TypeError('dimensions must be a dictionary mapping dimension names to rebin factors.')
if all(isinstance(value, Numeric) and value == 1 for value in dimensions.values()): # Reverts to original data
# self.revert_rebin() # If we want secondary rebins to work on the original data
Copy link
Member

Choose a reason for hiding this comment

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

Should presumably not be outcommented?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It should, or be completely removed, I wrote this code before deciding exactly how secondary rebins should work, but I've kept the code there, commented out, in case I change my mind later.
I decided that when rebinning, you always rebin the "current" array, which I feel like is what most users would expect, if they see a 16x16 grid, and rebin by a factor of 2, they would expect to see an 8x8 grid, rather than a 64x64 grid (if the original data was a 128x128 grid).

'title': self.display_name + title_suffix,
'clabel': 'Transmission',
'cmin': 0.0,
'cmax': 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.

Should perhaps be something like 1.1*max(self._data_array.values)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope, the colour-scale is the transmission values, which should ideally always be <= 1, but due to extinction and other effects can sometimes be slightly above 1. In my test data I have a few errenous pixels with values in the 1000's, so if the cmax is automatic, you can't see the real data at all. This can be overwritten by the user anyways. Maybe 3.0 isn't the best value, but only user-testing can really decide what the best default would be :)


monkeypatch.setattr(measurement, '_is_notebook', mock_is_notebook)
# Then Expect
measurement.plot(time_of_flight=time_of_flight) # Just ensure no exception is raised
Copy link
Member

Choose a reason for hiding this comment

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

Could you mock plot and check that it gets the right kwargs?

# Then Expect
measurement.slicer_plot() # Just ensure no exception is raised

def test_spectrum_inspector_fails_outside_notebook(self, valid_data_array):
Copy link
Member

Choose a reason for hiding this comment

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

You mentioned that some tests don't work yet, perhaps this is one of them. In any case, it fails for me with the following error, just posting in case it's useful:

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src\easyimaging\measurement\measurement.py:524: in spectrum_inspector
    return pp.inspector(self._data_array, dim='t', orientation='vertical', **inspector_kwargs_defaults)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.pixi\envs\dev\Lib\site-packages\plopp\plotting\inspector.py:206: in inspector
    f1d = linefigure(
.pixi\envs\dev\Lib\site-packages\plopp\graphics\figures.py:31: in linefigure
    return backends.get(group='2d', name='figure')(view_maker, *nodes, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.pixi\envs\dev\Lib\site-packages\plopp\backends\matplotlib\figure.py:195: in Figure
    return InteractiveFigure(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.pixi\envs\dev\Lib\site-packages\plopp\backends\matplotlib\figure.py:136: in __init__
    self.__init_figure__(View, *args, **kwargs)
.pixi\envs\dev\Lib\site-packages\plopp\backends\matplotlib\figure.py:24: in __init_figure__
    self.view = View(*args, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^
.pixi\envs\dev\Lib\site-packages\plopp\graphics\graphicalview.py:100: in __init__
    self.canvas = canvas_maker(
.pixi\envs\dev\Lib\site-packages\plopp\backends\matplotlib\canvas.py:157: in __init__
    self.fig = make_figure(figsize=(6.0, 4.0) if figsize is None else figsize)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (), kwargs = {'figsize': (6.0, 4.0)}
fig = <Figure size 600x400 with 0 Axes>

    def make_figure(*args, **kwargs) -> plt.Figure:
        """
        Create a new figure.
    
        If we use ``plt.figure()`` directly, the figures auto-show in the notebooks.
        We want to display the figures when the figure repr is requested.
    
        When using the static backend, we can return the ``plt.Figure`` (note the uppercase
        F) directly.
        When using the interactive backend, we need to do more work. The ``plt.Figure``
        will not have a toolbar nor will it be interactive, as opposed to what
        ``plt.figure`` returns. To fix this, we need to create a manager for the figure
        (see https://stackoverflow.com/a/75477367).
        """
        fig = plt.Figure(*args, **kwargs)
        if is_interactive_backend():
            # Create a manager for the figure, which makes it interactive, as well as
            # making it possible to show the figure from the terminal.
>           plt._backend_mod.new_figure_manager_given_figure(1, fig)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'NoneType' object has no attribute 'new_figure_manager_given_figure'

.pixi\envs\dev\Lib\site-packages\plopp\backends\matplotlib\utils.py:71: AttributeError

): # noqa: E501
measurement.spectrum_inspector()

def test_spectrum_inspector_runs_in_notebook_with_widget_backend(self, valid_data_array, monkeypatch, _use_ipympl):
Copy link
Member

Choose a reason for hiding this comment

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

_use_ipympl not found

Copy link
Member

Choose a reason for hiding this comment

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

Do you need to have the same file in two different places in the project?

else:
raise ValueError('Both x_range and y_range must be provided together or not at all.')

def set_pixel_coord_range(self, x_pixel_range: Sequence[int, int], y_pixel_range: Sequence[int, int]) -> 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 and similar lines are quite long and therefore not so easy to read:

def set_pixel_coord_range(self, x_pixel_range: Sequence[int, int], y_pixel_range: Sequence[int, int]) -> None:

Maybe it would be better to format them like this:

def set_pixel_coord_range(
        self, 
        x_pixel_range: Sequence[int, int], 
        y_pixel_range: Sequence[int, int]
) -> None:

Yes, this uses more lines, but it is much easier to read. It is faster to see what the arguments are, their types, and the return value.

Copy link
Member

@rozyczko rozyczko left a comment

Choose a reason for hiding this comment

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

A few issues, nothing blocking.

Revert any rebinning applied to the measurement data, restoring it to its original resolution.
"""
if self._rebinned_data_array is not None:
del self._rebinned_data_array
Copy link
Member

Choose a reason for hiding this comment

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

This will call AttributeError if called before any rebinning (_rebinned_data_array is not defined!)
Consider guards

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good find.

Comment on lines +190 to +192
@_data_array.setter
def _data_array(self, value: sc.DataArray) -> None:
raise AttributeError('Cannot set _data_array, it is a read-only property.')
Copy link
Member

Choose a reason for hiding this comment

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

Pytohn will raise AttributeError even without the setter - this is a private property.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are no private properties in Python . . .
The _ prefix is just a convention to mark something as "private", users can access these without issue and also set them if they so desire. But that is what I am blocking here. It's mostly a help to myself as users shouldn't even be aware of the _data_array property :)

Copy link
Member

Choose a reason for hiding this comment

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

I probably meant something which I didn't articulate properly. Of course I don't remember what now...
feel free to disregard

Comment on lines +597 to +610
def _is_notebook(self) -> bool:
"""
Check if the code is running in a Jupyter notebook environment.
"""
try:
shell = get_ipython().__class__.__name__ # pyright: ignore[reportUndefinedVariable]
if shell == 'ZMQInteractiveShell':
return True # Jupyter notebook or qtconsole
elif shell == 'TerminalInteractiveShell':
return False # Terminal running IPython
else:
return False # Other type (possibly other IDE)
except NameError:
return False # Probably standard Python interpreter
Copy link
Member

Choose a reason for hiding this comment

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

This should be a @staticmethod

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Better yet, it should be a utility function.

Comment on lines +23 to +24
if TYPE_CHECKING:
pass
Copy link
Member

Choose a reason for hiding this comment

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

or just remove this

@codecov
Copy link

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 88.30409% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.88%. Comparing base (6a33ca5) to head (de17e30).

Files with missing lines Patch % Lines
src/easyimaging/measurement/measurement.py 84.66% 47 Missing and 3 partials ⚠️
src/easyimaging/measurement/regions.py 96.98% 5 Missing ⚠️
...rc/easyimaging/utils/jupyter_notebook_utilities.py 50.00% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           develop      #21       +/-   ##
============================================
+ Coverage    57.14%   87.88%   +30.74%     
============================================
  Files            2        8        +6     
  Lines            7      520      +513     
  Branches         0       84       +84     
============================================
+ Hits             4      457      +453     
- Misses           3       60       +57     
- Partials         0        3        +3     
Flag Coverage Δ
unittests 87.88% <88.30%> (+30.74%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[area] Measurement The data-container and viewer chore temporary [priority] high Should be prioritized soon [scope] significant Breaking or major changes (MAJOR.minor.patch)

Projects

4 participants