Skip to content

Commit

Permalink
Enable saving as JSON from CL tools (#205)
Browse files Browse the repository at this point in the history
Partially addresses #130 

* Add initial --save-json to euphonic-powder-map

* Add --save-json to other CL tools

* Check type of x_tick_labels

In euphonic.cli.utils._get_tick_labels, the label indices
are found with np.where, which returns an array of np.int32
NOT int. np.int32 is not JSON serialisable so this was causing
an error on writing to JSON.

This could have been solved in _get_tick_labels, but as setting
the labels like this on a Spectrum object seems like a common
thing to do, I thought it was better to instead validate
x_tick_labels and convert to plain ints inside the class. This goes
against the rest of Euphonic, where things are only validated on class
creation, but given the use-case I feel it is necessary (and maybe
this is a sign we should be stricter on validation elsewhere in Euphonic)

* Increase coverage

* Small Codacy updates

* Update changelog and docs
  • Loading branch information
rebeccafair authored Mar 1, 2022
1 parent e7c8827 commit 54f250b
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
``euphonic-intensity-map`` command-line tools can now read
files that don't contain eigenvectors, if eigenvectors are
not required for the chosen options.
- A new ``--save-json`` option is available for command-line tools
which produce plots, this will output the produced spectrum to
a Euphonic .json file.
- There is now the option to use a fast, approximate variable-width broadening method when
adaptively broadening dos:

Expand Down
13 changes: 13 additions & 0 deletions doc/source/powder-map-script.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
euphonic-powder-map
======================

.. contents:: :local:

.. highlight:: bash

Overview
--------

The ``euphonic-powder-map`` program can be used to sample
spherically-averaged properties from force constants data over a range
of :math:`|q|`. The results are plotted as a 2-dimensional map in :math:`(|q|, \omega)`.
Expand Down Expand Up @@ -36,6 +41,7 @@ q range with denser sampling, in THz and with the intensity widget disabled::
and energy on the y axis, showing powder-averaged coherent
inelastic neutron scattering intensities for NaCl.


To see all the command line options, run::

euphonic-powder-map -h
Expand Down Expand Up @@ -73,6 +79,13 @@ Sampling many q-points can be computationally expensive, so a progress
bar will automatically be displayed if `tqdm <https://tqdm.github.io/>`_
is installed

Output to file
--------------

The ``--save-json`` option can be used to output the produced
:ref:`Spectrum2D` object as a Euphonic .json file with a specified
name for further use in Euphonic or other programs.

Command Line Options
--------------------

Expand Down
2 changes: 2 additions & 0 deletions euphonic/cli/dispersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def main(params: Optional[List[str]] = None) -> None:
style = _compose_style(user_args=args,
base=[base_style])

if args.save_json:
spectrum.to_json_file(args.save_json)
with matplotlib.style.context(style):
_ = plot_1d(spectra,
ymin=args.e_min,
Expand Down
2 changes: 2 additions & 0 deletions euphonic/cli/dos.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def main(params: Optional[List[str]] = None) -> None:
plot_label_kwargs = _plot_label_kwargs(
args, default_xlabel=f"Energy / {dos.x_data.units:~P}")

if args.save_json:
dos.to_json_file(args.save_json)
style = _compose_style(user_args=args, base=[base_style])
with matplotlib.style.context(style):
_ = plot_1d(dos, ymin=0, **plot_label_kwargs)
Expand Down
2 changes: 2 additions & 0 deletions euphonic/cli/intensity_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def main(params: Optional[List[str]] = None) -> None:
if len(spectra) > 1:
print(f"Found {len(spectra)} regions in q-point path")

if args.save_json:
spectrum.to_json_file(args.save_json)
style = _compose_style(user_args=args, base=[base_style])
with matplotlib.style.context(style):

Expand Down
2 changes: 2 additions & 0 deletions euphonic/cli/powder_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ def main(params: Optional[List[str]] = None) -> None:
args, default_xlabel=f"|q| / {q_min.units:~P}",
default_ylabel=f"Energy / {spectrum.y_data.units:~P}")

if args.save_json:
spectrum.to_json_file(args.save_json)
if args.disable_widgets:
base = [base_style]
else:
Expand Down
3 changes: 3 additions & 0 deletions euphonic/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,9 @@ def __call__(self, parser, args, values, option_string=None):
'"golden" and "random-sphere".'))

if 'plotting' in features:
sections['file'].add_argument(
'--save-json', dest='save_json', default=None,
help='Save spectrum to a .json file with this name')
section = sections['plotting']
section.add_argument(
'-s', '--save-to', dest='save_to', default=None,
Expand Down
24 changes: 24 additions & 0 deletions euphonic/spectra.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,30 @@ def y_data(self, value: Quantity) -> None:
self.y_data_unit = str(value.units)
self._y_data = value.to(self._internal_y_data_unit).magnitude

@property
def x_tick_labels(self) -> List[Tuple[int, str]]:
return self._x_tick_labels

@x_tick_labels.setter
def x_tick_labels(self, value: Sequence[Tuple[int, str]]) -> None:
err_msg = ('x_tick_labels should be of type '
'Sequence[Tuple[int, str]] e.g. '
'[(0, "label1"), (5, "label2")]')
if value is not None:
if isinstance(value, Sequence):
for elem in value:
if not (isinstance(elem, tuple)
and len(elem) == 2
and isinstance(elem[0], Integral)
and isinstance(elem[1], str)):
raise TypeError(err_msg)
# Ensure indices in x_tick_labels are plain ints as
# np.int64/32 etc. are not JSON serializable
value = [(int(idx), label) for idx, label in value]
else:
raise TypeError(err_msg)
self._x_tick_labels = value

@abstractmethod
def to_dict(self) -> Dict[str, Any]:
"""Write to dict using euphonic.io._obj_to_dict"""
Expand Down
31 changes: 31 additions & 0 deletions tests_and_analysis/test/euphonic_test/test_spectrum1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ def test_correct_object_creation(self, spec1d_creator):
('x_tick_labels',
get_expected_spectrum1d('xsq_spectrum1d.json').x_tick_labels[0],
TypeError),
('x_tick_labels',
[(0,), (1, 'one'), (2, 'two')],
TypeError),
('x_tick_labels',
[(0, 'zero'), (1,), (2,)],
TypeError),
('x_tick_labels',
[(0, 1), (2, 3), (4, 5)],
TypeError),
('metadata',
['Not', 'a', 'dictionary'],
TypeError)])
Expand Down Expand Up @@ -297,6 +306,28 @@ def test_incorrect_unit_conversion(self, spectrum1d_file, attr,
with pytest.raises(err):
setattr(spec1d, attr, new_attr)

@pytest.mark.parametrize('value', [[(0, 'zero'), (1, 'one')]])
def test_x_tick_labels_setter(self, value):
spec1d = get_spectrum1d('xsq_spectrum1d.json')
spec1d.x_tick_labels = value
assert spec1d.x_tick_labels == value

@pytest.mark.parametrize('value', [
[(0,), (1, 'one')],
[(0, 'zero'), ('one', 'one')],
0])
def test_x_tick_labels_incorrect_setter(self, value):
spec1d = get_spectrum1d('xsq_spectrum1d.json')
with pytest.raises(TypeError):
spec1d.x_tick_labels = value

def test_x_tick_labels_converted_to_plain_int(self):
# np.arange returns an array of type np.int32
x_tick_labels = [(idx, 'label') for idx in np.arange(5)]
spec1d = get_spectrum1d('xsq_spectrum1d.json')
spec1d.x_tick_labels = x_tick_labels
assert np.all([isinstance(x[0], int) for x in spec1d.x_tick_labels])


class TestSpectrum1DMethods:
@pytest.mark.parametrize(
Expand Down
10 changes: 10 additions & 0 deletions tests_and_analysis/test/script_tests/test_dispersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from packaging import version
from scipy import __version__ as scipy_ver

from euphonic import Spectrum1DCollection
from tests_and_analysis.test.utils import (
get_data_path, get_castep_path, get_phonopy_path)
from tests_and_analysis.test.script_tests.utils import (
Expand Down Expand Up @@ -126,6 +127,15 @@ def test_plot_save_to_file(self, inject_mocks, tmpdir, dispersion_args):
euphonic.cli.dispersion.main(dispersion_args + [output_file])
assert os.path.exists(output_file)

@pytest.mark.parametrize('dispersion_args', [
[quartz_json_file, '--save-json'],
[lzo_fc_file, '--save-json']])
def test_plot_save_to_json(self, inject_mocks, tmpdir, dispersion_args):
output_file = str(tmpdir.join('test.json'))
euphonic.cli.dispersion.main(dispersion_args + [output_file])
spec = Spectrum1DCollection.from_json_file(output_file)
assert isinstance(spec, Spectrum1DCollection)

@pytest.mark.parametrize('dispersion_args', [
[quartz_no_evec_json_file, '--reorder']])
def test_no_evecs_with_reorder_raises_type_error(self, dispersion_args):
Expand Down
9 changes: 9 additions & 0 deletions tests_and_analysis/test/script_tests/test_dos.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np
import numpy.testing as npt

from euphonic import Spectrum1D
from tests_and_analysis.test.utils import (
get_data_path, get_castep_path, get_phonopy_path)
from tests_and_analysis.test.script_tests.utils import (
Expand Down Expand Up @@ -97,6 +98,14 @@ def test_plot_save_to_file(self, inject_mocks, tmpdir, dos_args):
euphonic.cli.dos.main(dos_args + [output_file])
assert os.path.exists(output_file)

@pytest.mark.parametrize('dos_args', [
[nah_phonon_file, '--save-json']])
def test_plot_save_to_json(self, inject_mocks, tmpdir, dos_args):
output_file = str(tmpdir.join('test.json'))
euphonic.cli.dos.main(dos_args + [output_file])
spec = Spectrum1D.from_json_file(output_file)
assert isinstance(spec, Spectrum1D)

@pytest.mark.parametrize('dos_args', [
[get_data_path('crystal', 'crystal_LZO.json')]])
def test_invalid_file_raises_value_error(self, dos_args):
Expand Down
9 changes: 9 additions & 0 deletions tests_and_analysis/test/script_tests/test_intensity_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from packaging import version
from scipy import __version__ as scipy_ver

from euphonic import Spectrum2D
from tests_and_analysis.test.utils import get_data_path, get_castep_path
from tests_and_analysis.test.script_tests.utils import (
get_script_test_data_path, get_current_plot_image_data, args_to_key)
Expand Down Expand Up @@ -118,6 +119,14 @@ def test_plot_save_to_file(self, inject_mocks, tmpdir, intensity_map_args):
euphonic.cli.intensity_map.main(intensity_map_args + [output_file])
assert os.path.exists(output_file)

@pytest.mark.parametrize('intensity_map_args', [
[quartz_json_file, '--save-json']])
def test_plot_save_to_json(self, inject_mocks, tmpdir, intensity_map_args):
output_file = str(tmpdir.join('test.json'))
euphonic.cli.intensity_map.main(intensity_map_args + [output_file])
spec = Spectrum2D.from_json_file(output_file)
assert isinstance(spec, Spectrum2D)

@pytest.mark.parametrize('intensity_map_args', [
[get_data_path('util', 'qgrid_444.txt')]])
def test_invalid_file_raises_value_error(self, intensity_map_args):
Expand Down
10 changes: 10 additions & 0 deletions tests_and_analysis/test/script_tests/test_powder_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from packaging import version
from scipy import __version__ as scipy_ver

from euphonic import Spectrum2D
from tests_and_analysis.test.utils import get_data_path, get_castep_path, get_phonopy_path
from tests_and_analysis.test.script_tests.utils import (
get_script_test_data_path, get_current_plot_image_data, args_to_key)
Expand Down Expand Up @@ -133,6 +134,15 @@ def test_plot_save_to_file(self, inject_mocks, tmpdir, powder_map_args):
+ quick_calc_params)
assert os.path.exists(output_file)

@pytest.mark.parametrize('powder_map_args', [
[graphite_fc_file, '--save-json']])
def test_plot_save_to_json(self, inject_mocks, tmpdir, powder_map_args):
output_file = str(tmpdir.join('test.json'))
euphonic.cli.powder_map.main(powder_map_args + [output_file]
+ quick_calc_params)
spec = Spectrum2D.from_json_file(output_file)
assert isinstance(spec, Spectrum2D)

@pytest.mark.parametrize('powder_map_args', [
[os.path.join(get_data_path(), 'util', 'qgrid_444.txt')]])
def test_invalid_file_raises_value_error(self, powder_map_args):
Expand Down

0 comments on commit 54f250b

Please sign in to comment.