nbs-bl 0.2.0__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.
- nbs_bl/__init__.py +15 -0
- nbs_bl/beamline.py +450 -0
- nbs_bl/configuration.py +838 -0
- nbs_bl/detectors.py +89 -0
- nbs_bl/devices/__init__.py +12 -0
- nbs_bl/devices/detectors.py +154 -0
- nbs_bl/devices/motors.py +242 -0
- nbs_bl/devices/sampleholders.py +360 -0
- nbs_bl/devices/shutters.py +120 -0
- nbs_bl/devices/slits.py +51 -0
- nbs_bl/gGrEqns.py +171 -0
- nbs_bl/geometry/__init__.py +0 -0
- nbs_bl/geometry/affine.py +197 -0
- nbs_bl/geometry/bars.py +189 -0
- nbs_bl/geometry/frames.py +534 -0
- nbs_bl/geometry/linalg.py +138 -0
- nbs_bl/geometry/polygons.py +56 -0
- nbs_bl/help.py +126 -0
- nbs_bl/hw.py +270 -0
- nbs_bl/load.py +113 -0
- nbs_bl/motors.py +19 -0
- nbs_bl/planStatus.py +5 -0
- nbs_bl/plans/__init__.py +8 -0
- nbs_bl/plans/batches.py +174 -0
- nbs_bl/plans/conditions.py +77 -0
- nbs_bl/plans/flyscan_base.py +180 -0
- nbs_bl/plans/groups.py +55 -0
- nbs_bl/plans/maximizers.py +423 -0
- nbs_bl/plans/metaplans.py +179 -0
- nbs_bl/plans/plan_stubs.py +246 -0
- nbs_bl/plans/preprocessors.py +160 -0
- nbs_bl/plans/scan_base.py +58 -0
- nbs_bl/plans/scan_decorators.py +524 -0
- nbs_bl/plans/scans.py +145 -0
- nbs_bl/plans/suspenders.py +87 -0
- nbs_bl/plans/time_estimation.py +168 -0
- nbs_bl/plans/xas.py +123 -0
- nbs_bl/printing.py +221 -0
- nbs_bl/qt/models/beamline.py +11 -0
- nbs_bl/qt/models/energy.py +53 -0
- nbs_bl/qt/widgets/energy.py +225 -0
- nbs_bl/queueserver.py +249 -0
- nbs_bl/redisDevice.py +96 -0
- nbs_bl/run_engine.py +63 -0
- nbs_bl/samples.py +130 -0
- nbs_bl/settings.py +68 -0
- nbs_bl/shutters.py +39 -0
- nbs_bl/sim/__init__.py +2 -0
- nbs_bl/sim/config/polphase.nc +0 -0
- nbs_bl/sim/energy.py +403 -0
- nbs_bl/sim/manipulator.py +14 -0
- nbs_bl/sim/utils.py +36 -0
- nbs_bl/startup.py +27 -0
- nbs_bl/status.py +114 -0
- nbs_bl/tests/__init__.py +0 -0
- nbs_bl/tests/modify_regions.py +160 -0
- nbs_bl/tests/test_frames.py +99 -0
- nbs_bl/tests/test_panels.py +69 -0
- nbs_bl/utils.py +235 -0
- nbs_bl-0.2.0.dist-info/METADATA +71 -0
- nbs_bl-0.2.0.dist-info/RECORD +64 -0
- nbs_bl-0.2.0.dist-info/WHEEL +4 -0
- nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
- nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from ..utils import merge_func
|
|
2
|
+
from importlib.metadata import entry_points
|
|
3
|
+
from bluesky.preprocessors import finalize_wrapper
|
|
4
|
+
from bluesky.suspenders import SuspenderBase
|
|
5
|
+
from bluesky import Msg
|
|
6
|
+
from ..beamline import GLOBAL_BEAMLINE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_suspender_entrypoints():
|
|
10
|
+
"""
|
|
11
|
+
Get suspender entrypoints from beamline configuration.
|
|
12
|
+
|
|
13
|
+
Returns
|
|
14
|
+
-------
|
|
15
|
+
list
|
|
16
|
+
List of decorator functions loaded from entrypoints
|
|
17
|
+
"""
|
|
18
|
+
config = GLOBAL_BEAMLINE.config
|
|
19
|
+
suspender_entrypoints = config.get("settings", {}).get("suspenders", [])
|
|
20
|
+
print(f"Suspender entrypoints: {suspender_entrypoints}")
|
|
21
|
+
suspenders = []
|
|
22
|
+
if suspender_entrypoints:
|
|
23
|
+
eps = entry_points()
|
|
24
|
+
|
|
25
|
+
for ep_name in suspender_entrypoints:
|
|
26
|
+
try:
|
|
27
|
+
# Look for entrypoint in nbs_bl.suspenders group
|
|
28
|
+
matches = eps.select(group="nbs_bl.suspenders", name=ep_name)
|
|
29
|
+
for match in matches:
|
|
30
|
+
print(f"Loading suspender {match.name}")
|
|
31
|
+
suspender = match.load()
|
|
32
|
+
|
|
33
|
+
# Handle both single suspender and list of suspenders
|
|
34
|
+
if isinstance(suspender, (list, tuple)):
|
|
35
|
+
suspenders.extend(suspender)
|
|
36
|
+
else:
|
|
37
|
+
suspenders.append(suspender)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Failed to load suspender {ep_name}: {e}")
|
|
40
|
+
|
|
41
|
+
return suspenders
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def dynamic_suspenders(func):
|
|
45
|
+
# Load suspenders from entrypoints at decoration time
|
|
46
|
+
suspenders = get_suspender_entrypoints()
|
|
47
|
+
|
|
48
|
+
@merge_func(func)
|
|
49
|
+
def wrapper(*args, skip_suspenders=False, **kwargs):
|
|
50
|
+
"""
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
skip_suspenders : bool, optional
|
|
54
|
+
If True, do not install suspenders
|
|
55
|
+
"""
|
|
56
|
+
# Check if suspenders should be skipped
|
|
57
|
+
|
|
58
|
+
if skip_suspenders or not suspenders:
|
|
59
|
+
return (yield from func(*args, **kwargs))
|
|
60
|
+
else:
|
|
61
|
+
suspender_list = []
|
|
62
|
+
for sus in suspenders:
|
|
63
|
+
if callable(sus) and not isinstance(sus, SuspenderBase):
|
|
64
|
+
sus = sus()
|
|
65
|
+
if isinstance(sus, (list, tuple)):
|
|
66
|
+
suspender_list.extend(sus)
|
|
67
|
+
else:
|
|
68
|
+
suspender_list.append(sus)
|
|
69
|
+
|
|
70
|
+
def _install():
|
|
71
|
+
for susp in suspender_list:
|
|
72
|
+
yield Msg("install_suspender", None, susp)
|
|
73
|
+
|
|
74
|
+
def _remove():
|
|
75
|
+
for susp in suspender_list:
|
|
76
|
+
yield Msg("remove_suspender", None, susp)
|
|
77
|
+
|
|
78
|
+
@merge_func(func)
|
|
79
|
+
def _inner_plan(*args, **kwargs):
|
|
80
|
+
yield from _install()
|
|
81
|
+
return (yield from func(*args, **kwargs))
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
yield from finalize_wrapper(_inner_plan(*args, **kwargs), _remove())
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return wrapper
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .scan_base import _make_gscan_points
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Time estimation functions for plan execution time calculation.
|
|
6
|
+
|
|
7
|
+
Usage Examples:
|
|
8
|
+
---------------
|
|
9
|
+
1. Basic time estimation:
|
|
10
|
+
estimation_dict = {
|
|
11
|
+
"estimator": "generic_estimate",
|
|
12
|
+
"fixed": 10,
|
|
13
|
+
"overhead": 0.5,
|
|
14
|
+
"dwell": "dwell_time"
|
|
15
|
+
}
|
|
16
|
+
plan_args = {"dwell_time": 2.0, "points": 100}
|
|
17
|
+
time = generic_estimate("my_plan", plan_args, estimation_dict)
|
|
18
|
+
|
|
19
|
+
2. With repeat functionality:
|
|
20
|
+
estimation_dict = {
|
|
21
|
+
"estimator": "generic_estimate",
|
|
22
|
+
"fixed": 10,
|
|
23
|
+
"overhead": 0.5,
|
|
24
|
+
"dwell": "dwell_time",
|
|
25
|
+
"reset": 5.0 # 5 seconds between repeats
|
|
26
|
+
}
|
|
27
|
+
plan_args = {"dwell_time": 2.0, "points": 100, "repeat": 3}
|
|
28
|
+
time = generic_estimate("my_plan", plan_args, estimation_dict)
|
|
29
|
+
# Result: (base_time * 3) + (5.0 * 2) = base_time * 3 + 10.0
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def with_repeat(estimator_func):
|
|
34
|
+
"""
|
|
35
|
+
Decorator to add repeat functionality to time estimators.
|
|
36
|
+
|
|
37
|
+
This decorator handles the "repeat" parameter in plan_args and "reset" parameter
|
|
38
|
+
in estimation_dict. It calculates the base plan time, multiplies by repeats,
|
|
39
|
+
and adds reset time for each repeat except the last one.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
estimator_func : callable
|
|
44
|
+
The original time estimator function
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
callable
|
|
49
|
+
Wrapped estimator function that handles repeats
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def wrapper(plan_name, plan_args, estimation_dict):
|
|
53
|
+
# Get repeat count and remove from plan_args to avoid confusion
|
|
54
|
+
repeat_count = plan_args.get("repeat", 1)
|
|
55
|
+
|
|
56
|
+
# Get reset time from estimation parameters
|
|
57
|
+
reset_time = estimation_dict.get("reset_time", 0)
|
|
58
|
+
|
|
59
|
+
# Calculate base plan time using original estimator
|
|
60
|
+
base_time = estimator_func(plan_name, plan_args, estimation_dict)
|
|
61
|
+
|
|
62
|
+
if base_time is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Calculate total time: base_time * repeats + reset_time * (repeats - 1)
|
|
66
|
+
total_time = base_time * repeat_count + reset_time * (repeat_count - 1)
|
|
67
|
+
|
|
68
|
+
return total_time
|
|
69
|
+
|
|
70
|
+
return wrapper
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_dwell(plan_args, estimation_dict):
|
|
74
|
+
dwell = estimation_dict.get("dwell", "dwell")
|
|
75
|
+
if isinstance(dwell, str):
|
|
76
|
+
return plan_args.get(dwell, 0)
|
|
77
|
+
else:
|
|
78
|
+
return dwell
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@with_repeat
|
|
82
|
+
def generic_estimate(plan_name, plan_args, estimation_dict):
|
|
83
|
+
a = estimation_dict.get("fixed", 0)
|
|
84
|
+
b = estimation_dict.get("overhead", 0)
|
|
85
|
+
c = get_dwell(plan_args, estimation_dict)
|
|
86
|
+
if "points" in estimation_dict:
|
|
87
|
+
points = estimation_dict.get("points")
|
|
88
|
+
if isinstance(points, str):
|
|
89
|
+
if points == "args":
|
|
90
|
+
n = len(plan_args.get("args", []))
|
|
91
|
+
else:
|
|
92
|
+
n = plan_args.get(points, 0)
|
|
93
|
+
else:
|
|
94
|
+
n = points
|
|
95
|
+
else:
|
|
96
|
+
n = 0
|
|
97
|
+
return a + b * n + c * n
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@with_repeat
|
|
101
|
+
def list_scan_estimate(plan_name, plan_args, estimation_dict):
|
|
102
|
+
a = estimation_dict.get("fixed", 0)
|
|
103
|
+
b = estimation_dict.get("overhead", 0)
|
|
104
|
+
c = get_dwell(plan_args, estimation_dict)
|
|
105
|
+
|
|
106
|
+
n = len(plan_args.get("args", ["", []])[1])
|
|
107
|
+
return a + b * n + c * n
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@with_repeat
|
|
111
|
+
def grid_scan_estimate(plan_name, plan_args, estimation_dict):
|
|
112
|
+
a = estimation_dict.get("fixed", 0)
|
|
113
|
+
b = estimation_dict.get("overhead", 0)
|
|
114
|
+
c = get_dwell(plan_args, estimation_dict)
|
|
115
|
+
|
|
116
|
+
args = plan_args.get("args")
|
|
117
|
+
n_axes = len(args) // 4
|
|
118
|
+
n_points = 1
|
|
119
|
+
for i in range(n_axes):
|
|
120
|
+
n_points *= args[i * 4 + 3]
|
|
121
|
+
return a + b * n_points + c * n_points
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@with_repeat
|
|
125
|
+
def list_grid_scan_estimate(plan_name, plan_args, estimation_dict):
|
|
126
|
+
a = estimation_dict.get("fixed", 0)
|
|
127
|
+
b = estimation_dict.get("overhead", 0)
|
|
128
|
+
c = get_dwell(plan_args, estimation_dict)
|
|
129
|
+
|
|
130
|
+
args = plan_args.get("args")
|
|
131
|
+
n_axes = len(args) // 2
|
|
132
|
+
n_points = 1
|
|
133
|
+
for i in range(n_axes):
|
|
134
|
+
n_points *= len(args[i * 2 + 1])
|
|
135
|
+
return a + b * n_points + c * n_points
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@with_repeat
|
|
139
|
+
def fly_scan_estimate(plan_name, plan_args, estimation_dict):
|
|
140
|
+
if "args" in plan_args:
|
|
141
|
+
args = plan_args["args"]
|
|
142
|
+
if len(args) % 2 == 0:
|
|
143
|
+
args = args[1:]
|
|
144
|
+
start = args[0]
|
|
145
|
+
stop = args[1]
|
|
146
|
+
speed = args[2]
|
|
147
|
+
else:
|
|
148
|
+
start = plan_args.get("start", 0)
|
|
149
|
+
stop = plan_args.get("stop", 0)
|
|
150
|
+
speed = plan_args.get("speed", 1)
|
|
151
|
+
a = estimation_dict.get("fixed", 0)
|
|
152
|
+
return a + np.abs((stop - start) / speed)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@with_repeat
|
|
156
|
+
def gscan_estimate(plan_name, plan_args, estimation_dict):
|
|
157
|
+
if "region" in estimation_dict:
|
|
158
|
+
region = estimation_dict.get("region")
|
|
159
|
+
else:
|
|
160
|
+
region = plan_args.get("args")
|
|
161
|
+
if len(region) % 2 == 0:
|
|
162
|
+
region = region[1:] # First argument is motor name
|
|
163
|
+
points = len(_make_gscan_points(*region))
|
|
164
|
+
a = estimation_dict.get("fixed", 0)
|
|
165
|
+
b = estimation_dict.get("overhead", 0)
|
|
166
|
+
c = get_dwell(plan_args, estimation_dict)
|
|
167
|
+
|
|
168
|
+
return a + b * points + c * points
|
nbs_bl/plans/xas.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# from ..settings import settings
|
|
2
|
+
from ..plans.scans import nbs_gscan
|
|
3
|
+
from ..utils import merge_func
|
|
4
|
+
from ..plans.preprocessors import wrap_metadata
|
|
5
|
+
from .plan_stubs import set_roi, clear_one_roi
|
|
6
|
+
from ..help import _add_to_import_list, add_to_func_list, add_to_plan_time_dict
|
|
7
|
+
from ..queueserver import GLOBAL_USER_STATUS
|
|
8
|
+
from ..status import StatusDict
|
|
9
|
+
from ..beamline import GLOBAL_BEAMLINE
|
|
10
|
+
from .scan_base import _make_gscan_points
|
|
11
|
+
|
|
12
|
+
# from ..settings import GLOBAL_SETTINGS as settings
|
|
13
|
+
from os.path import join
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tomllib
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
import tomli as tomllib
|
|
19
|
+
|
|
20
|
+
GLOBAL_XAS_PLANS = GLOBAL_USER_STATUS.request_status_dict("XAS_PLANS", use_redis=True)
|
|
21
|
+
GLOBAL_XAS_PLANS.clear()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_to_xas_list(f, key, **plan_info):
|
|
25
|
+
"""
|
|
26
|
+
A function decorator that will add the plan to the built-in list
|
|
27
|
+
"""
|
|
28
|
+
_add_to_import_list(f, "xas")
|
|
29
|
+
GLOBAL_XAS_PLANS[key] = {}
|
|
30
|
+
GLOBAL_XAS_PLANS[key].update(plan_info)
|
|
31
|
+
return f
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _wrap_xas(element, edge):
|
|
35
|
+
def decorator(func):
|
|
36
|
+
return wrap_metadata({"element": element, "edge": edge, "scantype": "xas"})(
|
|
37
|
+
func
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return decorator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Needs to have Element, Edge, Ref Element,
|
|
44
|
+
def _xas_factory(energy_grid, element, edge, key):
|
|
45
|
+
@_wrap_xas(element, edge)
|
|
46
|
+
@wrap_metadata({"plan_name": key})
|
|
47
|
+
@merge_func(nbs_gscan, omit_params=["motor", "start", "stop", "step", "args"])
|
|
48
|
+
def inner(**kwargs):
|
|
49
|
+
"""Parameters
|
|
50
|
+
----------
|
|
51
|
+
repeat : int
|
|
52
|
+
Number of times to repeat the scan
|
|
53
|
+
**kwargs :
|
|
54
|
+
Arguments to be passed to tes_gscan
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
eref_sample = kwargs.pop("eref_sample", None)
|
|
58
|
+
if eref_sample is None:
|
|
59
|
+
eref_sample = element
|
|
60
|
+
yield from set_roi("pfy", energy_grid[0], energy_grid[-2])
|
|
61
|
+
yield from nbs_gscan(
|
|
62
|
+
GLOBAL_BEAMLINE.energy, *energy_grid, eref_sample=eref_sample, **kwargs
|
|
63
|
+
)
|
|
64
|
+
yield from clear_one_roi("pfy")
|
|
65
|
+
|
|
66
|
+
d = f"Perform an in-place xas scan for {element} with energy pattern {energy_grid} \n"
|
|
67
|
+
inner.__doc__ = d + inner.__doc__
|
|
68
|
+
|
|
69
|
+
inner.__qualname__ = key
|
|
70
|
+
inner.__name__ = key
|
|
71
|
+
inner._edge = edge
|
|
72
|
+
inner._short_doc = (
|
|
73
|
+
f"Do XAS for {element} from {energy_grid[0]} to {energy_grid[-2]}"
|
|
74
|
+
)
|
|
75
|
+
return inner
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@add_to_func_list
|
|
79
|
+
def load_xas(filename):
|
|
80
|
+
"""Load XAS plans from a TOML file and inject them into the IPython user namespace.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
filename : str
|
|
85
|
+
Path to the TOML file containing XAS plan definitions
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Get IPython's user namespace
|
|
89
|
+
ip = get_ipython()
|
|
90
|
+
user_ns = ip.user_ns
|
|
91
|
+
except (NameError, AttributeError):
|
|
92
|
+
# Not running in IPython, just return the generated functions
|
|
93
|
+
user_ns = None
|
|
94
|
+
|
|
95
|
+
generated_plans = {}
|
|
96
|
+
with open(filename, "rb") as f:
|
|
97
|
+
regions = tomllib.load(f)
|
|
98
|
+
for key, value in regions.items():
|
|
99
|
+
name = value.get("name", key)
|
|
100
|
+
region = value.get("region")
|
|
101
|
+
element = value.get("element", "")
|
|
102
|
+
edge = value.get("edge", "")
|
|
103
|
+
xas_func = _xas_factory(region, element, edge, key)
|
|
104
|
+
add_to_xas_list(
|
|
105
|
+
xas_func, key, name=name, element=element, edge=edge, region=region
|
|
106
|
+
)
|
|
107
|
+
add_to_plan_time_dict(
|
|
108
|
+
xas_func,
|
|
109
|
+
"gscan_estimate",
|
|
110
|
+
fixed=5,
|
|
111
|
+
overhead=0.5,
|
|
112
|
+
dwell="dwell",
|
|
113
|
+
region=region,
|
|
114
|
+
)
|
|
115
|
+
# Store the function
|
|
116
|
+
generated_plans[key] = xas_func
|
|
117
|
+
|
|
118
|
+
# If we're in IPython, inject into user namespace
|
|
119
|
+
if user_ns is not None:
|
|
120
|
+
user_ns[key] = xas_func
|
|
121
|
+
|
|
122
|
+
# Return the generated plans dictionary in case it's needed
|
|
123
|
+
return generated_plans
|
nbs_bl/printing.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from IPython.utils.coloransi import TermColors as color
|
|
2
|
+
from rich import print
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
from select import select
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _strip_ansi(text):
|
|
9
|
+
"""
|
|
10
|
+
Remove ANSI escape sequences from text.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
text : str
|
|
15
|
+
Text containing ANSI escape sequences
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
str
|
|
20
|
+
Text with ANSI sequences removed
|
|
21
|
+
"""
|
|
22
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
23
|
+
return ansi_escape.sub("", text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ansilen(text):
|
|
27
|
+
"""
|
|
28
|
+
Get visible length of text containing ANSI escape sequences.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
text : str
|
|
33
|
+
Text containing ANSI escape sequences
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
int
|
|
38
|
+
Visual length of text
|
|
39
|
+
"""
|
|
40
|
+
return len(_strip_ansi(text))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _wrap_text(text, width, subsequent_indent=""):
|
|
44
|
+
"""
|
|
45
|
+
Wrap text while preserving ANSI escape sequences.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
text : str
|
|
50
|
+
Text to wrap
|
|
51
|
+
width : int
|
|
52
|
+
Maximum line width
|
|
53
|
+
subsequent_indent : str
|
|
54
|
+
Indentation for lines after the first
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
str
|
|
59
|
+
Wrapped text with preserved ANSI sequences
|
|
60
|
+
"""
|
|
61
|
+
words = text.split()
|
|
62
|
+
lines = []
|
|
63
|
+
current_line = []
|
|
64
|
+
current_length = 0
|
|
65
|
+
|
|
66
|
+
for word in words:
|
|
67
|
+
word_length = _ansilen(word)
|
|
68
|
+
if current_length + word_length + (1 if current_line else 0) <= width:
|
|
69
|
+
current_line.append(word)
|
|
70
|
+
current_length += word_length + (1 if current_line else 0)
|
|
71
|
+
else:
|
|
72
|
+
if current_line:
|
|
73
|
+
lines.append(" ".join(current_line))
|
|
74
|
+
current_line = [word]
|
|
75
|
+
current_length = word_length
|
|
76
|
+
|
|
77
|
+
if current_line:
|
|
78
|
+
lines.append(" ".join(current_line))
|
|
79
|
+
|
|
80
|
+
# Add indentation to subsequent lines
|
|
81
|
+
if subsequent_indent and len(lines) > 1:
|
|
82
|
+
lines[1:] = [subsequent_indent + line for line in lines[1:]]
|
|
83
|
+
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def colored(text, tint="white", attrs=[]):
|
|
88
|
+
"""
|
|
89
|
+
A simple wrapper around IPython's interface to TermColors
|
|
90
|
+
"""
|
|
91
|
+
tint = tint.lower()
|
|
92
|
+
if "dark" in tint:
|
|
93
|
+
tint = tint[4:].lower()
|
|
94
|
+
elif "light" in tint:
|
|
95
|
+
tint = "bright_" + tint[5:].lower()
|
|
96
|
+
elif "blink" in tint:
|
|
97
|
+
tint = "bold " + tint[5:].lower()
|
|
98
|
+
elif "no" in tint:
|
|
99
|
+
tint = "reset"
|
|
100
|
+
else:
|
|
101
|
+
tint = tint.lower()
|
|
102
|
+
return "[{0}]{1}[/{0}]".format(tint, str(text))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# def colored(text, tint="white", attrs=[]):
|
|
106
|
+
# """
|
|
107
|
+
# A simple wrapper around IPython's interface to TermColors
|
|
108
|
+
# """
|
|
109
|
+
# tint = tint.lower()
|
|
110
|
+
# if "dark" in tint:
|
|
111
|
+
# tint = "Dark" + tint[4:].capitalize()
|
|
112
|
+
# elif "light" in tint:
|
|
113
|
+
# tint = "Light" + tint[5:].capitalize()
|
|
114
|
+
# elif "blink" in tint:
|
|
115
|
+
# tint = "Blink" + tint[5:].capitalize()
|
|
116
|
+
# elif "no" in tint:
|
|
117
|
+
# tint = "Normal"
|
|
118
|
+
# else:
|
|
119
|
+
# tint = tint.capitalize()
|
|
120
|
+
# return "{0}{1}{2}".format(getattr(color, tint), str(text), color.Normal)
|
|
121
|
+
|
|
122
|
+
def read_input(message, default, timeout, secs):
|
|
123
|
+
print(message)
|
|
124
|
+
rlist, _, _ = select([sys.stdin], [], [], secs)
|
|
125
|
+
if rlist:
|
|
126
|
+
s = sys.stdin.readline()
|
|
127
|
+
if ord(s[0]) == 13 or ord(s[0]) == 10:
|
|
128
|
+
return default
|
|
129
|
+
else:
|
|
130
|
+
return s.rstrip(chr(10) + chr(13))
|
|
131
|
+
else:
|
|
132
|
+
return timeout
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_report(thisfile):
|
|
136
|
+
"""
|
|
137
|
+
Noisily proclaim to be importing a file of python code.
|
|
138
|
+
"""
|
|
139
|
+
print(colored("Importing %s ..." % thisfile.split("/")[-1], "lightcyan"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def boxed_text(title, text, tint, width=82, shrink=False):
|
|
143
|
+
"""
|
|
144
|
+
Put text in a lovely unicode block element box.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
title : str
|
|
149
|
+
Title to display at top of box
|
|
150
|
+
text : str or list
|
|
151
|
+
Text content to display in box
|
|
152
|
+
tint : str
|
|
153
|
+
Color to use for box borders
|
|
154
|
+
width : int
|
|
155
|
+
Maximum width of box
|
|
156
|
+
shrink : bool
|
|
157
|
+
Whether to shrink box to content width
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(text, str):
|
|
160
|
+
textlines = text.splitlines()
|
|
161
|
+
else:
|
|
162
|
+
textlines = text
|
|
163
|
+
|
|
164
|
+
if shrink:
|
|
165
|
+
width = min(width, max((_ansilen(line) for line in textlines)))
|
|
166
|
+
|
|
167
|
+
remainder = width - 5 - len(title)
|
|
168
|
+
ul = "\u250C"
|
|
169
|
+
ur = "\u2510"
|
|
170
|
+
ll = "\u2514"
|
|
171
|
+
lr = "\u2518"
|
|
172
|
+
bar = "\u2500"
|
|
173
|
+
strut = "\u2502"
|
|
174
|
+
|
|
175
|
+
print("")
|
|
176
|
+
print(colored("".join([ul, bar * 3, " ", title, " ", bar * remainder, ur]), tint))
|
|
177
|
+
|
|
178
|
+
for line1 in textlines:
|
|
179
|
+
lines = _wrap_text(line1, width, subsequent_indent=" " * 26)
|
|
180
|
+
for line in lines.splitlines():
|
|
181
|
+
line = line.rstrip()
|
|
182
|
+
add = " " * (width - _ansilen(line))
|
|
183
|
+
print("".join([colored(strut, tint), line, add, colored(strut, tint)]))
|
|
184
|
+
|
|
185
|
+
print(colored("".join([ll, bar * width, lr]), tint))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def error_msg(text):
|
|
189
|
+
return colored(text, "lightred")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def warning_msg(text):
|
|
193
|
+
return colored(text, "yellow")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def url_msg(text):
|
|
197
|
+
return text
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def bold_msg(text):
|
|
201
|
+
return colored(text, "white")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def verbosebold_msg(text):
|
|
205
|
+
return colored(text, "lightcyan")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def list_msg(text):
|
|
209
|
+
return colored(text, "cyan")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def disconnected_msg(text):
|
|
213
|
+
return colored(text, "purple")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def info_msg(text):
|
|
217
|
+
return colored(text, "brown")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def whisper(text):
|
|
221
|
+
return colored(text, "darkgray")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from nbs_gui.models.beamline import GUIBeamlineModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SSTBeamlineModel(GUIBeamlineModel):
|
|
5
|
+
def __init__(self, *args, **kwargs):
|
|
6
|
+
super().__init__(*args, **kwargs)
|
|
7
|
+
try:
|
|
8
|
+
self.energy.obj.rotation_motor = self.primary_sampleholder.obj.r
|
|
9
|
+
except AttributeError as e:
|
|
10
|
+
print(f"Problem loading energy model: {e}")
|
|
11
|
+
print("Finished loading BeamlineModel")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from ..widgets.energy import EnergyControl, EnergyMonitor
|
|
2
|
+
from nbs_gui.models import PseudoPositionerModel, PVPositionerModel, PVModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Copied from ucal as an example
|
|
6
|
+
class EnergyModel:
|
|
7
|
+
default_controller = EnergyControl
|
|
8
|
+
default_monitor = EnergyMonitor
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name,
|
|
13
|
+
obj,
|
|
14
|
+
group,
|
|
15
|
+
long_name,
|
|
16
|
+
**kwargs,
|
|
17
|
+
):
|
|
18
|
+
print("Initializing Energy")
|
|
19
|
+
self.name = name
|
|
20
|
+
self.obj = obj
|
|
21
|
+
self.energy = PseudoPositionerModel(name, obj, group, name)
|
|
22
|
+
self.grating_motor = PVPositionerModel(
|
|
23
|
+
name=obj.monoen.gratingx.name,
|
|
24
|
+
obj=obj.monoen.gratingx,
|
|
25
|
+
group=group,
|
|
26
|
+
long_name=f"{name} Grating",
|
|
27
|
+
)
|
|
28
|
+
self.cff = PVModel(
|
|
29
|
+
obj.monoen.cff.name, obj.monoen.cff, group=group, long_name=f"{name} CFF"
|
|
30
|
+
)
|
|
31
|
+
self.real_motors = self.energy.real_motors
|
|
32
|
+
self.group = group
|
|
33
|
+
self.label = long_name
|
|
34
|
+
for key, value in kwargs.items():
|
|
35
|
+
setattr(self, key, value)
|
|
36
|
+
print("Done Initializing Energy")
|
|
37
|
+
|
|
38
|
+
def iter_models(self):
|
|
39
|
+
"""
|
|
40
|
+
Yield contained energy-related models for traversal.
|
|
41
|
+
|
|
42
|
+
Yields
|
|
43
|
+
------
|
|
44
|
+
BaseModel
|
|
45
|
+
Contained models.
|
|
46
|
+
"""
|
|
47
|
+
yield from (
|
|
48
|
+
self.energy,
|
|
49
|
+
self.grating_motor,
|
|
50
|
+
self.cff,
|
|
51
|
+
)
|
|
52
|
+
for motor in self.real_motors:
|
|
53
|
+
yield motor
|