BCDA-APS / use_bluesky

Tools to help APS use the Bluesky Framework (https://blueskyproject.io/)
8 stars 3 forks source link

happi.loader tutorial #70

Closed prjemian closed 2 years ago

prjemian commented 4 years ago

Use [happi.loader]() to define ophyd objects.

prjemian commented 4 years ago

With the synApps docker IOC and prefix sky:, create a happi database (json text file) that describes 16 motors and one scaler.

#!/usr/bin/env python

"""
create_happi_db.py: create the JSON file for happi

==========  ==============================
item        description
==========  ==============================
IOC prefix  sky:
motors      16: sky:m1 .. sky:m16
scaler      1: sky:scaler1
==========  ==============================
"""

# extracted lines

from apstools.utils import *
import json
import re

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)

HAPPI_FILE = "sky_db.json"
IOC_PREFIX = "sky:"
MASTER_PREFIX = "zz_"  # Why "zz_"?  Sorts last, makes happi happy!

def prefix_to_mnemonic(prefix):
    """
    make an IOC prefix safe to use with the happi name attribute

    ioc:  -->  ioc_
    AD42: -->  ad42_
    13ID:Sim1: --> zz_13id_sim1_
    XF:11IDB-BI{Cam:09} --> xf_11idb_bi_cam_09_
    """
    # required regexp from happi.item
    compliant_pattern = r'^[a-z][a-z\_0-9]*'
    replacement = '_'
    safer = prefix.lower()
    match = re.match(compliant_pattern, safer)
    if match is None:   # Fix for IOC prefixes that start with other characters
        prefix = MASTER_PREFIX + prefix
        safer = prefix.lower()
        match = re.match(compliant_pattern, safer)
    last = match.span()[-1]
    while last < len(prefix):
        safer = match.group(0) + replacement + safer[last+1:]
        match = re.match(compliant_pattern, safer)
        last = match.span()[-1]
    return safer

def motors(prefix="ioc:", num=8):
    """
    create ophyd motors provided by iocsky
    """
    db = dict()
    for i in range(num):
        mne = prefix_to_mnemonic(prefix).rstrip("_") + f"_m{i+1}"
        entry = dict(
            documentation = f"{prefix} motor {i+1}",
            prefix = f"{prefix}m{i+1}",
            _id = mne,
            name = mne,
            active = True,
            args = ['{{prefix}}'],
            kwargs = dict(
                name = '{{name}}',
                labels = ["motors",]
            ),
            type = 'OphydItem',
            device_class = "ophyd.EpicsMotor",
        )
        db[mne] = entry
    return db

def scaler(prefix="ioc:", num=1):
    """
    create ophyd scaler ``num`` provided by iocsky
    """
    db = dict()
    mne = prefix_to_mnemonic(prefix).rstrip("_") + f"_scaler{num}"
    entry = dict(
        documentation = f"{prefix} scaler {num}",
        prefix = f"{prefix}scaler{num}",
        _id = mne,
        name = mne,
        active = True,
        args = ['{{prefix}}'],
        kwargs = dict(
            name = '{{name}}',
            labels = ["detectors",]
        ),
        type = 'OphydItem',
        device_class = "ophyd.scaler.ScalerCH",
    )
    db[mne] = entry
    return db

def main():
    ioc = {}
    ioc.update(motors(IOC_PREFIX, 16))
    ioc.update(scaler(IOC_PREFIX, 1))
    print(dictionary_table(ioc))
    with open(HAPPI_FILE, "w") as fp:
        json.dump(ioc, fp, indent=2)

if __name__ == "__main__":
    main()
prjemian commented 4 years ago

read the happi database (json text file) and load the created ophyd objects into the global namespace, then demonstrate them

#!/usr/bin/env python

# extracted lines

from apstools.utils import *
import happi
import happi.loader
import logging

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)

HAPPI_FILE = "sky_db.json"

def get_happi_as_globals(happi_config_file):
    hclient = happi.Client(path=HAPPI_FILE)
    devs = {
        v.name: v 
        for v in [
            happi.loader.from_container(_) 
            for _ in hclient.all_items
            ]
        }
    return devs

devs = get_happi_as_globals(HAPPI_FILE)
print(f"{len(devs)} devices loaded.")
globals().update(devs)

device_read2table(devs["sky_m1"])

sky_scaler1.channels.chan01.s = "clock"
sky_scaler1.channels.chan02.s = "counter"
sky_scaler1.channels.chan03.s = "monitor"
sky_scaler1.select_channels(None)

sky_scaler1.preset_time.put(2.5)
status = sky_scaler1.trigger()
ophyd.wait(status)
sky_scaler1.preset_time.put(1)

device_read2table(sky_scaler1)
prjemian commented 4 years ago

FWIW: So far, this does not demonstrate why such a database is worthwhile since it is a lot more code, just to create the objects. The intrinsic value of this move will become apparent later.

prjemian commented 4 years ago

Ok, I cheated a bit here and used this to load the devices: globals().update(get_happi_as_globals(HAPPI_FILE)) so devs is not defined in the global space.

In [21]: device_read2table(sky_m1)                                                                                                                             
==================== ===== ==========================
name                 value timestamp                 
==================== ===== ==========================
sky_m1               0.0   2020-08-26 15:54:47.675602
sky_m1_user_setpoint 0.0   2020-08-26 15:54:47.675602
==================== ===== ==========================

Out[21]: <pyRestTable.rest_table.Table at 0x7fb2d96fb100>

In [21]: device_read2table(sky_m1)                                                                                                                             
==================== ===== ==========================
name                 value timestamp                 
==================== ===== ==========================
sky_m1               0.0   2020-08-26 15:54:47.675602
sky_m1_user_setpoint 0.0   2020-08-26 15:54:47.675602
==================== ===== ==========================

Out[21]: <pyRestTable.rest_table.Table at 0x7fb2d96fb100>

In [22]: sky_scaler1.channels.chan02.chname.put("clock") 
    ...: sky_scaler1.channels.chan02.chname.put("counter") 
    ...: sky_scaler1.channels.chan03.chname.put("monitor") 
    ...: sky_scaler1.select_channels(None) 
    ...:  
    ...: sky_scaler1.preset_time.put(2.5) 
    ...: status = sky_scaler1.trigger() 
    ...: ophyd.wait(status) 
    ...: sky_scaler1.preset_time.put(1) 
    ...:  
    ...: device_read2table(sky_scaler1)                                                                                                                        
================ ========== ==========================
name             value      timestamp                 
================ ========== ==========================
                 26000000.0 2020-08-26 16:16:29.246043
counter          10.0       2020-08-26 16:16:29.246043
monitor          13.0       2020-08-26 16:16:29.246043
sky_scaler1_time 2.6        2020-08-26 16:16:29.246043
================ ========== ==========================

Out[22]: <pyRestTable.rest_table.Table at 0x7fb2dabc8c10>
prjemian commented 4 years ago

Try to scan scaler1 vs m1: RE(bp.scan([sky_scaler1], sky_m1, -1, 0, 5)) and a famously long error results:

``` In [23]: RE(bp.scan([sky_scaler1], sky_m1, -1, 0, 5)) Transient Scan ID: 3843 Time: 2020-08-26 16:19:37 Persistent Unique Scan ID: '2f39476c-48c0-443d-9d06-5f45fe5c4d33' ERROR:bluesky:Run aborted Traceback (most recent call last): File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1365, in _run msg = self._plan_stack[-1].send(resp) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 1307, in __call__ return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 1160, in baseline_wrapper return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 803, in monitor_during_wrapper return (yield from plan2) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 170, in plan_mutator raise ex File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 123, in plan_mutator msg = plan_stack[-1].send(ret) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 170, in plan_mutator raise ex File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 123, in plan_mutator msg = plan_stack[-1].send(ret) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 861, in fly_during_wrapper return (yield from plan2) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 170, in plan_mutator raise ex File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 123, in plan_mutator msg = plan_stack[-1].send(ret) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 170, in plan_mutator raise ex File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 123, in plan_mutator msg = plan_stack[-1].send(ret) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py", line 1100, in scan return (yield from scan_nd(detectors, full_cycler, File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py", line 995, in scan_nd return (yield from inner_scan_nd()) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py", line 1066, in dec_inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 952, in stage_wrapper return (yield from finalize_wrapper(inner(), unstage_devices())) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 509, in finalize_wrapper ret = yield from plan File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 950, in inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py", line 1066, in dec_inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 325, in run_wrapper yield from contingency_wrapper(plan, File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 575, in contingency_wrapper ret = yield from plan File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py", line 993, in inner_scan_nd yield from per_step(detectors, step, pos_cache) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 1115, in one_nd_step yield from take_reading(list(detectors) + list(motors)) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 913, in trigger_and_read return (yield from rewindable_wrapper(inner_trigger_and_read(), File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 693, in rewindable_wrapper return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 910, in inner_trigger_and_read yield from save() File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 62, in save return (yield Msg('save')) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 195, in plan_mutator inner_ret = yield msg File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 195, in plan_mutator inner_ret = yield msg File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 195, in plan_mutator inner_ret = yield msg [Previous line repeated 1 more time] File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1425, in _run new_response = await coro(msg) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1748, in _save await current_run.save(msg) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/bundlers.py", line 411, in save await self.emit(DocumentNames.descriptor, doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 2272, in emit self.emit_sync(name, doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 2268, in emit_sync schema_validators[name].validate(doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/jsonschema/validators.py", line 353, in validate raise error jsonschema.exceptions.ValidationError: '' does not match any of the regexes: '^([^./]+)$' Failed validating 'additionalProperties' in schema['patternProperties']['^([^./]+)$']: {'additionalProperties': False, 'patternProperties': {'^([^./]+)$': {'$ref': '#/definitions/data_type'}}, 'title': 'data_type'} On instance['data_keys']: {'': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S1', 'units': '', 'upper_ctrl_limit': 0.0}, 'counter': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S2', 'units': '', 'upper_ctrl_limit': 0.0}, 'monitor': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S3', 'units': '', 'upper_ctrl_limit': 0.0}, 'sky_m1': {'dtype': 'number', 'lower_ctrl_limit': -32000.0, 'object_name': 'sky_m1', 'precision': 5, 'shape': [], 'source': 'PV:sky:m1.RBV', 'units': 'degrees', 'upper_ctrl_limit': 32000.0}, 'sky_m1_user_setpoint': {'dtype': 'number', 'lower_ctrl_limit': -32000.0, 'object_name': 'sky_m1', 'precision': 5, 'shape': [], 'source': 'PV:sky:m1.VAL', 'units': 'degrees', 'upper_ctrl_limit': 32000.0}, 'sky_scaler1_time': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 3, 'shape': [], 'source': 'PV:sky:scaler1.T', 'units': '', --------------------------------------------------------------------------- ValidationError Traceback (most recent call last) in ----> 1 RE(bp.scan([sky_scaler1], sky_m1, -1, 0, 5)) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in __call__(self, *args, **metadata_kw) 805 self._task_fut.add_done_callback(set_blocking_event) 806 --> 807 self._resume_task(init_func=_build_task) 808 809 if self._interrupted: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _resume_task(self, init_func) 929 if (exc is not None 930 and not isinstance(exc, _RunEnginePanic)): --> 931 raise exc 932 933 def install_suspender(self, suspender): ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1498 exit_reason = str(err) 1499 self.log.exception("Run aborted") -> 1500 raise err 1501 finally: 1502 if not exit_reason: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1363 else: 1364 try: -> 1365 msg = self._plan_stack[-1].send(resp) 1366 # We have exhausted the top generator 1367 except StopIteration: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in __call__(self, plan) 1305 plan = monitor_during_wrapper(plan, self.monitors) 1306 plan = baseline_wrapper(plan, self.baseline) -> 1307 return (yield from plan) 1308 1309 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in baseline_wrapper(plan, devices, name) 1158 if not devices: 1159 # no-op -> 1160 return (yield from plan) 1161 else: 1162 return (yield from plan_mutator(plan, insert_baseline)) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in monitor_during_wrapper(plan, signals) 801 plan1 = plan_mutator(plan, insert_after_open) 802 plan2 = plan_mutator(plan1, insert_before_close) --> 803 return (yield from plan2) 804 805 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 168 continue 169 else: --> 170 raise ex 171 # if inserting / mutating, put new generator on the stack 172 # and replace the current msg with the first element from the ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 121 ret = result_stack.pop() 122 try: --> 123 msg = plan_stack[-1].send(ret) 124 except StopIteration as e: 125 # discard the exhausted generator ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 168 continue 169 else: --> 170 raise ex 171 # if inserting / mutating, put new generator on the stack 172 # and replace the current msg with the first element from the ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 121 ret = result_stack.pop() 122 try: --> 123 msg = plan_stack[-1].send(ret) 124 except StopIteration as e: 125 # discard the exhausted generator ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in fly_during_wrapper(plan, flyers) 859 plan1 = plan_mutator(plan, insert_after_open) 860 plan2 = plan_mutator(plan1, insert_before_close) --> 861 return (yield from plan2) 862 863 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 168 continue 169 else: --> 170 raise ex 171 # if inserting / mutating, put new generator on the stack 172 # and replace the current msg with the first element from the ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 121 ret = result_stack.pop() 122 try: --> 123 msg = plan_stack[-1].send(ret) 124 except StopIteration as e: 125 # discard the exhausted generator ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 168 continue 169 else: --> 170 raise ex 171 # if inserting / mutating, put new generator on the stack 172 # and replace the current msg with the first element from the ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 121 ret = result_stack.pop() 122 try: --> 123 msg = plan_stack[-1].send(ret) 124 except StopIteration as e: 125 # discard the exhausted generator ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py in scan(detectors, num, per_step, md, *args) 1098 full_cycler = plan_patterns.inner_product(num=num, args=args) 1099 -> 1100 return (yield from scan_nd(detectors, full_cycler, 1101 per_step=per_step, md=_md)) 1102 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py in scan_nd(detectors, cycler, per_step, md) 993 yield from per_step(detectors, step, pos_cache) 994 --> 995 return (yield from inner_scan_nd()) 996 997 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py in dec_inner(*inner_args, **inner_kwargs) 1064 plan = gen_func(*inner_args, **inner_kwargs) 1065 plan = wrapper(plan, *args, **kwargs) -> 1066 return (yield from plan) 1067 return dec_inner 1068 return dec ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in stage_wrapper(plan, devices) 950 return (yield from plan) 951 --> 952 return (yield from finalize_wrapper(inner(), unstage_devices())) 953 954 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in finalize_wrapper(plan, final_plan, pause_for_debug) 507 cleanup = True 508 try: --> 509 ret = yield from plan 510 except GeneratorExit: 511 cleanup = False ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in inner() 948 def inner(): 949 yield from stage_devices() --> 950 return (yield from plan) 951 952 return (yield from finalize_wrapper(inner(), unstage_devices())) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py in dec_inner(*inner_args, **inner_kwargs) 1064 plan = gen_func(*inner_args, **inner_kwargs) 1065 plan = wrapper(plan, *args, **kwargs) -> 1066 return (yield from plan) 1067 return dec_inner 1068 return dec ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in run_wrapper(plan, md) 323 yield from close_run(exit_status='fail', reason=str(e)) 324 --> 325 yield from contingency_wrapper(plan, 326 except_plan=except_plan, 327 else_plan=close_run) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in contingency_wrapper(plan, except_plan, else_plan, final_plan, pause_for_debug) 573 cleanup = True 574 try: --> 575 ret = yield from plan 576 except GeneratorExit: 577 cleanup = False ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py in inner_scan_nd() 991 def inner_scan_nd(): 992 for step in list(cycler): --> 993 yield from per_step(detectors, step, pos_cache) 994 995 return (yield from inner_scan_nd()) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in one_nd_step(detectors, step, pos_cache, take_reading) 1113 motors = step.keys() 1114 yield from move_per_step(step, pos_cache) -> 1115 yield from take_reading(list(detectors) + list(motors)) 1116 1117 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in trigger_and_read(devices, name) 911 return ret 912 from .preprocessors import rewindable_wrapper --> 913 return (yield from rewindable_wrapper(inner_trigger_and_read(), 914 rewindable)) 915 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in rewindable_wrapper(plan, rewindable) 691 restore_rewindable())) 692 else: --> 693 return (yield from plan) 694 695 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in inner_trigger_and_read() 908 if reading is not None: 909 ret.update(reading) --> 910 yield from save() 911 return ret 912 from .preprocessors import rewindable_wrapper ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in save() 60 :func:`bluesky.plan_stubs.create` 61 """ ---> 62 return (yield Msg('save')) 63 64 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 193 try: 194 # yield out the 'current message' and collect the return --> 195 inner_ret = yield msg 196 except GeneratorExit: 197 # special case GeneratorExit. We must clean up all of our plans ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 193 try: 194 # yield out the 'current message' and collect the return --> 195 inner_ret = yield msg 196 except GeneratorExit: 197 # special case GeneratorExit. We must clean up all of our plans ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 193 try: 194 # yield out the 'current message' and collect the return --> 195 inner_ret = yield msg 196 except GeneratorExit: 197 # special case GeneratorExit. We must clean up all of our plans ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in plan_mutator(plan, msg_proc) 193 try: 194 # yield out the 'current message' and collect the return --> 195 inner_ret = yield msg 196 except GeneratorExit: 197 # special case GeneratorExit. We must clean up all of our plans ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1423 # exceptions (coming in via throw) can be 1424 # raised -> 1425 new_response = await coro(msg) 1426 1427 # special case `CancelledError` and let the outer ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _save(self, msg) 1746 ) from ke 1747 -> 1748 await current_run.save(msg) 1749 1750 async def _drop(self, msg): ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/bundlers.py in save(self, msg) 409 object_keys=object_keys, 410 ) --> 411 await self.emit(DocumentNames.descriptor, doc) 412 doc_logger.debug("[descriptor] document is emitted with name %r containing " 413 "data keys %r (run_uid=%r)", name, data_keys.keys(), ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in emit(self, name, doc) 2270 2271 async def emit(self, name, doc): -> 2272 self.emit_sync(name, doc) 2273 2274 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in emit_sync(self, name, doc) 2266 def emit_sync(self, name, doc): 2267 "Process blocking callbacks and schedule non-blocking callbacks." -> 2268 schema_validators[name].validate(doc) 2269 self.dispatcher.process(name, doc) 2270 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/jsonschema/validators.py in validate(self, *args, **kwargs) 351 def validate(self, *args, **kwargs): 352 for error in self.iter_errors(*args, **kwargs): --> 353 raise error 354 355 def is_type(self, instance, type): ValidationError: '' does not match any of the regexes: '^([^./]+)$' Failed validating 'additionalProperties' in schema['patternProperties']['^([^./]+)$']: {'additionalProperties': False, 'patternProperties': {'^([^./]+)$': {'$ref': '#/definitions/data_type'}}, 'title': 'data_type'} On instance['data_keys']: {'': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S1', 'units': '', 'upper_ctrl_limit': 0.0}, 'counter': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S2', 'units': '', 'upper_ctrl_limit': 0.0}, 'monitor': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S3', 'units': '', 'upper_ctrl_limit': 0.0}, 'sky_m1': {'dtype': 'number', 'lower_ctrl_limit': -32000.0, 'object_name': 'sky_m1', 'precision': 5, 'shape': [], 'source': 'PV:sky:m1.RBV', 'units': 'degrees', 'upper_ctrl_limit': 32000.0}, 'sky_m1_user_setpoint': {'dtype': 'number', 'lower_ctrl_limit': -32000.0, 'object_name': 'sky_m1', 'precision': 5, 'shape': [], 'source': 'PV:sky:m1.VAL', 'units': 'degrees', 'upper_ctrl_limit': 32000.0}, 'sky_scaler1_time': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 3, 'shape': [], 'source': 'PV:sky:scaler1.T', 'units': '', 'upper_ctrl_limit': 0.0}} ```
prjemian commented 4 years ago

The scaler has problems, won't ct:

``` In [27]: ct [This data will not be saved. Use the RunEngine to collect data.] ERROR:bluesky:Run aborted Traceback (most recent call last): File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1365, in _run msg = self._plan_stack[-1].send(resp) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py", line 77, in count return (yield from inner_count()) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py", line 1066, in dec_inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 952, in stage_wrapper return (yield from finalize_wrapper(inner(), unstage_devices())) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 509, in finalize_wrapper ret = yield from plan File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 950, in inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py", line 1066, in dec_inner return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 325, in run_wrapper yield from contingency_wrapper(plan, File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 575, in contingency_wrapper ret = yield from plan File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py", line 74, in inner_count return (yield from bps.repeat(partial(per_shot, detectors), File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 1185, in repeat return (yield from repeated_plan()) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 1168, in repeated_plan yield from ensure_generator(plan()) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 1028, in one_shot yield from take_reading(list(detectors)) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 913, in trigger_and_read return (yield from rewindable_wrapper(inner_trigger_and_read(), File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py", line 693, in rewindable_wrapper return (yield from plan) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 910, in inner_trigger_and_read yield from save() File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py", line 62, in save return (yield Msg('save')) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1425, in _run new_response = await coro(msg) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 1748, in _save await current_run.save(msg) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/bundlers.py", line 411, in save await self.emit(DocumentNames.descriptor, doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 2272, in emit self.emit_sync(name, doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py", line 2268, in emit_sync schema_validators[name].validate(doc) File "/home/prjemian/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/jsonschema/validators.py", line 353, in validate raise error jsonschema.exceptions.ValidationError: '' does not match any of the regexes: '^([^./]+)$' Failed validating 'additionalProperties' in schema['patternProperties']['^([^./]+)$']: {'additionalProperties': False, 'patternProperties': {'^([^./]+)$': {'$ref': '#/definitions/data_type'}}, 'title': 'data_type'} On instance['data_keys']: {'': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S1', 'units': '', 'upper_ctrl_limit': 0.0}, 'counter': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S2', 'units': '', 'upper_ctrl_limit': 0.0}, 'sky_scaler1_time': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 3, 'shape': [], 'source': 'PV:sky:scaler1.T', 'units': '', 'upper_ctrl_limit': 0.0}} --------------------------------------------------------------------------- ValidationError Traceback (most recent call last) in ----> 1 get_ipython().run_line_magic('ct', '') ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/IPython/core/interactiveshell.py in run_line_magic(self, magic_name, line, _stack_depth) 2324 kwargs['local_ns'] = sys._getframe(stack_depth).f_locals 2325 with self.builtin_trap: -> 2326 result = fn(*args, **kwargs) 2327 return result 2328 in ct(self, line) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/IPython/core/magic.py in (f, *a, **k) 185 # but it's overkill for just that one bit of state. 186 def magic_deco(arg): --> 187 call = lambda f, *a, **k: f(*a, **k) 188 189 if callable(arg): ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/magics.py in ct(self, line) 160 "Use the RunEngine to collect data.]") 161 try: --> 162 self.RE(plan, _ct_callback) 163 except RunEngineInterrupted: 164 ... ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in __call__(self, *args, **metadata_kw) 805 self._task_fut.add_done_callback(set_blocking_event) 806 --> 807 self._resume_task(init_func=_build_task) 808 809 if self._interrupted: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _resume_task(self, init_func) 929 if (exc is not None 930 and not isinstance(exc, _RunEnginePanic)): --> 931 raise exc 932 933 def install_suspender(self, suspender): ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1498 exit_reason = str(err) 1499 self.log.exception("Run aborted") -> 1500 raise err 1501 finally: 1502 if not exit_reason: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1363 else: 1364 try: -> 1365 msg = self._plan_stack[-1].send(resp) 1366 # We have exhausted the top generator 1367 except StopIteration: ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py in count(detectors, num, delay, per_shot, md) 75 num=num, delay=delay)) 76 ---> 77 return (yield from inner_count()) 78 79 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py in dec_inner(*inner_args, **inner_kwargs) 1064 plan = gen_func(*inner_args, **inner_kwargs) 1065 plan = wrapper(plan, *args, **kwargs) -> 1066 return (yield from plan) 1067 return dec_inner 1068 return dec ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in stage_wrapper(plan, devices) 950 return (yield from plan) 951 --> 952 return (yield from finalize_wrapper(inner(), unstage_devices())) 953 954 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in finalize_wrapper(plan, final_plan, pause_for_debug) 507 cleanup = True 508 try: --> 509 ret = yield from plan 510 except GeneratorExit: 511 cleanup = False ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in inner() 948 def inner(): 949 yield from stage_devices() --> 950 return (yield from plan) 951 952 return (yield from finalize_wrapper(inner(), unstage_devices())) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/utils.py in dec_inner(*inner_args, **inner_kwargs) 1064 plan = gen_func(*inner_args, **inner_kwargs) 1065 plan = wrapper(plan, *args, **kwargs) -> 1066 return (yield from plan) 1067 return dec_inner 1068 return dec ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in run_wrapper(plan, md) 323 yield from close_run(exit_status='fail', reason=str(e)) 324 --> 325 yield from contingency_wrapper(plan, 326 except_plan=except_plan, 327 else_plan=close_run) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in contingency_wrapper(plan, except_plan, else_plan, final_plan, pause_for_debug) 573 cleanup = True 574 try: --> 575 ret = yield from plan 576 except GeneratorExit: 577 cleanup = False ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plans.py in inner_count() 72 @bpp.run_decorator(md=_md) 73 def inner_count(): ---> 74 return (yield from bps.repeat(partial(per_shot, detectors), 75 num=num, delay=delay)) 76 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in repeat(plan, num, delay) 1183 yield Msg('sleep', None, d) 1184 -> 1185 return (yield from repeated_plan()) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in repeated_plan() 1166 now = time.time() # Intercept the flow in its earliest moment. 1167 yield Msg('checkpoint') -> 1168 yield from ensure_generator(plan()) 1169 try: 1170 d = next(delay) ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in one_shot(detectors, take_reading) 1026 """ 1027 yield Msg('checkpoint') -> 1028 yield from take_reading(list(detectors)) 1029 1030 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in trigger_and_read(devices, name) 911 return ret 912 from .preprocessors import rewindable_wrapper --> 913 return (yield from rewindable_wrapper(inner_trigger_and_read(), 914 rewindable)) 915 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/preprocessors.py in rewindable_wrapper(plan, rewindable) 691 restore_rewindable())) 692 else: --> 693 return (yield from plan) 694 695 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in inner_trigger_and_read() 908 if reading is not None: 909 ret.update(reading) --> 910 yield from save() 911 return ret 912 from .preprocessors import rewindable_wrapper ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/plan_stubs.py in save() 60 :func:`bluesky.plan_stubs.create` 61 """ ---> 62 return (yield Msg('save')) 63 64 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _run(self) 1423 # exceptions (coming in via throw) can be 1424 # raised -> 1425 new_response = await coro(msg) 1426 1427 # special case `CancelledError` and let the outer ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in _save(self, msg) 1746 ) from ke 1747 -> 1748 await current_run.save(msg) 1749 1750 async def _drop(self, msg): ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/bundlers.py in save(self, msg) 409 object_keys=object_keys, 410 ) --> 411 await self.emit(DocumentNames.descriptor, doc) 412 doc_logger.debug("[descriptor] document is emitted with name %r containing " 413 "data keys %r (run_uid=%r)", name, data_keys.keys(), ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in emit(self, name, doc) 2270 2271 async def emit(self, name, doc): -> 2272 self.emit_sync(name, doc) 2273 2274 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/bluesky/run_engine.py in emit_sync(self, name, doc) 2266 def emit_sync(self, name, doc): 2267 "Process blocking callbacks and schedule non-blocking callbacks." -> 2268 schema_validators[name].validate(doc) 2269 self.dispatcher.process(name, doc) 2270 ~/Apps/anaconda/envs/bluesky_2020_9/lib/python3.8/site-packages/jsonschema/validators.py in validate(self, *args, **kwargs) 351 def validate(self, *args, **kwargs): 352 for error in self.iter_errors(*args, **kwargs): --> 353 raise error 354 355 def is_type(self, instance, type): ValidationError: '' does not match any of the regexes: '^([^./]+)$' Failed validating 'additionalProperties' in schema['patternProperties']['^([^./]+)$']: {'additionalProperties': False, 'patternProperties': {'^([^./]+)$': {'$ref': '#/definitions/data_type'}}, 'title': 'data_type'} On instance['data_keys']: {'': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S1', 'units': '', 'upper_ctrl_limit': 0.0}, 'counter': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 0, 'shape': [], 'source': 'PV:sky:scaler1.S2', 'units': '', 'upper_ctrl_limit': 0.0}, 'sky_scaler1_time': {'dtype': 'number', 'lower_ctrl_limit': 0.0, 'object_name': 'sky_scaler1', 'precision': 3, 'shape': [], 'source': 'PV:sky:scaler1.T', 'units': '', 'upper_ctrl_limit': 0.0}} In [28]: ```
prjemian commented 4 years ago

As simple as this?

In [28]: sky_scaler1.channels.chan01.chname.get()                                                                                                              
Out[28]: ''
In [29]: sky_scaler1.channels.chan01.chname.put("clock") 

In [30]: ct

nope, same problem

prjemian commented 4 years ago

perhaps: sky_scaler1.channels.chan01.s.name = "clock"?

Yes.

prjemian commented 4 years ago

Now it counts:

In [44]: sky_scaler1.channels.chan01.s.name = "clock"                                                                                                          

In [45]: ct                                                                                                                                                    
[This data will not be saved. Use the RunEngine to collect data.]
clock                          11000000.0
counter                        5.0
sky_scaler1_time               1.1
prjemian commented 4 years ago

and scans:

In [46]: RE(bp.scan([sky_scaler1], sky_m1, -1, 0, 5))                                                                                                          

Transient Scan ID: 3844     Time: 2020-08-26 16:30:39
Persistent Unique Scan ID: 'e4180f3d-4213-4223-93ff-649eb9a870eb'
New stream: 'primary'                                                                                                                                          
+-----------+------------+------------+------------+------------+
|   seq_num |       time |     sky_m1 |      clock |    counter |
+-----------+------------+------------+------------+------------+
|         1 | 16:30:40.9 |   -1.00000 |   11000000 |          5 |
|         2 | 16:30:42.5 |   -0.75000 |   11000000 |          3 |                                                                                              
|         3 | 16:30:44.1 |   -0.50000 |   11000000 |          6 |                                                                                              
|         4 | 16:30:45.7 |   -0.25000 |   11000000 |          5 |                                                                                              
|         5 | 16:30:47.3 |    0.00000 |   11000000 |          5 |                                                                                              
+-----------+------------+------------+------------+------------+
generator scan ['e4180f3d'] (scan num: 3844)
Out[46]: ('e4180f3d-4213-4223-93ff-649eb9a870eb',)

Clipboard01

tacaswell commented 4 years ago

The scalars have either too much or too little magic on the name / chname syncing...

prjemian commented 4 years ago

Updated the example code above.

The revised way is more intuitive but (as you saw) not what I expected. Deep dive showed .s.name is a property. Wahoo! Love those. Learning just how useful is the dir(obj) command for debugging.

prjemian commented 3 years ago

Kicking this down the road. Not enough interest to solve this now.