Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/docs/tutorials/tutorial1_brownian.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
"source": [
"We can visualize the data in multiple ways, relying on plopp: https://scipp.github.io/plopp/\n",
"\n",
"We here show two ways to look at the data: as a 2d colormap with intensity as function of `Q` and `energy`, and as a slicer with intensity as function of `energy` for various `Q`."
"We here show two ways to look at the data: as a 2d colormap with intensity as function of `Q` and `energy`, and as a slicer with intensity as function of `energy` for various `Q`.\n",
"\n",
"If you want $Q$ on the x axis, then set `transpose_axes=True`"
]
},
{
Expand All @@ -86,7 +88,7 @@
"metadata": {},
"outputs": [],
"source": [
"vanadium_experiment.plot_data(slicer=False)"
"vanadium_experiment.plot_data(slicer=False, transpose_axes=False)"
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,7 @@ select = [
# Ignore specific rules globally
ignore = [
'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/
# The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint]
'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc
# The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc
# Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout
'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/
]
Expand Down
48 changes: 38 additions & 10 deletions src/easydynamics/experiment/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import plopp as pp
import scipp as sc
from easyscience.base_classes.new_base import NewBase
from plopp.backends.matplotlib.figure import InteractiveFigure
from scipp.io import load_hdf5 as sc_load_hdf5
from scipp.io import save_hdf5 as sc_save_hdf5

Expand Down Expand Up @@ -146,9 +147,9 @@ def Q(self) -> sc.Variable | None:
sc.Variable | None
The Q values from the dataset, or None if no data is loaded.
"""
if self._binned_data is None:
if self.binned_data is None:
return None
return self._binned_data.coords['Q']
return self.binned_data.coords['Q']

@Q.setter
def Q(self, _value: sc.Variable) -> None:
Expand Down Expand Up @@ -179,9 +180,9 @@ def energy(self) -> sc.Variable | None:
sc.Variable | None
The energy values from the dataset, or None if no data is loaded.
"""
if self._binned_data is None:
if self.binned_data is None:
return None
return self._binned_data.coords['energy']
return self.binned_data.coords['energy']

@energy.setter
def energy(self, _value: sc.Variable) -> None:
Expand Down Expand Up @@ -222,7 +223,7 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None:
sc.Variable | None
The masked energy values from the dataset, or None if no data is loaded.
"""
if self._binned_data is None:
if self.binned_data is None:
return None

if (
Expand All @@ -232,7 +233,7 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None:
):
raise IndexError('Q_index must be a valid index for the Q values.')

energy = self._binned_data.coords['energy']
energy = self.binned_data.coords['energy']
_, _, _, mask = self._extract_x_y_weights_only_finite(Q_index=Q_index)

mask_var = sc.array(dims=['energy'], values=mask)
Expand Down Expand Up @@ -372,31 +373,54 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None:
# other methods
###########

def plot_data(self, slicer: bool = False, **kwargs: dict) -> None:
def plot_data(
self,
slicer: bool = False,
transpose_axes: bool = False,
Comment on lines +378 to +379
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this mean that transpose_axes is silently ignored when slicer=True? Maybe add a docstring in this case,

"Only applies when slicer=False"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, it only applies to 2d plots without the slicer. When I add more coordinataes to the data I may have to revisit this, since a 2d slicer is possible. But I'll cross that bridge when I get to it

**kwargs: dict,
) -> InteractiveFigure:
"""
Plot the dataset using plopp: https://scipp.github.io/plopp/.

Parameters
----------
slicer : bool, default=False
If True, use plopp's slicer instead of plot.
transpose_axes : bool, default=False
If True, transpose the data to have dimensions in the order (energy, Q) before
plotting, so that energy is on the x-axis. This only applies when slicer=False.
**kwargs : dict
Additional keyword arguments to pass to plopp.

Returns
-------
InteractiveFigure
A plot of the data and model.

Raises
------
ValueError
If there is no data to plot.
RuntimeError
If not in a Jupyter notebook environment.
TypeError
If slicer or transpose_axes are not True or False.
"""

if self._binned_data is None:
if self.binned_data is None:
raise ValueError('No data to plot. Please load data first.')

if not _in_notebook():
raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.')

if not isinstance(slicer, bool):
raise TypeError(f'slicer must be True or False, not {type(slicer).__name__}')

if not isinstance(transpose_axes, bool):
raise TypeError(
f'transpose_axes must be True or False, not {type(transpose_axes).__name__}'
)

plot_kwargs_defaults = {
'title': self.display_name,
}
Expand All @@ -408,15 +432,19 @@ def plot_data(self, slicer: bool = False, **kwargs: dict) -> None:
plot_kwargs_defaults.update(kwargs)
if slicer:
fig = pp.slicer(
self._binned_data,
self.binned_data,
**plot_kwargs_defaults,
)
for widget in fig.bottom_bar[0].controls.values():
widget.slider_toggler.value = '-o-'

else:
if transpose_axes:
data_to_plot = self.binned_data.transpose(dims=['energy', 'Q'])
else:
data_to_plot = self.binned_data.transpose(dims=['Q', 'energy'])
fig = pp.plot(
self._binned_data.transpose(dims=['energy', 'Q']),
data_to_plot,
**plot_kwargs_defaults,
)
return fig
Expand Down
14 changes: 10 additions & 4 deletions tests/unit/easydynamics/analysis/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ def test_plot_data_and_model_defaults(self, analysis):
fake_fig.bottom_bar = [MagicMock()]
fake_fig.bottom_bar[0].controls = {'test': fake_widget}

fake_data = MagicMock()
fake_data.coords = {'Q': 'Q_VALUES', 'energy': 'ENERGY_VALUES'}

analysis._create_model_array = MagicMock(return_value='MODEL')
with (
patch('plopp.slicer', return_value=fake_fig) as mock_slicer,
Expand All @@ -270,7 +273,7 @@ def test_plot_data_and_model_defaults(self, analysis):
) as mock_binned,
patch('easydynamics.analysis.analysis._in_notebook', return_value=True),
):
mock_binned.return_value = 'DATA'
mock_binned.return_value = fake_data
# THEN
fig = analysis.plot_data_and_model(plot_components=False)

Expand All @@ -284,7 +287,7 @@ def test_plot_data_and_model_defaults(self, analysis):
assert 'Data' in data_passed
assert 'Model' in data_passed

assert data_passed['Data'] == 'DATA'
assert data_passed['Data'] == fake_data
assert data_passed['Model'] == 'MODEL'

# Check the default kwargs
Expand All @@ -310,6 +313,9 @@ def test_plot_data_and_model_plot_components_true(self, analysis):
fake_fig.bottom_bar = [MagicMock()]
fake_fig.bottom_bar[0].controls = {'test': fake_widget}

fake_data = MagicMock()
fake_data.coords = {'Q': 'Q_VALUES', 'energy': 'ENERGY_VALUES'}

analysis._create_model_array = MagicMock(return_value='MODEL')
analysis._create_components_dataset = MagicMock(return_value={'Gaussian': 'GAUSS'})
with (
Expand All @@ -321,7 +327,7 @@ def test_plot_data_and_model_plot_components_true(self, analysis):
) as mock_binned,
patch('easydynamics.analysis.analysis._in_notebook', return_value=True),
):
mock_binned.return_value = 'DATA'
mock_binned.return_value = fake_data
# THEN
fig = analysis.plot_data_and_model(plot_components=True)

Expand All @@ -335,7 +341,7 @@ def test_plot_data_and_model_plot_components_true(self, analysis):
assert 'Data' in data_passed
assert 'Model' in data_passed

assert data_passed['Data'] == 'DATA'
assert data_passed['Data'] == fake_data
assert data_passed['Model'] == 'MODEL'
# Check the default kwargs
assert kwargs['title'] == 'TestAnalysis'
Expand Down
33 changes: 30 additions & 3 deletions tests/unit/easydynamics/experiment/test_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,15 @@ def test_get_masked_energy_invalid_Q_index_raises(self, experiment_with_data, Q_
# test plotting
##############

def test_plot_data_success(self, experiment):
@pytest.mark.parametrize(
'transpose_axes, expected_dims',
[
(False, ['Q', 'energy']),
(True, ['energy', 'Q']),
],
ids=['no_transpose', 'transpose'],
)
def test_plot_data_success(self, experiment, transpose_axes, expected_dims):
"Test plotting data successfully when in notebook environment"
# WHEN
with (
Expand All @@ -343,12 +351,12 @@ def test_plot_data_success(self, experiment):
mock_plot.return_value = mock_fig

# THEN
result = experiment.plot_data()
result = experiment.plot_data(transpose_axes=transpose_axes)

# EXPECT
mock_plot.assert_called_once()
args, kwargs = mock_plot.call_args
assert sc.identical(args[0], experiment.data.transpose())
assert sc.identical(args[0], experiment.data.transpose(dims=expected_dims))
assert kwargs['title'] == f'{experiment.display_name}'
assert result == mock_fig

Expand Down Expand Up @@ -395,6 +403,25 @@ def test_plot_data_not_in_notebook_raises(self, experiment):
):
experiment.plot_data()

def test_plot_data_invalid_slicer_type_raises(self, experiment):
"Test plotting data raises TypeError when slicer argument is invalid"
# WHEN THEN EXPECT

with (
patch(f'{Experiment.__module__}._in_notebook', return_value=True),
pytest.raises(TypeError, match='slicer must be True or False'),
):
experiment.plot_data(slicer='not_a_boolean')

def test_plot_data_invalid_transpose_type_raises(self, experiment):
"Test plotting data raises TypeError when transpose argument is invalid"
# WHEN THEN EXPECT
with (
patch(f'{Experiment.__module__}._in_notebook', return_value=True),
pytest.raises(TypeError, match='transpose_axes must be True or False'),
):
experiment.plot_data(transpose_axes='not_a_boolean')

##############
# test private methods
##############
Expand Down
Loading