Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom device tutorial #42

Open
prjemian opened this issue May 16, 2020 · 11 comments
Open

custom device tutorial #42

prjemian opened this issue May 16, 2020 · 11 comments
Assignees
Labels
documentation Improvements or additions to documentation
Milestone

Comments

@prjemian
Copy link
Contributor

per BCDA-APS/use_bluesky#29

@prjemian prjemian self-assigned this May 16, 2020
@prjemian
Copy link
Contributor Author

move to next milestone -- might be able to satisfy this by pointing to existing examples

@prjemian prjemian transferred this issue from BCDA-APS/use_bluesky Nov 5, 2021
@prjemian
Copy link
Contributor Author

prjemian commented Nov 8, 2022

Suppose you have an EPICS database and IOC that creates these PVs: thing:pv1, thing:pv2, thing:pv3 (ignore PVs with fields for now). Notice they all have the same PV prefix thing: so we specify that later, when creating the Python object. We create a Device class, defining Components for each of the PV suffixes as EpicsSignal.

To create ophyd code for Bluesky, this is a starting template:

from ophyd import Component, Device, EpicsSignal

class MyUserThing(Device):
    pv1 = Component(EpicsSignal, "pv1")
    pv2 = Component(EpicsSignal"pv2")
    pv3 = Component(EpicsSignal"pv3")

# create the Python object:
thing = MyUserThing("thing:", name="thing")

This connects PV thing:pv1 with ophyd control thing.pv1, and the other two as well.

That's the general picture. There are details about when to use kind="config" and when to subclass from PvPositioner and the like. But if you keep the interface simple, this is the outline to follow.

Make change as your skills allow.

@prjemian
Copy link
Contributor Author

prjemian commented Nov 8, 2022

One variation might be recognizing that all of the PVs are the same EPICS record type, such as EPICS ao records. Then, these are all floating point PVs which share many extra fields. To add them, make a custom Device for the additional configuration support from apstools. Note that this relocates the signal of thing:pv1 from thing.pv1 to thing.pv1.value (since we have changed the EpicsSignal to a EpicsAoRecord device). Like this:

from apstools.synApps import EpicsRecordDeviceCommonAll
from apstools.synApps import EpicsRecordFloatFields
from ophyd import Component, Device, EpicsSignal

class EpicsAoRecord(EpicsRecordFloatFields, EpicsRecordDeviceCommonAll):
    value = Component(EpicsSignal, ".VAL")

class MyUserThing(Device):
    pv1 = Component(EpicsAoRecord, "pv1")
    pv2 = Component(EpicsAoRecord, "pv2")
    pv3 = Component(EpicsAoRecord, "pv3")

# create the Python object:
thing = MyUserThing("thing:", name="thing")

This gives you many, many additional fields with standard names, such as:

    description = Component(EpicsSignal, ".DESC", kind="config")
    processing_active = Component(EpicsSignalRO, ".PACT", kind="omitted")
    scanning_rate = Component(EpicsSignal, ".SCAN", kind="config")
    disable_value = Component(EpicsSignal, ".DISV", kind="config")
    scan_disable_input_link_value = Component(EpicsSignal, ".DISA", kind="config")
    scan_disable_value_input_link = Component(EpicsSignal, ".SDIS", kind="config")
    process_record = Component(EpicsSignal, ".PROC", kind="omitted", put_complete=True)
    forward_link = Component(EpicsSignal, ".FLNK", kind="config")
    trace_processing = Component(EpicsSignal, ".TPRO", kind="omitted")
    device_type = Component(EpicsSignalRO, ".DTYP", kind="config")


    alarm_status = Component(EpicsSignalRO, ".STAT", kind="config")
    alarm_severity = Component(EpicsSignalRO, ".SEVR", kind="config")
    new_alarm_status = Component(EpicsSignalRO, ".NSTA", kind="config")
    new_alarm_severity = Component(EpicsSignalRO, ".NSEV", kind="config")
    disable_alarm_severity = Component(EpicsSignal, ".DISS", kind="config")

    units = Component(EpicsSignal, ".EGU", kind="config")
    precision = Component(EpicsSignal, ".PREC", kind="config")

    monitor_deadband = Component(EpicsSignal, ".MDEL", kind="config")

@prjemian
Copy link
Contributor Author

A realistic demonstration might be a heater controller simulation, such as epics-modules/optics#10 (comment) (which needs some updates).

prjemian added a commit that referenced this issue Apr 19, 2023
prjemian added a commit that referenced this issue Apr 19, 2023
prjemian added a commit that referenced this issue Apr 29, 2023
prjemian added a commit that referenced this issue Apr 29, 2023
prjemian added a commit that referenced this issue Apr 29, 2023
@prjemian
Copy link
Contributor Author

prjemian commented May 1, 2023

Could start with more basic examples. Make note of common prefix.

Reasons for a custom Device

  • groupings (such as: related metadata or a motor stage)
  • custom configuration (such as area detector)
  • modify existing Device
  • new support
  • pseudo-positioner

Here are examples from 2019 slides:

Groupings - Neat Stage 3IDD

2019 Neat stage 3ID

class NeatStage_3IDD(Device):
    x = Component(EpicsMotor, "m1", labels=("NEAT stage",))
    y = Component(EpicsMotor, "m2", labels=("NEAT stage",))
    theta = Component(EpicsMotor, "m3", labels=("NEAT stage",))

neat_stage = NeatStage_3IDD("3idd:", name="neat_stage")

APS undulator support uses this pattern.

class ApsUndulator(Device):
    """
    APS Undulator

    EXAMPLE::

        undulator = ApsUndulator("ID09ds:", name="undulator")
    """

    energy = Component(
        EpicsSignal, "Energy", write_pv="EnergySet", put_complete=True, kind="hinted",
    )
    energy_taper = Component(
        EpicsSignal, "TaperEnergy", write_pv="TaperEnergySet", kind="config",
    )
    gap = Component(EpicsSignal, "Gap", write_pv="GapSet")
    gap_taper = Component(
        EpicsSignal, "TaperGap", write_pv="TaperGapSet", kind="config"
    )
    start_button = Component(EpicsSignal, "Start", put_complete=True, kind="omitted")
    stop_button = Component(EpicsSignal, "Stop", kind="omitted")
    harmonic_value = Component(EpicsSignal, "HarmonicValue", kind="config")
    gap_deadband = Component(EpicsSignal, "DeadbandGap", kind="config")
    device_limit = Component(EpicsSignal, "DeviceLimit", kind="config")
    # ... more

Devices can be nested. For example, the dual undulator is a Device that contains an upstream (us:) and a downstream (ds:) undulator.

class ApsUndulatorDual(Device):
    upstream = Component(ApsUndulator, "us:")
    downstream = Component(ApsUndulator, "ds:")

Aggregate custom data - User Info

class ExperimentInfo(Device):		# from the APS General User Proposal system
    GUP_number = Component(EpicsSignalRO, "ProposalNumber", string=True)
    title = Component(EpicsSignalRO, "ProposalTitle", string=True)
    user_name = Component(EpicsSignalRO, "UserName", string=True)
    user_institution = Component(EpicsSignalRO, "UserInstitution", string=True)
    user_badge_number = Component(EpicsSignalRO, "UserBadge", string=True)

user_info = ExperimentInfo("2bmS1:", name="user_info")

Modify a standard device

Sometimes, a standard device is missing a feature, such as connection with an additional field in an EPICS record. For example, the ophyd.EpicsMotor does not connect with every field of the EPICS motor record.

class MyEpicsMotor(EpicsMotor):
    steps_per_revolution = Component(EpicsSignal, ".SREV", kind="omitted")

Also see

  • CamMixin - updates a text attribute
  • SingleTrigger - overrides existing methods (__init__(), stage(), unstage())

Mixin

See the EpicsAoRecord above.

@prjemian
Copy link
Contributor Author

prjemian commented May 1, 2023

Start with the Hello, World! example, which is pure ophyd (does not involve EPICS).

@prjemian
Copy link
Contributor Author

prjemian commented May 1, 2023

Connect EPICS contains another grouping example, which also describes the wait_for_connection() method.. This is a good second example.

@prjemian
Copy link
Contributor Author

prjemian commented May 1, 2023

Form follows function: Integral to the implementation of a custom ophyd.Device is the consideration of its architecture: how the control is provided and how it will be used.

prjemian added a commit that referenced this issue May 1, 2023
@prjemian
Copy link
Contributor Author

This is really a howto. Moving to that section.

prjemian added a commit that referenced this issue May 25, 2023
prjemian added a commit that referenced this issue May 25, 2023
@prjemian
Copy link
Contributor Author

The document has more depth than a HowTo. Moving back to tutorials.

prjemian added a commit that referenced this issue May 26, 2023
prjemian added a commit that referenced this issue May 26, 2023
prjemian added a commit that referenced this issue May 26, 2023
prjemian added a commit that referenced this issue May 26, 2023
prjemian added a commit that referenced this issue May 26, 2023
@prjemian prjemian added this to the v1.0.1 milestone Jun 2, 2023
@prjemian prjemian added this to To do in v1.0.1 project via automation Jun 2, 2023
@prjemian prjemian added the documentation Improvements or additions to documentation label Jun 2, 2023
@prjemian prjemian modified the milestones: v1.0.1, v1.0.2 Aug 21, 2023
@prjemian prjemian removed this from To do in v1.0.1 project Aug 21, 2023
@prjemian prjemian added this to To do in bluesky_training v1.0.2 project via automation Aug 21, 2023
@prjemian prjemian modified the milestones: v1.0.2, v1.0.3 Feb 24, 2024
@prjemian prjemian modified the milestones: v1.0.3, v1.0.4 Mar 29, 2024
@prjemian
Copy link
Contributor Author

Might be good to start with a FAQ.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant