diff --git a/.github/workflows/conda_unit_test.yml b/.github/workflows/conda_unit_test.yml index 5090d8e9..cb22b0b2 100644 --- a/.github/workflows/conda_unit_test.yml +++ b/.github/workflows/conda_unit_test.yml @@ -30,7 +30,7 @@ jobs: max-parallel: 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Create Python ${{ matrix.python-version }} environment uses: mamba-org/setup-micromamba@v1 diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 92806704..340f4f58 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -54,16 +54,21 @@ jobs: run: | echo "The docs will be published from this workflow run." - - name: Install Python ${{ matrix.python-version }} Conda environment with micromamba from env-dev.yml - uses: mamba-org/provision-with-micromamba@main + - name: Create Python documentation environment + uses: mamba-org/setup-micromamba@v1 with: environment-file: env-dev.yml - environment-name: test + environment-name: documentation + condarc: | + channel-priority: flexible + init-shell: bash + cache-environment: true + cache-environment-key: env-key - name: Install the package locally shell: bash -l {0} run: | - pip install -e . + pip install --no-deps -e . -vv - name: Diagnostics shell: bash -l {0} @@ -72,7 +77,7 @@ jobs: conda list env | sort | grep -i CONDA - - name: Build Docs + - name: Build Docs with Sphinx shell: bash -l {0} run: | make -C docs clean html diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 644ca98b..7e40d519 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -30,6 +30,7 @@ New Features and/or Enhancements * Add ``SoleilNanoscopiumRobot`` diffractometer geometry. * Add ``SoleilSixsMed2p3v2`` diffractometer geometry. * Export and restore diffractometer configuration as JSON string, YAML string, Python dictionary, or file. +* Add ``user.current_diffractometer()`` function. Fixes ----- diff --git a/env-dev.yml b/env-dev.yml index 07449f0b..e2ebbeb6 100644 --- a/env-dev.yml +++ b/env-dev.yml @@ -16,7 +16,7 @@ dependencies: - apischema - black - bluesky - - databroker =1.2 + - databroker - flake8 - hkl - ipython diff --git a/environment.yml b/environment.yml index 3f639352..b573ab49 100644 --- a/environment.yml +++ b/environment.yml @@ -15,7 +15,6 @@ dependencies: - python >=3.8,<3.12 - apischema - bluesky - - databroker =1.2 - hkl - numpy - ophyd diff --git a/hkl/configuration.py b/hkl/configuration.py index ebc14f3d..e0aeee99 100644 --- a/hkl/configuration.py +++ b/hkl/configuration.py @@ -53,6 +53,8 @@ DEFAULT_WAVELENGTH = 1.54 # angstrom EXPORT_FORMATS = "dict json yaml".split() +EQUAL_TOLERANCE = 1.0e-7 + # standard value checks, raise exception(s) when appropriate def _check_key(key, biblio, intro): @@ -261,12 +263,12 @@ def validate(self, dc_obj): def write(self, diffractometer): """Write sample details to diffractometer.""" - s = diffractometer.calc._samples.get(self.name) + sample = diffractometer.calc._samples.get(self.name) lattice_parameters = list(self.lattice.values) - if s is None: - s = diffractometer.calc.new_sample(self.name, lattice=lattice_parameters) + if sample is None: + sample = diffractometer.calc.new_sample(self.name, lattice=lattice_parameters) else: - s.lattice = lattice_parameters + sample.lattice = lattice_parameters reflection_list = [] for reflection in self.reflections: @@ -282,18 +284,18 @@ def write(self, diffractometer): w1 = rdict["wavelength"] try: diffractometer.calc.wavelength = w1 - r = diffractometer.calc.sample.add_reflection(*args) + r = sample.add_reflection(*args) + if rdict["orientation_reflection"]: + reflection_list.append(r) except RuntimeError as exc: raise RuntimeError(f"could not add reflection({args}, wavelength={w1})") from exc finally: diffractometer.calc.wavelength = w0 - - if rdict["orientation_reflection"]: - reflection_list.append(r) + # Remaining code will not be executed if exception was raised. if len(reflection_list) > 1: r1, r2 = reflection_list[0], reflection_list[1] - diffractometer.calc.sample.compute_UB(r1, r2) + sample.compute_UB(r1, r2) @dataclass @@ -550,6 +552,11 @@ def restore(self, data, clear=True, restore_constraints=True): If ``True`` (default), remove any previous configuration of the diffractometer and reset it to default values before restoring the configuration. + + If ``False``, sample reflections will be append with all reflections + included in the configuration data for that sample. Existing + reflections will not be changed. The user may need to edit the + list of reflections after ``restore(clear=False)``. restore_constraints *bool*: If ``True`` (default), restore any constraints provided. diff --git a/hkl/sample.py b/hkl/sample.py index 1c52a535..e67a0f69 100644 --- a/hkl/sample.py +++ b/hkl/sample.py @@ -1,3 +1,5 @@ +"""Sample on the diffractometer.""" + import logging import numpy as np @@ -88,21 +90,19 @@ class HklSample(object): .. autosummary:: + ~name ~add_reflection - ~affine + ~remove_reflection + ~swap_orientation_reflections ~clear_reflections + ~affine ~compute_UB - ~hkl_calc - ~hkl_sample ~lattice - ~name ~reciprocal ~reflection_measured_angles ~reflection_theoretical_angles ~reflections ~reflections_details - ~remove_reflection - ~swap_orientation_reflections ~U ~UB ~ux @@ -113,6 +113,8 @@ class HklSample(object): .. autosummary:: + ~hkl_calc + ~hkl_sample ~__repr__ ~__str__ ~_create_reflection diff --git a/hkl/tests/__init__.py b/hkl/tests/__init__.py index bff05675..e69de29b 100644 --- a/hkl/tests/__init__.py +++ b/hkl/tests/__init__.py @@ -1,32 +0,0 @@ -import logging - -import numpy - -from ..util import new_lattice - -logger = logging.getLogger("ophyd_session_test") - -TARDIS_TEST_MODE = "lifting_detector_mu" -TWO_PI = 2 * numpy.pi - - -def new_sample(diffractometer, name, lattice): - diffractometer.calc.new_sample(name, lattice=lattice) - - -def sample_kryptonite(diffractometer): - triclinic = new_lattice(4, 5, 6, 75, 85, 95) - new_sample(diffractometer, "kryptonite", lattice=triclinic) - - -def sample_silicon(diffractometer): - from .. import SI_LATTICE_PARAMETER - - cubic = new_lattice(SI_LATTICE_PARAMETER) - new_sample(diffractometer, "silicon", lattice=cubic) - - -def sample_vibranium(diffractometer): - a0 = TWO_PI - cubic = new_lattice(a0) - new_sample(diffractometer, "vibranium", lattice=cubic) diff --git a/hkl/tests/test_configuration.py b/hkl/tests/test_configuration.py index 20ca0173..41decc99 100644 --- a/hkl/tests/test_configuration.py +++ b/hkl/tests/test_configuration.py @@ -15,7 +15,7 @@ from ..configuration import DCSample from ..util import Constraint from ..util import new_lattice -from . import TWO_PI +from .tools import TWO_PI TEST_CONFIG_FILE = "data/e4c-config.json" diff --git a/hkl/tests/test_restore_reflections.py b/hkl/tests/test_restore_reflections.py new file mode 100644 index 00000000..7d4bd103 --- /dev/null +++ b/hkl/tests/test_restore_reflections.py @@ -0,0 +1,61 @@ +""" +Test that reflections from one sample are not restored to other samples. +""" + +from .. import DiffractometerConfiguration +from .tools import sample_kryptonite +from .tools import sample_silicon +from .tools import sample_vibranium + + +def test_issue289(e4cv): + assert e4cv is not None + + # setup four samples with reflections, all of them different + main = e4cv.calc.sample # the default sample + assert len(main.reflections) == 0 + m_100 = main.add_reflection(1, 0, 0, (-45, 0, 0, 0)) + m_010 = main.add_reflection(0, 1, 0, (45, 0, 0, 0)) + main.compute_UB(m_100, m_010) + + kryptonite = sample_kryptonite(e4cv) + assert len(kryptonite.reflections) == 0 + k_200 = kryptonite.add_reflection(2, 0, 0, (30, 0, 10, 60)) + k_020 = kryptonite.add_reflection(0, 2, 0, (30, 90, 10, 60)) + kryptonite.compute_UB(k_200, k_020) + + vibranium = sample_vibranium(e4cv) + assert len(vibranium.reflections) == 0 + v_003 = vibranium.add_reflection(0, 0, 3, (20.33, 4.33, 8.33, 40.33)) + v_033 = vibranium.add_reflection(0, 3, 3, (20.33, -94.33, 8.33, 40.33)) + vibranium.compute_UB(v_003, v_033) + + silicon = sample_silicon(e4cv) + assert len(silicon.reflections) == 0 + s_440 = silicon.add_reflection(4, 4, 0, (34, 44, 54, 64)) + s_444 = silicon.add_reflection(4, 4, 4, (34, 134, 54, 64)) + silicon.compute_UB(s_440, s_444) + + n_saved_reflections = 2 + assert len(main.reflections) == n_saved_reflections + assert len(kryptonite.reflections) == n_saved_reflections + assert len(silicon.reflections) == n_saved_reflections + assert len(vibranium.reflections) == n_saved_reflections + # same test, using diffractometer sample dictionary now. + for sample in e4cv.calc._samples.values(): + assert len(sample.reflections) == n_saved_reflections, f"{sample.name=}" + assert len(e4cv.calc._samples) == 4 + + agent = DiffractometerConfiguration(e4cv) + assert agent is not None + config = agent.export() + assert isinstance(config, str) + + # Restore the configuration without clearing the diffractometer first. + # This should not change the number of samples or the number of + # reflections for any of the samples. + agent.restore(config, clear=False) + + assert len(e4cv.calc._samples) == 4 + for sample in e4cv.calc._samples.values(): + assert len(sample.reflections) == 2 * n_saved_reflections diff --git a/hkl/tests/test_tardis.py b/hkl/tests/test_tardis.py index 6cd826e4..85cdf625 100644 --- a/hkl/tests/test_tardis.py +++ b/hkl/tests/test_tardis.py @@ -9,7 +9,7 @@ @pytest.fixture(scope="function") def kcf_sample(tardis): - from . import new_sample + from .tools import new_sample # note: orientation matrix (below) was pre-computed with this wavelength # wavelength units must match lattice unit cell length units @@ -90,7 +90,7 @@ def test_params(tardis): """ Make sure the parameters are set correctly """ - from . import TARDIS_TEST_MODE + from .tools import TARDIS_TEST_MODE calc = tardis.calc assert calc.pseudo_axis_names == "h k l".split() @@ -319,7 +319,7 @@ def test_sample1(sample1, tardis): def test_sample1_calc_only(): """Comparisons start with the Tardis' calc support (no Diffractometer object).""" - from . import TARDIS_TEST_MODE + from .tools import TARDIS_TEST_MODE tardis_calc = hkl_calc.CalcE6C() diff --git a/hkl/tests/tools.py b/hkl/tests/tools.py new file mode 100644 index 00000000..7e0f6e24 --- /dev/null +++ b/hkl/tests/tools.py @@ -0,0 +1,38 @@ +""" +Common code, setups, constants, ... for these tests. + +Avoids direct imports of __init__.py. +""" + +import logging + +import numpy + +from ..util import new_lattice + +logger = logging.getLogger("ophyd_session_test") + +TARDIS_TEST_MODE = "lifting_detector_mu" +TWO_PI = 2 * numpy.pi + + +def new_sample(diffractometer, name, lattice): + return diffractometer.calc.new_sample(name, lattice=lattice) + + +def sample_kryptonite(diffractometer): + triclinic = new_lattice(4, 5, 6, 75, 85, 95) + return new_sample(diffractometer, "kryptonite", lattice=triclinic) + + +def sample_silicon(diffractometer): + from .. import SI_LATTICE_PARAMETER + + cubic = new_lattice(SI_LATTICE_PARAMETER) + return new_sample(diffractometer, "silicon", lattice=cubic) + + +def sample_vibranium(diffractometer): + a0 = TWO_PI + cubic = new_lattice(a0) + return new_sample(diffractometer, "vibranium", lattice=cubic) diff --git a/hkl/user.py b/hkl/user.py index 057b82fe..75c14a06 100644 --- a/hkl/user.py +++ b/hkl/user.py @@ -11,6 +11,7 @@ ~cahkl ~cahkl_table ~calc_UB + ~current_diffractometer ~list_samples ~new_sample ~or_swap @@ -29,6 +30,7 @@ cahkl_table calc_UB change_sample + current_diffractometer list_samples new_sample or_swap @@ -124,6 +126,11 @@ def change_sample(sample): show_sample(sample) +def current_diffractometer(): + """Return the currently-selected diffractometer (or ``None``).""" + return _geom_ + + def list_samples(verbose=True): """List all defined crystal samples.""" _check_geom_selected() @@ -256,9 +263,10 @@ def show_sample(sample_name=None, verbose=True): def show_selected_diffractometer(instrument=None): """Print the name of the selected diffractometer.""" - if _geom_ is None: + geom = current_diffractometer() + if geom is None: print("No diffractometer selected.") - print(_geom_.name) + print(geom.name) def update_sample(a, b, c, alpha, beta, gamma): @@ -278,10 +286,10 @@ def update_sample(a, b, c, alpha, beta, gamma): def pa(): """Report (all) the diffractometer settings.""" _check_geom_selected() - _geom_.pa() + current_diffractometer().pa() def wh(): """Report (brief) where is the diffractometer.""" _check_geom_selected() - _geom_.wh() + current_diffractometer().wh() diff --git a/pyproject.toml b/pyproject.toml index c4b725c9..697f24f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ docs = [ ] tests = [ "bluesky", - "databroker ==1.2", + "databroker", "packaging", ] all = ["hklpy[docs,tests]"]