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.
Files changed (64) hide show
  1. nbs_bl/__init__.py +15 -0
  2. nbs_bl/beamline.py +450 -0
  3. nbs_bl/configuration.py +838 -0
  4. nbs_bl/detectors.py +89 -0
  5. nbs_bl/devices/__init__.py +12 -0
  6. nbs_bl/devices/detectors.py +154 -0
  7. nbs_bl/devices/motors.py +242 -0
  8. nbs_bl/devices/sampleholders.py +360 -0
  9. nbs_bl/devices/shutters.py +120 -0
  10. nbs_bl/devices/slits.py +51 -0
  11. nbs_bl/gGrEqns.py +171 -0
  12. nbs_bl/geometry/__init__.py +0 -0
  13. nbs_bl/geometry/affine.py +197 -0
  14. nbs_bl/geometry/bars.py +189 -0
  15. nbs_bl/geometry/frames.py +534 -0
  16. nbs_bl/geometry/linalg.py +138 -0
  17. nbs_bl/geometry/polygons.py +56 -0
  18. nbs_bl/help.py +126 -0
  19. nbs_bl/hw.py +270 -0
  20. nbs_bl/load.py +113 -0
  21. nbs_bl/motors.py +19 -0
  22. nbs_bl/planStatus.py +5 -0
  23. nbs_bl/plans/__init__.py +8 -0
  24. nbs_bl/plans/batches.py +174 -0
  25. nbs_bl/plans/conditions.py +77 -0
  26. nbs_bl/plans/flyscan_base.py +180 -0
  27. nbs_bl/plans/groups.py +55 -0
  28. nbs_bl/plans/maximizers.py +423 -0
  29. nbs_bl/plans/metaplans.py +179 -0
  30. nbs_bl/plans/plan_stubs.py +246 -0
  31. nbs_bl/plans/preprocessors.py +160 -0
  32. nbs_bl/plans/scan_base.py +58 -0
  33. nbs_bl/plans/scan_decorators.py +524 -0
  34. nbs_bl/plans/scans.py +145 -0
  35. nbs_bl/plans/suspenders.py +87 -0
  36. nbs_bl/plans/time_estimation.py +168 -0
  37. nbs_bl/plans/xas.py +123 -0
  38. nbs_bl/printing.py +221 -0
  39. nbs_bl/qt/models/beamline.py +11 -0
  40. nbs_bl/qt/models/energy.py +53 -0
  41. nbs_bl/qt/widgets/energy.py +225 -0
  42. nbs_bl/queueserver.py +249 -0
  43. nbs_bl/redisDevice.py +96 -0
  44. nbs_bl/run_engine.py +63 -0
  45. nbs_bl/samples.py +130 -0
  46. nbs_bl/settings.py +68 -0
  47. nbs_bl/shutters.py +39 -0
  48. nbs_bl/sim/__init__.py +2 -0
  49. nbs_bl/sim/config/polphase.nc +0 -0
  50. nbs_bl/sim/energy.py +403 -0
  51. nbs_bl/sim/manipulator.py +14 -0
  52. nbs_bl/sim/utils.py +36 -0
  53. nbs_bl/startup.py +27 -0
  54. nbs_bl/status.py +114 -0
  55. nbs_bl/tests/__init__.py +0 -0
  56. nbs_bl/tests/modify_regions.py +160 -0
  57. nbs_bl/tests/test_frames.py +99 -0
  58. nbs_bl/tests/test_panels.py +69 -0
  59. nbs_bl/utils.py +235 -0
  60. nbs_bl-0.2.0.dist-info/METADATA +71 -0
  61. nbs_bl-0.2.0.dist-info/RECORD +64 -0
  62. nbs_bl-0.2.0.dist-info/WHEEL +4 -0
  63. nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
  64. nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,423 @@
1
+ from bluesky_live.bluesky_run import BlueskyRun, DocumentCache
2
+ import bluesky.preprocessors as bpp
3
+ from .preprocessors import run_return_decorator
4
+ from bluesky.plan_stubs import mv, mvr, trigger_and_read
5
+ import numpy as np
6
+
7
+ from nbs_bl.plans.flyscan_base import fly_scan
8
+ from .scan_decorators import wrap_plan_name
9
+ import pandas as pd
10
+ from typing import List
11
+
12
+
13
+ def process_fly_scan(run, signal_names=None, time_offsets=None):
14
+ if time_offsets is None:
15
+ time_offsets = {}
16
+
17
+ df = pd.DataFrame()
18
+ for stream_name in run:
19
+ if "monitor" not in stream_name:
20
+ print(stream_name)
21
+ continue
22
+ column_name = stream_name.replace("_monitor", "")
23
+ newdf = pd.DataFrame(
24
+ {
25
+ "time": run[stream_name].read()["time"].to_numpy(),
26
+ column_name: run[stream_name].read()[column_name].to_numpy(),
27
+ }
28
+ ).set_index("time")
29
+ newdf.index += time_offsets.get(stream_name, 0.0)
30
+ df = pd.concat((df, newdf))
31
+ non_monitor_signals = [
32
+ signal for signal in signal_names if signal not in df.columns
33
+ ]
34
+ print("Non-monitor signals: ", non_monitor_signals)
35
+ primary = run.primary.read()
36
+ for signal_name in non_monitor_signals:
37
+ if signal_name in primary:
38
+ newdf = pd.DataFrame(
39
+ {
40
+ "time": primary["time"].to_numpy(),
41
+ signal_name: primary[signal_name].to_numpy(),
42
+ }
43
+ ).set_index("time")
44
+ newdf.index += time_offsets.get(signal_name, 0.0)
45
+ df = pd.concat((df, newdf))
46
+ # df = df[~df.index.duplicated(keep='first')].sort_index().interpolate(method='index').ffill().bfill()
47
+
48
+ df = (
49
+ df.groupby("time")
50
+ .mean()
51
+ .sort_index()
52
+ .interpolate(method="index")
53
+ .ffill()
54
+ .bfill()
55
+ )
56
+
57
+ return df
58
+
59
+
60
+ def find_optimum_motor_pos(
61
+ run,
62
+ motor_name: str,
63
+ signal_names: List[str],
64
+ time_offsets=None,
65
+ invert=False,
66
+ ):
67
+ """
68
+ df = process_fly_scan(run, [motor_name] + signal_names, time_offsets)
69
+ max_signal_dict = {}
70
+ for signal_name in signal_names:
71
+ idx = df[signal_name].idxmax() if not invert else df[signal_name].idxmin()
72
+ max_signal_dict[signal_name] = {}
73
+ max_signal_dict[signal_name]["time"] = idx
74
+ max_signal_dict[signal_name][motor_name] = df[motor_name][idx]
75
+ max_signal_dict[signal_name]["value"] = df[signal_name][idx]
76
+ return max_signal_dict
77
+ """
78
+ table = run.primary.read()
79
+ max_info = {}
80
+ for detname in signal_names:
81
+ if invert:
82
+ idx = int(table[detname].argmin())
83
+ print(f"Minimum found at step {idx} for detector {detname}")
84
+ else:
85
+ idx = int(table[detname].argmax())
86
+ print(f"Maximum found at step {idx} for detector {detname}")
87
+ max_info[detname] = {"idx": idx, "value": table[detname][idx]}
88
+ max_val = float(table[motor_name][idx])
89
+ max_info[detname][motor_name] = max_val
90
+ return max_info
91
+
92
+
93
+ @wrap_plan_name
94
+ def fly_max(
95
+ detectors,
96
+ motor,
97
+ *args,
98
+ max_channel=None,
99
+ invert=False,
100
+ end_on_max=True,
101
+ md=None,
102
+ period: float = 1,
103
+ time_offsets: dict = None,
104
+ **kwargs,
105
+ ):
106
+ r"""
107
+ plan: tune a motor to the maximum of signal(motor)
108
+
109
+ Initially, traverse the range from start to stop with
110
+ the number of points specified. Repeat with progressively
111
+ smaller step size until the minimum step size is reached.
112
+ Rescans will be centered on the signal maximum
113
+ with original scan range reduced by ``step_factor``.
114
+
115
+ Set ``snake=True`` if your positions are reproducible
116
+ moving from either direction. This will not
117
+ decrease the number of traversals required to reach convergence.
118
+ Snake motion reduces the total time spent on motion
119
+ to reset the positioner. For some positioners, such as
120
+ those with hysteresis, snake scanning may not be appropriate.
121
+ For such positioners, always approach the positions from the
122
+ same direction.
123
+
124
+ Note: if there are multiple maxima, this function may find a smaller one
125
+ unless the initial number of steps is large enough.
126
+
127
+ Parameters
128
+ ----------
129
+ detectors : Signal
130
+ list of 'readable' objects
131
+ motor : object
132
+ any 'settable' object (motor, temp controller, etc.)
133
+ start : float
134
+ start of range
135
+ stop : float
136
+ end of range, note: start < stop
137
+ velocities : list of floats
138
+ list of speeds to set motor to during run.
139
+ max_channel : list of strings
140
+ detector fields whose output is to maximize. If not given, the first detector is used.
141
+ (the first will be maximized, but secondardy maxes will be recorded during the scans for the first -
142
+ if the maxima are not in the same range this will not be useful)
143
+ md : dict, optional
144
+ metadata
145
+ period: float, optional
146
+ Detector integration time in seconds.
147
+ **kwargs : dict, optional
148
+ additional arguments to pass to fly_scan
149
+
150
+ """
151
+ if max_channel is None:
152
+ max_channel = [detectors[0].name]
153
+ _md = {
154
+ "maximizer_args": {
155
+ "plan": "fly_scan",
156
+ "max_channel": max_channel,
157
+ "end_on_max": end_on_max,
158
+ "invert": invert,
159
+ },
160
+ "hints": {},
161
+ }
162
+ _md.update(md or {})
163
+
164
+ dc = DocumentCache()
165
+
166
+ @bpp.subs_decorator(dc)
167
+ def inner_maximizer():
168
+ yield from fly_scan(detectors, motor, *args, md=_md, period=period, **kwargs)
169
+ run = BlueskyRun(dc)
170
+ motor_name = motor.name
171
+ max_info = {}
172
+ max_info = find_optimum_motor_pos(run, motor_name, max_channel, invert=invert)
173
+ if end_on_max:
174
+ motor_pos = max_info[max_channel[0]][motor_name]
175
+ print(f"going to position {motor_pos} for {motor_name}")
176
+ yield from mv(motor, motor_pos)
177
+ return max_info
178
+
179
+ return (yield from inner_maximizer())
180
+
181
+
182
+ @wrap_plan_name
183
+ def find_max(
184
+ plan,
185
+ dets,
186
+ *args,
187
+ max_channel=None,
188
+ invert=False,
189
+ end_on_max=True,
190
+ md=None,
191
+ **kwargs,
192
+ ):
193
+ """
194
+ invert turns find_max into find_min
195
+ """
196
+ dc = DocumentCache()
197
+
198
+ md = md or {}
199
+
200
+ _md = {
201
+ "maximizer_args": {
202
+ "plan": plan.__name__,
203
+ "max_channel": max_channel,
204
+ "invert": invert,
205
+ "end_on_max": end_on_max,
206
+ },
207
+ }
208
+ _md.update(md)
209
+
210
+ @bpp.subs_decorator(dc)
211
+ def inner_maximizer():
212
+ yield from plan(dets, *args, md=_md, **kwargs)
213
+ run = BlueskyRun(dc)
214
+ table = run.primary.read()
215
+ motor_names = run.metadata["start"]["motors"]
216
+ motors = [m for m in args if getattr(m, "name", None) in motor_names]
217
+ if max_channel is None:
218
+ detname = dets[0].name
219
+ else:
220
+ detname = max_channel
221
+ if invert:
222
+ max_idx = int(table[detname].argmin())
223
+ print(f"Minimum found at step {max_idx} for detector {detname}")
224
+ else:
225
+ max_idx = int(table[detname].argmax())
226
+ print(f"Maximum found at step {max_idx} for detector {detname}")
227
+ ret = []
228
+ for m in motors:
229
+ max_val = float(table[m.name][max_idx])
230
+ print(f"setting {m.name} to {max_val}")
231
+ ret.append([m, max_val])
232
+ if end_on_max:
233
+ print("going to found motor positions")
234
+ flat_list = [item for sublist in ret for item in sublist]
235
+ yield from mv(*flat_list)
236
+ return ret
237
+
238
+ return (yield from inner_maximizer())
239
+
240
+
241
+ def find_min(plan, dets, *args):
242
+ return (yield from find_max(plan, dets, *args, invert=True))
243
+
244
+
245
+ def find_max_deriv(plan, dets, *args, max_channel=None):
246
+ dc = DocumentCache()
247
+
248
+ @bpp.subs_decorator(dc)
249
+ def inner_maximizer():
250
+ yield from plan(dets, *args)
251
+ run = BlueskyRun(dc)
252
+ table = run.primary.read()
253
+ motor_names = run.metadata["start"]["motors"]
254
+ motors = [m for m in args if getattr(m, "name", None) in motor_names]
255
+ if len(motors) > 1:
256
+ print(
257
+ "Derivative with multiple motors unsupported, \
258
+ taking first motor"
259
+ )
260
+
261
+ if max_channel is None:
262
+ detname = dets[0].name
263
+ else:
264
+ detname = max_channel
265
+ motname = motors[0].name
266
+ max_idx = np.argmax(np.abs(np.gradient(table[detname], table[motname])))
267
+ print(f"Maximum derivative found at step {max_idx} for detector {detname}")
268
+ ret = []
269
+ for m in motors:
270
+ max_val = float(table[m.name][max_idx])
271
+ print(f"setting {m.name} to {max_val}")
272
+ ret.append([m, max_val])
273
+ yield from mv(m, max_val)
274
+ return ret
275
+
276
+ return (yield from inner_maximizer())
277
+
278
+
279
+ def find_halfmax(plan, dets, *args, max_channel=None, **kwargs):
280
+ """
281
+ For a plan and detector that goes from low to high, find
282
+ the motor value where the detector is half of the maximum
283
+ value
284
+ """
285
+ dc = DocumentCache()
286
+
287
+ @bpp.subs_decorator(dc)
288
+ def inner_maximizer():
289
+ yield from plan(dets, *args, **kwargs)
290
+ run = BlueskyRun(dc)
291
+ table = run.primary.read()
292
+ motor_names = run.metadata["start"]["motors"]
293
+ motors = [m for m in args if getattr(m, "name", None) in motor_names]
294
+ if max_channel is None:
295
+ detname = dets[0].name
296
+ else:
297
+ detname = max_channel
298
+ max_val = float(table[detname].max())
299
+ halftable = table[detname] - max_val / 2.0
300
+ half_idx = 0
301
+ for n, v in enumerate(halftable.data):
302
+ if v > 0:
303
+ half_idx = n
304
+ break
305
+ ret = []
306
+ for m in motors:
307
+ mot_val = float(table[m.name][half_idx])
308
+ print(f"setting {m.name} to {mot_val}")
309
+ ret.append([m, mot_val])
310
+ yield from mv(m, mot_val)
311
+ return ret
312
+
313
+ return (yield from inner_maximizer())
314
+
315
+
316
+ def halfmax_adaptive(
317
+ dets, motor, step: float = 5, precision: float = 1, maxct=None, max_channel=None
318
+ ):
319
+ if max_channel is None:
320
+ detname = dets[0].name
321
+ else:
322
+ detname = max_channel
323
+
324
+ def ct():
325
+ ret = yield from trigger_and_read(dets)
326
+ return ret[detname]["value"]
327
+
328
+ @bpp.stage_decorator(dets)
329
+ @run_return_decorator()
330
+ def halfmax_inner(step, maxct=None):
331
+ if maxct is None:
332
+ maxct = yield from ct()
333
+ current = maxct
334
+ while np.abs(step) > precision / 2.0:
335
+ yield from mvr(motor, -1 * step)
336
+ current = yield from ct()
337
+ while current > maxct / 2.0:
338
+ yield from mvr(motor, step)
339
+ current = yield from ct()
340
+ step = step / 2.0
341
+ if np.abs(step) > precision / 2.0:
342
+ print(f"{detname} halfmax at {motor.name}:{motor.position}")
343
+ print(f"Value: {current}, Max: {maxct}, reducing step to {step}")
344
+
345
+ print(f"{detname} halfmax at {motor.name}:{motor.position}")
346
+ print(f"Value: {current}, Max: {maxct}, " f"precision: {precision} reached")
347
+ return motor.position
348
+
349
+ return (yield from halfmax_inner(step, maxct))
350
+
351
+
352
+ def threshold_adaptive(
353
+ dets, motor, threshold, step: float = 2, limit: int = 15, max_channel=None
354
+ ):
355
+ """
356
+ Attempt to get a detector over a threshold by moving a motor
357
+ """
358
+
359
+ if max_channel is None:
360
+ detname = dets[0].name
361
+ else:
362
+ detname = max_channel
363
+
364
+ def ct():
365
+ ret = yield from trigger_and_read(dets)
366
+ return ret[detname]["value"]
367
+
368
+ @bpp.stage_decorator(dets)
369
+ @run_return_decorator()
370
+ def inner_threshold():
371
+ pos = motor.position
372
+ maxpos = pos
373
+
374
+ n = 0
375
+ current = yield from ct()
376
+
377
+ if current > threshold:
378
+ mincurrent = current
379
+ minpos = motor.position
380
+ print(
381
+ f"Starting above threshold of {detname}, try to get below {threshold} by moving {motor.name} with starting position {pos} and step -{step}"
382
+ )
383
+ while current > threshold and n < limit:
384
+ yield from mvr(motor, -1 * step)
385
+ current = yield from ct()
386
+ if current < mincurrent:
387
+ mincurrent = current
388
+ minpos = motor.position
389
+ n += 1
390
+ if current < threshold:
391
+ pass
392
+ else:
393
+ raise ValueError(
394
+ f"Detector {detname} did not fall below {threshold} after"
395
+ f" {limit} moves of {motor.name} with -{step} step "
396
+ f"size. Minimum value was {mincurrent} at {minpos}."
397
+ f"Check if {motor.name} is going the right direction,"
398
+ f" and if {detname} is on."
399
+ )
400
+
401
+ print(
402
+ f"Searching for threshold value {threshold} of {detname} for {motor.name} with starting position {pos} and step {step}"
403
+ )
404
+ maxcurrent = current
405
+ while current < threshold and n < limit:
406
+ yield from mvr(motor, step)
407
+ current = yield from ct()
408
+ if current > maxcurrent:
409
+ maxcurrent = current
410
+ maxpos = motor.position
411
+ n += 1
412
+ if current > threshold:
413
+ return motor.position
414
+ else:
415
+ raise ValueError(
416
+ f"Detector {detname} did not exceed {threshold} after"
417
+ f" {limit} moves of {motor.name} with {step} step "
418
+ f"size. Maximum value was {maxcurrent} at {maxpos}."
419
+ f"Check if {motor.name} is going the right direction,"
420
+ f" and if {detname} is on."
421
+ )
422
+
423
+ return (yield from inner_threshold())
@@ -0,0 +1,179 @@
1
+ import time
2
+ import typing
3
+ from ..help import add_to_plan_list
4
+
5
+ from bluesky_queueserver import parameter_annotation_decorator
6
+
7
+
8
+ @add_to_plan_list
9
+ @parameter_annotation_decorator(
10
+ {
11
+ "parameters": {
12
+ "plans": {
13
+ "annotation": "typing.List[__PLAN__]",
14
+ "description": "List of plan names or callables to run in sequence.",
15
+ }
16
+ }
17
+ }
18
+ )
19
+ def repeat_plan_sequence_for_duration(
20
+ plans,
21
+ plan_args_list: typing.List[typing.List],
22
+ plan_kwargs_list: typing.List[typing.Dict],
23
+ duration: float,
24
+ ):
25
+ """
26
+ Repeat a sequence of plans for a specified duration.
27
+
28
+ Parameters
29
+ ----------
30
+ plans : list of str or callables
31
+ List of plan names (or callables) to run in sequence.
32
+ plan_args_list : list of list
33
+ List of argument lists, one for each plan.
34
+ plan_kwargs_list : list of dict
35
+ List of kwargs dicts, one for each plan.
36
+ duration : float
37
+ Total duration to repeat the sequence (seconds).
38
+
39
+ Yields
40
+ ------
41
+ Msg
42
+ Bluesky messages from the repeated plans.
43
+ """
44
+ start_time = time.time()
45
+ n_plans = len(plans)
46
+ idx = 0
47
+ while (time.time() - start_time) < duration:
48
+ plan = plans[idx % n_plans]
49
+ args = plan_args_list[idx % n_plans]
50
+ kwargs = plan_kwargs_list[idx % n_plans]
51
+ yield from plan(*args, **kwargs)
52
+ idx += 1
53
+
54
+
55
+ @add_to_plan_list
56
+ @parameter_annotation_decorator(
57
+ {
58
+ "parameters": {
59
+ "plans": {
60
+ "annotation": "typing.List[__PLAN__]",
61
+ "description": "List of plan names or callables to run in sequence.",
62
+ },
63
+ "condition": {
64
+ "annotation": "__PLAN__",
65
+ "description": "A plan that returns a boolean; controls loop continuation.",
66
+ },
67
+ }
68
+ }
69
+ )
70
+ def repeat_plan_sequence_while_condition(
71
+ plans,
72
+ plan_args_list: typing.List[typing.List],
73
+ plan_kwargs_list: typing.List[typing.Dict],
74
+ condition,
75
+ condition_args: typing.List = None,
76
+ condition_kwargs: typing.Dict = None,
77
+ ):
78
+ """
79
+ Repeat a sequence of plans while a condition plan returns True.
80
+
81
+ Parameters
82
+ ----------
83
+ plans : list of str or callables
84
+ List of plan names (or callables) to run in sequence.
85
+ plan_args_list : list of list
86
+ List of argument lists, one for each plan.
87
+ plan_kwargs_list : list of dict
88
+ List of kwargs dicts, one for each plan.
89
+ condition : str or callable
90
+ A plan that returns a boolean; controls loop continuation.
91
+ condition_args : list, optional
92
+ Arguments for the condition plan.
93
+ condition_kwargs : dict, optional
94
+ Keyword arguments for the condition plan.
95
+
96
+ Yields
97
+ ------
98
+ Msg
99
+ Bluesky messages from the repeated plans.
100
+ """
101
+ n_plans = len(plans)
102
+ idx = 0
103
+ if condition_args is None:
104
+ condition_args = []
105
+ if condition_kwargs is None:
106
+ condition_kwargs = {}
107
+ while True:
108
+ val = yield from condition(*condition_args, **condition_kwargs)
109
+ if not val:
110
+ break
111
+ plan = plans[idx % n_plans]
112
+ args = plan_args_list[idx % n_plans]
113
+ kwargs = plan_kwargs_list[idx % n_plans]
114
+ yield from plan(*args, **kwargs)
115
+ idx += 1
116
+
117
+
118
+ @add_to_plan_list
119
+ @parameter_annotation_decorator(
120
+ {
121
+ "parameters": {
122
+ "plans": {
123
+ "annotation": "typing.List[__PLAN__]",
124
+ "description": "List of plan names or callables to run in sequence.",
125
+ },
126
+ "condition": {
127
+ "annotation": "__PLAN__",
128
+ "description": "A plan that returns a boolean; controls loop continuation.",
129
+ },
130
+ }
131
+ }
132
+ )
133
+ def repeat_plan_sequence_until_condition(
134
+ plans,
135
+ plan_args_list: typing.List[typing.List],
136
+ plan_kwargs_list: typing.List[typing.Dict],
137
+ condition,
138
+ condition_args: typing.List = None,
139
+ condition_kwargs: typing.Dict = None,
140
+ ):
141
+ """
142
+ Repeat a sequence of plans until a condition plan returns True.
143
+
144
+ Parameters
145
+ ----------
146
+ plans : list of str or callables
147
+ List of plan names (or callables) to run in sequence.
148
+ plan_args_list : list of list
149
+ List of argument lists, one for each plan.
150
+ plan_kwargs_list : list of dict
151
+ List of kwargs dicts, one for each plan.
152
+ condition : str or callable
153
+ A plan that returns a boolean; controls loop continuation.
154
+ condition_args : list, optional
155
+ Arguments for the condition plan.
156
+ condition_kwargs : dict, optional
157
+ Keyword arguments for the condition plan.
158
+
159
+ Yields
160
+ ------
161
+ Msg
162
+ Bluesky messages from the repeated plans.
163
+ """
164
+ n_plans = len(plans)
165
+ idx = 0
166
+ if condition_args is None:
167
+ condition_args = []
168
+ if condition_kwargs is None:
169
+ condition_kwargs = {}
170
+ while True:
171
+ val = yield from condition(*condition_args, **condition_kwargs)
172
+ if val:
173
+ break
174
+ plan = plans[idx % n_plans]
175
+ args = plan_args_list[idx % n_plans]
176
+ kwargs = plan_kwargs_list[idx % n_plans]
177
+ yield from plan(*args, **kwargs)
178
+ idx += 1
179
+