diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f85163359..bb6ac7249 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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: diff --git a/doc/source/powder-map-script.rst b/doc/source/powder-map-script.rst index 832200a56..b2db81bae 100644 --- a/doc/source/powder-map-script.rst +++ b/doc/source/powder-map-script.rst @@ -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)`. @@ -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 @@ -73,6 +79,13 @@ Sampling many q-points can be computationally expensive, so a progress bar will automatically be displayed if `tqdm `_ 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 -------------------- diff --git a/euphonic/cli/dispersion.py b/euphonic/cli/dispersion.py index 909550923..1a03521d9 100644 --- a/euphonic/cli/dispersion.py +++ b/euphonic/cli/dispersion.py @@ -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, diff --git a/euphonic/cli/dos.py b/euphonic/cli/dos.py index d6d1e743d..0116eb6b1 100644 --- a/euphonic/cli/dos.py +++ b/euphonic/cli/dos.py @@ -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) diff --git a/euphonic/cli/intensity_map.py b/euphonic/cli/intensity_map.py index 2cc545b65..5e37c7b0f 100755 --- a/euphonic/cli/intensity_map.py +++ b/euphonic/cli/intensity_map.py @@ -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): diff --git a/euphonic/cli/powder_map.py b/euphonic/cli/powder_map.py index 0ae1c62e7..f876e0d9d 100755 --- a/euphonic/cli/powder_map.py +++ b/euphonic/cli/powder_map.py @@ -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: diff --git a/euphonic/cli/utils.py b/euphonic/cli/utils.py index 04b922a27..169cdad9b 100644 --- a/euphonic/cli/utils.py +++ b/euphonic/cli/utils.py @@ -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, diff --git a/euphonic/spectra.py b/euphonic/spectra.py index 0b0198e23..e62bf9634 100644 --- a/euphonic/spectra.py +++ b/euphonic/spectra.py @@ -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""" diff --git a/tests_and_analysis/test/euphonic_test/test_spectrum1d.py b/tests_and_analysis/test/euphonic_test/test_spectrum1d.py index f6c83bf7e..74756bb82 100644 --- a/tests_and_analysis/test/euphonic_test/test_spectrum1d.py +++ b/tests_and_analysis/test/euphonic_test/test_spectrum1d.py @@ -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)]) @@ -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( diff --git a/tests_and_analysis/test/script_tests/test_dispersion.py b/tests_and_analysis/test/script_tests/test_dispersion.py index 9941659d5..87584f1c5 100644 --- a/tests_and_analysis/test/script_tests/test_dispersion.py +++ b/tests_and_analysis/test/script_tests/test_dispersion.py @@ -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 ( @@ -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): diff --git a/tests_and_analysis/test/script_tests/test_dos.py b/tests_and_analysis/test/script_tests/test_dos.py index 0de46fca9..6a242da6c 100644 --- a/tests_and_analysis/test/script_tests/test_dos.py +++ b/tests_and_analysis/test/script_tests/test_dos.py @@ -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 ( @@ -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): diff --git a/tests_and_analysis/test/script_tests/test_intensity_map.py b/tests_and_analysis/test/script_tests/test_intensity_map.py index f3a9eef76..abe5f6553 100644 --- a/tests_and_analysis/test/script_tests/test_intensity_map.py +++ b/tests_and_analysis/test/script_tests/test_intensity_map.py @@ -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) @@ -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): diff --git a/tests_and_analysis/test/script_tests/test_powder_map.py b/tests_and_analysis/test/script_tests/test_powder_map.py index 5c5ed95cc..5e8c7e57a 100644 --- a/tests_and_analysis/test/script_tests/test_powder_map.py +++ b/tests_and_analysis/test/script_tests/test_powder_map.py @@ -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) @@ -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):