apsbits 1.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsbits/__init__.py +18 -0
- apsbits/_version.py +21 -0
- apsbits/core/__init__.py +11 -0
- apsbits/core/best_effort_init.py +38 -0
- apsbits/core/catalog_init.py +29 -0
- apsbits/core/run_engine_init.py +71 -0
- apsbits/demo_instrument/README.md +1 -0
- apsbits/demo_instrument/__init__.py +3 -0
- apsbits/demo_instrument/callbacks/__init__.py +1 -0
- apsbits/demo_instrument/callbacks/nexus_data_file_writer.py +60 -0
- apsbits/demo_instrument/callbacks/spec_data_file_writer.py +93 -0
- apsbits/demo_instrument/configs/__init__.py +1 -0
- apsbits/demo_instrument/configs/devices.yml +52 -0
- apsbits/demo_instrument/configs/devices_aps_only.yml +6 -0
- apsbits/demo_instrument/configs/iconfig.yml +81 -0
- apsbits/demo_instrument/configs/logging.yml +41 -0
- apsbits/demo_instrument/devices/__init__.py +1 -0
- apsbits/demo_instrument/plans/__init__.py +8 -0
- apsbits/demo_instrument/plans/dm_plans.py +111 -0
- apsbits/demo_instrument/plans/sim_plans.py +69 -0
- apsbits/demo_instrument/startup.py +63 -0
- apsbits/tests/__init__.py +1 -0
- apsbits/tests/conftest.py +32 -0
- apsbits/tests/test_device_factories.py +44 -0
- apsbits/tests/test_general.py +83 -0
- apsbits/tests/test_stored_dict.py +139 -0
- apsbits/utils/__init__.py +1 -0
- apsbits/utils/aps_functions.py +67 -0
- apsbits/utils/config_loaders.py +61 -0
- apsbits/utils/context_aware.py +187 -0
- apsbits/utils/controls_setup.py +113 -0
- apsbits/utils/create_new_instrument.py +144 -0
- apsbits/utils/helper_functions.py +113 -0
- apsbits/utils/logging_setup.py +211 -0
- apsbits/utils/make_devices_yaml.py +127 -0
- apsbits/utils/metadata.py +101 -0
- apsbits/utils/sim_creator.py +199 -0
- apsbits/utils/stored_dict.py +192 -0
- apsbits-1.0.0rc2.dist-info/LICENSE +48 -0
- apsbits-1.0.0rc2.dist-info/METADATA +349 -0
- apsbits-1.0.0rc2.dist-info/RECORD +44 -0
- apsbits-1.0.0rc2.dist-info/WHEEL +5 -0
- apsbits-1.0.0rc2.dist-info/entry_points.txt +2 -0
- apsbits-1.0.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configure logging for this session.
|
|
3
|
+
|
|
4
|
+
.. rubric:: Public
|
|
5
|
+
.. autosummary::
|
|
6
|
+
~configure_logging
|
|
7
|
+
|
|
8
|
+
.. rubric:: Internal
|
|
9
|
+
.. autosummary::
|
|
10
|
+
~_setup_console_logger
|
|
11
|
+
~_setup_file_logger
|
|
12
|
+
~_setup_ipython_logger
|
|
13
|
+
~_setup_module_logging
|
|
14
|
+
|
|
15
|
+
.. seealso:: https://blueskyproject.io/bluesky/main/debugging.html
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import logging.handlers
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
|
|
23
|
+
BYTE = 1
|
|
24
|
+
kB = 1024 * BYTE
|
|
25
|
+
MB = 1024 * kB
|
|
26
|
+
|
|
27
|
+
BRIEF_DATE = "%a-%H:%M:%S"
|
|
28
|
+
BRIEF_FORMAT = "%(levelname)-.1s %(asctime)s.%(msecs)03d: %(message)s"
|
|
29
|
+
DEFAULT_CONFIG_FILE = (
|
|
30
|
+
pathlib.Path(__file__).parent.parent / "demo_instrument" / "configs" / "logging.yml"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Add your custom logging level at the top-level, before configure_logging()
|
|
35
|
+
def addLoggingLevel(levelName, levelNum, methodName=None):
|
|
36
|
+
"""
|
|
37
|
+
Comprehensively adds a new logging level to the `logging` module and the
|
|
38
|
+
currently configured logging class.
|
|
39
|
+
|
|
40
|
+
`levelName` becomes an attribute of the `logging` module with the value
|
|
41
|
+
`levelNum`. `methodName` becomes a convenience method for both `logging`
|
|
42
|
+
itself and the class returned by `logging.getLoggerClass()` (usually just
|
|
43
|
+
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
|
|
44
|
+
used.
|
|
45
|
+
|
|
46
|
+
To avoid accidental clobberings of existing attributes, this method will
|
|
47
|
+
raise an `AttributeError` if the level name is already an attribute of the
|
|
48
|
+
`logging` module or if the method name is already present
|
|
49
|
+
|
|
50
|
+
Example
|
|
51
|
+
-------
|
|
52
|
+
>>> addLoggingLevel('TRACE', logging.INFO - 5)
|
|
53
|
+
>>> logging.getLogger(__name__).setLevel("TEST")
|
|
54
|
+
>>> logging.getLogger(__name__).test('that worked')
|
|
55
|
+
>>> logging.test('so did this')
|
|
56
|
+
>>> logging.TEST
|
|
57
|
+
5
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
if not methodName:
|
|
61
|
+
methodName = levelName.lower()
|
|
62
|
+
|
|
63
|
+
if hasattr(logging, levelName):
|
|
64
|
+
raise AttributeError("{} already defined in logging module".format(levelName))
|
|
65
|
+
if hasattr(logging, methodName):
|
|
66
|
+
raise AttributeError("{} already defined in logging module".format(methodName))
|
|
67
|
+
if hasattr(logging.getLoggerClass(), methodName):
|
|
68
|
+
raise AttributeError("{} already defined in logger class".format(methodName))
|
|
69
|
+
|
|
70
|
+
# This method was inspired by the answers to Stack Overflow post
|
|
71
|
+
# http://stackoverflow.com/q/2183233/2988730, especially
|
|
72
|
+
# http://stackoverflow.com/a/13638084/2988730
|
|
73
|
+
def logForLevel(self, message, *args, **kwargs):
|
|
74
|
+
if self.isEnabledFor(levelNum):
|
|
75
|
+
self._log(levelNum, message, args, **kwargs)
|
|
76
|
+
|
|
77
|
+
def logToRoot(message, *args, **kwargs):
|
|
78
|
+
logging.log(levelNum, message, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
logging.addLevelName(levelNum, levelName)
|
|
81
|
+
setattr(logging, levelName, levelNum)
|
|
82
|
+
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
|
83
|
+
setattr(logging, methodName, logToRoot)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
addLoggingLevel("BSDEV", logging.INFO - 5)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def configure_logging():
|
|
90
|
+
"""Configure logging as described in file."""
|
|
91
|
+
from .config_loaders import load_config_yaml
|
|
92
|
+
|
|
93
|
+
# (Re)configure the root logger.
|
|
94
|
+
logger = logging.getLogger(__name__).root
|
|
95
|
+
logger.debug("logger=%r", logger)
|
|
96
|
+
|
|
97
|
+
config_file = os.environ.get("BLUESKY_INSTRUMENT_CONFIG_FILE")
|
|
98
|
+
if config_file is None:
|
|
99
|
+
config_file = DEFAULT_CONFIG_FILE
|
|
100
|
+
else:
|
|
101
|
+
config_file = pathlib.Path(config_file)
|
|
102
|
+
|
|
103
|
+
logging_configuration = load_config_yaml(config_file)
|
|
104
|
+
for part, cfg in logging_configuration.items():
|
|
105
|
+
logging.debug("%r - %s", part, cfg)
|
|
106
|
+
|
|
107
|
+
if part == "console_logs":
|
|
108
|
+
_setup_console_logger(logger, cfg)
|
|
109
|
+
|
|
110
|
+
elif part == "file_logs":
|
|
111
|
+
_setup_file_logger(logger, cfg)
|
|
112
|
+
|
|
113
|
+
elif part == "ipython_logs":
|
|
114
|
+
_setup_ipython_logger(logger, cfg)
|
|
115
|
+
|
|
116
|
+
elif part == "modules":
|
|
117
|
+
_setup_module_logging(cfg)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _setup_console_logger(logger, cfg):
|
|
121
|
+
"""
|
|
122
|
+
Reconfigure the root logger as configured by the user.
|
|
123
|
+
|
|
124
|
+
We can't apply user configurations in ``configure_logging()`` above
|
|
125
|
+
because the code to read the config file triggers initialization of
|
|
126
|
+
the logging system.
|
|
127
|
+
|
|
128
|
+
.. seealso:: https://docs.python.org/3/library/logging.html#logging.basicConfig
|
|
129
|
+
"""
|
|
130
|
+
logging.basicConfig(
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
level=cfg["root_level"].upper(),
|
|
133
|
+
format=cfg["log_format"],
|
|
134
|
+
datefmt=cfg["date_format"],
|
|
135
|
+
force=True, # replace any previous setup
|
|
136
|
+
)
|
|
137
|
+
h = logger.handlers[0]
|
|
138
|
+
h.setLevel(cfg["level"].upper())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _setup_file_logger(logger, cfg):
|
|
142
|
+
"""Record log messages in file(s)."""
|
|
143
|
+
formatter = logging.Formatter(
|
|
144
|
+
fmt=cfg["log_format"],
|
|
145
|
+
datefmt=cfg["date_format"],
|
|
146
|
+
style="%",
|
|
147
|
+
validate=True,
|
|
148
|
+
)
|
|
149
|
+
formatter.default_msec_format = "%s.%03d"
|
|
150
|
+
|
|
151
|
+
backupCount = cfg.get("backupCount", 9)
|
|
152
|
+
maxBytes = cfg.get("maxBytes", 1 * MB)
|
|
153
|
+
log_path = pathlib.Path(cfg.get("log_directory", ".logs")).resolve()
|
|
154
|
+
if not log_path.exists():
|
|
155
|
+
os.makedirs(str(log_path))
|
|
156
|
+
|
|
157
|
+
file_name = log_path / cfg.get("log_filename_base", "logging.log")
|
|
158
|
+
if maxBytes > 0 or backupCount > 0:
|
|
159
|
+
backupCount = max(backupCount, 1) # impose minimum standards
|
|
160
|
+
maxBytes = max(maxBytes, 100 * kB)
|
|
161
|
+
handler = logging.handlers.RotatingFileHandler(
|
|
162
|
+
file_name,
|
|
163
|
+
maxBytes=maxBytes,
|
|
164
|
+
backupCount=backupCount,
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
handler = logging.FileHandler(file_name)
|
|
168
|
+
handler.setFormatter(formatter)
|
|
169
|
+
if cfg.get("rotate_on_startup", False):
|
|
170
|
+
handler.doRollover()
|
|
171
|
+
logger.addHandler(handler)
|
|
172
|
+
logger.info("%s Bluesky Startup Initialized", "*" * 40)
|
|
173
|
+
logger.bsdev(__file__)
|
|
174
|
+
logger.bsdev("Log file: %s", file_name)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _setup_ipython_logger(logger, cfg):
|
|
178
|
+
"""
|
|
179
|
+
Internal: Log IPython console session In and Out to a file.
|
|
180
|
+
|
|
181
|
+
See ``logrotate?`` int he IPython console for more information.
|
|
182
|
+
"""
|
|
183
|
+
log_path = pathlib.Path(cfg.get("log_directory", ".logs")).resolve()
|
|
184
|
+
try:
|
|
185
|
+
from IPython import get_ipython
|
|
186
|
+
|
|
187
|
+
# start logging console to file
|
|
188
|
+
# https://ipython.org/ipython-doc/3/interactive/magics.html#magic-logstart
|
|
189
|
+
_ipython = get_ipython()
|
|
190
|
+
log_file = log_path / cfg.get("log_filename_base", "ipython_log.py")
|
|
191
|
+
log_mode = cfg.get("log_mode", "rotate")
|
|
192
|
+
options = cfg.get("options", "-o -t")
|
|
193
|
+
if _ipython is not None:
|
|
194
|
+
print(
|
|
195
|
+
"\nBelow are the IPython logging settings for your session."
|
|
196
|
+
"\nThese settings have no impact on your experiment.\n"
|
|
197
|
+
)
|
|
198
|
+
_ipython.run_line_magic("logstart", f"{options} {log_file} {log_mode}")
|
|
199
|
+
if logger is not None:
|
|
200
|
+
logger.bsdev("Console logging: %s", log_file)
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
if logger is None:
|
|
203
|
+
print(f"Could not setup console logging: {exc}")
|
|
204
|
+
else:
|
|
205
|
+
logger.exception("Could not setup console logging.")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _setup_module_logging(cfg):
|
|
209
|
+
"""Internal: Set logging level for each named module."""
|
|
210
|
+
for module, level in cfg.items():
|
|
211
|
+
logging.getLogger(module).setLevel(level.upper())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Make devices from YAML files
|
|
3
|
+
=============================
|
|
4
|
+
|
|
5
|
+
Construct ophyd-style devices from simple specifications in YAML files.
|
|
6
|
+
|
|
7
|
+
.. autosummary::
|
|
8
|
+
:nosignatures:
|
|
9
|
+
|
|
10
|
+
~make_devices
|
|
11
|
+
~Instrument
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import pathlib
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import guarneri
|
|
20
|
+
from apstools.plans import run_blocking_function
|
|
21
|
+
from apstools.utils import dynamic_import
|
|
22
|
+
from bluesky import plan_stubs as bps
|
|
23
|
+
|
|
24
|
+
from apsbits.utils.aps_functions import host_on_aps_subnet
|
|
25
|
+
from apsbits.utils.config_loaders import load_config_yaml
|
|
26
|
+
from apsbits.utils.context_aware import iconfig
|
|
27
|
+
from apsbits.utils.context_aware import resolve_path
|
|
28
|
+
from apsbits.utils.controls_setup import oregistry # noqa: F401
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
logger.bsdev(__file__)
|
|
32
|
+
|
|
33
|
+
# Get the main module (same as before)
|
|
34
|
+
main_namespace = sys.modules["__main__"]
|
|
35
|
+
|
|
36
|
+
# Resolve device files directly using resolve_path
|
|
37
|
+
local_control_devices_file = resolve_path(iconfig["DEVICES_FILE"])
|
|
38
|
+
aps_control_devices_file = resolve_path(iconfig["APS_DEVICES_FILE"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_devices(*, pause: float = 1):
|
|
42
|
+
"""
|
|
43
|
+
(plan stub) Create the ophyd-style controls for this instrument.
|
|
44
|
+
|
|
45
|
+
Feel free to modify this plan to suit the needs of your instrument.
|
|
46
|
+
|
|
47
|
+
EXAMPLE::
|
|
48
|
+
|
|
49
|
+
RE(make_devices())
|
|
50
|
+
|
|
51
|
+
PARAMETERS
|
|
52
|
+
|
|
53
|
+
pause : float
|
|
54
|
+
Wait 'pause' seconds (default: 1) for slow objects to connect.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
logger.debug("(Re)Loading local control objects.")
|
|
58
|
+
yield from run_blocking_function(_loader, local_control_devices_file, main=True)
|
|
59
|
+
|
|
60
|
+
if host_on_aps_subnet():
|
|
61
|
+
yield from run_blocking_function(_loader, aps_control_devices_file, main=True)
|
|
62
|
+
|
|
63
|
+
if pause > 0:
|
|
64
|
+
logger.debug(
|
|
65
|
+
"Waiting %s seconds for slow objects to connect.",
|
|
66
|
+
pause,
|
|
67
|
+
)
|
|
68
|
+
yield from bps.sleep(pause)
|
|
69
|
+
|
|
70
|
+
# Configure any of the controls here, or in plan stubs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _loader(yaml_device_file, main=True):
|
|
74
|
+
"""
|
|
75
|
+
Load our ophyd-style controls as described in a YAML file.
|
|
76
|
+
|
|
77
|
+
PARAMETERS
|
|
78
|
+
|
|
79
|
+
yaml_device_file : str or pathlib.Path
|
|
80
|
+
YAML file describing ophyd-style controls to be created.
|
|
81
|
+
main : bool
|
|
82
|
+
If ``True`` add these devices to the ``__main__`` namespace.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
logger.debug("Devices file %r.", str(yaml_device_file))
|
|
86
|
+
t0 = time.time()
|
|
87
|
+
_instr.load(yaml_device_file)
|
|
88
|
+
logger.debug("Devices loaded in %.3f s.", time.time() - t0)
|
|
89
|
+
|
|
90
|
+
if main:
|
|
91
|
+
for label in oregistry.device_names:
|
|
92
|
+
# add to __main__ namespace
|
|
93
|
+
setattr(main_namespace, label, oregistry[label])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Instrument(guarneri.Instrument):
|
|
97
|
+
"""Custom YAML loader for guarneri."""
|
|
98
|
+
|
|
99
|
+
def parse_yaml_file(self, config_file: pathlib.Path | str) -> list[dict]:
|
|
100
|
+
"""Read device configurations from YAML format file."""
|
|
101
|
+
if isinstance(config_file, str):
|
|
102
|
+
config_file = pathlib.Path(config_file)
|
|
103
|
+
|
|
104
|
+
def parser(creator, specs):
|
|
105
|
+
if creator not in self.device_classes:
|
|
106
|
+
self.device_classes[creator] = dynamic_import(creator)
|
|
107
|
+
entries = [
|
|
108
|
+
{
|
|
109
|
+
"device_class": creator,
|
|
110
|
+
"args": (), # ALL specs are kwargs!
|
|
111
|
+
"kwargs": table,
|
|
112
|
+
}
|
|
113
|
+
for table in specs
|
|
114
|
+
]
|
|
115
|
+
return entries
|
|
116
|
+
|
|
117
|
+
devices = [
|
|
118
|
+
device
|
|
119
|
+
# parse the file
|
|
120
|
+
for k, v in load_config_yaml(config_file).items()
|
|
121
|
+
# each support type (class, factory, function, ...)
|
|
122
|
+
for device in parser(k, v)
|
|
123
|
+
]
|
|
124
|
+
return devices
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_instr = Instrument({}, registry=oregistry) # singleton
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RunEngine Metadata
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
.. autosummary::
|
|
6
|
+
~MD_PATH
|
|
7
|
+
~get_md_path
|
|
8
|
+
~re_metadata
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import getpass
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import pathlib
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import apstools
|
|
19
|
+
import bluesky
|
|
20
|
+
import databroker
|
|
21
|
+
import epics
|
|
22
|
+
import h5py
|
|
23
|
+
import intake
|
|
24
|
+
import matplotlib
|
|
25
|
+
import numpy
|
|
26
|
+
import ophyd
|
|
27
|
+
import pyRestTable
|
|
28
|
+
import pysumreg
|
|
29
|
+
import spec2nexus
|
|
30
|
+
|
|
31
|
+
import apsbits
|
|
32
|
+
from apsbits.utils.config_loaders import iconfig
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
logger.bsdev(__file__)
|
|
36
|
+
|
|
37
|
+
re_config = iconfig.get("RUN_ENGINE", {})
|
|
38
|
+
|
|
39
|
+
DEFAULT_MD_PATH = pathlib.Path.home() / ".config" / "Bluesky_RunEngine_md"
|
|
40
|
+
HOSTNAME = socket.gethostname() or "localhost"
|
|
41
|
+
USERNAME = getpass.getuser() or "Bluesky user"
|
|
42
|
+
VERSIONS = dict(
|
|
43
|
+
apstools=apstools.__version__,
|
|
44
|
+
bluesky=bluesky.__version__,
|
|
45
|
+
databroker=databroker.__version__,
|
|
46
|
+
epics=epics.__version__,
|
|
47
|
+
h5py=h5py.__version__,
|
|
48
|
+
intake=intake.__version__,
|
|
49
|
+
matplotlib=matplotlib.__version__,
|
|
50
|
+
numpy=numpy.__version__,
|
|
51
|
+
ophyd=ophyd.__version__,
|
|
52
|
+
pyRestTable=pyRestTable.__version__,
|
|
53
|
+
python=sys.version.split(" ")[0],
|
|
54
|
+
pysumreg=pysumreg.__version__,
|
|
55
|
+
spec2nexus=spec2nexus.__version__,
|
|
56
|
+
apsbits=apsbits.__version__,
|
|
57
|
+
)
|
|
58
|
+
RE_CONFIG = iconfig.get("RUN_ENGINE", {})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_md_path():
|
|
62
|
+
"""
|
|
63
|
+
Get path for RE metadata.
|
|
64
|
+
|
|
65
|
+
============== ==============================================
|
|
66
|
+
support path
|
|
67
|
+
============== ==============================================
|
|
68
|
+
PersistentDict Directory where dictionary keys are stored in separate files.
|
|
69
|
+
StoredDict File where dictionary is stored as YAML.
|
|
70
|
+
============== ==============================================
|
|
71
|
+
|
|
72
|
+
In either case, the 'path' can be relative or absolute. Relative
|
|
73
|
+
paths are with respect to the present working directory when the
|
|
74
|
+
bluesky session is started.
|
|
75
|
+
"""
|
|
76
|
+
md_path_name = RE_CONFIG.get("MD_PATH", DEFAULT_MD_PATH)
|
|
77
|
+
path = pathlib.Path(md_path_name)
|
|
78
|
+
logger.info("RunEngine metadata saved in directory: %s", str(path))
|
|
79
|
+
return str(path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def re_metadata(cat=None):
|
|
83
|
+
"""Programmatic metadata for the RunEngine."""
|
|
84
|
+
md = {
|
|
85
|
+
"login_id": f"{USERNAME}@{HOSTNAME}",
|
|
86
|
+
"versions": VERSIONS,
|
|
87
|
+
"pid": os.getpid(),
|
|
88
|
+
"iconfig": iconfig,
|
|
89
|
+
}
|
|
90
|
+
if cat is not None:
|
|
91
|
+
md["databroker_catalog"] = cat.name
|
|
92
|
+
md.update(RE_CONFIG.get("DEFAULT_METADATA", {}))
|
|
93
|
+
|
|
94
|
+
conda_prefix = os.environ.get("CONDA_PREFIX")
|
|
95
|
+
if conda_prefix is not None:
|
|
96
|
+
md["conda_prefix"] = conda_prefix
|
|
97
|
+
return md
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
MD_PATH = get_md_path()
|
|
101
|
+
"""Storage path to save RE metadata between sessions."""
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device factories
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
Device factories are used to:
|
|
6
|
+
|
|
7
|
+
* *Create* several similar ophyd-style devices (such as ``ophyd.Device``
|
|
8
|
+
or ``ophyd.Signal``) that fit a pattern.
|
|
9
|
+
|
|
10
|
+
* *Import* a device which is pre-defined in a module, such as the
|
|
11
|
+
ophyd simulators in ``ophyd.sim``.
|
|
12
|
+
|
|
13
|
+
.. autosummary::
|
|
14
|
+
|
|
15
|
+
~factory_base
|
|
16
|
+
~motors
|
|
17
|
+
~predefined_device
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
from apstools.utils import dynamic_import
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
logger.bsdev(__file__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def predefined_device(*, name="", creator=""):
|
|
29
|
+
"""
|
|
30
|
+
Provide a predefined device such as from the 'ophyd.sim' module.
|
|
31
|
+
|
|
32
|
+
PARAMETERS
|
|
33
|
+
|
|
34
|
+
creator : str
|
|
35
|
+
Name of the predefined device to be used
|
|
36
|
+
name : str
|
|
37
|
+
Simulator will be assigned this name. (default: use existing name)
|
|
38
|
+
|
|
39
|
+
Example entry in `devices.yml` file:
|
|
40
|
+
|
|
41
|
+
.. code-block:: yaml
|
|
42
|
+
:linenos:
|
|
43
|
+
|
|
44
|
+
instrument.devices.factories.predefined_device:
|
|
45
|
+
- {creator: ophyd.sim.motor, name: sim_motor}
|
|
46
|
+
- {creator: ophyd.sim.noisy_det, name: sim_det}
|
|
47
|
+
"""
|
|
48
|
+
if creator == "":
|
|
49
|
+
raise ValueError("Must provide a value for 'creator'.")
|
|
50
|
+
device = dynamic_import(creator)
|
|
51
|
+
if name != "":
|
|
52
|
+
device.name = name
|
|
53
|
+
logger.debug(device)
|
|
54
|
+
yield device
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def factory_base(
|
|
58
|
+
*,
|
|
59
|
+
prefix=None,
|
|
60
|
+
names="object{}",
|
|
61
|
+
first=0,
|
|
62
|
+
last=0,
|
|
63
|
+
creator="ophyd.Signal",
|
|
64
|
+
**kwargs,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Make one or more objects using 'creator'.
|
|
68
|
+
|
|
69
|
+
PARAMETERS
|
|
70
|
+
|
|
71
|
+
prefix : str
|
|
72
|
+
Prefix *pattern* for the EPICS PVs (default: ``None``).
|
|
73
|
+
|
|
74
|
+
names : str
|
|
75
|
+
Name *pattern* for the objects. The default pattern is ``"object{}"`` which
|
|
76
|
+
produces devices named ``object1, object2, ..., ```. If a formatting
|
|
77
|
+
specification (``{}``) is not provided, it is appended. Each object
|
|
78
|
+
will be named using this code: ``names.format(number)``, such as::
|
|
79
|
+
|
|
80
|
+
In [23]: "object{}".format(22)
|
|
81
|
+
Out[23]: 'object22'
|
|
82
|
+
|
|
83
|
+
first : int
|
|
84
|
+
The first object number in the continuous series from 'first' through
|
|
85
|
+
'last' (inclusive).
|
|
86
|
+
|
|
87
|
+
last : int
|
|
88
|
+
The first object number in the continuous series from 'first' through
|
|
89
|
+
'last' (inclusive).
|
|
90
|
+
|
|
91
|
+
creator : str
|
|
92
|
+
Name of the *creator* code that will be used to construct each device.
|
|
93
|
+
(default: ``"ophyd.Signal"``)
|
|
94
|
+
|
|
95
|
+
kwargs : dict
|
|
96
|
+
Dictionary of additional keyword arguments. This is included
|
|
97
|
+
when creating each object.
|
|
98
|
+
"""
|
|
99
|
+
if "{" not in names:
|
|
100
|
+
names += "{}"
|
|
101
|
+
if prefix is not None and "{" not in prefix:
|
|
102
|
+
prefix += "{}"
|
|
103
|
+
|
|
104
|
+
klass = dynamic_import(creator)
|
|
105
|
+
|
|
106
|
+
first, last = sorted([first, last])
|
|
107
|
+
for i in range(first, 1 + last):
|
|
108
|
+
keywords = {"name": names.format(i)}
|
|
109
|
+
if prefix is not None:
|
|
110
|
+
keywords["prefix"] = prefix.format(i)
|
|
111
|
+
keywords.update(kwargs)
|
|
112
|
+
device = klass(**keywords)
|
|
113
|
+
logger.debug(device)
|
|
114
|
+
yield device
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def motors(
|
|
118
|
+
*,
|
|
119
|
+
prefix=None,
|
|
120
|
+
names="m{}",
|
|
121
|
+
first=0,
|
|
122
|
+
last=0,
|
|
123
|
+
**kwargs,
|
|
124
|
+
):
|
|
125
|
+
"""
|
|
126
|
+
Make one or more '``ophyd.EpicsMotor``' objects.
|
|
127
|
+
|
|
128
|
+
Example entry in `devices.yml` file:
|
|
129
|
+
|
|
130
|
+
.. code-block:: yaml
|
|
131
|
+
:linenos:
|
|
132
|
+
|
|
133
|
+
instrument.devices.factories.motors:
|
|
134
|
+
- {prefix: "ioc:m", first: 1, last: 4, labels: ["motor"]}
|
|
135
|
+
# skip m5 & m6
|
|
136
|
+
- {prefix: "ioc:m", first: 7, last: 22, labels: ["motor"]}
|
|
137
|
+
|
|
138
|
+
Uses this pattern:
|
|
139
|
+
|
|
140
|
+
.. code-block:: py
|
|
141
|
+
:linenos:
|
|
142
|
+
|
|
143
|
+
ophyd.EpicsMotor(
|
|
144
|
+
prefix=prefix.format(i),
|
|
145
|
+
name=names.format(i),
|
|
146
|
+
**kwargs,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
where ``i`` iterates from 'first' through 'last' (inclusive).
|
|
150
|
+
|
|
151
|
+
PARAMETERS
|
|
152
|
+
|
|
153
|
+
prefix : str
|
|
154
|
+
Name *pattern* for the EPICS PVs. There is no default pattern. If a
|
|
155
|
+
formatting specification (``{}``) is not provided, it is appended (as
|
|
156
|
+
with other ophyd devices). Each motor will be configured with this
|
|
157
|
+
prefix: ``prefix.format(number)``, such as::
|
|
158
|
+
|
|
159
|
+
In [23]: "ioc:m{}".format(22)
|
|
160
|
+
Out[23]: 'ioc:m22'
|
|
161
|
+
|
|
162
|
+
names : str
|
|
163
|
+
Name *pattern* for the motors. The default pattern is ``"m{}"`` which
|
|
164
|
+
produces motors named ``m1, m2, ..., m22, m23, ...```. If a formatting
|
|
165
|
+
specification (``{}``) is not provided, it is appended. Each motor
|
|
166
|
+
will be named using this code: ``names.format(number)``, such as::
|
|
167
|
+
|
|
168
|
+
In [23]: "m{}".format(22)
|
|
169
|
+
Out[23]: 'm22'
|
|
170
|
+
|
|
171
|
+
first : int
|
|
172
|
+
The first motor number in the continuous series from 'first' through
|
|
173
|
+
'last' (inclusive).
|
|
174
|
+
|
|
175
|
+
last : int
|
|
176
|
+
The first motor number in the continuous series from 'first' through
|
|
177
|
+
'last' (inclusive).
|
|
178
|
+
|
|
179
|
+
kwargs : dict
|
|
180
|
+
Dictionary of additional keyword arguments. This is included
|
|
181
|
+
with each EpicsMotor object.
|
|
182
|
+
"""
|
|
183
|
+
if prefix is None:
|
|
184
|
+
raise ValueError("Must define a string value for 'prefix'.")
|
|
185
|
+
|
|
186
|
+
kwargs["names"] = names or "m{}"
|
|
187
|
+
kwargs["prefix"] = prefix
|
|
188
|
+
kwargs.update(
|
|
189
|
+
{
|
|
190
|
+
"prefix": prefix,
|
|
191
|
+
"names": names or "m{}",
|
|
192
|
+
"first": first,
|
|
193
|
+
"last": last,
|
|
194
|
+
"creator": "ophyd.EpicsMotor",
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
for motor in factory_base(**kwargs):
|
|
199
|
+
yield motor
|