Skip to content

Commit 9177695

Browse files
authored
Merge pull request #17 from DiamondLightSource/add-code-from-artemis
(#2) Move devices in from Artemis
2 parents 7923622 + 38f1780 commit 9177695

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+4985
-21
lines changed

.github/workflows/code.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
run: pipdeptree
6767

6868
- name: Run tests
69-
run: pytest
69+
run: pytest --random-order -m "not s03"
7070

7171
- name: Upload coverage to Codecov
7272
uses: codecov/codecov-action@v3
@@ -108,7 +108,7 @@ jobs:
108108

109109
- name: Test module --version works using the installed wheel
110110
# If more than one module in src/ replace with module name to test
111-
run: python -m $(ls src | head -1) --version
111+
run: python -m dodal --version
112112

113113
container:
114114
needs: [lint, dist, test]

.pre-commit-config.yaml

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,41 @@ repos:
66
- id: check-yaml
77
- id: check-merge-conflict
88

9-
- repo: local
9+
# Automatic source code formatting
10+
- repo: https://github.com/psf/black
11+
rev: 22.3.0
1012
hooks:
1113
- id: black
12-
name: Run black
13-
stages: [commit]
14-
language: system
15-
entry: black --check --diff
16-
types: [python]
14+
args: [--safe, --quiet]
15+
files: \.pyi?
16+
types: [file]
1717

18+
# Sort imports
19+
- repo: https://github.com/pycqa/isort
20+
rev: 5.12.0
21+
hooks:
22+
- id: isort
23+
name: isort (python)
24+
args:
25+
[
26+
"--profile=black",
27+
'--add_imports="from __future__ import annotations',
28+
]
29+
30+
# Linting
31+
- repo: https://github.com/pycqa/flake8
32+
rev: 4.0.1
33+
hooks:
1834
- id: flake8
19-
name: Run flake8
20-
stages: [commit]
21-
language: system
22-
entry: flake8
23-
types: [python]
35+
additional_dependencies:
36+
["flake8-comprehensions==3.8.0", "Flake8-pyproject"]
37+
args: ["--max-line-length=88", "--ignore=E203,F811,F722,E501,W503,C408"]
38+
39+
# Type checking
40+
- repo: https://github.com/pre-commit/mirrors-mypy
41+
rev: v0.931
42+
hooks:
43+
- id: mypy
44+
files: 'src/.*\.py$'
45+
additional_dependencies: [types-requests]
46+
args: ["--ignore-missing-imports", "--no-strict-optional"]

.vscode/settings.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,17 @@
1111
"editor.formatOnSave": true,
1212
"editor.codeActionsOnSave": {
1313
"source.organizeImports": true
14-
}
14+
},
15+
"python.sortImports.args": [
16+
"--profile",
17+
"black"
18+
],
19+
"[python]": {
20+
"editor.rulers": [
21+
88
22+
]
23+
},
24+
"python.analysis.extraPaths": [
25+
"./src"
26+
],
1527
}

pyproject.toml

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ classifiers = [
1313
"Programming Language :: Python :: 3.11",
1414
]
1515
description = "Ophyd devices and other utils that could be used across DLS beamlines"
16-
dependencies = ["ophyd", "bluesky", "pyepics", "pydantic"]
16+
dependencies = [
17+
"ophyd",
18+
"bluesky",
19+
"pyepics",
20+
"dataclasses-json",
21+
"pillow",
22+
"requests",
23+
"pydantic",
24+
]
1725
dynamic = ["version"]
1826
license.file = "LICENSE"
1927
readme = "README.rst"
@@ -25,14 +33,17 @@ dev = [
2533
"mypy",
2634
"flake8-isort",
2735
"Flake8-pyproject",
36+
"mockito",
2837
"pipdeptree",
2938
"pre-commit",
3039
"pydata-sphinx-theme>=0.12",
3140
"pytest-cov",
41+
"pytest-random-order",
3242
"sphinx-autobuild",
3343
"sphinx-copybutton",
3444
"sphinx-design",
3545
"tox-direct",
46+
"types-requests",
3647
"types-mock",
3748
]
3849

@@ -43,6 +54,11 @@ GitHub = "https://github.com/DiamondLightSource/python-dodal"
4354
email = "dominic.oram@diamond.ac.uk"
4455
name = "Dominic Oram"
4556

57+
[tool.setuptools.packages.find]
58+
where = ["src"]
59+
60+
[tool.setuptools.package-data]
61+
dodal = ["*.txt"]
4662

4763
[tool.setuptools_scm]
4864
write_to = "src/dodal/_version.py"
@@ -59,19 +75,22 @@ extend-ignore = [
5975
"E203", # See https://github.com/PyCQA/pycodestyle/issues/373
6076
"F811", # support typing.overload decorator
6177
"F722", # allow Annotated[typ, some_func("some string")]
78+
"E501",
79+
"W503",
6280
]
6381
max-line-length = 88 # Respect black's line length (default 88),
6482
exclude = [".tox", "venv"]
6583

6684

6785
[tool.pytest.ini_options]
6886
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
87+
markers = [
88+
"s03: marks tests as requiring the s03 simulator running (deselect with '-m \"not s03\"')",
89+
]
6990
addopts = """
7091
--tb=native -vv --doctest-modules --doctest-glob="*.rst"
7192
--cov=dodal --cov-report term --cov-report xml:cov.xml
7293
"""
73-
# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings
74-
filterwarnings = "error"
7594
# Doctest python code in docs, python code in src docstrings, test functions in tests
7695
testpaths = "docs src tests"
7796

@@ -93,15 +112,15 @@ skipsdist=True
93112
# Don't create a virtualenv for the command, requires tox-direct plugin
94113
direct = True
95114
passenv = *
96-
allowlist_externals =
97-
pytest
115+
allowlist_externals =
116+
pytest
98117
pre-commit
99118
mypy
100119
sphinx-build
101120
sphinx-autobuild
102121
commands =
103-
pytest: pytest {posargs}
104-
mypy: mypy src tests {posargs}
122+
pytest: pytest -m 'not s03' {posargs}
123+
mypy: mypy src tests --ignore-missing-imports --no-strict-optional {posargs}
105124
pre-commit: pre-commit run --all-files {posargs}
106125
docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html
107126
"""

src/__init__.py

Whitespace-only changes.

src/dodal/devices/CTAB.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from ophyd import Component as Cpt
2+
from ophyd import Device, EpicsMotor, EpicsSignalRO
3+
4+
5+
class CTAB(Device):
6+
"""Basic collimantion table (CTAB) device for motion plus the motion disable signal
7+
when laser curtain triggered and hutch not locked.
8+
9+
CTAB has 3 physical vertical motors, the jacks. 1 upstream and 2 downstream.
10+
The two downstream jacks are labelled as outboard (away from the ring) and
11+
inboard (towards the ring).
12+
Together these 3 jacks provide compound motion for vertical motion and pitch/roll.
13+
There are 2 physical horizontal motors 1 upstream, 1 downstream. These provide yaw.
14+
15+
CTAB motion is disabled by an object being within the laser curtain area and can be
16+
overriden by use of the dead man's handle device or locking the hutch. The effect of
17+
these disabling systems is to cut power to the motors - signal for this is crate_power
18+
"""
19+
20+
inboard_y: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:INBOARDY")
21+
outboard_y: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:OUTBOARDY")
22+
upstream_y: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:UPSTREAMY")
23+
combined_downstream_y: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:DOWNSTREAMY")
24+
combined_all_y: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:Y")
25+
26+
downstream_x: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:DOWNSTREAMX")
27+
upstream_x: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:UPSTREAMX")
28+
combined_all_x: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:X")
29+
30+
pitch: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:PITCH")
31+
roll: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:ROLL")
32+
yaw: EpicsMotor = Cpt(EpicsMotor, "-MO-TABLE-01:YAW")
33+
34+
crate_power: EpicsSignalRO = Cpt(
35+
EpicsSignalRO, "-MO-PMAC-02:CRATE2_HEALTHY"
36+
) # returns 0 if no power

src/dodal/devices/DCM.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from ophyd import Component as Cpt
2+
from ophyd import Device, EpicsMotor, EpicsSignalRO
3+
4+
5+
class DCM(Device):
6+
"""
7+
A double crystal monochromator (DCM), used to select the energy of the beam.
8+
9+
perp describes the gap between the 2 DCM crystals which has to change as you alter
10+
the angle to select the requested energy.
11+
12+
offset ensures that the beam exits the DCM at the same point, regardless of energy.
13+
"""
14+
15+
bragg: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:BRAGG")
16+
roll: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:ROLL")
17+
offset: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:OFFSET")
18+
perp: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:PERP")
19+
energy: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:ENERGY")
20+
pitch: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:PITCH")
21+
wavelength: EpicsMotor = Cpt(EpicsMotor, "-MO-DCM-01:WAVELENGTH")
22+
23+
# temperatures
24+
xtal1_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP1")
25+
xtal2_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP2")
26+
xtal1_heater_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP3")
27+
xtal2_heater_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP4")
28+
backplate_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP5")
29+
perp_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP6")
30+
perp_sub_assembly_temp: EpicsSignalRO = Cpt(EpicsSignalRO, "-MO-DCM-01:TEMP7")

src/dodal/devices/aperture.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from ophyd import Component, Device, EpicsMotor
2+
3+
4+
class Aperture(Device):
5+
x: EpicsMotor = Component(EpicsMotor, "X")
6+
y: EpicsMotor = Component(EpicsMotor, "Y")
7+
z: EpicsMotor = Component(EpicsMotor, "Z")
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from dataclasses import dataclass
2+
from typing import Optional, Tuple
3+
4+
from ophyd import Component as Cpt
5+
from ophyd.status import AndStatus
6+
7+
from dodal.devices.aperture import Aperture
8+
from dodal.devices.logging_ophyd_device import InfoLoggingDevice
9+
from dodal.devices.scatterguard import Scatterguard
10+
11+
12+
class InvalidApertureMove(Exception):
13+
pass
14+
15+
16+
@dataclass
17+
class AperturePositions:
18+
"""Holds tuples (miniap_x, miniap_y, miniap_z, scatterguard_x, scatterguard_y)
19+
representing the motor positions needed to select a particular aperture size.
20+
"""
21+
22+
LARGE: Tuple[float, float, float, float, float]
23+
MEDIUM: Tuple[float, float, float, float, float]
24+
SMALL: Tuple[float, float, float, float, float]
25+
ROBOT_LOAD: Tuple[float, float, float, float, float]
26+
27+
@classmethod
28+
def from_gda_beamline_params(cls, params):
29+
return cls(
30+
LARGE=(
31+
params["miniap_x_LARGE_APERTURE"],
32+
params["miniap_y_LARGE_APERTURE"],
33+
params["miniap_z_LARGE_APERTURE"],
34+
params["sg_x_LARGE_APERTURE"],
35+
params["sg_y_LARGE_APERTURE"],
36+
),
37+
MEDIUM=(
38+
params["miniap_x_MEDIUM_APERTURE"],
39+
params["miniap_y_MEDIUM_APERTURE"],
40+
params["miniap_z_MEDIUM_APERTURE"],
41+
params["sg_x_MEDIUM_APERTURE"],
42+
params["sg_y_MEDIUM_APERTURE"],
43+
),
44+
SMALL=(
45+
params["miniap_x_SMALL_APERTURE"],
46+
params["miniap_y_SMALL_APERTURE"],
47+
params["miniap_z_SMALL_APERTURE"],
48+
params["sg_x_SMALL_APERTURE"],
49+
params["sg_y_SMALL_APERTURE"],
50+
),
51+
ROBOT_LOAD=(
52+
params["miniap_x_ROBOT_LOAD"],
53+
params["miniap_y_ROBOT_LOAD"],
54+
params["miniap_z_ROBOT_LOAD"],
55+
params["sg_x_ROBOT_LOAD"],
56+
params["sg_y_ROBOT_LOAD"],
57+
),
58+
)
59+
60+
def position_valid(self, pos: Tuple[float, float, float, float, float]):
61+
"""
62+
Check if argument 'pos' is a valid position in this AperturePositions object.
63+
"""
64+
if pos not in [self.LARGE, self.MEDIUM, self.SMALL, self.ROBOT_LOAD]:
65+
return False
66+
return True
67+
68+
69+
class ApertureScatterguard(InfoLoggingDevice):
70+
aperture: Aperture = Cpt(Aperture, "-MO-MAPT-01:")
71+
scatterguard: Scatterguard = Cpt(Scatterguard, "-MO-SCAT-01:")
72+
aperture_positions: Optional[AperturePositions] = None
73+
74+
def load_aperture_positions(self, positions: AperturePositions):
75+
self.aperture_positions = positions
76+
77+
def set(self, pos: Tuple[float, float, float, float, float]) -> AndStatus:
78+
try:
79+
assert isinstance(self.aperture_positions, AperturePositions)
80+
assert self.aperture_positions.position_valid(pos)
81+
except AssertionError as e:
82+
raise InvalidApertureMove(repr(e))
83+
return self._safe_move_within_datacollection_range(*pos)
84+
85+
def _safe_move_within_datacollection_range(
86+
self,
87+
aperture_x: float,
88+
aperture_y: float,
89+
aperture_z: float,
90+
scatterguard_x: float,
91+
scatterguard_y: float,
92+
) -> AndStatus:
93+
"""
94+
Move the aperture and scatterguard combo safely to a new position.
95+
See https://github.com/DiamondLightSource/python-artemis/wiki/Aperture-Scatterguard-Collisions
96+
for why this is required.
97+
"""
98+
# EpicsMotor does not have deadband/MRES field, so the way to check if we are
99+
# in a datacollection position is to see if we are "ready" (DMOV) and the target
100+
# position is correct
101+
ap_z_in_position = self.aperture.z.motor_done_move.get()
102+
if not ap_z_in_position:
103+
return
104+
current_ap_z = self.aperture.z.user_setpoint.get()
105+
if current_ap_z != aperture_z:
106+
raise InvalidApertureMove(
107+
"ApertureScatterguard safe move is not yet defined for positions "
108+
"outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD."
109+
)
110+
111+
current_ap_y = self.aperture.y.user_readback.get()
112+
if aperture_y > current_ap_y:
113+
sg_status: AndStatus = self.scatterguard.x.set(
114+
scatterguard_x
115+
) & self.scatterguard.y.set(scatterguard_y)
116+
sg_status.wait()
117+
final_status = (
118+
sg_status
119+
& self.aperture.x.set(aperture_x)
120+
& self.aperture.y.set(aperture_y)
121+
& self.aperture.z.set(aperture_z)
122+
)
123+
return final_status
124+
125+
else:
126+
ap_status: AndStatus = (
127+
self.aperture.x.set(aperture_x)
128+
& self.aperture.y.set(aperture_y)
129+
& self.aperture.z.set(aperture_z)
130+
)
131+
ap_status.wait()
132+
final_status = (
133+
ap_status
134+
& self.scatterguard.x.set(scatterguard_x)
135+
& self.scatterguard.y.set(scatterguard_y)
136+
)
137+
return final_status

0 commit comments

Comments
 (0)